Skip to content

Commit a67d579

Browse files
committed
refactor: improve session command code organization and add documentation
Implements feedback from code review: - Extract session ID lookup logic into separate helper functions - Improve totalTokens calculation readability with dedicated function - Use formatDateCompact() instead of toISOString() for better date formatting - Add proper TypeScript types to helper functions using CostMode and UsageData - Add comprehensive documentation for --id option in session-reports.md This refactoring makes the code more maintainable and follows the single-responsibility principle while preserving all functionality.
1 parent 1ecea4c commit a67d579

2 files changed

Lines changed: 201 additions & 99 deletions

File tree

‎docs/guide/session-reports.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,44 @@ Session reports show your Claude Code usage grouped by individual conversation s
88
ccusage session
99
```
1010

11+
## Specific Session Lookup
12+
13+
Query individual session details by providing a session ID:
14+
15+
```bash
16+
ccusage session --id <session-id>
17+
```
18+
19+
This is particularly useful for:
20+
- **Custom statuslines**: Integrate specific session data into your development environment
21+
- **Programmatic usage**: Extract session metrics for scripts and automation
22+
- **Detailed analysis**: Get comprehensive data about a single conversation
23+
24+
### Examples
25+
26+
```bash
27+
# Get session data in table format
28+
ccusage session --id session-abc123-def456
29+
30+
# Get session data as JSON for scripting
31+
ccusage session --id session-abc123-def456 --json
32+
33+
# Extract just the cost using jq
34+
ccusage session --id session-abc123-def456 --json --jq '.totalCost'
35+
36+
# Use in a custom statusline script
37+
COST=$(ccusage session --id "$SESSION_ID" --json --jq '.totalCost')
38+
echo "Current session: \$${COST}"
39+
```
40+
41+
### Session ID Format
42+
43+
Session IDs are the actual filenames (without `.jsonl` extension) stored in Claude's data directories. They typically look like:
44+
- `session-20250621-abc123-def456`
45+
- `project-conversation-xyz789`
46+
47+
You can find session IDs by running `ccusage session` and looking for the files in your Claude data directory.
48+
1149
## Example Output
1250

1351
```
@@ -51,6 +89,27 @@ Sessions are sorted by cost (highest first) by default, making it easy to identi
5189

5290
## Command Options
5391

92+
### Session ID Lookup
93+
94+
Get detailed information about a specific session:
95+
96+
```bash
97+
# Query a specific session by ID
98+
ccusage session --id <session-id>
99+
100+
# Get JSON output for a specific session
101+
ccusage session --id <session-id> --json
102+
103+
# Short form using -i flag
104+
ccusage session -i <session-id>
105+
```
106+
107+
**Use cases:**
108+
- Building custom statuslines that show current session costs
109+
- Creating scripts that monitor specific conversation expenses
110+
- Debugging or analyzing individual conversation patterns
111+
- Integrating session data into development workflows
112+
54113
### Date Filtering
55114

56115
Filter sessions by their last activity date:

‎src/commands/session.ts‎

Lines changed: 142 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { CostMode } from '../_types.ts';
2+
import type { UsageData } from '../data-loader.ts';
13
import process from 'node:process';
24
import { Result } from '@praha/byethrow';
35
import { define } from 'gunshi';
@@ -14,6 +16,145 @@ import { formatDateCompact, loadSessionData, loadSessionUsageById } from '../dat
1416
import { detectMismatches, printMismatchReport } from '../debug.ts';
1517
import { 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+
17158
export 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

Comments
 (0)