bctx 0.1.7

bctx CLI — intercept CLI commands and compress output for LLM coding agents
use anyhow::Result;
use std::path::Path;

pub fn handle() -> Result<()> {
    println!("bctx doctor — system health check");
    println!();

    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    let bctx_dir = format!("{home}/.bctx");
    let beacons_dir = format!("{home}/.bctx/beacons");
    let vault_dir = format!("{home}/.bctx/vault");
    let exec_db = format!("{home}/.bctx/executions.db");
    let claude_mcp = format!("{home}/.claude/mcp.json");
    let cursor_mcp = format!("{home}/.cursor/mcp.json");
    let cloud_token = format!("{home}/.bctx/cloud_token.json");

    section("Environment");
    check("HOME set", std::env::var("HOME").is_ok());
    check_bin("bctx in PATH", "bctx");
    check_bin("git in PATH", "git");
    check_bin("cargo in PATH", "cargo");

    section("Local storage");
    check("~/.bctx/ exists", Path::new(&bctx_dir).exists());
    check("~/.bctx/executions.db exists", Path::new(&exec_db).exists());
    info(
        "~/.bctx/vault/",
        if Path::new(&vault_dir).exists() {
            "present"
        } else {
            "not yet created (lazy)"
        },
    );
    info(
        "~/.bctx/beacons/",
        if Path::new(&beacons_dir).exists() {
            "present"
        } else {
            "not yet created (lazy)"
        },
    );

    section("Vault");
    let crystallized = format!("{vault_dir}/crystallized.json");
    let resonant = format!("{vault_dir}/resonant.json");
    let crystal_facts = count_json_array(&crystallized);
    let resonant_facts = count_json_array(&resonant);
    check_count("crystallized facts", crystal_facts);
    check_count("resonant facts", resonant_facts);

    section("Beacons");
    let beacon_count = count_dir_files(&beacons_dir);
    check_count("beacon events recorded", beacon_count);

    section("Agent integration");
    check("Claude Code mcp.json", Path::new(&claude_mcp).exists());
    check("Cursor mcp.json", Path::new(&cursor_mcp).exists());

    section("Cloud");
    let token_present = Path::new(&cloud_token).exists();
    check("cloud_token.json present", token_present);
    if token_present {
        if let Some(tier) = read_cloud_tier(&cloud_token) {
            println!("      tier: {tier}");
        }
    }

    section("Runtime");
    let tiktoken_ok = forge::budget::estimator::TokenEstimator::count("hello world") > 0;
    check("tiktoken BPE loaded", tiktoken_ok);

    // Quick vault query smoke-test
    let vault_ok = std::panic::catch_unwind(|| {
        let v = atlas::vault_store::vault().lock().unwrap();
        let _ = v.query(&vault::retrieval::query::VaultQuery::new("__doctor_probe__").top_k(0));
    })
    .is_ok();
    check("Vault query smoke test", vault_ok);

    println!();
    if !Path::new(&claude_mcp).exists() && !Path::new(&cursor_mcp).exists() {
        println!("  Tip: run `bctx init --agent claude` to set up agent hooks.");
    }
    if !token_present {
        println!("  Tip: run `bctx login` to connect to the cloud for Vault sync.");
    }

    Ok(())
}

fn section(label: &str) {
    println!("\n  {label}");
    println!("  {}", "".repeat(label.len()));
}

fn check(label: &str, ok: bool) {
    let mark = if ok { "" } else { "" };
    println!("  {mark}  {label}");
}

fn check_bin(label: &str, bin: &str) {
    let ok = which_bin(bin);
    let mark = if ok { "" } else { "" };
    println!("  {mark}  {label}");
}

fn check_count(label: &str, n: usize) {
    println!("{label}: {n}");
}

fn info(label: &str, detail: &str) {
    println!("  ·  {label}: {detail}");
}

fn which_bin(bin: &str) -> bool {
    std::process::Command::new("which")
        .arg(bin)
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

fn count_json_array(path: &str) -> usize {
    let Ok(data) = std::fs::read_to_string(path) else {
        return 0;
    };
    let Ok(arr) = serde_json::from_str::<serde_json::Value>(&data) else {
        return 0;
    };
    arr.as_array().map(|a| a.len()).unwrap_or(0)
}

fn count_dir_files(path: &str) -> usize {
    std::fs::read_dir(path)
        .map(|d| d.filter_map(|e| e.ok()).count())
        .unwrap_or(0)
}

fn read_cloud_tier(path: &str) -> Option<String> {
    let data = std::fs::read_to_string(path).ok()?;
    let v: serde_json::Value = serde_json::from_str(&data).ok()?;
    v["tier"].as_str().map(String::from)
}