mod commands;
mod tui;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "bctx",
about = "better-ctx: context-aware execution runtime for LLM agents",
version
)]
struct Cli {
#[command(subcommand)]
command: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
#[command(external_subcommand)]
Run(Vec<String>),
Mcp {
#[arg(long, help = "Use HTTP transport instead of stdio")]
http: bool,
#[arg(long, default_value = "3000", help = "HTTP port (when --http)")]
port: u16,
},
Index {
#[arg(default_value = ".", help = "Project root to index")]
path: String,
#[arg(long, help = "Force full re-index")]
force: bool,
},
Search {
#[arg(help = "Search query")]
query: String,
#[arg(long, default_value = "10", help = "Max results")]
top_k: usize,
},
Compress {
#[arg(help = "File to compress (reads stdin if omitted)")]
file: Option<String>,
#[arg(long, default_value = "2000", help = "Target token budget")]
budget: usize,
#[arg(long, help = "Output raw JSON (used by the Chrome extension)")]
json: bool,
},
Gain,
Discover {
#[arg(long, default_value = "7", help = "Look back N days of shell history")]
days: u32,
#[arg(help = "Ignored — discover reads shell history, not a directory")]
_path: Option<String>,
},
Modes {
#[arg(long, help = "Output as JSON")]
json: bool,
#[arg(long, help = "Show a live demo of how a mode transforms sample code")]
demo: Option<String>,
},
Read {
#[arg(long, default_value = "auto", help = "Lens mode to apply")]
mode: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
Dashboard,
Init {
#[arg(long, help = "Agent to configure: claude, cursor, zsh, bash")]
agent: Option<String>,
},
Uninstall {
#[arg(long, help = "Agent to unconfigure: claude, cursor, zsh, bash, ...")]
agent: Option<String>,
},
Doctor,
Recall {
#[arg(help = "Query string")]
query: String,
#[arg(long, default_value = "5", help = "Max facts to return")]
top_k: usize,
},
Login {
#[arg(long, help = "Custom cloud endpoint URL")]
endpoint: Option<String>,
#[arg(long, help = "Log out and remove stored credentials")]
logout: bool,
},
Logout,
Status,
Plan {
#[arg(help = "Task description in plain English")]
task: String,
#[arg(long, default_value = "6000", help = "Total token budget to allocate")]
budget: usize,
#[arg(long, help = "Output as JSON")]
json: bool,
},
Smells {
#[arg(help = "File or directory to analyse (default: git diff HEAD)")]
path: Option<String>,
#[arg(long, help = "Analyse only staged changes (git diff --staged)")]
staged: bool,
#[arg(
long,
default_value = "all",
help = "Filter findings: all | security | performance | correctness | style"
)]
focus: String,
},
Sync {
#[arg(
long,
help = "Push local crystallized facts to cloud (default when neither flag set)"
)]
push: bool,
#[arg(long, help = "Pull facts from cloud and merge into local Vault")]
pull: bool,
#[arg(long, help = "Project identifier (default: hash of current directory)")]
project: Option<String>,
},
Update,
Patterns {
#[arg(
long,
help = "Filter to a specific category: vcs | build | test | lint | pkg | infra | db | ai | sys"
)]
category: Option<String>,
#[arg(long, help = "Output as JSON")]
json: bool,
},
Benchmark {
#[arg(default_value = ".", help = "File or directory to benchmark")]
path: String,
#[arg(long, help = "Run only this mode (e.g. signatures, map, aggressive)")]
mode: Option<String>,
#[arg(
long,
help = "Show all 7 modes per file instead of the 4-column summary"
)]
all_modes: bool,
#[arg(long, help = "Output results as JSON")]
json: bool,
#[arg(long, default_value = "1", help = "Minimum file size in KB to include")]
min_kb: u64,
},
}
fn main() {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
std::thread::spawn(forge::budget::estimator::TokenEstimator::warmup);
let args: Vec<std::ffi::OsString> = std::iter::once("bctx".into())
.chain(std::env::args_os().skip(1))
.collect();
let cli = Cli::parse_from(args);
let result = match cli.command {
Cmd::Run(args) => commands::run::handle(args),
Cmd::Compress { budget, file, json } => commands::compress::handle(budget, file, json),
Cmd::Mcp { http, port } => commands::mcp::handle(http, port),
Cmd::Index { path, force } => commands::index::handle(path, force),
Cmd::Search { query, top_k } => commands::search::handle(query, top_k),
Cmd::Gain => commands::gain::handle(),
Cmd::Discover { days, _path: _ } => commands::discover::handle(days),
Cmd::Read { mode, command } => {
let parsed = weave::ReadMode::parse(&mode).unwrap_or_else(|| {
eprintln!("bctx: unknown read mode '{mode}' — falling back to auto");
weave::ReadMode::Auto
});
commands::run::handle_with_mode(command, parsed)
}
Cmd::Modes { json, demo } => commands::modes::handle(json, demo),
Cmd::Dashboard => commands::dashboard::handle(),
Cmd::Init { agent } => commands::init::handle(agent),
Cmd::Uninstall { agent } => commands::uninstall::handle(agent),
Cmd::Doctor => commands::doctor::handle(),
Cmd::Recall { query, top_k } => commands::recall::handle(query, top_k),
Cmd::Login { endpoint, logout } => commands::login::handle(endpoint, logout),
Cmd::Logout => commands::login::handle(None, true),
Cmd::Status => commands::login::handle_status(),
Cmd::Plan { task, budget, json } => commands::plan::handle(task, budget, json),
Cmd::Smells {
path,
staged,
focus,
} => commands::smells::handle(path, staged, focus),
Cmd::Sync {
push,
pull,
project,
} => commands::sync::handle(push, pull, project),
Cmd::Update => commands::update::handle(),
Cmd::Patterns { category, json } => commands::patterns::handle(json, category),
Cmd::Benchmark {
path,
mode,
all_modes,
json,
min_kb,
} => commands::benchmark::handle(path, mode, all_modes, json, min_kb),
};
if let Err(e) = result {
eprintln!("error: {e}");
std::process::exit(1);
}
}