cascade_agent/skills/
mod.rs1pub 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
19pub struct SkillManager {
24 skills_dir: std::path::PathBuf,
26 skills: HashMap<String, types::Skill>,
28}
29
30impl SkillManager {
31 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 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 pub fn get(&self, name: &str) -> Option<&types::Skill> {
102 self.skills.get(name)
103 }
104
105 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 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 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 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 pub fn skills_dir(&self) -> &Path {
193 &self.skills_dir
194 }
195}