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/README.md +239 -0
- package/dist/commands/config.js +17 -0
- package/dist/commands/interactive.js +217 -0
- package/dist/commands/overview.js +98 -0
- package/dist/commands/project.js +78 -0
- package/dist/commands/projects.js +66 -0
- package/dist/commands/session.js +98 -0
- package/dist/config.js +33 -0
- package/dist/index.js +65 -0
- package/dist/loader.js +299 -0
- package/dist/pricing.js +55 -0
- package/dist/types.js +2 -0
- package/package.json +48 -0
- package/src/commands/config.ts +16 -0
- package/src/commands/interactive.ts +271 -0
- package/src/commands/overview.ts +126 -0
- package/src/commands/project.ts +85 -0
- package/src/commands/projects.ts +83 -0
- package/src/commands/session.ts +105 -0
- package/src/config.ts +34 -0
- package/src/index.ts +73 -0
- package/src/loader.ts +360 -0
- package/src/pricing.ts +68 -0
- package/src/types.ts +78 -0
- package/tsconfig.json +17 -0
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
|
+
}
|