1+ import type { CostMode } from '../_types.ts' ;
2+ import type { UsageData } from '../data-loader.ts' ;
13import process from 'node:process' ;
24import { Result } from '@praha/byethrow' ;
35import { define } from 'gunshi' ;
@@ -14,6 +16,145 @@ import { formatDateCompact, loadSessionData, loadSessionUsageById } from '../dat
1416import { detectMismatches , printMismatchReport } from '../debug.ts' ;
1517import { log , logger } from '../logger.ts' ;
1618
19+ /**
20+ * Calculates the total tokens for a session's entries
21+ */
22+ function calculateSessionTotalTokens ( entries : UsageData [ ] ) : number {
23+ return entries . reduce ( ( sum , entry ) => {
24+ const usage = entry . message . usage ;
25+ return sum
26+ + usage . input_tokens
27+ + usage . output_tokens
28+ + ( usage . cache_creation_input_tokens ?? 0 )
29+ + ( usage . cache_read_input_tokens ?? 0 ) ;
30+ } , 0 ) ;
31+ }
32+
33+ /**
34+ * Handles session ID lookup and output formatting
35+ */
36+ async function handleSessionIdLookup (
37+ ctx : { values : { id : string ; mode : CostMode ; offline : boolean ; jq ?: string ; timezone ?: string ; locale ?: string } } ,
38+ useJson : boolean ,
39+ ) : Promise < void > {
40+ const sessionUsage = await loadSessionUsageById ( ctx . values . id , {
41+ mode : ctx . values . mode ,
42+ offline : ctx . values . offline ,
43+ } ) ;
44+
45+ if ( sessionUsage == null ) {
46+ if ( useJson ) {
47+ log ( JSON . stringify ( null ) ) ;
48+ }
49+ else {
50+ logger . warn ( `No session found with ID: ${ ctx . values . id } ` ) ;
51+ }
52+ process . exit ( 0 ) ;
53+ }
54+
55+ if ( useJson ) {
56+ await outputSessionJsonFormat ( ctx , sessionUsage ) ;
57+ }
58+ else {
59+ await outputSessionTableFormat ( ctx , sessionUsage ) ;
60+ }
61+ }
62+
63+ /**
64+ * Outputs session data in JSON format
65+ */
66+ async function outputSessionJsonFormat (
67+ ctx : { values : { id : string ; jq ?: string } } ,
68+ sessionUsage : { totalCost : number ; entries : UsageData [ ] } ,
69+ ) : Promise < void > {
70+ const jsonOutput = {
71+ sessionId : ctx . values . id ,
72+ totalCost : sessionUsage . totalCost ,
73+ entries : sessionUsage . entries . map ( entry => ( {
74+ timestamp : entry . timestamp ,
75+ inputTokens : entry . message . usage . input_tokens ,
76+ outputTokens : entry . message . usage . output_tokens ,
77+ cacheCreationTokens : entry . message . usage . cache_creation_input_tokens ?? 0 ,
78+ cacheReadTokens : entry . message . usage . cache_read_input_tokens ?? 0 ,
79+ model : entry . message . model ?? 'unknown' ,
80+ costUSD : entry . costUSD ?? 0 ,
81+ } ) ) ,
82+ } ;
83+
84+ // Process with jq if specified
85+ if ( ctx . values . jq != null ) {
86+ const jqResult = await processWithJq ( jsonOutput , ctx . values . jq ) ;
87+ if ( Result . isFailure ( jqResult ) ) {
88+ logger . error ( ( jqResult . error ) . message ) ;
89+ process . exit ( 1 ) ;
90+ }
91+ log ( jqResult . value ) ;
92+ }
93+ else {
94+ log ( JSON . stringify ( jsonOutput , null , 2 ) ) ;
95+ }
96+ }
97+
98+ /**
99+ * Outputs session data in table format
100+ */
101+ async function outputSessionTableFormat (
102+ ctx : { values : { id : string ; timezone ?: string ; locale ?: string } } ,
103+ sessionUsage : { totalCost : number ; entries : UsageData [ ] } ,
104+ ) : Promise < void > {
105+ // Print header
106+ logger . box ( `Claude Code Session Usage - ${ ctx . values . id } ` ) ;
107+
108+ // Show session summary
109+ const totalTokens = calculateSessionTotalTokens ( sessionUsage . entries ) ;
110+
111+ log ( `Total Cost: ${ formatCurrency ( sessionUsage . totalCost ) } ` ) ;
112+ log ( `Total Tokens: ${ formatNumber ( totalTokens ) } ` ) ;
113+ log ( `Total Entries: ${ sessionUsage . entries . length } ` ) ;
114+ log ( '' ) ;
115+
116+ // Show individual entries
117+ if ( sessionUsage . entries . length > 0 ) {
118+ const table = new ResponsiveTable ( {
119+ head : [
120+ 'Timestamp' ,
121+ 'Model' ,
122+ 'Input' ,
123+ 'Output' ,
124+ 'Cache Create' ,
125+ 'Cache Read' ,
126+ 'Cost (USD)' ,
127+ ] ,
128+ style : {
129+ head : [ 'cyan' ] ,
130+ } ,
131+ colAligns : [
132+ 'left' ,
133+ 'left' ,
134+ 'right' ,
135+ 'right' ,
136+ 'right' ,
137+ 'right' ,
138+ 'right' ,
139+ ] ,
140+ } ) ;
141+
142+ for ( const entry of sessionUsage . entries ) {
143+ table . push ( [
144+ formatDateCompact ( entry . timestamp , ctx . values . timezone , ctx . values . locale ) ,
145+ entry . message . model ?? 'unknown' ,
146+ formatNumber ( entry . message . usage . input_tokens ) ,
147+ formatNumber ( entry . message . usage . output_tokens ) ,
148+ formatNumber ( entry . message . usage . cache_creation_input_tokens ?? 0 ) ,
149+ formatNumber ( entry . message . usage . cache_read_input_tokens ?? 0 ) ,
150+ formatCurrency ( entry . costUSD ?? 0 ) ,
151+ ] ) ;
152+ }
153+
154+ log ( table . toString ( ) ) ;
155+ }
156+ }
157+
17158export const sessionCommand = define ( {
18159 name : 'session' ,
19160 description : 'Show usage report grouped by conversation session' ,
@@ -35,105 +176,7 @@ export const sessionCommand = define({
35176
36177 // Handle specific session ID lookup
37178 if ( ctx . values . id != null ) {
38- const sessionUsage = await loadSessionUsageById ( ctx . values . id , {
39- mode : ctx . values . mode ,
40- offline : ctx . values . offline ,
41- } ) ;
42-
43- if ( sessionUsage == null ) {
44- if ( useJson ) {
45- log ( JSON . stringify ( null ) ) ;
46- }
47- else {
48- logger . warn ( `No session found with ID: ${ ctx . values . id } ` ) ;
49- }
50- process . exit ( 0 ) ;
51- }
52-
53- if ( useJson ) {
54- const jsonOutput = {
55- sessionId : ctx . values . id ,
56- totalCost : sessionUsage . totalCost ,
57- entries : sessionUsage . entries . map ( entry => ( {
58- timestamp : entry . timestamp ,
59- inputTokens : entry . message . usage . input_tokens ,
60- outputTokens : entry . message . usage . output_tokens ,
61- cacheCreationTokens : entry . message . usage . cache_creation_input_tokens ?? 0 ,
62- cacheReadTokens : entry . message . usage . cache_read_input_tokens ?? 0 ,
63- model : entry . message . model ?? 'unknown' ,
64- costUSD : entry . costUSD ?? 0 ,
65- } ) ) ,
66- } ;
67-
68- // Process with jq if specified
69- if ( ctx . values . jq != null ) {
70- const jqResult = await processWithJq ( jsonOutput , ctx . values . jq ) ;
71- if ( Result . isFailure ( jqResult ) ) {
72- logger . error ( ( jqResult . error ) . message ) ;
73- process . exit ( 1 ) ;
74- }
75- log ( jqResult . value ) ;
76- }
77- else {
78- log ( JSON . stringify ( jsonOutput , null , 2 ) ) ;
79- }
80- }
81- else {
82- // Print header
83- logger . box ( `Claude Code Session Usage - ${ ctx . values . id } ` ) ;
84-
85- // Show session summary
86- const totalTokens = sessionUsage . entries . reduce ( ( sum , entry ) =>
87- sum + entry . message . usage . input_tokens + entry . message . usage . output_tokens
88- + ( entry . message . usage . cache_creation_input_tokens ?? 0 )
89- + ( entry . message . usage . cache_read_input_tokens ?? 0 ) , 0 ) ;
90-
91- log ( `Total Cost: ${ formatCurrency ( sessionUsage . totalCost ) } ` ) ;
92- log ( `Total Tokens: ${ formatNumber ( totalTokens ) } ` ) ;
93- log ( `Total Entries: ${ sessionUsage . entries . length } ` ) ;
94- log ( '' ) ;
95-
96- // Show individual entries
97- if ( sessionUsage . entries . length > 0 ) {
98- const table = new ResponsiveTable ( {
99- head : [
100- 'Timestamp' ,
101- 'Model' ,
102- 'Input' ,
103- 'Output' ,
104- 'Cache Create' ,
105- 'Cache Read' ,
106- 'Cost (USD)' ,
107- ] ,
108- style : {
109- head : [ 'cyan' ] ,
110- } ,
111- colAligns : [
112- 'left' ,
113- 'left' ,
114- 'right' ,
115- 'right' ,
116- 'right' ,
117- 'right' ,
118- 'right' ,
119- ] ,
120- } ) ;
121-
122- for ( const entry of sessionUsage . entries ) {
123- table . push ( [
124- new Date ( entry . timestamp ) . toISOString ( ) ,
125- entry . message . model ?? 'unknown' ,
126- formatNumber ( entry . message . usage . input_tokens ) ,
127- formatNumber ( entry . message . usage . output_tokens ) ,
128- formatNumber ( entry . message . usage . cache_creation_input_tokens ?? 0 ) ,
129- formatNumber ( entry . message . usage . cache_read_input_tokens ?? 0 ) ,
130- formatCurrency ( entry . costUSD ?? 0 ) ,
131- ] ) ;
132- }
133-
134- log ( table . toString ( ) ) ;
135- }
136- }
179+ await handleSessionIdLookup ( ctx , useJson ) ;
137180 return ;
138181 }
139182
0 commit comments