use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct SkillResources {
pub scripts: Vec<PathBuf>,
pub references: Vec<PathBuf>,
pub assets: Vec<PathBuf>,
}
impl SkillResources {
pub fn is_empty(&self) -> bool {
self.scripts.is_empty() && self.references.is_empty() && self.assets.is_empty()
}
pub fn total_count(&self) -> usize {
self.scripts.len() + self.references.len() + self.assets.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SkillSource {
#[default]
Personal,
Project,
Builtin,
}
impl std::fmt::Display for SkillSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SkillSource::Personal => write!(f, "personal"),
SkillSource::Project => write!(f, "project"),
SkillSource::Builtin => write!(f, "builtin"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SkillExecutionMode {
#[default]
Inline,
Subagent,
Script,
}
impl SkillExecutionMode {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"subagent" => SkillExecutionMode::Subagent,
"script" => SkillExecutionMode::Script,
_ => SkillExecutionMode::Inline,
}
}
}
impl std::fmt::Display for SkillExecutionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SkillExecutionMode::Inline => write!(f, "inline"),
SkillExecutionMode::Subagent => write!(f, "subagent"),
SkillExecutionMode::Script => write!(f, "script"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillMetadata {
pub name: String,
pub description: String,
#[serde(rename = "allowed-tools")]
pub allowed_tools: Option<Vec<String>>,
pub license: Option<String>,
pub compatibility: Option<String>,
pub model: Option<String>,
pub metadata: Option<HashMap<String, String>>,
#[serde(default)]
pub hooks: Option<Vec<String>>,
#[serde(skip)]
pub source: SkillSource,
#[serde(skip)]
pub source_path: PathBuf,
#[serde(skip)]
pub resources_dir: Option<PathBuf>,
}
impl SkillMetadata {
pub fn new(name: String, description: String) -> Self {
Self {
name,
description,
allowed_tools: None,
license: None,
compatibility: None,
model: None,
metadata: None,
hooks: None,
source: SkillSource::Personal,
source_path: PathBuf::new(),
resources_dir: None,
}
}
pub fn with_source(mut self, source: SkillSource) -> Self {
self.source = source;
self
}
pub fn with_source_path(mut self, path: PathBuf) -> Self {
self.source_path = path;
self
}
pub fn execution_mode(&self) -> SkillExecutionMode {
self.metadata
.as_ref()
.and_then(|m| m.get("execution"))
.map(|e| SkillExecutionMode::parse(e))
.unwrap_or_default()
}
pub fn get_metadata(&self, key: &str) -> Option<&String> {
self.metadata.as_ref().and_then(|m| m.get(key))
}
pub fn has_tool_restrictions(&self) -> bool {
self.allowed_tools
.as_ref()
.map(|t| !t.is_empty())
.unwrap_or(false)
}
pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
match &self.allowed_tools {
Some(allowed) => allowed.iter().any(|t| t == tool_name),
None => true, }
}
}
#[derive(Debug, Clone)]
pub struct Skill {
pub metadata: SkillMetadata,
pub instructions: String,
pub execution_mode: SkillExecutionMode,
}
impl Skill {
pub fn new(metadata: SkillMetadata, instructions: String) -> Self {
let execution_mode = metadata.execution_mode();
Self {
metadata,
instructions,
execution_mode,
}
}
pub fn name(&self) -> &str {
&self.metadata.name
}
pub fn description(&self) -> &str {
&self.metadata.description
}
pub fn allowed_tools(&self) -> Option<&Vec<String>> {
self.metadata.allowed_tools.as_ref()
}
pub fn model(&self) -> Option<&String> {
self.metadata.model.as_ref()
}
pub fn runs_as_subagent(&self) -> bool {
matches!(self.execution_mode, SkillExecutionMode::Subagent)
}
pub fn is_script(&self) -> bool {
matches!(self.execution_mode, SkillExecutionMode::Script)
}
}
#[derive(Debug, Clone)]
pub enum SkillResult {
Inline {
instructions: String,
model_override: Option<String>,
},
Subagent {
agent_id: String,
},
Script {
output: String,
is_error: bool,
},
}
impl SkillResult {
pub fn inline(instructions: String, model_override: Option<String>) -> Self {
SkillResult::Inline {
instructions,
model_override,
}
}
pub fn subagent(agent_id: String) -> Self {
SkillResult::Subagent { agent_id }
}
pub fn script(output: String, is_error: bool) -> Self {
SkillResult::Script { output, is_error }
}
pub fn is_error(&self) -> bool {
matches!(self, SkillResult::Script { is_error: true, .. })
}
}
#[derive(Debug, Clone)]
pub struct SkillMatch {
pub skill_name: String,
pub confidence: f32,
pub source: MatchSource,
}
impl SkillMatch {
pub fn new(skill_name: String, confidence: f32, source: MatchSource) -> Self {
Self {
skill_name,
confidence,
source,
}
}
pub fn semantic(skill_name: String, confidence: f32) -> Self {
Self::new(skill_name, confidence, MatchSource::Semantic)
}
pub fn keyword(skill_name: String, confidence: f32) -> Self {
Self::new(skill_name, confidence, MatchSource::Keyword)
}
pub fn explicit(skill_name: String) -> Self {
Self::new(skill_name, 1.0, MatchSource::Explicit)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchSource {
Semantic,
Keyword,
Explicit,
}
impl std::fmt::Display for MatchSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchSource::Semantic => write!(f, "semantic"),
MatchSource::Keyword => write!(f, "keyword"),
MatchSource::Explicit => write!(f, "explicit"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skill_source_display() {
assert_eq!(SkillSource::Personal.to_string(), "personal");
assert_eq!(SkillSource::Project.to_string(), "project");
assert_eq!(SkillSource::Builtin.to_string(), "builtin");
}
#[test]
fn test_execution_mode_from_str() {
assert_eq!(
SkillExecutionMode::parse("inline"),
SkillExecutionMode::Inline
);
assert_eq!(
SkillExecutionMode::parse("subagent"),
SkillExecutionMode::Subagent
);
assert_eq!(
SkillExecutionMode::parse("script"),
SkillExecutionMode::Script
);
assert_eq!(
SkillExecutionMode::parse("SUBAGENT"),
SkillExecutionMode::Subagent
);
assert_eq!(
SkillExecutionMode::parse("unknown"),
SkillExecutionMode::Inline
);
}
#[test]
fn test_skill_metadata_creation() {
let metadata = SkillMetadata::new(
"test-skill".to_string(),
"A test skill for unit testing".to_string(),
);
assert_eq!(metadata.name, "test-skill");
assert_eq!(metadata.description, "A test skill for unit testing");
assert!(metadata.allowed_tools.is_none());
assert!(metadata.license.is_none());
assert!(metadata.model.is_none());
assert_eq!(metadata.source, SkillSource::Personal);
}
#[test]
fn test_skill_metadata_with_source() {
let metadata = SkillMetadata::new("test".to_string(), "desc".to_string())
.with_source(SkillSource::Project);
assert_eq!(metadata.source, SkillSource::Project);
}
#[test]
fn test_skill_metadata_tool_restrictions() {
let mut metadata = SkillMetadata::new("test".to_string(), "desc".to_string());
assert!(!metadata.has_tool_restrictions());
assert!(metadata.is_tool_allowed("any_tool"));
metadata.allowed_tools = Some(vec!["Read".to_string(), "Grep".to_string()]);
assert!(metadata.has_tool_restrictions());
assert!(metadata.is_tool_allowed("Read"));
assert!(metadata.is_tool_allowed("Grep"));
assert!(!metadata.is_tool_allowed("Write"));
}
#[test]
fn test_skill_metadata_execution_mode() {
let mut metadata = SkillMetadata::new("test".to_string(), "desc".to_string());
assert_eq!(metadata.execution_mode(), SkillExecutionMode::Inline);
let mut custom_metadata = HashMap::new();
custom_metadata.insert("execution".to_string(), "subagent".to_string());
metadata.metadata = Some(custom_metadata);
assert_eq!(metadata.execution_mode(), SkillExecutionMode::Subagent);
}
#[test]
fn test_skill_creation() {
let metadata =
SkillMetadata::new("review-pr".to_string(), "Reviews pull requests".to_string());
let skill = Skill::new(
metadata,
"# Review Instructions\n\nDo the review.".to_string(),
);
assert_eq!(skill.name(), "review-pr");
assert_eq!(skill.description(), "Reviews pull requests");
assert!(skill.instructions.contains("Review Instructions"));
assert_eq!(skill.execution_mode, SkillExecutionMode::Inline);
}
#[test]
fn test_skill_result_types() {
let inline = SkillResult::inline("instructions".to_string(), None);
assert!(!inline.is_error());
let subagent = SkillResult::subagent("agent-123".to_string());
assert!(!subagent.is_error());
let script_ok = SkillResult::script("output".to_string(), false);
assert!(!script_ok.is_error());
let script_err = SkillResult::script("error".to_string(), true);
assert!(script_err.is_error());
}
#[test]
fn test_skill_match() {
let semantic = SkillMatch::semantic("review-pr".to_string(), 0.85);
assert_eq!(semantic.source, MatchSource::Semantic);
assert_eq!(semantic.confidence, 0.85);
let keyword = SkillMatch::keyword("commit".to_string(), 0.6);
assert_eq!(keyword.source, MatchSource::Keyword);
let explicit = SkillMatch::explicit("explain-code".to_string());
assert_eq!(explicit.source, MatchSource::Explicit);
assert_eq!(explicit.confidence, 1.0);
}
}