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 std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;

/// Loaded skill with its manifest and path.
#[derive(Debug, Clone)]
pub struct Skill {
    pub name: Arc<str>,
    pub path: PathBuf,
    pub manifest: Option<SkillManifest>,
}

/// Skill loader for scanning and loading skills from directory.
pub struct SkillLoader {
    skills_dir: PathBuf,
    skills: HashMap<Arc<str>, Skill>,
}

impl SkillLoader {
    /// Create a new skill loader.
    pub fn new(skills_dir: PathBuf) -> Self {
        Self {
            skills_dir,
            skills: HashMap::new(),
        }
    }

    /// Get the skills directory path.
    pub fn skills_dir(&self) -> &Path {
        &self.skills_dir
    }

    /// Load all skills from the skills directory.
    pub fn load_all(&mut self) -> Result<usize> {
        if !self.skills_dir.exists() {
            fs::create_dir_all(&self.skills_dir).context("Failed to create skills directory")?;
            return Ok(0);
        }

        let mut count = 0;

        let entries = fs::read_dir(&self.skills_dir).context("Failed to read skills directory")?;

        for entry in entries {
            let entry = entry.context("Failed to read directory entry")?;
            let path = entry.path();

            if path.is_file() {
                let file_name = path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or_default();

                if file_name.ends_with(".manifest.json") {
                    continue;
                }

                let skill_name = Arc::from(file_name);

                if self.skills.contains_key(&skill_name) {
                    continue;
                }

                let manifest = self.load_manifest(&path);
                let skill = Skill {
                    name: skill_name.clone(),
                    path: path.clone(),
                    manifest,
                };

                self.skills.insert(skill_name, skill);
                count += 1;
            }
        }

        Ok(count)
    }

    /// Load manifest for a skill if it exists.
    fn load_manifest(&self, skill_path: &Path) -> Option<SkillManifest> {
        let manifest_path = skill_path.with_extension("manifest.json");

        if !manifest_path.exists() {
            return None;
        }

        let content = fs::read_to_string(&manifest_path).ok()?;
        let manifest: SkillManifest = serde_json::from_str(&content).ok()?;

        Some(manifest)
    }

    /// Get a skill by name.
    pub fn get(&self, name: &str) -> Option<&Skill> {
        self.skills.get(name)
    }

    /// List all loaded skills.
    pub fn list(&self) -> Vec<&Skill> {
        self.skills.values().collect()
    }

    /// Get the number of loaded skills.
    pub fn count(&self) -> usize {
        self.skills.len()
    }
}