use std::path::{Path, PathBuf};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SkillMeta {
pub name: String,
pub description: String,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Skill {
pub meta: SkillMeta,
pub instructions: String,
pub schemas: HashMap<String, String>,
pub references: HashMap<String, String>,
}
pub struct SkillRegistry {
search_paths: Vec<PathBuf>,
discovered: Vec<SkillMeta>,
loaded: Vec<Skill>,
}
impl SkillRegistry {
pub fn new() -> Self {
Self {
search_paths: Vec::new(),
discovered: Vec::new(),
loaded: Vec::new(),
}
}
pub fn with_defaults() -> Self {
let mut reg = Self::new();
reg.add_default_paths();
reg
}
pub fn add_default_paths(&mut self) {
if let Ok(cwd) = std::env::current_dir() {
self.search_paths.push(cwd.join(".llama-agent").join("skills"));
self.search_paths.push(cwd.join(".agents").join("skills"));
}
if let Some(home) = dirs::home_dir() {
self.search_paths.push(home.join(".llama-agent").join("skills"));
self.search_paths.push(home.join(".agents").join("skills"));
}
}
pub fn add_search_path(&mut self, path: PathBuf) {
self.search_paths.push(path);
}
pub fn discover(&mut self) {
self.discovered.clear();
for search_path in &self.search_paths {
if !search_path.is_dir() {
continue;
}
if let Ok(entries) = std::fs::read_dir(search_path) {
for entry in entries.flatten() {
let skill_dir = entry.path();
if skill_dir.is_dir() {
let skill_md = skill_dir.join("SKILL.md");
if skill_md.is_file() {
if let Some(meta) = parse_skill_frontmatter(&skill_md, &skill_dir) {
if !self.discovered.iter().any(|s| s.name == meta.name) {
self.discovered.push(meta);
}
}
}
}
}
}
}
}
pub fn discovered(&self) -> &[SkillMeta] {
&self.discovered
}
pub fn load(&mut self, name: &str) -> Option<&Skill> {
if let Some(idx) = self.loaded.iter().position(|s| s.meta.name == name) {
return Some(&self.loaded[idx]);
}
let meta = self.discovered.iter().find(|s| s.name == name)?.clone();
let skill_md = meta.path.join("SKILL.md");
let content = std::fs::read_to_string(&skill_md).ok()?;
let instructions = strip_frontmatter(&content);
let mut schemas = HashMap::new();
let schemas_dir = meta.path.join("schemas");
if schemas_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(schemas_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let (
Some(name),
Ok(content),
) = (
path.file_name().and_then(|s| s.to_str()),
std::fs::read_to_string(&path),
) {
schemas.insert(name.to_string(), content);
}
}
}
}
}
let mut references = HashMap::new();
let refs_dir = meta.path.join("references");
if refs_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(refs_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("md") {
if let (
Some(name),
Ok(content),
) = (
path.file_name().and_then(|s| s.to_str()),
std::fs::read_to_string(&path),
) {
references.insert(name.to_string(), content);
}
}
}
}
}
let skill = Skill {
meta,
instructions,
schemas,
references,
};
self.loaded.push(skill);
self.loaded.last()
}
pub fn load_all(&mut self) {
let names: Vec<String> = self.discovered.iter().map(|s| s.name.clone()).collect();
for name in names {
self.load(&name);
}
}
pub fn loaded(&self) -> &[Skill] {
&self.loaded
}
pub fn skills_summary_prompt(&self) -> String {
if self.discovered.is_empty() {
return String::new();
}
let mut lines = Vec::new();
lines.push("# Available Skills".to_string());
lines.push("The following skills are available. They will be activated when relevant:\n".to_string());
for skill in &self.discovered {
lines.push(format!("- **{}**: {}", skill.name, skill.description));
}
lines.push(String::new());
lines.join("\n")
}
pub fn loaded_skills_prompt(&self) -> String {
if self.loaded.is_empty() {
return String::new();
}
let mut lines = Vec::new();
lines.push("# Active Skill Details\n".to_string());
for skill in &self.loaded {
lines.push(format!("## Skill: {}\n", skill.meta.name));
lines.push(skill.instructions.clone());
lines.push(String::new());
if !skill.schemas.is_empty() {
lines.push("### Required Output Schemas".to_string());
lines.push("When generating JSON, you MUST adhere to these schemas:".to_string());
for (name, content) in &skill.schemas {
lines.push(format!("\n#### Schema: {}\n```json\n{}\n```", name, content));
}
lines.push(String::new());
}
if !skill.references.is_empty() {
lines.push("### References & Guidelines".to_string());
for (name, content) in &skill.references {
lines.push(format!("\n#### Reference: {}\n{}", name, content));
}
lines.push(String::new());
}
}
lines.join("\n")
}
}
impl Default for SkillRegistry {
fn default() -> Self {
Self::new()
}
}
fn parse_skill_frontmatter(skill_md: &Path, skill_dir: &Path) -> Option<SkillMeta> {
let content = std::fs::read_to_string(skill_md).ok()?;
let content = content.trim();
if !content.starts_with("---") {
return None;
}
let rest = &content[3..];
let end = rest.find("---")?;
let frontmatter = &rest[..end];
let mut name = None;
let mut description = None;
for line in frontmatter.lines() {
let line = line.trim();
if let Some(val) = line.strip_prefix("name:") {
name = Some(val.trim().trim_matches('"').trim_matches('\'').to_string());
} else if let Some(val) = line.strip_prefix("description:") {
description = Some(val.trim().trim_matches('"').trim_matches('\'').to_string());
}
}
Some(SkillMeta {
name: name?,
description: description.unwrap_or_default(),
path: skill_dir.to_path_buf(),
})
}
fn strip_frontmatter(content: &str) -> String {
let content = content.trim();
if !content.starts_with("---") {
return content.to_string();
}
let rest = &content[3..];
if let Some(end) = rest.find("---") {
rest[end + 3..].trim().to_string()
} else {
content.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_frontmatter() {
let content = "---\nname: test\ndescription: A test skill\n---\n\n# Instructions\nDo something.";
let body = strip_frontmatter(content);
assert!(body.starts_with("# Instructions"));
assert!(body.contains("Do something."));
}
#[test]
fn test_strip_frontmatter_no_frontmatter() {
let content = "Just regular markdown.";
assert_eq!(strip_frontmatter(content), content);
}
}