microclaw-engine 0.10.1

Channel-independent Agent Engine and default services for MicroClaw
Documentation
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,
        };

        // Retry up to 3 times with brief delays for transient failures
        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;
                    }
                }
            }
        }

        // All retries failed - return the last error
        match last_error {
            Some(e) => ToolResult::error(e.to_string()),
            None => ToolResult::error("Unexpected error during installation".into()),
        }
    }
}