use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use crate::{backfill, blocks, db, hook, paths, pricing, recost, report, ui, update};
#[derive(Parser)]
#[command(
name = "tokr",
version,
about = "Persistent token-usage ledger for Claude Code + Codex."
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
#[command(about = "Dashboard: cost, tokens, sources, top models")]
Stats(StatsArgs),
#[command(about = "Tabular usage report, bucketed daily/weekly/monthly")]
Report(report::ReportArgs),
#[command(about = "5-hour billing windows with an active-block hero panel")]
Blocks(blocks::BlocksArgs),
#[command(
alias = "backfill",
about = "Walk transcripts on disk and ingest missing rows"
)]
Sync(SyncArgs),
#[command(about = "Harness-facing ingest; reads JSON on stdin")]
Hook(HookArgs),
#[command(subcommand, about = "List, update, or re-apply pricing")]
Prices(PricesCmd),
#[command(subcommand, about = "Ledger database operations")]
Db(DbCmd),
}
#[derive(Args)]
struct StatsArgs {
#[arg(long)]
global: bool,
}
#[derive(Args)]
struct HookArgs {
#[arg(long)]
r#final: bool,
}
#[derive(Args)]
struct SyncArgs {
#[arg(long, value_enum, default_value_t = backfill::Source::All)]
source: backfill::Source,
#[arg(long, value_name = "DAYS")]
archive_older_than: Option<u32>,
}
#[derive(Subcommand)]
enum PricesCmd {
#[command(about = "Print every loaded pricing entry")]
List,
#[command(about = "Fetch the latest pricing table and append new entries")]
Update {
#[arg(long, value_name = "URL")]
from: Option<String>,
},
#[command(about = "Recompute cost_usd on existing rows using current pricing")]
Recost {
#[arg(long)]
model: Option<String>,
#[arg(long, value_name = "ISO")]
since: Option<String>,
#[arg(long)]
dry_run: bool,
},
}
#[derive(Subcommand)]
enum DbCmd {
#[command(about = "Print the path to the SQLite database")]
Path,
#[command(about = "VACUUM and prune cursors for vanished transcripts")]
Vacuum,
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
match cli.cmd {
Cmd::Stats(a) => report::stats(a.global),
Cmd::Report(a) => report::run(a),
Cmd::Blocks(a) => blocks::run(a),
Cmd::Sync(a) => backfill::run(a.source, a.archive_older_than),
Cmd::Hook(a) => hook::run(a.r#final),
Cmd::Prices(sub) => run_prices(sub),
Cmd::Db(sub) => run_db(sub),
}
}
fn run_prices(cmd: PricesCmd) -> Result<()> {
match cmd {
PricesCmd::List => {
let p = pricing::Pricing::load()?;
print_prices(&p.list_all());
println!();
println!(
" {} {}",
ui::dim("Overrides file:"),
ui::cyan(&pricing::overrides_path()?.display().to_string()),
);
Ok(())
}
PricesCmd::Update { from } => update::run(from),
PricesCmd::Recost {
model,
since,
dry_run,
} => {
let stats = recost::run(recost::RecostFilters {
model_substr: model.as_deref(),
since_iso: since.as_deref(),
dry_run,
})?;
print_recost(&stats, dry_run);
Ok(())
}
}
}
fn run_db(cmd: DbCmd) -> Result<()> {
match cmd {
DbCmd::Path => {
println!("{}", paths::db_path()?.display());
Ok(())
}
DbCmd::Vacuum => {
let handle = db::Db::open()?;
handle.vacuum()?;
println!(
"{} {}",
ui::green("✓ Vacuumed"),
ui::dim(&paths::db_path()?.display().to_string()),
);
Ok(())
}
}
}
fn print_prices(prices: &[&pricing::ModelPrice]) {
println!(
" {} {}",
ui::bold_cyan("Pricing table"),
ui::dim(&format!("· {} entries", prices.len())),
);
println!();
let mut t = ui::Table::new(
vec![
"Model",
"Effective",
"Input",
"Output",
"CW 5m",
"CW 1h",
"CR",
],
vec![
ui::Align::Left,
ui::Align::Left,
ui::Align::Right,
ui::Align::Right,
ui::Align::Right,
ui::Align::Right,
ui::Align::Right,
],
);
for p in prices {
let eff = p
.effective_from
.split('T')
.next()
.unwrap_or(&p.effective_from);
t.push(vec![
ui::magenta(&p.name),
ui::dim(eff),
ui::green(&format!("${:.3}", p.input_per_mtok)),
ui::green(&format!("${:.2}", p.output_per_mtok)),
ui::dim(&format!("${:.2}", p.cache_write_5m_per_mtok)),
ui::dim(&format!("${:.2}", p.cache_write_1h_per_mtok)),
ui::dim(&format!("${:.3}", p.cache_read_per_mtok)),
]);
}
for line in t.render().lines() {
println!(" {line}");
}
println!(
" {}",
ui::dim("Prices are USD per million tokens (MTok). CW=cache write, CR=cache read."),
);
}
fn print_recost(stats: &recost::RecostStats, dry_run: bool) {
let inner = 70_usize;
let title = if dry_run {
"Recost · dry-run"
} else {
"Recost"
};
println!("{}", ui::box_top(title, inner));
println!("{}", ui::box_blank(inner));
let col_w = 20;
let labels = format!(
" {} {} {}",
ui::pad_right(&ui::dim("Examined"), col_w),
ui::pad_right(
&ui::dim(if dry_run { "Would change" } else { "Changed" }),
col_w,
),
ui::pad_right(&ui::dim("Δ cost"), col_w),
);
let delta = stats.new_total_usd - stats.old_total_usd;
let delta_s = if delta.abs() < 0.005 {
ui::dim(&format!("{delta:+.2}"))
} else if delta < 0.0 {
ui::bold_green(&format!("-${:.2}", delta.abs()))
} else {
ui::bold_red(&format!("+${delta:.2}"))
};
let values = format!(
" {} {} {}",
ui::pad_right(&ui::bold_white(&ui::fmt_int(stats.rows_examined)), col_w),
ui::pad_right(&ui::bold_white(&ui::fmt_int(stats.rows_changed)), col_w),
ui::pad_right(&delta_s, col_w),
);
println!("{}", ui::box_row(&labels, inner));
println!("{}", ui::box_row(&values, inner));
println!("{}", ui::box_blank(inner));
let totals = format!(
" {} old {} → new {}",
ui::dim("Totals"),
ui::dim(&ui::fmt_cost(stats.old_total_usd)),
ui::bold_green(&ui::fmt_cost(stats.new_total_usd)),
);
println!("{}", ui::box_row(&totals, inner));
println!("{}", ui::box_blank(inner));
println!("{}", ui::box_bottom(inner));
}