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);
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)
}