codetether_agent/tool/
skill.rs1use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde_json::{Value, json};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10pub struct SkillTool {
14 skills_dir: PathBuf,
15 #[allow(dead_code)]
16 cache: HashMap<String, String>,
17}
18
19impl Default for SkillTool {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl SkillTool {
26 pub fn new() -> Self {
27 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
28 Self {
29 skills_dir: PathBuf::from(home).join(".codetether").join("skills"),
30 cache: HashMap::new(),
31 }
32 }
33
34 #[allow(dead_code)]
35 pub fn with_dir(dir: PathBuf) -> Self {
36 Self {
37 skills_dir: dir,
38 cache: HashMap::new(),
39 }
40 }
41
42 #[allow(dead_code)]
44 pub fn cache(&self) -> &HashMap<String, String> {
45 &self.cache
46 }
47
48 async fn list_skills(&self) -> Result<Vec<String>> {
49 let mut skills = Vec::new();
50
51 if !self.skills_dir.exists() {
52 return Ok(skills);
53 }
54
55 let mut entries = tokio::fs::read_dir(&self.skills_dir).await?;
56 while let Some(entry) = entries.next_entry().await? {
57 let path = entry.path();
58 if path.is_dir() {
59 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
60 let skill_md = path.join("SKILL.md");
62 if skill_md.exists() {
63 skills.push(name.to_string());
64 }
65 }
66 } else if path.extension().is_some_and(|e| e == "md")
67 && let Some(stem) = path.file_stem().and_then(|n| n.to_str())
68 {
69 skills.push(stem.to_string());
70 }
71 }
72
73 Ok(skills)
74 }
75
76 async fn load_skill(&self, name: &str) -> Result<String> {
77 let dir_skill = self.skills_dir.join(name).join("SKILL.md");
79 if dir_skill.exists() {
80 return Ok(tokio::fs::read_to_string(&dir_skill).await?);
81 }
82
83 let file_skill = self.skills_dir.join(format!("{}.md", name));
85 if file_skill.exists() {
86 return Ok(tokio::fs::read_to_string(&file_skill).await?);
87 }
88
89 anyhow::bail!("Skill '{}' not found", name)
90 }
91}
92
93#[async_trait]
94impl Tool for SkillTool {
95 fn id(&self) -> &str {
96 "skill"
97 }
98
99 fn name(&self) -> &str {
100 "Skill"
101 }
102
103 fn description(&self) -> &str {
104 "Load and invoke learned skill patterns. Skills are reusable instruction sets for specific tasks like code review, testing, documentation, etc. Use 'list' action to see available skills, 'load' to read a skill's instructions."
105 }
106
107 fn parameters(&self) -> Value {
108 json!({
109 "type": "object",
110 "properties": {
111 "action": {
112 "type": "string",
113 "description": "Action to perform: 'list' (show available skills) or 'load' (load a skill)",
114 "enum": ["list", "load"]
115 },
116 "skill_name": {
117 "type": "string",
118 "description": "Name of the skill to load (required for 'load' action)"
119 }
120 },
121 "required": ["action"]
122 })
123 }
124
125 async fn execute(&self, args: Value) -> Result<ToolResult> {
126 let action = args["action"]
127 .as_str()
128 .ok_or_else(|| anyhow::anyhow!("action is required"))?;
129
130 match action {
131 "list" => {
132 let skills = self.list_skills().await?;
133 if skills.is_empty() {
134 Ok(ToolResult::success(format!(
135 "No skills found. Create skills in: {}\n\
136 \n\
137 Skill format:\n\
138 - Directory: ~/.codetether/skills/<skill-name>/SKILL.md\n\
139 - File: ~/.codetether/skills/<skill-name>.md\n\
140 \n\
141 A skill file contains markdown instructions the agent follows.",
142 self.skills_dir.display()
143 )))
144 } else {
145 Ok(ToolResult::success(format!(
146 "Available skills ({}):\n{}",
147 skills.len(),
148 skills
149 .iter()
150 .map(|s| format!(" - {}", s))
151 .collect::<Vec<_>>()
152 .join("\n")
153 )))
154 }
155 }
156 "load" => {
157 let name = args["skill_name"]
158 .as_str()
159 .ok_or_else(|| anyhow::anyhow!("skill_name is required for 'load' action"))?;
160
161 match self.load_skill(name).await {
162 Ok(content) => Ok(ToolResult::success(format!(
163 "=== Skill: {} ===\n\n{}",
164 name, content
165 ))),
166 Err(e) => Ok(ToolResult::error(format!("Failed to load skill: {}", e))),
167 }
168 }
169 _ => Ok(ToolResult::error(format!(
170 "Unknown action: {}. Use 'list' or 'load'.",
171 action
172 ))),
173 }
174 }
175}