use crate::{Result, Skill, SkillError, SkillIndex, SkillLoader, estimate_tokens};
use std::collections::HashMap;
use std::path::PathBuf;
pub struct SkillManager {
loader: SkillLoader,
active_skills: HashMap<String, Skill>,
skill_index: Vec<SkillIndex>,
}
impl SkillManager {
pub fn new(loader: SkillLoader) -> Self {
Self {
loader,
active_skills: HashMap::new(),
skill_index: Vec::new(),
}
}
pub fn get_index(&mut self) -> &[SkillIndex] {
if self.skill_index.is_empty() && !self.loader.skills.is_empty() {
self.skill_index = self
.loader
.skills
.iter()
.map(|s| {
let level0_text = format!("{}: {}", s.name, s.description);
let estimated_tokens = estimate_tokens(&level0_text);
SkillIndex {
name: s.name.clone(),
description: s.description.clone(),
triggers: s.triggers.clone(),
estimated_tokens,
}
})
.collect();
}
&self.skill_index
}
pub fn get_index_tokens(&mut self) -> usize {
self.get_index().iter().map(|e| e.estimated_tokens).sum()
}
pub fn load_skill(&mut self, name: &str) -> Result<&Skill> {
if self.active_skills.contains_key(name) {
return Ok(self.active_skills.get(name).expect("key exists"));
}
let skill = self
.loader
.skills
.iter()
.find(|s| s.name == name)
.ok_or_else(|| {
SkillError::FileNotFound(PathBuf::from(format!("skill '{name}' not found")))
})?;
let skill = skill.clone();
self.active_skills.insert(name.to_string(), skill);
Ok(self.active_skills.get(name).expect("key just inserted"))
}
pub fn load_reference(&self, skill_name: &str, file_path: &str) -> Result<String> {
let skill = self.active_skills.get(skill_name).ok_or_else(|| {
SkillError::FileNotFound(PathBuf::from(format!(
"skill '{skill_name}' not loaded (call load_skill first)"
)))
})?;
let skill_dir = skill.source_path.parent().ok_or_else(|| {
SkillError::InvalidFrontmatter("skill has no parent directory".into())
})?;
let ref_path = skill_dir.join(file_path);
if !ref_path.exists() {
return Err(SkillError::FileNotFound(ref_path));
}
std::fs::read_to_string(&ref_path).map_err(SkillError::IoError)
}
pub fn match_skill(&self, task_description: &str) -> Option<String> {
let task_lower = task_description.to_lowercase();
self.loader
.skills
.iter()
.find(|skill| {
skill.triggers.iter().any(|trigger| {
let trigger_lower = trigger.to_lowercase();
task_lower.contains(&trigger_lower)
})
})
.map(|s| s.name.clone())
}
pub fn unload_skill(&mut self, name: &str) {
self.active_skills.remove(name);
}
pub fn get_active_skills(&self) -> Vec<&Skill> {
self.active_skills.values().collect()
}
}