microclaw_engine/clawhub/
tools.rs1use crate::clawhub::service::{ClawHubGateway, RegistryClawHubGateway};
2use crate::config::Config;
3use crate::internal::clawhub::install::InstallOptions;
4use crate::llm_types::ToolDefinition;
5use crate::tools::{schema_object, Tool, ToolResult};
6use async_trait::async_trait;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10pub struct ClawHubSearchTool {
11 gateway: Arc<dyn ClawHubGateway>,
12}
13
14pub struct ClawHubInstallTool {
15 gateway: Arc<dyn ClawHubGateway>,
16 skills_dir: PathBuf,
17 lockfile_path: PathBuf,
18 skip_security: bool,
19}
20
21impl ClawHubSearchTool {
22 pub fn new(config: &Config) -> Self {
23 let gateway: Arc<dyn ClawHubGateway> =
24 Arc::new(RegistryClawHubGateway::from_config(config));
25 Self { gateway }
26 }
27}
28
29#[async_trait]
30impl Tool for ClawHubSearchTool {
31 fn name(&self) -> &str {
32 "clawhub_search"
33 }
34
35 fn definition(&self) -> ToolDefinition {
36 ToolDefinition {
37 name: "clawhub_search".to_string(),
38 description: "Search ClawHub registry for available skills. Use this instead of running clawdhub CLI commands - this is the built-in way to discover skills.".to_string(),
39 input_schema: schema_object(
40 serde_json::json!({
41 "query": {
42 "type": "string",
43 "description": "Natural language search query"
44 },
45 "limit": {
46 "type": "integer",
47 "description": "Maximum number of results (default 10, max 50)"
48 },
49 "sort": {
50 "type": "string",
51 "description": "Sort order: trending, installs, latest"
52 }
53 }),
54 &["query"],
55 ),
56 }
57 }
58
59 async fn execute(&self, params: serde_json::Value) -> ToolResult {
60 let query = match params.get("query").and_then(|v| v.as_str()) {
61 Some(q) => q,
62 None => return ToolResult::error("Missing required parameter: query".into()),
63 };
64 let limit = params.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
65 let sort = params
66 .get("sort")
67 .and_then(|v| v.as_str())
68 .unwrap_or("trending");
69
70 let gateway = self.gateway.clone();
71 let results = gateway.search(query, limit.min(50), sort).await;
72
73 match results {
74 Ok(results) => {
75 let output = results
76 .iter()
77 .map(|r| {
78 let vt_info = r
79 .virustotal
80 .as_ref()
81 .map(|v| {
82 format!(" | VirusTotal: {} ({} reports)", v.status, v.report_count)
83 })
84 .unwrap_or_default();
85 format!(
86 "• {} — {} ({} installs){}",
87 r.slug, r.description, r.install_count, vt_info
88 )
89 })
90 .collect::<Vec<_>>()
91 .join("\n");
92 ToolResult::success(output)
93 }
94 Err(e) => ToolResult::error(e.to_string()),
95 }
96 }
97}
98
99impl ClawHubInstallTool {
100 pub fn new(config: &Config) -> Self {
101 let gateway: Arc<dyn ClawHubGateway> =
102 Arc::new(RegistryClawHubGateway::from_config(config));
103 let skills_dir = PathBuf::from(config.skills_data_dir());
104 let lockfile_path = config.clawhub_lockfile_path();
105 Self {
106 gateway,
107 skills_dir,
108 lockfile_path,
109 skip_security: config.clawhub.skip_security_warnings,
110 }
111 }
112}
113
114#[async_trait]
115impl Tool for ClawHubInstallTool {
116 fn name(&self) -> &str {
117 "clawhub_install"
118 }
119
120 fn definition(&self) -> ToolDefinition {
121 ToolDefinition {
122 name: "clawhub_install".to_string(),
123 description: "Install a skill from ClawHub into ~/.microclaw/skills/ (or configured skills dir). Use this instead of running clawdhub CLI commands - this is the built-in way to install ClawHub skills."
124 .to_string(),
125 input_schema: schema_object(
126 serde_json::json!({
127 "slug": {
128 "type": "string",
129 "description": "The ClawHub skill slug to install"
130 },
131 "version": {
132 "type": "string",
133 "description": "Specific version to install (default: latest)"
134 },
135 "force": {
136 "type": "boolean",
137 "description": "Overwrite if already installed"
138 }
139 }),
140 &["slug"],
141 ),
142 }
143 }
144
145 async fn execute(&self, params: serde_json::Value) -> ToolResult {
146 let slug = match params.get("slug").and_then(|v| v.as_str()) {
147 Some(s) => s,
148 None => return ToolResult::error("Missing required parameter: slug".into()),
149 };
150 let version = params.get("version").and_then(|v| v.as_str());
151 let force = params
152 .get("force")
153 .and_then(|v| v.as_bool())
154 .unwrap_or(false);
155
156 let gateway = self.gateway.clone();
157 let options = InstallOptions {
158 force,
159 skip_gates: false,
160 skip_security: self.skip_security,
161 };
162
163 let mut last_error = None;
165 for attempt in 1..=3 {
166 match gateway
167 .install(
168 slug,
169 version,
170 &self.skills_dir,
171 &self.lockfile_path,
172 &options,
173 )
174 .await
175 {
176 Ok(result) => {
177 let mut msg = result.message;
178 for w in &result.warnings {
179 msg.push_str(&format!("\n⚠ {}", w));
180 }
181 if !result.success {
182 return ToolResult::error(msg);
183 }
184 if result.requires_restart {
185 msg.push_str("\nRestart MicroClaw or run /reload-skills to activate.");
186 }
187 return ToolResult::success(msg);
188 }
189 Err(e) => {
190 last_error = Some(e);
191 if attempt < 3 {
192 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
193 }
194 }
195 }
196 }
197
198 match last_error {
200 Some(e) => ToolResult::error(e.to_string()),
201 None => ToolResult::error("Unexpected error during installation".into()),
202 }
203 }
204}