use anyhow::{bail, Context, Result};
use clap::Parser;
use cc_token_usage::analysis::overview::analyze_overview;
use cc_token_usage::analysis::project::analyze_projects;
use cc_token_usage::analysis::session::analyze_session;
use cc_token_usage::analysis::trend::analyze_trend;
use cc_token_usage::analysis::validate;
use cc_token_usage::cli::{Cli, Command, GroupBy, OutputFormat};
use cc_token_usage::config::Config;
use cc_token_usage::data::loader;
use cc_token_usage::data::models::SessionData;
use cc_token_usage::output::html::{render_full_report_html, render_session_html};
use cc_token_usage::output::text::{render_overview, render_projects, render_session, render_trend, render_validation};
use cc_token_usage::pricing::calculator::PricingCalculator;
fn main() -> Result<()> {
let cli = Cli::parse();
let command = cli.command.unwrap_or(Command::Overview);
if let Command::Update { check } = &command {
if *check {
let status = cc_token_usage::update::check_for_update()?;
eprintln!("Current version: v{}", status.current_version);
eprintln!("Latest version: v{}", status.latest_version);
if status.update_available {
eprintln!("\nUpdate available! Run `cc-token-usage update` to upgrade.");
} else {
eprintln!("\nAlready up to date.");
}
} else {
cc_token_usage::update::perform_update()?;
}
return Ok(());
}
let claude_home = match cli.claude_home {
Some(ref path) => path.clone(),
None => dirs::home_dir()
.context("could not determine home directory")?
.join(".claude"),
};
let config = if let Some(ref config_path) = cli.config {
Config::load(config_path)
.with_context(|| format!("failed to load config from {}", config_path.display()))?
} else {
let default_config_path = dirs::home_dir()
.context("could not determine home directory")?
.join(".config/cc-token-analyzer/config.toml");
if default_config_path.exists() {
Config::load(&default_config_path).unwrap_or_default()
} else {
Config::default()
}
};
let calc = PricingCalculator::new().with_overrides(config.to_model_prices());
let subscription_price = cli.subscription_price.or_else(|| {
config
.subscription
.last()
.map(|p| p.monthly_price_usd)
});
let (sessions, quality) = loader::load_all(&claude_home)
.with_context(|| format!("failed to load data from {}", claude_home.display()))?;
match command {
Command::Overview => {
let overview = analyze_overview(&sessions, quality.clone(), &calc, subscription_price);
match cli.format {
OutputFormat::Text => {
println!("{}", render_overview(&overview, &calc));
}
OutputFormat::Html => {
let projects = analyze_projects(&sessions, &calc, 20);
let trend = analyze_trend(&sessions, &calc, 0, false);
let html = render_full_report_html(&overview, &projects, &trend, &calc);
let output_path = cli
.output
.unwrap_or_else(|| std::env::temp_dir().join("cc-token-report.html"));
std::fs::write(&output_path, &html)
.with_context(|| format!("failed to write {}", output_path.display()))?;
println!("Report written to {}", output_path.display());
}
}
}
Command::Project { name: _, top } => {
let projects = analyze_projects(&sessions, &calc, top);
match cli.format {
OutputFormat::Text => {
println!("{}", render_projects(&projects));
}
OutputFormat::Html => {
let overview =
analyze_overview(&sessions, quality.clone(), &calc, subscription_price);
let trend = analyze_trend(&sessions, &calc, 0, false);
let html = render_full_report_html(&overview, &projects, &trend, &calc);
let output_path = cli
.output
.unwrap_or_else(|| std::env::temp_dir().join("cc-token-report.html"));
std::fs::write(&output_path, &html)
.with_context(|| format!("failed to write {}", output_path.display()))?;
println!("Report written to {}", output_path.display());
}
}
}
Command::Session { id, latest } => {
let session = if latest {
sessions
.iter()
.filter(|s| s.last_timestamp.is_some())
.max_by_key(|s| s.last_timestamp)
.context("no sessions found with timestamps")?
} else if let Some(ref prefix) = id {
let matches: Vec<_> = sessions
.iter()
.filter(|s| s.session_id.starts_with(prefix))
.collect();
match matches.len() {
0 => bail!("no session found matching prefix '{}'", prefix),
1 => matches[0],
n => bail!(
"ambiguous prefix '{}': {} sessions match. Provide a longer prefix.",
prefix,
n
),
}
} else {
bail!("specify a session ID or use --latest");
};
let raw_meta = cc_token_usage::data::scanner::load_agent_meta(&session.session_id, &claude_home);
let agent_meta: std::collections::HashMap<String, cc_token_usage::analysis::session::AgentMeta> = raw_meta.into_iter()
.map(|(k, (t, d))| (k, cc_token_usage::analysis::session::AgentMeta { agent_type: t, description: d }))
.collect();
let result = analyze_session(session, &calc, &agent_meta);
match cli.format {
OutputFormat::Text => {
println!("{}", render_session(&result));
}
OutputFormat::Html => {
let html = render_session_html(&result);
let output_path = cli
.output
.unwrap_or_else(|| std::env::temp_dir().join("cc-session-report.html"));
std::fs::write(&output_path, &html)
.with_context(|| format!("failed to write {}", output_path.display()))?;
println!("Report written to {}", output_path.display());
}
}
}
Command::Validate { id, failures_only } => {
let target_sessions: Vec<&SessionData> = if let Some(ref prefix) = id {
let matches: Vec<_> = sessions
.iter()
.filter(|s| s.session_id.starts_with(prefix))
.collect();
if matches.is_empty() {
bail!("no session found matching prefix '{}'", prefix);
}
matches
} else {
sessions.iter().collect()
};
let report = validate::validate_all(&target_sessions, &quality, &claude_home, &calc)
.context("validation failed")?;
println!("{}", render_validation(&report, failures_only));
}
Command::Update { .. } => unreachable!(),
Command::Trend { days, group_by } => {
let group_by_month = matches!(group_by, GroupBy::Month);
let trend = analyze_trend(&sessions, &calc, days, group_by_month);
match cli.format {
OutputFormat::Text => {
println!("{}", render_trend(&trend));
}
OutputFormat::Html => {
let overview =
analyze_overview(&sessions, quality.clone(), &calc, subscription_price);
let projects = analyze_projects(&sessions, &calc, 20);
let html = render_full_report_html(&overview, &projects, &trend, &calc);
let output_path = cli
.output
.unwrap_or_else(|| std::env::temp_dir().join("cc-token-report.html"));
std::fs::write(&output_path, &html)
.with_context(|| format!("failed to write {}", output_path.display()))?;
println!("Report written to {}", output_path.display());
}
}
}
}
Ok(())
}