use crate::core::service::{ServiceError, SkillId};
use crate::core::skill_manager::{SkillDefinition, SkillManagementService};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillMetadata {
pub id: SkillId,
pub name: String,
pub description: String,
pub version: String,
pub author: Option<String>,
pub enabled: bool,
pub token_estimate: usize,
pub last_updated: chrono::DateTime<chrono::Utc>,
}
impl From<&SkillDefinition> for SkillMetadata {
fn from(skill: &SkillDefinition) -> Self {
Self {
id: skill.id.clone(),
name: skill.name.clone(),
description: skill.description.clone(),
version: skill.version.clone(),
author: skill.author.clone(),
enabled: skill.enabled,
token_estimate: skill.description.len() / 4, last_updated: skill.updated_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillFrontmatter {
pub name: String,
pub description: String,
#[serde(default)]
pub version: Option<String>,
pub author: Option<String>,
pub license: Option<String>,
pub compatibility: Option<String>,
pub metadata: Option<std::collections::HashMap<String, String>>,
pub allowed_tools: Option<String>,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, serde_yaml::Value>,
}
#[async_trait]
pub trait MetadataService: Send + Sync {
async fn discover_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError>;
async fn search_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError>;
async fn get_skill_frontmatter(&self, skill_id: &str)
-> Result<SkillFrontmatter, ServiceError>;
}
pub struct MetadataServiceImpl {
skill_manager: Arc<dyn SkillManagementService>,
}
impl MetadataServiceImpl {
pub fn new(skill_manager: Arc<dyn SkillManagementService>) -> Self {
Self { skill_manager }
}
fn matches_query(&self, skill: &SkillDefinition, query: &str) -> bool {
let query_lower = query.to_lowercase();
if skill.name.to_lowercase().contains(&query_lower)
|| skill.description.to_lowercase().contains(&query_lower)
{
return true;
}
let query_words: Vec<&str> = query_lower.split_whitespace().collect();
for word in query_words {
if word.len() > 2 {
if skill.name.to_lowercase().contains(word)
|| skill.description.to_lowercase().contains(word)
{
return true;
}
}
}
false
}
fn score_skill(&self, skill: &SkillDefinition, query: &str) -> f32 {
let query_lower = query.to_lowercase();
let mut score = 0.0;
if skill.name.to_lowercase() == query_lower {
score += 1.0;
} else if skill.name.to_lowercase().contains(&query_lower) {
score += 0.8;
}
if skill.description.to_lowercase().contains(&query_lower) {
score += 0.6;
}
score
}
fn parse_frontmatter(&self, content: &str) -> Result<SkillFrontmatter, ServiceError> {
parse_yaml_frontmatter(content)
}
}
pub fn parse_yaml_frontmatter(content: &str) -> Result<SkillFrontmatter, ServiceError> {
let lines: Vec<&str> = content.lines().collect();
let mut frontmatter_lines = Vec::new();
let mut in_frontmatter = false;
let _frontmatter_end;
for (i, line) in lines.iter().enumerate() {
if line.trim() == "---" {
if !in_frontmatter {
in_frontmatter = true;
} else {
_frontmatter_end = Some(i);
break;
}
} else if in_frontmatter {
frontmatter_lines.push(*line);
}
}
if frontmatter_lines.is_empty() {
return Err(ServiceError::Custom(
"No YAML frontmatter found in SKILL.md".to_string(),
));
}
let frontmatter_str = frontmatter_lines.join("\n");
let mut frontmatter: std::collections::HashMap<String, serde_yaml::Value> =
serde_yaml::from_str(&frontmatter_str).map_err(|e| {
ServiceError::Custom(format!("Failed to parse YAML frontmatter: {}", e))
})?;
let name = frontmatter
.remove("name")
.and_then(|v| serde_yaml::from_value(v).ok())
.unwrap_or_else(|| "Unknown".to_string());
let description = frontmatter
.remove("description")
.and_then(|v| serde_yaml::from_value(v).ok())
.unwrap_or_else(|| "No description".to_string());
let metadata_value = frontmatter.remove("metadata");
let metadata: Option<std::collections::HashMap<String, serde_yaml::Value>> = metadata_value
.as_ref()
.and_then(|v| serde_yaml::from_value(v.clone()).ok());
let top_level_version = frontmatter
.remove("version")
.and_then(|v| serde_yaml::from_value(v).ok());
let top_level_author = frontmatter
.remove("author")
.and_then(|v| serde_yaml::from_value(v).ok());
let version = top_level_version.or_else(|| {
metadata
.as_ref()
.and_then(|m| m.get("version").and_then(|v| v.as_str().map(String::from)))
});
let author = top_level_author.or_else(|| {
metadata
.as_ref()
.and_then(|m| m.get("author").and_then(|v| v.as_str().map(String::from)))
});
Ok(SkillFrontmatter {
name,
description,
version,
author,
license: frontmatter
.remove("license")
.and_then(|v| serde_yaml::from_value(v).ok()),
compatibility: frontmatter
.remove("compatibility")
.and_then(|v| serde_yaml::from_value(v).ok()),
metadata: metadata.and_then(|m| {
let string_map: std::collections::HashMap<String, String> = m
.into_iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k, s.to_string())))
.collect();
if string_map.is_empty() {
None
} else {
Some(string_map)
}
}),
allowed_tools: frontmatter
.remove("allowed_tools")
.and_then(|v| serde_yaml::from_value(v).ok()),
extra: frontmatter,
})
}
#[async_trait]
impl MetadataService for MetadataServiceImpl {
async fn discover_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError> {
let all_skills = self.skill_manager.list_skills(None).await?;
let mut scored_skills: Vec<(f32, &SkillDefinition)> = all_skills
.iter()
.filter(|skill| self.matches_query(skill, query))
.map(|skill| (self.score_skill(skill, query), skill))
.collect();
scored_skills.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let results: Vec<SkillMetadata> = scored_skills
.into_iter()
.take(10) .map(|(_, skill)| SkillMetadata::from(skill))
.collect();
Ok(results)
}
async fn search_skills(&self, query: &str) -> Result<Vec<SkillMetadata>, ServiceError> {
self.discover_skills(query).await
}
async fn get_skill_frontmatter(
&self,
skill_id: &str,
) -> Result<SkillFrontmatter, ServiceError> {
let skill_id_parsed = crate::core::service::SkillId::new(skill_id.to_string())?;
let skill = self
.skill_manager
.get_skill(&skill_id_parsed)
.await?
.ok_or_else(|| ServiceError::Custom(format!("Skill not found: {}", skill_id)))?;
let skill_file = &skill.skill_file;
if !skill_file.exists() {
return Err(ServiceError::Custom(format!(
"Skill file not found: {}",
skill_file.display()
)));
}
let content = tokio::fs::read_to_string(skill_file).await?;
self.parse_frontmatter(&content)
}
}