mod builtin;
pub mod feedback;
mod manage;
pub mod preprocessor;
mod registry;
pub mod validator;
pub use builtin::builtin_skills;
pub use feedback::{DefaultSkillScorer, SkillFeedback, SkillOutcome, SkillScore, SkillScorer};
pub use manage::ManageSkillTool;
pub use registry::SkillRegistry;
pub use validator::{
DefaultSkillValidator, SkillValidationError, SkillValidator, ValidationErrorKind,
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SkillKind {
#[default]
Instruction,
Persona,
Tool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ToolPermission {
pub tool: String,
pub pattern: String,
}
impl ToolPermission {
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
let open = s.find('(')?;
let close = s.rfind(')')?;
if close <= open {
return None;
}
let tool = s[..open].trim().to_string();
let pattern = s[open + 1..close].trim().to_string();
Some(ToolPermission { tool, pattern })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default, rename = "allowed-tools")]
pub allowed_tools: Option<String>,
#[serde(default, rename = "disable-model-invocation")]
pub disable_model_invocation: bool,
#[serde(default)]
pub kind: SkillKind,
#[serde(skip)]
pub content: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub version: Option<String>,
}
impl Skill {
pub fn parse(content: &str) -> Option<Self> {
let parts: Vec<&str> = content.splitn(3, "---").collect();
if parts.len() < 3 {
return None;
}
let frontmatter = parts[1].trim();
let body = parts[2].trim();
let mut skill: Skill = serde_yaml::from_str(frontmatter).ok()?;
skill.content = body.to_string();
Some(skill)
}
pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path.as_ref())?;
let mut skill =
Self::parse(&content).ok_or_else(|| anyhow::anyhow!("Failed to parse skill file"))?;
if skill.name.is_empty() {
if let Some(stem) = path.as_ref().file_stem() {
skill.name = stem.to_string_lossy().to_string();
}
}
Ok(skill)
}
pub fn parse_allowed_tools(&self) -> HashSet<ToolPermission> {
let mut permissions = HashSet::new();
let Some(allowed) = &self.allowed_tools else {
return permissions;
};
for part in allowed.split(',') {
let part = part.trim();
if let Some(perm) = ToolPermission::parse(part) {
permissions.insert(perm);
}
}
permissions
}
pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
let permissions = self.parse_allowed_tools();
if permissions.is_empty() {
return true;
}
permissions
.iter()
.any(|perm| perm.tool.eq_ignore_ascii_case(tool_name) && perm.pattern == "*")
}
pub fn to_system_prompt(&self) -> String {
format!(
"# Skill: {}\n\n{}\n\n{}",
self.name, self.description, self.content
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_skill() {
let content = r#"---
name: test-skill
description: A test skill
allowed-tools: "read(*), grep(*)"
kind: instruction
---
# Instructions
You are a test assistant.
"#;
let skill = Skill::parse(content).unwrap();
assert_eq!(skill.name, "test-skill");
assert_eq!(skill.description, "A test skill");
assert_eq!(skill.kind, SkillKind::Instruction);
assert!(skill.content.contains("You are a test assistant"));
}
#[test]
fn test_parse_tool_permission() {
let perm = ToolPermission::parse("Bash(gh issue view:*)").unwrap();
assert_eq!(perm.tool, "Bash");
assert_eq!(perm.pattern, "gh issue view:*");
let perm = ToolPermission::parse("read(*)").unwrap();
assert_eq!(perm.tool, "read");
assert_eq!(perm.pattern, "*");
}
#[test]
fn test_parse_allowed_tools() {
let skill = Skill {
name: "test".to_string(),
description: "test".to_string(),
allowed_tools: Some("read(*), grep(*), Bash(gh:*)".to_string()),
disable_model_invocation: false,
kind: SkillKind::Instruction,
content: String::new(),
tags: Vec::new(),
version: None,
};
let permissions = skill.parse_allowed_tools();
assert_eq!(permissions.len(), 3);
}
#[test]
fn test_is_tool_allowed() {
let skill = Skill {
name: "test".to_string(),
description: "test".to_string(),
allowed_tools: Some("read(*), grep(*)".to_string()),
disable_model_invocation: false,
kind: SkillKind::Instruction,
content: String::new(),
tags: Vec::new(),
version: None,
};
assert!(skill.is_tool_allowed("read"));
assert!(skill.is_tool_allowed("grep"));
assert!(!skill.is_tool_allowed("write"));
}
}