apollo/tools/
skill_manager.rs1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use serde::Deserialize;
7
8use crate::skills::{
9 discover_skills_for_workspace, load_skill_content, save_managed_skill, ManagedSkillInput,
10};
11
12use super::{Tool, ToolResult, ToolSpec};
13
14pub struct SkillManagerTool {
15 workspace: PathBuf,
16}
17
18impl SkillManagerTool {
19 pub fn new(workspace: PathBuf) -> Self {
20 Self { workspace }
21 }
22}
23
24#[derive(Deserialize)]
25struct SkillManagerArgs {
26 action: String,
27 name: Option<String>,
28 description: Option<String>,
29 body: Option<String>,
30}
31
32#[async_trait]
33impl Tool for SkillManagerTool {
34 fn name(&self) -> &str {
35 "skill_manager"
36 }
37
38 fn spec(&self) -> ToolSpec {
39 ToolSpec {
40 name: "skill_manager".to_string(),
41 description: "List, inspect, or save managed skills in the workspace.".to_string(),
42 parameters: serde_json::json!({
43 "type": "object",
44 "properties": {
45 "action": { "type": "string", "enum": ["list", "get", "save"] },
46 "name": { "type": "string", "description": "Skill name for get/save" },
47 "description": { "type": "string", "description": "Skill description for save" },
48 "body": { "type": "string", "description": "Skill body markdown for save" }
49 },
50 "required": ["action"]
51 }),
52 }
53 }
54
55 async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
56 let args: SkillManagerArgs = serde_json::from_str(arguments)?;
57 match args.action.as_str() {
58 "list" => {
59 let skills = discover_skills_for_workspace(Some(&self.workspace));
60 if skills.is_empty() {
61 return Ok(ToolResult::success("No skills found."));
62 }
63 let output = skills
64 .iter()
65 .map(|skill| format!("{} — {}", skill.name, skill.description))
66 .collect::<Vec<_>>()
67 .join("\n");
68 Ok(ToolResult::success(output))
69 }
70 "get" => {
71 let Some(name) = args.name else {
72 return Ok(ToolResult::error("name is required for get"));
73 };
74 let skills = discover_skills_for_workspace(Some(&self.workspace));
75 let Some(skill) = skills.into_iter().find(|skill| skill.name == name) else {
76 return Ok(ToolResult::error("Skill not found"));
77 };
78 Ok(ToolResult::success(
79 load_skill_content(&skill).unwrap_or_default(),
80 ))
81 }
82 "save" => {
83 let Some(name) = args.name else {
84 return Ok(ToolResult::error("name is required for save"));
85 };
86 let Some(description) = args.description else {
87 return Ok(ToolResult::error("description is required for save"));
88 };
89 let Some(body) = args.body else {
90 return Ok(ToolResult::error("body is required for save"));
91 };
92 let path = save_managed_skill(
93 &self.workspace,
94 &ManagedSkillInput {
95 name,
96 description,
97 body,
98 },
99 )?;
100 Ok(ToolResult::success(format!(
101 "Managed skill saved at {}",
102 path.display()
103 )))
104 }
105 _ => Ok(ToolResult::error("Unknown action")),
106 }
107 }
108}