lean-ctx 3.7.3

Context Runtime for AI Agents with CCP. 68 MCP tools, 10 read modes, 60+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use crate::core::plugins::{executor::HookPoint, registry::PluginRegistry, PluginManager};

pub fn handle(action: &str, name: Option<&str>) -> String {
    match action {
        "list" => handle_list(),
        "enable" => handle_enable(name),
        "disable" => handle_disable(name),
        "info" => handle_info(name),
        "hooks" => handle_hooks(),
        _ => format!("Unknown action: {action}. Valid: list, enable, disable, info, hooks"),
    }
}

fn handle_list() -> String {
    let mut registry = PluginRegistry::from_default_dir();
    let errors = registry.discover();

    let mut out = String::new();
    if !errors.is_empty() {
        for err in &errors {
            out.push_str(&format!("{}: {}\n", err.path.display(), err.error));
        }
        out.push('\n');
    }

    let plugins = registry.list();
    if plugins.is_empty() {
        out.push_str("No plugins installed.\n");
        out.push_str(&format!(
            "Plugin directory: {}\n",
            registry.plugin_dir().display()
        ));
        return out;
    }

    out.push_str(&format!("{} plugin(s):\n\n", plugins.len()));
    for plugin in &plugins {
        let status = if plugin.enabled {
            "enabled"
        } else {
            "disabled"
        };
        let hooks_count = plugin.manifest.hooks.len();
        out.push_str(&format!(
            "{} v{} [{status}] ({hooks_count} hook{})\n",
            plugin.manifest.plugin.name,
            plugin.manifest.plugin.version,
            if hooks_count == 1 { "" } else { "s" }
        ));
        if !plugin.manifest.plugin.description.is_empty() {
            out.push_str(&format!("  {}\n", plugin.manifest.plugin.description));
        }
    }
    out
}

fn handle_enable(name: Option<&str>) -> String {
    let Some(name) = name else {
        return "Error: 'name' parameter required for enable action".to_string();
    };
    PluginManager::init();
    match PluginManager::with_registry_mut(|reg| reg.enable(name)) {
        Some(Ok(())) => format!("Enabled plugin: {name}"),
        Some(Err(e)) => format!("Error: {e}"),
        None => "Error: plugin registry not initialized".to_string(),
    }
}

fn handle_disable(name: Option<&str>) -> String {
    let Some(name) = name else {
        return "Error: 'name' parameter required for disable action".to_string();
    };
    PluginManager::init();
    match PluginManager::with_registry_mut(|reg| reg.disable(name)) {
        Some(Ok(())) => format!("Disabled plugin: {name}"),
        Some(Err(e)) => format!("Error: {e}"),
        None => "Error: plugin registry not initialized".to_string(),
    }
}

fn handle_info(name: Option<&str>) -> String {
    let Some(name) = name else {
        return "Error: 'name' parameter required for info action".to_string();
    };
    let mut registry = PluginRegistry::from_default_dir();
    registry.discover();

    match registry.get(name) {
        Some(plugin) => {
            let mut out = String::new();
            out.push_str(&format!("Plugin: {}\n", plugin.manifest.plugin.name));
            out.push_str(&format!("Version: {}\n", plugin.manifest.plugin.version));
            if !plugin.manifest.plugin.description.is_empty() {
                out.push_str(&format!(
                    "Description: {}\n",
                    plugin.manifest.plugin.description
                ));
            }
            if !plugin.manifest.plugin.author.is_empty() {
                out.push_str(&format!("Author: {}\n", plugin.manifest.plugin.author));
            }
            out.push_str(&format!("Enabled: {}\n", plugin.enabled));
            out.push_str(&format!("Path: {}\n", plugin.path.display()));
            if !plugin.manifest.hooks.is_empty() {
                out.push_str("\nHooks:\n");
                for (hook_name, entry) in &plugin.manifest.hooks {
                    out.push_str(&format!(
                        "  {hook_name}: {} (timeout: {}ms)\n",
                        entry.command, entry.timeout_ms
                    ));
                }
            }
            out
        }
        None => format!("Plugin not found: {name}"),
    }
}

fn handle_hooks() -> String {
    let mut out = String::from("Available hook points:\n\n");
    for name in HookPoint::all_hook_names() {
        let desc = match *name {
            "on_session_start" => "Called when a new session begins",
            "on_session_end" => "Called when a session ends",
            "pre_read" => "Called before a file is read (stdin: {\"hook\":\"pre_read\",\"path\":\"...\"})",
            "post_compress" => "Called after compression (stdin: {\"hook\":\"post_compress\",\"path\":\"...\",\"original_tokens\":N,\"compressed_tokens\":N})",
            "on_knowledge_update" => "Called when knowledge is updated (stdin: {\"hook\":\"on_knowledge_update\",\"fact_id\":\"...\"})",
            _ => "",
        };
        out.push_str(&format!("{name}\n  {desc}\n\n"));
    }
    out
}