use std::sync::Arc; use async_trait::async_trait; use serde_json::json; use crate::skills::{Skill, SkillsLoader}; use crate::tools::traits::{Tool, ToolResult}; pub struct GetSkillTool { skills_loader: Arc, } impl GetSkillTool { pub fn new(skills_loader: Arc) -> Self { Self { skills_loader } } fn format_skill(&self, skill: &Skill) -> String { let mut result = format!("# Skill: {}\n\n{}", skill.name, skill.description); if let Some(path) = &skill.path { result.push_str(&format!( "\n\n**Skill Root Directory:** `{}`\n\nAll files and references in this skill are relative to this directory.", path.to_string_lossy() )); } result.push_str(&format!("\n\n---\n\n{}", skill.content)); result } } #[async_trait] impl Tool for GetSkillTool { fn name(&self) -> &str { "get_skill" } fn description(&self) -> &str { "Get complete content and guidance for a specified skill. Use this when you need detailed instructions for a specific type of task." } fn parameters_schema(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "skill_name": { "type": "string", "description": "Name of the skill to retrieve" } }, "required": ["skill_name"] }) } fn read_only(&self) -> bool { true } async fn execute(&self, args: serde_json::Value) -> anyhow::Result { let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) { Some(name) => name, None => { return Ok(ToolResult { success: false, output: String::new(), error: Some("Missing required parameter: skill_name".to_string()), }); } }; match self.skills_loader.get_skill(skill_name) { Some(skill) => { let formatted = self.format_skill(&skill); Ok(ToolResult { success: true, output: formatted, error: None, }) } None => { let available = self.skills_loader.list_skills(); let available_str = if available.is_empty() { "No skills available".to_string() } else { available .iter() .map(|(name, _)| name.as_str()) .collect::>() .join(", ") }; Ok(ToolResult { success: false, output: String::new(), error: Some(format!( "Skill '{}' not found. Available skills: {}", skill_name, available_str )), }) } } } } #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; use std::fs::File; use std::io::Write; use std::path::PathBuf; #[tokio::test] async fn test_get_existing_skill() { let temp_dir = tempdir().unwrap(); let skill_dir = temp_dir.path().join("test-skill"); std::fs::create_dir(&skill_dir).unwrap(); let mut skill_file = File::create(skill_dir.join("SKILL.md")).unwrap(); writeln!(skill_file, "---").unwrap(); writeln!(skill_file, "name: test-skill").unwrap(); writeln!(skill_file, "description: A test skill").unwrap(); writeln!(skill_file, "---").unwrap(); writeln!(skill_file, "# Test Skill").unwrap(); writeln!(skill_file, "This is the test content.").unwrap(); let mut loader = SkillsLoader::new_for_testing( temp_dir.path().to_path_buf(), PathBuf::from("/nonexistent"), ); loader.load_skills(); let tool = GetSkillTool::new(Arc::new(loader)); let result = tool .execute(json!({ "skill_name": "test-skill" })) .await .unwrap(); assert!(result.success); assert!(result.output.contains("test-skill")); assert!(result.output.contains("test content")); } #[tokio::test] async fn test_get_nonexistent_skill() { let loader = SkillsLoader::new(); let tool = GetSkillTool::new(Arc::new(loader)); let result = tool .execute(json!({ "skill_name": "nonexistent" })) .await .unwrap(); assert!(!result.success); assert!(result.error.is_some()); } }