a3s_code_core/skills/
registry.rs1use super::Skill;
6use anyhow::Context;
7use std::collections::HashMap;
8use std::path::Path;
9use std::sync::{Arc, RwLock};
10
11pub struct SkillRegistry {
15 skills: Arc<RwLock<HashMap<String, Arc<Skill>>>>,
16}
17
18impl SkillRegistry {
19 pub fn new() -> Self {
21 Self {
22 skills: Arc::new(RwLock::new(HashMap::new())),
23 }
24 }
25
26 pub fn with_builtins() -> Self {
28 let registry = Self::new();
29 for skill in super::builtin::builtin_skills() {
30 registry.register(skill);
31 }
32 registry
33 }
34
35 pub fn register(&self, skill: Arc<Skill>) {
37 let mut skills = self.skills.write().unwrap();
38 skills.insert(skill.name.clone(), skill);
39 }
40
41 pub fn get(&self, name: &str) -> Option<Arc<Skill>> {
43 let skills = self.skills.read().unwrap();
44 skills.get(name).cloned()
45 }
46
47 pub fn list(&self) -> Vec<String> {
49 let skills = self.skills.read().unwrap();
50 skills.keys().cloned().collect()
51 }
52
53 pub fn all(&self) -> Vec<Arc<Skill>> {
55 let skills = self.skills.read().unwrap();
56 skills.values().cloned().collect()
57 }
58
59 pub fn load_from_dir(&self, dir: impl AsRef<Path>) -> anyhow::Result<usize> {
64 let dir = dir.as_ref();
65
66 if !dir.exists() {
67 return Ok(0);
68 }
69
70 if !dir.is_dir() {
71 anyhow::bail!("Path is not a directory: {}", dir.display());
72 }
73
74 let mut loaded = 0;
75
76 for entry in std::fs::read_dir(dir)
77 .with_context(|| format!("Failed to read directory: {}", dir.display()))?
78 {
79 let entry = entry?;
80 let path = entry.path();
81
82 if path.extension().and_then(|s| s.to_str()) != Some("md") {
84 continue;
85 }
86
87 match Skill::from_file(&path) {
89 Ok(skill) => {
90 self.register(Arc::new(skill));
91 loaded += 1;
92 }
93 Err(e) => {
94 tracing::debug!("Skipped {}: {}", path.display(), e);
96 }
97 }
98 }
99
100 Ok(loaded)
101 }
102
103 pub fn load_from_file(&self, path: impl AsRef<Path>) -> anyhow::Result<Arc<Skill>> {
105 let skill = Skill::from_file(path)?;
106 let skill = Arc::new(skill);
107 self.register(skill.clone());
108 Ok(skill)
109 }
110
111 pub fn remove(&self, name: &str) -> Option<Arc<Skill>> {
113 let mut skills = self.skills.write().unwrap();
114 skills.remove(name)
115 }
116
117 pub fn clear(&self) {
119 let mut skills = self.skills.write().unwrap();
120 skills.clear();
121 }
122
123 pub fn len(&self) -> usize {
125 let skills = self.skills.read().unwrap();
126 skills.len()
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.len() == 0
132 }
133
134 pub fn by_kind(&self, kind: super::SkillKind) -> Vec<Arc<Skill>> {
136 let skills = self.skills.read().unwrap();
137 skills
138 .values()
139 .filter(|s| s.kind == kind)
140 .cloned()
141 .collect()
142 }
143
144 pub fn by_tag(&self, tag: &str) -> Vec<Arc<Skill>> {
146 let skills = self.skills.read().unwrap();
147 skills
148 .values()
149 .filter(|s| s.tags.iter().any(|t| t == tag))
150 .cloned()
151 .collect()
152 }
153
154 pub fn to_system_prompt(&self) -> String {
159 let skills = self.skills.read().unwrap();
160
161 let instruction_skills: Vec<_> = skills
162 .values()
163 .filter(|s| s.kind == super::SkillKind::Instruction)
164 .collect();
165
166 if instruction_skills.is_empty() {
167 return String::new();
168 }
169
170 let mut prompt = String::from("# Available Skills\n\n");
171
172 for skill in instruction_skills {
173 prompt.push_str(&skill.to_system_prompt());
174 prompt.push_str("\n\n---\n\n");
175 }
176
177 prompt
178 }
179}
180
181impl Default for SkillRegistry {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::skills::SkillKind;
191 use std::io::Write;
192 use tempfile::TempDir;
193
194 #[test]
195 fn test_new_registry() {
196 let registry = SkillRegistry::new();
197 assert_eq!(registry.len(), 0);
198 assert!(registry.is_empty());
199 }
200
201 #[test]
202 fn test_with_builtins() {
203 let registry = SkillRegistry::with_builtins();
204 assert_eq!(registry.len(), 7, "Expected 7 built-in skills");
205 assert!(!registry.is_empty());
206
207 assert!(registry.get("code-search").is_some());
209 assert!(registry.get("code-review").is_some());
210 assert!(registry.get("explain-code").is_some());
211 assert!(registry.get("find-bugs").is_some());
212
213 assert!(registry.get("builtin-tools").is_some());
215 assert!(registry.get("delegate-task").is_some());
216 assert!(registry.get("find-skills").is_some());
217 }
218
219 #[test]
220 fn test_register_and_get() {
221 let registry = SkillRegistry::new();
222
223 let skill = Arc::new(Skill {
224 name: "test-skill".to_string(),
225 description: "A test skill".to_string(),
226 allowed_tools: None,
227 disable_model_invocation: false,
228 kind: SkillKind::Instruction,
229 content: "Test content".to_string(),
230 tags: vec![],
231 version: None,
232 });
233
234 registry.register(skill.clone());
235
236 assert_eq!(registry.len(), 1);
237 let retrieved = registry.get("test-skill").unwrap();
238 assert_eq!(retrieved.name, "test-skill");
239 }
240
241 #[test]
242 fn test_list() {
243 let registry = SkillRegistry::with_builtins();
244 let names = registry.list();
245
246 assert_eq!(names.len(), 7, "Expected 7 built-in skills");
247 assert!(names.contains(&"code-search".to_string()));
248 assert!(names.contains(&"code-review".to_string()));
249 assert!(names.contains(&"builtin-tools".to_string()));
250 assert!(names.contains(&"delegate-task".to_string()));
251 assert!(names.contains(&"find-skills".to_string()));
252 }
253
254 #[test]
255 fn test_remove() {
256 let registry = SkillRegistry::with_builtins();
257 assert_eq!(registry.len(), 7);
258
259 let removed = registry.remove("code-search");
260 assert!(removed.is_some());
261 assert_eq!(registry.len(), 6);
262 assert!(registry.get("code-search").is_none());
263 }
264
265 #[test]
266 fn test_clear() {
267 let registry = SkillRegistry::with_builtins();
268 assert_eq!(registry.len(), 7);
269
270 registry.clear();
271 assert_eq!(registry.len(), 0);
272 assert!(registry.is_empty());
273 }
274
275 #[test]
276 fn test_by_kind() {
277 let registry = SkillRegistry::with_builtins();
278 let instruction_skills = registry.by_kind(SkillKind::Instruction);
279
280 assert_eq!(
281 instruction_skills.len(),
282 7,
283 "Expected 7 instruction skills (4 code assistance + 3 tool documentation)"
284 );
285
286 let tool_skills = registry.by_kind(SkillKind::Tool);
287 assert_eq!(tool_skills.len(), 0);
288 }
289
290 #[test]
291 fn test_by_tag() {
292 let registry = SkillRegistry::with_builtins();
293 let search_skills = registry.by_tag("search");
294
295 assert_eq!(search_skills.len(), 1);
296 assert_eq!(search_skills[0].name, "code-search");
297
298 let security_skills = registry.by_tag("security");
299 assert_eq!(security_skills.len(), 1);
300 assert_eq!(security_skills[0].name, "find-bugs");
301 }
302
303 #[test]
304 fn test_load_from_dir() -> anyhow::Result<()> {
305 let temp_dir = TempDir::new()?;
306
307 let skill_path = temp_dir.path().join("test-skill.md");
309 let mut file = std::fs::File::create(&skill_path)?;
310 writeln!(file, "---")?;
311 writeln!(file, "name: test-skill")?;
312 writeln!(file, "description: A test skill")?;
313 writeln!(file, "kind: instruction")?;
314 writeln!(file, "---")?;
315 writeln!(file, "# Test Skill")?;
316 writeln!(file, "This is a test skill.")?;
317 drop(file);
318
319 let readme_path = temp_dir.path().join("README.md");
321 std::fs::write(&readme_path, "# README\nNot a skill")?;
322
323 let txt_path = temp_dir.path().join("notes.txt");
325 std::fs::write(&txt_path, "Some notes")?;
326
327 let registry = SkillRegistry::new();
328 let loaded = registry.load_from_dir(temp_dir.path())?;
329
330 assert_eq!(loaded, 1);
331 assert_eq!(registry.len(), 1);
332 assert!(registry.get("test-skill").is_some());
333
334 Ok(())
335 }
336
337 #[test]
338 fn test_load_from_file() -> anyhow::Result<()> {
339 let temp_dir = TempDir::new()?;
340 let skill_path = temp_dir.path().join("my-skill.md");
341
342 let mut file = std::fs::File::create(&skill_path)?;
343 writeln!(file, "---")?;
344 writeln!(file, "name: my-skill")?;
345 writeln!(file, "description: My custom skill")?;
346 writeln!(file, "---")?;
347 writeln!(file, "# My Skill")?;
348 drop(file);
349
350 let registry = SkillRegistry::new();
351 let skill = registry.load_from_file(&skill_path)?;
352
353 assert_eq!(skill.name, "my-skill");
354 assert_eq!(registry.len(), 1);
355
356 Ok(())
357 }
358
359 #[test]
360 fn test_to_system_prompt() {
361 let registry = SkillRegistry::with_builtins();
362 let prompt = registry.to_system_prompt();
363
364 assert!(prompt.contains("# Available Skills"));
365 assert!(prompt.contains("code-search"));
366 assert!(prompt.contains("code-review"));
367 assert!(prompt.contains("explain-code"));
368 assert!(prompt.contains("find-bugs"));
369 }
370
371 #[test]
372 fn test_load_from_nonexistent_dir() {
373 let registry = SkillRegistry::new();
374 let result = registry.load_from_dir("/nonexistent/path");
375
376 assert!(result.is_ok());
377 assert_eq!(result.unwrap(), 0);
378 }
379}