//! Skill registry //! //! Manage loaded skills and their execution. use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; use zclaw_types::{Result, SkillId}; use super::{Skill, SkillContext, SkillManifest, SkillMode, SkillResult}; use crate::loader; use crate::runner::{PromptOnlySkill, PythonSkill, ShellSkill}; #[cfg(feature = "wasm")] use crate::wasm_runner::WasmSkill; /// Skill registry pub struct SkillRegistry { skills: RwLock>>, manifests: RwLock>, skill_dirs: RwLock>, } impl SkillRegistry { pub fn new() -> Self { Self { skills: RwLock::new(HashMap::new()), manifests: RwLock::new(HashMap::new()), skill_dirs: RwLock::new(Vec::new()), } } /// Add a skill directory to scan pub async fn add_skill_dir(&self, dir: PathBuf) -> Result<()> { if !dir.exists() { return Err(zclaw_types::ZclawError::NotFound(format!("Directory not found: {}", dir.display()))); } { let mut dirs = self.skill_dirs.write().await; if !dirs.contains(&dir) { dirs.push(dir.clone()); } } // Scan for skills let skill_paths = loader::discover_skills(&dir)?; for skill_path in skill_paths { self.load_skill_from_dir(&skill_path).await?; } Ok(()) } /// Load a skill from directory async fn load_skill_from_dir(&self, dir: &PathBuf) -> Result<()> { let md_path = dir.join("SKILL.md"); let toml_path = dir.join("skill.toml"); let manifest = if md_path.exists() { loader::load_skill_md(&md_path)? } else if toml_path.exists() { loader::load_skill_toml(&toml_path)? } else { return Err(zclaw_types::ZclawError::NotFound( format!("No SKILL.md or skill.toml found in {}", dir.display()) )); }; // Create skill instance let skill: Arc = match &manifest.mode { SkillMode::PromptOnly => { let prompt = std::fs::read_to_string(&md_path).unwrap_or_default(); Arc::new(PromptOnlySkill::new(manifest.clone(), prompt)) } SkillMode::Shell => { let cmd = std::fs::read_to_string(dir.join("command.sh")) .unwrap_or_else(|_| "echo 'Shell skill not configured'".to_string()); Arc::new(ShellSkill::new(manifest.clone(), cmd)) } SkillMode::Python => { let script_path = dir.join("main.py"); if script_path.exists() { Arc::new(PythonSkill::new(manifest.clone(), script_path)) } else { // Fallback to PromptOnly if no main.py found let prompt = std::fs::read_to_string(&md_path).unwrap_or_default(); Arc::new(PromptOnlySkill::new(manifest.clone(), prompt)) } } #[cfg(feature = "wasm")] SkillMode::Wasm => { let wasm_path = dir.join("main.wasm"); if wasm_path.exists() { Arc::new(WasmSkill::new(manifest.clone(), wasm_path)?) } else { let prompt = std::fs::read_to_string(&md_path).unwrap_or_default(); Arc::new(PromptOnlySkill::new(manifest.clone(), prompt)) } } _ => { let prompt = std::fs::read_to_string(&md_path).unwrap_or_default(); Arc::new(PromptOnlySkill::new(manifest.clone(), prompt)) } }; // Register (use async write instead of blocking_write) let mut skills = self.skills.write().await; let mut manifests = self.manifests.write().await; skills.insert(manifest.id.clone(), skill); manifests.insert(manifest.id.clone(), manifest); Ok(()) } /// Get a skill by ID pub async fn get(&self, id: &SkillId) -> Option> { let skills = self.skills.read().await; skills.get(id).cloned() } /// Get skill manifest pub async fn get_manifest(&self, id: &SkillId) -> Option { let manifests = self.manifests.read().await; manifests.get(id).cloned() } /// List all skills pub async fn list(&self) -> Vec { let manifests = self.manifests.read().await; manifests.values().cloned().collect() } /// Synchronous snapshot of all manifests. /// Uses `try_read` — returns empty map if write lock is held (should be rare at steady state). pub fn manifests_snapshot(&self) -> HashMap { self.manifests.try_read() .map(|guard| guard.clone()) .unwrap_or_default() } /// Execute a skill pub async fn execute( &self, id: &SkillId, context: &SkillContext, input: serde_json::Value, ) -> Result { let skill = self.get(id).await .ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Skill not found: {}", id)))?; skill.execute(context, input).await } /// Remove a skill pub async fn remove(&self, id: &SkillId) { let mut skills = self.skills.write().await; let mut manifests = self.manifests.write().await; skills.remove(id); manifests.remove(id); } /// Register a skill directly pub async fn register(&self, skill: Arc, manifest: SkillManifest) { let mut skills = self.skills.write().await; let mut manifests = self.manifests.write().await; skills.insert(manifest.id.clone(), skill); manifests.insert(manifest.id.clone(), manifest); } /// Create a skill from manifest, writing SKILL.md to disk pub async fn create_skill( &self, skills_dir: &std::path::Path, manifest: SkillManifest, ) -> Result<()> { let skill_dir = skills_dir.join(manifest.id.as_str()); if skill_dir.exists() { return Err(zclaw_types::ZclawError::InvalidInput( format!("Skill directory already exists: {}", skill_dir.display()) )); } // Create directory std::fs::create_dir_all(&skill_dir) .map_err(|e| zclaw_types::ZclawError::StorageError( format!("Failed to create skill directory: {}", e) ))?; // Write SKILL.md let content = serialize_skill_md(&manifest); std::fs::write(skill_dir.join("SKILL.md"), &content) .map_err(|e| zclaw_types::ZclawError::StorageError( format!("Failed to write SKILL.md: {}", e) ))?; // Load into registry self.load_skill_from_dir(&skill_dir).await?; Ok(()) } /// Update a skill manifest, rewriting SKILL.md on disk pub async fn update_skill( &self, skills_dir: &std::path::Path, id: &SkillId, updates: SkillManifest, ) -> Result { // Find existing skill directory let skill_dir = skills_dir.join(id.as_str()); if !skill_dir.exists() { return Err(zclaw_types::ZclawError::NotFound( format!("Skill directory not found: {}", skill_dir.display()) )); } // Merge: start from existing manifest, apply updates let existing = self.get_manifest(id).await .ok_or_else(|| zclaw_types::ZclawError::NotFound( format!("Skill not found in registry: {}", id) ))?; let updated = SkillManifest { id: existing.id.clone(), name: if updates.name.is_empty() { existing.name } else { updates.name }, description: if updates.description.is_empty() { existing.description } else { updates.description }, version: if updates.version.is_empty() { existing.version } else { updates.version }, author: updates.author.or(existing.author), mode: existing.mode, capabilities: if updates.capabilities.is_empty() { existing.capabilities } else { updates.capabilities }, input_schema: updates.input_schema.or(existing.input_schema), output_schema: updates.output_schema.or(existing.output_schema), tags: if updates.tags.is_empty() { existing.tags } else { updates.tags }, category: updates.category.or(existing.category), triggers: if updates.triggers.is_empty() { existing.triggers } else { updates.triggers }, enabled: updates.enabled, }; // Rewrite SKILL.md let content = serialize_skill_md(&updated); std::fs::write(skill_dir.join("SKILL.md"), &content) .map_err(|e| zclaw_types::ZclawError::StorageError( format!("Failed to write SKILL.md: {}", e) ))?; // Reload into registry self.remove(id).await; self.load_skill_from_dir(&skill_dir).await?; Ok(updated) } /// Delete a skill: remove directory from disk and unregister pub async fn delete_skill( &self, skills_dir: &std::path::Path, id: &SkillId, ) -> Result<()> { let skill_dir = skills_dir.join(id.as_str()); if skill_dir.exists() { std::fs::remove_dir_all(&skill_dir) .map_err(|e| zclaw_types::ZclawError::StorageError( format!("Failed to remove skill directory: {}", e) ))?; } self.remove(id).await; Ok(()) } } /// Serialize a SkillManifest into SKILL.md frontmatter format fn serialize_skill_md(manifest: &SkillManifest) -> String { let mut parts = Vec::new(); // Frontmatter parts.push("---".to_string()); parts.push(format!("name: \"{}\"", manifest.name)); parts.push(format!("description: \"{}\"", manifest.description)); parts.push(format!("version: \"{}\"", manifest.version)); parts.push(format!("mode: {}", match manifest.mode { SkillMode::PromptOnly => "prompt-only", SkillMode::Python => "python", SkillMode::Shell => "shell", SkillMode::Wasm => "wasm", SkillMode::Native => "native", })); if !manifest.capabilities.is_empty() { parts.push(format!("capabilities: {}", manifest.capabilities.join(", "))); } if !manifest.tags.is_empty() { parts.push(format!("tags: {}", manifest.tags.join(", "))); } if !manifest.triggers.is_empty() { parts.push("triggers:".to_string()); for trigger in &manifest.triggers { parts.push(format!(" - \"{}\"", trigger)); } } if let Some(ref cat) = manifest.category { parts.push(format!("category: \"{}\"", cat)); } parts.push(format!("enabled: {}", manifest.enabled)); parts.push("---".to_string()); parts.push(String::new()); // Body: use description as the skill content parts.push(format!("# {}", manifest.name)); parts.push(String::new()); parts.push(manifest.description.clone()); parts.join("\n") } impl Default for SkillRegistry { fn default() -> Self { Self::new() } }