bctx 0.1.7

bctx CLI — intercept CLI commands and compress output for LLM coding agents
use anyhow::Result;
use forge::budget::estimator::TokenEstimator;
use forge::substrate::local::LocalSubstrate;
use forge::substrate::{Substrate, SubstrateCommand};
use std::sync::OnceLock;
use weave::lenses::LensContext;
use weave::mesh::registry::FilterMesh;
use weave::output::savings::SavesReport;

static MESH: OnceLock<FilterMesh> = OnceLock::new();
fn mesh() -> &'static FilterMesh {
    MESH.get_or_init(FilterMesh::default_mesh)
}

/// Returns true if BCTX_BYPASS=1 is set or a .bctx-bypass file exists in CWD or any parent.
fn bypass_active() -> bool {
    if std::env::var("BCTX_BYPASS").as_deref() == Ok("1") {
        return true;
    }
    let mut dir = std::env::current_dir().ok();
    while let Some(d) = dir {
        if d.join(".bctx-bypass").exists() {
            return true;
        }
        dir = d.parent().map(|p| p.to_path_buf());
    }
    false
}

pub fn handle(args: Vec<String>) -> Result<()> {
    if args.is_empty() {
        anyhow::bail!("usage: bctx <command> [args...]");
    }

    let program = args[0].clone();
    let cmd_args: Vec<String> = args[1..].to_vec();

    let substrate = LocalSubstrate;
    let cmd = SubstrateCommand {
        program: program.clone(),
        args: cmd_args.clone(),
        working_dir: None,
        timeout_secs: 60,
        capture_stderr: true,
        env: Vec::new(),
    };

    let result = substrate.execute(cmd)?;
    let raw_stdout = String::from_utf8_lossy(&result.stdout).to_string();
    let raw_stderr = String::from_utf8_lossy(&result.stderr).to_string();

    // Bypass: passthrough raw output, no compression, no savings report, no cloud sync.
    if bypass_active() {
        print!("{raw_stdout}");
        if !raw_stderr.is_empty() {
            eprint!("{raw_stderr}");
        }
        std::process::exit(result.exit_code);
    }

    let exit_code = result.exit_code;
    let tokens_before = TokenEstimator::count_nonblocking(&raw_stdout);

    let ctx = LensContext::new(2000).with_exit_code(exit_code);

    let compressed_content = match mesh().find(&program, &cmd_args) {
        Some(node) => node.apply(&raw_stdout, &ctx).content,
        None => raw_stdout.clone(),
    };

    let tokens_after = TokenEstimator::count_nonblocking(&compressed_content);
    print!("{compressed_content}");
    if !raw_stderr.is_empty() {
        eprint!("{raw_stderr}");
    }

    if tokens_before > 0 {
        let report = SavesReport::new(program.as_str(), tokens_before, tokens_after);
        eprintln!("{}", report.render_inline());

        // Send savings to cloud if logged in — join before exit so the thread completes
        if let Some(token) = cloud::client::auth::load_token() {
            let handle = cloud::client::sync::push_signals_bg(
                token.endpoint,
                token.access_token,
                tokens_after as i64,
                (tokens_before as i64 - tokens_after as i64).max(0),
                program.clone(),
            );
            let _ = handle.join();
        }
    }

    std::process::exit(result.exit_code);
}