use std::collections::{HashMap, HashSet};
use crate::skill::Skill;
pub struct SkillRegistry {
skills: HashMap<String, Skill>,
trigger_map: HashMap<String, String>,
}
impl SkillRegistry {
pub fn new(skills: Vec<Skill>, available_tools: &[&str]) -> Self {
let mut skill_map = HashMap::new();
let mut trigger_map = HashMap::new();
let tool_set: HashSet<&str> = available_tools.iter().copied().collect();
for skill in skills {
for required in &skill.frontmatter.tools_required {
if !tool_set.contains(required.as_str()) {
tracing::warn!(
"Skill '{}' requires tool '{}' but that tool is not enabled",
skill.frontmatter.name,
required
);
}
}
for trigger in &skill.frontmatter.triggers {
if let Some(existing_name) = trigger_map.get(trigger) {
tracing::warn!(
"Skill trigger '{}' is already used by skill '{}', now being overwritten by '{}'",
trigger,
existing_name,
skill.frontmatter.name
);
}
trigger_map.insert(trigger.clone(), skill.frontmatter.name.clone());
}
skill_map.insert(skill.frontmatter.name.clone(), skill);
}
Self {
skills: skill_map,
trigger_map,
}
}
pub fn get(&self, name: &str) -> Option<&Skill> {
self.skills.get(name)
}
pub fn match_trigger(&self, input: &str) -> Option<(&Skill, String)> {
let mut best_trigger: Option<&str> = None;
for trigger in self.trigger_map.keys() {
if input.starts_with(trigger.as_str()) {
let rest = &input[trigger.len()..];
if rest.is_empty() || rest.starts_with(' ') {
if best_trigger.is_none() || trigger.len() > best_trigger.unwrap().len() {
best_trigger = Some(trigger);
}
}
}
}
best_trigger.and_then(|trigger| {
let skill_name = self.trigger_map.get(trigger)?;
let skill = self.skills.get(skill_name.as_str())?;
let args = input[trigger.len()..].trim().to_string();
Some((skill, args))
})
}
pub fn skill_descriptions(&self) -> Vec<(&str, &str)> {
self.skills
.values()
.map(|s| {
(
s.frontmatter.name.as_str(),
s.frontmatter.description.as_str(),
)
})
.collect()
}
pub fn skill_names(&self) -> Vec<&str> {
self.skills.keys().map(|s| s.as_str()).collect()
}
pub fn count(&self) -> usize {
self.skills.len()
}
pub fn skills(&self) -> Vec<&Skill> {
self.skills.values().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skill::SkillFrontmatter;
use std::path::PathBuf;
fn make_skill(name: &str, triggers: Vec<&str>) -> Skill {
Skill {
frontmatter: SkillFrontmatter {
name: name.to_string(),
description: format!("Description for {}", name),
version: "1.0.0".to_string(),
triggers: triggers.into_iter().map(String::from).collect(),
tools_required: vec![],
enabled: true,
},
content: format!("Content for {}", name),
source_path: PathBuf::from("test.md"),
}
}
#[test]
fn test_match_trigger_exact() {
let skills = vec![
make_skill("code-review", vec!["/review"]),
make_skill("refactor", vec!["/refactor"]),
];
let registry = SkillRegistry::new(skills, &[]);
let (skill, args) = registry.match_trigger("/review").unwrap();
assert_eq!(skill.frontmatter.name, "code-review");
assert_eq!(args, "");
}
#[test]
fn test_match_trigger_with_args() {
let skills = vec![make_skill("code-review", vec!["/review"])];
let registry = SkillRegistry::new(skills, &[]);
let (skill, args) = registry.match_trigger("/review src/main.rs").unwrap();
assert_eq!(skill.frontmatter.name, "code-review");
assert_eq!(args, "src/main.rs");
}
#[test]
fn test_match_trigger_longest_match() {
let skills = vec![
make_skill("review", vec!["/review"]),
make_skill("review-quick", vec!["/review-quick"]),
];
let registry = SkillRegistry::new(skills, &[]);
let (skill, _) = registry.match_trigger("/review-quick src/main.rs").unwrap();
assert_eq!(skill.frontmatter.name, "review-quick");
}
#[test]
fn test_match_trigger_partial_not_matched() {
let skills = vec![make_skill("review", vec!["/review"])];
let registry = SkillRegistry::new(skills, &[]);
assert!(registry.match_trigger("/reviews").is_none());
}
#[test]
fn test_match_trigger_no_match() {
let skills = vec![make_skill("review", vec!["/review"])];
let registry = SkillRegistry::new(skills, &[]);
assert!(registry.match_trigger("/unknown").is_none());
assert!(registry.match_trigger("plain text").is_none());
}
#[test]
fn test_skill_descriptions() {
let skills = vec![
make_skill("review", vec!["/review"]),
make_skill("refactor", vec!["/refactor"]),
];
let registry = SkillRegistry::new(skills, &["read", "bash"]);
let descs = registry.skill_descriptions();
assert_eq!(descs.len(), 2);
assert!(descs.iter().any(|(n, d)| *n == "review" && d.contains("review")));
}
#[test]
fn test_warns_missing_tools() {
let skills = vec![make_skill("review", vec!["/review"])];
let mut front = skills[0].frontmatter.clone();
front.tools_required = vec!["bash".to_string(), "nonexistent_tool".to_string()];
let skill = Skill {
frontmatter: front,
..skills[0].clone()
};
let _registry = SkillRegistry::new(vec![skill], &["bash", "read"]);
}
}