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;
pub struct ClawHubClient {
base_url: Arc<str>,
client: Client,
}
#[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 {
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(),
}
}
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)
}
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)
}
}