bctx 0.1.23

bctx CLI — intercept CLI commands and compress output for LLM coding agents
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 {
    /// Intercept a domain command (e.g. bctx git log -20)
    #[command(external_subcommand)]
    Run(Vec<String>),

    /// Start the MCP/Nexus gateway server
    Mcp {
        #[arg(long, help = "Use HTTP transport instead of stdio")]
        http: bool,
        #[arg(long, default_value = "3000", help = "HTTP port (when --http)")]
        port: u16,
    },

    /// Build/update the project code index
    Index {
        #[arg(default_value = ".", help = "Project root to index")]
        path: String,
        #[arg(long, help = "Force full re-index")]
        force: bool,
    },

    /// Search the project code index
    Search {
        #[arg(help = "Search query")]
        query: String,
        #[arg(long, default_value = "10", help = "Max results")]
        top_k: usize,
    },

    /// Compress a file or stdin and print the result
    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,
    },

    /// Show token savings summary (local · offline)
    Gain,

    /// Find CLI commands that ran without bctx interception and estimate missed savings
    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>,
    },

    /// List all lens modes with descriptions, savings estimates, and use cases
    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>,
    },

    /// Run a command with an explicit read mode (overrides FilterMesh auto-selection)
    Read {
        /// Read mode: auto | full | map | signatures | diff | aggressive | entropy | task | reference | lines:N-M
        #[arg(long, default_value = "auto", help = "Lens mode to apply")]
        mode: String,
        /// Command and arguments to run (after --)
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        command: Vec<String>,
    },

    /// Open the local token savings dashboard (TUI)
    Dashboard,

    /// Initialize bctx for AI agents (auto-detects installed agents if --agent omitted)
    Init {
        #[arg(long, help = "Agent to configure: claude, cursor, zsh, bash")]
        agent: Option<String>,
    },

    /// Remove bctx configuration from AI agents (reverse of init)
    Uninstall {
        #[arg(long, help = "Agent to unconfigure: claude, cursor, zsh, bash, ...")]
        agent: Option<String>,
    },

    /// Diagnose bctx installation and health
    Doctor,

    /// Query the Vault for remembered facts
    Recall {
        #[arg(help = "Query string")]
        query: String,
        #[arg(long, default_value = "5", help = "Max facts to return")]
        top_k: usize,
    },

    /// Log in to the better-ctx cloud (device flow)
    Login {
        #[arg(long, help = "Custom cloud endpoint URL")]
        endpoint: Option<String>,
        #[arg(long, help = "Log out and remove stored credentials")]
        logout: bool,
    },

    /// Log out and remove stored credentials
    Logout,

    /// Show current authentication status
    Status,

    /// Generate a skill execution plan for a task (surfaces the SpiralCycle DECIDE phase)
    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,
    },

    /// Scan code for smells: unwraps, TODOs, unsafe blocks, debug output (default: git diff HEAD)
    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 Vault facts with the cloud
    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>,
    },

    /// Auto-detect how bctx was installed and upgrade to the latest version
    Update,

    /// List domain compressor patterns organized by category
    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,
    },

    /// Measure token savings and compression quality across your codebase
    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() {
    // Pre-warm BPE tokenizer in background so first lens application is instant.
    // Warm BPE tokenizer in background — count_nonblocking() uses fast path
    // until this finishes, then upgrades to BPE automatically for free.
    std::thread::spawn(forge::budget::estimator::TokenEstimator::warmup);

    // Always present as "bctx" regardless of the installed binary name (e.g. bctx-native).
    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);
    }
}