fastskill_core/core/
metadata.rs1use crate::core::service::{ServiceError, SkillId};
4use crate::core::skill_manager::{SkillDefinition, SkillManagementService};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SkillMetadata {
11 pub id: SkillId,
12 pub name: String,
13 pub description: String,
14 pub version: String,
15 pub author: Option<String>,
16 pub enabled: bool,
17 pub token_estimate: usize,
18 pub last_updated: chrono::DateTime<chrono::Utc>,
19}
20
21impl From<&SkillDefinition> for SkillMetadata {
22 fn from(skill: &SkillDefinition) -> Self {
23 Self {
24 id: skill.id.clone(),
25 name: skill.name.clone(),
26 description: skill.description.clone(),
27 version: skill.version.clone(),
28 author: skill.author.clone(),
29 enabled: skill.enabled,
30 token_estimate: skill.description.len() / 4, last_updated: skill.updated_at,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SkillFrontmatter {
39 pub name: String,
40 pub description: String,
41 #[serde(default)]
42 pub version: Option<String>,
43 pub author: Option<String>,
44 pub license: Option<String>,
45 pub compatibility: Option<String>,
46 pub metadata: Option<std::collections::HashMap<String, String>>,
47 pub allowed_tools: Option<String>,
48 #[serde(flatten)]
49 pub extra: std::collections::HashMap<String, serde_yaml::Value>,
50}
51
52#[async_trait]
53pub trait MetadataService: Send + Sync {
54 async fn discover_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError>;
55 async fn search_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError>;
56 async fn get_skill_frontmatter(&self, skill_id: &str)
57 -> Result<SkillFrontmatter, ServiceError>;
58}
59
60pub struct MetadataServiceImpl {
61 skill_manager: Arc<dyn SkillManagementService>,
62}
63
64impl MetadataServiceImpl {
65 pub fn new(skill_manager: Arc<dyn SkillManagementService>) -> Self {
66 Self { skill_manager }
67 }
68
69 fn matches_query(&self, skill: &SkillDefinition, query: &str) -> bool {
71 let query_lower = query.to_lowercase();
72
73 if skill.name.to_lowercase().contains(&query_lower)
75 || skill.description.to_lowercase().contains(&query_lower)
76 {
77 return true;
78 }
79
80 let query_words: Vec<&str> = query_lower.split_whitespace().collect();
82 for word in query_words {
83 if word.len() > 2 {
84 if skill.name.to_lowercase().contains(word)
86 || skill.description.to_lowercase().contains(word)
87 {
88 return true;
89 }
90 }
91 }
92
93 false
94 }
95
96 fn score_skill(&self, skill: &SkillDefinition, query: &str) -> f32 {
98 let query_lower = query.to_lowercase();
99 let mut score = 0.0;
100
101 if skill.name.to_lowercase() == query_lower {
103 score += 1.0;
104 } else if skill.name.to_lowercase().contains(&query_lower) {
105 score += 0.8;
106 }
107
108 if skill.description.to_lowercase().contains(&query_lower) {
110 score += 0.6;
111 }
112
113 score
114 }
115
116 fn parse_frontmatter(&self, content: &str) -> Result<SkillFrontmatter, ServiceError> {
118 parse_yaml_frontmatter(content)
119 }
120}
121
122pub fn parse_yaml_frontmatter(content: &str) -> Result<SkillFrontmatter, ServiceError> {
125 let lines: Vec<&str> = content.lines().collect();
126 let mut frontmatter_lines = Vec::new();
127 let mut in_frontmatter = false;
128 let _frontmatter_end;
129
130 for (i, line) in lines.iter().enumerate() {
132 if line.trim() == "---" {
133 if !in_frontmatter {
134 in_frontmatter = true;
135 } else {
136 _frontmatter_end = Some(i);
137 break;
138 }
139 } else if in_frontmatter {
140 frontmatter_lines.push(*line);
141 }
142 }
143
144 if frontmatter_lines.is_empty() {
146 return Err(ServiceError::Custom(
149 "No YAML frontmatter found in SKILL.md".to_string(),
150 ));
151 }
152
153 let frontmatter_str = frontmatter_lines.join("\n");
155 let mut frontmatter: std::collections::HashMap<String, serde_yaml::Value> =
156 serde_yaml::from_str(&frontmatter_str).map_err(|e| {
157 ServiceError::Custom(format!("Failed to parse YAML frontmatter: {}", e))
158 })?;
159
160 let name = frontmatter
162 .remove("name")
163 .and_then(|v| serde_yaml::from_value(v).ok())
164 .unwrap_or_else(|| "Unknown".to_string());
165
166 let description = frontmatter
167 .remove("description")
168 .and_then(|v| serde_yaml::from_value(v).ok())
169 .unwrap_or_else(|| "No description".to_string());
170
171 let metadata_value = frontmatter.remove("metadata");
173 let metadata: Option<std::collections::HashMap<String, serde_yaml::Value>> = metadata_value
174 .as_ref()
175 .and_then(|v| serde_yaml::from_value(v.clone()).ok());
176
177 let top_level_version = frontmatter
179 .remove("version")
180 .and_then(|v| serde_yaml::from_value(v).ok());
181
182 let top_level_author = frontmatter
183 .remove("author")
184 .and_then(|v| serde_yaml::from_value(v).ok());
185
186 let version = top_level_version.or_else(|| {
188 metadata
189 .as_ref()
190 .and_then(|m| m.get("version").and_then(|v| v.as_str().map(String::from)))
191 });
192
193 let author = top_level_author.or_else(|| {
194 metadata
195 .as_ref()
196 .and_then(|m| m.get("author").and_then(|v| v.as_str().map(String::from)))
197 });
198
199 Ok(SkillFrontmatter {
200 name,
201 description,
202 version,
203 author,
204 license: frontmatter
205 .remove("license")
206 .and_then(|v| serde_yaml::from_value(v).ok()),
207 compatibility: frontmatter
208 .remove("compatibility")
209 .and_then(|v| serde_yaml::from_value(v).ok()),
210 metadata: metadata.and_then(|m| {
211 let string_map: std::collections::HashMap<String, String> = m
213 .into_iter()
214 .filter_map(|(k, v)| v.as_str().map(|s| (k, s.to_string())))
215 .collect();
216 if string_map.is_empty() {
217 None
218 } else {
219 Some(string_map)
220 }
221 }),
222 allowed_tools: frontmatter
223 .remove("allowed_tools")
224 .and_then(|v| serde_yaml::from_value(v).ok()),
225 extra: frontmatter,
226 })
227}
228
229#[async_trait]
230impl MetadataService for MetadataServiceImpl {
231 async fn discover_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError> {
232 let all_skills = self.skill_manager.list_skills(None).await?;
233
234 let mut scored_skills: Vec<(f32, &SkillDefinition)> = all_skills
236 .iter()
237 .filter(|skill| self.matches_query(skill, query))
238 .map(|skill| (self.score_skill(skill, query), skill))
239 .collect();
240
241 scored_skills.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
244
245 let results: Vec<SkillMetadata> = scored_skills
247 .into_iter()
248 .take(10) .map(|(_, skill)| SkillMetadata::from(skill))
250 .collect();
251
252 Ok(results)
253 }
254
255 async fn search_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError> {
256 self.discover_skills(query).await
258 }
259
260 async fn get_skill_frontmatter(
261 &self,
262 skill_id: &str,
263 ) -> Result<SkillFrontmatter, ServiceError> {
264 let skill_id_parsed = crate::core::service::SkillId::new(skill_id.to_string())?;
266 let skill = self
267 .skill_manager
268 .get_skill(&skill_id_parsed)
269 .await?
270 .ok_or_else(|| ServiceError::Custom(format!("Skill not found: {}", skill_id)))?;
271
272 let skill_file = &skill.skill_file;
273
274 if !skill_file.exists() {
276 return Err(ServiceError::Custom(format!(
277 "Skill file not found: {}",
278 skill_file.display()
279 )));
280 }
281
282 let content = tokio::fs::read_to_string(skill_file).await?;
283
284 self.parse_frontmatter(&content)
286 }
287}