bctx 0.1.9

bctx CLI — intercept CLI commands and compress output for LLM coding agents
use anyhow::Result;
use forge::tracker::store::ExecutionStore;

pub fn handle() -> Result<()> {
    let home = std::env::var("HOME").unwrap_or_default();
    let db_path = std::path::PathBuf::from(&home)
        .join(".bctx")
        .join("executions.db");

    if !db_path.exists() {
        println!();
        println!("  No executions recorded yet.");
        println!("  Run  bctx git log  or  bctx cargo build  to start tracking.");
        println!();
        return Ok(());
    }

    let store = ExecutionStore::open(&db_path)?;
    let total_saved = store.total_saved()?;
    let summary = store.summary()?;
    let recent = store.recent(100_000)?;

    let total_sent: usize = recent.iter().map(|r| r.tokens_after).sum();
    let total_before: usize = recent.iter().map(|r| r.tokens_before).sum();
    let cmd_count = recent.len();
    let compression_pct = total_saved
        .checked_mul(100)
        .and_then(|v| v.checked_div(total_before))
        .unwrap_or(0);
    let cost_avoided = total_saved as f64 * 0.000_015;

    let sep = "  ─────────────────────────────────────────────────";
    println!();
    println!("  TOKEN SAVINGS  (local · all time)");
    println!("{sep}");
    println!(
        "  Tokens saved  {:>10}    Compression   {:>3}%",
        fmt_tokens(total_saved),
        compression_pct
    );
    println!(
        "  Tokens sent   {:>10}    Commands run  {:>6}",
        fmt_tokens(total_sent),
        cmd_count
    );
    println!("  Cost avoided  {:>10}", format!("${cost_avoided:.2}"));
    println!();

    if !summary.is_empty() {
        println!("  TOP COMMANDS");
        let max_saved = summary
            .iter()
            .map(|(_, tb, ta, _)| tb.saturating_sub(*ta))
            .max()
            .unwrap_or(1);
        for (prog, tb, ta, pct) in summary.iter().take(8) {
            let saved = tb.saturating_sub(*ta);
            let bar = bar(saved, max_saved, 16);
            println!(
                "  {:<10}  {}  {:>8} saved  {:>3.0}%",
                prog,
                bar,
                fmt_tokens(saved),
                pct
            );
        }
        println!();
    }

    println!("{sep}");

    // Cloud quota — append if logged in
    if let Some(token) = cloud::client::auth::load_token() {
        if let Ok(gauge) = fetch_gauge(&token.endpoint, &token.access_token) {
            let used = gauge["used_tokens"].as_u64().unwrap_or(0);
            let quota = gauge["quota_tokens"].as_u64().unwrap_or(0);
            let reset = gauge["reset_at"].as_str().unwrap_or("");
            if quota > 0 {
                let pct = used
                    .checked_mul(100)
                    .and_then(|v| v.checked_div(quota))
                    .unwrap_or(0);
                println!(
                    "  Cloud quota   {:>6} / {} used  ({}%)  resets {}",
                    fmt_tokens(used as usize),
                    fmt_tokens(quota as usize),
                    pct,
                    reset
                );
            } else {
                println!(
                    "  Cloud quota   {} used  (unlimited)  resets {}",
                    fmt_tokens(used as usize),
                    reset
                );
            }
            println!("{sep}");
        }
    }

    println!("  Run `bctx dashboard` to open the full dashboard");
    println!();
    Ok(())
}

fn fmt_tokens(n: usize) -> String {
    if n >= 1_000_000 {
        format!("{:.1}M", n as f64 / 1_000_000.0)
    } else if n >= 1_000 {
        format!("{:.1}K", n as f64 / 1_000.0)
    } else {
        n.to_string()
    }
}

fn bar(value: usize, max: usize, width: usize) -> String {
    use std::io::IsTerminal;
    let filled = value
        .checked_mul(width)
        .and_then(|v| v.checked_div(max))
        .unwrap_or(0)
        .min(width);
    let empty = width - filled;
    let blocks = "".repeat(filled);
    let shade = "".repeat(empty);
    if std::io::stdout().is_terminal() {
        format!("\x1b[32m{blocks}\x1b[2m{shade}\x1b[0m")
    } else {
        format!("{blocks}{shade}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fmt_tokens_formats_millions() {
        assert_eq!(fmt_tokens(1_000_000), "1.0M");
        assert_eq!(fmt_tokens(2_400_000), "2.4M");
    }

    #[test]
    fn fmt_tokens_formats_thousands() {
        assert_eq!(fmt_tokens(1_000), "1.0K");
        assert_eq!(fmt_tokens(890_000), "890.0K");
    }

    #[test]
    fn fmt_tokens_formats_small() {
        assert_eq!(fmt_tokens(0), "0");
        assert_eq!(fmt_tokens(42), "42");
        assert_eq!(fmt_tokens(999), "999");
    }

    #[test]
    fn bar_full_when_value_equals_max() {
        let b = bar(16, 16, 16);
        assert!(b.contains("████████████████"));
        assert!(!b.contains(""));
    }

    #[test]
    fn bar_empty_when_value_is_zero() {
        let b = bar(0, 16, 16);
        assert!(b.contains("░░░░░░░░░░░░░░░░"));
    }

    #[test]
    fn bar_half_fill() {
        let b = bar(8, 16, 16);
        // ANSI codes sit between filled and empty segments
        assert!(b.contains("████████"));
        assert!(b.contains("░░░░░░░░"));
    }

    #[test]
    fn bar_zero_max_does_not_panic() {
        let b = bar(5, 0, 16);
        assert!(b.contains(""));
    }
}

fn fetch_gauge(endpoint: &str, access_token: &str) -> Result<serde_json::Value> {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?;
    rt.block_on(async {
        let resp = reqwest::Client::new()
            .get(format!("{endpoint}/dashboard/gauge"))
            .bearer_auth(access_token)
            .send()
            .await?;
        let val = resp.json::<serde_json::Value>().await?;
        Ok(val)
    })
}