use claude_storage::{ Storage, Result };
use std::env;
fn main() -> Result< () >
{
let args : Vec< String > = env::args().collect();
let storage = Storage::new()?;
let project_filter = args.iter()
.position( | a | a == "--project" )
.and_then( | i | args.get( i + 1 ) )
.map( std::string::String::as_str );
let top_n = args.iter()
.position( | a | a == "--top" )
.and_then( | i | args.get( i + 1 ) )
.and_then( | s | s.parse::< usize >().ok() )
.unwrap_or( usize::MAX );
if let Some( project_id ) = project_filter
{
analyze_project( &storage, project_id )?;
}
else
{
analyze_global( &storage, top_n )?;
}
Ok( () )
}
fn analyze_global( storage : &Storage, top_n : usize ) -> Result< () >
{
println!( "Claude Code Token Usage Analysis" );
println!( "=================================\n" );
let stats = match storage.global_stats()
{
Ok( s ) => s,
Err( e ) =>
{
eprintln!( "Error: Could not analyze storage: {e}" );
eprintln!();
eprintln!( "This may indicate:" );
eprintln!( " - Corrupted or empty JSONL files in ~/.claude/" );
eprintln!( " - Invalid JSON in conversation files" );
eprintln!( " - Incompatible format version" );
eprintln!();
eprintln!( "Try running with a specific project ID to isolate the issue:" );
eprintln!( " cargo run --example token_usage -- --project <project_id>" );
return Err( e );
}
};
println!( "Total Projects: {}", stats.total_projects );
println!( " UUID Projects: {}", stats.uuid_projects );
println!( " Path Projects: {}", stats.path_projects );
println!();
println!( "Total Sessions: {}", stats.total_sessions );
println!( " Main Sessions: {}", stats.main_sessions );
println!( " Agent Sessions: {}", stats.agent_sessions );
println!();
println!( "Total Entries: {}", stats.total_entries );
println!( " User: {}", stats.total_user_entries );
println!( " Assistant: {}", stats.total_assistant_entries );
println!();
println!( "Token Usage:" );
println!( " Input: {}", format_tokens( stats.total_input_tokens ) );
println!( " Output: {}", format_tokens( stats.total_output_tokens ) );
println!( " Cache Read: {}", format_tokens( stats.total_cache_read_tokens ) );
println!( " Cache Creation: {}", format_tokens( stats.total_cache_creation_tokens ) );
println!();
let total_tokens = stats.total_input_tokens + stats.total_output_tokens;
println!( "Total Tokens: {}", format_tokens( total_tokens ) );
println!();
let mut projects : Vec< _ > = stats.project_breakdown.values().collect();
projects.sort_by_key( | p | core::cmp::Reverse( p.total_input_tokens + p.total_output_tokens ) );
println!( "Top {} Projects by Token Usage:", top_n.min( projects.len() ) );
println!( "{:<40} {:>12} {:>12} {:>12}", "Project ID", "Sessions", "Input", "Output" );
println!( "{}", "-".repeat( 80 ) );
for project in projects.iter().take( top_n )
{
println!( "{:<40} {:>12} {:>12} {:>12}",
truncate_str( &project.project_id, 40 ),
project.session_count,
format_tokens( project.total_input_tokens ),
format_tokens( project.total_output_tokens )
);
}
Ok( () )
}
fn analyze_project( storage : &Storage, project_id : &str ) -> Result< () >
{
println!( "Project Token Analysis: {project_id}" );
println!( "=================================\n" );
let projects = storage.list_projects()?;
let project = projects.iter()
.find( | p |
{
use claude_storage::ProjectId;
match p.id()
{
ProjectId::Uuid( uuid ) => uuid.contains( project_id ),
ProjectId::Path( path ) => path.to_string_lossy().contains( project_id ),
}
})
.ok_or_else( || std::io::Error::new( std::io::ErrorKind::NotFound, "Project not found" ) )?;
let mut all_sessions = project.all_sessions()?;
println!( "Total Sessions: {}", all_sessions.len() );
let mut session_stats_list = Vec::new();
for session in &mut all_sessions
{
let stats = session.stats()?;
session_stats_list.push( stats );
}
session_stats_list.sort_by_key( | s |
core::cmp::Reverse( s.total_input_tokens + s.total_output_tokens )
);
println!();
println!( "{:<20} {:>10} {:>12} {:>12} {:>8}",
"Session ID", "Entries", "Input", "Output", "Type" );
println!( "{}", "-".repeat( 70 ) );
for stats in &session_stats_list
{
let session_type = if stats.is_agent_session { "Agent" } else { "Main" };
println!( "{:<20} {:>10} {:>12} {:>12} {:>8}",
truncate_str( &stats.session_id, 20 ),
stats.total_entries,
format_tokens( stats.total_input_tokens ),
format_tokens( stats.total_output_tokens ),
session_type
);
}
println!( "{}", "-".repeat( 70 ) );
let total_input : u64 = session_stats_list.iter().map( | s | s.total_input_tokens ).sum();
let total_output : u64 = session_stats_list.iter().map( | s | s.total_output_tokens ).sum();
let total_entries : usize = session_stats_list.iter().map( | s | s.total_entries ).sum();
println!( "{:<20} {:>10} {:>12} {:>12}",
"TOTAL",
total_entries,
format_tokens( total_input ),
format_tokens( total_output )
);
Ok( () )
}
fn format_tokens( tokens : u64 ) -> String
{
let s = tokens.to_string();
let mut result = String::new();
for ( count, c ) in s.chars().rev().enumerate()
{
if count > 0 && count % 3 == 0
{
result.insert( 0, ',' );
}
result.insert( 0, c );
}
result
}
fn truncate_str( s : &str, max_len : usize ) -> String
{
if s.len() <= max_len
{
s.to_string()
}
else
{
format!( "{}...", &s[ ..max_len - 3 ] )
}
}