microclaw_engine/internal/clawhub/
client.rs1use crate::internal::clawhub::types::*;
2use microclaw_core::error::MicroClawError;
3
4pub struct ClawHubClient {
5 base_url: String,
6 token: Option<String>,
7 client: reqwest::Client,
8}
9
10impl ClawHubClient {
11 pub fn new(base_url: &str, token: Option<String>) -> Self {
12 Self {
13 base_url: base_url.to_string(),
14 token,
15 client: reqwest::Client::new(),
16 }
17 }
18
19 pub async fn search(
21 &self,
22 query: &str,
23 limit: usize,
24 _sort: &str,
25 ) -> Result<Vec<SearchResult>, MicroClawError> {
26 let url = format!(
28 "{}/api/v1/search?q={}&limit={}",
29 self.base_url, query, limit
30 );
31 let mut req = self.client.get(&url);
32 if let Some(ref token) = self.token {
33 req = req.header("Authorization", format!("Bearer {}", token));
34 }
35 let resp = req
36 .send()
37 .await
38 .map_err(|e| MicroClawError::Config(format!("ClawHub request failed: {}", e)))?;
39 let search_response: ApiSearchResponse = resp.json().await.map_err(|e| {
40 MicroClawError::Config(format!("Failed to parse search results: {}", e))
41 })?;
42 Ok(search_response
44 .results
45 .into_iter()
46 .take(limit)
47 .map(SearchResult::from)
48 .collect())
49 }
50
51 pub async fn get_skill(&self, slug: &str) -> Result<SkillMeta, MicroClawError> {
53 let url = format!("{}/api/v1/skills/{}", self.base_url, slug);
54 let mut req = self.client.get(&url);
55 if let Some(ref token) = self.token {
56 req = req.header("Authorization", format!("Bearer {}", token));
57 }
58 let resp = req
59 .send()
60 .await
61 .map_err(|e| MicroClawError::Config(format!("ClawHub request failed: {}", e)))?;
62 let get_response: GetSkillResponse = resp.json().await.map_err(|e| {
63 MicroClawError::Config(format!("Failed to parse skill metadata: {}", e))
64 })?;
65 Ok(SkillMeta::from(get_response))
67 }
68
69 pub async fn download_skill(
71 &self,
72 slug: &str,
73 version: &str,
74 ) -> Result<Vec<u8>, MicroClawError> {
75 let mut candidate_urls = vec![
77 format!(
78 "{}/api/v1/download?slug={}&version={}",
79 self.base_url, slug, version
80 ),
81 format!(
82 "{}/api/v1/skills/{}/download?version={}",
83 self.base_url, slug, version
84 ),
85 ];
86
87 if self.base_url.contains("clawhub.ai") {
89 candidate_urls.push(format!(
90 "https://wry-manatee-359.convex.site/api/v1/download?slug={}&version={}",
91 slug, version
92 ));
93 }
94
95 let mut last_error: Option<MicroClawError> = None;
96 for url in candidate_urls {
97 let mut req = self.client.get(&url);
98 if let Some(ref token) = self.token {
99 req = req.header("Authorization", format!("Bearer {}", token));
100 }
101
102 let resp = match req.send().await {
103 Ok(resp) => resp,
104 Err(e) => {
105 last_error = Some(MicroClawError::Config(format!(
106 "ClawHub download failed at {}: {}",
107 url, e
108 )));
109 continue;
110 }
111 };
112
113 let resp = match resp.error_for_status() {
114 Ok(resp) => resp,
115 Err(e) => {
116 last_error = Some(MicroClawError::Config(format!(
117 "ClawHub download HTTP error at {}: {}",
118 url, e
119 )));
120 continue;
121 }
122 };
123
124 let bytes = resp.bytes().await.map_err(|e| {
125 MicroClawError::Config(format!("Failed to read download from {}: {}", url, e))
126 })?;
127 return Ok(bytes.to_vec());
128 }
129
130 Err(last_error.unwrap_or_else(|| {
131 MicroClawError::Config("ClawHub download failed: no usable endpoint".into())
132 }))
133 }
134
135 pub async fn get_versions(&self, slug: &str) -> Result<Vec<SkillVersion>, MicroClawError> {
137 let url = format!("{}/api/v1/skills/{}/versions", self.base_url, slug);
138 let mut req = self.client.get(&url);
139 if let Some(ref token) = self.token {
140 req = req.header("Authorization", format!("Bearer {}", token));
141 }
142 let resp = req
143 .send()
144 .await
145 .map_err(|e| MicroClawError::Config(format!("ClawHub request failed: {}", e)))?;
146 let versions: Vec<SkillVersion> = resp
147 .json()
148 .await
149 .map_err(|e| MicroClawError::Config(format!("Failed to parse versions: {}", e)))?;
150 Ok(versions)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_client_construction() {
160 let client = ClawHubClient::new("https://clawhub.ai", None);
161 assert_eq!(client.base_url, "https://clawhub.ai");
162 assert!(client.token.is_none());
163 }
164
165 #[test]
166 fn test_client_with_token() {
167 let client = ClawHubClient::new("https://clawhub.ai", Some("test-token".into()));
168 assert!(client.token.is_some());
169 }
170}