use anyhow::{Context, Result};
use std::path::PathBuf;
pub fn handle(agent: Option<String>) -> Result<()> {
match agent.as_deref() {
Some(name) => install_agent(name)?,
None => {
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(())
}
const SUPPORTED_AGENTS: &[&str] = &[
"claude", "cursor", "windsurf", "gemini", "zed", "continue", "cline", "roo", "copilot", "antigravity", "amazonq", "kiro", "codex", "trae", "zsh", "bash", ];
fn is_installed(agent: &str) -> bool {
let home = std::env::var("HOME").unwrap_or_default();
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(),
"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())
}
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(),
"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);
}
}
}
fn install_claude() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
let claude_dir = PathBuf::from(&home).join(".claude");
std::fs::create_dir_all(&claude_dir)?;
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)");
}
#[cfg(target_os = "macos")]
write_vscode_mcp(&home, "Code/User", &bctx_bin, "Claude Code VSCode")?;
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());
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]);
}
std::fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?;
println!(" ✓ settings.json hook registered");
}
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. \
Always use bctx MCP skills through the `bctx` MCP server.\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");
if !rules_file.exists() {
std::fs::write(&rules_file, BCTX_RULES_MD)?;
println!(" ✓ rules/bctx.md written");
}
Ok(())
}
fn install_cursor() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
write_mcp_servers(&home, ".cursor/mcp.json", &bctx_bin, "Cursor (global)")?;
#[cfg(target_os = "macos")]
write_vscode_mcp(&home, "Cursor/User", &bctx_bin, "Cursor VSCode")?;
println!(" → Restart Cursor to activate.");
Ok(())
}
fn install_windsurf() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
write_mcp_servers(
&home,
".codeium/windsurf/mcp_config.json",
&bctx_bin,
"Windsurf (global)",
)?;
#[cfg(target_os = "macos")]
write_vscode_mcp(&home, "Windsurf/User", &bctx_bin, "Windsurf VSCode")?;
println!(" → Restart Windsurf to activate.");
Ok(())
}
fn install_gemini() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
let gemini_dir = PathBuf::from(&home).join(".gemini");
std::fs::create_dir_all(&gemini_dir)?;
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(())
}
fn install_zed() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
let zed_dir = PathBuf::from(&home).join(".config/zed");
std::fs::create_dir_all(&zed_dir)?;
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(())
}
fn install_continue() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
let continue_dir = PathBuf::from(&home).join(".continue");
std::fs::create_dir_all(&continue_dir)?;
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(())
}
fn install_vscode_extension(label: &str) -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
#[cfg(target_os = "macos")]
write_vscode_mcp(&home, "Code/User", &bctx_bin, label)?;
#[cfg(not(target_os = "macos"))]
{
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(())
}
fn install_antigravity() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
write_mcp_servers(
&home,
".gemini/antigravity/mcp_config.json",
&bctx_bin,
"Antigravity (global)",
)?;
#[cfg(target_os = "macos")]
write_vscode_mcp(&home, "Antigravity/User", &bctx_bin, "Antigravity VSCode")?;
println!(" → Restart Antigravity to activate.");
Ok(())
}
fn install_amazonq() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
write_mcp_servers(&home, ".aws/amazonq/mcp.json", &bctx_bin, "Amazon Q")?;
println!(" → Restart Amazon Q to activate.");
Ok(())
}
fn install_kiro() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
write_mcp_servers(&home, ".kiro/settings/mcp.json", &bctx_bin, "AWS Kiro")?;
println!(" → Restart Kiro to activate.");
Ok(())
}
fn install_codex() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
write_mcp_servers(&home, ".codex/mcp.json", &bctx_bin, "OpenAI Codex")?;
println!(" → Restart Codex CLI to activate.");
Ok(())
}
fn install_trae() -> Result<()> {
#[cfg(target_os = "macos")]
{
let home = std::env::var("HOME").context("HOME not set")?;
let bctx_bin = which_bctx();
write_vscode_mcp(&home, "Trae/User", &bctx_bin, "Trae VSCode")?;
}
println!(" → Restart Trae to activate.");
Ok(())
}
fn install_shell(shell: &str) -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let rc_file = match shell {
"zsh" => PathBuf::from(&home).join(".zshrc"),
"bash" => {
let p = PathBuf::from(&home).join(".bashrc");
if p.exists() {
p
} else {
PathBuf::from(&home).join(".bash_profile")
}
}
_ => unreachable!(),
};
let snippet = concat!(
"\n# bctx — intercept git/cargo/etc. through the context-aware lens pipeline\n",
"for _bctx_cmd in git cargo npm pnpm python3 kubectl docker terraform aws; do\n",
" eval \"$_bctx_cmd() { bctx \\\"$_bctx_cmd\\\" \\\"$@\\\"; }\"\n",
"done\nunset _bctx_cmd\n"
);
let current = std::fs::read_to_string(&rc_file).unwrap_or_default();
if current.contains("# bctx —") {
println!(" (bctx shell hook already present — 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(())
}
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(())
}
#[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(()); }
}
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(())
}
fn which_bctx() -> String {
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")
{
return s;
}
}
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() {
return s;
}
}
let home = std::env::var("HOME").unwrap_or_default();
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;
}
}
"bctx".to_string()
}
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
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. Use bctx MCP skills for intelligent
context operations. The bctx hook in settings.json automatically rewrites interceptable
shell commands (git, cargo, npm, kubectl, docker, terraform, aws, etc.) to route through
the bctx lens pipeline for token savings.
## Available MCP Skills (via `bctx mcp`)
| 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 |
## 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.
"#;