bctx 0.1.9

bctx CLI — intercept CLI commands and compress output for LLM coding agents
use anyhow::Result;
use prism::{index::ProjectIndex, search::lexical_search};

pub fn handle(query: String, top_k: usize) -> Result<()> {
    let root = std::env::current_dir()
        .unwrap_or_else(|_| std::path::PathBuf::from("."))
        .to_string_lossy()
        .to_string();

    let index_path = std::path::Path::new(&root).join(".bctx").join("index.json");
    let index = if index_path.exists() {
        let json = std::fs::read_to_string(&index_path).unwrap_or_default();
        serde_json::from_str::<ProjectIndex>(&json).unwrap_or_else(|_| ProjectIndex::new(&root))
    } else {
        ProjectIndex::new(&root)
    };

    let results = lexical_search(&index, &query, top_k);

    if results.is_empty() {
        println!("No results for \"{query}\".");
        println!("Tip: run `bctx index` first to build the project index.");
        return Ok(());
    }

    println!("bctx search: \"{query}\"{} result(s)\n", results.len());
    for (i, r) in results.iter().enumerate() {
        let sym = r.symbol.as_deref().unwrap_or("");
        if sym.is_empty() {
            println!("  {}. {}  (score: {:.2})", i + 1, r.path, r.score);
        } else {
            println!(
                "  {}. {}  ::{}  (score: {:.2})",
                i + 1,
                r.path,
                sym,
                r.score
            );
        }
        if !r.snippet.is_empty() {
            for line in r.snippet.lines().take(3) {
                println!("      {line}");
            }
        }
        println!();
    }
    Ok(())
}