1use std::collections::HashMap;
15use std::path::PathBuf;
16
17use serde::{Deserialize, Serialize};
18
19#[non_exhaustive]
21#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
22pub enum SkillFormat {
23 #[default]
25 Legacy,
26 SkillMd,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SkillDefinition {
37 pub name: String,
39
40 pub description: String,
42
43 #[serde(default)]
45 pub version: String,
46
47 #[serde(default)]
49 pub variables: Vec<String>,
50
51 #[serde(default)]
53 pub argument_hint: Option<String>,
54
55 #[serde(default)]
57 pub allowed_tools: Vec<String>,
58
59 #[serde(default)]
61 pub user_invocable: bool,
62
63 #[serde(default)]
65 pub disable_model_invocation: bool,
66
67 #[serde(skip)]
69 pub instructions: String,
70
71 #[serde(skip)]
73 pub format: SkillFormat,
74
75 #[serde(skip)]
77 pub source_path: Option<PathBuf>,
78
79 #[serde(default, flatten)]
81 pub metadata: HashMap<String, serde_json::Value>,
82}
83
84impl SkillDefinition {
85 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
87 Self {
88 name: name.into(),
89 description: description.into(),
90 version: String::new(),
91 variables: Vec::new(),
92 argument_hint: None,
93 allowed_tools: Vec::new(),
94 user_invocable: false,
95 disable_model_invocation: false,
96 instructions: String::new(),
97 format: SkillFormat::default(),
98 source_path: None,
99 metadata: HashMap::new(),
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn skill_definition_new() {
110 let skill = SkillDefinition::new("test", "A test skill");
111 assert_eq!(skill.name, "test");
112 assert_eq!(skill.description, "A test skill");
113 assert!(skill.version.is_empty());
114 assert!(skill.variables.is_empty());
115 assert!(skill.instructions.is_empty());
116 assert_eq!(skill.format, SkillFormat::Legacy);
117 assert!(skill.source_path.is_none());
118 }
119
120 #[test]
121 fn skill_format_default() {
122 assert_eq!(SkillFormat::default(), SkillFormat::Legacy);
123 }
124
125 #[test]
126 fn skill_definition_serde_roundtrip() {
127 let mut skill = SkillDefinition::new("roundtrip", "Roundtrip test");
128 skill.version = "2.0.0".into();
129 skill.variables = vec!["var1".into(), "var2".into()];
130 skill.user_invocable = true;
131 skill.instructions = "These should be skipped".into();
132
133 let json = serde_json::to_string(&skill).unwrap();
134 let restored: SkillDefinition = serde_json::from_str(&json).unwrap();
135
136 assert_eq!(restored.name, "roundtrip");
137 assert_eq!(restored.version, "2.0.0");
138 assert_eq!(restored.variables, vec!["var1", "var2"]);
139 assert!(restored.user_invocable);
140 assert!(restored.instructions.is_empty());
142 assert_eq!(restored.format, SkillFormat::Legacy);
144 }
145
146 #[test]
147 fn skill_definition_from_json_with_extras() {
148 let json = r#"{
149 "name": "extra",
150 "description": "Has extra fields",
151 "custom_field": "custom_value",
152 "priority": 5
153 }"#;
154 let skill: SkillDefinition = serde_json::from_str(json).unwrap();
155 assert_eq!(skill.name, "extra");
156 assert_eq!(
157 skill.metadata.get("custom_field"),
158 Some(&serde_json::json!("custom_value"))
159 );
160 assert_eq!(skill.metadata.get("priority"), Some(&serde_json::json!(5)));
161 }
162}