Skip to main content

microclaw_engine/internal/clawhub/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// =============================================================================
5// API Response Types - These match the actual ClawHub API responses
6// =============================================================================
7
8/// Dedicated search API response (the one that actually filters by query)
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ApiSearchResponse {
11    pub results: Vec<ApiSearchResult>,
12}
13
14/// Search result from dedicated search API
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ApiSearchResult {
17    pub score: f64,
18    pub slug: String,
19    #[serde(rename = "displayName")]
20    pub display_name: String,
21    pub summary: String,
22    #[serde(default)]
23    pub version: Option<String>,
24    #[serde(default, rename = "updatedAt")]
25    pub updated_at: i64,
26}
27
28impl From<ApiSearchResult> for SearchResult {
29    fn from(item: ApiSearchResult) -> Self {
30        Self {
31            slug: item.slug,
32            name: item.display_name,
33            description: item.summary,
34            install_count: 0, // Not available in search response
35            virustotal: None,
36        }
37    }
38}
39
40/// Search API response wrapper (list endpoint, doesn't filter by query)
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SearchResponse {
43    pub items: Vec<SearchItem>,
44    #[serde(rename = "nextCursor")]
45    pub next_cursor: Option<String>,
46}
47
48/// Search result item from API
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SearchItem {
51    pub slug: String,
52    #[serde(rename = "displayName")]
53    pub display_name: String,
54    pub summary: String,
55    pub tags: HashMap<String, String>,
56    pub stats: SkillStats,
57    #[serde(rename = "createdAt")]
58    pub created_at: i64,
59    #[serde(rename = "updatedAt")]
60    pub updated_at: i64,
61    #[serde(rename = "latestVersion")]
62    pub latest_version: Option<VersionItem>,
63}
64
65/// Skill statistics
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SkillStats {
68    #[serde(default)]
69    pub comments: i32,
70    #[serde(default)]
71    pub downloads: i32,
72    #[serde(default, rename = "installsAllTime", alias = "installs")]
73    pub installs_all_time: i32,
74    #[serde(default, rename = "installsCurrent")]
75    pub installs_current: i32,
76    #[serde(default)]
77    pub stars: i32,
78    #[serde(default)]
79    pub versions: i32,
80}
81
82/// Version item from API
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct VersionItem {
85    pub version: String,
86    #[serde(rename = "createdAt")]
87    pub created_at: i64,
88    pub changelog: String,
89}
90
91/// Get skill API response wrapper
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct GetSkillResponse {
94    pub skill: SkillItem,
95    #[serde(rename = "latestVersion")]
96    pub latest_version: VersionItem,
97    pub owner: Owner,
98    pub moderation: Option<serde_json::Value>,
99}
100
101/// Skill item from get skill API
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SkillItem {
104    pub slug: String,
105    #[serde(rename = "displayName")]
106    pub display_name: String,
107    pub summary: String,
108    pub tags: HashMap<String, String>,
109    pub stats: SkillStats,
110    #[serde(rename = "createdAt")]
111    pub created_at: i64,
112    #[serde(rename = "updatedAt")]
113    pub updated_at: i64,
114}
115
116/// Owner information
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Owner {
119    pub handle: String,
120    #[serde(rename = "userId")]
121    pub user_id: String,
122    #[serde(rename = "displayName")]
123    pub display_name: String,
124    pub image: String,
125}
126
127// =============================================================================
128// Internal Types - These are what the rest of the app uses
129// =============================================================================
130
131/// Lockfile format for tracking ClawHub-installed skills
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct LockFile {
134    pub version: u32,
135    pub skills: HashMap<String, LockEntry>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct LockEntry {
140    pub slug: String,
141    #[serde(rename = "installedVersion")]
142    pub installed_version: String,
143    #[serde(rename = "installedAt")]
144    pub installed_at: String,
145    #[serde(rename = "contentHash")]
146    pub content_hash: String,
147    /// sha256 over the EXTRACTED tree (sorted rel-paths + per-file hashes).
148    /// Verifiable against disk at any time, unlike contentHash which covers
149    /// the downloaded archive bytes. Absent on entries installed before this
150    /// field existed.
151    #[serde(rename = "treeHash", default, skip_serializing_if = "Option::is_none")]
152    pub tree_hash: Option<String>,
153    #[serde(rename = "localPath")]
154    pub local_path: String,
155}
156
157/// Skill metadata from ClawHub API
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct SkillMeta {
160    pub slug: String,
161    pub name: String,
162    pub description: String,
163    pub versions: Vec<SkillVersion>,
164    #[serde(default)]
165    pub virustotal: Option<VirusTotal>,
166    #[serde(default)]
167    pub metadata: SkillMetadata,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct SkillVersion {
172    pub version: String,
173    #[serde(default)]
174    pub latest: bool,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct VirusTotal {
179    #[serde(rename = "reportCount")]
180    pub report_count: i32,
181    #[serde(rename = "pendingScan")]
182    pub pending_scan: bool,
183    pub status: String,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187pub struct SkillMetadata {
188    #[serde(default)]
189    pub openclaw: Option<OpenClawMeta>,
190    #[serde(default)]
191    pub clawdbot: Option<ClawdbotMeta>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, Default)]
195pub struct OpenClawMeta {
196    #[serde(default)]
197    pub requires: Option<Requires>,
198    #[serde(default)]
199    pub os: Vec<String>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, Default)]
203pub struct ClawdbotMeta {
204    #[serde(default)]
205    pub requires: Option<Requires>,
206    #[serde(default)]
207    pub os: Vec<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, Default)]
211pub struct Requires {
212    #[serde(default)]
213    pub bins: Vec<String>,
214    #[serde(default)]
215    pub env: Vec<String>,
216    #[serde(default, rename = "anyBins")]
217    pub any_bins: Vec<String>,
218}
219
220/// Search result item (internal representation)
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct SearchResult {
223    pub slug: String,
224    pub name: String,
225    pub description: String,
226    #[serde(rename = "installCount")]
227    pub install_count: i32,
228    #[serde(default)]
229    pub virustotal: Option<VirusTotal>,
230}
231
232impl From<SearchItem> for SearchResult {
233    fn from(item: SearchItem) -> Self {
234        Self {
235            slug: item.slug,
236            name: item.display_name,
237            description: item.summary,
238            install_count: item.stats.installs_current,
239            virustotal: None, // Not available in search response
240        }
241    }
242}
243
244impl From<GetSkillResponse> for SkillMeta {
245    fn from(resp: GetSkillResponse) -> Self {
246        Self {
247            slug: resp.skill.slug,
248            name: resp.skill.display_name,
249            description: resp.skill.summary,
250            versions: vec![SkillVersion {
251                version: resp.latest_version.version,
252                latest: true,
253            }],
254            virustotal: None, // Not available in this response
255            metadata: SkillMetadata::default(),
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_lockfile_serde() {
266        let lock = LockFile {
267            version: 1,
268            skills: std::collections::HashMap::new(),
269        };
270        let json = serde_json::to_string(&lock).unwrap();
271        assert!(json.contains(r#""version":1"#));
272    }
273
274    #[test]
275    fn test_skill_meta_deserialize() {
276        let json = r#"{
277            "slug": "test-skill",
278            "name": "Test Skill",
279            "description": "A test skill",
280            "versions": [{"version": "1.0.0", "latest": true}]
281        }"#;
282        let _meta: SkillMeta = serde_json::from_str(json).unwrap();
283    }
284}