Skip to main content

apollo/tools/
skill_manager.rs

1//! Managed skills tool — create, inspect, and list persisted workspace skills.
2
3use 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}