lean_ctx/tools/
ctx_plugins.rs1use crate::core::plugins::{executor::HookPoint, registry::PluginRegistry, PluginManager};
2
3pub fn handle(action: &str, name: Option<&str>) -> String {
4 match action {
5 "list" => handle_list(),
6 "enable" => handle_enable(name),
7 "disable" => handle_disable(name),
8 "info" => handle_info(name),
9 "hooks" => handle_hooks(),
10 _ => format!("Unknown action: {action}. Valid: list, enable, disable, info, hooks"),
11 }
12}
13
14fn handle_list() -> String {
15 let mut registry = PluginRegistry::from_default_dir();
16 let errors = registry.discover();
17
18 let mut out = String::new();
19 if !errors.is_empty() {
20 for err in &errors {
21 out.push_str(&format!("⚠ {}: {}\n", err.path.display(), err.error));
22 }
23 out.push('\n');
24 }
25
26 let plugins = registry.list();
27 if plugins.is_empty() {
28 out.push_str("No plugins installed.\n");
29 out.push_str(&format!(
30 "Plugin directory: {}\n",
31 registry.plugin_dir().display()
32 ));
33 return out;
34 }
35
36 out.push_str(&format!("{} plugin(s):\n\n", plugins.len()));
37 for plugin in &plugins {
38 let status = if plugin.enabled {
39 "enabled"
40 } else {
41 "disabled"
42 };
43 let hooks_count = plugin.manifest.hooks.len();
44 out.push_str(&format!(
45 "• {} v{} [{status}] ({hooks_count} hook{})\n",
46 plugin.manifest.plugin.name,
47 plugin.manifest.plugin.version,
48 if hooks_count == 1 { "" } else { "s" }
49 ));
50 if !plugin.manifest.plugin.description.is_empty() {
51 out.push_str(&format!(" {}\n", plugin.manifest.plugin.description));
52 }
53 }
54 out
55}
56
57fn handle_enable(name: Option<&str>) -> String {
58 let Some(name) = name else {
59 return "Error: 'name' parameter required for enable action".to_string();
60 };
61 PluginManager::init();
62 match PluginManager::with_registry_mut(|reg| reg.enable(name)) {
63 Some(Ok(())) => format!("Enabled plugin: {name}"),
64 Some(Err(e)) => format!("Error: {e}"),
65 None => "Error: plugin registry not initialized".to_string(),
66 }
67}
68
69fn handle_disable(name: Option<&str>) -> String {
70 let Some(name) = name else {
71 return "Error: 'name' parameter required for disable action".to_string();
72 };
73 PluginManager::init();
74 match PluginManager::with_registry_mut(|reg| reg.disable(name)) {
75 Some(Ok(())) => format!("Disabled plugin: {name}"),
76 Some(Err(e)) => format!("Error: {e}"),
77 None => "Error: plugin registry not initialized".to_string(),
78 }
79}
80
81fn handle_info(name: Option<&str>) -> String {
82 let Some(name) = name else {
83 return "Error: 'name' parameter required for info action".to_string();
84 };
85 let mut registry = PluginRegistry::from_default_dir();
86 registry.discover();
87
88 match registry.get(name) {
89 Some(plugin) => {
90 let mut out = String::new();
91 out.push_str(&format!("Plugin: {}\n", plugin.manifest.plugin.name));
92 out.push_str(&format!("Version: {}\n", plugin.manifest.plugin.version));
93 if !plugin.manifest.plugin.description.is_empty() {
94 out.push_str(&format!(
95 "Description: {}\n",
96 plugin.manifest.plugin.description
97 ));
98 }
99 if !plugin.manifest.plugin.author.is_empty() {
100 out.push_str(&format!("Author: {}\n", plugin.manifest.plugin.author));
101 }
102 out.push_str(&format!("Enabled: {}\n", plugin.enabled));
103 out.push_str(&format!("Path: {}\n", plugin.path.display()));
104 if !plugin.manifest.hooks.is_empty() {
105 out.push_str("\nHooks:\n");
106 for (hook_name, entry) in &plugin.manifest.hooks {
107 out.push_str(&format!(
108 " {hook_name}: {} (timeout: {}ms)\n",
109 entry.command, entry.timeout_ms
110 ));
111 }
112 }
113 out
114 }
115 None => format!("Plugin not found: {name}"),
116 }
117}
118
119fn handle_hooks() -> String {
120 let mut out = String::from("Available hook points:\n\n");
121 for name in HookPoint::all_hook_names() {
122 let desc = match *name {
123 "on_session_start" => "Called when a new session begins",
124 "on_session_end" => "Called when a session ends",
125 "pre_read" => "Called before a file is read (stdin: {\"hook\":\"pre_read\",\"path\":\"...\"})",
126 "post_compress" => "Called after compression (stdin: {\"hook\":\"post_compress\",\"path\":\"...\",\"original_tokens\":N,\"compressed_tokens\":N})",
127 "on_knowledge_update" => "Called when knowledge is updated (stdin: {\"hook\":\"on_knowledge_update\",\"fact_id\":\"...\"})",
128 _ => "",
129 };
130 out.push_str(&format!("• {name}\n {desc}\n\n"));
131 }
132 out
133}