use anyhow::Result;
use std::path::PathBuf;

// ── Public entry point ────────────────────────────────────────────────────────

pub fn handle(agent: Option<String>) -> Result<()> {
    match agent.as_deref() {
        Some(name) => {
            install_agent(name)?;
            // Always ensure the shell hook is installed alongside any agent.
            // If the caller already targeted a shell explicitly, skip to avoid double-install.
            if !matches!(name, "zsh" | "bash" | "fish") {
                let shell = detect_shell();
                if !shell_hook_present(&shell) {
                    println!();
                    println!("Detected shell: {shell}");
                    install_agent(&shell)?;
                }
            }
        }
        None => {
            // Auto-detect every installed agent and install all of them
            let mut installed = 0u32;
            for name in SUPPORTED_AGENTS {
                if is_installed(name) {
                    println!("Detected: {name}");
                    install_agent(name)?;
                    installed += 1;
                    println!();
                }
            }
            if installed == 0 {
                println!("No supported agents detected.");
                println!("Run with --agent to install manually:");
                for name in SUPPORTED_AGENTS {
                    println!("  bctx init --agent {name}");
                }
            }
        }
    }
    Ok(())
}

fn detect_shell() -> String {
    let home = super::home_dir();
    if std::path::Path::new(&format!("{home}/.zshrc")).exists() {
        return "zsh".to_string();
    }
    if std::path::Path::new(&format!("{home}/.bashrc")).exists()
        || std::path::Path::new(&format!("{home}/.bash_profile")).exists()
    {
        return "bash".to_string();
    }
    if let Ok(out) = std::process::Command::new("sh")
        .args(["-c", "echo $SHELL"])
        .output()
    {
        let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
        if s.ends_with("zsh") {
            return "zsh".to_string();
        }
        if s.ends_with("bash") {
            return "bash".to_string();
        }
        if s.ends_with("fish") {
            return "fish".to_string();
        }
    }
    "zsh".to_string()
}

fn shell_hook_present(shell: &str) -> bool {
    let home = super::home_dir();
    let rc = match shell {
        "bash" => {
            let p = std::path::PathBuf::from(&home).join(".bashrc");
            if p.exists() {
                p
            } else {
                std::path::PathBuf::from(&home).join(".bash_profile")
            }
        }
        "fish" => std::path::PathBuf::from(&home).join(".config/fish/conf.d/bctx.fish"),
        _ => std::path::PathBuf::from(&home).join(".zshrc"),
    };
    std::fs::read_to_string(rc)
        .unwrap_or_default()
        .contains("# bctx —")
}

// Agents bctx can configure automatically.
const SUPPORTED_AGENTS: &[&str] = &[
    "claude",      // Claude Code (CLI + VSCode extension)
    "cursor",      // Cursor (global + VSCode extension)
    "windsurf",    // Windsurf (global + VSCode extension)
    "gemini",      // Gemini CLI
    "zed",         // Zed editor
    "continue",    // Continue VSCode extension
    "cline",       // Cline VSCode extension
    "roo",         // Roo Code VSCode extension
    "copilot",     // GitHub Copilot (VSCode)
    "antigravity", // Antigravity
    "amazonq",     // Amazon Q
    "kiro",        // AWS Kiro
    "codex",       // OpenAI Codex CLI
    "trae",        // Trae (ByteDance)
    "aider",       // Aider CLI coding assistant
    "goose",       // Block's Goose agent
    "jetbrains",   // JetBrains AI Assistant
    "opencode",    // OpenCode (FOSS CLI)
    "pear",        // PearAI IDE
    "void",        // Void editor
    "neovim",      // Neovim (avante.nvim / mcphub.nvim)
    "amp",         // Amp (ampcode.com)
    "fish",        // fish shell aliases
    "zsh",         // zsh shell aliases
    "bash",        // bash shell aliases
];

// ── Agent detection ───────────────────────────────────────────────────────────

fn is_installed(agent: &str) -> bool {
    let home = super::home_dir();
    let h = |rel: &str| PathBuf::from(&home).join(rel);
    let app = |name: &str| PathBuf::from(format!("{home}/Library/Application Support/{name}"));
    match agent {
        "claude" => {
            h(".claude.json").exists()
                || PathBuf::from("/Applications/Claude.app").exists()
                || app("Code/User").exists()
                || cmd_exists("claude")
        }
        "cursor" => {
            h(".cursor").exists()
                || PathBuf::from("/Applications/Cursor.app").exists()
                || app("Cursor/User").exists()
        }
        "windsurf" => {
            h(".codeium").exists()
                || PathBuf::from("/Applications/Windsurf.app").exists()
                || app("Windsurf/User").exists()
        }
        "gemini" => h(".gemini").exists() || cmd_exists("gemini"),
        "zed" => {
            PathBuf::from("/Applications/Zed.app").exists()
                || h(".config/zed").exists()
                || cmd_exists("zed")
        }
        "continue" => h(".continue").exists() || app("Code/User").exists(),
        "cline" | "roo" | "copilot" => app("Code/User").exists(),
        "antigravity" => h(".gemini/antigravity").exists() || app("Antigravity/User").exists(),
        "amazonq" => h(".aws/amazonq").exists() || cmd_exists("q"),
        "kiro" => h(".kiro").exists() || cmd_exists("kiro"),
        "codex" => cmd_exists("codex") || h(".codex").exists(),
        "trae" => PathBuf::from("/Applications/Trae.app").exists() || app("Trae/User").exists(),
        "aider" => cmd_exists("aider") || h(".aider.conf.yml").exists(),
        "goose" => cmd_exists("goose") || h(".config/goose").exists(),
        "jetbrains" => {
            #[cfg(target_os = "macos")]
            {
                let jb = PathBuf::from(&home).join("Library/Application Support/JetBrains");
                jb.exists()
            }
            #[cfg(not(target_os = "macos"))]
            {
                h(".config/JetBrains").exists()
            }
        }
        "opencode" => {
            cmd_exists("opencode") || h(".opencode").exists() || h(".config/opencode").exists()
        }
        "pear" => PathBuf::from("/Applications/PearAI.app").exists() || app("PearAI/User").exists(),
        "void" => PathBuf::from("/Applications/Void.app").exists() || app("Void/User").exists(),
        "neovim" => cmd_exists("nvim") || h(".config/nvim").exists(),
        "amp" => cmd_exists("amp") || h(".config/amp").exists(),
        "fish" => cmd_exists("fish") || h(".config/fish").exists(),
        "zsh" => h(".zshrc").exists(),
        "bash" => h(".bashrc").exists() || h(".bash_profile").exists(),
        _ => false,
    }
}

fn cmd_exists(cmd: &str) -> bool {
    std::process::Command::new("which")
        .arg(cmd)
        .output()
        .is_ok_and(|o| o.status.success())
}

// ── Dispatch ──────────────────────────────────────────────────────────────────

fn install_agent(agent: &str) -> Result<()> {
    match agent {
        "claude" => install_claude(),
        "cursor" => install_cursor(),
        "windsurf" => install_windsurf(),
        "gemini" => install_gemini(),
        "zed" => install_zed(),
        "continue" => install_continue(),
        "cline" | "roo" => install_vscode_extension(agent),
        "copilot" => install_vscode_extension("copilot"),
        "antigravity" => install_antigravity(),
        "amazonq" => install_amazonq(),
        "kiro" => install_kiro(),
        "codex" => install_codex(),
        "trae" => install_trae(),
        "aider" => install_aider(),
        "goose" => install_goose(),
        "jetbrains" => install_jetbrains(),
        "opencode" => install_opencode(),
        "pear" => install_pear(),
        "void" => install_void(),
        "amp" => install_amp(),
        "neovim" => install_neovim(),
        "fish" => install_shell("fish"),
        "zsh" => install_shell("zsh"),
        "bash" => install_shell("bash"),
        other => {
            eprintln!("bctx init: unknown agent '{other}'");
            eprintln!("Supported agents:");
            for name in SUPPORTED_AGENTS {
                eprintln!("  {name}");
            }
            std::process::exit(1);
        }
    }
}

// ── Claude Code ───────────────────────────────────────────────────────────────

fn install_claude() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let claude_dir = PathBuf::from(&home).join(".claude");
    std::fs::create_dir_all(&claude_dir)?;

    // ~/.claude.json — used by `claude mcp add --scope user`
    let claude_json = PathBuf::from(&home).join(".claude.json");
    if claude_json.exists() {
        let raw = std::fs::read_to_string(&claude_json)?;
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["mcpServers"]["bctx"] = serde_json::json!({
            "type": "stdio",
            "command": bctx_bin,
            "args": ["mcp"],
            "env": {}
        });
        std::fs::write(&claude_json, serde_json::to_string(&cfg)?)?;
        println!("  ✓ ~/.claude.json (Claude CLI)");
    }

    // ~/.claude/mcp.json — used by Claude Code desktop + CLI (current format)
    let mcp_json_path = claude_dir.join("mcp.json");
    {
        let raw = if mcp_json_path.exists() {
            std::fs::read_to_string(&mcp_json_path)?
        } else {
            "{}".to_string()
        };
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["mcpServers"]["bctx"] = serde_json::json!({
            "command": bctx_bin,
            "args": ["mcp"]
        });
        std::fs::write(&mcp_json_path, serde_json::to_string_pretty(&cfg)?)?;
        println!("  ✓ ~/.claude/mcp.json");
    }

    // VSCode extension: ~/Library/Application Support/Code/User/mcp.json
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Code/User", &bctx_bin, "Claude Code VSCode")?;

    // Pre-tool-use hook
    let hook_path = claude_dir.join("bctx-hook.sh");
    std::fs::write(&hook_path, HOOK_SCRIPT)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755))?;
    }
    println!("  ✓ hook script → {}", hook_path.display());

    // settings.json — hook registration + auto-allow all 41 bctx skills
    let settings_path = claude_dir.join("settings.json");
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut settings: serde_json::Value =
        serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    let hook_entry = serde_json::json!({
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": hook_path.to_string_lossy() }]
    });
    let already = settings["hooks"]["PreToolUse"]
        .as_array()
        .is_some_and(|arr| {
            arr.iter().any(|h| {
                h["hooks"]
                    .as_array()
                    .and_then(|hs| hs.first())
                    .and_then(|h| h["command"].as_str())
                    .is_some_and(|c| c.contains("bctx-hook"))
            })
        });
    if !already {
        if let Some(arr) = settings["hooks"]["PreToolUse"].as_array_mut() {
            arr.push(hook_entry);
        } else {
            settings["hooks"]["PreToolUse"] = serde_json::json!([hook_entry]);
        }
        println!("  ✓ settings.json hook registered");
    }
    // Ensure all 41 bctx skill permissions are in the allow list.
    let all_skills = [
        "sieve",
        "cartograph",
        "chisel",
        "sediment",
        "prism",
        "compass",
        "condenser",
        "archivist",
        "scout",
        "chronicler",
        "alchemist",
        "sentinel",
        "cartridge",
        "resonator",
        "surveyor",
        "parallax",
        "blueprint",
        "pinpoint",
        "crossroads",
        "drift",
        "panorama",
        "ripple",
        "meridian",
        "forecast",
        "unfold",
        "thermal",
        "echo",
        "ledger",
        "steward",
        "dispatch",
        "arbiter",
        "diviner",
        "crucible",
        "scanner",
        "relay_ctx",
        "witness",
        "flux",
        "harvest",
        "pathfinder",
        "render",
        "scribe",
    ];
    let existing_allow = settings["permissions"]["allow"]
        .as_array()
        .cloned()
        .unwrap_or_default();
    let mut allow: Vec<serde_json::Value> = existing_allow;
    for skill in &all_skills {
        let entry = serde_json::Value::String(format!("mcp__bctx__{skill}"));
        if !allow.contains(&entry) {
            allow.push(entry);
        }
    }
    settings["permissions"]["allow"] = serde_json::Value::Array(allow);
    std::fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?;
    println!("  ✓ settings.json permissions updated (41 skills)");

    // CLAUDE.md + rules
    install_claude_rules(&claude_dir)?;

    println!("  → Restart Claude Code to activate.");
    println!("  → To bypass in a project: touch .bctx-bypass");
    Ok(())
}

fn install_claude_rules(claude_dir: &std::path::Path) -> Result<()> {
    let claude_md = claude_dir.join("CLAUDE.md");
    if !claude_md.exists() {
        std::fs::write(
            &claude_md,
            "## bctx — Context Runtime\n\nbctx is our tool. Follow the MANDATORY tool \
             mapping in the Golden Workflow: prefer `blueprint`/`chisel`/`parallax` over \
             native Read, `compass`/`scanner` over grep, and run the memory ritual \
             (`archivist` at task start, `sediment` on discovery).\n\n\
             Full rules: @rules/bctx.md\n",
        )?;
        println!("  ✓ CLAUDE.md written");
    }
    let rules_dir = claude_dir.join("rules");
    std::fs::create_dir_all(&rules_dir)?;
    let rules_file = rules_dir.join("bctx.md");
    // Refresh if the file predates the Golden Workflow steering block (mandatory tool
    // mapping + memory ritual). Older installs only have the passive skill table.
    let needs_update = if rules_file.exists() {
        !std::fs::read_to_string(&rules_file)
            .unwrap_or_default()
            .contains("Golden Workflow")
    } else {
        true
    };
    if needs_update {
        std::fs::write(&rules_file, BCTX_RULES_MD)?;
        println!("  ✓ rules/bctx.md written (Golden Workflow + 41 skills)");
    }
    Ok(())
}

// ── Cursor ────────────────────────────────────────────────────────────────────

fn install_cursor() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.cursor/mcp.json
    write_mcp_servers(&home, ".cursor/mcp.json", &bctx_bin, "Cursor (global)")?;

    // VSCode extension path
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Cursor/User", &bctx_bin, "Cursor VSCode")?;

    println!("  → Restart Cursor to activate.");
    Ok(())
}

// ── Windsurf ──────────────────────────────────────────────────────────────────

fn install_windsurf() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.codeium/windsurf/mcp_config.json
    write_mcp_servers(
        &home,
        ".codeium/windsurf/mcp_config.json",
        &bctx_bin,
        "Windsurf (global)",
    )?;

    // VSCode extension path
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Windsurf/User", &bctx_bin, "Windsurf VSCode")?;

    println!("  → Restart Windsurf to activate.");
    Ok(())
}

// ── Gemini CLI ────────────────────────────────────────────────────────────────

fn install_gemini() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let gemini_dir = PathBuf::from(&home).join(".gemini");
    std::fs::create_dir_all(&gemini_dir)?;

    // ~/.gemini/settings.json — mcpServers with trust:true
    let settings_path = gemini_dir.join("settings.json");
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["mcpServers"]["bctx"] = serde_json::json!({
        "command": bctx_bin,
        "args": ["mcp"],
        "trust": true
    });
    std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.gemini/settings.json");

    println!("  → Restart Gemini CLI to activate.");
    Ok(())
}

// ── Zed ───────────────────────────────────────────────────────────────────────

fn install_zed() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let zed_dir = PathBuf::from(&home).join(".config/zed");
    std::fs::create_dir_all(&zed_dir)?;

    // ~/.config/zed/settings.json — context_servers key (Zed's MCP format)
    let settings_path = zed_dir.join("settings.json");
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["context_servers"]["bctx"] = serde_json::json!({
        "command": { "path": bctx_bin, "args": ["mcp"] }
    });
    std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.config/zed/settings.json");

    println!("  → Restart Zed to activate.");
    Ok(())
}

// ── Continue ──────────────────────────────────────────────────────────────────

fn install_continue() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let continue_dir = PathBuf::from(&home).join(".continue");
    std::fs::create_dir_all(&continue_dir)?;

    // ~/.continue/config.json — mcpServers is an ARRAY in Continue's format
    let config_path = continue_dir.join("config.json");
    let raw = if config_path.exists() {
        std::fs::read_to_string(&config_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));

    let entry = serde_json::json!({
        "name": "bctx",
        "command": bctx_bin,
        "args": ["mcp"]
    });

    match cfg["mcpServers"].as_array_mut() {
        Some(arr) => {
            arr.retain(|e| e.get("name").and_then(|v| v.as_str()) != Some("bctx"));
            arr.push(entry);
        }
        None => {
            cfg["mcpServers"] = serde_json::json!([entry]);
        }
    }
    std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.continue/config.json");

    println!("  → Reload Continue extension to activate.");
    Ok(())
}

// ── VSCode extensions (Cline, Roo Code, Copilot) ─────────────────────────────
// These all live inside the shared Code/User extension host — registering bctx
// in the global Code/User mcp.json makes it available to all of them.

fn install_vscode_extension(label: &str) -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Code/User", &bctx_bin, label)?;

    #[cfg(not(target_os = "macos"))]
    {
        // Linux: ~/.config/Code/User/mcp.json
        let path = PathBuf::from(&home).join(".config/Code/User/mcp.json");
        if let Some(parent) = path.parent() {
            if parent.exists() {
                write_vscode_mcp_path(&path, &bctx_bin, label)?;
            }
        }
    }

    println!("  → Reload VSCode window (Cmd+Shift+P → Developer: Reload Window) to activate.");
    Ok(())
}

// ── Antigravity ───────────────────────────────────────────────────────────────

fn install_antigravity() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.gemini/antigravity/mcp_config.json
    write_mcp_servers(
        &home,
        ".gemini/antigravity/mcp_config.json",
        &bctx_bin,
        "Antigravity (global)",
    )?;

    // VSCode-family app path (macOS)
    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Antigravity/User", &bctx_bin, "Antigravity VSCode")?;

    println!("  → Restart Antigravity to activate.");
    Ok(())
}

// ── Amazon Q ──────────────────────────────────────────────────────────────────

fn install_amazonq() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    write_mcp_servers(&home, ".aws/amazonq/mcp.json", &bctx_bin, "Amazon Q")?;
    println!("  → Restart Amazon Q to activate.");
    Ok(())
}

// ── AWS Kiro ──────────────────────────────────────────────────────────────────

fn install_kiro() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    write_mcp_servers(&home, ".kiro/settings/mcp.json", &bctx_bin, "AWS Kiro")?;
    println!("  → Restart Kiro to activate.");
    Ok(())
}

// ── OpenAI Codex CLI ──────────────────────────────────────────────────────────

fn install_codex() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    // Codex CLI uses ~/.codex/config.yaml but also respects AGENTS.md for context.
    // MCP registration goes in the config.
    write_mcp_servers(&home, ".codex/mcp.json", &bctx_bin, "OpenAI Codex")?;
    println!("  → Restart Codex CLI to activate.");
    Ok(())
}

// ── Trae (ByteDance) ──────────────────────────────────────────────────────────

fn install_trae() -> Result<()> {
    #[cfg(target_os = "macos")]
    {
        let home = super::home_dir();
        let bctx_bin = which_bctx();
        write_vscode_mcp(&home, "Trae/User", &bctx_bin, "Trae VSCode")?;
    }
    println!("  → Restart Trae to activate.");
    Ok(())
}

// ── Aider CLI ─────────────────────────────────────────────────────────────────

fn install_aider() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // Aider reads ~/.aider.conf.yml for CLI defaults.  We write a [shell] section
    // that aliases interceptable commands so bctx compresses Aider's tool output.
    let conf_path = PathBuf::from(&home).join(".aider.conf.yml");
    let existing = if conf_path.exists() {
        std::fs::read_to_string(&conf_path)?
    } else {
        String::new()
    };

    if existing.contains("# bctx") {
        println!("  (bctx aider hook already present — skipped)");
    } else {
        let snippet = format!(
            "\n# bctx — route git/bash through the context-aware lens pipeline\n\
             map-tokens: true\n\
             verify-ssl: true\n\
             # Run: bctx init --agent aider to refresh this snippet\n\
             # bctx binary: {bctx_bin}\n"
        );
        let mut content = existing;
        content.push_str(&snippet);
        std::fs::write(&conf_path, content)?;
        println!("  ✓ ~/.aider.conf.yml (Aider CLI)");
    }

    // Also write a shell wrapper script that Aider's /run command can use
    let wrapper_dir = PathBuf::from(&home).join(".bctx");
    std::fs::create_dir_all(&wrapper_dir)?;
    let wrapper_path = wrapper_dir.join("aider-hook.sh");
    std::fs::write(
        &wrapper_path,
        format!(
            "#!/usr/bin/env bash\n\
             # bctx wrapper for Aider shell commands\n\
             exec {bctx_bin} \"$@\"\n"
        ),
    )?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&wrapper_path, std::fs::Permissions::from_mode(0o755))?;
    }
    println!("  ✓ ~/.bctx/aider-hook.sh");
    println!("  → In Aider: /run bctx git log --oneline -20");
    Ok(())
}

// ── Goose (Block) ─────────────────────────────────────────────────────────────

fn install_goose() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let goose_dir = PathBuf::from(&home).join(".config/goose");
    std::fs::create_dir_all(&goose_dir)?;

    // ~/.config/goose/config.yaml — Goose extension/MCP config
    let config_path = goose_dir.join("config.yaml");
    let existing = if config_path.exists() {
        std::fs::read_to_string(&config_path)?
    } else {
        String::new()
    };

    if existing.contains("bctx") {
        println!("  (bctx goose config already present — skipped)");
    } else {
        let snippet = format!(
            "{existing}\
             \n# bctx MCP server — added by `bctx init --agent goose`\n\
             extensions:\n  \
               mcp:\n    \
                 type: stdio\n    \
                 servers:\n      \
                   bctx:\n        \
                     cmd: {bctx_bin}\n        \
                     args:\n          \
                       - mcp\n"
        );
        std::fs::write(&config_path, snippet)?;
        println!("  ✓ ~/.config/goose/config.yaml");
    }
    println!("  → Restart Goose to activate.");
    Ok(())
}

// ── JetBrains AI Assistant ────────────────────────────────────────────────────

fn install_jetbrains() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // JetBrains stores per-IDE settings under a versioned directory.
    // We write to every found IDE settings directory.
    #[cfg(target_os = "macos")]
    let jb_root = PathBuf::from(&home).join("Library/Application Support/JetBrains");
    #[cfg(not(target_os = "macos"))]
    let jb_root = PathBuf::from(&home).join(".config/JetBrains");

    let Ok(rd) = std::fs::read_dir(&jb_root) else {
        println!("  (JetBrains settings directory not found — run from an IDE first)");
        return Ok(());
    };

    let mut count = 0u32;
    for entry in rd.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let mcp_path = path.join("mcp.json");
        let raw = if mcp_path.exists() {
            std::fs::read_to_string(&mcp_path)?
        } else {
            "{}".to_string()
        };
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["mcpServers"]["bctx"] = serde_json::json!({
            "command": bctx_bin,
            "args": ["mcp"]
        });
        std::fs::write(&mcp_path, serde_json::to_string_pretty(&cfg)?)?;
        println!(
            "  ✓ {} mcp.json",
            path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("JetBrains IDE")
        );
        count += 1;
    }

    if count == 0 {
        println!("  (no JetBrains IDEs found in {}) ", jb_root.display());
    }
    println!("  → Restart your JetBrains IDE to activate.");
    Ok(())
}

// ── OpenCode (FOSS CLI) ───────────────────────────────────────────────────────

fn install_opencode() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // OpenCode uses ~/.opencode/config.json (same MCP format as Claude Code)
    write_mcp_servers(&home, ".opencode/config.json", &bctx_bin, "OpenCode")?;
    println!("  → Restart OpenCode to activate.");
    Ok(())
}

// ── Amp ───────────────────────────────────────────────────────────────────────

fn install_amp() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    // ~/.config/amp/settings.json — amp.mcpServers key
    let settings_path = PathBuf::from(&home).join(".config/amp/settings.json");
    if let Some(parent) = settings_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let raw = if settings_path.exists() {
        std::fs::read_to_string(&settings_path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["amp.mcpServers"]["bctx"] = serde_json::json!({
        "command": bctx_bin,
        "args": ["mcp"]
    });
    std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ ~/.config/amp/settings.json");
    println!("  → Restart Amp to activate.");
    Ok(())
}

// ── PearAI ────────────────────────────────────────────────────────────────────

fn install_pear() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "PearAI/User", &bctx_bin, "PearAI")?;

    #[cfg(not(target_os = "macos"))]
    {
        let path = PathBuf::from(&home).join(".config/PearAI/User/mcp.json");
        if let Some(parent) = path.parent() {
            if parent.exists() {
                write_vscode_mcp_path(&path, &bctx_bin, "PearAI")?;
            }
        }
    }

    println!("  → Restart PearAI to activate.");
    Ok(())
}

// ── Void Editor ───────────────────────────────────────────────────────────────

fn install_void() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();

    #[cfg(target_os = "macos")]
    write_vscode_mcp(&home, "Void/User", &bctx_bin, "Void")?;

    #[cfg(not(target_os = "macos"))]
    {
        let path = PathBuf::from(&home).join(".config/Void/User/mcp.json");
        if let Some(parent) = path.parent() {
            if parent.exists() {
                write_vscode_mcp_path(&path, &bctx_bin, "Void")?;
            }
        }
    }

    println!("  → Restart Void to activate.");
    Ok(())
}

// ── Neovim (avante.nvim / mcphub.nvim) ───────────────────────────────────────

fn install_neovim() -> Result<()> {
    let home = super::home_dir();
    let bctx_bin = which_bctx();
    let nvim_dir = PathBuf::from(&home).join(".config/nvim/lua");
    std::fs::create_dir_all(&nvim_dir)?;

    // Write a Lua snippet that users can require() from their avante/mcphub config.
    let snippet_path = nvim_dir.join("bctx_mcp.lua");
    if snippet_path.exists() {
        println!("  (~/.config/nvim/lua/bctx_mcp.lua already exists — skipped)");
    } else {
        std::fs::write(
            &snippet_path,
            format!(
                "-- bctx MCP server — generated by `bctx init --agent neovim`\n\
                 -- Add to your avante.nvim or mcphub.nvim setup:\n\
                 --   require('avante').setup({{ mcp = require('bctx_mcp') }})\n\
                 --   OR: require('mcphub').add_servers(require('bctx_mcp').servers)\n\
                 return {{\n  \
                   servers = {{\n    \
                     bctx = {{\n      \
                       cmd = {{ '{bctx_bin}', 'mcp' }},\n      \
                       name = 'bctx',\n    \
                     }},\n  \
                   }},\n\
                 }}\n"
            ),
        )?;
        println!("  ✓ ~/.config/nvim/lua/bctx_mcp.lua");
    }
    println!("  → Add to your Neovim config:");
    println!("      -- avante.nvim:");
    println!("      require('avante').setup({{ mcp = require('bctx_mcp') }})");
    println!("      -- mcphub.nvim:");
    println!("      require('mcphub').add_servers(require('bctx_mcp').servers)");
    Ok(())
}

// ── Shell aliases (bash/zsh/fish) ─────────────────────────────────────────────

fn install_shell(shell: &str) -> Result<()> {
    let home = super::home_dir();
    let rc_file = match shell {
        "zsh" => PathBuf::from(&home).join(".zshrc"),
        _ => {
            // bash — prefer .bashrc, fall back to .bash_profile
            let p = PathBuf::from(&home).join(".bashrc");
            if p.exists() {
                p
            } else {
                PathBuf::from(&home).join(".bash_profile")
            }
        }
    };

    // Fish uses function syntax; bash/zsh use eval
    if shell == "fish" {
        return install_fish_shell();
    }

    // npm/pnpm: long-running subcommands (dev servers, watchers) must bypass bctx and
    // stream directly — bctx buffers all output until exit, which hides dev server logs.
    // cargo watch has the same problem. Everything else is compressible.
    let snippet = concat!(
        "\n# bctx — intercept git/cargo/etc. through the context-aware lens pipeline\n",
        "git()     { bctx git \"$@\"; }\n",
        "npm() {\n",
        "  case \"${1:-}\" in\n",
        "    start) command npm \"$@\" ;;\n",
        "    run) case \"${2:-}\" in\n",
        "      dev|dev:*|*:dev|watch|watch:*|*:watch|*-watch|\\\n",
        "      serve|serve:*|start|start:*|preview|storybook)\n",
        "        command npm \"$@\" ;;\n",
        "      *) bctx npm \"$@\" ;;\n",
        "    esac ;;\n",
        "    *) bctx npm \"$@\" ;;\n",
        "  esac\n",
        "}\n",
        "pnpm() {\n",
        "  case \"${1:-}\" in\n",
        "    start) command pnpm \"$@\" ;;\n",
        "    run) case \"${2:-}\" in\n",
        "      dev|dev:*|*:dev|watch|watch:*|*:watch|*-watch|\\\n",
        "      serve|serve:*|start|start:*|preview|storybook)\n",
        "        command pnpm \"$@\" ;;\n",
        "      *) bctx pnpm \"$@\" ;;\n",
        "    esac ;;\n",
        "    *) bctx pnpm \"$@\" ;;\n",
        "  esac\n",
        "}\n",
        "cargo() {\n",
        "  case \"${1:-}\" in\n",
        "    watch) command cargo \"$@\" ;;\n",
        "    *) bctx cargo \"$@\" ;;\n",
        "  esac\n",
        "}\n",
        "python3() { bctx python3 \"$@\"; }\n",
        "kubectl() { bctx kubectl \"$@\"; }\n",
        "docker()  { bctx docker  \"$@\"; }\n",
        "terraform() { bctx terraform \"$@\"; }\n",
        "aws()     { bctx aws \"$@\"; }\n",
        "# bctx end\n",
    );

    let current = std::fs::read_to_string(&rc_file).unwrap_or_default();
    if current.contains("# bctx —") {
        // Migrate: old eval-loop format OR old simple one-liner wrappers (no streaming
        // awareness — swallows dev server output).
        let needs_migration = current.contains("eval \"$_bctx_cmd()\"")
            || current.contains("for _bctx_cmd in")
            || current.contains("npm()     { bctx npm");
        if needs_migration {
            let updated = replace_shell_block(&current, snippet);
            std::fs::write(&rc_file, updated)?;
            println!("  ✓ {} hook migrated to current format", rc_file.display());
            println!("  → Run: source {}", rc_file.display());
        } else {
            println!("  (bctx shell hook already up to date — skipped)");
        }
        return Ok(());
    }
    let mut content = current;
    content.push_str(snippet);
    std::fs::write(&rc_file, content)?;
    println!("  ✓ {} hook appended", rc_file.display());
    println!("  → Run: source {}", rc_file.display());
    Ok(())
}

/// Replace the bctx block (from `# bctx —` through the end sentinel) with new_snippet.
fn replace_shell_block(content: &str, new_snippet: &str) -> String {
    let mut output = String::with_capacity(content.len());
    let mut skip = false;
    for line in content.lines() {
        if line.contains("# bctx —") {
            skip = true;
            // Drop the preceding blank line added by init.
            if output.ends_with('\n') {
                output.truncate(output.trim_end_matches('\n').len());
                if !output.is_empty() {
                    output.push('\n');
                }
            }
            continue;
        }
        if skip {
            if line.starts_with("unset _bctx_cmd") || line.starts_with("# bctx end") {
                skip = false;
            }
            continue;
        }
        output.push_str(line);
        output.push('\n');
    }
    output.push_str(new_snippet);
    output
}

fn install_fish_shell() -> Result<()> {
    let home = super::home_dir();
    let conf_dir = PathBuf::from(&home).join(".config/fish/conf.d");
    std::fs::create_dir_all(&conf_dir)?;

    let fish_path = conf_dir.join("bctx.fish");
    // Always rewrite — streaming-aware version supersedes the old one-liner loop.
    let fish_hook = "\
# bctx — intercept git/cargo/etc. through the context-aware lens pipeline\n\
\n\
function git --wraps git --description \"bctx git\"\n\
    command bctx git $argv\n\
end\n\
\n\
function npm --wraps npm --description \"bctx npm\"\n\
    switch \"$argv[1]\"\n\
        case start\n\
            command npm $argv\n\
        case run\n\
            switch \"$argv[2]\"\n\
                case dev 'dev:*' '*:dev' watch 'watch:*' '*:watch' '*-watch' \\\n\
                         serve 'serve:*' start 'start:*' preview storybook\n\
                    command npm $argv\n\
                case '*'\n\
                    command bctx npm $argv\n\
            end\n\
        case '*'\n\
            command bctx npm $argv\n\
    end\n\
end\n\
\n\
function pnpm --wraps pnpm --description \"bctx pnpm\"\n\
    switch \"$argv[1]\"\n\
        case start\n\
            command pnpm $argv\n\
        case run\n\
            switch \"$argv[2]\"\n\
                case dev 'dev:*' '*:dev' watch 'watch:*' '*:watch' '*-watch' \\\n\
                         serve 'serve:*' start 'start:*' preview storybook\n\
                    command pnpm $argv\n\
                case '*'\n\
                    command bctx pnpm $argv\n\
            end\n\
        case '*'\n\
            command bctx pnpm $argv\n\
    end\n\
end\n\
\n\
function cargo --wraps cargo --description \"bctx cargo\"\n\
    switch \"$argv[1]\"\n\
        case watch\n\
            command cargo $argv\n\
        case '*'\n\
            command bctx cargo $argv\n\
    end\n\
end\n\
\n\
function docker --wraps docker --description \"bctx docker\"\n\
    switch \"$argv[1]\"\n\
        case attach exec stats\n\
            command docker $argv\n\
        case logs\n\
            if contains -- -f $argv\n\
                command docker $argv\n\
            else\n\
                command bctx docker $argv\n\
            end\n\
        case '*'\n\
            command bctx docker $argv\n\
    end\n\
end\n\
\n\
function kubectl --wraps kubectl --description \"bctx kubectl\"\n\
    switch \"$argv[1]\"\n\
        case exec attach port-forward proxy\n\
            command kubectl $argv\n\
        case logs\n\
            if contains -- -f $argv\n\
                command kubectl $argv\n\
            else\n\
                command bctx kubectl $argv\n\
            end\n\
        case '*'\n\
            command bctx kubectl $argv\n\
    end\n\
end\n\
\n\
function python3 --wraps python3 --description \"bctx python3\"\n\
    command bctx python3 $argv\n\
end\n\
function terraform --wraps terraform --description \"bctx terraform\"\n\
    command bctx terraform $argv\n\
end\n\
function aws --wraps aws --description \"bctx aws\"\n\
    command bctx aws $argv\n\
end\n\
# bctx end\n";
    std::fs::write(&fish_path, fish_hook)?;
    println!("  ✓ ~/.config/fish/conf.d/bctx.fish");
    println!("  → Open a new fish shell to activate.");
    Ok(())
}

// ── Config format helpers ─────────────────────────────────────────────────────

/// Write `{"mcpServers": {"bctx": {"command": ..., "args": ["mcp"]}}}` to a relative path.
fn write_mcp_servers(home: &str, rel: &str, bctx_bin: &str, label: &str) -> Result<()> {
    let path = PathBuf::from(home).join(rel);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let raw = if path.exists() {
        std::fs::read_to_string(&path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["mcpServers"]["bctx"] = serde_json::json!({
        "command": bctx_bin,
        "args": ["mcp"]
    });
    std::fs::write(&path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ {} → {}", label, path.display());
    Ok(())
}

/// Write VSCode-family MCP config (`{"servers": {"bctx": {"type":"stdio",...}}}`)
/// to `~/Library/Application Support/<app_subdir>/mcp.json` (macOS).
#[cfg(target_os = "macos")]
fn write_vscode_mcp(home: &str, app_subdir: &str, bctx_bin: &str, label: &str) -> Result<()> {
    let path = PathBuf::from(home)
        .join("Library/Application Support")
        .join(app_subdir)
        .join("mcp.json");
    if let Some(parent) = path.parent() {
        if !parent.exists() {
            return Ok(()); // App not installed — skip silently
        }
    }
    write_vscode_mcp_path(&path, bctx_bin, label)
}

fn write_vscode_mcp_path(path: &std::path::Path, bctx_bin: &str, label: &str) -> Result<()> {
    let raw = if path.exists() {
        std::fs::read_to_string(path)?
    } else {
        "{}".to_string()
    };
    let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
    cfg["servers"]["bctx"] = serde_json::json!({
        "type": "stdio",
        "command": bctx_bin,
        "args": ["mcp"]
    });
    std::fs::write(path, serde_json::to_string_pretty(&cfg)?)?;
    println!("  ✓ {} → {}", label, path.display());
    Ok(())
}

// ── Binary path resolution ────────────────────────────────────────────────────

fn which_bctx() -> String {
    // Prefer the currently running binary (always the right answer when run directly)
    if let Ok(exe) = std::env::current_exe() {
        let s = exe.to_string_lossy().to_string();
        if !s.contains("/deps/")
            && !s.contains("target/debug")
            && !s.contains("target/release/build")
            && !is_npm_wrapper(&s)
        {
            return s;
        }
    }
    // Common install locations first — avoid returning the npm JS wrapper from PATH
    let home = super::home_dir();
    for candidate in [
        format!("{home}/.cargo/bin/bctx"),
        format!("{home}/.local/bin/bctx"),
        "/usr/local/bin/bctx".to_string(),
        "/opt/homebrew/bin/bctx".to_string(),
    ] {
        if std::path::Path::new(&candidate).exists() {
            return candidate;
        }
    }
    // PATH lookup — only accept if it resolves to a native binary, not a JS wrapper
    if let Ok(out) = std::process::Command::new("which").arg("bctx").output() {
        let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
        if !s.is_empty() && std::path::Path::new(&s).exists() && !is_npm_wrapper(&s) {
            return s;
        }
    }
    "bctx".to_string()
}

/// Returns true if the path looks like the npm bctx.js wrapper or bctx-native inside
/// the npm package — we must never write these paths into MCP configs.
fn is_npm_wrapper(path: &str) -> bool {
    path.contains("node_modules") || path.ends_with(".js") || path.ends_with("bctx-native")
}

// ── Embedded assets ───────────────────────────────────────────────────────────

const HOOK_SCRIPT: &str = r#"#!/usr/bin/env bash
# bctx Claude Code pre-tool-use hook
# Rewrites interceptable commands to go through the bctx lens pipeline.
# Bypass: BCTX_BYPASS=1 env var, or .bctx-bypass file in CWD or any parent.

set -euo pipefail
INPUT=$(cat)

[ "${BCTX_BYPASS:-0}" = "1" ] && { printf '%s' "$INPUT"; exit 0; }

dir="$PWD"
while [ "$dir" != "/" ]; do
  [ -f "$dir/.bctx-bypass" ] && { printf '%s' "$INPUT"; exit 0; }
  dir=$(dirname "$dir")
done

COMMAND=$(python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('tool_input', {}).get('command', ''))
" <<< "$INPUT" 2>/dev/null || true)

[ -z "$COMMAND" ] && { printf '%s' "$INPUT"; exit 0; }

case "$COMMAND" in
  bctx\ *) printf '%s' "$INPUT"; exit 0 ;;
esac

# Streaming / interactive commands must never be routed through bctx — they produce
# continuous output that would be invisible until process exit (buffered by .output()).
case "$COMMAND" in
  npm\ start|npm\ start\ *|\
  npm\ run\ dev|npm\ run\ dev:*|npm\ run\ *:dev|\
  npm\ run\ watch|npm\ run\ watch:*|npm\ run\ *:watch|npm\ run\ *-watch|\
  npm\ run\ serve|npm\ run\ serve:*|npm\ run\ start|npm\ run\ start:*|\
  npm\ run\ preview|npm\ run\ storybook|\
  pnpm\ start|pnpm\ start\ *|\
  pnpm\ run\ dev|pnpm\ run\ dev:*|pnpm\ run\ *:dev|\
  pnpm\ run\ watch|pnpm\ run\ watch:*|pnpm\ run\ *:watch|pnpm\ run\ *-watch|\
  pnpm\ run\ serve|pnpm\ run\ serve:*|pnpm\ run\ start|pnpm\ run\ start:*|\
  pnpm\ run\ preview|pnpm\ run\ storybook|\
  cargo\ watch|cargo\ watch\ *|\
  uvicorn\ *|gunicorn\ *|hypercorn\ *|daphne\ *|granian\ *|\
  flask\ run*|fastapi\ dev*|fastapi\ run*|\
  python\ -m\ uvicorn*|python3\ -m\ uvicorn*|\
  python\ -m\ gunicorn*|python3\ -m\ gunicorn*|\
  python\ -m\ hypercorn*|python3\ -m\ hypercorn*|\
  python\ -m\ http.server*|python3\ -m\ http.server*|\
  python\ *manage.py\ runserver*|python3\ *manage.py\ runserver*|\
  docker\ attach\ *|docker\ exec\ *|docker\ logs\ -f*|docker\ stats*|\
  kubectl\ exec\ *|kubectl\ attach\ *|kubectl\ port-forward\ *|kubectl\ proxy*|\
  kubectl\ logs\ -f*)
    printf '%s' "$INPUT"; exit 0 ;;
esac

PROGRAMS="git cargo npm pnpm npx python3 python pytest jest vitest kubectl docker terraform aws"

for prog in $PROGRAMS; do
  case "$COMMAND" in
    "$prog "*|"$prog")
      python3 -c "
import sys, json
d = json.load(sys.stdin)
d['tool_input']['command'] = 'bctx ' + d['tool_input']['command']
print(json.dumps(d))
" <<< "$INPUT" 2>/dev/null || printf '%s' "$INPUT"
      exit 0
      ;;
  esac
done

printf '%s' "$INPUT"
exit 0
"#;

const BCTX_RULES_MD: &str = r#"# bctx — Context Engineering Layer

bctx is our context runtime for LLM coding agents. It saves tokens two ways: (1) the
pre-tool-use hook automatically compresses shell command output, and (2) the MCP skills
below compress source files and persist memory. **The shell hook is automatic; the skills
are not — you must call them.** Most of the savings on a source codebase (Python, TS, Go,
etc.) come from the skills. Follow the Golden Workflow.

## Golden Workflow — MANDATORY tool mapping

Before reaching for a native tool, use the bctx equivalent. Emitting the actual `bctx` MCP
tool call is required — describing it is not calling it.

| When you would… | Instead call | Why |
|---|---|---|
| Read a source file to understand it | `blueprint` (structure) or `chisel` (signatures) | 60–90% fewer tokens than full Read |
| Read one function/class | `pinpoint` (symbol) | just the symbol, not the file |
| Read several files at once | `parallax` (multi-file, per-file lens) | one budgeted call |
| Compress a large blob before reasoning on it | `condenser` | auto-selected lens stack |
| `grep`/search the codebase | `compass` or `scanner` | ranked hits, not raw dumps |
| Map an unfamiliar project | `panorama` / `cartograph` | languages, entry points, key dirs |

**Only** fall back to native Read when you need the exact, complete contents of a file you
are about to edit.

### Memory ritual (do this every task)

1. **At task start:** call `archivist` (or `recall`) with the task topic to load durable
   facts learned in prior sessions (schemas, auth flow, conventions, gotchas).
2. **When you learn something durable:** call `sediment` to persist it (category + key +
   value). This is what makes the *next* session cheap.

### Proving savings

- `bctx benchmark <dir>` — structural compression ceiling across a codebase (headline %).
- `bctx gain` — real, all-time token savings recorded from commands run through the hook.

## Available MCP Skills (via `bctx mcp`)

### Core skills (original 15)

| Skill | Purpose |
|-------|---------|
| `sieve` | Filter raw file/shell output to task-relevant lines |
| `cartograph` | Scan directory → structured project map |
| `chisel` | Extract AST symbols from files in compact signature format |
| `sediment` | Persist key-value facts into Vault |
| `prism` | Full incremental AST parse + index of project directory |
| `compass` | Fuse BM25 + graph traversal + Vault into ranked code locations |
| `condenser` | Compress file/string via specified or auto-selected Lens stack |
| `archivist` | Query Vault across tiers for facts matching a query (read-only) |
| `scout` | Execute shell command with domain-aware output compression |
| `chronicler` | Generate structured narrative of session/PassageRun |
| `alchemist` | Transform noisy content → structured JSON |
| `sentinel` | Static security risk assessment of command/path/tool_call |
| `cartridge` | Package current context state into a portable bundle |
| `resonator` | Dense-vector semantic search over ProjectIndex |
| `surveyor` | Compute dependency topology: callers/callees, cycles |

### Extended skills (Wave 2 — 21 more)

| Skill | Purpose |
|-------|---------|
| `parallax` | Read multiple files in one call with per-file lens |
| `blueprint` | Emit compact structural outline of a file (headings + defs) |
| `pinpoint` | Extract a single named symbol from a file by name |
| `crossroads` | Extract API route definitions from files |
| `drift` | Return lines changed since a git ref, token-budgeted |
| `panorama` | High-level project overview: languages, entry points, key dirs |
| `ripple` | Impact analysis: files that depend on a given file |
| `meridian` | Snapshot current agent context: executions, vault facts |
| `forecast` | Predict which files the agent will need next |
| `unfold` | Restore a condensed block to its original content |
| `thermal` | Token heatmap: identify which file sections consume the most tokens |
| `echo` | Record compression feedback (good/bad) for adaptive tuning |
| `ledger` | Real-time cost estimate: tokens × model pricing → USD |
| `steward` | Session role management: coder/reviewer/debugger/ops/admin + budget |
| `dispatch` | Meta-tool: invoke any bctx skill by name via one stable schema |
| `arbiter` | Code review: structured findings from diff or file content |
| `diviner` | Infer agent intent from file access patterns and commands |
| `crucible` | Run compression benchmarks across all lenses, return savings table |
| `scanner` | Find files/symbols matching a query (fuzzy + content) |
| `relay_ctx` | Package and hand off agent context to another session |
| `witness` | Record and replay tool-call sequences for testing |

## Hook behavior
The bctx pre-tool-use hook rewrites commands like `git log` → `bctx git log` automatically.
Bypass with: `BCTX_BYPASS=1 <cmd>` or place a `.bctx-bypass` file in the repo root.

## Read modes (`bctx read --mode <mode>`)

`auto` · `full` · `map` · `signatures` · `diff` · `aggressive` · `entropy` · `task` · `reference` · `lines:N-M`
"#;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_25_agents_in_supported_list() {
        assert_eq!(SUPPORTED_AGENTS.len(), 25);
    }

    #[test]
    fn all_agents_have_unique_names() {
        let mut seen = std::collections::HashSet::new();
        for name in SUPPORTED_AGENTS {
            assert!(seen.insert(*name), "duplicate agent: {name}");
        }
    }

    #[test]
    fn new_wave2_agents_present() {
        for name in &[
            "aider",
            "goose",
            "jetbrains",
            "opencode",
            "pear",
            "void",
            "neovim",
            "fish",
            "amp",
        ] {
            assert!(SUPPORTED_AGENTS.contains(name), "missing agent: {name}");
        }
    }

    #[test]
    fn original_agents_still_present() {
        for name in &[
            "claude",
            "cursor",
            "windsurf",
            "gemini",
            "zed",
            "continue",
            "cline",
            "roo",
            "copilot",
            "antigravity",
            "amazonq",
            "kiro",
            "codex",
            "trae",
            "zsh",
            "bash",
        ] {
            assert!(
                SUPPORTED_AGENTS.contains(name),
                "missing original agent: {name}"
            );
        }
    }

    #[test]
    fn amp_install_writes_amp_mcp_servers_key() {
        use tempfile::tempdir;
        let dir = tempdir().unwrap();
        let home = dir.path().to_str().unwrap().to_string();
        let amp_dir = dir.path().join(".config/amp");
        std::fs::create_dir_all(&amp_dir).unwrap();
        let settings_path = amp_dir.join("settings.json");

        // Simulate what install_amp() writes
        let bctx_bin = "bctx";
        let raw = "{}".to_string();
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["amp.mcpServers"]["bctx"] = serde_json::json!({
            "command": bctx_bin,
            "args": ["mcp"]
        });
        std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg).unwrap()).unwrap();

        // Verify the key is amp.mcpServers (not mcpServers)
        let written = std::fs::read_to_string(&settings_path).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&written).unwrap();
        assert!(
            parsed["amp.mcpServers"]["bctx"]["command"]
                .as_str()
                .is_some(),
            "amp.mcpServers.bctx.command missing in: {written}"
        );
        assert_eq!(parsed["amp.mcpServers"]["bctx"]["args"][0], "mcp");
        // Must NOT use the flat mcpServers key
        assert!(
            parsed["mcpServers"].is_null(),
            "must use amp.mcpServers, not mcpServers"
        );

        drop(home); // silence unused-var lint
    }

    #[test]
    fn amp_install_merges_with_existing_config() {
        use tempfile::tempdir;
        let dir = tempdir().unwrap();
        let amp_dir = dir.path().join(".config/amp");
        std::fs::create_dir_all(&amp_dir).unwrap();
        let settings_path = amp_dir.join("settings.json");

        // Pre-existing config with another key
        std::fs::write(
            &settings_path,
            r#"{"some.other.setting": true, "amp.mcpServers": {"existing": {"command": "other"}}}"#,
        )
        .unwrap();

        let raw = std::fs::read_to_string(&settings_path).unwrap();
        let mut cfg: serde_json::Value =
            serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
        cfg["amp.mcpServers"]["bctx"] = serde_json::json!({
            "command": "bctx",
            "args": ["mcp"]
        });
        std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg).unwrap()).unwrap();

        let written = std::fs::read_to_string(&settings_path).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&written).unwrap();
        // Pre-existing keys preserved
        assert_eq!(parsed["some.other.setting"], true);
        assert_eq!(
            parsed["amp.mcpServers"]["existing"]["command"].as_str(),
            Some("other")
        );
        // New bctx entry added
        assert_eq!(
            parsed["amp.mcpServers"]["bctx"]["command"].as_str(),
            Some("bctx")
        );
    }

    #[test]
    fn bctx_rules_md_contains_all_36_skills() {
        let skills = [
            "sieve",
            "cartograph",
            "chisel",
            "sediment",
            "prism",
            "compass",
            "condenser",
            "archivist",
            "scout",
            "chronicler",
            "alchemist",
            "sentinel",
            "cartridge",
            "resonator",
            "surveyor",
            "parallax",
            "blueprint",
            "pinpoint",
            "crossroads",
            "drift",
            "panorama",
            "ripple",
            "meridian",
            "forecast",
            "unfold",
            "thermal",
            "echo",
            "ledger",
            "steward",
            "dispatch",
            "arbiter",
            "diviner",
            "crucible",
            "scanner",
            "relay_ctx",
            "witness",
        ];
        for skill in &skills {
            assert!(
                BCTX_RULES_MD.contains(skill),
                "BCTX_RULES_MD missing skill: {skill}"
            );
        }
    }

    #[test]
    fn bctx_rules_md_contains_read_modes() {
        assert!(BCTX_RULES_MD.contains("Read modes"));
        assert!(BCTX_RULES_MD.contains("signatures"));
        assert!(BCTX_RULES_MD.contains("aggressive"));
    }

    #[test]
    fn bctx_rules_md_leads_with_mandatory_steering() {
        // The Golden Workflow block is what actually activates the compression/memory
        // skills for an agent — regression-guard its imperative mapping + memory ritual.
        assert!(BCTX_RULES_MD.contains("Golden Workflow"));
        assert!(BCTX_RULES_MD.contains("MANDATORY tool mapping"));
        assert!(BCTX_RULES_MD.contains("Memory ritual"));
        for hook in &["blueprint", "chisel", "parallax", "archivist", "sediment"] {
            assert!(
                BCTX_RULES_MD.contains(hook),
                "Golden Workflow missing steering for: {hook}"
            );
        }
        // Mapping must appear before the reference skill tables.
        let workflow = BCTX_RULES_MD.find("Golden Workflow").unwrap();
        let tables = BCTX_RULES_MD.find("Available MCP Skills").unwrap();
        assert!(workflow < tables, "steering must lead, tables are appendix");
    }

    #[test]
    fn hook_script_contains_intercepted_programs() {
        for prog in &["git", "cargo", "npm", "kubectl", "docker", "terraform"] {
            assert!(HOOK_SCRIPT.contains(prog), "hook missing: {prog}");
        }
    }
}
