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 pub fn cache(&self) -> &HashMap<String, String> {
44 &self.cache
45 }
46
47 async fn list_skills(&self) -> Result<Vec<String>> {
48 let mut skills = Vec::new();
49
50 if !self.skills_dir.exists() {
51 return Ok(skills);
52 }
53
54 let mut entries = tokio::fs::read_dir(&self.skills_dir).await?;
55 while let Some(entry) = entries.next_entry().await? {
56 let path = entry.path();
57 if path.is_dir() {
58 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
59 let skill_md = path.join("SKILL.md");
61 if skill_md.exists() {
62 skills.push(name.to_string());
63 }
64 }
65 } else if path.extension().is_some_and(|e| e == "md") {
66 if let Some(stem) = path.file_stem().and_then(|n| n.to_str()) {
67 skills.push(stem.to_string());
68 }
69 }
70 }
71
72 Ok(skills)
73 }
74
75 async fn load_skill(&self, name: &str) -> Result<String> {
76 let dir_skill = self.skills_dir.join(name).join("SKILL.md");
78 if dir_skill.exists() {
79 return Ok(tokio::fs::read_to_string(&dir_skill).await?);
80 }
81
82 let file_skill = self.skills_dir.join(format!("{}.md", name));
84 if file_skill.exists() {
85 return Ok(tokio::fs::read_to_string(&file_skill).await?);
86 }
87
88 anyhow::bail!("Skill '{}' not found", name)
89 }
90}
91
92#[async_trait]
93impl Tool for SkillTool {
94 fn id(&self) -> &str {
95 "skill"
96 }
97
98 fn name(&self) -> &str {
99 "Skill"
100 }
101
102 fn description(&self) -> &str {
103 "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."
104 }
105
106 fn parameters(&self) -> Value {
107 json!({
108 "type": "object",
109 "properties": {
110 "action": {
111 "type": "string",
112 "description": "Action to perform: 'list' (show available skills) or 'load' (load a skill)",
113 "enum": ["list", "load"]
114 },
115 "skill_name": {
116 "type": "string",
117 "description": "Name of the skill to load (required for 'load' action)"
118 }
119 },
120 "required": ["action"]
121 })
122 }
123
124 async fn execute(&self, args: Value) -> Result<ToolResult> {
125 let action = args["action"]
126 .as_str()
127 .ok_or_else(|| anyhow::anyhow!("action is required"))?;
128
129 match action {
130 "list" => {
131 let skills = self.list_skills().await?;
132 if skills.is_empty() {
133 Ok(ToolResult::success(format!(
134 "No skills found. Create skills in: {}\n\
135 \n\
136 Skill format:\n\
137 - Directory: ~/.codetether/skills/<skill-name>/SKILL.md\n\
138 - File: ~/.codetether/skills/<skill-name>.md\n\
139 \n\
140 A skill file contains markdown instructions the agent follows.",
141 self.skills_dir.display()
142 )))
143 } else {
144 Ok(ToolResult::success(format!(
145 "Available skills ({}):\n{}",
146 skills.len(),
147 skills
148 .iter()
149 .map(|s| format!(" - {}", s))
150 .collect::<Vec<_>>()
151 .join("\n")
152 )))
153 }
154 }
155 "load" => {
156 let name = args["skill_name"]
157 .as_str()
158 .ok_or_else(|| anyhow::anyhow!("skill_name is required for 'load' action"))?;
159
160 match self.load_skill(name).await {
161 Ok(content) => Ok(ToolResult::success(format!(
162 "=== Skill: {} ===\n\n{}",
163 name, content
164 ))),
165 Err(e) => Ok(ToolResult::error(format!("Failed to load skill: {}", e))),
166 }
167 }
168 _ => Ok(ToolResult::error(format!(
169 "Unknown action: {}. Use 'list' or 'load'.",
170 action
171 ))),
172 }
173 }
174}