vibestats 1.0.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 +80 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1833 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1833 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { defineCommand, runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/usage/loader.ts
|
|
7
|
+
import { readFileSync, existsSync, readdirSync, statSync, realpathSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
|
|
11
|
+
// src/pricing.ts
|
|
12
|
+
var MODEL_PRICING = {
|
|
13
|
+
// Opus 4.5 (cheaper than Opus 4.1)
|
|
14
|
+
"claude-opus-4-5-20251101": {
|
|
15
|
+
input: 5,
|
|
16
|
+
output: 25,
|
|
17
|
+
cacheWrite: 6.25,
|
|
18
|
+
cacheRead: 0.5
|
|
19
|
+
},
|
|
20
|
+
// Sonnet 4.5
|
|
21
|
+
"claude-sonnet-4-5-20250929": {
|
|
22
|
+
input: 3,
|
|
23
|
+
output: 15,
|
|
24
|
+
cacheWrite: 3.75,
|
|
25
|
+
cacheRead: 0.3
|
|
26
|
+
},
|
|
27
|
+
// Opus 4.1 (MORE expensive than Opus 4.5)
|
|
28
|
+
"claude-opus-4-1-20250805": {
|
|
29
|
+
input: 15,
|
|
30
|
+
output: 75,
|
|
31
|
+
cacheWrite: 18.75,
|
|
32
|
+
cacheRead: 1.5
|
|
33
|
+
},
|
|
34
|
+
// Haiku 4.5
|
|
35
|
+
"claude-haiku-4-5-20251001": {
|
|
36
|
+
input: 1,
|
|
37
|
+
output: 5,
|
|
38
|
+
cacheWrite: 1.25,
|
|
39
|
+
cacheRead: 0.1
|
|
40
|
+
},
|
|
41
|
+
// Sonnet 3.5 (legacy - same as Sonnet 4.5)
|
|
42
|
+
"claude-3-5-sonnet-20241022": {
|
|
43
|
+
input: 3,
|
|
44
|
+
output: 15,
|
|
45
|
+
cacheWrite: 3.75,
|
|
46
|
+
cacheRead: 0.3
|
|
47
|
+
},
|
|
48
|
+
"claude-3-5-sonnet-20240620": {
|
|
49
|
+
input: 3,
|
|
50
|
+
output: 15,
|
|
51
|
+
cacheWrite: 3.75,
|
|
52
|
+
cacheRead: 0.3
|
|
53
|
+
},
|
|
54
|
+
// Haiku 3.5 (legacy - same as Haiku 4.5)
|
|
55
|
+
"claude-3-5-haiku-20241022": {
|
|
56
|
+
input: 1,
|
|
57
|
+
output: 5,
|
|
58
|
+
cacheWrite: 1.25,
|
|
59
|
+
cacheRead: 0.1
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
function getModelPricing(modelName) {
|
|
63
|
+
if (MODEL_PRICING[modelName]) {
|
|
64
|
+
return MODEL_PRICING[modelName];
|
|
65
|
+
}
|
|
66
|
+
if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) {
|
|
67
|
+
return MODEL_PRICING["claude-opus-4-5-20251101"];
|
|
68
|
+
}
|
|
69
|
+
if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1") || modelName.includes("opus-4")) {
|
|
70
|
+
return MODEL_PRICING["claude-opus-4-1-20250805"];
|
|
71
|
+
}
|
|
72
|
+
if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) {
|
|
73
|
+
return MODEL_PRICING["claude-sonnet-4-5-20250929"];
|
|
74
|
+
}
|
|
75
|
+
if (modelName.includes("sonnet")) {
|
|
76
|
+
return MODEL_PRICING["claude-3-5-sonnet-20241022"];
|
|
77
|
+
}
|
|
78
|
+
if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) {
|
|
79
|
+
return MODEL_PRICING["claude-haiku-4-5-20251001"];
|
|
80
|
+
}
|
|
81
|
+
if (modelName.includes("haiku")) {
|
|
82
|
+
return MODEL_PRICING["claude-3-5-haiku-20241022"];
|
|
83
|
+
}
|
|
84
|
+
return MODEL_PRICING["claude-sonnet-4-5-20250929"];
|
|
85
|
+
}
|
|
86
|
+
function getModelDisplayName(modelName) {
|
|
87
|
+
if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
|
|
88
|
+
if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
|
|
89
|
+
if (modelName.includes("opus")) return "Opus";
|
|
90
|
+
if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) return "Sonnet 4.5";
|
|
91
|
+
if (modelName.includes("sonnet-3-5") || modelName.includes("sonnet-3.5")) return "Sonnet 3.5";
|
|
92
|
+
if (modelName.includes("sonnet")) return "Sonnet";
|
|
93
|
+
if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) return "Haiku 4.5";
|
|
94
|
+
if (modelName.includes("haiku-3-5") || modelName.includes("haiku-3.5")) return "Haiku 3.5";
|
|
95
|
+
if (modelName.includes("haiku")) return "Haiku";
|
|
96
|
+
return modelName;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/codex-pricing.ts
|
|
100
|
+
var CODEX_MODEL_PRICING = {
|
|
101
|
+
// GPT-5.2 (latest flagship)
|
|
102
|
+
// Cached input is ~10% of input price (prompt caching discount)
|
|
103
|
+
"gpt-5.2": {
|
|
104
|
+
input: 1.75,
|
|
105
|
+
output: 14,
|
|
106
|
+
cachedInput: 0.175
|
|
107
|
+
},
|
|
108
|
+
"gpt-5.2-codex": {
|
|
109
|
+
input: 1.75,
|
|
110
|
+
output: 14,
|
|
111
|
+
cachedInput: 0.175
|
|
112
|
+
},
|
|
113
|
+
// GPT-5.1 models
|
|
114
|
+
"gpt-5.1": {
|
|
115
|
+
input: 1.25,
|
|
116
|
+
output: 10,
|
|
117
|
+
cachedInput: 0.125
|
|
118
|
+
},
|
|
119
|
+
"gpt-5.1-codex": {
|
|
120
|
+
input: 1.25,
|
|
121
|
+
output: 10,
|
|
122
|
+
cachedInput: 0.125
|
|
123
|
+
},
|
|
124
|
+
"gpt-5.1-codex-max": {
|
|
125
|
+
input: 1.25,
|
|
126
|
+
output: 10,
|
|
127
|
+
cachedInput: 0.125
|
|
128
|
+
},
|
|
129
|
+
// GPT-5.1 Mini
|
|
130
|
+
"gpt-5.1-codex-mini": {
|
|
131
|
+
input: 0.25,
|
|
132
|
+
output: 2,
|
|
133
|
+
cachedInput: 0.025
|
|
134
|
+
},
|
|
135
|
+
// GPT-5 base models
|
|
136
|
+
"gpt-5": {
|
|
137
|
+
input: 1.25,
|
|
138
|
+
output: 10,
|
|
139
|
+
cachedInput: 0.125
|
|
140
|
+
},
|
|
141
|
+
"gpt-5-codex": {
|
|
142
|
+
input: 1.25,
|
|
143
|
+
output: 10,
|
|
144
|
+
cachedInput: 0.125
|
|
145
|
+
},
|
|
146
|
+
// GPT-5 Mini variants
|
|
147
|
+
"gpt-5-mini": {
|
|
148
|
+
input: 0.25,
|
|
149
|
+
output: 2,
|
|
150
|
+
cachedInput: 0.025
|
|
151
|
+
},
|
|
152
|
+
"gpt-5-codex-mini": {
|
|
153
|
+
input: 0.25,
|
|
154
|
+
output: 2,
|
|
155
|
+
cachedInput: 0.025
|
|
156
|
+
},
|
|
157
|
+
// GPT-5 Nano
|
|
158
|
+
"gpt-5-nano": {
|
|
159
|
+
input: 0.05,
|
|
160
|
+
output: 0.4,
|
|
161
|
+
cachedInput: 5e-3
|
|
162
|
+
},
|
|
163
|
+
// GPT-4o (fallback for older sessions)
|
|
164
|
+
"gpt-4o": {
|
|
165
|
+
input: 2.5,
|
|
166
|
+
output: 10,
|
|
167
|
+
cachedInput: 1.25
|
|
168
|
+
},
|
|
169
|
+
"gpt-4o-mini": {
|
|
170
|
+
input: 0.15,
|
|
171
|
+
output: 0.6,
|
|
172
|
+
cachedInput: 0.075
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function getCodexModelPricing(modelName) {
|
|
176
|
+
if (CODEX_MODEL_PRICING[modelName]) {
|
|
177
|
+
return CODEX_MODEL_PRICING[modelName];
|
|
178
|
+
}
|
|
179
|
+
const normalized = modelName.toLowerCase();
|
|
180
|
+
if (normalized.includes("5.2")) {
|
|
181
|
+
return CODEX_MODEL_PRICING["gpt-5.2"];
|
|
182
|
+
}
|
|
183
|
+
if (normalized.includes("5.1") && normalized.includes("max")) {
|
|
184
|
+
return CODEX_MODEL_PRICING["gpt-5.1-codex-max"];
|
|
185
|
+
}
|
|
186
|
+
if (normalized.includes("5.1") && normalized.includes("mini")) {
|
|
187
|
+
return CODEX_MODEL_PRICING["gpt-5.1-codex-mini"];
|
|
188
|
+
}
|
|
189
|
+
if (normalized.includes("5.1")) {
|
|
190
|
+
return CODEX_MODEL_PRICING["gpt-5.1"];
|
|
191
|
+
}
|
|
192
|
+
if (normalized.includes("nano")) {
|
|
193
|
+
return CODEX_MODEL_PRICING["gpt-5-nano"];
|
|
194
|
+
}
|
|
195
|
+
if (normalized.includes("5") && normalized.includes("mini")) {
|
|
196
|
+
return CODEX_MODEL_PRICING["gpt-5-mini"];
|
|
197
|
+
}
|
|
198
|
+
if (normalized.includes("gpt-5") || normalized.includes("gpt5")) {
|
|
199
|
+
return CODEX_MODEL_PRICING["gpt-5"];
|
|
200
|
+
}
|
|
201
|
+
if (normalized.includes("4o-mini")) {
|
|
202
|
+
return CODEX_MODEL_PRICING["gpt-4o-mini"];
|
|
203
|
+
}
|
|
204
|
+
if (normalized.includes("4o")) {
|
|
205
|
+
return CODEX_MODEL_PRICING["gpt-4o"];
|
|
206
|
+
}
|
|
207
|
+
return CODEX_MODEL_PRICING["gpt-5"];
|
|
208
|
+
}
|
|
209
|
+
function getCodexModelDisplayName(modelName) {
|
|
210
|
+
const normalized = modelName.toLowerCase();
|
|
211
|
+
if (normalized.includes("5.2") && normalized.includes("pro")) return "GPT-5.2 Pro";
|
|
212
|
+
if (normalized.includes("5.2")) return "GPT-5.2";
|
|
213
|
+
if (normalized.includes("5.1") && normalized.includes("max")) return "GPT-5.1 Max";
|
|
214
|
+
if (normalized.includes("5.1") && normalized.includes("mini")) return "GPT-5.1 Mini";
|
|
215
|
+
if (normalized.includes("5.1") && normalized.includes("codex")) return "GPT-5.1 Codex";
|
|
216
|
+
if (normalized.includes("5.1")) return "GPT-5.1";
|
|
217
|
+
if (normalized.includes("nano")) return "GPT-5 Nano";
|
|
218
|
+
if (normalized.includes("5") && normalized.includes("mini")) return "GPT-5 Mini";
|
|
219
|
+
if (normalized.includes("5") && normalized.includes("codex")) return "GPT-5 Codex";
|
|
220
|
+
if (normalized.includes("gpt-5")) return "GPT-5";
|
|
221
|
+
if (normalized.includes("4o-mini")) return "GPT-4o Mini";
|
|
222
|
+
if (normalized.includes("4o")) return "GPT-4o";
|
|
223
|
+
return modelName;
|
|
224
|
+
}
|
|
225
|
+
function calculateCodexCost(modelName, inputTokens, outputTokens, cachedInputTokens) {
|
|
226
|
+
const pricing = getCodexModelPricing(modelName);
|
|
227
|
+
const regularInputTokens = inputTokens - cachedInputTokens;
|
|
228
|
+
const inputCost = regularInputTokens / 1e6 * pricing.input;
|
|
229
|
+
const cachedCost = cachedInputTokens / 1e6 * pricing.cachedInput;
|
|
230
|
+
const outputCost = outputTokens / 1e6 * pricing.output;
|
|
231
|
+
return inputCost + cachedCost + outputCost;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/usage/loader.ts
|
|
235
|
+
function getClaudeDir() {
|
|
236
|
+
return process.env.CLAUDE_HOME || join(homedir(), ".claude");
|
|
237
|
+
}
|
|
238
|
+
function getCodexDir() {
|
|
239
|
+
return join(homedir(), ".codex");
|
|
240
|
+
}
|
|
241
|
+
function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth = 0, result = []) {
|
|
242
|
+
if (!existsSync(dir)) return result;
|
|
243
|
+
if (depth > 10) return result;
|
|
244
|
+
let realPath;
|
|
245
|
+
try {
|
|
246
|
+
realPath = realpathSync(dir);
|
|
247
|
+
} catch {
|
|
248
|
+
realPath = dir;
|
|
249
|
+
}
|
|
250
|
+
if (visited.has(realPath)) return result;
|
|
251
|
+
visited.add(realPath);
|
|
252
|
+
try {
|
|
253
|
+
const entries = readdirSync(dir);
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
const fullPath = join(dir, entry);
|
|
256
|
+
try {
|
|
257
|
+
const stat = statSync(fullPath);
|
|
258
|
+
if (stat.isDirectory()) {
|
|
259
|
+
findJsonlFiles(fullPath, visited, depth + 1, result);
|
|
260
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
261
|
+
result.push(fullPath);
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
function parseClaudeJsonl() {
|
|
271
|
+
const entries = [];
|
|
272
|
+
const claudeDir = getClaudeDir();
|
|
273
|
+
const projectsDir = join(claudeDir, "projects");
|
|
274
|
+
if (!existsSync(projectsDir)) return entries;
|
|
275
|
+
const jsonlFiles = findJsonlFiles(projectsDir);
|
|
276
|
+
for (const filePath of jsonlFiles) {
|
|
277
|
+
try {
|
|
278
|
+
const content = readFileSync(filePath, "utf-8");
|
|
279
|
+
const lines = content.split("\n");
|
|
280
|
+
for (const line of lines) {
|
|
281
|
+
if (!line.trim()) continue;
|
|
282
|
+
try {
|
|
283
|
+
const entry = JSON.parse(line);
|
|
284
|
+
if (entry.type !== "assistant" || !entry.message?.usage) continue;
|
|
285
|
+
const usage = entry.message.usage;
|
|
286
|
+
const model = entry.message.model || "unknown";
|
|
287
|
+
const timestamp = entry.timestamp;
|
|
288
|
+
if (!timestamp) continue;
|
|
289
|
+
const date = timestamp.split("T")[0];
|
|
290
|
+
const pricing = getModelPricing(model);
|
|
291
|
+
const inputTokens = usage.input_tokens || 0;
|
|
292
|
+
const outputTokens = usage.output_tokens || 0;
|
|
293
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
|
|
294
|
+
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
295
|
+
const cost = inputTokens * pricing.input / 1e6 + outputTokens * pricing.output / 1e6 + cacheWriteTokens * pricing.cacheWrite / 1e6 + cacheReadTokens * pricing.cacheRead / 1e6;
|
|
296
|
+
entries.push({
|
|
297
|
+
date,
|
|
298
|
+
model: getModelDisplayName(model),
|
|
299
|
+
inputTokens,
|
|
300
|
+
outputTokens,
|
|
301
|
+
cacheWriteTokens,
|
|
302
|
+
cacheReadTokens,
|
|
303
|
+
cost,
|
|
304
|
+
source: "claude"
|
|
305
|
+
});
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return entries;
|
|
313
|
+
}
|
|
314
|
+
function parseCodexJsonl() {
|
|
315
|
+
const entries = [];
|
|
316
|
+
const codexDir = getCodexDir();
|
|
317
|
+
const sessionsDir = join(codexDir, "sessions");
|
|
318
|
+
if (!existsSync(sessionsDir)) return entries;
|
|
319
|
+
const jsonlFiles = findJsonlFiles(sessionsDir);
|
|
320
|
+
for (const filePath of jsonlFiles) {
|
|
321
|
+
try {
|
|
322
|
+
const content = readFileSync(filePath, "utf-8");
|
|
323
|
+
const lines = content.split("\n");
|
|
324
|
+
for (const line of lines) {
|
|
325
|
+
if (!line.trim()) continue;
|
|
326
|
+
try {
|
|
327
|
+
const entry = JSON.parse(line);
|
|
328
|
+
if (entry.type !== "response" || !entry.response?.usage) continue;
|
|
329
|
+
const usage = entry.response.usage;
|
|
330
|
+
const model = entry.response.model || "unknown";
|
|
331
|
+
const timestamp = entry.timestamp;
|
|
332
|
+
if (!timestamp) continue;
|
|
333
|
+
const date = timestamp.split("T")[0];
|
|
334
|
+
const pricing = getCodexModelPricing(model);
|
|
335
|
+
const inputTokens = usage.input_tokens || 0;
|
|
336
|
+
const outputTokens = usage.output_tokens || 0;
|
|
337
|
+
const cachedInputTokens = usage.input_tokens_details?.cached_tokens || 0;
|
|
338
|
+
const cost = (inputTokens - cachedInputTokens) * pricing.input / 1e6 + outputTokens * pricing.output / 1e6 + cachedInputTokens * pricing.cachedInput / 1e6;
|
|
339
|
+
entries.push({
|
|
340
|
+
date,
|
|
341
|
+
model: getCodexModelDisplayName(model),
|
|
342
|
+
inputTokens,
|
|
343
|
+
outputTokens,
|
|
344
|
+
cacheWriteTokens: 0,
|
|
345
|
+
cacheReadTokens: cachedInputTokens,
|
|
346
|
+
cost,
|
|
347
|
+
source: "codex"
|
|
348
|
+
});
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return entries;
|
|
356
|
+
}
|
|
357
|
+
function filterByDateRange(entries, since, until) {
|
|
358
|
+
return entries.filter((e) => {
|
|
359
|
+
if (since && e.date < since) return false;
|
|
360
|
+
if (until && e.date > until) return false;
|
|
361
|
+
return true;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function aggregateByDay(entries) {
|
|
365
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
366
|
+
for (const e of entries) {
|
|
367
|
+
const existing = dayMap.get(e.date);
|
|
368
|
+
if (existing) {
|
|
369
|
+
existing.inputTokens += e.inputTokens;
|
|
370
|
+
existing.outputTokens += e.outputTokens;
|
|
371
|
+
existing.cacheWriteTokens += e.cacheWriteTokens;
|
|
372
|
+
existing.cacheReadTokens += e.cacheReadTokens;
|
|
373
|
+
existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
374
|
+
existing.cost += e.cost;
|
|
375
|
+
existing.modelsSet.add(e.model);
|
|
376
|
+
} else {
|
|
377
|
+
dayMap.set(e.date, {
|
|
378
|
+
key: e.date,
|
|
379
|
+
inputTokens: e.inputTokens,
|
|
380
|
+
outputTokens: e.outputTokens,
|
|
381
|
+
cacheWriteTokens: e.cacheWriteTokens,
|
|
382
|
+
cacheReadTokens: e.cacheReadTokens,
|
|
383
|
+
totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
|
|
384
|
+
cost: e.cost,
|
|
385
|
+
modelsSet: /* @__PURE__ */ new Set([e.model])
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return Array.from(dayMap.values()).map(({ modelsSet, ...row }) => ({
|
|
390
|
+
...row,
|
|
391
|
+
models: Array.from(modelsSet).sort()
|
|
392
|
+
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
393
|
+
}
|
|
394
|
+
function aggregateByMonth(entries) {
|
|
395
|
+
const monthMap = /* @__PURE__ */ new Map();
|
|
396
|
+
for (const e of entries) {
|
|
397
|
+
const month = e.date.slice(0, 7);
|
|
398
|
+
const existing = monthMap.get(month);
|
|
399
|
+
if (existing) {
|
|
400
|
+
existing.inputTokens += e.inputTokens;
|
|
401
|
+
existing.outputTokens += e.outputTokens;
|
|
402
|
+
existing.cacheWriteTokens += e.cacheWriteTokens;
|
|
403
|
+
existing.cacheReadTokens += e.cacheReadTokens;
|
|
404
|
+
existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
405
|
+
existing.cost += e.cost;
|
|
406
|
+
existing.modelsSet.add(e.model);
|
|
407
|
+
} else {
|
|
408
|
+
monthMap.set(month, {
|
|
409
|
+
key: month,
|
|
410
|
+
inputTokens: e.inputTokens,
|
|
411
|
+
outputTokens: e.outputTokens,
|
|
412
|
+
cacheWriteTokens: e.cacheWriteTokens,
|
|
413
|
+
cacheReadTokens: e.cacheReadTokens,
|
|
414
|
+
totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
|
|
415
|
+
cost: e.cost,
|
|
416
|
+
modelsSet: /* @__PURE__ */ new Set([e.model])
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return Array.from(monthMap.values()).map(({ modelsSet, ...row }) => ({
|
|
421
|
+
...row,
|
|
422
|
+
models: Array.from(modelsSet).sort()
|
|
423
|
+
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
424
|
+
}
|
|
425
|
+
function aggregateByModel(entries) {
|
|
426
|
+
const modelMap = /* @__PURE__ */ new Map();
|
|
427
|
+
for (const e of entries) {
|
|
428
|
+
const existing = modelMap.get(e.model);
|
|
429
|
+
if (existing) {
|
|
430
|
+
existing.inputTokens += e.inputTokens;
|
|
431
|
+
existing.outputTokens += e.outputTokens;
|
|
432
|
+
existing.cacheWriteTokens += e.cacheWriteTokens;
|
|
433
|
+
existing.cacheReadTokens += e.cacheReadTokens;
|
|
434
|
+
existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
435
|
+
existing.cost += e.cost;
|
|
436
|
+
} else {
|
|
437
|
+
modelMap.set(e.model, {
|
|
438
|
+
key: e.model,
|
|
439
|
+
inputTokens: e.inputTokens,
|
|
440
|
+
outputTokens: e.outputTokens,
|
|
441
|
+
cacheWriteTokens: e.cacheWriteTokens,
|
|
442
|
+
cacheReadTokens: e.cacheReadTokens,
|
|
443
|
+
totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
|
|
444
|
+
cost: e.cost
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return Array.from(modelMap.values()).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
449
|
+
}
|
|
450
|
+
function computeTotals(rows) {
|
|
451
|
+
return rows.reduce(
|
|
452
|
+
(acc, row) => ({
|
|
453
|
+
inputTokens: acc.inputTokens + row.inputTokens,
|
|
454
|
+
outputTokens: acc.outputTokens + row.outputTokens,
|
|
455
|
+
cacheWriteTokens: acc.cacheWriteTokens + row.cacheWriteTokens,
|
|
456
|
+
cacheReadTokens: acc.cacheReadTokens + row.cacheReadTokens,
|
|
457
|
+
totalTokens: acc.totalTokens + row.totalTokens,
|
|
458
|
+
cost: acc.cost + row.cost
|
|
459
|
+
}),
|
|
460
|
+
{ inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalTokens: 0, cost: 0 }
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function computeModelBreakdown(entries) {
|
|
464
|
+
const modelMap = /* @__PURE__ */ new Map();
|
|
465
|
+
for (const e of entries) {
|
|
466
|
+
const total = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
467
|
+
const existing = modelMap.get(e.model);
|
|
468
|
+
if (existing) {
|
|
469
|
+
existing.tokens += total;
|
|
470
|
+
existing.cost += e.cost;
|
|
471
|
+
} else {
|
|
472
|
+
modelMap.set(e.model, { tokens: total, cost: e.cost });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const totalTokens = Array.from(modelMap.values()).reduce((sum, m) => sum + m.tokens, 0);
|
|
476
|
+
return Array.from(modelMap.entries()).map(([model, data]) => ({
|
|
477
|
+
model,
|
|
478
|
+
tokens: data.tokens,
|
|
479
|
+
cost: data.cost,
|
|
480
|
+
percentage: totalTokens > 0 ? Math.round(data.tokens / totalTokens * 100) : 0
|
|
481
|
+
})).sort((a, b) => b.tokens - a.tokens);
|
|
482
|
+
}
|
|
483
|
+
function loadUsageStats(options) {
|
|
484
|
+
const { aggregation, since, until, codexOnly, combined } = options;
|
|
485
|
+
let entries = [];
|
|
486
|
+
if (!codexOnly) {
|
|
487
|
+
entries = entries.concat(parseClaudeJsonl());
|
|
488
|
+
}
|
|
489
|
+
if (codexOnly || combined) {
|
|
490
|
+
entries = entries.concat(parseCodexJsonl());
|
|
491
|
+
}
|
|
492
|
+
if (entries.length === 0) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
entries = filterByDateRange(entries, since, until);
|
|
496
|
+
if (entries.length === 0) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const dates = entries.map((e) => e.date).sort();
|
|
500
|
+
const dateRange = {
|
|
501
|
+
start: dates[0],
|
|
502
|
+
end: dates[dates.length - 1]
|
|
503
|
+
};
|
|
504
|
+
let rows;
|
|
505
|
+
switch (aggregation) {
|
|
506
|
+
case "monthly":
|
|
507
|
+
rows = aggregateByMonth(entries);
|
|
508
|
+
break;
|
|
509
|
+
case "model":
|
|
510
|
+
rows = aggregateByModel(entries);
|
|
511
|
+
break;
|
|
512
|
+
case "total":
|
|
513
|
+
rows = [];
|
|
514
|
+
break;
|
|
515
|
+
case "daily":
|
|
516
|
+
default:
|
|
517
|
+
rows = aggregateByDay(entries);
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
const totals = aggregation === "total" ? computeTotals(aggregateByDay(entries)) : computeTotals(rows);
|
|
521
|
+
const modelBreakdown = computeModelBreakdown(entries);
|
|
522
|
+
let source = "claude";
|
|
523
|
+
if (codexOnly) source = "codex";
|
|
524
|
+
else if (combined) source = "combined";
|
|
525
|
+
return {
|
|
526
|
+
rows,
|
|
527
|
+
totals,
|
|
528
|
+
source,
|
|
529
|
+
aggregation,
|
|
530
|
+
dateRange,
|
|
531
|
+
modelBreakdown
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/usage/table.ts
|
|
536
|
+
var colors = {
|
|
537
|
+
reset: "\x1B[0m",
|
|
538
|
+
bold: "\x1B[1m",
|
|
539
|
+
dim: "\x1B[2m",
|
|
540
|
+
orange: "\x1B[38;5;208m",
|
|
541
|
+
amber: "\x1B[38;5;214m",
|
|
542
|
+
yellow: "\x1B[33m",
|
|
543
|
+
green: "\x1B[32m",
|
|
544
|
+
cyan: "\x1B[36m",
|
|
545
|
+
white: "\x1B[37m",
|
|
546
|
+
gray: "\x1B[90m"
|
|
547
|
+
};
|
|
548
|
+
var noColors = Object.fromEntries(
|
|
549
|
+
Object.keys(colors).map((k) => [k, ""])
|
|
550
|
+
);
|
|
551
|
+
function getColors(showColors) {
|
|
552
|
+
if (showColors === false) {
|
|
553
|
+
return noColors;
|
|
554
|
+
}
|
|
555
|
+
return colors;
|
|
556
|
+
}
|
|
557
|
+
function formatNumber(n) {
|
|
558
|
+
return n.toLocaleString("en-US");
|
|
559
|
+
}
|
|
560
|
+
function formatCost(n) {
|
|
561
|
+
if (n >= 1e3) return `$${(n / 1e3).toFixed(1)}K`;
|
|
562
|
+
if (n >= 1) return `$${n.toFixed(2)}`;
|
|
563
|
+
return `$${n.toFixed(4)}`;
|
|
564
|
+
}
|
|
565
|
+
function padLeft(str, width) {
|
|
566
|
+
return str.padStart(width);
|
|
567
|
+
}
|
|
568
|
+
function padRight(str, width) {
|
|
569
|
+
return str.padEnd(width);
|
|
570
|
+
}
|
|
571
|
+
function displayUsageTable(stats, options = {}) {
|
|
572
|
+
const c = getColors(options.showColors);
|
|
573
|
+
const { compact, hideCost } = options;
|
|
574
|
+
const termWidth = process.stdout.columns || 140;
|
|
575
|
+
const useCompact = compact || termWidth < 120;
|
|
576
|
+
console.log();
|
|
577
|
+
let title = "Claude Code Usage";
|
|
578
|
+
if (stats.source === "codex") title = "Codex CLI Usage";
|
|
579
|
+
else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
|
|
580
|
+
const aggLabel = stats.aggregation === "daily" ? "Daily" : stats.aggregation === "monthly" ? "Monthly" : stats.aggregation === "model" ? "By Model" : "Total";
|
|
581
|
+
console.log(`${c.orange}${c.bold}${title} - ${aggLabel} Report${c.reset}`);
|
|
582
|
+
console.log(`${c.gray}Date range: ${stats.dateRange.start} to ${stats.dateRange.end}${c.reset}`);
|
|
583
|
+
console.log(`${c.gray}${"\u2500".repeat(Math.min(termWidth - 2, 120))}${c.reset}`);
|
|
584
|
+
console.log();
|
|
585
|
+
const headers = useCompact ? ["Date", "Input", "Output", "Total", ...hideCost ? [] : ["Cost"]] : ["Date", "", "Input", "Output", "Cache W", "Cache R", "Total", ...hideCost ? [] : ["Cost"]];
|
|
586
|
+
const widths = useCompact ? [12, 14, 12, 16, ...hideCost ? [] : [12]] : [12, 16, 14, 12, 14, 14, 16, ...hideCost ? [] : [12]];
|
|
587
|
+
const headerRow = headers.map((h, i) => padLeft(h, widths[i])).join(" ");
|
|
588
|
+
console.log(`${c.bold}${headerRow}${c.reset}`);
|
|
589
|
+
for (const row of stats.rows) {
|
|
590
|
+
const models = row.models || [];
|
|
591
|
+
const firstModel = models[0] || "";
|
|
592
|
+
const remainingModels = models.slice(1);
|
|
593
|
+
if (useCompact) {
|
|
594
|
+
const cols = [
|
|
595
|
+
padRight(row.key.slice(0, 12), widths[0]),
|
|
596
|
+
padLeft(formatNumber(row.inputTokens), widths[1]),
|
|
597
|
+
padLeft(formatNumber(row.outputTokens), widths[2]),
|
|
598
|
+
padLeft(formatNumber(row.totalTokens), widths[3]),
|
|
599
|
+
...hideCost ? [] : [padLeft(formatCost(row.cost), widths[4])]
|
|
600
|
+
];
|
|
601
|
+
console.log(cols.join(" "));
|
|
602
|
+
} else {
|
|
603
|
+
const modelDisplay = firstModel ? `- ${firstModel}` : "";
|
|
604
|
+
const cols = [
|
|
605
|
+
padRight(row.key.slice(0, 12), widths[0]),
|
|
606
|
+
padRight(modelDisplay.slice(0, 16), widths[1]),
|
|
607
|
+
padLeft(formatNumber(row.inputTokens), widths[2]),
|
|
608
|
+
padLeft(formatNumber(row.outputTokens), widths[3]),
|
|
609
|
+
padLeft(formatNumber(row.cacheWriteTokens), widths[4]),
|
|
610
|
+
padLeft(formatNumber(row.cacheReadTokens), widths[5]),
|
|
611
|
+
padLeft(`${c.orange}${formatNumber(row.totalTokens)}${c.reset}`, widths[6] + c.orange.length + c.reset.length),
|
|
612
|
+
...hideCost ? [] : [padLeft(`${c.amber}${formatCost(row.cost)}${c.reset}`, widths[7] + c.amber.length + c.reset.length)]
|
|
613
|
+
];
|
|
614
|
+
console.log(cols.join(" "));
|
|
615
|
+
for (const model of remainingModels) {
|
|
616
|
+
const modelLine = `${" ".repeat(widths[0])} ${c.gray}- ${model}${c.reset}`;
|
|
617
|
+
console.log(modelLine);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const totalWidth = widths.reduce((a, b) => a + b + 1, 0);
|
|
622
|
+
console.log(`${c.gray}${"\u2500".repeat(totalWidth)}${c.reset}`);
|
|
623
|
+
const totals = stats.totals;
|
|
624
|
+
if (useCompact) {
|
|
625
|
+
const totalCols = [
|
|
626
|
+
padRight("Total", widths[0]),
|
|
627
|
+
padLeft(formatNumber(totals.inputTokens), widths[1]),
|
|
628
|
+
padLeft(formatNumber(totals.outputTokens), widths[2]),
|
|
629
|
+
padLeft(formatNumber(totals.totalTokens), widths[3]),
|
|
630
|
+
...hideCost ? [] : [padLeft(formatCost(totals.cost), widths[4])]
|
|
631
|
+
];
|
|
632
|
+
console.log(`${c.bold}${c.amber}${totalCols.join(" ")}${c.reset}`);
|
|
633
|
+
} else {
|
|
634
|
+
const totalCols = [
|
|
635
|
+
padRight("Total", widths[0]),
|
|
636
|
+
padRight("", widths[1]),
|
|
637
|
+
padLeft(formatNumber(totals.inputTokens), widths[2]),
|
|
638
|
+
padLeft(formatNumber(totals.outputTokens), widths[3]),
|
|
639
|
+
padLeft(formatNumber(totals.cacheWriteTokens), widths[4]),
|
|
640
|
+
padLeft(formatNumber(totals.cacheReadTokens), widths[5]),
|
|
641
|
+
padLeft(formatNumber(totals.totalTokens), widths[6]),
|
|
642
|
+
...hideCost ? [] : [padLeft(formatCost(totals.cost), widths[7])]
|
|
643
|
+
];
|
|
644
|
+
console.log(`${c.bold}${c.amber}${totalCols.join(" ")}${c.reset}`);
|
|
645
|
+
}
|
|
646
|
+
console.log();
|
|
647
|
+
}
|
|
648
|
+
function displayTotalOnly(stats, options = {}) {
|
|
649
|
+
const c = getColors(options.showColors);
|
|
650
|
+
const { hideCost } = options;
|
|
651
|
+
console.log();
|
|
652
|
+
let title = "Claude Code Usage";
|
|
653
|
+
if (stats.source === "codex") title = "Codex CLI Usage";
|
|
654
|
+
else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
|
|
655
|
+
console.log(`${c.orange}${c.bold}${title} - Summary${c.reset}`);
|
|
656
|
+
console.log(`${c.gray}Date range: ${stats.dateRange.start} to ${stats.dateRange.end}${c.reset}`);
|
|
657
|
+
console.log(`${c.gray}${"\u2500".repeat(50)}${c.reset}`);
|
|
658
|
+
console.log();
|
|
659
|
+
const t = stats.totals;
|
|
660
|
+
console.log(` ${c.bold}Input Tokens:${c.reset} ${c.amber}${formatNumber(t.inputTokens)}${c.reset}`);
|
|
661
|
+
console.log(` ${c.bold}Output Tokens:${c.reset} ${c.amber}${formatNumber(t.outputTokens)}${c.reset}`);
|
|
662
|
+
console.log(` ${c.bold}Cache Write:${c.reset} ${c.amber}${formatNumber(t.cacheWriteTokens)}${c.reset}`);
|
|
663
|
+
console.log(` ${c.bold}Cache Read:${c.reset} ${c.amber}${formatNumber(t.cacheReadTokens)}${c.reset}`);
|
|
664
|
+
console.log(` ${c.gray}${"\u2500".repeat(30)}${c.reset}`);
|
|
665
|
+
console.log(` ${c.bold}Total Tokens:${c.reset} ${c.amber}${c.bold}${formatNumber(t.totalTokens)}${c.reset}`);
|
|
666
|
+
if (!hideCost) {
|
|
667
|
+
console.log(` ${c.bold}Total Cost:${c.reset} ${c.amber}${c.bold}${formatCost(t.cost)}${c.reset}`);
|
|
668
|
+
}
|
|
669
|
+
console.log();
|
|
670
|
+
if (stats.modelBreakdown.length > 0) {
|
|
671
|
+
console.log(`${c.bold}Model Breakdown${c.reset}`);
|
|
672
|
+
console.log(`${c.gray}${"\u2500".repeat(40)}${c.reset}`);
|
|
673
|
+
for (const m of stats.modelBreakdown) {
|
|
674
|
+
const filled = Math.max(0, Math.min(20, Math.round(m.percentage / 100 * 20)));
|
|
675
|
+
const empty = Math.max(0, 20 - filled);
|
|
676
|
+
const bar = `${c.amber}${"\u2588".repeat(filled)}${c.gray}${"\u2591".repeat(empty)}${c.reset}`;
|
|
677
|
+
const costStr = hideCost ? "" : ` ${c.gray}${formatCost(m.cost)}${c.reset}`;
|
|
678
|
+
console.log(` ${m.model.padEnd(14)} ${bar} ${m.percentage}%${costStr}`);
|
|
679
|
+
}
|
|
680
|
+
console.log();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/claude-jsonl-loader.ts
|
|
685
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
686
|
+
import { homedir as homedir2 } from "os";
|
|
687
|
+
import { join as join2 } from "path";
|
|
688
|
+
function getClaudeDir2() {
|
|
689
|
+
return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
|
|
690
|
+
}
|
|
691
|
+
function claudeJsonlDataExists() {
|
|
692
|
+
const claudeDir = getClaudeDir2();
|
|
693
|
+
const projectsDir = join2(claudeDir, "projects");
|
|
694
|
+
return existsSync2(projectsDir);
|
|
695
|
+
}
|
|
696
|
+
function findJsonlFiles2(dir) {
|
|
697
|
+
const files = [];
|
|
698
|
+
if (!existsSync2(dir)) return files;
|
|
699
|
+
const entries = readdirSync2(dir);
|
|
700
|
+
for (const entry of entries) {
|
|
701
|
+
const fullPath = join2(dir, entry);
|
|
702
|
+
try {
|
|
703
|
+
const stat = statSync2(fullPath);
|
|
704
|
+
if (stat.isDirectory()) {
|
|
705
|
+
files.push(...findJsonlFiles2(fullPath));
|
|
706
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
707
|
+
files.push(fullPath);
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return files;
|
|
713
|
+
}
|
|
714
|
+
function loadClaudeStatsFromJsonl() {
|
|
715
|
+
const claudeDir = getClaudeDir2();
|
|
716
|
+
const projectsDir = join2(claudeDir, "projects");
|
|
717
|
+
if (!claudeJsonlDataExists()) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
const jsonlFiles = findJsonlFiles2(projectsDir);
|
|
721
|
+
if (jsonlFiles.length === 0) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
const modelUsage = {};
|
|
725
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
726
|
+
const hourCounts = {};
|
|
727
|
+
let firstTimestamp = null;
|
|
728
|
+
let totalMessages = 0;
|
|
729
|
+
const messageIds = /* @__PURE__ */ new Set();
|
|
730
|
+
for (const filePath of jsonlFiles) {
|
|
731
|
+
try {
|
|
732
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
733
|
+
const lines = content.split("\n");
|
|
734
|
+
for (const line of lines) {
|
|
735
|
+
if (!line.trim()) continue;
|
|
736
|
+
try {
|
|
737
|
+
const entry = JSON.parse(line);
|
|
738
|
+
if (entry.type !== "assistant") continue;
|
|
739
|
+
if (!entry.message?.usage) continue;
|
|
740
|
+
const usage = entry.message.usage;
|
|
741
|
+
const model = entry.message.model || "unknown";
|
|
742
|
+
const timestamp = entry.timestamp;
|
|
743
|
+
const messageId = entry.message.id;
|
|
744
|
+
if (!modelUsage[model]) {
|
|
745
|
+
modelUsage[model] = {
|
|
746
|
+
inputTokens: 0,
|
|
747
|
+
outputTokens: 0,
|
|
748
|
+
cacheReadInputTokens: 0,
|
|
749
|
+
cacheCreationInputTokens: 0
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
modelUsage[model].inputTokens += usage.input_tokens || 0;
|
|
753
|
+
modelUsage[model].outputTokens += usage.output_tokens || 0;
|
|
754
|
+
modelUsage[model].cacheReadInputTokens += usage.cache_read_input_tokens || 0;
|
|
755
|
+
modelUsage[model].cacheCreationInputTokens += usage.cache_creation_input_tokens || 0;
|
|
756
|
+
if (messageId) {
|
|
757
|
+
messageIds.add(messageId);
|
|
758
|
+
}
|
|
759
|
+
if (timestamp) {
|
|
760
|
+
const date = timestamp.split("T")[0];
|
|
761
|
+
const hour = new Date(timestamp).getHours().toString();
|
|
762
|
+
if (!dailyMap.has(date)) {
|
|
763
|
+
dailyMap.set(date, { messageCount: 0, sessionCount: 0, toolCallCount: 0 });
|
|
764
|
+
}
|
|
765
|
+
const daily = dailyMap.get(date);
|
|
766
|
+
daily.messageCount++;
|
|
767
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
768
|
+
}
|
|
769
|
+
if (timestamp && (!firstTimestamp || timestamp < firstTimestamp)) {
|
|
770
|
+
firstTimestamp = timestamp;
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
totalMessages = messageIds.size;
|
|
779
|
+
if (totalMessages === 0) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
const statsCacheModelUsage = {};
|
|
783
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
784
|
+
statsCacheModelUsage[model] = {
|
|
785
|
+
inputTokens: usage.inputTokens,
|
|
786
|
+
outputTokens: usage.outputTokens,
|
|
787
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
788
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
|
789
|
+
webSearchRequests: 0,
|
|
790
|
+
costUSD: 0,
|
|
791
|
+
// Will be calculated by metrics.ts
|
|
792
|
+
contextWindow: 2e5
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
const dailyActivity = Array.from(dailyMap.entries()).map(([date, data]) => ({
|
|
796
|
+
date,
|
|
797
|
+
messageCount: data.messageCount,
|
|
798
|
+
sessionCount: 1,
|
|
799
|
+
// Each day counts as at least 1 session for activity
|
|
800
|
+
toolCallCount: data.toolCallCount
|
|
801
|
+
})).sort((a, b) => a.date.localeCompare(b.date));
|
|
802
|
+
const sessionsByDate = /* @__PURE__ */ new Map();
|
|
803
|
+
return {
|
|
804
|
+
version: 1,
|
|
805
|
+
lastComputedDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
806
|
+
dailyActivity,
|
|
807
|
+
dailyModelTokens: [],
|
|
808
|
+
// Not needed for wrapped stats
|
|
809
|
+
modelUsage: statsCacheModelUsage,
|
|
810
|
+
totalSessions: jsonlFiles.length,
|
|
811
|
+
totalMessages,
|
|
812
|
+
longestSession: {
|
|
813
|
+
sessionId: "",
|
|
814
|
+
duration: 0,
|
|
815
|
+
messageCount: 0,
|
|
816
|
+
timestamp: ""
|
|
817
|
+
},
|
|
818
|
+
firstSessionDate: firstTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
819
|
+
hourCounts
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/codex-loader.ts
|
|
824
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
825
|
+
import { homedir as homedir3 } from "os";
|
|
826
|
+
import { join as join3 } from "path";
|
|
827
|
+
function getCodexDir2() {
|
|
828
|
+
return process.env.CODEX_HOME || join3(homedir3(), ".codex");
|
|
829
|
+
}
|
|
830
|
+
function codexDataExists() {
|
|
831
|
+
const codexDir = getCodexDir2();
|
|
832
|
+
if (!existsSync3(codexDir)) return false;
|
|
833
|
+
const sessionsDir = join3(codexDir, "sessions");
|
|
834
|
+
const archivedDir = join3(codexDir, "archived_sessions");
|
|
835
|
+
return existsSync3(sessionsDir) || existsSync3(archivedDir);
|
|
836
|
+
}
|
|
837
|
+
function findJsonlFiles3(dir) {
|
|
838
|
+
const files = [];
|
|
839
|
+
if (!existsSync3(dir)) return files;
|
|
840
|
+
const entries = readdirSync3(dir);
|
|
841
|
+
for (const entry of entries) {
|
|
842
|
+
const fullPath = join3(dir, entry);
|
|
843
|
+
const stat = statSync3(fullPath);
|
|
844
|
+
if (stat.isDirectory()) {
|
|
845
|
+
files.push(...findJsonlFiles3(fullPath));
|
|
846
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
847
|
+
files.push(fullPath);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return files;
|
|
851
|
+
}
|
|
852
|
+
function parseSessionFile(filePath) {
|
|
853
|
+
try {
|
|
854
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
855
|
+
const lines = content.trim().split("\n");
|
|
856
|
+
let sessionMeta = null;
|
|
857
|
+
let currentModel = "gpt-5";
|
|
858
|
+
const perModelUsage = {};
|
|
859
|
+
for (const line of lines) {
|
|
860
|
+
if (!line.trim()) continue;
|
|
861
|
+
try {
|
|
862
|
+
const entry = JSON.parse(line);
|
|
863
|
+
if (entry.type === "session_meta") {
|
|
864
|
+
sessionMeta = entry.payload;
|
|
865
|
+
} else if (entry.type === "turn_context") {
|
|
866
|
+
const payload = entry.payload;
|
|
867
|
+
if (payload.model) {
|
|
868
|
+
currentModel = payload.model;
|
|
869
|
+
}
|
|
870
|
+
} else if (entry.type === "event_msg") {
|
|
871
|
+
const payload = entry.payload;
|
|
872
|
+
if (payload.type === "token_count" && payload.info?.last_token_usage) {
|
|
873
|
+
const usage = payload.info.last_token_usage;
|
|
874
|
+
if (!perModelUsage[currentModel]) {
|
|
875
|
+
perModelUsage[currentModel] = {
|
|
876
|
+
input_tokens: 0,
|
|
877
|
+
cached_input_tokens: 0,
|
|
878
|
+
output_tokens: 0,
|
|
879
|
+
reasoning_output_tokens: 0,
|
|
880
|
+
total_tokens: 0
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
perModelUsage[currentModel].input_tokens += usage.input_tokens || 0;
|
|
884
|
+
perModelUsage[currentModel].cached_input_tokens += usage.cached_input_tokens || 0;
|
|
885
|
+
perModelUsage[currentModel].output_tokens += usage.output_tokens || 0;
|
|
886
|
+
perModelUsage[currentModel].reasoning_output_tokens += usage.reasoning_output_tokens || 0;
|
|
887
|
+
perModelUsage[currentModel].total_tokens += usage.total_tokens || 0;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (!sessionMeta) return null;
|
|
894
|
+
const summedUsage = {
|
|
895
|
+
input_tokens: 0,
|
|
896
|
+
cached_input_tokens: 0,
|
|
897
|
+
output_tokens: 0,
|
|
898
|
+
reasoning_output_tokens: 0,
|
|
899
|
+
total_tokens: 0
|
|
900
|
+
};
|
|
901
|
+
let primaryModel = "gpt-5";
|
|
902
|
+
let maxTokens = 0;
|
|
903
|
+
for (const [model, usage] of Object.entries(perModelUsage)) {
|
|
904
|
+
summedUsage.input_tokens += usage.input_tokens;
|
|
905
|
+
summedUsage.cached_input_tokens += usage.cached_input_tokens;
|
|
906
|
+
summedUsage.output_tokens += usage.output_tokens;
|
|
907
|
+
summedUsage.reasoning_output_tokens += usage.reasoning_output_tokens;
|
|
908
|
+
summedUsage.total_tokens += usage.total_tokens;
|
|
909
|
+
if (usage.total_tokens > maxTokens) {
|
|
910
|
+
maxTokens = usage.total_tokens;
|
|
911
|
+
primaryModel = model;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
id: sessionMeta.id,
|
|
916
|
+
timestamp: sessionMeta.timestamp,
|
|
917
|
+
cwd: sessionMeta.cwd,
|
|
918
|
+
model: primaryModel,
|
|
919
|
+
tokenUsage: summedUsage,
|
|
920
|
+
perModelUsage
|
|
921
|
+
// New: per-model token breakdown
|
|
922
|
+
};
|
|
923
|
+
} catch {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function loadCodexStats() {
|
|
928
|
+
const codexDir = getCodexDir2();
|
|
929
|
+
if (!codexDataExists()) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const sessionsDir = join3(codexDir, "sessions");
|
|
933
|
+
const archivedDir = join3(codexDir, "archived_sessions");
|
|
934
|
+
const jsonlFiles = [
|
|
935
|
+
...findJsonlFiles3(sessionsDir),
|
|
936
|
+
...findJsonlFiles3(archivedDir)
|
|
937
|
+
];
|
|
938
|
+
if (jsonlFiles.length === 0) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const sessions = [];
|
|
942
|
+
for (const file of jsonlFiles) {
|
|
943
|
+
const session = parseSessionFile(file);
|
|
944
|
+
if (session) {
|
|
945
|
+
sessions.push(session);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (sessions.length === 0) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
sessions.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
952
|
+
const modelUsage = {};
|
|
953
|
+
for (const session of sessions) {
|
|
954
|
+
if (session.perModelUsage) {
|
|
955
|
+
for (const [model, usage] of Object.entries(session.perModelUsage)) {
|
|
956
|
+
if (!modelUsage[model]) {
|
|
957
|
+
modelUsage[model] = {
|
|
958
|
+
inputTokens: 0,
|
|
959
|
+
outputTokens: 0,
|
|
960
|
+
cachedInputTokens: 0,
|
|
961
|
+
reasoningTokens: 0
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
modelUsage[model].inputTokens += usage.input_tokens;
|
|
965
|
+
modelUsage[model].outputTokens += usage.output_tokens;
|
|
966
|
+
modelUsage[model].cachedInputTokens += usage.cached_input_tokens;
|
|
967
|
+
modelUsage[model].reasoningTokens += usage.reasoning_output_tokens;
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
if (!modelUsage[session.model]) {
|
|
971
|
+
modelUsage[session.model] = {
|
|
972
|
+
inputTokens: 0,
|
|
973
|
+
outputTokens: 0,
|
|
974
|
+
cachedInputTokens: 0,
|
|
975
|
+
reasoningTokens: 0
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
modelUsage[session.model].inputTokens += session.tokenUsage.input_tokens;
|
|
979
|
+
modelUsage[session.model].outputTokens += session.tokenUsage.output_tokens;
|
|
980
|
+
modelUsage[session.model].cachedInputTokens += session.tokenUsage.cached_input_tokens;
|
|
981
|
+
modelUsage[session.model].reasoningTokens += session.tokenUsage.reasoning_output_tokens;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
985
|
+
for (const session of sessions) {
|
|
986
|
+
const date = session.timestamp.split("T")[0];
|
|
987
|
+
dailyMap.set(date, (dailyMap.get(date) || 0) + 1);
|
|
988
|
+
}
|
|
989
|
+
const dailyActivity = Array.from(dailyMap.entries()).map(([date, sessionCount]) => ({ date, sessionCount })).sort((a, b) => a.date.localeCompare(b.date));
|
|
990
|
+
const hourCounts = {};
|
|
991
|
+
for (const session of sessions) {
|
|
992
|
+
const hour = new Date(session.timestamp).getHours().toString();
|
|
993
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
994
|
+
}
|
|
995
|
+
const totalMessages = sessions.length;
|
|
996
|
+
return {
|
|
997
|
+
sessions,
|
|
998
|
+
totalSessions: sessions.length,
|
|
999
|
+
totalMessages,
|
|
1000
|
+
modelUsage,
|
|
1001
|
+
dailyActivity,
|
|
1002
|
+
hourCounts,
|
|
1003
|
+
firstSessionDate: sessions[0].timestamp.split("T")[0]
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/shared/data-loader.ts
|
|
1008
|
+
function loadData(options) {
|
|
1009
|
+
const { codexOnly, combined } = options;
|
|
1010
|
+
let claude = null;
|
|
1011
|
+
let codex = null;
|
|
1012
|
+
if (!codexOnly) {
|
|
1013
|
+
if (claudeJsonlDataExists()) {
|
|
1014
|
+
claude = loadClaudeStatsFromJsonl();
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (codexOnly || combined) {
|
|
1018
|
+
if (codexDataExists()) {
|
|
1019
|
+
codex = loadCodexStats();
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
let source = "claude";
|
|
1023
|
+
if (codexOnly) source = "codex";
|
|
1024
|
+
else if (combined) source = "combined";
|
|
1025
|
+
return { claude, codex, source };
|
|
1026
|
+
}
|
|
1027
|
+
function validateData(data, options) {
|
|
1028
|
+
if (!data.claude && !data.codex) {
|
|
1029
|
+
if (options.codexOnly) {
|
|
1030
|
+
console.error("Error: OpenAI Codex data not found at ~/.codex");
|
|
1031
|
+
console.error("Make sure you have used the Codex CLI at least once.");
|
|
1032
|
+
} else if (options.combined) {
|
|
1033
|
+
console.error("Error: No usage data found");
|
|
1034
|
+
console.error("Make sure you have used Claude Code or Codex CLI at least once.");
|
|
1035
|
+
} else {
|
|
1036
|
+
console.error("Error: Claude Code data not found at ~/.claude");
|
|
1037
|
+
console.error("Make sure you have used Claude Code at least once.");
|
|
1038
|
+
}
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/metrics.ts
|
|
1044
|
+
function calculateModelCost(modelName, usage) {
|
|
1045
|
+
const pricing = getModelPricing(modelName);
|
|
1046
|
+
const inputCost = usage.inputTokens / 1e6 * pricing.input;
|
|
1047
|
+
const outputCost = usage.outputTokens / 1e6 * pricing.output;
|
|
1048
|
+
const cacheWriteCost = usage.cacheCreationInputTokens / 1e6 * pricing.cacheWrite;
|
|
1049
|
+
const cacheReadCost = usage.cacheReadInputTokens / 1e6 * pricing.cacheRead;
|
|
1050
|
+
return inputCost + outputCost + cacheWriteCost + cacheReadCost;
|
|
1051
|
+
}
|
|
1052
|
+
function calculateTotalTokens(modelUsage) {
|
|
1053
|
+
let total = 0;
|
|
1054
|
+
for (const usage of Object.values(modelUsage)) {
|
|
1055
|
+
total += usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
|
|
1056
|
+
}
|
|
1057
|
+
return total;
|
|
1058
|
+
}
|
|
1059
|
+
function calculateStreaks(dailyActivity) {
|
|
1060
|
+
if (dailyActivity.length === 0) return { longest: 0, current: 0 };
|
|
1061
|
+
const dates = dailyActivity.map((d) => d.date).sort();
|
|
1062
|
+
const dateSet = new Set(dates);
|
|
1063
|
+
let longest = 1;
|
|
1064
|
+
let current = 1;
|
|
1065
|
+
let tempStreak = 1;
|
|
1066
|
+
for (let i = 1; i < dates.length; i++) {
|
|
1067
|
+
const prevDate = new Date(dates[i - 1]);
|
|
1068
|
+
const currDate = new Date(dates[i]);
|
|
1069
|
+
const diffDays = Math.floor((currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1070
|
+
if (diffDays === 1) {
|
|
1071
|
+
tempStreak++;
|
|
1072
|
+
longest = Math.max(longest, tempStreak);
|
|
1073
|
+
} else if (diffDays > 1) {
|
|
1074
|
+
tempStreak = 1;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const today = /* @__PURE__ */ new Date();
|
|
1078
|
+
today.setHours(0, 0, 0, 0);
|
|
1079
|
+
let checkDate = today;
|
|
1080
|
+
current = 0;
|
|
1081
|
+
while (true) {
|
|
1082
|
+
const dateStr = checkDate.toISOString().split("T")[0];
|
|
1083
|
+
if (dateSet.has(dateStr)) {
|
|
1084
|
+
current++;
|
|
1085
|
+
checkDate = new Date(checkDate.getTime() - 24 * 60 * 60 * 1e3);
|
|
1086
|
+
} else {
|
|
1087
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1e3);
|
|
1088
|
+
if (checkDate.getTime() === today.getTime()) {
|
|
1089
|
+
checkDate = yesterday;
|
|
1090
|
+
} else {
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { longest, current };
|
|
1096
|
+
}
|
|
1097
|
+
function findPeakHour(hourCounts) {
|
|
1098
|
+
let peakHour = 0;
|
|
1099
|
+
let maxCount = 0;
|
|
1100
|
+
for (const [hour, count] of Object.entries(hourCounts)) {
|
|
1101
|
+
if (count > maxCount) {
|
|
1102
|
+
maxCount = count;
|
|
1103
|
+
peakHour = parseInt(hour, 10);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return peakHour;
|
|
1107
|
+
}
|
|
1108
|
+
function findPeakDay(dailyActivity) {
|
|
1109
|
+
const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1110
|
+
const dayCounts = new Array(7).fill(0);
|
|
1111
|
+
for (const day of dailyActivity) {
|
|
1112
|
+
const date = new Date(day.date);
|
|
1113
|
+
const dayOfWeek = date.getDay();
|
|
1114
|
+
dayCounts[dayOfWeek] += day.messageCount;
|
|
1115
|
+
}
|
|
1116
|
+
let peakDay = 0;
|
|
1117
|
+
let maxCount = 0;
|
|
1118
|
+
for (let i = 0; i < 7; i++) {
|
|
1119
|
+
if (dayCounts[i] > maxCount) {
|
|
1120
|
+
maxCount = dayCounts[i];
|
|
1121
|
+
peakDay = i;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return dayNames[peakDay];
|
|
1125
|
+
}
|
|
1126
|
+
function findBusiestMonth(dailyActivity) {
|
|
1127
|
+
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1128
|
+
const monthCounts = {};
|
|
1129
|
+
for (const day of dailyActivity) {
|
|
1130
|
+
const date = new Date(day.date);
|
|
1131
|
+
const monthKey = `${date.getFullYear()}-${monthNames[date.getMonth()]}`;
|
|
1132
|
+
monthCounts[monthKey] = (monthCounts[monthKey] || 0) + day.messageCount;
|
|
1133
|
+
}
|
|
1134
|
+
let busiestMonth = "";
|
|
1135
|
+
let maxCount = 0;
|
|
1136
|
+
for (const [month, count] of Object.entries(monthCounts)) {
|
|
1137
|
+
if (count > maxCount) {
|
|
1138
|
+
maxCount = count;
|
|
1139
|
+
busiestMonth = month;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return busiestMonth;
|
|
1143
|
+
}
|
|
1144
|
+
var EXCLUDED_MODELS = ["synthetic", "test", "internal"];
|
|
1145
|
+
function shouldExcludeModel(modelName) {
|
|
1146
|
+
const lower = modelName.toLowerCase();
|
|
1147
|
+
return EXCLUDED_MODELS.some((excluded) => lower.includes(excluded));
|
|
1148
|
+
}
|
|
1149
|
+
function getModelBreakdown(modelUsage) {
|
|
1150
|
+
const totalTokens = calculateTotalTokens(modelUsage);
|
|
1151
|
+
if (totalTokens === 0) return [];
|
|
1152
|
+
const breakdown = [];
|
|
1153
|
+
for (const [modelName, usage] of Object.entries(modelUsage)) {
|
|
1154
|
+
if (shouldExcludeModel(modelName)) continue;
|
|
1155
|
+
const modelTokens = usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
|
|
1156
|
+
breakdown.push({
|
|
1157
|
+
model: getModelDisplayName(modelName),
|
|
1158
|
+
percentage: Math.round(modelTokens / totalTokens * 100),
|
|
1159
|
+
tokens: modelTokens
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
breakdown.sort((a, b) => b.tokens - a.tokens);
|
|
1163
|
+
return breakdown.map(({ model, percentage }) => ({ model, percentage }));
|
|
1164
|
+
}
|
|
1165
|
+
function getFavoriteModel(modelUsage) {
|
|
1166
|
+
let favoriteModel = "";
|
|
1167
|
+
let maxTokens = 0;
|
|
1168
|
+
for (const [modelName, usage] of Object.entries(modelUsage)) {
|
|
1169
|
+
if (shouldExcludeModel(modelName)) continue;
|
|
1170
|
+
const modelTokens = usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
|
|
1171
|
+
if (modelTokens > maxTokens) {
|
|
1172
|
+
maxTokens = modelTokens;
|
|
1173
|
+
favoriteModel = modelName;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return getModelDisplayName(favoriteModel);
|
|
1177
|
+
}
|
|
1178
|
+
function calculateWordsGenerated(modelUsage) {
|
|
1179
|
+
let totalOutputTokens = 0;
|
|
1180
|
+
for (const usage of Object.values(modelUsage)) {
|
|
1181
|
+
totalOutputTokens += usage.outputTokens;
|
|
1182
|
+
}
|
|
1183
|
+
return Math.round(totalOutputTokens * 0.75);
|
|
1184
|
+
}
|
|
1185
|
+
function computeWrappedStats(cache) {
|
|
1186
|
+
let totalCost = 0;
|
|
1187
|
+
for (const [modelName, usage] of Object.entries(cache.modelUsage)) {
|
|
1188
|
+
totalCost += calculateModelCost(modelName, usage);
|
|
1189
|
+
}
|
|
1190
|
+
const streaks = calculateStreaks(cache.dailyActivity);
|
|
1191
|
+
const daysActive = cache.dailyActivity.length;
|
|
1192
|
+
const peakHour = findPeakHour(cache.hourCounts);
|
|
1193
|
+
const peakDay = findPeakDay(cache.dailyActivity);
|
|
1194
|
+
const busiestMonth = findBusiestMonth(cache.dailyActivity);
|
|
1195
|
+
const favoriteModel = getFavoriteModel(cache.modelUsage);
|
|
1196
|
+
const modelBreakdown = getModelBreakdown(cache.modelUsage);
|
|
1197
|
+
const totalTokens = calculateTotalTokens(cache.modelUsage);
|
|
1198
|
+
const wordsGenerated = calculateWordsGenerated(cache.modelUsage);
|
|
1199
|
+
return {
|
|
1200
|
+
sessions: cache.totalSessions,
|
|
1201
|
+
messages: cache.totalMessages,
|
|
1202
|
+
totalTokens,
|
|
1203
|
+
totalCost,
|
|
1204
|
+
daysActive,
|
|
1205
|
+
longestStreak: streaks.longest,
|
|
1206
|
+
currentStreak: streaks.current,
|
|
1207
|
+
peakHour,
|
|
1208
|
+
peakDay,
|
|
1209
|
+
busiestMonth,
|
|
1210
|
+
favoriteModel,
|
|
1211
|
+
modelBreakdown,
|
|
1212
|
+
wordsGenerated,
|
|
1213
|
+
firstSessionDate: cache.firstSessionDate,
|
|
1214
|
+
source: "claude"
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function computeCodexWrappedStats(cache) {
|
|
1218
|
+
let totalCost = 0;
|
|
1219
|
+
let totalTokens = 0;
|
|
1220
|
+
let totalOutputTokens = 0;
|
|
1221
|
+
for (const [modelName, usage] of Object.entries(cache.modelUsage)) {
|
|
1222
|
+
totalCost += calculateCodexCost(
|
|
1223
|
+
modelName,
|
|
1224
|
+
usage.inputTokens,
|
|
1225
|
+
usage.outputTokens,
|
|
1226
|
+
usage.cachedInputTokens
|
|
1227
|
+
);
|
|
1228
|
+
totalTokens += usage.inputTokens + usage.outputTokens;
|
|
1229
|
+
totalOutputTokens += usage.outputTokens + usage.reasoningTokens;
|
|
1230
|
+
}
|
|
1231
|
+
const streaks = calculateStreaks(cache.dailyActivity);
|
|
1232
|
+
const daysActive = cache.dailyActivity.length;
|
|
1233
|
+
const peakHour = findPeakHour(cache.hourCounts);
|
|
1234
|
+
const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1235
|
+
const dayCounts = new Array(7).fill(0);
|
|
1236
|
+
for (const day of cache.dailyActivity) {
|
|
1237
|
+
const date = new Date(day.date);
|
|
1238
|
+
const dayOfWeek = date.getDay();
|
|
1239
|
+
dayCounts[dayOfWeek] += day.sessionCount;
|
|
1240
|
+
}
|
|
1241
|
+
let peakDayIdx = 0;
|
|
1242
|
+
let maxDayCount = 0;
|
|
1243
|
+
for (let i = 0; i < 7; i++) {
|
|
1244
|
+
if (dayCounts[i] > maxDayCount) {
|
|
1245
|
+
maxDayCount = dayCounts[i];
|
|
1246
|
+
peakDayIdx = i;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const peakDay = dayNames[peakDayIdx];
|
|
1250
|
+
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1251
|
+
const monthCounts = {};
|
|
1252
|
+
for (const day of cache.dailyActivity) {
|
|
1253
|
+
const date = new Date(day.date);
|
|
1254
|
+
const monthKey = `${date.getFullYear()}-${monthNames[date.getMonth()]}`;
|
|
1255
|
+
monthCounts[monthKey] = (monthCounts[monthKey] || 0) + day.sessionCount;
|
|
1256
|
+
}
|
|
1257
|
+
let busiestMonth = "";
|
|
1258
|
+
let maxMonthCount = 0;
|
|
1259
|
+
for (const [month, count] of Object.entries(monthCounts)) {
|
|
1260
|
+
if (count > maxMonthCount) {
|
|
1261
|
+
maxMonthCount = count;
|
|
1262
|
+
busiestMonth = month;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
let favoriteModel = "";
|
|
1266
|
+
let maxModelTokens = 0;
|
|
1267
|
+
const breakdown = [];
|
|
1268
|
+
for (const [modelName, usage] of Object.entries(cache.modelUsage)) {
|
|
1269
|
+
const modelTokens = usage.inputTokens + usage.outputTokens;
|
|
1270
|
+
breakdown.push({
|
|
1271
|
+
model: getCodexModelDisplayName(modelName),
|
|
1272
|
+
percentage: totalTokens > 0 ? Math.round(modelTokens / totalTokens * 100) : 0,
|
|
1273
|
+
tokens: modelTokens
|
|
1274
|
+
});
|
|
1275
|
+
if (modelTokens > maxModelTokens) {
|
|
1276
|
+
maxModelTokens = modelTokens;
|
|
1277
|
+
favoriteModel = modelName;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
breakdown.sort((a, b) => b.tokens - a.tokens);
|
|
1281
|
+
const modelBreakdown = breakdown.map(({ model, percentage }) => ({ model, percentage }));
|
|
1282
|
+
const wordsGenerated = Math.round(totalOutputTokens * 0.75);
|
|
1283
|
+
return {
|
|
1284
|
+
sessions: cache.totalSessions,
|
|
1285
|
+
messages: cache.totalMessages,
|
|
1286
|
+
totalTokens,
|
|
1287
|
+
totalCost,
|
|
1288
|
+
daysActive,
|
|
1289
|
+
longestStreak: streaks.longest,
|
|
1290
|
+
currentStreak: streaks.current,
|
|
1291
|
+
peakHour,
|
|
1292
|
+
peakDay,
|
|
1293
|
+
busiestMonth,
|
|
1294
|
+
favoriteModel: getCodexModelDisplayName(favoriteModel),
|
|
1295
|
+
modelBreakdown,
|
|
1296
|
+
wordsGenerated,
|
|
1297
|
+
firstSessionDate: cache.firstSessionDate,
|
|
1298
|
+
source: "codex"
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function combineWrappedStats(claude, codex) {
|
|
1302
|
+
if (!claude && !codex) {
|
|
1303
|
+
throw new Error("No stats available from either source");
|
|
1304
|
+
}
|
|
1305
|
+
if (!claude) return { ...codex, source: "codex" };
|
|
1306
|
+
if (!codex) return { ...claude, source: "claude" };
|
|
1307
|
+
const allDates = /* @__PURE__ */ new Set();
|
|
1308
|
+
const longestStreak = Math.max(claude.longestStreak, codex.longestStreak);
|
|
1309
|
+
const currentStreak = Math.max(claude.currentStreak, codex.currentStreak);
|
|
1310
|
+
const modelTokensMap = /* @__PURE__ */ new Map();
|
|
1311
|
+
const totalCombinedTokens = claude.totalTokens + codex.totalTokens;
|
|
1312
|
+
for (const mb of claude.modelBreakdown) {
|
|
1313
|
+
const tokens = mb.percentage / 100 * claude.totalTokens;
|
|
1314
|
+
modelTokensMap.set(mb.model, (modelTokensMap.get(mb.model) || 0) + tokens);
|
|
1315
|
+
}
|
|
1316
|
+
for (const mb of codex.modelBreakdown) {
|
|
1317
|
+
const tokens = mb.percentage / 100 * codex.totalTokens;
|
|
1318
|
+
modelTokensMap.set(mb.model, (modelTokensMap.get(mb.model) || 0) + tokens);
|
|
1319
|
+
}
|
|
1320
|
+
const combinedBreakdown = Array.from(modelTokensMap.entries()).map(([model, tokens]) => ({
|
|
1321
|
+
model,
|
|
1322
|
+
percentage: totalCombinedTokens > 0 ? Math.round(tokens / totalCombinedTokens * 100) : 0,
|
|
1323
|
+
tokens
|
|
1324
|
+
})).sort((a, b) => b.tokens - a.tokens).map(({ model, percentage }) => ({ model, percentage }));
|
|
1325
|
+
const favoriteModel = combinedBreakdown[0]?.model || claude.favoriteModel;
|
|
1326
|
+
const firstSessionDate = claude.firstSessionDate < codex.firstSessionDate ? claude.firstSessionDate : codex.firstSessionDate;
|
|
1327
|
+
const peakHour = claude.sessions > codex.sessions ? claude.peakHour : codex.peakHour;
|
|
1328
|
+
const peakDay = claude.sessions > codex.sessions ? claude.peakDay : codex.peakDay;
|
|
1329
|
+
const busiestMonth = claude.sessions > codex.sessions ? claude.busiestMonth : codex.busiestMonth;
|
|
1330
|
+
return {
|
|
1331
|
+
sessions: claude.sessions + codex.sessions,
|
|
1332
|
+
messages: claude.messages + codex.messages,
|
|
1333
|
+
totalTokens: claude.totalTokens + codex.totalTokens,
|
|
1334
|
+
totalCost: claude.totalCost + codex.totalCost,
|
|
1335
|
+
daysActive: claude.daysActive + codex.daysActive,
|
|
1336
|
+
// May overcount, but close enough
|
|
1337
|
+
longestStreak,
|
|
1338
|
+
currentStreak,
|
|
1339
|
+
peakHour,
|
|
1340
|
+
peakDay,
|
|
1341
|
+
busiestMonth,
|
|
1342
|
+
favoriteModel,
|
|
1343
|
+
modelBreakdown: combinedBreakdown,
|
|
1344
|
+
wordsGenerated: claude.wordsGenerated + codex.wordsGenerated,
|
|
1345
|
+
firstSessionDate,
|
|
1346
|
+
source: "combined"
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/url-encoder.ts
|
|
1351
|
+
function formatCompactNumber(num) {
|
|
1352
|
+
if (num >= 1e9) {
|
|
1353
|
+
return `${(num / 1e9).toFixed(1)}B`;
|
|
1354
|
+
}
|
|
1355
|
+
if (num >= 1e6) {
|
|
1356
|
+
return `${(num / 1e6).toFixed(1)}M`;
|
|
1357
|
+
}
|
|
1358
|
+
if (num >= 1e3) {
|
|
1359
|
+
return `${(num / 1e3).toFixed(1)}K`;
|
|
1360
|
+
}
|
|
1361
|
+
return num.toString();
|
|
1362
|
+
}
|
|
1363
|
+
var dayToNumber = {
|
|
1364
|
+
Sunday: 0,
|
|
1365
|
+
Monday: 1,
|
|
1366
|
+
Tuesday: 2,
|
|
1367
|
+
Wednesday: 3,
|
|
1368
|
+
Thursday: 4,
|
|
1369
|
+
Friday: 5,
|
|
1370
|
+
Saturday: 6
|
|
1371
|
+
};
|
|
1372
|
+
function encodeStatsToUrl(stats, baseUrl = "https://wrapped.wolfai.dev") {
|
|
1373
|
+
const params = new URLSearchParams();
|
|
1374
|
+
params.set("s", stats.sessions.toString());
|
|
1375
|
+
params.set("t", formatCompactNumber(stats.totalTokens));
|
|
1376
|
+
params.set("c", stats.totalCost.toFixed(2));
|
|
1377
|
+
params.set("d", stats.daysActive.toString());
|
|
1378
|
+
params.set("ls", stats.longestStreak.toString());
|
|
1379
|
+
params.set("cs", stats.currentStreak.toString());
|
|
1380
|
+
params.set("ph", stats.peakHour.toString());
|
|
1381
|
+
params.set("pd", (dayToNumber[stats.peakDay] ?? 0).toString());
|
|
1382
|
+
params.set("fm", getModelAbbrevFromDisplayName(stats.favoriteModel));
|
|
1383
|
+
const mbParts = stats.modelBreakdown.slice(0, 3).map((m) => {
|
|
1384
|
+
const abbr = getModelAbbrevFromDisplayName(m.model);
|
|
1385
|
+
return `${abbr}:${m.percentage}`;
|
|
1386
|
+
});
|
|
1387
|
+
params.set("mb", mbParts.join(","));
|
|
1388
|
+
if (stats.topTools && stats.topTools.length > 0) {
|
|
1389
|
+
params.set("tt", stats.topTools.slice(0, 5).join(","));
|
|
1390
|
+
}
|
|
1391
|
+
if (stats.developerStyle) {
|
|
1392
|
+
const styleMap = {
|
|
1393
|
+
reader: "r",
|
|
1394
|
+
writer: "w",
|
|
1395
|
+
executor: "e",
|
|
1396
|
+
balanced: "b"
|
|
1397
|
+
};
|
|
1398
|
+
params.set("st", styleMap[stats.developerStyle] || "b");
|
|
1399
|
+
}
|
|
1400
|
+
if (stats.topProject) {
|
|
1401
|
+
params.set("tp", stats.topProject);
|
|
1402
|
+
}
|
|
1403
|
+
if (stats.projectCount) {
|
|
1404
|
+
params.set("pc", stats.projectCount.toString());
|
|
1405
|
+
}
|
|
1406
|
+
params.set("wg", formatCompactNumber(stats.wordsGenerated));
|
|
1407
|
+
const firstDate = new Date(stats.firstSessionDate);
|
|
1408
|
+
params.set("fad", firstDate.toISOString().split("T")[0].replace(/-/g, ""));
|
|
1409
|
+
if (stats.source && stats.source !== "claude") {
|
|
1410
|
+
params.set("src", stats.source);
|
|
1411
|
+
}
|
|
1412
|
+
return `${baseUrl}?${params.toString()}`;
|
|
1413
|
+
}
|
|
1414
|
+
function getModelAbbrevFromDisplayName(displayName) {
|
|
1415
|
+
const map = {
|
|
1416
|
+
// Claude models
|
|
1417
|
+
"Opus 4.5": "o45",
|
|
1418
|
+
"Opus 4.1": "o41",
|
|
1419
|
+
Opus: "opus",
|
|
1420
|
+
"Sonnet 4.5": "s45",
|
|
1421
|
+
"Sonnet 3.5": "s35",
|
|
1422
|
+
Sonnet: "sonnet",
|
|
1423
|
+
"Haiku 4.5": "h45",
|
|
1424
|
+
"Haiku 3.5": "h35",
|
|
1425
|
+
Haiku: "haiku",
|
|
1426
|
+
// Codex/OpenAI models
|
|
1427
|
+
"GPT-5.2": "g52",
|
|
1428
|
+
"GPT-5.1 Max": "g51m",
|
|
1429
|
+
"GPT-5.1 Mini": "g51n",
|
|
1430
|
+
"GPT-5.1": "g51",
|
|
1431
|
+
"GPT-5": "g5",
|
|
1432
|
+
"GPT-5 Mini": "g5n",
|
|
1433
|
+
"GPT-4o": "g4o",
|
|
1434
|
+
"GPT-4o Mini": "g4om"
|
|
1435
|
+
};
|
|
1436
|
+
return map[displayName] || displayName.slice(0, 5).toLowerCase();
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/display.ts
|
|
1440
|
+
var colors2 = {
|
|
1441
|
+
reset: "\x1B[0m",
|
|
1442
|
+
bold: "\x1B[1m",
|
|
1443
|
+
dim: "\x1B[2m",
|
|
1444
|
+
orange: "\x1B[38;5;208m",
|
|
1445
|
+
amber: "\x1B[38;5;214m",
|
|
1446
|
+
yellow: "\x1B[33m",
|
|
1447
|
+
green: "\x1B[32m",
|
|
1448
|
+
cyan: "\x1B[36m",
|
|
1449
|
+
white: "\x1B[37m",
|
|
1450
|
+
gray: "\x1B[90m"
|
|
1451
|
+
};
|
|
1452
|
+
var noColors2 = Object.fromEntries(
|
|
1453
|
+
Object.keys(colors2).map((k) => [k, ""])
|
|
1454
|
+
);
|
|
1455
|
+
function getColors2(theme) {
|
|
1456
|
+
if (theme?.enabled === false) {
|
|
1457
|
+
return noColors2;
|
|
1458
|
+
}
|
|
1459
|
+
return colors2;
|
|
1460
|
+
}
|
|
1461
|
+
function displayWrappedStats(stats, url, options) {
|
|
1462
|
+
const c = getColors2(options?.theme);
|
|
1463
|
+
console.log();
|
|
1464
|
+
let title = "\u2728 Claude Code Wrapped 2025 \u2728";
|
|
1465
|
+
let sourceLabel = "";
|
|
1466
|
+
if (stats.source === "codex") {
|
|
1467
|
+
title = "\u2728 Codex CLI Wrapped 2025 \u2728";
|
|
1468
|
+
sourceLabel = " (Codex)";
|
|
1469
|
+
} else if (stats.source === "combined") {
|
|
1470
|
+
title = "\u2728 AI Coding Wrapped 2025 \u2728";
|
|
1471
|
+
sourceLabel = " (Claude + Codex)";
|
|
1472
|
+
}
|
|
1473
|
+
console.log(`${c.orange}${c.bold}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${c.reset}`);
|
|
1474
|
+
console.log(`${c.orange}${c.bold}\u2551${c.reset} ${c.orange}${c.bold}\u2551${c.reset}`);
|
|
1475
|
+
console.log(`${c.orange}${c.bold}\u2551${c.reset} ${c.amber}${c.bold}${title}${c.reset} ${c.orange}${c.bold}\u2551${c.reset}`);
|
|
1476
|
+
console.log(`${c.orange}${c.bold}\u2551${c.reset} ${c.orange}${c.bold}\u2551${c.reset}`);
|
|
1477
|
+
console.log(`${c.orange}${c.bold}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${c.reset}`);
|
|
1478
|
+
console.log();
|
|
1479
|
+
console.log(`${c.bold}\u{1F4CA} Your Year in Numbers${c.reset}`);
|
|
1480
|
+
console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
|
|
1481
|
+
console.log();
|
|
1482
|
+
const statsLine = [
|
|
1483
|
+
`${c.amber}${c.bold}${stats.sessions}${c.reset} sessions`,
|
|
1484
|
+
`${c.amber}${c.bold}${formatCompactNumber(stats.totalTokens)}${c.reset} tokens`,
|
|
1485
|
+
...options?.hideCost ? [] : [`${c.amber}${c.bold}$${stats.totalCost.toFixed(0)}${c.reset} spent`],
|
|
1486
|
+
`${c.amber}${c.bold}${stats.daysActive}${c.reset} days active`
|
|
1487
|
+
];
|
|
1488
|
+
console.log(` ${statsLine.join(" \u2502 ")}`);
|
|
1489
|
+
console.log();
|
|
1490
|
+
console.log(`${c.bold}\u{1F525} Consistency${c.reset}`);
|
|
1491
|
+
console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
|
|
1492
|
+
console.log(` Longest streak: ${c.amber}${c.bold}${stats.longestStreak} days${c.reset}`);
|
|
1493
|
+
console.log(` Current streak: ${c.green}${stats.currentStreak} days${c.reset}`);
|
|
1494
|
+
console.log();
|
|
1495
|
+
console.log(`${c.bold}\u23F0 Your Coding Patterns${c.reset}`);
|
|
1496
|
+
console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
|
|
1497
|
+
console.log(` Peak hour: ${c.amber}${c.bold}${formatHour(stats.peakHour)}${c.reset}`);
|
|
1498
|
+
console.log(` Favorite day: ${c.amber}${c.bold}${stats.peakDay}${c.reset}`);
|
|
1499
|
+
console.log(` Busiest month: ${c.cyan}${stats.busiestMonth}${c.reset}`);
|
|
1500
|
+
console.log();
|
|
1501
|
+
console.log(`${c.bold}\u{1F916} Your AI Partners${c.reset}`);
|
|
1502
|
+
console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
|
|
1503
|
+
console.log(` Favorite model: ${c.amber}${c.bold}${stats.favoriteModel}${c.reset}`);
|
|
1504
|
+
if (stats.modelBreakdown.length > 0) {
|
|
1505
|
+
console.log(` Model breakdown:`);
|
|
1506
|
+
for (const { model, percentage } of stats.modelBreakdown) {
|
|
1507
|
+
const filled = Math.max(0, Math.min(20, Math.round(percentage / 100 * 20)));
|
|
1508
|
+
const empty = Math.max(0, 20 - filled);
|
|
1509
|
+
const bar = `${c.amber}${"\u2588".repeat(filled)}${c.gray}${"\u2591".repeat(empty)}${c.reset}`;
|
|
1510
|
+
console.log(` ${model.padEnd(12)} ${bar} ${percentage}%`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
console.log();
|
|
1514
|
+
console.log(`${c.bold}\u270D\uFE0F Words Generated${c.reset}`);
|
|
1515
|
+
console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
|
|
1516
|
+
console.log(` ${c.amber}${c.bold}${formatCompactNumber(stats.wordsGenerated)}${c.reset} words of code, docs, and conversation`);
|
|
1517
|
+
console.log(` ${c.dim}(That's like ${Math.round(stats.wordsGenerated / 5e4)} novels!)${c.reset}`);
|
|
1518
|
+
console.log();
|
|
1519
|
+
console.log(`${c.bold}\u{1F517} Share Your Wrapped${c.reset}`);
|
|
1520
|
+
console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
|
|
1521
|
+
if (options?.shortUrl) {
|
|
1522
|
+
console.log(` ${c.amber}${c.bold}${options.shortUrl}${c.reset}`);
|
|
1523
|
+
console.log(` ${c.dim}(Full URL: ${url})${c.reset}`);
|
|
1524
|
+
} else {
|
|
1525
|
+
console.log(` ${c.cyan}${url}${c.reset}`);
|
|
1526
|
+
}
|
|
1527
|
+
console.log();
|
|
1528
|
+
console.log(`${c.dim}Generated by vibestats (npx vibestats)${c.reset}`);
|
|
1529
|
+
console.log();
|
|
1530
|
+
}
|
|
1531
|
+
function formatHour(hour) {
|
|
1532
|
+
if (hour === 0) return "12am";
|
|
1533
|
+
if (hour < 12) return `${hour}am`;
|
|
1534
|
+
if (hour === 12) return "12pm";
|
|
1535
|
+
return `${hour - 12}pm`;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/shortener.ts
|
|
1539
|
+
async function createShortlink(params, baseUrl) {
|
|
1540
|
+
const queryString = params.includes("?") ? params.split("?")[1] : params;
|
|
1541
|
+
try {
|
|
1542
|
+
const apiUrl = new URL("/api/shorten", baseUrl);
|
|
1543
|
+
const response = await fetch(apiUrl.toString(), {
|
|
1544
|
+
method: "POST",
|
|
1545
|
+
headers: {
|
|
1546
|
+
"Content-Type": "application/json"
|
|
1547
|
+
},
|
|
1548
|
+
body: JSON.stringify({ params: queryString })
|
|
1549
|
+
});
|
|
1550
|
+
if (!response.ok) {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
const data = await response.json();
|
|
1554
|
+
if (data.slug) {
|
|
1555
|
+
return `${baseUrl}/s/${data.slug}`;
|
|
1556
|
+
}
|
|
1557
|
+
return null;
|
|
1558
|
+
} catch {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/config.ts
|
|
1564
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4, writeFileSync } from "fs";
|
|
1565
|
+
import { homedir as homedir4 } from "os";
|
|
1566
|
+
import { join as join4 } from "path";
|
|
1567
|
+
var CONFIG_PATH = join4(homedir4(), ".vibestats.json");
|
|
1568
|
+
var DEFAULT_CONFIG = {
|
|
1569
|
+
baseUrl: "https://wrapped.wolfai.dev",
|
|
1570
|
+
outputFormat: "normal",
|
|
1571
|
+
theme: {
|
|
1572
|
+
enabled: true
|
|
1573
|
+
},
|
|
1574
|
+
hideCost: false
|
|
1575
|
+
};
|
|
1576
|
+
function loadConfig() {
|
|
1577
|
+
if (!existsSync4(CONFIG_PATH)) {
|
|
1578
|
+
return DEFAULT_CONFIG;
|
|
1579
|
+
}
|
|
1580
|
+
try {
|
|
1581
|
+
const content = readFileSync4(CONFIG_PATH, "utf-8");
|
|
1582
|
+
const userConfig = JSON.parse(content);
|
|
1583
|
+
return mergeConfig(DEFAULT_CONFIG, userConfig);
|
|
1584
|
+
} catch {
|
|
1585
|
+
console.warn(`Warning: Could not parse config at ${CONFIG_PATH}`);
|
|
1586
|
+
return DEFAULT_CONFIG;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function mergeConfig(defaults, user) {
|
|
1590
|
+
return {
|
|
1591
|
+
...defaults,
|
|
1592
|
+
...user,
|
|
1593
|
+
theme: {
|
|
1594
|
+
...defaults.theme,
|
|
1595
|
+
...user.theme
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function initConfig() {
|
|
1600
|
+
if (existsSync4(CONFIG_PATH)) {
|
|
1601
|
+
console.log(`Config file already exists at ${CONFIG_PATH}`);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const defaultConfig = {
|
|
1605
|
+
baseUrl: "https://wrapped.wolfai.dev",
|
|
1606
|
+
outputFormat: "normal",
|
|
1607
|
+
theme: {
|
|
1608
|
+
enabled: true
|
|
1609
|
+
},
|
|
1610
|
+
hideCost: false
|
|
1611
|
+
};
|
|
1612
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
1613
|
+
console.log(`Created config file at ${CONFIG_PATH}`);
|
|
1614
|
+
}
|
|
1615
|
+
function resolveOptions(cliArgs, config) {
|
|
1616
|
+
let outputFormat = config.outputFormat || "normal";
|
|
1617
|
+
if (cliArgs.json) outputFormat = "json";
|
|
1618
|
+
if (cliArgs.quiet) outputFormat = "quiet";
|
|
1619
|
+
return {
|
|
1620
|
+
baseUrl: cliArgs.url || config.baseUrl || DEFAULT_CONFIG.baseUrl,
|
|
1621
|
+
outputFormat,
|
|
1622
|
+
theme: {
|
|
1623
|
+
enabled: config.theme?.enabled ?? true
|
|
1624
|
+
},
|
|
1625
|
+
statsCachePath: config.statsCachePath,
|
|
1626
|
+
hideCost: config.hideCost ?? false
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/index.ts
|
|
1631
|
+
var main = defineCommand({
|
|
1632
|
+
meta: {
|
|
1633
|
+
name: "vibestats",
|
|
1634
|
+
version: "1.0.0",
|
|
1635
|
+
description: "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex"
|
|
1636
|
+
},
|
|
1637
|
+
args: {
|
|
1638
|
+
// Mode selection (wrapped is opt-in, usage is default)
|
|
1639
|
+
wrapped: {
|
|
1640
|
+
type: "boolean",
|
|
1641
|
+
alias: "w",
|
|
1642
|
+
description: "Show annual wrapped summary instead of usage stats",
|
|
1643
|
+
default: false
|
|
1644
|
+
},
|
|
1645
|
+
// Data source
|
|
1646
|
+
codex: {
|
|
1647
|
+
type: "boolean",
|
|
1648
|
+
description: "Show only OpenAI Codex CLI stats",
|
|
1649
|
+
default: false
|
|
1650
|
+
},
|
|
1651
|
+
combined: {
|
|
1652
|
+
type: "boolean",
|
|
1653
|
+
description: "Show combined Claude Code + Codex stats",
|
|
1654
|
+
default: false
|
|
1655
|
+
},
|
|
1656
|
+
// Output format
|
|
1657
|
+
json: {
|
|
1658
|
+
type: "boolean",
|
|
1659
|
+
description: "Output raw JSON stats",
|
|
1660
|
+
default: false
|
|
1661
|
+
},
|
|
1662
|
+
quiet: {
|
|
1663
|
+
type: "boolean",
|
|
1664
|
+
alias: "q",
|
|
1665
|
+
description: "Only output the shareable URL (wrapped mode)",
|
|
1666
|
+
default: false
|
|
1667
|
+
},
|
|
1668
|
+
// Usage-specific options
|
|
1669
|
+
daily: {
|
|
1670
|
+
type: "boolean",
|
|
1671
|
+
alias: "d",
|
|
1672
|
+
description: "Aggregate by day (default for usage)",
|
|
1673
|
+
default: false
|
|
1674
|
+
},
|
|
1675
|
+
monthly: {
|
|
1676
|
+
type: "boolean",
|
|
1677
|
+
alias: "m",
|
|
1678
|
+
description: "Show monthly aggregation",
|
|
1679
|
+
default: false
|
|
1680
|
+
},
|
|
1681
|
+
model: {
|
|
1682
|
+
type: "boolean",
|
|
1683
|
+
description: "Aggregate by model",
|
|
1684
|
+
default: false
|
|
1685
|
+
},
|
|
1686
|
+
total: {
|
|
1687
|
+
type: "boolean",
|
|
1688
|
+
alias: "t",
|
|
1689
|
+
description: "Show only totals",
|
|
1690
|
+
default: false
|
|
1691
|
+
},
|
|
1692
|
+
since: {
|
|
1693
|
+
type: "string",
|
|
1694
|
+
description: "Start date for filtering (YYYY-MM-DD)"
|
|
1695
|
+
},
|
|
1696
|
+
until: {
|
|
1697
|
+
type: "string",
|
|
1698
|
+
description: "End date for filtering (YYYY-MM-DD)"
|
|
1699
|
+
},
|
|
1700
|
+
compact: {
|
|
1701
|
+
type: "boolean",
|
|
1702
|
+
alias: "c",
|
|
1703
|
+
description: "Use compact table format (hide cache columns)",
|
|
1704
|
+
default: false
|
|
1705
|
+
},
|
|
1706
|
+
// Wrapped-specific options
|
|
1707
|
+
url: {
|
|
1708
|
+
type: "string",
|
|
1709
|
+
description: "Custom base URL for the wrapped page"
|
|
1710
|
+
},
|
|
1711
|
+
"no-short": {
|
|
1712
|
+
type: "boolean",
|
|
1713
|
+
description: "Disable shortlink generation",
|
|
1714
|
+
default: false
|
|
1715
|
+
},
|
|
1716
|
+
// Config management
|
|
1717
|
+
init: {
|
|
1718
|
+
type: "boolean",
|
|
1719
|
+
description: "Create a default config file",
|
|
1720
|
+
default: false
|
|
1721
|
+
},
|
|
1722
|
+
config: {
|
|
1723
|
+
type: "boolean",
|
|
1724
|
+
description: "Show current config location and values",
|
|
1725
|
+
default: false
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
async run({ args }) {
|
|
1729
|
+
if (args.init) {
|
|
1730
|
+
initConfig();
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const config = loadConfig();
|
|
1734
|
+
if (args.config) {
|
|
1735
|
+
console.log(`Config file: ${CONFIG_PATH}`);
|
|
1736
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (args.wrapped) {
|
|
1740
|
+
await runWrapped(args, config);
|
|
1741
|
+
} else {
|
|
1742
|
+
await runUsage(args, config);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
async function runUsage(args, config) {
|
|
1747
|
+
let aggregation = "daily";
|
|
1748
|
+
if (args.monthly) aggregation = "monthly";
|
|
1749
|
+
else if (args.model) aggregation = "model";
|
|
1750
|
+
else if (args.total) aggregation = "total";
|
|
1751
|
+
const stats = loadUsageStats({
|
|
1752
|
+
aggregation,
|
|
1753
|
+
since: args.since,
|
|
1754
|
+
until: args.until,
|
|
1755
|
+
codexOnly: args.codex,
|
|
1756
|
+
combined: args.combined
|
|
1757
|
+
});
|
|
1758
|
+
if (!stats) {
|
|
1759
|
+
if (args.codex) {
|
|
1760
|
+
console.error("Error: OpenAI Codex data not found at ~/.codex");
|
|
1761
|
+
console.error("Make sure you have used the Codex CLI at least once.");
|
|
1762
|
+
} else if (args.combined) {
|
|
1763
|
+
console.error("Error: No usage data found");
|
|
1764
|
+
console.error("Make sure you have used Claude Code or Codex CLI at least once.");
|
|
1765
|
+
} else {
|
|
1766
|
+
console.error("Error: Claude Code data not found at ~/.claude");
|
|
1767
|
+
console.error("Make sure you have used Claude Code at least once.");
|
|
1768
|
+
}
|
|
1769
|
+
process.exit(1);
|
|
1770
|
+
}
|
|
1771
|
+
if (args.json) {
|
|
1772
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
1773
|
+
} else if (args.quiet) {
|
|
1774
|
+
const t = stats.totals;
|
|
1775
|
+
const costStr = config.hideCost ? "" : ` | $${t.cost.toFixed(2)}`;
|
|
1776
|
+
console.log(`Total: ${formatNumber2(t.totalTokens)} tokens${costStr}`);
|
|
1777
|
+
} else if (aggregation === "total") {
|
|
1778
|
+
displayTotalOnly(stats, {
|
|
1779
|
+
hideCost: config.hideCost,
|
|
1780
|
+
showColors: config.theme?.enabled !== false
|
|
1781
|
+
});
|
|
1782
|
+
} else {
|
|
1783
|
+
displayUsageTable(stats, {
|
|
1784
|
+
compact: args.compact,
|
|
1785
|
+
hideCost: config.hideCost,
|
|
1786
|
+
showColors: config.theme?.enabled !== false
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
async function runWrapped(args, config) {
|
|
1791
|
+
const options = resolveOptions(args, config);
|
|
1792
|
+
const data = loadData({ codexOnly: args.codex, combined: args.combined });
|
|
1793
|
+
validateData(data, { codexOnly: args.codex, combined: args.combined });
|
|
1794
|
+
let claudeStats = null;
|
|
1795
|
+
let codexStats = null;
|
|
1796
|
+
if (data.claude) {
|
|
1797
|
+
claudeStats = computeWrappedStats(data.claude);
|
|
1798
|
+
}
|
|
1799
|
+
if (data.codex) {
|
|
1800
|
+
codexStats = computeCodexWrappedStats(data.codex);
|
|
1801
|
+
}
|
|
1802
|
+
let stats;
|
|
1803
|
+
if (args.codex && codexStats) {
|
|
1804
|
+
stats = codexStats;
|
|
1805
|
+
} else if (args.combined) {
|
|
1806
|
+
stats = combineWrappedStats(claudeStats, codexStats);
|
|
1807
|
+
} else {
|
|
1808
|
+
stats = claudeStats;
|
|
1809
|
+
}
|
|
1810
|
+
const url = encodeStatsToUrl(stats, options.baseUrl);
|
|
1811
|
+
let shortUrl = null;
|
|
1812
|
+
if (!args["no-short"]) {
|
|
1813
|
+
shortUrl = await createShortlink(url, options.baseUrl);
|
|
1814
|
+
}
|
|
1815
|
+
if (options.outputFormat === "json") {
|
|
1816
|
+
console.log(JSON.stringify({ ...stats, url, shortUrl }, null, 2));
|
|
1817
|
+
} else if (options.outputFormat === "quiet") {
|
|
1818
|
+
console.log(shortUrl || url);
|
|
1819
|
+
} else {
|
|
1820
|
+
displayWrappedStats(stats, url, {
|
|
1821
|
+
theme: options.theme,
|
|
1822
|
+
hideCost: options.hideCost,
|
|
1823
|
+
shortUrl
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
function formatNumber2(n) {
|
|
1828
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
|
|
1829
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
1830
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
1831
|
+
return n.toString();
|
|
1832
|
+
}
|
|
1833
|
+
runMain(main);
|