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.
package/src/config.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+
5
+ let dataDir: string | null = null;
6
+
7
+ export function getDataDir(): string {
8
+ if (dataDir) {
9
+ return dataDir;
10
+ }
11
+
12
+ // Check environment variable
13
+ const envDir = process.env.CLAUDE_DATA_DIR;
14
+ if (envDir) {
15
+ dataDir = envDir;
16
+ return dataDir;
17
+ }
18
+
19
+ // Default to ~/.claude
20
+ dataDir = join(homedir(), '.claude');
21
+ return dataDir;
22
+ }
23
+
24
+ export function setDataDir(path: string): void {
25
+ dataDir = path;
26
+ }
27
+
28
+ export function hasStatsCache(): boolean {
29
+ return existsSync(join(getDataDir(), 'stats-cache.json'));
30
+ }
31
+
32
+ export function hasProjects(): boolean {
33
+ return existsSync(join(getDataDir(), 'projects'));
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { setDataDir } from './config';
5
+ import { interactiveCommand } from './commands/interactive';
6
+ import { overviewCommand } from './commands/overview';
7
+ import { projectsCommand } from './commands/projects';
8
+ import { projectCommand } from './commands/project';
9
+ import { sessionCommand } from './commands/session';
10
+ import { configCommand } from './commands/config';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('claude-usage')
16
+ .description('View Claude Code API usage directly in your terminal')
17
+ .version('0.1.0')
18
+ .option('--data-dir <path>', 'Path to Claude data directory')
19
+ .hook('preAction', (thisCommand) => {
20
+ const opts = thisCommand.opts();
21
+ if (opts.dataDir) {
22
+ setDataDir(opts.dataDir);
23
+ }
24
+ });
25
+
26
+ // Default command (interactive)
27
+ program
28
+ .action(async () => {
29
+ await interactiveCommand();
30
+ });
31
+
32
+ // Overview command (non-interactive, one-shot)
33
+ program
34
+ .command('overview')
35
+ .description('Show usage overview and statistics (non-interactive)')
36
+ .action(() => {
37
+ overviewCommand();
38
+ });
39
+
40
+ // Projects command
41
+ program
42
+ .command('projects')
43
+ .description('List all projects with session counts and costs')
44
+ .action(() => {
45
+ projectsCommand();
46
+ });
47
+
48
+ // Project command
49
+ program
50
+ .command('project <path>')
51
+ .description('Show sessions for a specific project')
52
+ .action((path: string) => {
53
+ projectCommand(path);
54
+ });
55
+
56
+ // Session command
57
+ program
58
+ .command('session <session-id>')
59
+ .description('Show detailed message breakdown for a session')
60
+ .option('--project <path>', 'Project path (encoded or decoded)')
61
+ .action((sessionId: string, options: { project?: string }) => {
62
+ sessionCommand(sessionId, options);
63
+ });
64
+
65
+ // Config command
66
+ program
67
+ .command('config')
68
+ .description('Show current configuration')
69
+ .action(() => {
70
+ configCommand();
71
+ });
72
+
73
+ program.parse();
package/src/loader.ts ADDED
@@ -0,0 +1,360 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import {
4
+ OverviewStats,
5
+ ModelStats,
6
+ DailyActivity,
7
+ DailyModelTokens,
8
+ HistoryEntry,
9
+ SessionMessage,
10
+ SessionSummary,
11
+ AssistantMessage,
12
+ } from './types';
13
+ import { getDataDir } from './config';
14
+ import { estimateCost } from './pricing';
15
+
16
+ export function loadStatsCache(): OverviewStats | null {
17
+ const statsPath = join(getDataDir(), 'stats-cache.json');
18
+
19
+ if (!existsSync(statsPath)) {
20
+ return null;
21
+ }
22
+
23
+ try {
24
+ const raw = JSON.parse(readFileSync(statsPath, 'utf-8'));
25
+
26
+ // Parse daily activity
27
+ const dailyActivity: DailyActivity[] = (raw.dailyActivity || []).map((d: any) => ({
28
+ date: d.date,
29
+ messageCount: d.messageCount || 0,
30
+ sessionCount: d.sessionCount || 0,
31
+ toolCallCount: d.toolCallCount || 0,
32
+ }));
33
+
34
+ // Parse daily model tokens
35
+ const dailyModelTokens: DailyModelTokens[] = (raw.dailyModelTokens || []).map((d: any) => ({
36
+ date: d.date,
37
+ tokensByModel: d.tokensByModel || {},
38
+ }));
39
+
40
+ // Parse model stats
41
+ const modelStats: Record<string, ModelStats> = {};
42
+ let totalInputTokens = 0;
43
+ let totalOutputTokens = 0;
44
+
45
+ for (const [modelId, usage] of Object.entries(raw.modelUsage || {})) {
46
+ const u = usage as any;
47
+ const stats: ModelStats = {
48
+ modelId,
49
+ inputTokens: u.inputTokens || 0,
50
+ outputTokens: u.outputTokens || 0,
51
+ cacheCreationInputTokens: u.cacheCreationInputTokens || 0,
52
+ cacheReadInputTokens: u.cacheReadInputTokens || 0,
53
+ };
54
+ modelStats[modelId] = stats;
55
+ totalInputTokens += stats.inputTokens;
56
+ totalOutputTokens += stats.outputTokens;
57
+ }
58
+
59
+ return {
60
+ totalSessions: raw.totalSessions || 0,
61
+ totalMessages: raw.totalMessages || 0,
62
+ totalInputTokens,
63
+ totalOutputTokens,
64
+ firstSessionDate: raw.firstSessionDate,
65
+ lastComputedDate: raw.lastComputedDate,
66
+ dailyActivity,
67
+ dailyModelTokens,
68
+ modelStats,
69
+ hourCounts: raw.hourCounts || {},
70
+ };
71
+ } catch (error) {
72
+ console.error('Failed to load stats-cache.json:', error);
73
+ return null;
74
+ }
75
+ }
76
+
77
+ export function loadHistory(): Record<string, HistoryEntry> {
78
+ const historyPath = join(getDataDir(), 'history.jsonl');
79
+
80
+ if (!existsSync(historyPath)) {
81
+ return {};
82
+ }
83
+
84
+ const history: Record<string, HistoryEntry> = {};
85
+
86
+ try {
87
+ const lines = readFileSync(historyPath, 'utf-8').split('\n');
88
+
89
+ for (const line of lines) {
90
+ if (!line.trim()) continue;
91
+
92
+ try {
93
+ const entry = JSON.parse(line);
94
+ const sessionId = entry.sessionId;
95
+
96
+ if (!sessionId) continue;
97
+
98
+ const display = (entry.display || '').trim();
99
+
100
+ // Check for /rename command
101
+ if (display.startsWith('/rename')) {
102
+ const renamed = display.substring(7).trim();
103
+ if (renamed) {
104
+ if (sessionId in history) {
105
+ history[sessionId].display = renamed;
106
+ history[sessionId].renamed = true;
107
+ } else {
108
+ history[sessionId] = {
109
+ project: entry.project,
110
+ timestamp: entry.timestamp,
111
+ display: renamed,
112
+ renamed: true,
113
+ };
114
+ }
115
+ }
116
+ } else {
117
+ // Regular entry
118
+ if (!(sessionId in history)) {
119
+ history[sessionId] = {
120
+ project: entry.project,
121
+ timestamp: entry.timestamp,
122
+ display,
123
+ renamed: false,
124
+ };
125
+ }
126
+ }
127
+ } catch (err) {
128
+ // Skip malformed lines
129
+ continue;
130
+ }
131
+ }
132
+ } catch (error) {
133
+ console.error('Failed to read history.jsonl:', error);
134
+ return {};
135
+ }
136
+
137
+ return history;
138
+ }
139
+
140
+ function parseMessageUsage(usage: any): {
141
+ inputTokens: number;
142
+ outputTokens: number;
143
+ cacheCreationInputTokens: number;
144
+ cacheReadInputTokens: number;
145
+ } {
146
+ return {
147
+ inputTokens: usage.input_tokens || 0,
148
+ outputTokens: usage.output_tokens || 0,
149
+ cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
150
+ cacheReadInputTokens: usage.cache_read_input_tokens || 0,
151
+ };
152
+ }
153
+
154
+ export function loadSessionMessages(
155
+ encodedPath: string,
156
+ sessionId: string
157
+ ): SessionMessage[] {
158
+ const sessionPath = join(getDataDir(), 'projects', encodedPath, `${sessionId}.jsonl`);
159
+
160
+ if (!existsSync(sessionPath)) {
161
+ return [];
162
+ }
163
+
164
+ const messages: SessionMessage[] = [];
165
+
166
+ try {
167
+ const lines = readFileSync(sessionPath, 'utf-8').split('\n');
168
+
169
+ for (const line of lines) {
170
+ if (!line.trim()) continue;
171
+
172
+ try {
173
+ const msg: AssistantMessage = JSON.parse(line);
174
+
175
+ // Skip non-assistant messages
176
+ if (msg.type !== 'assistant') continue;
177
+
178
+ // Skip synthetic models
179
+ if (msg.message?.model === '<synthetic>') continue;
180
+
181
+ // Skip sidechain
182
+ if (msg.isSidechain) continue;
183
+
184
+ const usage = msg.message?.usage;
185
+ if (!usage) continue;
186
+
187
+ const parsed = parseMessageUsage(usage);
188
+ const model = msg.message?.model || 'unknown';
189
+ const cost = estimateCost(
190
+ model,
191
+ parsed.inputTokens,
192
+ parsed.outputTokens,
193
+ parsed.cacheCreationInputTokens,
194
+ parsed.cacheReadInputTokens
195
+ );
196
+
197
+ messages.push({
198
+ timestamp: msg.timestamp || '',
199
+ model,
200
+ inputTokens: parsed.inputTokens,
201
+ outputTokens: parsed.outputTokens,
202
+ cacheCreationInputTokens: parsed.cacheCreationInputTokens,
203
+ cacheReadInputTokens: parsed.cacheReadInputTokens,
204
+ costUsd: cost,
205
+ });
206
+ } catch (err) {
207
+ // Skip malformed lines
208
+ continue;
209
+ }
210
+ }
211
+ } catch (error) {
212
+ console.error(`Failed to read session ${sessionId}:`, error);
213
+ return [];
214
+ }
215
+
216
+ return messages;
217
+ }
218
+
219
+ export function buildSessionSummary(
220
+ encodedPath: string,
221
+ sessionId: string,
222
+ history: Record<string, HistoryEntry>
223
+ ): SessionSummary | null {
224
+ const messages = loadSessionMessages(encodedPath, sessionId);
225
+
226
+ if (messages.length === 0) {
227
+ return null;
228
+ }
229
+
230
+ // Compute totals
231
+ let totalInput = messages.reduce((sum, m) => sum + m.inputTokens, 0);
232
+ let totalOutput = messages.reduce((sum, m) => sum + m.outputTokens, 0);
233
+ let totalCost = messages.reduce((sum, m) => sum + m.costUsd, 0);
234
+ const modelsUsed = new Set(messages.map(m => m.model));
235
+
236
+ const firstMsgTimestamp = messages[0]?.timestamp || '';
237
+ const histEntry = history[sessionId] || {};
238
+ const project = histEntry.project || `/${encodedPath}`;
239
+
240
+ let slug = histEntry.display?.trim() || '';
241
+
242
+ // Handle renamed sessions
243
+ if (histEntry.renamed && slug) {
244
+ slug = slug.substring(0, 100);
245
+ } else if (slug) {
246
+ slug = slug.replace(/\n/g, ' ').trim();
247
+ // Skip meta commands
248
+ if (slug.startsWith('/') || slug.startsWith('!') || ['yes', 'Sure', 'go ahead'].includes(slug)) {
249
+ slug = '';
250
+ } else {
251
+ slug = slug.substring(0, 80);
252
+ }
253
+ }
254
+
255
+ if (!slug) {
256
+ slug = sessionId.substring(0, 8);
257
+ }
258
+
259
+ // Merge subagent tokens
260
+ const subagentDir = join(getDataDir(), 'projects', encodedPath, sessionId, 'subagents');
261
+ if (existsSync(subagentDir)) {
262
+ try {
263
+ const agentFiles = readdirSync(subagentDir).filter(f => f.startsWith('agent-') && f.endsWith('.jsonl'));
264
+
265
+ for (const agentFile of agentFiles) {
266
+ const agentPath = join(subagentDir, agentFile);
267
+ const lines = readFileSync(agentPath, 'utf-8').split('\n');
268
+
269
+ for (const line of lines) {
270
+ if (!line.trim()) continue;
271
+
272
+ try {
273
+ const msg: AssistantMessage = JSON.parse(line);
274
+ if (msg.type !== 'assistant') continue;
275
+ if (msg.message?.model === '<synthetic>') continue;
276
+
277
+ const usage = msg.message?.usage;
278
+ if (!usage) continue;
279
+
280
+ const parsed = parseMessageUsage(usage);
281
+ const model = msg.message?.model || 'unknown';
282
+ const cost = estimateCost(
283
+ model,
284
+ parsed.inputTokens,
285
+ parsed.outputTokens,
286
+ parsed.cacheCreationInputTokens,
287
+ parsed.cacheReadInputTokens
288
+ );
289
+
290
+ totalInput += parsed.inputTokens;
291
+ totalOutput += parsed.outputTokens;
292
+ totalCost += cost;
293
+ modelsUsed.add(model);
294
+ } catch (err) {
295
+ continue;
296
+ }
297
+ }
298
+ }
299
+ } catch (error) {
300
+ // Ignore subagent errors
301
+ }
302
+ }
303
+
304
+ return {
305
+ sessionId,
306
+ slug,
307
+ project,
308
+ timestamp: firstMsgTimestamp,
309
+ messageCount: messages.length,
310
+ totalInputTokens: totalInput,
311
+ totalOutputTokens: totalOutput,
312
+ totalCostUsd: totalCost,
313
+ modelsUsed,
314
+ };
315
+ }
316
+
317
+ export function buildProjectSummaries(): Record<string, SessionSummary[]> {
318
+ const projectsDir = join(getDataDir(), 'projects');
319
+
320
+ if (!existsSync(projectsDir)) {
321
+ return {};
322
+ }
323
+
324
+ const history = loadHistory();
325
+ const projects: Record<string, SessionSummary[]> = {};
326
+
327
+ try {
328
+ const encodedDirs = readdirSync(projectsDir);
329
+
330
+ for (const encodedDir of encodedDirs) {
331
+ const dirPath = join(projectsDir, encodedDir);
332
+ const stat = require('fs').statSync(dirPath);
333
+
334
+ if (!stat.isDirectory()) continue;
335
+
336
+ const sessions: SessionSummary[] = [];
337
+ const files = readdirSync(dirPath);
338
+
339
+ for (const file of files) {
340
+ if (!file.endsWith('.jsonl')) continue;
341
+
342
+ const sessionId = file.replace('.jsonl', '');
343
+ const summary = buildSessionSummary(encodedDir, sessionId, history);
344
+
345
+ if (summary) {
346
+ sessions.push(summary);
347
+ }
348
+ }
349
+
350
+ if (sessions.length > 0) {
351
+ projects[encodedDir] = sessions;
352
+ }
353
+ }
354
+ } catch (error) {
355
+ console.error('Failed to build project summaries:', error);
356
+ return {};
357
+ }
358
+
359
+ return projects;
360
+ }
package/src/pricing.ts ADDED
@@ -0,0 +1,68 @@
1
+ interface ModelPricing {
2
+ input: number;
3
+ output: number;
4
+ cacheWrite: number;
5
+ cacheRead: number;
6
+ }
7
+
8
+ // Pricing in $/MTok (million tokens)
9
+ const FALLBACK_PRICING: Record<string, ModelPricing> = {
10
+ 'claude-opus-4-6': {
11
+ input: 15.0,
12
+ output: 75.0,
13
+ cacheWrite: 18.75,
14
+ cacheRead: 1.5,
15
+ },
16
+ 'claude-opus-4-5-20251101': {
17
+ input: 15.0,
18
+ output: 75.0,
19
+ cacheWrite: 18.75,
20
+ cacheRead: 1.5,
21
+ },
22
+ 'claude-sonnet-4-5-20250929': {
23
+ input: 3.0,
24
+ output: 15.0,
25
+ cacheWrite: 3.75,
26
+ cacheRead: 0.3,
27
+ },
28
+ 'claude-haiku-4-5-20251001': {
29
+ input: 0.8,
30
+ output: 4.0,
31
+ cacheWrite: 1.0,
32
+ cacheRead: 0.08,
33
+ },
34
+ };
35
+
36
+ const DEFAULT_PRICING = FALLBACK_PRICING['claude-sonnet-4-5-20250929'];
37
+
38
+ export function getModelPricing(modelId: string): ModelPricing {
39
+ if (modelId in FALLBACK_PRICING) {
40
+ return FALLBACK_PRICING[modelId];
41
+ }
42
+ return DEFAULT_PRICING;
43
+ }
44
+
45
+ export function estimateCost(
46
+ modelId: string,
47
+ inputTokens: number = 0,
48
+ outputTokens: number = 0,
49
+ cacheCreationInputTokens: number = 0,
50
+ cacheReadInputTokens: number = 0
51
+ ): number {
52
+ const pricing = getModelPricing(modelId);
53
+ const cost =
54
+ (inputTokens * pricing.input +
55
+ outputTokens * pricing.output +
56
+ cacheCreationInputTokens * pricing.cacheWrite +
57
+ cacheReadInputTokens * pricing.cacheRead) /
58
+ 1_000_000;
59
+ return cost;
60
+ }
61
+
62
+ export function formatCost(cost: number): string {
63
+ return `$${cost.toFixed(4)}`;
64
+ }
65
+
66
+ export function formatTokens(tokens: number): string {
67
+ return tokens.toLocaleString('en-US');
68
+ }
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
1
+ export interface ModelStats {
2
+ modelId: string;
3
+ inputTokens: number;
4
+ outputTokens: number;
5
+ cacheCreationInputTokens: number;
6
+ cacheReadInputTokens: number;
7
+ }
8
+
9
+ export interface DailyActivity {
10
+ date: string;
11
+ messageCount: number;
12
+ sessionCount: number;
13
+ toolCallCount: number;
14
+ }
15
+
16
+ export interface DailyModelTokens {
17
+ date: string;
18
+ tokensByModel: Record<string, number>;
19
+ }
20
+
21
+ export interface OverviewStats {
22
+ totalSessions: number;
23
+ totalMessages: number;
24
+ totalInputTokens: number;
25
+ totalOutputTokens: number;
26
+ firstSessionDate?: string;
27
+ lastComputedDate?: string;
28
+ dailyActivity: DailyActivity[];
29
+ dailyModelTokens: DailyModelTokens[];
30
+ modelStats: Record<string, ModelStats>;
31
+ hourCounts: Record<string, number>;
32
+ }
33
+
34
+ export interface SessionMessage {
35
+ timestamp: string;
36
+ model: string;
37
+ inputTokens: number;
38
+ outputTokens: number;
39
+ cacheCreationInputTokens: number;
40
+ cacheReadInputTokens: number;
41
+ costUsd: number;
42
+ }
43
+
44
+ export interface SessionSummary {
45
+ sessionId: string;
46
+ slug: string;
47
+ project: string;
48
+ timestamp: string;
49
+ messageCount: number;
50
+ totalInputTokens: number;
51
+ totalOutputTokens: number;
52
+ totalCostUsd: number;
53
+ modelsUsed: Set<string>;
54
+ }
55
+
56
+ export interface HistoryEntry {
57
+ project?: string;
58
+ timestamp?: string;
59
+ display: string;
60
+ renamed: boolean;
61
+ }
62
+
63
+ export interface MessageUsage {
64
+ input_tokens: number;
65
+ output_tokens: number;
66
+ cache_creation_input_tokens: number;
67
+ cache_read_input_tokens: number;
68
+ }
69
+
70
+ export interface AssistantMessage {
71
+ type: string;
72
+ timestamp?: string;
73
+ isSidechain?: boolean;
74
+ message?: {
75
+ model?: string;
76
+ usage?: MessageUsage;
77
+ };
78
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "moduleResolution": "node"
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }