pub mod types;
pub use types::*;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
fn parse_frontmatter(content: &str) -> (HashMap<String, String>, String) {
let frontmatter_regex =
regex::Regex::new(r"^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$").expect("Invalid regex");
if let Some(captures) = frontmatter_regex.captures(content) {
let yaml_content = captures.get(1).map(|m| m.as_str()).unwrap_or("");
let body = captures.get(2).map(|m| m.as_str()).unwrap_or("");
let mut data = HashMap::new();
for line in yaml_content.lines() {
if let Some(colon_index) = line.find(':') {
let key = line[..colon_index].trim().to_string();
let mut value = line[colon_index + 1..].trim().to_string();
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
value = value[1..value.len() - 1].to_string();
}
data.insert(key, value);
}
}
(data, body.to_string())
} else {
(HashMap::new(), content.to_string())
}
}
fn load_skill_from_file(skill_path: &Path, skill_name: &str) -> Option<BuiltinSkill> {
let content = fs::read_to_string(skill_path).ok()?;
let (data, body) = parse_frontmatter(&content);
Some(BuiltinSkill {
name: data
.get("name")
.cloned()
.unwrap_or_else(|| skill_name.to_string()),
description: data.get("description").cloned().unwrap_or_default(),
template: body.trim().to_string(),
license: None,
compatibility: None,
metadata: None,
allowed_tools: None,
agent: data.get("agent").cloned(),
model: data.get("model").cloned(),
subtask: None,
argument_hint: data.get("argument-hint").cloned(),
mcp_config: None,
})
}
fn load_skills_from_directory(skills_dir: &Path) -> Vec<BuiltinSkill> {
if !skills_dir.exists() {
return vec![];
}
let mut skills = vec![];
if let Ok(entries) = fs::read_dir(skills_dir) {
for entry in entries.flatten() {
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
let skill_path = entry.path().join("SKILL.md");
if skill_path.exists() {
if let Some(skill_name) = entry.file_name().to_str() {
if let Some(skill) = load_skill_from_file(&skill_path, skill_name) {
skills.push(skill);
}
}
}
}
}
}
skills
}
fn get_skills_dir() -> PathBuf {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let candidates = [
current_dir.join("skills"),
current_dir.join("../skills"),
current_dir.join("../../skills"),
current_dir.join("../../../skills"),
];
for candidate in &candidates {
if candidate.exists() {
return candidate.clone();
}
}
current_dir.join("skills")
}
use std::sync::OnceLock;
static CACHED_SKILLS: OnceLock<Vec<BuiltinSkill>> = OnceLock::new();
pub fn create_builtin_skills() -> Vec<BuiltinSkill> {
CACHED_SKILLS
.get_or_init(|| {
let skills_dir = get_skills_dir();
load_skills_from_directory(&skills_dir)
})
.clone()
}
pub fn get_builtin_skill(name: &str) -> Option<BuiltinSkill> {
let skills = create_builtin_skills();
skills
.into_iter()
.find(|s| s.name.to_lowercase() == name.to_lowercase())
}
pub fn list_builtin_skill_names() -> Vec<String> {
create_builtin_skills()
.into_iter()
.map(|s| s.name)
.collect()
}
pub fn clear_skills_cache() {
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_frontmatter() {
let content = r#"---
name: test-skill
description: A test skill
model: claude-3-opus
---
This is the template content"#;
let (data, body) = parse_frontmatter(content);
assert_eq!(data.get("name"), Some(&"test-skill".to_string()));
assert_eq!(data.get("description"), Some(&"A test skill".to_string()));
assert_eq!(data.get("model"), Some(&"claude-3-opus".to_string()));
assert_eq!(body.trim(), "This is the template content");
}
#[test]
fn test_parse_frontmatter_no_frontmatter() {
let content = "Just plain content";
let (data, body) = parse_frontmatter(content);
assert!(data.is_empty());
assert_eq!(body, content);
}
#[test]
fn test_list_builtin_skill_names_does_not_panic() {
let names = list_builtin_skill_names();
for name in &names {
assert!(!name.is_empty(), "Skill names should not be empty");
}
}
}