thulp_skill_files/
frontmatter.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "kebab-case")]
14pub struct SkillFrontmatter {
15 #[serde(default)]
17 pub name: Option<String>,
18
19 #[serde(default)]
21 pub description: Option<String>,
22
23 #[serde(default)]
25 pub argument_hint: Option<String>,
26
27 #[serde(default)]
29 pub disable_model_invocation: bool,
30
31 #[serde(default = "default_true")]
33 pub user_invocable: bool,
34
35 #[serde(default)]
38 pub allowed_tools: Option<Vec<String>>,
39
40 #[serde(default)]
42 pub model: Option<String>,
43
44 #[serde(default)]
46 pub context: Option<SkillContext>,
47
48 #[serde(default)]
50 pub agent: Option<String>,
51
52 #[serde(default)]
54 pub hooks: Option<SkillHooks>,
55
56 #[serde(default)]
59 pub version: Option<String>,
60
61 #[serde(default)]
63 pub author: Option<String>,
64
65 #[serde(default)]
67 pub price: Option<PriceModel>,
68
69 #[serde(default)]
71 pub requires_approval: bool,
72
73 #[serde(default)]
75 pub tags: Vec<String>,
76
77 #[serde(default)]
79 pub min_thulp_version: Option<String>,
80}
81
82fn default_true() -> bool {
83 true
84}
85
86impl Default for SkillFrontmatter {
87 fn default() -> Self {
88 Self {
89 name: None,
90 description: None,
91 argument_hint: None,
92 disable_model_invocation: false,
93 user_invocable: true, allowed_tools: None,
95 model: None,
96 context: None,
97 agent: None,
98 hooks: None,
99 version: None,
100 author: None,
101 price: None,
102 requires_approval: false,
103 tags: Vec::new(),
104 min_thulp_version: None,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
111#[serde(rename_all = "lowercase")]
112pub enum SkillContext {
113 #[default]
115 Inline,
116 Fork,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122#[serde(untagged)]
123pub enum PriceModel {
124 Free,
126 PerCall(f64),
128 Subscription {
130 monthly: f64,
132 },
133 Custom(String),
135}
136
137#[allow(clippy::derivable_impls)]
139impl Default for PriceModel {
140 fn default() -> Self {
141 Self::Free
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
147pub struct SkillHooks {
148 #[serde(default)]
150 pub pre_execute: Option<String>,
151
152 #[serde(default)]
154 pub post_execute: Option<String>,
155
156 #[serde(default)]
158 pub on_error: Option<String>,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_default_frontmatter() {
167 let fm = SkillFrontmatter::default();
168 assert!(fm.name.is_none());
169 assert!(fm.user_invocable); assert!(!fm.disable_model_invocation);
171 assert!(!fm.requires_approval);
172 }
173
174 #[test]
175 fn test_parse_minimal_frontmatter() {
176 let yaml = r#"
177name: test-skill
178description: A test skill
179"#;
180 let fm: SkillFrontmatter = serde_yaml::from_str(yaml).unwrap();
181 assert_eq!(fm.name, Some("test-skill".to_string()));
182 assert_eq!(fm.description, Some("A test skill".to_string()));
183 }
184
185 #[test]
186 fn test_parse_full_frontmatter() {
187 let yaml = r#"
188name: advanced-skill
189description: An advanced skill with all options
190argument-hint: <file-path>
191disable-model-invocation: true
192user-invocable: false
193allowed-tools:
194 - Read
195 - Write
196 - Bash
197model: claude-sonnet-4-20250514
198context: fork
199agent: code-reviewer
200requires-approval: true
201tags:
202 - code
203 - review
204version: 1.2.3
205author: dirmacs
206"#;
207 let fm: SkillFrontmatter = serde_yaml::from_str(yaml).unwrap();
208 assert_eq!(fm.name, Some("advanced-skill".to_string()));
209 assert!(fm.disable_model_invocation);
210 assert!(!fm.user_invocable);
211 assert_eq!(
212 fm.allowed_tools,
213 Some(vec![
214 "Read".to_string(),
215 "Write".to_string(),
216 "Bash".to_string()
217 ])
218 );
219 assert_eq!(fm.context, Some(SkillContext::Fork));
220 assert_eq!(fm.agent, Some("code-reviewer".to_string()));
221 assert!(fm.requires_approval);
222 assert_eq!(fm.tags, vec!["code".to_string(), "review".to_string()]);
223 }
224
225 #[test]
226 fn test_skill_context_serialization() {
227 assert_eq!(
228 serde_yaml::to_string(&SkillContext::Fork).unwrap().trim(),
229 "fork"
230 );
231 assert_eq!(
232 serde_yaml::to_string(&SkillContext::Inline).unwrap().trim(),
233 "inline"
234 );
235 }
236}