use crate::skill::{Skill, SkillRegistry};
use serde::{Deserialize, Deserializer};
use std::{collections::BTreeMap, path::Path};
use wcore::utils::split_yaml_frontmatter;
fn string_or_vec<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<String>, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
match StringOrVec::deserialize(deserializer)? {
StringOrVec::Vec(v) => Ok(v),
StringOrVec::String(s) => Ok(s
.split([',', ' '])
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()),
}
}
#[derive(Debug, Deserialize)]
struct SkillFrontmatter {
name: String,
#[serde(default)]
description: String,
#[serde(default)]
license: Option<String>,
#[serde(default)]
compatibility: Option<String>,
#[serde(default)]
metadata: BTreeMap<String, String>,
#[serde(default, rename = "allowed-tools", deserialize_with = "string_or_vec")]
allowed_tools: Vec<String>,
}
pub fn parse_skill_md(content: &str) -> anyhow::Result<Skill> {
let (frontmatter, body) = split_yaml_frontmatter(content)?;
let fm: SkillFrontmatter = serde_yml::from_str(frontmatter)?;
let metadata = fm.metadata;
Ok(Skill {
name: fm.name,
description: fm.description,
license: fm.license,
compatibility: fm.compatibility,
metadata,
allowed_tools: fm.allowed_tools,
body: body.to_owned(),
})
}
pub fn load_skills_dir(path: impl AsRef<Path>) -> anyhow::Result<SkillRegistry> {
let path = path.as_ref();
let mut registry = SkillRegistry::new();
scan_skills(path, &mut registry)?;
Ok(registry)
}
fn scan_skills(dir: &Path, registry: &mut SkillRegistry) -> anyhow::Result<()> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Ok(()),
};
for entry in entries {
let entry = entry?;
let entry_path = entry.path();
if !entry_path.is_dir() {
continue;
}
if entry
.file_name()
.to_str()
.is_some_and(|n| n.starts_with('.'))
{
continue;
}
let skill_file = entry_path.join("SKILL.md");
if skill_file.exists() {
let content = std::fs::read_to_string(&skill_file)
.map_err(|e| anyhow::anyhow!("failed to read {}: {e}", skill_file.display()))?;
match parse_skill_md(&content) {
Ok(skill) => registry.add(skill),
Err(e) => {
tracing::warn!("failed to parse {}: {e}", skill_file.display());
}
}
} else {
scan_skills(&entry_path, registry)?;
}
}
Ok(())
}