bctx 0.1.26

bctx CLI — intercept CLI commands and compress output for LLM coding agents
use anyhow::Result;
use cloud::client::auth::{clear_token, load_token, save_token, TokenStore};

const DEFAULT_ENDPOINT: &str = "https://api.betterctx.com";
const CLIENT_ID: &str = "bctx-cli";

pub fn handle(endpoint: Option<String>, logout: bool) -> Result<()> {
    if logout {
        clear_token()?;
        println!("Logged out.");
        return Ok(());
    }

    if let Some(t) = load_token() {
        println!("Already authenticated as {} (tier: {})", t.user_id, t.tier);
        println!("Run `bctx login --logout` to sign out.");
        return Ok(());
    }

    let ep = endpoint.unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());

    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?;

    rt.block_on(async {
        let device = cloud::client::auth::device_flow_start(&ep, CLIENT_ID)
            .await
            .map_err(|e| {
                anyhow::anyhow!(
                    "Could not reach cloud server: {e}\nCheck your network or try again."
                )
            })?;

        println!();
        println!("  Opening browser for authorization...");
        println!("  If the browser doesn't open, visit:");
        println!("  {}", device.verification_uri);
        println!();
        println!("  Your code: {}", device.user_code);
        println!();

        // Try to open the browser; ignore errors (user can open manually)
        let _ = open::that(&device.verification_uri);

        print!("Waiting for authorization");
        std::io::Write::flush(&mut std::io::stdout()).ok();

        let token = cloud::client::auth::device_flow_poll(
            &ep,
            CLIENT_ID,
            &device.device_code,
            device.interval,
        )
        .await?;

        println!();

        let store = TokenStore {
            access_token: token.access_token,
            refresh_token: token.refresh_token,
            expires_at: None,
            endpoint: ep.clone(),
            user_id: token.user_id.clone(),
            email: token.email.clone(),
            tier: token.tier.clone(),
        };
        save_token(&store)?;

        println!("Logged in!");
        println!("  User : {}", token.user_id);
        println!("  Tier : {}", token.tier);
        if token.tier == "free" {
            println!();
            println!("  Upgrade to Beacon for cloud sync: https://betterctx.com/upgrade");
        }
        Ok::<_, anyhow::Error>(())
    })
}

pub fn handle_status() -> Result<()> {
    match load_token() {
        Some(t) => {
            println!("Authenticated");
            println!("  User ID  : {}", t.user_id);
            println!("  Tier     : {}", t.tier);
            println!("  Endpoint : {}", t.endpoint);
            println!();

            // Fetch live account info from server
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()?;
            let result = rt.block_on(cloud::client::auth::fetch_account(
                &t.endpoint,
                &t.access_token,
            ));
            match result {
                Ok(info) => {
                    if let Some(quota) = info["quota_tokens"].as_u64() {
                        println!("  Cloud quota : {:>10} tokens/mo", quota);
                    }
                    if let Some(limit) = info["vault_fact_limit"].as_u64() {
                        println!("  Vault limit : {:>10} facts", limit);
                    }
                    let sync = info["cloud_sync_enabled"].as_bool().unwrap_or(false);
                    println!(
                        "  Cloud sync  : {}",
                        if sync { "enabled" } else { "disabled" }
                    );
                }
                Err(e) => {
                    println!("  (Could not reach server: {e})");
                }
            }
        }
        None => {
            println!("Not authenticated. Run `bctx login` to connect.");
        }
    }

    // Shell hook status
    println!();
    let home = std::env::var("HOME").unwrap_or_default();
    let hook_marker = "# bctx";
    let shell_configs = [
        format!("{home}/.zshrc"),
        format!("{home}/.bashrc"),
        format!("{home}/.bash_profile"),
        format!("{home}/.config/fish/config.fish"),
    ];
    let hook_rc = shell_configs.iter().find_map(|rc| {
        std::fs::read_to_string(rc)
            .ok()
            .filter(|c| c.contains(hook_marker))
            .map(|_| rc.replace(&home, "~"))
    });
    match &hook_rc {
        Some(rc) => println!("Shell hook  : active ({})", rc),
        None => println!("Shell hook  : not installed — run `bctx init` to set up"),
    }

    // Agent integrations
    let agents = [
        ("Claude Code", format!("{home}/.claude/settings.json")),
        ("Cursor", format!("{home}/.cursor/settings.json")),
        ("Windsurf", format!("{home}/.windsurf/settings.json")),
        ("Zed", format!("{home}/.config/zed/settings.json")),
    ];
    let found: Vec<&str> = agents
        .iter()
        .filter(|(_, p)| {
            std::fs::read_to_string(p)
                .map(|c| c.contains("bctx"))
                .unwrap_or(false)
        })
        .map(|(name, _)| *name)
        .collect();
    if found.is_empty() {
        println!("Agents      : none configured — run `bctx init --agent <name>`");
    } else {
        println!("Agents      : {}", found.join(", "));
    }

    Ok(())
}