tycode_core/skills/
command.rs1use 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}