use anyhow::{Context, Result};
use std::path::PathBuf;
pub fn handle(agent: String) -> Result<()> {
match agent.as_str() {
"claude" => install_claude()?,
"cursor" => install_cursor()?,
"zsh" => install_shell("zsh")?,
"bash" => install_shell("bash")?,
other => {
eprintln!("bctx init: unknown agent '{other}'");
eprintln!("Supported: claude, cursor, zsh, bash");
std::process::exit(1);
}
}
Ok(())
}
fn install_claude() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let claude_dir = PathBuf::from(&home).join(".claude");
std::fs::create_dir_all(&claude_dir)?;
let mcp_path = claude_dir.join("mcp.json");
let existing = if mcp_path.exists() {
std::fs::read_to_string(&mcp_path)?
} else {
"{}".to_string()
};
let mut mcp_config: serde_json::Value =
serde_json::from_str(&existing).unwrap_or(serde_json::json!({}));
let bctx_bin = which_bctx();
mcp_config["mcpServers"]["bctx"] = serde_json::json!({
"command": bctx_bin,
"args": ["mcp"]
});
std::fs::write(&mcp_path, serde_json::to_string_pretty(&mcp_config)?)?;
println!("✓ bctx MCP server registered at {}", mcp_path.display());
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!("✓ bctx hook script written to {}", hook_path.display());
let settings_path = claude_dir.join("settings.json");
let existing_settings = if settings_path.exists() {
std::fs::read_to_string(&settings_path)?
} else {
"{}".to_string()
};
let mut settings: serde_json::Value =
serde_json::from_str(&existing_settings).unwrap_or(serde_json::json!({}));
let hook_entry = serde_json::json!({
"matcher": "Bash",
"hooks": [{ "type": "command", "command": hook_path.to_string_lossy() }]
});
let already_present = 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_present {
println!(" (bctx hook already present in settings.json — skipped)");
} else 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!(
"✓ bctx pre-tool-use hook registered in {}",
settings_path.display()
);
println!();
println!(" Restart Claude Code to activate.");
println!(" To bypass bctx in a project: touch .bctx-bypass");
println!(" To bypass globally: BCTX_BYPASS=1 <command>");
Ok(())
}
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 signals (any one is sufficient):
# - BCTX_BYPASS=1 environment variable
# - .bctx-bypass file in current directory or any parent
# - command already starts with "bctx "
set -euo pipefail
INPUT=$(cat)
# Global env bypass
[ "${BCTX_BYPASS:-0}" = "1" ] && { printf '%s' "$INPUT"; exit 0; }
# Project-level bypass: walk up from CWD
dir="$PWD"
while [ "$dir" != "/" ]; do
[ -f "$dir/.bctx-bypass" ] && { printf '%s' "$INPUT"; exit 0; }
dir=$(dirname "$dir")
done
# Extract the bash command string from tool input JSON
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; }
# Already going through bctx
case "$COMMAND" in
bctx\ *) printf '%s' "$INPUT"; exit 0 ;;
esac
# Programs bctx knows how to compress
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
"#;
fn install_cursor() -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let cursor_dir = PathBuf::from(&home).join(".cursor");
let mcp_path = cursor_dir.join("mcp.json");
let existing = if mcp_path.exists() {
std::fs::read_to_string(&mcp_path)?
} else {
"{}".to_string()
};
let mut config: serde_json::Value =
serde_json::from_str(&existing).unwrap_or(serde_json::json!({}));
let bctx_bin = which_bctx();
config["mcpServers"]["bctx"] = serde_json::json!({
"command": bctx_bin,
"args": ["mcp"]
});
std::fs::create_dir_all(&cursor_dir)?;
std::fs::write(&mcp_path, serde_json::to_string_pretty(&config)?)?;
println!("✓ bctx MCP server registered at {}", mcp_path.display());
println!(" Restart Cursor to activate the 15 bctx tools.");
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" => PathBuf::from(&home).join(".bashrc"),
_ => unreachable!(),
};
let snippet = format!(
"\n# bctx — intercept git/cargo/etc. through the context-aware lens pipeline\n\
for _bctx_cmd in git cargo npm pnpm python3 kubectl docker; do\n\
{INDENT}eval \"$_bctx_cmd() {{ bctx \\\"$_bctx_cmd\\\" \\\"$@\\\"; }}\"\n\
done\nunset _bctx_cmd\n",
INDENT = " "
);
let current = std::fs::read_to_string(&rc_file).unwrap_or_default();
if current.contains("# bctx —") {
println!("bctx shell hook already present in {}", rc_file.display());
return Ok(());
}
let mut content = current;
content.push_str(&snippet);
std::fs::write(&rc_file, content)?;
println!("✓ bctx shell hook appended to {}", rc_file.display());
println!(" Run: source {}", rc_file.display());
Ok(())
}
fn which_bctx() -> String {
let candidates = [
std::env::current_exe()
.ok()
.map(|p| p.to_string_lossy().to_string()),
Some("/Users/azeno/.cargo/bin/bctx".to_string()),
Some("/usr/local/bin/bctx".to_string()),
];
for c in candidates.into_iter().flatten() {
if std::path::Path::new(&c).exists() {
return c;
}
}
"bctx".to_string()
}