Skip to main content

walrus_skill/
loader.rs

1//! Skill markdown loading.
2//!
3//! Parses `SKILL.md` files (YAML frontmatter + Markdown body) from skill
4//! directories and builds a [`SkillRegistry`].
5
6use crate::{Skill, SkillRegistry, SkillTier};
7use compact_str::CompactString;
8use serde::Deserialize;
9use std::{collections::BTreeMap, path::Path};
10use wcore::utils::split_yaml_frontmatter;
11
12/// YAML frontmatter deserialization target for SKILL.md files.
13#[derive(Debug, Deserialize)]
14struct SkillFrontmatter {
15    name: String,
16    #[serde(default)]
17    description: String,
18    #[serde(default)]
19    license: Option<String>,
20    #[serde(default)]
21    compatibility: Option<String>,
22    #[serde(default)]
23    metadata: BTreeMap<String, String>,
24    #[serde(default, rename = "allowed-tools")]
25    allowed_tools: Option<String>,
26}
27
28/// Parse a SKILL.md file (YAML frontmatter + Markdown body) into a [`Skill`].
29pub fn parse_skill_md(content: &str) -> anyhow::Result<Skill> {
30    let (frontmatter, body) = split_yaml_frontmatter(content)?;
31    let fm: SkillFrontmatter = serde_yaml::from_str(frontmatter)?;
32
33    let allowed_tools = fm
34        .allowed_tools
35        .map(|s| {
36            s.split_whitespace()
37                .map(CompactString::from)
38                .collect::<Vec<_>>()
39        })
40        .unwrap_or_default();
41
42    let metadata = fm
43        .metadata
44        .into_iter()
45        .map(|(k, v)| (CompactString::from(k), v))
46        .collect();
47
48    Ok(Skill {
49        name: CompactString::from(fm.name),
50        description: fm.description,
51        license: fm.license.map(CompactString::from),
52        compatibility: fm.compatibility.map(CompactString::from),
53        metadata,
54        allowed_tools,
55        body: body.to_owned(),
56    })
57}
58
59/// Load skills from a directory. Each subdirectory should contain a `SKILL.md`.
60/// The given tier is assigned to all loaded skills.
61pub fn load_skills_dir(path: impl AsRef<Path>, tier: SkillTier) -> anyhow::Result<SkillRegistry> {
62    let path = path.as_ref();
63    let mut registry = SkillRegistry::new();
64
65    let entries = std::fs::read_dir(path)
66        .map_err(|e| anyhow::anyhow!("failed to read skill directory {}: {e}", path.display()))?;
67
68    for entry in entries {
69        let entry = entry?;
70        let entry_path = entry.path();
71        if !entry_path.is_dir() {
72            continue;
73        }
74
75        let skill_file = entry_path.join("SKILL.md");
76        if !skill_file.exists() {
77            continue;
78        }
79
80        let content = std::fs::read_to_string(&skill_file)
81            .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", skill_file.display()))?;
82
83        let skill = parse_skill_md(&content)?;
84        registry.add(skill, tier);
85    }
86
87    Ok(registry)
88}