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