crabtalk_runtime/skill/
loader.rs1use crate::skill::{Skill, SkillRegistry};
4use serde::Deserialize;
5use std::{collections::BTreeMap, path::Path};
6use wcore::utils::split_yaml_frontmatter;
7
8#[derive(Debug, Deserialize)]
10struct SkillFrontmatter {
11 name: String,
12 #[serde(default)]
13 description: String,
14 #[serde(default)]
15 license: Option<String>,
16 #[serde(default)]
17 compatibility: Option<String>,
18 #[serde(default)]
19 metadata: BTreeMap<String, String>,
20 #[serde(default, rename = "allowed-tools")]
21 allowed_tools: Vec<String>,
22}
23
24pub fn parse_skill_md(content: &str) -> anyhow::Result<Skill> {
26 let (frontmatter, body) = split_yaml_frontmatter(content)?;
27 let fm: SkillFrontmatter = serde_yml::from_str(frontmatter)?;
28
29 let metadata = fm.metadata;
30
31 Ok(Skill {
32 name: fm.name,
33 description: fm.description,
34 license: fm.license,
35 compatibility: fm.compatibility,
36 metadata,
37 allowed_tools: fm.allowed_tools,
38 body: body.to_owned(),
39 })
40}
41
42pub fn load_skills_dir(path: impl AsRef<Path>) -> anyhow::Result<SkillRegistry> {
44 let path = path.as_ref();
45 let mut registry = SkillRegistry::new();
46 scan_skills(path, &mut registry)?;
47 Ok(registry)
48}
49
50fn scan_skills(dir: &Path, registry: &mut SkillRegistry) -> anyhow::Result<()> {
51 let entries = match std::fs::read_dir(dir) {
52 Ok(e) => e,
53 Err(_) => return Ok(()),
54 };
55
56 for entry in entries {
57 let entry = entry?;
58 let entry_path = entry.path();
59 if !entry_path.is_dir() {
60 continue;
61 }
62
63 if entry
64 .file_name()
65 .to_str()
66 .is_some_and(|n| n.starts_with('.'))
67 {
68 continue;
69 }
70
71 let skill_file = entry_path.join("SKILL.md");
72 if skill_file.exists() {
73 let content = std::fs::read_to_string(&skill_file)
74 .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", skill_file.display()))?;
75 match parse_skill_md(&content) {
76 Ok(skill) => registry.add(skill),
77 Err(e) => {
78 tracing::warn!("failed to parse {}: {e}", skill_file.display());
79 }
80 }
81 } else {
82 scan_skills(&entry_path, registry)?;
83 }
84 }
85
86 Ok(())
87}