1pub mod loader;
36pub mod parser;
37
38use serde::{Deserialize, Serialize};
39use std::collections::HashMap;
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct Skill {
49 pub name: String,
51
52 pub description: String,
54
55 pub system_prompt: String,
57
58 pub tools: Vec<String>,
61
62 pub allowed_tools: Option<Vec<String>>,
65
66 pub denied_tools: Option<Vec<String>>,
69
70 #[serde(default)]
72 pub metadata: HashMap<String, serde_json::Value>,
73}
74
75impl Skill {
76 #[must_use]
78 pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
79 Self {
80 name: name.into(),
81 description: String::new(),
82 system_prompt: system_prompt.into(),
83 tools: Vec::new(),
84 allowed_tools: None,
85 denied_tools: None,
86 metadata: HashMap::new(),
87 }
88 }
89
90 #[must_use]
92 pub fn with_description(mut self, description: impl Into<String>) -> Self {
93 self.description = description.into();
94 self
95 }
96
97 #[must_use]
99 pub fn with_tools(mut self, tools: Vec<String>) -> Self {
100 self.tools = tools;
101 self
102 }
103
104 #[must_use]
106 pub fn with_allowed_tools(mut self, tools: Vec<String>) -> Self {
107 self.allowed_tools = Some(tools);
108 self
109 }
110
111 #[must_use]
113 pub fn with_denied_tools(mut self, tools: Vec<String>) -> Self {
114 self.denied_tools = Some(tools);
115 self
116 }
117
118 #[must_use]
124 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
125 if let Some(ref denied) = self.denied_tools
127 && denied.iter().any(|t| t == tool_name)
128 {
129 return false;
130 }
131
132 if let Some(ref allowed) = self.allowed_tools {
134 return allowed.iter().any(|t| t == tool_name);
135 }
136
137 true
139 }
140}
141
142pub use loader::{FileSkillLoader, SkillLoader};
143pub use parser::parse_skill_file;
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_skill_builder() {
151 let skill = Skill::new("test", "You are a test assistant.")
152 .with_description("A test skill")
153 .with_tools(vec!["read".into(), "write".into()])
154 .with_denied_tools(vec!["bash".into()]);
155
156 assert_eq!(skill.name, "test");
157 assert_eq!(skill.description, "A test skill");
158 assert_eq!(skill.system_prompt, "You are a test assistant.");
159 assert_eq!(skill.tools, vec!["read", "write"]);
160 assert_eq!(skill.denied_tools, Some(vec!["bash".into()]));
161 }
162
163 #[test]
164 fn test_is_tool_allowed_no_restrictions() {
165 let skill = Skill::new("test", "prompt");
166
167 assert!(skill.is_tool_allowed("read"));
168 assert!(skill.is_tool_allowed("write"));
169 assert!(skill.is_tool_allowed("bash"));
170 }
171
172 #[test]
173 fn test_is_tool_allowed_with_denied() {
174 let skill = Skill::new("test", "prompt").with_denied_tools(vec!["bash".into()]);
175
176 assert!(skill.is_tool_allowed("read"));
177 assert!(skill.is_tool_allowed("write"));
178 assert!(!skill.is_tool_allowed("bash"));
179 }
180
181 #[test]
182 fn test_is_tool_allowed_with_whitelist() {
183 let skill =
184 Skill::new("test", "prompt").with_allowed_tools(vec!["read".into(), "grep".into()]);
185
186 assert!(skill.is_tool_allowed("read"));
187 assert!(skill.is_tool_allowed("grep"));
188 assert!(!skill.is_tool_allowed("write"));
189 assert!(!skill.is_tool_allowed("bash"));
190 }
191
192 #[test]
193 fn test_is_tool_allowed_denied_takes_precedence() {
194 let skill = Skill::new("test", "prompt")
195 .with_allowed_tools(vec!["read".into(), "bash".into()])
196 .with_denied_tools(vec!["bash".into()]);
197
198 assert!(skill.is_tool_allowed("read"));
199 assert!(!skill.is_tool_allowed("bash")); }
201}