1+ import type { Argv } from "yargs"
12import { cmd } from "./cmd"
3+ import { Session } from "../../session"
4+ import { bootstrap } from "../bootstrap"
5+ import { Storage } from "../../storage/storage"
6+ import { Project } from "../../project/project"
7+ import { Instance } from "../../project/instance"
28
39interface SessionStats {
410 totalSessions : number
@@ -24,10 +30,186 @@ interface SessionStats {
2430
2531export const StatsCommand = cmd ( {
2632 command : "stats" ,
27- handler : async ( ) => { } ,
33+ describe : "show token usage and cost statistics" ,
34+ builder : ( yargs : Argv ) => {
35+ return yargs
36+ . option ( "days" , {
37+ describe : "show stats for the last N days (default: all time)" ,
38+ type : "number" ,
39+ } )
40+ . option ( "tools" , {
41+ describe : "number of tools to show (default: all)" ,
42+ type : "number" ,
43+ } )
44+ . option ( "project" , {
45+ describe : "filter by project (default: all projects, empty string: current project)" ,
46+ type : "string" ,
47+ } )
48+ } ,
49+ handler : async ( args ) => {
50+ await bootstrap ( process . cwd ( ) , async ( ) => {
51+ const stats = await aggregateSessionStats ( args . days , args . project )
52+ displayStats ( stats , args . tools )
53+ } )
54+ } ,
2855} )
2956
30- export function displayStats ( stats : SessionStats ) {
57+ async function getCurrentProject ( ) : Promise < Project . Info > {
58+ return Instance . project
59+ }
60+
61+ async function getAllSessions ( ) : Promise < Session . Info [ ] > {
62+ const sessions : Session . Info [ ] = [ ]
63+
64+ const projectKeys = await Storage . list ( [ "project" ] )
65+ const projects = await Promise . all ( projectKeys . map ( ( key ) => Storage . read < Project . Info > ( key ) ) )
66+
67+ for ( const project of projects ) {
68+ if ( ! project ) continue
69+
70+ const sessionKeys = await Storage . list ( [ "session" , project . id ] )
71+ const projectSessions = await Promise . all (
72+ sessionKeys . map ( ( key ) => Storage . read < Session . Info > ( key ) ) ,
73+ )
74+
75+ for ( const session of projectSessions ) {
76+ if ( session ) {
77+ sessions . push ( session )
78+ }
79+ }
80+ }
81+
82+ return sessions
83+ }
84+
85+ async function aggregateSessionStats ( days ?: number , projectFilter ?: string ) : Promise < SessionStats > {
86+ const sessions = await getAllSessions ( )
87+ const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
88+ const cutoffTime = days ? Date . now ( ) - days * DAYS_IN_SECOND : 0
89+
90+ let filteredSessions = days
91+ ? sessions . filter ( ( session ) => session . time . updated >= cutoffTime )
92+ : sessions
93+
94+ if ( projectFilter !== undefined ) {
95+ if ( projectFilter === "" ) {
96+ const currentProject = await getCurrentProject ( )
97+ filteredSessions = filteredSessions . filter (
98+ ( session ) => session . projectID === currentProject . id ,
99+ )
100+ } else {
101+ filteredSessions = filteredSessions . filter ( ( session ) => session . projectID === projectFilter )
102+ }
103+ }
104+
105+ const stats : SessionStats = {
106+ totalSessions : filteredSessions . length ,
107+ totalMessages : 0 ,
108+ totalCost : 0 ,
109+ totalTokens : {
110+ input : 0 ,
111+ output : 0 ,
112+ reasoning : 0 ,
113+ cache : {
114+ read : 0 ,
115+ write : 0 ,
116+ } ,
117+ } ,
118+ toolUsage : { } ,
119+ dateRange : {
120+ earliest : Date . now ( ) ,
121+ latest : Date . now ( ) ,
122+ } ,
123+ days : 0 ,
124+ costPerDay : 0 ,
125+ }
126+
127+ if ( filteredSessions . length > 1000 ) {
128+ console . log (
129+ `Large dataset detected (${ filteredSessions . length } sessions). This may take a while...` ,
130+ )
131+ }
132+
133+ if ( filteredSessions . length === 0 ) {
134+ return stats
135+ }
136+
137+ let earliestTime = Date . now ( )
138+ let latestTime = 0
139+
140+ const BATCH_SIZE = 20
141+ for ( let i = 0 ; i < filteredSessions . length ; i += BATCH_SIZE ) {
142+ const batch = filteredSessions . slice ( i , i + BATCH_SIZE )
143+
144+ const batchPromises = batch . map ( async ( session ) => {
145+ const messages = await Session . messages ( session . id )
146+
147+ let sessionCost = 0
148+ let sessionTokens = { input : 0 , output : 0 , reasoning : 0 , cache : { read : 0 , write : 0 } }
149+ let sessionToolUsage : Record < string , number > = { }
150+
151+ for ( const message of messages ) {
152+ if ( message . info . role === "assistant" ) {
153+ sessionCost += message . info . cost || 0
154+
155+ if ( message . info . tokens ) {
156+ sessionTokens . input += message . info . tokens . input || 0
157+ sessionTokens . output += message . info . tokens . output || 0
158+ sessionTokens . reasoning += message . info . tokens . reasoning || 0
159+ sessionTokens . cache . read += message . info . tokens . cache ?. read || 0
160+ sessionTokens . cache . write += message . info . tokens . cache ?. write || 0
161+ }
162+ }
163+
164+ for ( const part of message . parts ) {
165+ if ( part . type === "tool" && part . tool ) {
166+ sessionToolUsage [ part . tool ] = ( sessionToolUsage [ part . tool ] || 0 ) + 1
167+ }
168+ }
169+ }
170+
171+ return {
172+ messageCount : messages . length ,
173+ sessionCost,
174+ sessionTokens,
175+ sessionToolUsage,
176+ earliestTime : session . time . created ,
177+ latestTime : session . time . updated ,
178+ }
179+ } )
180+
181+ const batchResults = await Promise . all ( batchPromises )
182+
183+ for ( const result of batchResults ) {
184+ earliestTime = Math . min ( earliestTime , result . earliestTime )
185+ latestTime = Math . max ( latestTime , result . latestTime )
186+
187+ stats . totalMessages += result . messageCount
188+ stats . totalCost += result . sessionCost
189+ stats . totalTokens . input += result . sessionTokens . input
190+ stats . totalTokens . output += result . sessionTokens . output
191+ stats . totalTokens . reasoning += result . sessionTokens . reasoning
192+ stats . totalTokens . cache . read += result . sessionTokens . cache . read
193+ stats . totalTokens . cache . write += result . sessionTokens . cache . write
194+
195+ for ( const [ tool , count ] of Object . entries ( result . sessionToolUsage ) ) {
196+ stats . toolUsage [ tool ] = ( stats . toolUsage [ tool ] || 0 ) + count
197+ }
198+ }
199+ }
200+
201+ const actualDays = Math . max ( 1 , Math . ceil ( ( latestTime - earliestTime ) / DAYS_IN_SECOND ) )
202+ stats . dateRange = {
203+ earliest : earliestTime ,
204+ latest : latestTime ,
205+ }
206+ stats . days = actualDays
207+ stats . costPerDay = stats . totalCost / actualDays
208+
209+ return stats
210+ }
211+
212+ export function displayStats ( stats : SessionStats , toolLimit ?: number ) {
31213 const width = 56
32214
33215 function renderRow ( label : string , value : string ) : string {
@@ -64,30 +246,35 @@ export function displayStats(stats: SessionStats) {
64246
65247 // Tool Usage section
66248 if ( Object . keys ( stats . toolUsage ) . length > 0 ) {
67- const sortedTools = Object . entries ( stats . toolUsage )
68- . sort ( ( [ , a ] , [ , b ] ) => b - a )
69- . slice ( 0 , 10 )
249+ const sortedTools = Object . entries ( stats . toolUsage ) . sort ( ( [ , a ] , [ , b ] ) => b - a )
250+ const toolsToDisplay = toolLimit ? sortedTools . slice ( 0 , toolLimit ) : sortedTools
70251
71252 console . log ( "┌────────────────────────────────────────────────────────┐" )
72253 console . log ( "│ TOOL USAGE │" )
73254 console . log ( "├────────────────────────────────────────────────────────┤" )
74255
75- const maxCount = Math . max ( ...sortedTools . map ( ( [ , count ] ) => count ) )
256+ const maxCount = Math . max ( ...toolsToDisplay . map ( ( [ , count ] ) => count ) )
76257 const totalToolUsage = Object . values ( stats . toolUsage ) . reduce ( ( a , b ) => a + b , 0 )
77258
78- for ( const [ tool , count ] of sortedTools ) {
259+ for ( const [ tool , count ] of toolsToDisplay ) {
79260 const barLength = Math . max ( 1 , Math . floor ( ( count / maxCount ) * 20 ) )
80261 const bar = "█" . repeat ( barLength )
81262 const percentage = ( ( count / totalToolUsage ) * 100 ) . toFixed ( 1 )
82263
83- const content = ` ${ tool . padEnd ( 10 ) } ${ bar . padEnd ( 20 ) } ${ count . toString ( ) . padStart ( 3 ) } (${ percentage . padStart ( 4 ) } %)`
84- const padding = Math . max ( 0 , width - content . length )
264+ const maxToolLength = 18
265+ const truncatedTool =
266+ tool . length > maxToolLength ? tool . substring ( 0 , maxToolLength - 2 ) + ".." : tool
267+ const toolName = truncatedTool . padEnd ( maxToolLength )
268+
269+ const content = ` ${ toolName } ${ bar . padEnd ( 20 ) } ${ count . toString ( ) . padStart ( 3 ) } (${ percentage . padStart ( 4 ) } %)`
270+ const padding = Math . max ( 0 , width - content . length - 1 )
85271 console . log ( `│${ content } ${ " " . repeat ( padding ) } │` )
86272 }
87273 console . log ( "└────────────────────────────────────────────────────────┘" )
88274 }
89275 console . log ( )
90276}
277+
91278function formatNumber ( num : number ) : string {
92279 if ( num >= 1000000 ) {
93280 return ( num / 1000000 ) . toFixed ( 1 ) + "M"
0 commit comments