Skip to main content

crabtalk_daemon/hook/skill/
handler.rs

1//! Crabtalk skill handler — initial load from disk.
2
3use crate::hook::skill::{SkillRegistry, loader};
4use anyhow::Result;
5use std::path::PathBuf;
6use tokio::sync::Mutex;
7
8/// Skill registry owner.
9///
10/// Implements [`Hook`] — `on_build_agent` enriches the system prompt with
11/// matching skills based on agent tags. Tools and dispatch are no-ops
12/// (skills inject behavior via prompt, not via tools).
13pub struct SkillHandler {
14    /// The skill registry (Mutex for interior-mutability from `dispatch_load_skill`).
15    pub registry: Mutex<SkillRegistry>,
16    /// Skill directories to search (local first, then packages).
17    pub skill_dirs: Vec<PathBuf>,
18}
19
20impl Default for SkillHandler {
21    fn default() -> Self {
22        Self {
23            registry: Mutex::new(SkillRegistry::new()),
24            skill_dirs: Vec::new(),
25        }
26    }
27}
28
29impl SkillHandler {
30    /// Load skills from multiple directories. Tolerates missing directories
31    /// by skipping them. Returns a handler with all discovered skills.
32    pub fn load(skill_dirs: Vec<PathBuf>) -> Result<Self> {
33        let mut registry = SkillRegistry::new();
34        for dir in &skill_dirs {
35            if !dir.exists() {
36                continue;
37            }
38            match loader::load_skills_dir(dir) {
39                Ok(r) => {
40                    let count = r.len();
41                    for skill in r.skills() {
42                        if registry.contains(&skill.name) {
43                            tracing::warn!(
44                                "skill '{}' from {} conflicts with already-loaded skill, skipping",
45                                skill.name,
46                                dir.display()
47                            );
48                        } else {
49                            registry.add(skill.clone());
50                        }
51                    }
52                    tracing::info!("loaded {count} skill(s) from {}", dir.display());
53                }
54                Err(e) => {
55                    tracing::warn!("could not load skills from {}: {e}", dir.display());
56                }
57            }
58        }
59        tracing::info!("total {} skill(s) loaded", registry.len());
60        Ok(Self {
61            registry: Mutex::new(registry),
62            skill_dirs,
63        })
64    }
65}