1use crate::config::Config;
2use crate::providers;
3use crate::secrets::SecretsManager;
4use crate::skills::SkillManager;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum CommandAction {
8 None,
9 ClearMessages,
10 Quit,
11 GatewayStart,
13 GatewayStop,
15 GatewayRestart,
17 GatewayInfo,
19 SetProvider(String),
21 SetModel(String),
23 ShowSkills,
25 ShowSecrets,
27 ShowProviderSelector,
29 ShowToolPermissions,
31 GatewayReload,
33 Download(String, Option<String>),
35}
36
37#[derive(Debug, Clone)]
38pub struct CommandResponse {
39 pub messages: Vec<String>,
40 pub action: CommandAction,
41}
42
43pub struct CommandContext<'a> {
44 pub secrets_manager: &'a mut SecretsManager,
45 pub skill_manager: &'a mut SkillManager,
46 pub config: &'a mut Config,
47}
48
49pub fn command_names() -> Vec<String> {
52 let mut names: Vec<String> = vec![
53 "help".into(),
54 "clear".into(),
55 "download".into(),
56 "enable-access".into(),
57 "disable-access".into(),
58 "onboard".into(),
59 "reload-skills".into(),
60 "gateway".into(),
61 "gateway start".into(),
62 "gateway stop".into(),
63 "gateway restart".into(),
64 "reload".into(),
65 "provider".into(),
66 "model".into(),
67 "skills".into(),
68 "skill".into(),
69 "tools".into(),
70 "skill info".into(),
71 "skill remove".into(),
72 "skill search".into(),
73 "skill install".into(),
74 "skill publish".into(),
75 "skill link-secret".into(),
76 "skill unlink-secret".into(),
77 "skill create".into(),
78 "secrets".into(),
79 "clawhub".into(),
80 "clawhub auth".into(),
81 "clawhub auth login".into(),
82 "clawhub auth status".into(),
83 "clawhub auth logout".into(),
84 "clawhub search".into(),
85 "clawhub trending".into(),
86 "clawhub categories".into(),
87 "clawhub info".into(),
88 "clawhub browse".into(),
89 "clawhub profile".into(),
90 "clawhub starred".into(),
91 "clawhub star".into(),
92 "clawhub unstar".into(),
93 "clawhub install".into(),
94 "clawhub publish".into(),
95 "agent setup".into(),
96 "ollama".into(),
97 "exo".into(),
98 "uv".into(),
99 "npm".into(),
100 "quit".into(),
101 ];
102 for p in providers::provider_ids() {
103 names.push(format!("provider {}", p));
104 }
105 for m in providers::all_model_names() {
106 names.push(format!("model {}", m));
107 }
108 names
109}
110
111pub fn handle_command(input: &str, context: &mut CommandContext<'_>) -> CommandResponse {
112 let trimmed = input.trim().trim_start_matches('/');
114 if trimmed.is_empty() {
115 return CommandResponse {
116 messages: Vec::new(),
117 action: CommandAction::None,
118 };
119 }
120
121 let parts: Vec<&str> = trimmed.split_whitespace().collect();
122 if parts.is_empty() {
123 return CommandResponse {
124 messages: Vec::new(),
125 action: CommandAction::None,
126 };
127 }
128
129 match parts[0] {
130 "agent" => {
131 if parts.get(1) == Some(&"setup") {
132 let ws_dir = context.config.workspace_dir();
133 match crate::tools::agent_setup::exec_agent_setup(&serde_json::json!({}), &ws_dir) {
134 Ok(msg) => CommandResponse {
135 messages: vec![msg],
136 action: CommandAction::None,
137 },
138 Err(e) => CommandResponse {
139 messages: vec![format!("Agent setup failed: {}", e)],
140 action: CommandAction::None,
141 },
142 }
143 } else {
144 CommandResponse {
145 messages: vec!["Usage: /agent setup".to_string()],
146 action: CommandAction::None,
147 }
148 }
149 }
150 "ollama" => {
151 let action = parts.get(1).copied().unwrap_or("status");
153 let model = parts.get(2).copied();
154 let dest = parts.get(3).copied();
155 let mut args = serde_json::json!({"action": action});
156 if let Some(m) = model { args["model"] = serde_json::json!(m); }
157 if let Some(d) = dest { args["destination"] = serde_json::json!(d); }
158 let ws_dir = context.config.workspace_dir();
159 match crate::tools::ollama::exec_ollama_manage(&args, &ws_dir) {
160 Ok(msg) => CommandResponse { messages: vec![msg], action: CommandAction::None },
161 Err(e) => CommandResponse { messages: vec![format!("ollama error: {}", e)], action: CommandAction::None },
162 }
163 }
164 "exo" => {
165 let action = parts.get(1).copied().unwrap_or("status");
167 let model = parts.get(2).copied();
168 let mut args = serde_json::json!({"action": action});
169 if let Some(m) = model { args["model"] = serde_json::json!(m); }
170 let ws_dir = context.config.workspace_dir();
171 match crate::tools::exo_ai::exec_exo_manage(&args, &ws_dir) {
172 Ok(msg) => CommandResponse { messages: vec![msg], action: CommandAction::None },
173 Err(e) => CommandResponse { messages: vec![format!("exo error: {}", e)], action: CommandAction::None },
174 }
175 }
176 "uv" => {
177 let action = parts.get(1).copied().unwrap_or("version");
179 let rest: Vec<&str> = parts.iter().skip(2).copied().collect();
180 let mut args = serde_json::json!({"action": action});
181 if rest.len() == 1 {
182 args["package"] = serde_json::json!(rest[0]);
183 } else if rest.len() > 1 {
184 args["packages"] = serde_json::json!(rest);
185 }
186 let ws_dir = context.config.workspace_dir();
187 match crate::tools::uv::exec_uv_manage(&args, &ws_dir) {
188 Ok(msg) => CommandResponse { messages: vec![msg], action: CommandAction::None },
189 Err(e) => CommandResponse { messages: vec![format!("uv error: {}", e)], action: CommandAction::None },
190 }
191 }
192 "npm" => {
193 let action = parts.get(1).copied().unwrap_or("status");
195 let rest: Vec<&str> = parts.iter().skip(2).copied().collect();
196 let mut args = serde_json::json!({"action": action});
197 if rest.len() == 1 {
198 args["package"] = serde_json::json!(rest[0]);
199 } else if rest.len() > 1 {
200 args["packages"] = serde_json::json!(rest);
201 }
202 let ws_dir = context.config.workspace_dir();
203 match crate::tools::npm::exec_npm_manage(&args, &ws_dir) {
204 Ok(msg) => CommandResponse { messages: vec![msg], action: CommandAction::None },
205 Err(e) => CommandResponse { messages: vec![format!("npm error: {}", e)], action: CommandAction::None },
206 }
207 }
208 "help" => CommandResponse {
209 messages: vec![
210 "Available commands:".to_string(),
211 " /help - Show this help".to_string(),
212 " /clear - Clear messages and conversation memory".to_string(),
213 " /download <id> [path] - Download media attachment to file".to_string(),
214 " /enable-access - Enable agent access to secrets".to_string(),
215 " /disable-access - Disable agent access to secrets".to_string(),
216 " /onboard - Run setup wizard (use CLI: rustyclaw onboard)".to_string(),
217 " /reload-skills - Reload skills".to_string(),
218 " /gateway - Show gateway connection status".to_string(),
219 " /gateway start - Connect to the gateway".to_string(),
220 " /gateway stop - Disconnect from the gateway".to_string(),
221 " /gateway restart - Restart the gateway connection".to_string(),
222 " /reload - Reload gateway config (no restart)".to_string(),
223 " /provider <name> - Change the AI provider".to_string(),
224 " /model <name> - Change the AI model".to_string(),
225 " /skills - Show loaded skills".to_string(),
226 " /skill - Skill management (info/install/publish/link)".to_string(),
227 " /tools - Edit tool permissions (allow/deny/ask/skill)".to_string(),
228 " /secrets - Open the secrets vault".to_string(),
229 " /clawhub - ClawHub skill registry commands".to_string(),
230 " /agent setup - Set up local model tools (uv, exo, ollama)".to_string(),
231 " /ollama <action> [model] - Ollama admin (setup/pull/list/ps/status/…)".to_string(),
232 " /exo <action> [model] - Exo cluster admin (setup/start/stop/status/…)".to_string(),
233 " /uv <action> [pkg …] - Python/uv admin (setup/pip-install/list/…)".to_string(),
234 " /npm <action> [pkg …] - Node.js/npm admin (setup/install/run/build/…)".to_string(),
235 ],
236 action: CommandAction::None,
237 },
238 "clear" => CommandResponse {
239 messages: vec!["Messages and conversation memory cleared.".to_string()],
240 action: CommandAction::ClearMessages,
241 },
242 "download" => {
243 if parts.len() < 2 {
244 CommandResponse {
245 messages: vec![
246 "Usage: /download <media_id> [destination_path]".to_string(),
247 "Example: /download media_0001".to_string(),
248 "Example: /download media_0001 ~/Downloads/image.jpg".to_string(),
249 ],
250 action: CommandAction::None,
251 }
252 } else {
253 let media_id = parts[1].to_string();
254 let dest_path = parts.get(2).map(|s| s.to_string());
255 CommandResponse {
256 messages: vec![format!("Downloading {}...", media_id)],
257 action: CommandAction::Download(media_id, dest_path),
258 }
259 }
260 },
261 "enable-access" => {
262 context.secrets_manager.set_agent_access(true);
263 context.config.agent_access = true;
264 let _ = context.config.save(None);
265 CommandResponse {
266 messages: vec!["Agent access to secrets enabled.".to_string()],
267 action: CommandAction::None,
268 }
269 }
270 "disable-access" => {
271 context.secrets_manager.set_agent_access(false);
272 context.config.agent_access = false;
273 let _ = context.config.save(None);
274 CommandResponse {
275 messages: vec!["Agent access to secrets disabled.".to_string()],
276 action: CommandAction::None,
277 }
278 }
279 "reload-skills" => match context.skill_manager.load_skills() {
280 Ok(_) => CommandResponse {
281 messages: vec![format!(
282 "Reloaded {} skills.",
283 context.skill_manager.get_skills().len()
284 )],
285 action: CommandAction::None,
286 },
287 Err(err) => CommandResponse {
288 messages: vec![format!("Error reloading skills: {}", err)],
289 action: CommandAction::None,
290 },
291 },
292 "onboard" => CommandResponse {
293 messages: vec![
294 "The onboard wizard is an interactive CLI command.".to_string(),
295 "Run it from your terminal: rustyclaw onboard".to_string(),
296 ],
297 action: CommandAction::None,
298 },
299 "gateway" => match parts.get(1).copied() {
300 Some("start") => CommandResponse {
301 messages: vec!["Starting gateway connection…".to_string()],
302 action: CommandAction::GatewayStart,
303 },
304 Some("stop") => CommandResponse {
305 messages: vec!["Stopping gateway connection…".to_string()],
306 action: CommandAction::GatewayStop,
307 },
308 Some("restart") => CommandResponse {
309 messages: vec!["Restarting gateway connection…".to_string()],
310 action: CommandAction::GatewayRestart,
311 },
312 Some(sub) => CommandResponse {
313 messages: vec![
314 format!("Unknown gateway subcommand: {}", sub),
315 "Usage: /gateway start|stop|restart".to_string(),
316 ],
317 action: CommandAction::None,
318 },
319 None => CommandResponse {
320 messages: Vec::new(),
321 action: CommandAction::GatewayInfo,
322 },
323 },
324 "reload" => CommandResponse {
325 messages: vec!["Reloading gateway configuration…".to_string()],
326 action: CommandAction::GatewayReload,
327 },
328 "skills" => CommandResponse {
329 messages: Vec::new(),
330 action: CommandAction::ShowSkills,
331 },
332 "tools" => CommandResponse {
333 messages: Vec::new(),
334 action: CommandAction::ShowToolPermissions,
335 },
336 "skill" => handle_skill_subcommand(&parts[1..], context),
337 "secrets" => CommandResponse {
338 messages: Vec::new(),
339 action: CommandAction::ShowSecrets,
340 },
341 "provider" => match parts.get(1) {
342 Some(name) => {
343 let name = name.to_string();
344 CommandResponse {
345 messages: vec![format!("Switching provider to {}…", name)],
346 action: CommandAction::SetProvider(name),
347 }
348 }
349 None => {
350 CommandResponse {
351 messages: Vec::new(),
352 action: CommandAction::ShowProviderSelector,
353 }
354 }
355 },
356 "model" => match parts.get(1) {
357 Some(name) => {
358 let name = name.to_string();
359 CommandResponse {
360 messages: vec![format!("Switching model to {}…", name)],
361 action: CommandAction::SetModel(name),
362 }
363 }
364 None => {
365 let list = providers::all_model_names().join(", ");
366 CommandResponse {
367 messages: vec![
368 "Usage: /model <name>".to_string(),
369 format!("Known models: {}", list),
370 ],
371 action: CommandAction::None,
372 }
373 }
374 },
375 "clawhub" | "hub" | "registry" => handle_clawhub_subcommand(&parts[1..], context),
376 "q" | "quit" | "exit" => CommandResponse {
377 messages: Vec::new(),
378 action: CommandAction::Quit,
379 },
380 _ => CommandResponse {
381 messages: vec![
382 format!("Unknown command: /{}", parts[0]),
383 "Type /help for available commands".to_string(),
384 ],
385 action: CommandAction::None,
386 },
387 }
388}
389
390fn handle_skill_subcommand(parts: &[&str], context: &mut CommandContext<'_>) -> CommandResponse {
391 match parts.first().copied() {
392 Some("info") => {
393 let name = parts.get(1).copied().unwrap_or("");
394 if name.is_empty() {
395 return CommandResponse {
396 messages: vec!["Usage: /skill info <name>".to_string()],
397 action: CommandAction::None,
398 };
399 }
400 match context.skill_manager.skill_info(name) {
401 Some(info) => CommandResponse {
402 messages: vec![info],
403 action: CommandAction::None,
404 },
405 None => CommandResponse {
406 messages: vec![format!("Skill '{}' not found.", name)],
407 action: CommandAction::None,
408 },
409 }
410 }
411 Some("remove") => {
412 let name = parts.get(1).copied().unwrap_or("");
413 if name.is_empty() {
414 return CommandResponse {
415 messages: vec!["Usage: /skill remove <name>".to_string()],
416 action: CommandAction::None,
417 };
418 }
419 match context.skill_manager.remove_skill(name) {
420 Ok(()) => CommandResponse {
421 messages: vec![format!("Skill '{}' removed.", name)],
422 action: CommandAction::None,
423 },
424 Err(e) => CommandResponse {
425 messages: vec![e.to_string()],
426 action: CommandAction::None,
427 },
428 }
429 }
430 Some("search") => {
431 let query = parts[1..].join(" ");
432 if query.is_empty() {
433 return CommandResponse {
434 messages: vec!["Usage: /skill search <query>".to_string()],
435 action: CommandAction::None,
436 };
437 }
438 match context.skill_manager.search_registry(&query) {
439 Ok(results) => {
440 if results.is_empty() {
441 CommandResponse {
442 messages: vec![format!("No skills found matching '{}'.", query)],
443 action: CommandAction::None,
444 }
445 } else {
446 let has_local = results.iter().any(|r| r.version == "local");
447 let header = if has_local {
448 format!(
449 "{} local skill(s) matching '{}' (registry offline):",
450 results.len(),
451 query,
452 )
453 } else {
454 format!(
455 "{} result(s) for '{}':",
456 results.len(),
457 query,
458 )
459 };
460 let mut msgs: Vec<String> = vec![header];
461 for r in &results {
462 msgs.push(format!(
463 " • {} v{} by {} — {}",
464 r.name, r.version, r.author, r.description,
465 ));
466 }
467 CommandResponse {
468 messages: msgs,
469 action: CommandAction::None,
470 }
471 }
472 }
473 Err(e) => CommandResponse {
474 messages: vec![format!("Registry search failed: {}", e)],
475 action: CommandAction::None,
476 },
477 }
478 }
479 Some("install") => {
480 let name = parts.get(1).copied().unwrap_or("");
481 if name.is_empty() {
482 return CommandResponse {
483 messages: vec!["Usage: /skill install <name> [version]".to_string()],
484 action: CommandAction::None,
485 };
486 }
487 let version = parts.get(2).copied();
488 match context.skill_manager.install_from_registry(name, version) {
489 Ok(skill) => {
490 let _ = context.skill_manager.load_skills();
491 CommandResponse {
492 messages: vec![format!("Skill '{}' installed from ClawHub.", skill.name)],
493 action: CommandAction::None,
494 }
495 }
496 Err(e) => CommandResponse {
497 messages: vec![format!("Install failed: {}", e)],
498 action: CommandAction::None,
499 },
500 }
501 }
502 Some("publish") => {
503 let name = parts.get(1).copied().unwrap_or("");
504 if name.is_empty() {
505 return CommandResponse {
506 messages: vec!["Usage: /skill publish <name>".to_string()],
507 action: CommandAction::None,
508 };
509 }
510 match context.skill_manager.publish_to_registry(name) {
511 Ok(msg) => CommandResponse {
512 messages: vec![msg],
513 action: CommandAction::None,
514 },
515 Err(e) => CommandResponse {
516 messages: vec![format!("Publish failed: {}", e)],
517 action: CommandAction::None,
518 },
519 }
520 }
521 Some("link-secret") => {
522 let skill = parts.get(1).copied().unwrap_or("");
523 let secret = parts.get(2).copied().unwrap_or("");
524 if skill.is_empty() || secret.is_empty() {
525 return CommandResponse {
526 messages: vec!["Usage: /skill link-secret <skill> <secret>".to_string()],
527 action: CommandAction::None,
528 };
529 }
530 match context.skill_manager.link_secret(skill, secret) {
531 Ok(_) => CommandResponse {
532 messages: vec![format!(
533 "Secret '{}' linked to skill '{}'.",
534 secret, skill,
535 )],
536 action: CommandAction::None,
537 },
538 Err(e) => CommandResponse {
539 messages: vec![format!("Link failed: {}", e)],
540 action: CommandAction::None,
541 },
542 }
543 }
544 Some("unlink-secret") => {
545 let skill = parts.get(1).copied().unwrap_or("");
546 let secret = parts.get(2).copied().unwrap_or("");
547 if skill.is_empty() || secret.is_empty() {
548 return CommandResponse {
549 messages: vec!["Usage: /skill unlink-secret <skill> <secret>".to_string()],
550 action: CommandAction::None,
551 };
552 }
553 match context.skill_manager.unlink_secret(skill, secret) {
554 Ok(_) => CommandResponse {
555 messages: vec![format!(
556 "Secret '{}' unlinked from skill '{}'.",
557 secret, skill,
558 )],
559 action: CommandAction::None,
560 },
561 Err(e) => CommandResponse {
562 messages: vec![format!("Unlink failed: {}", e)],
563 action: CommandAction::None,
564 },
565 }
566 }
567 Some("create") => {
568 let name = parts.get(1).copied().unwrap_or("");
571 if name.is_empty() {
572 return CommandResponse {
573 messages: vec![
574 "Usage: /skill create <name> <one-line description>".to_string(),
575 "".to_string(),
576 "This creates an empty skill scaffold. To have the agent write a full".to_string(),
577 "skill from a prompt, just ask: \"Create a skill that deploys to S3\"".to_string(),
578 ],
579 action: CommandAction::None,
580 };
581 }
582 let description = if parts.len() > 2 {
583 parts[2..].join(" ")
584 } else {
585 format!("A skill called {}", name)
586 };
587 let instructions = format!("# {}\n\nTODO: Add instructions for this skill.", name);
588 match context.skill_manager.create_skill(name, &description, &instructions, None) {
589 Ok(path) => CommandResponse {
590 messages: vec![
591 format!("✅ Skill '{}' created at {}", name, path.display()),
592 "Edit the SKILL.md to add instructions, or ask the agent to fill it in.".to_string(),
593 ],
594 action: CommandAction::None,
595 },
596 Err(e) => CommandResponse {
597 messages: vec![format!("Create failed: {}", e)],
598 action: CommandAction::None,
599 },
600 }
601 }
602 Some(sub) => CommandResponse {
603 messages: vec![
604 format!("Unknown skill subcommand: {}", sub),
605 "Usage: /skill info|remove|search|install|publish|create|link-secret|unlink-secret".to_string(),
606 ],
607 action: CommandAction::None,
608 },
609 None => CommandResponse {
610 messages: vec![
611 "Skill commands:".to_string(),
612 " /skill info <name> — Show skill details".to_string(),
613 " /skill remove <name> — Remove a skill".to_string(),
614 " /skill search <query> — Search ClawHub registry".to_string(),
615 " /skill install <name> [version] — Install from ClawHub".to_string(),
616 " /skill publish <name> — Publish to ClawHub".to_string(),
617 " /skill create <name> [description] — Create a new skill".to_string(),
618 " /skill link-secret <skill> <secret> — Link secret to skill".to_string(),
619 " /skill unlink-secret <skill> <secret> — Unlink secret".to_string(),
620 ],
621 action: CommandAction::None,
622 },
623 }
624}
625
626fn handle_clawhub_subcommand(parts: &[&str], context: &mut CommandContext<'_>) -> CommandResponse {
627 match parts.first().copied() {
628 Some("auth") => match parts.get(1).copied() {
629 Some("login") => {
630 let token = parts.get(2).copied().unwrap_or("");
632 if token.is_empty() {
633 return CommandResponse {
634 messages: vec![
635 "Usage: /clawhub auth login <api_token>".to_string(),
636 "Get your token at https://clawhub.ai/settings/tokens".to_string(),
637 ],
638 action: CommandAction::None,
639 };
640 }
641 match context.skill_manager.auth_token(token) {
642 Ok(resp) if resp.ok => {
643 context.config.clawhub_token = Some(token.to_string());
645 let _ = context.config.save(None);
646 let url = context.skill_manager.registry_url().to_string();
647 context.skill_manager.set_registry(
648 &url,
649 Some(token.to_string()),
650 );
651 let user = resp.username.unwrap_or_else(|| "unknown".into());
652 CommandResponse {
653 messages: vec![format!("✓ Authenticated as '{}' on ClawHub.", user)],
654 action: CommandAction::None,
655 }
656 }
657 Ok(_) => CommandResponse {
658 messages: vec!["✗ Token is invalid.".to_string()],
659 action: CommandAction::None,
660 },
661 Err(e) => CommandResponse {
662 messages: vec![format!("✗ Auth failed: {}", e)],
663 action: CommandAction::None,
664 },
665 }
666 }
667 Some("status") => match context.skill_manager.auth_status() {
668 Ok(msg) => CommandResponse {
669 messages: vec![msg],
670 action: CommandAction::None,
671 },
672 Err(e) => CommandResponse {
673 messages: vec![format!("Auth status check failed: {}", e)],
674 action: CommandAction::None,
675 },
676 },
677 Some("logout") => {
678 context.config.clawhub_token = None;
679 let _ = context.config.save(None);
680 let url = context.skill_manager.registry_url().to_string();
681 context.skill_manager.set_registry(
682 &url,
683 None,
684 );
685 CommandResponse {
686 messages: vec!["Logged out from ClawHub.".to_string()],
687 action: CommandAction::None,
688 }
689 }
690 Some(sub) => CommandResponse {
691 messages: vec![
692 format!("Unknown auth subcommand: {}", sub),
693 "Usage: /clawhub auth login|status|logout".to_string(),
694 ],
695 action: CommandAction::None,
696 },
697 None => CommandResponse {
698 messages: vec![
699 "ClawHub auth commands:".to_string(),
700 " /clawhub auth login <token> — Authenticate with API token".to_string(),
701 " /clawhub auth status — Show auth status".to_string(),
702 " /clawhub auth logout — Remove stored credentials".to_string(),
703 ],
704 action: CommandAction::None,
705 },
706 },
707 Some("search") => {
708 let query = parts[1..].join(" ");
709 if query.is_empty() {
710 return CommandResponse {
711 messages: vec!["Usage: /clawhub search <query>".to_string()],
712 action: CommandAction::None,
713 };
714 }
715 match context.skill_manager.search_registry(&query) {
716 Ok(results) => {
717 if results.is_empty() {
718 CommandResponse {
719 messages: vec![format!("No skills found matching '{}'.", query)],
720 action: CommandAction::None,
721 }
722 } else {
723 let mut msgs = vec![format!("{} result(s) for '{}':", results.len(), query)];
724 for r in &results {
725 let dl = if r.downloads > 0 {
726 format!(" (↓{})", r.downloads)
727 } else {
728 String::new()
729 };
730 msgs.push(format!(
731 " • {} v{} by {} — {}{}",
732 r.name, r.version, r.author, r.description, dl,
733 ));
734 }
735 msgs.push(String::new());
736 msgs.push("Install with: /clawhub install <name>".to_string());
737 CommandResponse {
738 messages: msgs,
739 action: CommandAction::None,
740 }
741 }
742 }
743 Err(e) => CommandResponse {
744 messages: vec![format!("Search failed: {}", e)],
745 action: CommandAction::None,
746 },
747 }
748 }
749 Some("trending") => {
750 let category = parts.get(1).copied();
751 match context.skill_manager.trending(category, Some(15)) {
752 Ok(entries) => {
753 if entries.is_empty() {
754 CommandResponse {
755 messages: vec!["No trending skills found.".to_string()],
756 action: CommandAction::None,
757 }
758 } else {
759 let header = match category {
760 Some(cat) => format!("Trending skills in '{}':", cat),
761 None => "Trending skills on ClawHub:".to_string(),
762 };
763 let mut msgs = vec![header];
764 for (i, e) in entries.iter().enumerate() {
765 msgs.push(format!(
766 " {}. {} — {} (★{} ↓{})",
767 i + 1,
768 e.name,
769 e.description,
770 e.stars,
771 e.downloads,
772 ));
773 }
774 CommandResponse {
775 messages: msgs,
776 action: CommandAction::None,
777 }
778 }
779 }
780 Err(e) => CommandResponse {
781 messages: vec![format!("Failed to fetch trending: {}", e)],
782 action: CommandAction::None,
783 },
784 }
785 }
786 Some("categories" | "cats") => match context.skill_manager.categories() {
787 Ok(cats) => {
788 if cats.is_empty() {
789 CommandResponse {
790 messages: vec!["No categories found.".to_string()],
791 action: CommandAction::None,
792 }
793 } else {
794 let mut msgs = vec!["ClawHub skill categories:".to_string()];
795 for c in &cats {
796 let count = if c.count > 0 {
797 format!(" ({})", c.count)
798 } else {
799 String::new()
800 };
801 msgs.push(format!(" • {}{} — {}", c.name, count, c.description));
802 }
803 msgs.push(String::new());
804 msgs.push("Browse by category: /clawhub trending <category>".to_string());
805 CommandResponse {
806 messages: msgs,
807 action: CommandAction::None,
808 }
809 }
810 }
811 Err(e) => CommandResponse {
812 messages: vec![format!("Failed to fetch categories: {}", e)],
813 action: CommandAction::None,
814 },
815 },
816 Some("info") => {
817 let name = parts.get(1).copied().unwrap_or("");
818 if name.is_empty() {
819 return CommandResponse {
820 messages: vec!["Usage: /clawhub info <skill_name>".to_string()],
821 action: CommandAction::None,
822 };
823 }
824 match context.skill_manager.registry_info(name) {
825 Ok(detail) => {
826 let mut msgs = vec![format!("{} v{}", detail.name, detail.version)];
827 if !detail.description.is_empty() {
828 msgs.push(format!(" {}", detail.description));
829 }
830 if !detail.author.is_empty() {
831 msgs.push(format!(" Author: {}", detail.author));
832 }
833 if !detail.license.is_empty() {
834 msgs.push(format!(" License: {}", detail.license));
835 }
836 msgs.push(format!(" ★ {} ↓ {}", detail.stars, detail.downloads));
837 if let Some(ref repo) = detail.repository {
838 msgs.push(format!(" Repo: {}", repo));
839 }
840 if !detail.categories.is_empty() {
841 msgs.push(format!(" Categories: {}", detail.categories.join(", ")));
842 }
843 if !detail.required_secrets.is_empty() {
844 msgs.push(format!(" Requires secrets: {}", detail.required_secrets.join(", ")));
845 }
846 if !detail.updated_at.is_empty() {
847 msgs.push(format!(" Updated: {}", detail.updated_at));
848 }
849 CommandResponse {
850 messages: msgs,
851 action: CommandAction::None,
852 }
853 }
854 Err(e) => CommandResponse {
855 messages: vec![format!("Failed to fetch skill info: {}", e)],
856 action: CommandAction::None,
857 },
858 }
859 }
860 Some("browse" | "open") => {
861 let url = context.skill_manager.registry_url();
862 #[cfg(target_os = "macos")]
864 let _ = std::process::Command::new("open").arg(url).spawn();
865 #[cfg(target_os = "linux")]
866 let _ = std::process::Command::new("xdg-open").arg(url).spawn();
867 #[cfg(target_os = "windows")]
868 let _ = std::process::Command::new("cmd").args(["/C", "start", url]).spawn();
869 CommandResponse {
870 messages: vec![format!("Opening {} in your browser…", url)],
871 action: CommandAction::None,
872 }
873 }
874 Some("profile" | "me") => match context.skill_manager.profile() {
875 Ok(p) => {
876 let mut msgs = vec![
877 format!("ClawHub profile: {}", p.username),
878 ];
879 if !p.display_name.is_empty() {
880 msgs.push(format!(" Name: {}", p.display_name));
881 }
882 if !p.bio.is_empty() {
883 msgs.push(format!(" Bio: {}", p.bio));
884 }
885 msgs.push(format!(" Published: {} Starred: {}", p.published_count, p.starred_count));
886 if !p.joined.is_empty() {
887 msgs.push(format!(" Joined: {}", p.joined));
888 }
889 CommandResponse {
890 messages: msgs,
891 action: CommandAction::None,
892 }
893 }
894 Err(e) => CommandResponse {
895 messages: vec![format!("Failed to fetch profile: {}", e)],
896 action: CommandAction::None,
897 },
898 },
899 Some("starred" | "stars") => match context.skill_manager.starred() {
900 Ok(entries) => {
901 if entries.is_empty() {
902 CommandResponse {
903 messages: vec!["No starred skills. Star skills with: /clawhub star <name>".to_string()],
904 action: CommandAction::None,
905 }
906 } else {
907 let mut msgs = vec![format!("{} starred skill(s):", entries.len())];
908 for e in &entries {
909 msgs.push(format!(
910 " ★ {} v{} by {} — {}",
911 e.name, e.version, e.author, e.description,
912 ));
913 }
914 CommandResponse {
915 messages: msgs,
916 action: CommandAction::None,
917 }
918 }
919 }
920 Err(e) => CommandResponse {
921 messages: vec![format!("Failed to fetch starred skills: {}", e)],
922 action: CommandAction::None,
923 },
924 },
925 Some("star") => {
926 let name = parts.get(1).copied().unwrap_or("");
927 if name.is_empty() {
928 return CommandResponse {
929 messages: vec!["Usage: /clawhub star <skill_name>".to_string()],
930 action: CommandAction::None,
931 };
932 }
933 match context.skill_manager.star(name) {
934 Ok(msg) => CommandResponse {
935 messages: vec![format!("★ {}", msg)],
936 action: CommandAction::None,
937 },
938 Err(e) => CommandResponse {
939 messages: vec![format!("Star failed: {}", e)],
940 action: CommandAction::None,
941 },
942 }
943 }
944 Some("unstar") => {
945 let name = parts.get(1).copied().unwrap_or("");
946 if name.is_empty() {
947 return CommandResponse {
948 messages: vec!["Usage: /clawhub unstar <skill_name>".to_string()],
949 action: CommandAction::None,
950 };
951 }
952 match context.skill_manager.unstar(name) {
953 Ok(msg) => CommandResponse {
954 messages: vec![msg],
955 action: CommandAction::None,
956 },
957 Err(e) => CommandResponse {
958 messages: vec![format!("Unstar failed: {}", e)],
959 action: CommandAction::None,
960 },
961 }
962 }
963 Some("install") => {
964 let name = parts.get(1).copied().unwrap_or("");
965 if name.is_empty() {
966 return CommandResponse {
967 messages: vec!["Usage: /clawhub install <name> [version]".to_string()],
968 action: CommandAction::None,
969 };
970 }
971 let version = parts.get(2).copied();
972 match context.skill_manager.install_from_registry(name, version) {
973 Ok(skill) => {
974 let _ = context.skill_manager.load_skills();
975 CommandResponse {
976 messages: vec![format!("✓ Skill '{}' installed from ClawHub.", skill.name)],
977 action: CommandAction::None,
978 }
979 }
980 Err(e) => CommandResponse {
981 messages: vec![format!("Install failed: {}", e)],
982 action: CommandAction::None,
983 },
984 }
985 }
986 Some("publish") => {
987 let name = parts.get(1).copied().unwrap_or("");
988 if name.is_empty() {
989 return CommandResponse {
990 messages: vec!["Usage: /clawhub publish <name>".to_string()],
991 action: CommandAction::None,
992 };
993 }
994 match context.skill_manager.publish_to_registry(name) {
995 Ok(msg) => CommandResponse {
996 messages: vec![format!("✓ {}", msg)],
997 action: CommandAction::None,
998 },
999 Err(e) => CommandResponse {
1000 messages: vec![format!("Publish failed: {}", e)],
1001 action: CommandAction::None,
1002 },
1003 }
1004 }
1005 Some(sub) => CommandResponse {
1006 messages: vec![
1007 format!("Unknown clawhub subcommand: {}", sub),
1008 "Type /clawhub for available commands.".to_string(),
1009 ],
1010 action: CommandAction::None,
1011 },
1012 None => CommandResponse {
1013 messages: vec![
1014 "ClawHub — Skill Registry".to_string(),
1015 format!(" Registry: {}", context.skill_manager.registry_url()),
1016 String::new(),
1017 " /clawhub auth — Authentication commands".to_string(),
1018 " /clawhub search <query> — Search for skills".to_string(),
1019 " /clawhub trending [category] — Browse trending skills".to_string(),
1020 " /clawhub categories — List skill categories".to_string(),
1021 " /clawhub info <name> — Show skill details".to_string(),
1022 " /clawhub browse — Open ClawHub in browser".to_string(),
1023 " /clawhub profile — Show your profile".to_string(),
1024 " /clawhub starred — List your starred skills".to_string(),
1025 " /clawhub star <name> — Star a skill".to_string(),
1026 " /clawhub unstar <name> — Unstar a skill".to_string(),
1027 " /clawhub install <name> — Install a skill".to_string(),
1028 " /clawhub publish <name> — Publish a local skill".to_string(),
1029 ],
1030 action: CommandAction::None,
1031 },
1032 }
1033}