Skip to main content

cascade_agent/skills/
mod.rs

1//! Skill system: discovery, parsing, CRUD, and Tool adapter.
2
3pub mod executor;
4pub mod parser;
5pub mod skill_tool;
6pub mod types;
7
8use std::collections::HashMap;
9use std::path::Path;
10use tracing::{debug, info, warn};
11
12use crate::error::{AgentError, Result};
13use crate::tools::Tool;
14
15use parser::{parse_skill_file, read_skill_parts, serialize_skill_md, skill_file_name};
16use skill_tool::SkillTool;
17use types::SkillMetadata;
18
19/// Manages a collection of skills discovered from a directory on disk.
20///
21/// Each skill lives in its own subdirectory of `skills_dir` and must contain
22/// a `SKILL.md` file with YAML frontmatter.
23pub struct SkillManager {
24    /// Root directory under which skill subdirectories live.
25    skills_dir: std::path::PathBuf,
26    /// Parsed skills indexed by their metadata name.
27    skills: HashMap<String, types::Skill>,
28}
29
30impl SkillManager {
31    /// Create a new `SkillManager`, ensuring the skills directory exists.
32    pub fn new(skills_dir: std::path::PathBuf) -> Result<Self> {
33        if !skills_dir.exists() {
34            std::fs::create_dir_all(&skills_dir).map_err(|e| {
35                AgentError::SkillError(format!(
36                    "Cannot create skills directory '{}': {}",
37                    skills_dir.display(),
38                    e
39                ))
40            })?;
41            info!(dir = %skills_dir.display(), "Created skills directory");
42        }
43
44        Ok(Self {
45            skills_dir,
46            skills: HashMap::new(),
47        })
48    }
49
50    /// Scan the skills directory for subdirectories containing `SKILL.md`,
51    /// parse each, and insert into the internal map.
52    ///
53    /// Returns the names of all successfully discovered skills.
54    pub fn discover(&mut self) -> Result<Vec<String>> {
55        let mut discovered = Vec::new();
56
57        let entries = match std::fs::read_dir(&self.skills_dir) {
58            Ok(rd) => rd,
59            Err(e) => {
60                return Err(AgentError::SkillError(format!(
61                    "Cannot read skills directory '{}': {}",
62                    self.skills_dir.display(),
63                    e
64                )));
65            }
66        };
67
68        for entry in entries.flatten() {
69            let path = entry.path();
70            if !path.is_dir() {
71                continue;
72            }
73
74            let skill_md = path.join(skill_file_name());
75            if !skill_md.exists() {
76                continue;
77            }
78
79            match parse_skill_file(&skill_md) {
80                Ok(skill) => {
81                    let name = skill.metadata.name.clone();
82                    debug!(skill = %name, dir = %path.display(), "Discovered skill");
83                    self.skills.insert(name.clone(), skill);
84                    discovered.push(name);
85                }
86                Err(e) => {
87                    warn!(
88                        path = %skill_md.display(),
89                        error = %e,
90                        "Skipping invalid skill directory"
91                    );
92                }
93            }
94        }
95
96        info!(count = discovered.len(), "Skill discovery complete");
97        Ok(discovered)
98    }
99
100    /// Get a skill by name.
101    pub fn get(&self, name: &str) -> Option<&types::Skill> {
102        self.skills.get(name)
103    }
104
105    /// Create a new skill directory and `SKILL.md` file.
106    pub fn create_skill(&self, name: &str, metadata: &SkillMetadata, body: &str) -> Result<()> {
107        let skill_dir = self.skills_dir.join(name);
108
109        if skill_dir.exists() {
110            return Err(AgentError::SkillError(format!(
111                "Skill directory '{}' already exists",
112                skill_dir.display()
113            )));
114        }
115
116        std::fs::create_dir_all(&skill_dir).map_err(|e| {
117            AgentError::SkillError(format!(
118                "Cannot create skill directory '{}': {}",
119                skill_dir.display(),
120                e
121            ))
122        })?;
123
124        let content = serialize_skill_md(metadata, body);
125        let skill_md = skill_dir.join(skill_file_name());
126        std::fs::write(&skill_md, &content).map_err(|e| {
127            AgentError::SkillError(format!("Cannot write '{}': {}", skill_md.display(), e))
128        })?;
129
130        info!(skill = name, dir = %skill_dir.display(), "Created skill");
131        Ok(())
132    }
133
134    /// Update the body (instructions) of an existing skill, preserving its
135    /// frontmatter.
136    pub fn update_skill(&self, name: &str, body: &str) -> Result<()> {
137        let skill_dir = self.skills_dir.join(name);
138        let skill_md = skill_dir.join(skill_file_name());
139
140        if !skill_md.exists() {
141            return Err(AgentError::SkillError(format!(
142                "Skill '{}' not found (no SKILL.md at {})",
143                name,
144                skill_md.display()
145            )));
146        }
147
148        let (frontmatter, _old_body) = read_skill_parts(&skill_md)?;
149        let content = format!("{}{}", frontmatter, body);
150        std::fs::write(&skill_md, &content).map_err(|e| {
151            AgentError::SkillError(format!("Cannot write '{}': {}", skill_md.display(), e))
152        })?;
153
154        debug!(skill = name, "Updated skill instructions");
155        Ok(())
156    }
157
158    /// Remove a skill directory entirely.
159    pub fn remove_skill(&self, name: &str) -> Result<()> {
160        let skill_dir = self.skills_dir.join(name);
161
162        if !skill_dir.exists() {
163            return Err(AgentError::SkillError(format!(
164                "Skill '{}' not found at '{}'",
165                name,
166                skill_dir.display()
167            )));
168        }
169
170        std::fs::remove_dir_all(&skill_dir).map_err(|e| {
171            AgentError::SkillError(format!(
172                "Cannot remove skill directory '{}': {}",
173                skill_dir.display(),
174                e
175            ))
176        })?;
177
178        info!(skill = name, "Removed skill");
179        Ok(())
180    }
181
182    /// Convert all loaded skills into `SkillTool` instances implementing
183    /// the [`Tool`] trait.
184    pub fn all_tools(&self) -> Vec<Box<dyn Tool>> {
185        self.skills
186            .values()
187            .map(|skill| Box::new(SkillTool::new(skill.clone())) as Box<dyn Tool>)
188            .collect()
189    }
190
191    /// Return the path to the skills directory.
192    pub fn skills_dir(&self) -> &Path {
193        &self.skills_dir
194    }
195}