claudex-cli 0.11.0

Query, search, and analyze agent coding sessions from the command line
Documentation
use anyhow::Result;

use crate::cli::ResolvedFilter;
use crate::ui;
use claudex::index::IndexStore;
use claudex::providers::enabled_default;
use claudex::types::ModelPricing;

pub fn run(project: Option<&str>, json: bool, filter: &ResolvedFilter) -> Result<()> {
    let providers = enabled_default()?;
    let mut idx = IndexStore::open()?;
    idx.ensure_fresh(&providers)?;

    let rows = idx.query_model_usage(project, filter)?;

    if json {
        let output: Vec<_> = rows
            .iter()
            .map(|r| {
                serde_json::json!({
                    "model": r.model,
                    "model_family": ModelPricing::name(Some(&r.model)),
                    "session_count": r.session_count,
                    "input_tokens": r.input_tokens,
                    "output_tokens": r.output_tokens,
                    "cache_creation_tokens": r.cache_creation_tokens,
                    "cache_read_tokens": r.cache_read_tokens,
                    "avg_cost_per_session_usd": r.avg_cost_per_session_usd,
                    "avg_tokens_per_session": r.avg_tokens_per_session,
                    "service_tiers": r.service_tiers,
                    "inference_geos": r.inference_geos,
                    "avg_speed": r.avg_speed,
                    "total_iterations": r.total_iterations,
                    "cost_usd": r.cost_usd,
                })
            })
            .collect();
        println!("{}", serde_json::to_string_pretty(&output)?);
        return Ok(());
    }

    if rows.is_empty() {
        println!("No model usage data found.");
        return Ok(());
    }

    let mut table = ui::table();
    table.set_header(ui::header([
        "Model",
        "Sessions",
        "Input",
        "Output",
        "Cache Write",
        "Cache Read",
        "Avg/Session",
        "Avg Tokens",
        "Cost (USD)",
    ]));
    ui::right_align(&mut table, &[1, 2, 3, 4, 5, 6, 7, 8]);
    let mut total_sessions = 0i64;
    let mut total_input = 0i64;
    let mut total_output = 0i64;
    let mut total_cache_creation = 0i64;
    let mut total_cache_read = 0i64;
    let mut total_cost = 0.0f64;
    for r in &rows {
        let family = ModelPricing::name(Some(&r.model));
        let display = if r.model.is_empty() {
            family.to_string()
        } else {
            format!("{} ({})", family, r.model.trim_start_matches("claude-"))
        };
        table.add_row([
            ui::cell_model(&display),
            ui::cell_count(r.session_count as u64),
            ui::cell_count(r.input_tokens as u64),
            ui::cell_count(r.output_tokens as u64),
            ui::cell_count(r.cache_creation_tokens as u64),
            ui::cell_count(r.cache_read_tokens as u64),
            ui::cell_cost(r.avg_cost_per_session_usd),
            ui::cell_count(r.avg_tokens_per_session.round() as u64),
            ui::cell_cost(r.cost_usd),
        ]);
        total_sessions += r.session_count;
        total_input += r.input_tokens;
        total_output += r.output_tokens;
        total_cache_creation += r.cache_creation_tokens;
        total_cache_read += r.cache_read_tokens;
        total_cost += r.cost_usd;
    }
    table.add_row(ui::total_row([
        "TOTAL".to_string(),
        ui::fmt_count(total_sessions as u64),
        ui::fmt_count(total_input as u64),
        ui::fmt_count(total_output as u64),
        ui::fmt_count(total_cache_creation as u64),
        ui::fmt_count(total_cache_read as u64),
        String::new(),
        String::new(),
        ui::fmt_cost(total_cost),
    ]));
    println!("{table}");
    Ok(())
}