construct/tools/
read_skill.rs1use super::traits::{Tool, ToolResult};
2use async_trait::async_trait;
3use serde_json::json;
4use std::path::PathBuf;
5
6pub struct ReadSkillTool {
8 workspace_dir: PathBuf,
9 open_skills_enabled: bool,
10 open_skills_dir: Option<String>,
11}
12
13impl ReadSkillTool {
14 pub fn new(
15 workspace_dir: PathBuf,
16 open_skills_enabled: bool,
17 open_skills_dir: Option<String>,
18 ) -> Self {
19 Self {
20 workspace_dir,
21 open_skills_enabled,
22 open_skills_dir,
23 }
24 }
25}
26
27#[async_trait]
28impl Tool for ReadSkillTool {
29 fn name(&self) -> &str {
30 "read_skill"
31 }
32
33 fn description(&self) -> &str {
34 "Read the full source file for an available skill by name. Use this in compact skills mode when you need the complete skill instructions without remembering file paths."
35 }
36
37 fn parameters_schema(&self) -> serde_json::Value {
38 json!({
39 "type": "object",
40 "properties": {
41 "name": {
42 "type": "string",
43 "description": "The skill name exactly as listed in <available_skills>."
44 }
45 },
46 "required": ["name"]
47 })
48 }
49
50 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
51 let requested = args
52 .get("name")
53 .and_then(|value| value.as_str())
54 .map(str::trim)
55 .filter(|value| !value.is_empty())
56 .ok_or_else(|| anyhow::anyhow!("Missing 'name' parameter"))?;
57
58 let skills = crate::skills::load_skills_with_open_skills_settings(
59 &self.workspace_dir,
60 self.open_skills_enabled,
61 self.open_skills_dir.as_deref(),
62 );
63
64 let Some(skill) = skills
65 .iter()
66 .find(|skill| skill.name.eq_ignore_ascii_case(requested))
67 else {
68 let mut names: Vec<&str> = skills.iter().map(|skill| skill.name.as_str()).collect();
69 names.sort_unstable();
70 let available = if names.is_empty() {
71 "none".to_string()
72 } else {
73 names.join(", ")
74 };
75
76 return Ok(ToolResult {
77 success: false,
78 output: String::new(),
79 error: Some(format!(
80 "Unknown skill '{requested}'. Available skills: {available}"
81 )),
82 });
83 };
84
85 let Some(location) = skill.location.as_ref() else {
86 return Ok(ToolResult {
87 success: false,
88 output: String::new(),
89 error: Some(format!(
90 "Skill '{}' has no readable source location.",
91 skill.name
92 )),
93 });
94 };
95
96 match tokio::fs::read_to_string(location).await {
97 Ok(output) => Ok(ToolResult {
98 success: true,
99 output,
100 error: None,
101 }),
102 Err(err) => Ok(ToolResult {
103 success: false,
104 output: String::new(),
105 error: Some(format!(
106 "Failed to read skill '{}' from {}: {err}",
107 skill.name,
108 location.display()
109 )),
110 }),
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use tempfile::TempDir;
119
120 fn make_tool(tmp: &TempDir) -> ReadSkillTool {
121 ReadSkillTool::new(tmp.path().join("workspace"), false, None)
122 }
123
124 #[tokio::test]
125 async fn reads_markdown_skill_by_name() {
126 let tmp = TempDir::new().unwrap();
127 let skill_dir = tmp.path().join("workspace/skills/weather");
128 std::fs::create_dir_all(&skill_dir).unwrap();
129 std::fs::write(
130 skill_dir.join("SKILL.md"),
131 "# Weather\n\nUse this skill for forecast lookups.\n",
132 )
133 .unwrap();
134
135 let result = make_tool(&tmp)
136 .execute(json!({ "name": "weather" }))
137 .await
138 .unwrap();
139
140 assert!(result.success);
141 assert!(result.output.contains("# Weather"));
142 assert!(result.output.contains("forecast lookups"));
143 }
144
145 #[tokio::test]
146 async fn reads_toml_skill_manifest_by_name() {
147 let tmp = TempDir::new().unwrap();
148 let skill_dir = tmp.path().join("workspace/skills/deploy");
149 std::fs::create_dir_all(&skill_dir).unwrap();
150 std::fs::write(
151 skill_dir.join("SKILL.toml"),
152 r#"[skill]
153name = "deploy"
154description = "Ship safely"
155"#,
156 )
157 .unwrap();
158
159 let result = make_tool(&tmp)
160 .execute(json!({ "name": "deploy" }))
161 .await
162 .unwrap();
163
164 assert!(result.success);
165 assert!(result.output.contains("[skill]"));
166 assert!(result.output.contains("Ship safely"));
167 }
168
169 #[tokio::test]
170 async fn unknown_skill_lists_available_names() {
171 let tmp = TempDir::new().unwrap();
172 let skill_dir = tmp.path().join("workspace/skills/weather");
173 std::fs::create_dir_all(&skill_dir).unwrap();
174 std::fs::write(skill_dir.join("SKILL.md"), "# Weather\n").unwrap();
175
176 let result = make_tool(&tmp)
177 .execute(json!({ "name": "calendar" }))
178 .await
179 .unwrap();
180
181 assert!(!result.success);
182 assert_eq!(
183 result.error.as_deref(),
184 Some("Unknown skill 'calendar'. Available skills: weather")
185 );
186 }
187}