Skip to main content

tycode_core/skills/
command.rs

1use std::path::PathBuf;
2
3use chrono::Utc;
4
5use crate::chat::actor::ActorState;
6use crate::chat::events::{ChatMessage, MessageSender};
7use crate::module::SlashCommand;
8
9use super::discovery::SkillsManager;
10
11pub struct SkillsListCommand {
12    manager: SkillsManager,
13}
14
15impl SkillsListCommand {
16    pub fn new(manager: SkillsManager) -> Self {
17        Self { manager }
18    }
19}
20
21#[async_trait::async_trait(?Send)]
22impl SlashCommand for SkillsListCommand {
23    fn name(&self) -> &'static str {
24        "skills"
25    }
26
27    fn description(&self) -> &'static str {
28        "List and manage available skills"
29    }
30
31    fn usage(&self) -> &'static str {
32        "/skills [info <name>|reload]"
33    }
34
35    async fn execute(&self, _state: &mut ActorState, args: &[&str]) -> Vec<ChatMessage> {
36        if args.is_empty() {
37            let skills = self.manager.get_all_metadata();
38
39            if skills.is_empty() {
40                return vec![create_message(
41                    "No skills found. Skills are discovered from (in priority order):\n\
42                     - ~/.claude/skills/ (user-level Claude Code compatibility)\n\
43                     - ~/.tycode/skills/ (user-level)\n\
44                     - .claude/skills/ (project-level Claude Code compatibility)\n\
45                     - .tycode/skills/ (project-level, highest priority)\n\n\
46                     Each skill should be a directory containing a SKILL.md file."
47                        .to_string(),
48                    MessageSender::System,
49                )];
50            }
51
52            let mut message = format!("Available Skills ({} found):\n\n", skills.len());
53            for skill in &skills {
54                let status = if skill.enabled { "" } else { " [disabled]" };
55                message.push_str(&format!(
56                    "  {} ({}){}\n    {}\n\n",
57                    skill.name, skill.source, status, skill.description
58                ));
59            }
60
61            message.push_str("Use `/skill <name>` to invoke a skill manually.\n");
62            message.push_str("Use `/skills info <name>` to see skill details.\n");
63            message.push_str("Use `/skills reload` to re-scan skill directories.");
64
65            return vec![create_message(message, MessageSender::System)];
66        }
67
68        match args[0] {
69            "info" => self.handle_info(args).await,
70            "reload" => {
71                self.manager.reload();
72                let count = self.manager.get_all_metadata().len();
73                vec![create_message(
74                    format!("Skills reloaded. Found {} skill(s).", count),
75                    MessageSender::System,
76                )]
77            }
78            _ => vec![create_message(
79                "Usage: /skills [info <name>|reload]\n\
80                 Use `/skills` to list all available skills."
81                    .to_string(),
82                MessageSender::Error,
83            )],
84        }
85    }
86}
87
88impl SkillsListCommand {
89    async fn handle_info(&self, args: &[&str]) -> Vec<ChatMessage> {
90        if args.len() < 2 {
91            return vec![create_message(
92                "Usage: /skills info <name>".to_string(),
93                MessageSender::Error,
94            )];
95        }
96
97        let name = args[1];
98        match self.manager.get_skill(name) {
99            Some(skill) => {
100                let mut message = format!("# Skill: {}\n\n", skill.metadata.name);
101                message.push_str(&format!("**Source:** {}\n", skill.metadata.source));
102                message.push_str(&format!("**Path:** {}\n", skill.metadata.path.display()));
103                message.push_str(&format!(
104                    "**Status:** {}\n\n",
105                    if skill.metadata.enabled {
106                        "Enabled"
107                    } else {
108                        "Disabled"
109                    }
110                ));
111                message.push_str(&format!(
112                    "**Description:**\n{}\n\n",
113                    skill.metadata.description
114                ));
115                message.push_str("**Instructions:**\n\n");
116                message.push_str(&skill.instructions);
117
118                if !skill.reference_files.is_empty() {
119                    message.push_str("\n\n**Reference Files:**\n");
120                    message.push_str(&format_path_list(&skill.reference_files));
121                }
122
123                if !skill.scripts.is_empty() {
124                    message.push_str("\n**Scripts:**\n");
125                    message.push_str(&format_path_list(&skill.scripts));
126                }
127
128                vec![create_message(message, MessageSender::System)]
129            }
130            None => vec![create_message(
131                format!(
132                    "Skill '{}' not found. Use `/skills` to list available skills.",
133                    name
134                ),
135                MessageSender::Error,
136            )],
137        }
138    }
139}
140
141pub struct SkillInvokeCommand {
142    manager: SkillsManager,
143}
144
145impl SkillInvokeCommand {
146    pub fn new(manager: SkillsManager) -> Self {
147        Self { manager }
148    }
149}
150
151#[async_trait::async_trait(?Send)]
152impl SlashCommand for SkillInvokeCommand {
153    fn name(&self) -> &'static str {
154        "skill"
155    }
156
157    fn description(&self) -> &'static str {
158        "Manually invoke a skill"
159    }
160
161    fn usage(&self) -> &'static str {
162        "/skill <name>"
163    }
164
165    async fn execute(&self, _state: &mut ActorState, args: &[&str]) -> Vec<ChatMessage> {
166        if args.is_empty() {
167            return vec![create_message(
168                "Usage: /skill <name>\n\
169                 Use `/skills` to list available skills."
170                    .to_string(),
171                MessageSender::Error,
172            )];
173        }
174
175        let name = args[0];
176
177        match self.manager.get_skill(name) {
178            Some(skill) => {
179                if !skill.metadata.enabled {
180                    return vec![create_message(
181                        format!("Skill '{}' is disabled.", name),
182                        MessageSender::Error,
183                    )];
184                }
185
186                let mut message = format!(
187                    "## Skill Invoked: {}\n\n{}\n\n---\n\n**Instructions:**\n\n{}",
188                    skill.metadata.name, skill.metadata.description, skill.instructions
189                );
190
191                if !skill.reference_files.is_empty() {
192                    message.push_str("\n\n**Reference Files:**\n");
193                    message.push_str(&format_path_list(&skill.reference_files));
194                }
195
196                if !skill.scripts.is_empty() {
197                    message.push_str("\n**Scripts:**\n");
198                    message.push_str(&format_path_list(&skill.scripts));
199                }
200
201                vec![create_message(message, MessageSender::System)]
202            }
203            None => vec![create_message(
204                format!(
205                    "Skill '{}' not found. Use `/skills` to list available skills.",
206                    name
207                ),
208                MessageSender::Error,
209            )],
210        }
211    }
212}
213
214fn create_message(content: String, sender: MessageSender) -> ChatMessage {
215    ChatMessage {
216        content,
217        sender,
218        timestamp: Utc::now().timestamp_millis() as u64,
219        reasoning: None,
220        tool_calls: Vec::new(),
221        model_info: None,
222        token_usage: None,
223        images: vec![],
224    }
225}
226
227fn format_path_list(paths: &[PathBuf]) -> String {
228    paths
229        .iter()
230        .map(|p| format!("- {}\n", p.display()))
231        .collect()
232}