@@ -146,7 +146,8 @@ export async function loadDailyUsageData(
146146 const mode = options ?. mode || "auto" ;
147147 const modelPricing = mode === "display" ? { } : await fetchModelPricing ( ) ;
148148
149- const dailyMap = new Map < string , DailyUsage > ( ) ;
149+ // Collect all valid data entries first
150+ const allEntries : { data : UsageData ; date : string ; cost : number } [ ] = [ ] ;
150151
151152 for ( const file of files ) {
152153 const content = await readFile ( file , "utf-8" ) ;
@@ -165,35 +166,49 @@ export async function loadDailyUsageData(
165166 const data = result . output ;
166167
167168 const date = formatDate ( data . timestamp ) ;
168- const existing = dailyMap . get ( date ) || {
169- date,
170- inputTokens : 0 ,
171- outputTokens : 0 ,
172- cacheCreationTokens : 0 ,
173- cacheReadTokens : 0 ,
174- totalCost : 0 ,
175- } ;
176-
177- existing . inputTokens += data . message . usage . input_tokens ?? 0 ;
178- existing . outputTokens += data . message . usage . output_tokens ?? 0 ;
179- existing . cacheCreationTokens +=
180- data . message . usage . cache_creation_input_tokens ?? 0 ;
181- existing . cacheReadTokens +=
182- data . message . usage . cache_read_input_tokens ?? 0 ;
183-
184- // Calculate cost based on mode
185169 const cost = calculateCostForEntry ( data , mode , modelPricing ) ;
186- existing . totalCost += cost ;
187170
188- dailyMap . set ( date , existing ) ;
171+ allEntries . push ( { data , date, cost } ) ;
189172 } catch ( e ) {
190173 // Skip invalid JSON lines
191174 }
192175 }
193176 }
194177
195- // Convert map to array and filter by date range
196- let results = Array . from ( dailyMap . values ( ) ) ;
178+ // Group by date using Object.groupBy
179+ const groupedByDate = Object . groupBy ( allEntries , ( entry ) => entry . date ) ;
180+
181+ // Aggregate each group
182+ let results = Object . entries ( groupedByDate )
183+ . map ( ( [ date , entries ] ) => {
184+ if ( ! entries ) return null ;
185+
186+ return entries . reduce (
187+ ( acc , entry ) => ( {
188+ date,
189+ inputTokens :
190+ acc . inputTokens + ( entry . data . message . usage . input_tokens ?? 0 ) ,
191+ outputTokens :
192+ acc . outputTokens + ( entry . data . message . usage . output_tokens ?? 0 ) ,
193+ cacheCreationTokens :
194+ acc . cacheCreationTokens +
195+ ( entry . data . message . usage . cache_creation_input_tokens ?? 0 ) ,
196+ cacheReadTokens :
197+ acc . cacheReadTokens +
198+ ( entry . data . message . usage . cache_read_input_tokens ?? 0 ) ,
199+ totalCost : acc . totalCost + entry . cost ,
200+ } ) ,
201+ {
202+ date,
203+ inputTokens : 0 ,
204+ outputTokens : 0 ,
205+ cacheCreationTokens : 0 ,
206+ cacheReadTokens : 0 ,
207+ totalCost : 0 ,
208+ } ,
209+ ) ;
210+ } )
211+ . filter ( ( item ) : item is DailyUsage => item !== null ) ;
197212
198213 if ( options ?. since || options ?. until ) {
199214 results = results . filter ( ( data ) => {
@@ -230,27 +245,31 @@ export async function loadSessionData(
230245 const mode = options ?. mode || "auto" ;
231246 const modelPricing = mode === "display" ? { } : await fetchModelPricing ( ) ;
232247
233- const sessionMap = new Map <
234- string ,
235- SessionUsage & { versionSet : Set < string > }
236- > ( ) ;
248+ // Collect all valid data entries with session info first
249+ const allEntries : Array < {
250+ data : UsageData ;
251+ sessionKey : string ;
252+ sessionId : string ;
253+ projectPath : string ;
254+ cost : number ;
255+ timestamp : string ;
256+ } > = [ ] ;
237257
238258 for ( const file of files ) {
239259 // Extract session info from file path
240260 const relativePath = path . relative ( claudeDir , file ) ;
241261 const parts = relativePath . split ( path . sep ) ;
242262
243263 // Session ID is the directory name containing the JSONL file
244- const sessionId = parts [ parts . length - 2 ] ;
264+ const sessionId = parts [ parts . length - 2 ] || "unknown" ;
245265 // Project path is everything before the session ID
246- const projectPath = parts . slice ( 0 , - 2 ) . join ( path . sep ) ;
266+ const projectPath = parts . slice ( 0 , - 2 ) . join ( path . sep ) || "Unknown Project" ;
247267
248268 const content = await readFile ( file , "utf-8" ) ;
249269 const lines = content
250270 . trim ( )
251271 . split ( "\n" )
252272 . filter ( ( line ) => line . length > 0 ) ;
253- let lastTimestamp = "" ;
254273
255274 for ( const line of lines ) {
256275 try {
@@ -261,58 +280,84 @@ export async function loadSessionData(
261280 }
262281 const data = result . output ;
263282
264- const key = `${ projectPath } /${ sessionId } ` ;
265- const existing = sessionMap . get ( key ) || {
266- sessionId : sessionId || "unknown" ,
267- projectPath : projectPath || "Unknown Project" ,
268- inputTokens : 0 ,
269- outputTokens : 0 ,
270- cacheCreationTokens : 0 ,
271- cacheReadTokens : 0 ,
272- totalCost : 0 ,
273- lastActivity : "" ,
274- versions : [ ] ,
275- versionSet : new Set < string > ( ) ,
276- } ;
277-
278- existing . inputTokens += data . message . usage . input_tokens ?? 0 ;
279- existing . outputTokens += data . message . usage . output_tokens ?? 0 ;
280- existing . cacheCreationTokens +=
281- data . message . usage . cache_creation_input_tokens ?? 0 ;
282- existing . cacheReadTokens +=
283- data . message . usage . cache_read_input_tokens ?? 0 ;
284-
285- // Calculate cost based on mode
283+ const sessionKey = `${ projectPath } /${ sessionId } ` ;
286284 const cost = calculateCostForEntry ( data , mode , modelPricing ) ;
287- existing . totalCost += cost ;
288285
289- // Keep track of the latest timestamp
290- if ( data . timestamp > lastTimestamp ) {
291- lastTimestamp = data . timestamp ;
292- existing . lastActivity = formatDate ( data . timestamp ) ;
293- }
294-
295- // Collect version information
296- if ( data . version ) {
297- existing . versionSet . add ( data . version ) ;
298- }
299-
300- sessionMap . set ( key , existing ) ;
286+ allEntries . push ( {
287+ data,
288+ sessionKey,
289+ sessionId,
290+ projectPath,
291+ cost,
292+ timestamp : data . timestamp ,
293+ } ) ;
301294 } catch ( e ) {
302295 // Skip invalid JSON lines
303296 }
304297 }
305298 }
306299
307- // Convert map to array and filter by date range
308- let results = Array . from ( sessionMap . values ( ) ) . map ( ( session ) => {
309- // Convert Set to sorted array and remove temporary versionSet property
310- const { versionSet, ...sessionData } = session ;
311- return {
312- ...sessionData ,
313- versions : Array . from ( versionSet ) . sort ( ) ,
314- } ;
315- } ) ;
300+ // Group by session using Object.groupBy
301+ const groupedBySessions = Object . groupBy (
302+ allEntries ,
303+ ( entry ) => entry . sessionKey ,
304+ ) ;
305+
306+ // Aggregate each session group
307+ let results = Object . entries ( groupedBySessions )
308+ . map ( ( [ _ , entries ] ) => {
309+ if ( entries == null ) {
310+ return undefined ;
311+ }
312+
313+ // Find the latest timestamp for lastActivity
314+ const latestEntry = entries . reduce ( ( latest , current ) =>
315+ current . timestamp > latest . timestamp ? current : latest ,
316+ ) ;
317+
318+ // Collect all unique versions
319+ const versionSet = new Set < string > ( ) ;
320+ for ( const entry of entries ) {
321+ if ( entry . data . version ) {
322+ versionSet . add ( entry . data . version ) ;
323+ }
324+ }
325+
326+ // Aggregate totals
327+ const aggregated = entries . reduce (
328+ ( acc , entry ) => ( {
329+ sessionId : latestEntry . sessionId ,
330+ projectPath : latestEntry . projectPath ,
331+ inputTokens :
332+ acc . inputTokens + ( entry . data . message . usage . input_tokens ?? 0 ) ,
333+ outputTokens :
334+ acc . outputTokens + ( entry . data . message . usage . output_tokens ?? 0 ) ,
335+ cacheCreationTokens :
336+ acc . cacheCreationTokens +
337+ ( entry . data . message . usage . cache_creation_input_tokens ?? 0 ) ,
338+ cacheReadTokens :
339+ acc . cacheReadTokens +
340+ ( entry . data . message . usage . cache_read_input_tokens ?? 0 ) ,
341+ totalCost : acc . totalCost + entry . cost ,
342+ lastActivity : formatDate ( latestEntry . timestamp ) ,
343+ versions : Array . from ( versionSet ) . sort ( ) ,
344+ } ) ,
345+ {
346+ sessionId : latestEntry . sessionId ,
347+ projectPath : latestEntry . projectPath ,
348+ inputTokens : 0 ,
349+ outputTokens : 0 ,
350+ cacheCreationTokens : 0 ,
351+ cacheReadTokens : 0 ,
352+ totalCost : 0 ,
353+ lastActivity : formatDate ( latestEntry . timestamp ) ,
354+ versions : Array . from ( versionSet ) . sort ( ) ,
355+ } ,
356+ ) ;
357+
358+ return aggregated ;
359+ } )
360+ . filter ( ( item ) : item is SessionUsage => item !== null ) ;
316361
317362 if ( options ?. since || options ?. until ) {
318363 results = results . filter ( ( session ) => {
@@ -336,32 +381,39 @@ export async function loadMonthlyUsageData(
336381) : Promise < MonthlyUsage [ ] > {
337382 const dailyData = await loadDailyUsageData ( options ) ;
338383
339- const monthlyMap = new Map < string , MonthlyUsage > ( ) ;
340-
341- for ( const data of dailyData ) {
342- // Extract YYYY-MM from YYYY-MM-DD
343- const month = data . date . substring ( 0 , 7 ) ;
344-
345- const existing = monthlyMap . get ( month ) || {
346- month,
347- inputTokens : 0 ,
348- outputTokens : 0 ,
349- cacheCreationTokens : 0 ,
350- cacheReadTokens : 0 ,
351- totalCost : 0 ,
352- } ;
353-
354- existing . inputTokens += data . inputTokens ;
355- existing . outputTokens += data . outputTokens ;
356- existing . cacheCreationTokens += data . cacheCreationTokens ;
357- existing . cacheReadTokens += data . cacheReadTokens ;
358- existing . totalCost += data . totalCost ;
359-
360- monthlyMap . set ( month , existing ) ;
361- }
384+ // Group daily data by month using Object.groupBy
385+ const groupedByMonth = Object . groupBy ( dailyData , ( data ) =>
386+ data . date . substring ( 0 , 7 ) ,
387+ ) ;
388+
389+ // Aggregate each month group
390+ const monthlyArray = Object . entries ( groupedByMonth )
391+ . map ( ( [ month , dailyEntries ] ) => {
392+ if ( ! dailyEntries ) return null ;
393+
394+ return dailyEntries . reduce (
395+ ( acc , data ) => ( {
396+ month,
397+ inputTokens : acc . inputTokens + data . inputTokens ,
398+ outputTokens : acc . outputTokens + data . outputTokens ,
399+ cacheCreationTokens :
400+ acc . cacheCreationTokens + data . cacheCreationTokens ,
401+ cacheReadTokens : acc . cacheReadTokens + data . cacheReadTokens ,
402+ totalCost : acc . totalCost + data . totalCost ,
403+ } ) ,
404+ {
405+ month,
406+ inputTokens : 0 ,
407+ outputTokens : 0 ,
408+ cacheCreationTokens : 0 ,
409+ cacheReadTokens : 0 ,
410+ totalCost : 0 ,
411+ } ,
412+ ) ;
413+ } )
414+ . filter ( ( item ) : item is MonthlyUsage => item !== null ) ;
362415
363- // Convert to array and sort by month based on sortOrder
364- const monthlyArray = Array . from ( monthlyMap . values ( ) ) ;
416+ // Sort by month based on sortOrder
365417 const sortOrder = options ?. order || "desc" ;
366418 const sortedMonthly = sort ( monthlyArray ) ;
367419 return sortOrder === "desc"
0 commit comments