rsclaw 0.0.1-alpha.1

rsclaw: High-performance AI agent (BETA). Optimized for M4 Max and 2GB VPS. 100% compatible with openclaw
Documentation
use super::manifest::SkillManifest;
use anyhow::{Context, Result};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::Arc;

/// ClawHub API client for fetching skills.
pub struct ClawHubClient {
    base_url: Arc<str>,
    client: Client,
}

/// Skill response from ClawHub API.
#[derive(Debug, Serialize, Deserialize)]
pub struct SkillResponse {
    pub name: String,
    pub version: String,
    pub description: String,
    pub code: String,
    pub manifest: Option<serde_json::Value>,
}

impl ClawHubClient {
    /// Create a new ClawHub client.
    pub fn new(base_url: Option<Arc<str>>) -> Self {
        Self {
            base_url: base_url.unwrap_or_else(|| Arc::from("https://api.clawhub.ai")),
            client: Client::new(),
        }
    }

    /// Fetch a skill from ClawHub.
    pub async fn fetch_skill(&self, skill_name: &str) -> Result<SkillResponse> {
        let url = format!("{}/skills/{}", self.base_url, skill_name);

        let response = self
            .client
            .get(&url)
            .header("Accept", "application/json")
            .send()
            .await
            .context("Failed to fetch skill from ClawHub")?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("ClawHub API error ({}): {}", status, body);
        }

        let skill_response: SkillResponse = response
            .json()
            .await
            .context("Failed to parse ClawHub response")?;

        Ok(skill_response)
    }

    /// Install a skill to the skills directory.
    pub async fn install_skill(
        &self,
        skill_name: &str,
        skills_dir: &Path,
    ) -> Result<()> {
        let skill_response = self.fetch_skill(skill_name).await?;

        if !skills_dir.exists() {
            fs::create_dir_all(skills_dir)
                .context("Failed to create skills directory")?;
        }

        let skill_path = skills_dir.join(skill_name);
        fs::write(&skill_path, &skill_response.code)
            .context("Failed to write skill file")?;

        let perms = fs::Permissions::from_mode(0o755);
        fs::set_permissions(&skill_path, perms)
            .context("Failed to set skill permissions")?;

        let manifest_path = skill_path.with_extension("manifest.json");
        if let Some(manifest_value) = skill_response.manifest {
            let manifest_json = serde_json::to_string_pretty(&manifest_value)
                .context("Failed to serialize manifest")?;
            fs::write(&manifest_path, manifest_json)
                .context("Failed to write manifest file")?;
        } else {
            let manifest = SkillManifest::new(
                Arc::from(skill_response.name.as_str()),
                Arc::from(skill_response.version.as_str()),
                Arc::from(skill_response.description.as_str()),
                Arc::from(skill_name),
            );
            let manifest_json = serde_json::to_string_pretty(&manifest)
                .context("Failed to serialize manifest")?;
            fs::write(&manifest_path, manifest_json)
                .context("Failed to write manifest file")?;
        }

        tracing::info!("Skill '{}' installed successfully", skill_name);
        Ok(())
    }
}

impl Default for ClawHubClient {
    fn default() -> Self {
        Self::new(None)
    }
}