claude-usage-rzp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import Table from 'cli-table3';
4
+ import { loadStatsCache, buildProjectSummaries } from '../loader';
5
+ import { estimateCost, formatCost, formatTokens } from '../pricing';
6
+ import { getDataDir, hasStatsCache } from '../config';
7
+
8
+ async function showSummary() {
9
+ const stats = loadStatsCache();
10
+ if (!stats) return false;
11
+
12
+ // Calculate total cost
13
+ const totalCost = Object.entries(stats.modelStats).reduce((sum, [modelId, model]) => {
14
+ return (
15
+ sum +
16
+ estimateCost(
17
+ modelId,
18
+ model.inputTokens,
19
+ model.outputTokens,
20
+ model.cacheCreationInputTokens,
21
+ model.cacheReadInputTokens
22
+ )
23
+ );
24
+ }, 0);
25
+
26
+ // Calculate total cache tokens across all models
27
+ let totalCacheWrite = 0;
28
+ let totalCacheRead = 0;
29
+ for (const model of Object.values(stats.modelStats)) {
30
+ totalCacheWrite += model.cacheCreationInputTokens;
31
+ totalCacheRead += model.cacheReadInputTokens;
32
+ }
33
+
34
+ const totalTokens = stats.totalInputTokens + stats.totalOutputTokens + totalCacheWrite + totalCacheRead;
35
+
36
+ console.log(chalk.bold('\nšŸ“Š Summary:'));
37
+ console.log(` šŸ’¬ Total Sessions: ${chalk.green(stats.totalSessions.toLocaleString('en-US'))}`);
38
+ console.log(` šŸ“Ø Total Messages: ${chalk.green(stats.totalMessages.toLocaleString('en-US'))}`);
39
+ console.log(` šŸ”¢ Total Tokens Used: ${chalk.green.bold(formatTokens(totalTokens))}`);
40
+ console.log(` ā¬†ļø Total Input Tokens: ${chalk.cyan(formatTokens(stats.totalInputTokens))}`);
41
+ console.log(` ā¬‡ļø Total Output Tokens: ${chalk.green(formatTokens(stats.totalOutputTokens))}`);
42
+ console.log(` šŸ’° Estimated Cost: ${chalk.hex('#FF6B35').bold(formatCost(totalCost))}`);
43
+
44
+ return true;
45
+ }
46
+
47
+ async function showLast7Days() {
48
+ const stats = loadStatsCache();
49
+ if (!stats) return;
50
+
51
+ const recent = stats.dailyActivity.slice(-7);
52
+
53
+ console.log(chalk.bold('\nšŸ“… Recent Activity (Last 7 Days):'));
54
+
55
+ const activityTable = new Table({
56
+ head: [
57
+ chalk.cyan('Date'),
58
+ chalk.white('Sessions'),
59
+ chalk.white('Messages'),
60
+ chalk.white('Tool Calls'),
61
+ ],
62
+ colAligns: ['left', 'right', 'right', 'right'],
63
+ });
64
+
65
+ for (const day of recent) {
66
+ activityTable.push([
67
+ day.date,
68
+ day.sessionCount.toString(),
69
+ day.messageCount.toString(),
70
+ day.toolCallCount.toString(),
71
+ ]);
72
+ }
73
+
74
+ console.log(activityTable.toString());
75
+ }
76
+
77
+ async function showTokensByModel() {
78
+ const stats = loadStatsCache();
79
+ if (!stats) return;
80
+
81
+ console.log(chalk.bold('\nšŸ’Ž Token Usage by Model:'));
82
+
83
+ const table = new Table({
84
+ head: [
85
+ chalk.cyan('Model'),
86
+ chalk.blue('Input'),
87
+ chalk.green('Output'),
88
+ chalk.yellow('Cache Write'),
89
+ chalk.magenta('Cache Read'),
90
+ chalk.green.bold('Cost'),
91
+ ],
92
+ colAligns: ['left', 'right', 'right', 'right', 'right', 'right'],
93
+ });
94
+
95
+ for (const [modelId, model] of Object.entries(stats.modelStats)) {
96
+ const cost = estimateCost(
97
+ modelId,
98
+ model.inputTokens,
99
+ model.outputTokens,
100
+ model.cacheCreationInputTokens,
101
+ model.cacheReadInputTokens
102
+ );
103
+
104
+ let displayName = modelId.replace('claude-', '').replace(/-20\d{6}/, (match) => ` (${match.substring(1)})`);
105
+
106
+ table.push([
107
+ displayName,
108
+ formatTokens(model.inputTokens),
109
+ formatTokens(model.outputTokens),
110
+ formatTokens(model.cacheCreationInputTokens),
111
+ formatTokens(model.cacheReadInputTokens),
112
+ formatCost(cost),
113
+ ]);
114
+ }
115
+
116
+ console.log(table.toString());
117
+ }
118
+
119
+ async function showProjects() {
120
+ const projectSummaries = buildProjectSummaries();
121
+
122
+ if (Object.keys(projectSummaries).length === 0) {
123
+ console.log(chalk.yellow('\nNo projects found'));
124
+ return;
125
+ }
126
+
127
+ interface ProjectData {
128
+ path: string;
129
+ encoded: string;
130
+ sessions: number;
131
+ input: number;
132
+ output: number;
133
+ cost: number;
134
+ }
135
+
136
+ const projectData: ProjectData[] = [];
137
+
138
+ for (const [encodedPath, sessions] of Object.entries(projectSummaries)) {
139
+ const displayPath = decodeURIComponent(encodedPath);
140
+ const sessionCount = sessions.length;
141
+ const totalInput = sessions.reduce((sum, s) => sum + s.totalInputTokens, 0);
142
+ const totalOutput = sessions.reduce((sum, s) => sum + s.totalOutputTokens, 0);
143
+ const totalCost = sessions.reduce((sum, s) => sum + s.totalCostUsd, 0);
144
+
145
+ projectData.push({
146
+ path: displayPath,
147
+ encoded: encodedPath,
148
+ sessions: sessionCount,
149
+ input: totalInput,
150
+ output: totalOutput,
151
+ cost: totalCost,
152
+ });
153
+ }
154
+
155
+ projectData.sort((a, b) => b.cost - a.cost);
156
+
157
+ console.log(chalk.bold(`\nšŸ“ Projects (${projectData.length} total):`));
158
+
159
+ const table = new Table({
160
+ head: [
161
+ chalk.cyan('Project Path'),
162
+ chalk.white('Sessions'),
163
+ chalk.blue('Input Tokens'),
164
+ chalk.green('Output Tokens'),
165
+ chalk.green.bold('Total Cost'),
166
+ ],
167
+ colAligns: ['left', 'right', 'right', 'right', 'right'],
168
+ colWidths: [40, 10, 15, 15, 12],
169
+ wordWrap: true,
170
+ });
171
+
172
+ for (const proj of projectData) {
173
+ table.push([
174
+ proj.path,
175
+ proj.sessions.toString(),
176
+ formatTokens(proj.input),
177
+ formatTokens(proj.output),
178
+ formatCost(proj.cost),
179
+ ]);
180
+ }
181
+
182
+ console.log(table.toString());
183
+ }
184
+
185
+ export async function interactiveCommand() {
186
+ if (!hasStatsCache()) {
187
+ console.log(chalk.red(`\nāœ— No stats-cache.json found in ${getDataDir()}`));
188
+ console.log(
189
+ chalk.dim('Make sure CLAUDE_DATA_DIR is set correctly or use --data-dir option.\n')
190
+ );
191
+ process.exit(1);
192
+ }
193
+
194
+ // Clear screen and show header
195
+ console.clear();
196
+
197
+ // ASCII Art Header - Claude orange color
198
+ console.log(chalk.hex('#FF6B35').bold(`
199
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—
200
+ ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā•ā•ā•
201
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•—
202
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•
203
+ ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—
204
+ ā•šā•ā•ā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•
205
+ `));
206
+ console.log(chalk.hex('#FF6B35')(' Usage Visualizer') + chalk.dim(' 惻 Track your AI costs\n'));
207
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
208
+
209
+ // Show summary first
210
+ const hasData = await showSummary();
211
+ if (!hasData) {
212
+ console.log(chalk.red('\nāœ— Failed to load stats cache\n'));
213
+ process.exit(1);
214
+ }
215
+
216
+ // Interactive menu loop
217
+ let running = true;
218
+
219
+ while (running) {
220
+ console.log(); // spacing
221
+
222
+ const { action } = await inquirer.prompt([
223
+ {
224
+ type: 'list',
225
+ name: 'action',
226
+ message: 'What would you like to see?',
227
+ choices: [
228
+ { name: 'šŸ“… Last 7 Days Activity', value: 'last7days' },
229
+ { name: 'šŸ’Ž Token Usage by Model', value: 'tokens' },
230
+ { name: 'šŸ“ All Projects', value: 'projects' },
231
+ new inquirer.Separator(),
232
+ { name: 'šŸ”„ Refresh Data', value: 'refresh' },
233
+ { name: 'āŒ Exit', value: 'exit' },
234
+ ],
235
+ },
236
+ ]);
237
+
238
+ console.log(); // spacing
239
+
240
+ switch (action) {
241
+ case 'last7days':
242
+ await showLast7Days();
243
+ break;
244
+ case 'tokens':
245
+ await showTokensByModel();
246
+ break;
247
+ case 'projects':
248
+ await showProjects();
249
+ break;
250
+ case 'refresh':
251
+ console.clear();
252
+ // ASCII Art Header - Claude orange color
253
+ console.log(chalk.hex('#FF6B35').bold(`
254
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—
255
+ ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā•ā•ā•
256
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•—
257
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•
258
+ ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—
259
+ ā•šā•ā•ā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•
260
+ `));
261
+ console.log(chalk.hex('#FF6B35')(' Usage Visualizer') + chalk.dim(' 惻 Track your AI costs\n'));
262
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
263
+ await showSummary();
264
+ break;
265
+ case 'exit':
266
+ console.log(chalk.dim('\nHamid said Goodbye! šŸ‘‹\n'));
267
+ running = false;
268
+ break;
269
+ }
270
+ }
271
+ }
@@ -0,0 +1,126 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { loadStatsCache } from '../loader';
4
+ import { estimateCost, formatCost, formatTokens } from '../pricing';
5
+ import { getDataDir, hasStatsCache } from '../config';
6
+
7
+ export function overviewCommand() {
8
+ if (!hasStatsCache()) {
9
+ console.log(chalk.red(`āœ— No stats-cache.json found in ${getDataDir()}`));
10
+ console.log(
11
+ chalk.dim('\nMake sure CLAUDE_DATA_DIR is set correctly or use --data-dir option.')
12
+ );
13
+ process.exit(1);
14
+ }
15
+
16
+ const stats = loadStatsCache();
17
+
18
+ if (!stats) {
19
+ console.log(chalk.red('āœ— Failed to load stats cache'));
20
+ process.exit(1);
21
+ }
22
+
23
+ // Header
24
+ console.log(chalk.cyan.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
25
+ console.log(chalk.cyan.bold(' Claude Code Usage Overview'));
26
+ console.log(chalk.cyan.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
27
+
28
+ // Calculate total cost
29
+ const totalCost = Object.entries(stats.modelStats).reduce((sum, [modelId, model]) => {
30
+ return (
31
+ sum +
32
+ estimateCost(
33
+ modelId,
34
+ model.inputTokens,
35
+ model.outputTokens,
36
+ model.cacheCreationInputTokens,
37
+ model.cacheReadInputTokens
38
+ )
39
+ );
40
+ }, 0);
41
+
42
+ // Summary stats
43
+ console.log(chalk.bold('Summary:'));
44
+ console.log(` Total Sessions: ${chalk.green(stats.totalSessions.toLocaleString('en-US'))}`);
45
+ console.log(` Total Messages: ${chalk.green(stats.totalMessages.toLocaleString('en-US'))}`);
46
+ console.log(` Total Input Tokens: ${chalk.blue(formatTokens(stats.totalInputTokens))}`);
47
+ console.log(` Total Output Tokens: ${chalk.green(formatTokens(stats.totalOutputTokens))}`);
48
+ console.log(` Estimated Cost: ${chalk.green.bold(formatCost(totalCost))}`);
49
+
50
+ if (stats.firstSessionDate) {
51
+ console.log(` First Session: ${chalk.dim(stats.firstSessionDate)}`);
52
+ }
53
+ if (stats.lastComputedDate) {
54
+ console.log(` Last Computed: ${chalk.dim(stats.lastComputedDate)}`);
55
+ }
56
+
57
+ // Token breakdown by model
58
+ if (Object.keys(stats.modelStats).length > 0) {
59
+ console.log(chalk.bold('\nToken Usage by Model:'));
60
+
61
+ const table = new Table({
62
+ head: [
63
+ chalk.cyan('Model'),
64
+ chalk.blue('Input'),
65
+ chalk.green('Output'),
66
+ chalk.yellow('Cache Write'),
67
+ chalk.magenta('Cache Read'),
68
+ chalk.green.bold('Cost'),
69
+ ],
70
+ colAligns: ['left', 'right', 'right', 'right', 'right', 'right'],
71
+ });
72
+
73
+ for (const [modelId, model] of Object.entries(stats.modelStats)) {
74
+ const cost = estimateCost(
75
+ modelId,
76
+ model.inputTokens,
77
+ model.outputTokens,
78
+ model.cacheCreationInputTokens,
79
+ model.cacheReadInputTokens
80
+ );
81
+
82
+ // Simplify model name
83
+ let displayName = modelId.replace('claude-', '').replace(/-20\d{6}/, (match) => ` (${match.substring(1)})`);
84
+
85
+ table.push([
86
+ displayName,
87
+ formatTokens(model.inputTokens),
88
+ formatTokens(model.outputTokens),
89
+ formatTokens(model.cacheCreationInputTokens),
90
+ formatTokens(model.cacheReadInputTokens),
91
+ formatCost(cost),
92
+ ]);
93
+ }
94
+
95
+ console.log(table.toString());
96
+ }
97
+
98
+ // Recent activity
99
+ if (stats.dailyActivity.length > 0) {
100
+ const recent = stats.dailyActivity.slice(-7); // Last 7 days
101
+ console.log(chalk.bold('\nRecent Activity (Last 7 Days):'));
102
+
103
+ const activityTable = new Table({
104
+ head: [
105
+ chalk.cyan('Date'),
106
+ chalk.white('Sessions'),
107
+ chalk.white('Messages'),
108
+ chalk.white('Tool Calls'),
109
+ ],
110
+ colAligns: ['left', 'right', 'right', 'right'],
111
+ });
112
+
113
+ for (const day of recent) {
114
+ activityTable.push([
115
+ day.date,
116
+ day.sessionCount.toString(),
117
+ day.messageCount.toString(),
118
+ day.toolCallCount.toString(),
119
+ ]);
120
+ }
121
+
122
+ console.log(activityTable.toString());
123
+ }
124
+
125
+ console.log(chalk.dim('\nšŸ’” Tip: Use "claude-usage projects" to see all projects\n'));
126
+ }
@@ -0,0 +1,85 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { buildProjectSummaries } from '../loader';
4
+ import { formatCost, formatTokens } from '../pricing';
5
+ import { SessionSummary } from '../types';
6
+
7
+ export function projectCommand(projectPath: string) {
8
+ const projectSummaries = buildProjectSummaries();
9
+
10
+ // Try to find the project (handle both encoded and unencoded paths)
11
+ const encodedPath = encodeURIComponent(projectPath);
12
+ let sessions: SessionSummary[] | undefined;
13
+ let foundPath = '';
14
+
15
+ if (encodedPath in projectSummaries) {
16
+ sessions = projectSummaries[encodedPath];
17
+ foundPath = encodedPath;
18
+ } else {
19
+ // Try to find by partial match
20
+ for (const [path, sess] of Object.entries(projectSummaries)) {
21
+ if (projectPath.includes(decodeURIComponent(path)) || decodeURIComponent(path).includes(projectPath)) {
22
+ sessions = sess;
23
+ foundPath = path;
24
+ break;
25
+ }
26
+ }
27
+ }
28
+
29
+ if (!sessions) {
30
+ console.log(chalk.red(`āœ— Project not found: ${projectPath}`));
31
+ console.log(chalk.dim('\nAvailable projects:'));
32
+ for (const path of Object.keys(projectSummaries)) {
33
+ console.log(` - ${decodeURIComponent(path)}`);
34
+ }
35
+ process.exit(1);
36
+ }
37
+
38
+ // Sort sessions by timestamp descending
39
+ sessions = sessions.slice().sort((a, b) => b.timestamp.localeCompare(a.timestamp));
40
+
41
+ console.log(chalk.cyan.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
42
+ console.log(chalk.cyan.bold(` Project: ${decodeURIComponent(foundPath)}`));
43
+ console.log(chalk.dim(` ${sessions.length} sessions`));
44
+ console.log(chalk.cyan.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
45
+
46
+ const table = new Table({
47
+ head: [
48
+ chalk.cyan('Session'),
49
+ chalk.dim('Timestamp'),
50
+ chalk.white('Messages'),
51
+ chalk.blue('Input'),
52
+ chalk.green('Output'),
53
+ chalk.green.bold('Cost'),
54
+ chalk.yellow('Models'),
55
+ ],
56
+ colAligns: ['left', 'left', 'right', 'right', 'right', 'right', 'left'],
57
+ colWidths: [30, 20, 10, 12, 12, 10, 20],
58
+ wordWrap: true,
59
+ });
60
+
61
+ for (const session of sessions) {
62
+ // Truncate slug if too long
63
+ const slugDisplay = session.slug.length > 60 ? session.slug.substring(0, 57) + '...' : session.slug;
64
+
65
+ // Format timestamp
66
+ const timestamp = session.timestamp ? new Date(session.timestamp).toLocaleString('en-US') : 'N/A';
67
+
68
+ table.push([
69
+ slugDisplay,
70
+ timestamp,
71
+ session.messageCount.toString(),
72
+ formatTokens(session.totalInputTokens),
73
+ formatTokens(session.totalOutputTokens),
74
+ formatCost(session.totalCostUsd),
75
+ Array.from(session.modelsUsed)
76
+ .map(m => m.replace('claude-', ''))
77
+ .join(', '),
78
+ ]);
79
+ }
80
+
81
+ console.log(table.toString());
82
+ console.log(
83
+ chalk.dim('\nšŸ’” Tip: Use "claude-usage session <session-id>" to see message details\n')
84
+ );
85
+ }
@@ -0,0 +1,83 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { buildProjectSummaries } from '../loader';
4
+ import { formatCost, formatTokens } from '../pricing';
5
+ import { hasProjects, getDataDir } from '../config';
6
+
7
+ export function projectsCommand() {
8
+ if (!hasProjects()) {
9
+ console.log(chalk.red(`āœ— No projects directory found in ${getDataDir()}`));
10
+ process.exit(1);
11
+ }
12
+
13
+ const projectSummaries = buildProjectSummaries();
14
+
15
+ if (Object.keys(projectSummaries).length === 0) {
16
+ console.log(chalk.yellow('No projects found'));
17
+ return;
18
+ }
19
+
20
+ // Build project-level aggregates
21
+ interface ProjectData {
22
+ path: string;
23
+ encoded: string;
24
+ sessions: number;
25
+ input: number;
26
+ output: number;
27
+ cost: number;
28
+ }
29
+
30
+ const projectData: ProjectData[] = [];
31
+
32
+ for (const [encodedPath, sessions] of Object.entries(projectSummaries)) {
33
+ const displayPath = decodeURIComponent(encodedPath);
34
+ const sessionCount = sessions.length;
35
+ const totalInput = sessions.reduce((sum, s) => sum + s.totalInputTokens, 0);
36
+ const totalOutput = sessions.reduce((sum, s) => sum + s.totalOutputTokens, 0);
37
+ const totalCost = sessions.reduce((sum, s) => sum + s.totalCostUsd, 0);
38
+
39
+ projectData.push({
40
+ path: displayPath,
41
+ encoded: encodedPath,
42
+ sessions: sessionCount,
43
+ input: totalInput,
44
+ output: totalOutput,
45
+ cost: totalCost,
46
+ });
47
+ }
48
+
49
+ // Sort by cost descending
50
+ projectData.sort((a, b) => b.cost - a.cost);
51
+
52
+ console.log(chalk.cyan.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
53
+ console.log(chalk.cyan.bold(` Projects (${projectData.length} total)`));
54
+ console.log(chalk.cyan.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
55
+
56
+ const table = new Table({
57
+ head: [
58
+ chalk.cyan('Project Path'),
59
+ chalk.white('Sessions'),
60
+ chalk.blue('Input Tokens'),
61
+ chalk.green('Output Tokens'),
62
+ chalk.green.bold('Total Cost'),
63
+ ],
64
+ colAligns: ['left', 'right', 'right', 'right', 'right'],
65
+ colWidths: [40, 10, 15, 15, 12],
66
+ wordWrap: true,
67
+ });
68
+
69
+ for (const proj of projectData) {
70
+ table.push([
71
+ proj.path,
72
+ proj.sessions.toString(),
73
+ formatTokens(proj.input),
74
+ formatTokens(proj.output),
75
+ formatCost(proj.cost),
76
+ ]);
77
+ }
78
+
79
+ console.log(table.toString());
80
+ console.log(
81
+ chalk.dim('\nšŸ’” Tip: Use "claude-usage project <path>" to see sessions in a project\n')
82
+ );
83
+ }
@@ -0,0 +1,105 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { buildProjectSummaries, loadSessionMessages } from '../loader';
4
+ import { formatCost, formatTokens } from '../pricing';
5
+
6
+ export function sessionCommand(sessionId: string, options: { project?: string }) {
7
+ const projectSummaries = buildProjectSummaries();
8
+
9
+ let foundSession: any = null;
10
+ let foundProject = '';
11
+
12
+ if (options.project) {
13
+ // User specified project
14
+ const encoded = encodeURIComponent(options.project);
15
+ if (encoded in projectSummaries) {
16
+ for (const sess of projectSummaries[encoded]) {
17
+ if (sess.sessionId === sessionId || sess.sessionId.startsWith(sessionId)) {
18
+ foundSession = sess;
19
+ foundProject = encoded;
20
+ break;
21
+ }
22
+ }
23
+ }
24
+ } else {
25
+ // Search all projects
26
+ for (const [encodedPath, sessions] of Object.entries(projectSummaries)) {
27
+ for (const sess of sessions) {
28
+ if (sess.sessionId === sessionId || sess.sessionId.startsWith(sessionId)) {
29
+ foundSession = sess;
30
+ foundProject = encodedPath;
31
+ break;
32
+ }
33
+ }
34
+ if (foundSession) break;
35
+ }
36
+ }
37
+
38
+ if (!foundSession) {
39
+ console.log(chalk.red(`āœ— Session not found: ${sessionId}`));
40
+ process.exit(1);
41
+ }
42
+
43
+ // Load messages
44
+ const messages = loadSessionMessages(foundProject, foundSession.sessionId);
45
+
46
+ if (messages.length === 0) {
47
+ console.log(chalk.yellow('No messages found in this session'));
48
+ return;
49
+ }
50
+
51
+ console.log(chalk.cyan.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
52
+ console.log(chalk.cyan.bold(` Session: ${foundSession.slug}`));
53
+ console.log(chalk.dim(` Project: ${decodeURIComponent(foundProject)}`));
54
+ console.log(chalk.dim(` ID: ${foundSession.sessionId}`));
55
+ console.log(chalk.dim(` ${messages.length} messages`));
56
+ console.log(chalk.cyan.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
57
+
58
+ // Summary
59
+ console.log(chalk.bold('Summary:'));
60
+ console.log(` Total Input Tokens: ${chalk.blue(formatTokens(foundSession.totalInputTokens))}`);
61
+ console.log(` Total Output Tokens: ${chalk.green(formatTokens(foundSession.totalOutputTokens))}`);
62
+ console.log(` Total Cost: ${chalk.green.bold(formatCost(foundSession.totalCostUsd))}`);
63
+ console.log(` Models Used: ${chalk.yellow(Array.from(foundSession.modelsUsed).join(', '))}`);
64
+
65
+ // Message breakdown
66
+ console.log(chalk.bold('\nMessage Details:'));
67
+
68
+ const table = new Table({
69
+ head: [
70
+ chalk.dim('#'),
71
+ chalk.dim('Timestamp'),
72
+ chalk.cyan('Model'),
73
+ chalk.blue('Input'),
74
+ chalk.green('Output'),
75
+ chalk.yellow('Cache Write'),
76
+ chalk.magenta('Cache Read'),
77
+ chalk.green.bold('Cost'),
78
+ ],
79
+ colAligns: ['right', 'left', 'left', 'right', 'right', 'right', 'right', 'right'],
80
+ });
81
+
82
+ for (let i = 0; i < messages.length; i++) {
83
+ const msg = messages[i];
84
+
85
+ // Simplify model name
86
+ let modelDisplay = msg.model.replace('claude-', '').replace(/-20\d{6}/, (match) => ` (${match.substring(1)})`);
87
+
88
+ // Format timestamp
89
+ const timestamp = msg.timestamp ? new Date(msg.timestamp).toLocaleString('en-US') : 'N/A';
90
+
91
+ table.push([
92
+ (i + 1).toString(),
93
+ timestamp,
94
+ modelDisplay,
95
+ formatTokens(msg.inputTokens),
96
+ formatTokens(msg.outputTokens),
97
+ formatTokens(msg.cacheCreationInputTokens),
98
+ formatTokens(msg.cacheReadInputTokens),
99
+ formatCost(msg.costUsd),
100
+ ]);
101
+ }
102
+
103
+ console.log(table.toString());
104
+ console.log();
105
+ }