use crate::clawhub::service::{ClawHubGateway, RegistryClawHubGateway};
use crate::config::Config;
use crate::internal::clawhub::install::InstallOptions;
use crate::llm_types::ToolDefinition;
use crate::tools::{schema_object, Tool, ToolResult};
use async_trait::async_trait;
use std::path::PathBuf;
use std::sync::Arc;
pub struct ClawHubSearchTool {
gateway: Arc<dyn ClawHubGateway>,
}
pub struct ClawHubInstallTool {
gateway: Arc<dyn ClawHubGateway>,
skills_dir: PathBuf,
lockfile_path: PathBuf,
skip_security: bool,
}
impl ClawHubSearchTool {
pub fn new(config: &Config) -> Self {
let gateway: Arc<dyn ClawHubGateway> =
Arc::new(RegistryClawHubGateway::from_config(config));
Self { gateway }
}
}
#[async_trait]
impl Tool for ClawHubSearchTool {
fn name(&self) -> &str {
"clawhub_search"
}
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "clawhub_search".to_string(),
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(),
input_schema: schema_object(
serde_json::json!({
"query": {
"type": "string",
"description": "Natural language search query"
},
"limit": {
"type": "integer",
"description": "Maximum number of results (default 10, max 50)"
},
"sort": {
"type": "string",
"description": "Sort order: trending, installs, latest"
}
}),
&["query"],
),
}
}
async fn execute(&self, params: serde_json::Value) -> ToolResult {
let query = match params.get("query").and_then(|v| v.as_str()) {
Some(q) => q,
None => return ToolResult::error("Missing required parameter: query".into()),
};
let limit = params.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let sort = params
.get("sort")
.and_then(|v| v.as_str())
.unwrap_or("trending");
let gateway = self.gateway.clone();
let results = gateway.search(query, limit.min(50), sort).await;
match results {
Ok(results) => {
let output = results
.iter()
.map(|r| {
let vt_info = r
.virustotal
.as_ref()
.map(|v| {
format!(" | VirusTotal: {} ({} reports)", v.status, v.report_count)
})
.unwrap_or_default();
format!(
"• {} — {} ({} installs){}",
r.slug, r.description, r.install_count, vt_info
)
})
.collect::<Vec<_>>()
.join("\n");
ToolResult::success(output)
}
Err(e) => ToolResult::error(e.to_string()),
}
}
}
impl ClawHubInstallTool {
pub fn new(config: &Config) -> Self {
let gateway: Arc<dyn ClawHubGateway> =
Arc::new(RegistryClawHubGateway::from_config(config));
let skills_dir = PathBuf::from(config.skills_data_dir());
let lockfile_path = config.clawhub_lockfile_path();
Self {
gateway,
skills_dir,
lockfile_path,
skip_security: config.clawhub.skip_security_warnings,
}
}
}
#[async_trait]
impl Tool for ClawHubInstallTool {
fn name(&self) -> &str {
"clawhub_install"
}
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "clawhub_install".to_string(),
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."
.to_string(),
input_schema: schema_object(
serde_json::json!({
"slug": {
"type": "string",
"description": "The ClawHub skill slug to install"
},
"version": {
"type": "string",
"description": "Specific version to install (default: latest)"
},
"force": {
"type": "boolean",
"description": "Overwrite if already installed"
}
}),
&["slug"],
),
}
}
async fn execute(&self, params: serde_json::Value) -> ToolResult {
let slug = match params.get("slug").and_then(|v| v.as_str()) {
Some(s) => s,
None => return ToolResult::error("Missing required parameter: slug".into()),
};
let version = params.get("version").and_then(|v| v.as_str());
let force = params
.get("force")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let gateway = self.gateway.clone();
let options = InstallOptions {
force,
skip_gates: false,
skip_security: self.skip_security,
};
let mut last_error = None;
for attempt in 1..=3 {
match gateway
.install(
slug,
version,
&self.skills_dir,
&self.lockfile_path,
&options,
)
.await
{
Ok(result) => {
let mut msg = result.message;
for w in &result.warnings {
msg.push_str(&format!("\n⚠ {}", w));
}
if !result.success {
return ToolResult::error(msg);
}
if result.requires_restart {
msg.push_str("\nRestart MicroClaw or run /reload-skills to activate.");
}
return ToolResult::success(msg);
}
Err(e) => {
last_error = Some(e);
if attempt < 3 {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
}
}
}
match last_error {
Some(e) => ToolResult::error(e.to_string()),
None => ToolResult::error("Unexpected error during installation".into()),
}
}
}