1use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use synaptic_core::{SynapticError, Tool};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SkillManifest {
16 pub name: String,
18 pub description: String,
20 pub version: String,
22 pub author: Option<String>,
24 #[serde(default)]
26 pub tags: Vec<String>,
27}
28
29#[async_trait]
31pub trait Skill: Send + Sync {
32 fn manifest(&self) -> &SkillManifest;
34
35 fn tools(&self) -> Vec<Arc<dyn Tool>>;
37
38 fn system_prompt_fragment(&self) -> Option<String> {
40 None
41 }
42
43 async fn init(&self) -> Result<(), SynapticError> {
45 Ok(())
46 }
47}
48
49pub struct SkillRegistry {
51 skills: Vec<Arc<dyn Skill>>,
52}
53
54impl SkillRegistry {
55 pub fn new() -> Self {
56 Self { skills: Vec::new() }
57 }
58
59 pub fn register(&mut self, skill: Arc<dyn Skill>) {
61 self.skills.push(skill);
62 }
63
64 pub fn skills(&self) -> &[Arc<dyn Skill>] {
66 &self.skills
67 }
68
69 pub fn get(&self, name: &str) -> Option<&Arc<dyn Skill>> {
71 self.skills.iter().find(|s| s.manifest().name == name)
72 }
73
74 pub fn all_tools(&self) -> Vec<Arc<dyn Tool>> {
76 self.skills.iter().flat_map(|s| s.tools()).collect()
77 }
78
79 pub fn system_prompt_additions(&self) -> String {
81 self.skills
82 .iter()
83 .filter_map(|s| s.system_prompt_fragment())
84 .collect::<Vec<_>>()
85 .join("\n\n")
86 }
87
88 pub async fn init_all(&self) -> Result<(), SynapticError> {
90 for skill in &self.skills {
91 skill.init().await?;
92 }
93 Ok(())
94 }
95}
96
97impl Default for SkillRegistry {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103pub fn load_manifest(path: &Path) -> Result<SkillManifest, SynapticError> {
105 let content = std::fs::read_to_string(path)
106 .map_err(|e| SynapticError::Config(format!("cannot read manifest: {}", e)))?;
107 toml::from_str(&content).map_err(|e| SynapticError::Config(format!("invalid manifest: {}", e)))
108}
109
110pub fn discover_skills(dir: &Path) -> Vec<(PathBuf, SkillManifest)> {
112 let mut results = Vec::new();
113 if let Ok(entries) = std::fs::read_dir(dir) {
114 for entry in entries.flatten() {
115 let manifest_path = entry.path().join("manifest.toml");
116 if manifest_path.exists() {
117 if let Ok(manifest) = load_manifest(&manifest_path) {
118 results.push((entry.path(), manifest));
119 }
120 }
121 }
122 }
123 results
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 struct TestSkill {
131 manifest: SkillManifest,
132 }
133
134 #[async_trait]
135 impl Skill for TestSkill {
136 fn manifest(&self) -> &SkillManifest {
137 &self.manifest
138 }
139 fn tools(&self) -> Vec<Arc<dyn Tool>> {
140 vec![]
141 }
142 fn system_prompt_fragment(&self) -> Option<String> {
143 Some("I am a test skill.".to_string())
144 }
145 }
146
147 #[test]
148 fn test_registry_register_and_get() {
149 let mut registry = SkillRegistry::new();
150 let skill = Arc::new(TestSkill {
151 manifest: SkillManifest {
152 name: "test-skill".to_string(),
153 description: "A test skill".to_string(),
154 version: "1.0.0".to_string(),
155 author: None,
156 tags: vec!["test".to_string()],
157 },
158 });
159 registry.register(skill);
160 assert_eq!(registry.skills().len(), 1);
161 assert!(registry.get("test-skill").is_some());
162 assert!(registry.get("nonexistent").is_none());
163 }
164
165 #[test]
166 fn test_all_tools_empty() {
167 let mut registry = SkillRegistry::new();
168 let skill = Arc::new(TestSkill {
169 manifest: SkillManifest {
170 name: "empty".to_string(),
171 description: "No tools".to_string(),
172 version: "0.1.0".to_string(),
173 author: None,
174 tags: vec![],
175 },
176 });
177 registry.register(skill);
178 assert!(registry.all_tools().is_empty());
179 }
180
181 #[test]
182 fn test_system_prompt_additions() {
183 let mut registry = SkillRegistry::new();
184 registry.register(Arc::new(TestSkill {
185 manifest: SkillManifest {
186 name: "s1".to_string(),
187 description: "".to_string(),
188 version: "0.1.0".to_string(),
189 author: None,
190 tags: vec![],
191 },
192 }));
193 let additions = registry.system_prompt_additions();
194 assert!(additions.contains("test skill"));
195 }
196
197 #[test]
198 fn test_discover_skills_empty_dir() {
199 let dir = std::env::temp_dir().join("synaptic_test_discover_empty");
200 let _ = std::fs::create_dir_all(&dir);
201 let results = discover_skills(&dir);
202 let _ = results;
204 let _ = std::fs::remove_dir_all(&dir);
205 }
206
207 #[tokio::test]
208 async fn test_init_all() {
209 let mut registry = SkillRegistry::new();
210 registry.register(Arc::new(TestSkill {
211 manifest: SkillManifest {
212 name: "init-test".to_string(),
213 description: "".to_string(),
214 version: "0.1.0".to_string(),
215 author: None,
216 tags: vec![],
217 },
218 }));
219 assert!(registry.init_all().await.is_ok());
220 }
221}