use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::AgentError;
#[derive(Debug, Clone)]
pub struct SkillMetadata {
pub name: String,
pub description: String,
pub allowed_tools: Option<Vec<String>>,
pub argument_hint: Option<String>,
pub when_to_use: Option<String>,
pub user_invocable: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct LoadedSkill {
pub metadata: SkillMetadata,
pub content: String,
pub base_dir: String,
}
fn parse_frontmatter(content: &str) -> (HashMap<String, String>, String) {
let mut fields = HashMap::new();
let trimmed = content.trim();
if !trimmed.starts_with("---") {
return (fields, content.to_string());
}
if let Some(end_pos) = trimmed[3..].find("---") {
let frontmatter = &trimmed[3..end_pos + 3];
for line in frontmatter.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(colon_pos) = line.find(':') {
let key = line[..colon_pos].trim().to_string();
let value = line[colon_pos + 1..].trim().to_string();
fields.insert(key, value);
}
}
let body = trimmed[end_pos + 6..].trim_start().to_string();
return (fields, body);
}
(fields, content.to_string())
}
pub fn load_skill_from_dir(dir_path: &Path) -> Result<LoadedSkill, AgentError> {
let skill_file = dir_path.join("SKILL.md");
if !skill_file.exists() {
return Err(AgentError::Skill(format!("SKILL.md not found in {}", dir_path.display())));
}
let content = fs::read_to_string(&skill_file)
.map_err(|e| AgentError::Io(e))?;
let (fields, body) = parse_frontmatter(&content);
let name = dir_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let description = fields.get("description")
.cloned()
.unwrap_or_default();
let allowed_tools = fields.get("allowed-tools")
.map(|s| s.split(',').map(|x| x.trim().to_string()).collect());
let argument_hint = fields.get("argument-hint").cloned();
let when_to_use = fields.get("when_to_use").cloned();
let user_invocable = fields.get("user-invocable")
.and_then(|v| match v.as_str() {
"true" | "1" => Some(true),
"false" | "0" => Some(false),
_ => None,
});
let metadata = SkillMetadata {
name,
description,
allowed_tools,
argument_hint,
when_to_use,
user_invocable,
};
Ok(LoadedSkill {
metadata,
content: body,
base_dir: dir_path.to_string_lossy().to_string(),
})
}
pub fn load_skills_from_dir(base_path: &Path) -> Result<Vec<LoadedSkill>, AgentError> {
if !base_path.exists() {
return Ok(Vec::new());
}
let mut skills = Vec::new();
let entries = fs::read_dir(base_path)
.map_err(|e| AgentError::Io(e))?;
for entry in entries {
let entry = entry.map_err(|e| AgentError::Io(e))?;
let path = entry.path();
if path.is_dir() {
if let Ok(skill) = load_skill_from_dir(&path) {
skills.push(skill);
}
}
}
Ok(skills)
}