@sleekdesign/ccw25 0.0.1
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/dist/cli.js +2037 -0
- package/dist/index.d.ts +970 -0
- package/dist/index.js +2117 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2037 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from 'citty';
|
|
3
|
+
import { existsSync, readdirSync, statSync, createReadStream } from 'fs';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join, dirname, basename } from 'path';
|
|
7
|
+
|
|
8
|
+
function validateEntry(obj) {
|
|
9
|
+
if (typeof obj !== "object" || obj === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const entry = obj;
|
|
13
|
+
if (typeof entry.type !== "string") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
switch (entry.type) {
|
|
17
|
+
case "user":
|
|
18
|
+
case "assistant":
|
|
19
|
+
case "summary":
|
|
20
|
+
case "file-history-snapshot":
|
|
21
|
+
return true;
|
|
22
|
+
default:
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function parseLine(line, lineNumber) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (trimmed === "") {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: "Empty line",
|
|
32
|
+
line: lineNumber
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(trimmed);
|
|
37
|
+
if (!validateEntry(parsed)) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: "Invalid entry structure",
|
|
41
|
+
line: lineNumber
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
entry: parsed
|
|
47
|
+
};
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: e instanceof Error ? e.message : "Parse error",
|
|
52
|
+
line: lineNumber
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function* parseJSONLFile(filePath) {
|
|
57
|
+
const stats = {
|
|
58
|
+
totalLines: 0,
|
|
59
|
+
successfulLines: 0,
|
|
60
|
+
failedLines: 0,
|
|
61
|
+
errors: []
|
|
62
|
+
};
|
|
63
|
+
const fileStream = createReadStream(filePath, { encoding: "utf-8" });
|
|
64
|
+
const rl = createInterface({
|
|
65
|
+
input: fileStream,
|
|
66
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
67
|
+
});
|
|
68
|
+
let lineNumber = 0;
|
|
69
|
+
for await (const line of rl) {
|
|
70
|
+
lineNumber += 1;
|
|
71
|
+
stats.totalLines += 1;
|
|
72
|
+
const result = parseLine(line, lineNumber);
|
|
73
|
+
if (result.success) {
|
|
74
|
+
stats.successfulLines += 1;
|
|
75
|
+
yield result.entry;
|
|
76
|
+
} else {
|
|
77
|
+
stats.failedLines += 1;
|
|
78
|
+
if (result.error !== "Empty line") {
|
|
79
|
+
stats.errors.push({
|
|
80
|
+
line: result.line,
|
|
81
|
+
error: result.error
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return stats;
|
|
87
|
+
}
|
|
88
|
+
async function parseJSONLFileAll(filePath) {
|
|
89
|
+
const entries = [];
|
|
90
|
+
let finalStats = {
|
|
91
|
+
totalLines: 0,
|
|
92
|
+
successfulLines: 0,
|
|
93
|
+
failedLines: 0,
|
|
94
|
+
errors: []
|
|
95
|
+
};
|
|
96
|
+
const generator = parseJSONLFile(filePath);
|
|
97
|
+
while (true) {
|
|
98
|
+
const result = await generator.next();
|
|
99
|
+
if (result.done) {
|
|
100
|
+
finalStats = result.value;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
entries.push(result.value);
|
|
104
|
+
}
|
|
105
|
+
return { entries, stats: finalStats };
|
|
106
|
+
}
|
|
107
|
+
async function parseStatsCache(filePath) {
|
|
108
|
+
try {
|
|
109
|
+
const { readFile } = await import('fs/promises');
|
|
110
|
+
const content = await readFile(filePath, "utf-8");
|
|
111
|
+
return JSON.parse(content);
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
var DEFAULT_CLAUDE_DIR = ".claude";
|
|
117
|
+
var PROJECTS_DIR = "projects";
|
|
118
|
+
var HISTORY_FILE = "history.jsonl";
|
|
119
|
+
var STATS_CACHE_FILE = "stats-cache.json";
|
|
120
|
+
function detectClaudeDir() {
|
|
121
|
+
const envDir = process.env.CLAUDE_CONFIG_DIR;
|
|
122
|
+
if (envDir && existsSync(envDir)) {
|
|
123
|
+
return envDir;
|
|
124
|
+
}
|
|
125
|
+
const home = homedir();
|
|
126
|
+
return join(home, DEFAULT_CLAUDE_DIR);
|
|
127
|
+
}
|
|
128
|
+
function getClaudePaths(baseDir) {
|
|
129
|
+
const resolvedBase = baseDir ?? detectClaudeDir();
|
|
130
|
+
const paths = {
|
|
131
|
+
baseDir: resolvedBase,
|
|
132
|
+
projectsDir: join(resolvedBase, PROJECTS_DIR),
|
|
133
|
+
historyFile: join(resolvedBase, HISTORY_FILE),
|
|
134
|
+
statsCacheFile: join(resolvedBase, STATS_CACHE_FILE),
|
|
135
|
+
exists: existsSync(resolvedBase)
|
|
136
|
+
};
|
|
137
|
+
return paths;
|
|
138
|
+
}
|
|
139
|
+
function parseProjectName(dirName) {
|
|
140
|
+
const cleaned = dirName.startsWith("-") ? dirName.slice(1) : dirName;
|
|
141
|
+
const parts = cleaned.split("-").filter(Boolean);
|
|
142
|
+
if (parts.length === 0) {
|
|
143
|
+
return "Unknown Project";
|
|
144
|
+
}
|
|
145
|
+
const lastParts = parts.slice(-2);
|
|
146
|
+
const commonParents = ["projects", "repos", "code", "dev", "src", "work"];
|
|
147
|
+
const firstPart = lastParts[0];
|
|
148
|
+
const secondPart = lastParts[1];
|
|
149
|
+
if (lastParts.length === 2 && firstPart && secondPart && commonParents.includes(firstPart.toLowerCase())) {
|
|
150
|
+
return secondPart;
|
|
151
|
+
}
|
|
152
|
+
return lastParts.join("/");
|
|
153
|
+
}
|
|
154
|
+
function discoverProjects(projectsDir) {
|
|
155
|
+
if (!existsSync(projectsDir)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const projects = [];
|
|
159
|
+
try {
|
|
160
|
+
const entries = readdirSync(projectsDir, { withFileTypes: true });
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (!entry.isDirectory()) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const projectPath = join(projectsDir, entry.name);
|
|
166
|
+
const sessionFiles = discoverSessionFiles(projectPath);
|
|
167
|
+
if (sessionFiles.length > 0) {
|
|
168
|
+
projects.push({
|
|
169
|
+
path: projectPath,
|
|
170
|
+
displayName: parseProjectName(entry.name),
|
|
171
|
+
dirName: entry.name,
|
|
172
|
+
sessionFiles
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
return projects;
|
|
179
|
+
}
|
|
180
|
+
function discoverSessionFiles(projectPath) {
|
|
181
|
+
const files = [];
|
|
182
|
+
try {
|
|
183
|
+
const entries = readdirSync(projectPath, { withFileTypes: true });
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
186
|
+
files.push(join(projectPath, entry.name));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
return files;
|
|
192
|
+
}
|
|
193
|
+
function extractSessionId(filePath) {
|
|
194
|
+
const fileName = basename(filePath, ".jsonl");
|
|
195
|
+
return fileName;
|
|
196
|
+
}
|
|
197
|
+
function extractProjectPath(filePath) {
|
|
198
|
+
const projectDir = dirname(filePath);
|
|
199
|
+
return basename(projectDir);
|
|
200
|
+
}
|
|
201
|
+
function getFileModTime(filePath) {
|
|
202
|
+
try {
|
|
203
|
+
const stats = statSync(filePath);
|
|
204
|
+
return stats.mtime;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function filterFilesByDate(files, from, to) {
|
|
210
|
+
return files.filter((file) => {
|
|
211
|
+
const modTime = getFileModTime(file);
|
|
212
|
+
if (!modTime) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return modTime >= from && modTime <= to;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/loader/iterator.ts
|
|
220
|
+
function enumerateSessions(options = {}) {
|
|
221
|
+
const paths = getClaudePaths(options.baseDir);
|
|
222
|
+
if (!paths.exists) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
const projects = discoverProjects(paths.projectsDir);
|
|
226
|
+
const sessions = [];
|
|
227
|
+
for (const project of projects) {
|
|
228
|
+
if (options.projectFilter) {
|
|
229
|
+
const matchesFilter = options.projectFilter.some(
|
|
230
|
+
(filter) => project.displayName.toLowerCase().includes(filter.toLowerCase()) || project.dirName.toLowerCase().includes(filter.toLowerCase())
|
|
231
|
+
);
|
|
232
|
+
if (!matchesFilter) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
let files = project.sessionFiles;
|
|
237
|
+
if (options.dateRange) {
|
|
238
|
+
files = filterFilesByDate(
|
|
239
|
+
files,
|
|
240
|
+
options.dateRange.from,
|
|
241
|
+
options.dateRange.to
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
for (const filePath of files) {
|
|
245
|
+
sessions.push({
|
|
246
|
+
sessionId: extractSessionId(filePath),
|
|
247
|
+
projectPath: extractProjectPath(filePath),
|
|
248
|
+
projectDisplayName: project.displayName,
|
|
249
|
+
filePath
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return sessions;
|
|
254
|
+
}
|
|
255
|
+
async function loadSession(metadata) {
|
|
256
|
+
const { entries } = await parseJSONLFileAll(metadata.filePath);
|
|
257
|
+
return {
|
|
258
|
+
sessionId: metadata.sessionId,
|
|
259
|
+
projectPath: metadata.projectPath,
|
|
260
|
+
filePath: metadata.filePath,
|
|
261
|
+
entries
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function* iterateSessions(options = {}) {
|
|
265
|
+
const sessionList = enumerateSessions(options);
|
|
266
|
+
for (const metadata of sessionList) {
|
|
267
|
+
try {
|
|
268
|
+
const session = await loadSession(metadata);
|
|
269
|
+
if (session.entries.length > 0) {
|
|
270
|
+
yield session;
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function collectAllEntries(options = {}) {
|
|
277
|
+
const entries = [];
|
|
278
|
+
let sessionCount = 0;
|
|
279
|
+
for await (const session of iterateSessions(options)) {
|
|
280
|
+
sessionCount += 1;
|
|
281
|
+
for (const entry of session.entries) {
|
|
282
|
+
entries.push({
|
|
283
|
+
...entry,
|
|
284
|
+
sessionId: session.sessionId,
|
|
285
|
+
projectPath: session.projectPath
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { entries, sessionCount };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/loader/index.ts
|
|
293
|
+
async function loadClaudeData(config) {
|
|
294
|
+
const paths = getClaudePaths(config.baseDir);
|
|
295
|
+
let statsCache = null;
|
|
296
|
+
if (config.useStatsCache) {
|
|
297
|
+
const cache = await parseStatsCache(paths.statsCacheFile);
|
|
298
|
+
if (cache && typeof cache.version === "number") {
|
|
299
|
+
statsCache = cache;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const { entries } = await collectAllEntries({
|
|
303
|
+
baseDir: config.baseDir,
|
|
304
|
+
dateRange: config.dateRange,
|
|
305
|
+
projectFilter: config.projectFilter
|
|
306
|
+
});
|
|
307
|
+
const sessionMetadataList = enumerateSessions({
|
|
308
|
+
baseDir: config.baseDir,
|
|
309
|
+
dateRange: config.dateRange,
|
|
310
|
+
projectFilter: config.projectFilter
|
|
311
|
+
});
|
|
312
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const existing = sessionMap.get(entry.sessionId);
|
|
315
|
+
if (existing) {
|
|
316
|
+
existing.entries.push(entry);
|
|
317
|
+
} else {
|
|
318
|
+
const meta = sessionMetadataList.find(
|
|
319
|
+
(m) => m.sessionId === entry.sessionId
|
|
320
|
+
);
|
|
321
|
+
sessionMap.set(entry.sessionId, {
|
|
322
|
+
sessionId: entry.sessionId,
|
|
323
|
+
projectPath: entry.projectPath,
|
|
324
|
+
filePath: meta?.filePath ?? "",
|
|
325
|
+
entries: [entry]
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const sessions = Array.from(sessionMap.values());
|
|
330
|
+
return {
|
|
331
|
+
sessions,
|
|
332
|
+
entries,
|
|
333
|
+
statsCache,
|
|
334
|
+
config
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function resolveConfig(config = {}) {
|
|
338
|
+
const now = /* @__PURE__ */ new Date();
|
|
339
|
+
const year = config.year ?? now.getFullYear();
|
|
340
|
+
const defaultFrom = new Date(year, 0, 1);
|
|
341
|
+
const defaultTo = new Date(year, 11, 31, 23, 59, 59, 999);
|
|
342
|
+
return {
|
|
343
|
+
baseDir: config.baseDir ?? getClaudePaths().baseDir,
|
|
344
|
+
year,
|
|
345
|
+
dateRange: config.dateRange ?? {
|
|
346
|
+
from: defaultFrom,
|
|
347
|
+
to: defaultTo
|
|
348
|
+
},
|
|
349
|
+
projectFilter: config.projectFilter ?? null,
|
|
350
|
+
useStatsCache: config.useStatsCache ?? true
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/pricing/models.ts
|
|
355
|
+
var MODEL_PRICING = {
|
|
356
|
+
// Claude 4.5 family
|
|
357
|
+
"claude-opus-4-5-20251101": {
|
|
358
|
+
input: 15,
|
|
359
|
+
output: 75,
|
|
360
|
+
cacheWrite: 18.75,
|
|
361
|
+
cacheRead: 1.5
|
|
362
|
+
},
|
|
363
|
+
"claude-sonnet-4-5-20250929": {
|
|
364
|
+
input: 3,
|
|
365
|
+
output: 15,
|
|
366
|
+
cacheWrite: 3.75,
|
|
367
|
+
cacheRead: 0.3
|
|
368
|
+
},
|
|
369
|
+
"claude-haiku-4-5-20251015": {
|
|
370
|
+
input: 1,
|
|
371
|
+
output: 5,
|
|
372
|
+
cacheWrite: 1.25,
|
|
373
|
+
cacheRead: 0.1
|
|
374
|
+
},
|
|
375
|
+
// Claude 4.1 family
|
|
376
|
+
"claude-opus-4-1-20250805": {
|
|
377
|
+
input: 15,
|
|
378
|
+
output: 75,
|
|
379
|
+
cacheWrite: 18.75,
|
|
380
|
+
cacheRead: 1.5
|
|
381
|
+
},
|
|
382
|
+
// Claude 4 family
|
|
383
|
+
"claude-opus-4-20250514": {
|
|
384
|
+
input: 15,
|
|
385
|
+
output: 75,
|
|
386
|
+
cacheWrite: 18.75,
|
|
387
|
+
cacheRead: 1.5
|
|
388
|
+
},
|
|
389
|
+
"claude-sonnet-4-20250514": {
|
|
390
|
+
input: 3,
|
|
391
|
+
output: 15,
|
|
392
|
+
cacheWrite: 3.75,
|
|
393
|
+
cacheRead: 0.3
|
|
394
|
+
},
|
|
395
|
+
// Claude 3.7 Sonnet (hybrid reasoning)
|
|
396
|
+
"claude-3-7-sonnet-20250219": {
|
|
397
|
+
input: 3,
|
|
398
|
+
output: 15,
|
|
399
|
+
cacheWrite: 3.75,
|
|
400
|
+
cacheRead: 0.3
|
|
401
|
+
},
|
|
402
|
+
// Claude 3.5 family
|
|
403
|
+
"claude-3-5-sonnet-20241022": {
|
|
404
|
+
input: 3,
|
|
405
|
+
output: 15,
|
|
406
|
+
cacheWrite: 3.75,
|
|
407
|
+
cacheRead: 0.3
|
|
408
|
+
},
|
|
409
|
+
"claude-3-5-sonnet-20240620": {
|
|
410
|
+
input: 3,
|
|
411
|
+
output: 15,
|
|
412
|
+
cacheWrite: 3.75,
|
|
413
|
+
cacheRead: 0.3
|
|
414
|
+
},
|
|
415
|
+
"claude-3-5-haiku-20241022": {
|
|
416
|
+
input: 0.8,
|
|
417
|
+
output: 4,
|
|
418
|
+
cacheWrite: 1,
|
|
419
|
+
cacheRead: 0.08
|
|
420
|
+
},
|
|
421
|
+
// Claude 3 family
|
|
422
|
+
"claude-3-opus-20240229": {
|
|
423
|
+
input: 15,
|
|
424
|
+
output: 75,
|
|
425
|
+
cacheWrite: 18.75,
|
|
426
|
+
cacheRead: 1.5
|
|
427
|
+
},
|
|
428
|
+
"claude-3-sonnet-20240229": {
|
|
429
|
+
input: 3,
|
|
430
|
+
output: 15,
|
|
431
|
+
cacheWrite: 3.75,
|
|
432
|
+
cacheRead: 0.3
|
|
433
|
+
},
|
|
434
|
+
"claude-3-haiku-20240307": {
|
|
435
|
+
input: 0.25,
|
|
436
|
+
output: 1.25,
|
|
437
|
+
cacheWrite: 0.3,
|
|
438
|
+
cacheRead: 0.03
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
var DEFAULT_PRICING = {
|
|
442
|
+
input: 3,
|
|
443
|
+
output: 15,
|
|
444
|
+
cacheWrite: 3.75,
|
|
445
|
+
cacheRead: 0.3
|
|
446
|
+
};
|
|
447
|
+
var PRICING_PATTERNS = [
|
|
448
|
+
// Opus
|
|
449
|
+
{
|
|
450
|
+
test: (s) => s.includes("opus") && (s.includes("4-5") || s.includes("4.5")),
|
|
451
|
+
key: "claude-opus-4-5-20251101"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
test: (s) => s.includes("opus") && (s.includes("4-1") || s.includes("4.1")),
|
|
455
|
+
key: "claude-opus-4-1-20250805"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
test: (s) => s.includes("opus") && s.includes("4"),
|
|
459
|
+
key: "claude-opus-4-20250514"
|
|
460
|
+
},
|
|
461
|
+
{ test: (s) => s.includes("opus"), key: "claude-3-opus-20240229" },
|
|
462
|
+
// Haiku
|
|
463
|
+
{
|
|
464
|
+
test: (s) => s.includes("haiku") && (s.includes("4-5") || s.includes("4.5")),
|
|
465
|
+
key: "claude-haiku-4-5-20251015"
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
test: (s) => s.includes("haiku") && (s.includes("3-5") || s.includes("3.5")),
|
|
469
|
+
key: "claude-3-5-haiku-20241022"
|
|
470
|
+
},
|
|
471
|
+
{ test: (s) => s.includes("haiku"), key: "claude-3-haiku-20240307" },
|
|
472
|
+
// Sonnet
|
|
473
|
+
{
|
|
474
|
+
test: (s) => s.includes("sonnet") && (s.includes("4-5") || s.includes("4.5")),
|
|
475
|
+
key: "claude-sonnet-4-5-20250929"
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
test: (s) => s.includes("sonnet") && s.includes("4"),
|
|
479
|
+
key: "claude-sonnet-4-20250514"
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
test: (s) => s.includes("3-7") || s.includes("3.7"),
|
|
483
|
+
key: "claude-3-7-sonnet-20250219"
|
|
484
|
+
},
|
|
485
|
+
{ test: (s) => s.includes("sonnet"), key: "claude-3-5-sonnet-20241022" }
|
|
486
|
+
];
|
|
487
|
+
var DISPLAY_PATTERNS = [
|
|
488
|
+
// Opus
|
|
489
|
+
{
|
|
490
|
+
test: (s) => s.includes("opus") && (s.includes("4-5") || s.includes("4.5")),
|
|
491
|
+
name: "Claude Opus 4.5"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
test: (s) => s.includes("opus") && (s.includes("4-1") || s.includes("4.1")),
|
|
495
|
+
name: "Claude Opus 4.1"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
test: (s) => s.includes("opus") && s.includes("4"),
|
|
499
|
+
name: "Claude Opus 4"
|
|
500
|
+
},
|
|
501
|
+
{ test: (s) => s.includes("opus"), name: "Claude Opus 3" },
|
|
502
|
+
// Sonnet
|
|
503
|
+
{
|
|
504
|
+
test: (s) => s.includes("sonnet") && (s.includes("4-5") || s.includes("4.5")),
|
|
505
|
+
name: "Claude Sonnet 4.5"
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
test: (s) => s.includes("sonnet") && s.includes("4"),
|
|
509
|
+
name: "Claude Sonnet 4"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
test: (s) => s.includes("3-7") || s.includes("3.7"),
|
|
513
|
+
name: "Claude Sonnet 3.7"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
test: (s) => s.includes("3-5") && s.includes("sonnet"),
|
|
517
|
+
name: "Claude Sonnet 3.5"
|
|
518
|
+
},
|
|
519
|
+
{ test: (s) => s.includes("sonnet"), name: "Claude Sonnet" },
|
|
520
|
+
// Haiku
|
|
521
|
+
{
|
|
522
|
+
test: (s) => s.includes("haiku") && (s.includes("4-5") || s.includes("4.5")),
|
|
523
|
+
name: "Claude Haiku 4.5"
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
test: (s) => s.includes("3-5") && s.includes("haiku"),
|
|
527
|
+
name: "Claude Haiku 3.5"
|
|
528
|
+
},
|
|
529
|
+
{ test: (s) => s.includes("haiku"), name: "Claude Haiku" }
|
|
530
|
+
];
|
|
531
|
+
function getModelPricing(model) {
|
|
532
|
+
const exactMatch = MODEL_PRICING[model];
|
|
533
|
+
if (exactMatch) {
|
|
534
|
+
return exactMatch;
|
|
535
|
+
}
|
|
536
|
+
const lower = model.toLowerCase();
|
|
537
|
+
for (const pattern of PRICING_PATTERNS) {
|
|
538
|
+
if (pattern.test(lower)) {
|
|
539
|
+
return MODEL_PRICING[pattern.key] ?? DEFAULT_PRICING;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return DEFAULT_PRICING;
|
|
543
|
+
}
|
|
544
|
+
function getModelDisplayName(model) {
|
|
545
|
+
const lower = model.toLowerCase();
|
|
546
|
+
for (const pattern of DISPLAY_PATTERNS) {
|
|
547
|
+
if (pattern.test(lower)) {
|
|
548
|
+
return pattern.name;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return model;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/pricing/index.ts
|
|
555
|
+
function calculateCost(model, usage) {
|
|
556
|
+
const pricing = getModelPricing(model);
|
|
557
|
+
const inputCost = usage.input_tokens * pricing.input / 1e6;
|
|
558
|
+
const outputCost = usage.output_tokens * pricing.output / 1e6;
|
|
559
|
+
const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * pricing.cacheWrite / 1e6;
|
|
560
|
+
const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * pricing.cacheRead / 1e6;
|
|
561
|
+
return {
|
|
562
|
+
input: inputCost,
|
|
563
|
+
output: outputCost,
|
|
564
|
+
cacheCreation: cacheCreationCost,
|
|
565
|
+
cacheRead: cacheReadCost,
|
|
566
|
+
total: inputCost + outputCost + cacheCreationCost + cacheReadCost
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function aggregateCosts(costs) {
|
|
570
|
+
return costs.reduce(
|
|
571
|
+
(acc, cost) => ({
|
|
572
|
+
input: acc.input + cost.input,
|
|
573
|
+
output: acc.output + cost.output,
|
|
574
|
+
cacheCreation: acc.cacheCreation + cost.cacheCreation,
|
|
575
|
+
cacheRead: acc.cacheRead + cost.cacheRead,
|
|
576
|
+
total: acc.total + cost.total
|
|
577
|
+
}),
|
|
578
|
+
{ input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0 }
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
function estimateCacheSavings(model, cacheReadTokens) {
|
|
582
|
+
const pricing = getModelPricing(model);
|
|
583
|
+
const withoutCache = cacheReadTokens * pricing.input / 1e6;
|
|
584
|
+
const withCache = cacheReadTokens * pricing.cacheRead / 1e6;
|
|
585
|
+
return withoutCache - withCache;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/types/jsonl.ts
|
|
589
|
+
function isUserMessage(entry) {
|
|
590
|
+
return entry.type === "user";
|
|
591
|
+
}
|
|
592
|
+
function isAssistantMessage(entry) {
|
|
593
|
+
return entry.type === "assistant";
|
|
594
|
+
}
|
|
595
|
+
function isSummaryEntry(entry) {
|
|
596
|
+
return entry.type === "summary";
|
|
597
|
+
}
|
|
598
|
+
function isToolUseContent(content) {
|
|
599
|
+
return content.type === "tool_use";
|
|
600
|
+
}
|
|
601
|
+
function isTextContent(content) {
|
|
602
|
+
return content.type === "text";
|
|
603
|
+
}
|
|
604
|
+
function isThinkingContent(content) {
|
|
605
|
+
return content.type === "thinking";
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/utils/aggregators.ts
|
|
609
|
+
function getTopN(counts, n) {
|
|
610
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).slice(0, n).map(([key, count]) => ({ key, count }));
|
|
611
|
+
}
|
|
612
|
+
function findMax(counts) {
|
|
613
|
+
let maxKey = null;
|
|
614
|
+
let maxCount = 0;
|
|
615
|
+
for (const [key, count] of counts) {
|
|
616
|
+
if (count > maxCount) {
|
|
617
|
+
maxKey = key;
|
|
618
|
+
maxCount = count;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (maxKey === null) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
return { key: maxKey, count: maxCount };
|
|
625
|
+
}
|
|
626
|
+
function calculatePercentage(part, total) {
|
|
627
|
+
if (total === 0) {
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
return part / total * 100;
|
|
631
|
+
}
|
|
632
|
+
function initHourlyDistribution() {
|
|
633
|
+
const map = /* @__PURE__ */ new Map();
|
|
634
|
+
for (let i = 0; i < 24; i++) {
|
|
635
|
+
map.set(i, 0);
|
|
636
|
+
}
|
|
637
|
+
return map;
|
|
638
|
+
}
|
|
639
|
+
function initWeeklyDistribution() {
|
|
640
|
+
const map = /* @__PURE__ */ new Map();
|
|
641
|
+
for (let i = 0; i < 7; i++) {
|
|
642
|
+
map.set(i, 0);
|
|
643
|
+
}
|
|
644
|
+
return map;
|
|
645
|
+
}
|
|
646
|
+
function initMonthlyDistribution() {
|
|
647
|
+
const map = /* @__PURE__ */ new Map();
|
|
648
|
+
for (let i = 0; i < 12; i++) {
|
|
649
|
+
map.set(i, 0);
|
|
650
|
+
}
|
|
651
|
+
return map;
|
|
652
|
+
}
|
|
653
|
+
function incrementMap(map, key) {
|
|
654
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
655
|
+
}
|
|
656
|
+
function extractWords(text) {
|
|
657
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
658
|
+
"a",
|
|
659
|
+
"an",
|
|
660
|
+
"the",
|
|
661
|
+
"and",
|
|
662
|
+
"or",
|
|
663
|
+
"but",
|
|
664
|
+
"in",
|
|
665
|
+
"on",
|
|
666
|
+
"at",
|
|
667
|
+
"to",
|
|
668
|
+
"for",
|
|
669
|
+
"of",
|
|
670
|
+
"with",
|
|
671
|
+
"by",
|
|
672
|
+
"from",
|
|
673
|
+
"as",
|
|
674
|
+
"is",
|
|
675
|
+
"was",
|
|
676
|
+
"are",
|
|
677
|
+
"were",
|
|
678
|
+
"been",
|
|
679
|
+
"be",
|
|
680
|
+
"have",
|
|
681
|
+
"has",
|
|
682
|
+
"had",
|
|
683
|
+
"do",
|
|
684
|
+
"does",
|
|
685
|
+
"did",
|
|
686
|
+
"will",
|
|
687
|
+
"would",
|
|
688
|
+
"could",
|
|
689
|
+
"should",
|
|
690
|
+
"may",
|
|
691
|
+
"might",
|
|
692
|
+
"must",
|
|
693
|
+
"shall",
|
|
694
|
+
"can",
|
|
695
|
+
"need",
|
|
696
|
+
"this",
|
|
697
|
+
"that",
|
|
698
|
+
"these",
|
|
699
|
+
"those",
|
|
700
|
+
"i",
|
|
701
|
+
"you",
|
|
702
|
+
"he",
|
|
703
|
+
"she",
|
|
704
|
+
"it",
|
|
705
|
+
"we",
|
|
706
|
+
"they",
|
|
707
|
+
"what",
|
|
708
|
+
"which",
|
|
709
|
+
"who",
|
|
710
|
+
"whom",
|
|
711
|
+
"whose",
|
|
712
|
+
"where",
|
|
713
|
+
"when",
|
|
714
|
+
"why",
|
|
715
|
+
"how",
|
|
716
|
+
"all",
|
|
717
|
+
"each",
|
|
718
|
+
"every",
|
|
719
|
+
"both",
|
|
720
|
+
"few",
|
|
721
|
+
"more",
|
|
722
|
+
"most",
|
|
723
|
+
"other",
|
|
724
|
+
"some",
|
|
725
|
+
"such",
|
|
726
|
+
"no",
|
|
727
|
+
"nor",
|
|
728
|
+
"not",
|
|
729
|
+
"only",
|
|
730
|
+
"own",
|
|
731
|
+
"same",
|
|
732
|
+
"so",
|
|
733
|
+
"than",
|
|
734
|
+
"too",
|
|
735
|
+
"very",
|
|
736
|
+
"just",
|
|
737
|
+
"also",
|
|
738
|
+
"now",
|
|
739
|
+
"here"
|
|
740
|
+
]);
|
|
741
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((word) => word.length > 3 && !stopWords.has(word));
|
|
742
|
+
}
|
|
743
|
+
function buildWordCloud(texts, limit = 50) {
|
|
744
|
+
const wordCounts = /* @__PURE__ */ new Map();
|
|
745
|
+
for (const text of texts) {
|
|
746
|
+
const words = extractWords(text);
|
|
747
|
+
for (const word of words) {
|
|
748
|
+
incrementMap(wordCounts, word);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return getTopN(wordCounts, limit).map(({ key, count }) => ({
|
|
752
|
+
word: key,
|
|
753
|
+
count
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/metrics/collaboration.ts
|
|
758
|
+
function createCollaborationAccumulator() {
|
|
759
|
+
return {
|
|
760
|
+
totalInputTokens: 0,
|
|
761
|
+
totalCacheReadTokens: 0,
|
|
762
|
+
totalCacheCreationTokens: 0,
|
|
763
|
+
messagesWithThinking: 0,
|
|
764
|
+
totalAssistantMessages: 0,
|
|
765
|
+
questionsAsked: 0,
|
|
766
|
+
plansCreated: 0,
|
|
767
|
+
todoItemsCreated: 0,
|
|
768
|
+
standardRequests: 0,
|
|
769
|
+
priorityRequests: 0,
|
|
770
|
+
cacheSavings: 0
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function processCollaborationEntry(acc, entry) {
|
|
774
|
+
if (!isAssistantMessage(entry)) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
acc.totalAssistantMessages += 1;
|
|
778
|
+
const usage = entry.message.usage;
|
|
779
|
+
const model = entry.message.model;
|
|
780
|
+
acc.totalInputTokens += usage.input_tokens;
|
|
781
|
+
acc.totalCacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
782
|
+
acc.totalCacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
783
|
+
if (usage.service_tier === "priority") {
|
|
784
|
+
acc.priorityRequests += 1;
|
|
785
|
+
} else {
|
|
786
|
+
acc.standardRequests += 1;
|
|
787
|
+
}
|
|
788
|
+
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
789
|
+
if (cacheReadTokens > 0) {
|
|
790
|
+
acc.cacheSavings += estimateCacheSavings(model, cacheReadTokens);
|
|
791
|
+
}
|
|
792
|
+
const hasThinking = entry.message.content.some(isThinkingContent);
|
|
793
|
+
if (hasThinking) {
|
|
794
|
+
acc.messagesWithThinking += 1;
|
|
795
|
+
}
|
|
796
|
+
for (const content of entry.message.content) {
|
|
797
|
+
if (!isToolUseContent(content)) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
switch (content.name) {
|
|
801
|
+
case "AskUserQuestion":
|
|
802
|
+
acc.questionsAsked += 1;
|
|
803
|
+
break;
|
|
804
|
+
case "ExitPlanMode":
|
|
805
|
+
acc.plansCreated += 1;
|
|
806
|
+
break;
|
|
807
|
+
case "TodoWrite":
|
|
808
|
+
acc.todoItemsCreated += 1;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function finalizeCollaborationMetrics(acc) {
|
|
814
|
+
const totalEffectiveInput = acc.totalInputTokens + acc.totalCacheReadTokens;
|
|
815
|
+
const cacheHitRate = totalEffectiveInput > 0 ? calculatePercentage(acc.totalCacheReadTokens, totalEffectiveInput) : 0;
|
|
816
|
+
const cacheCreationRate = totalEffectiveInput > 0 ? calculatePercentage(acc.totalCacheCreationTokens, totalEffectiveInput) : 0;
|
|
817
|
+
const thinkingPercentage = acc.totalAssistantMessages > 0 ? calculatePercentage(
|
|
818
|
+
acc.messagesWithThinking,
|
|
819
|
+
acc.totalAssistantMessages
|
|
820
|
+
) : 0;
|
|
821
|
+
const totalRequests = acc.standardRequests + acc.priorityRequests;
|
|
822
|
+
const priorityPercentage = totalRequests > 0 ? calculatePercentage(acc.priorityRequests, totalRequests) : 0;
|
|
823
|
+
return {
|
|
824
|
+
cacheEfficiency: {
|
|
825
|
+
cacheHitRate,
|
|
826
|
+
cacheCreationRate,
|
|
827
|
+
estimatedSavings: acc.cacheSavings
|
|
828
|
+
},
|
|
829
|
+
extendedThinking: {
|
|
830
|
+
messagesWithThinking: acc.messagesWithThinking,
|
|
831
|
+
percentageOfTotal: thinkingPercentage
|
|
832
|
+
},
|
|
833
|
+
interactivity: {
|
|
834
|
+
questionsAsked: acc.questionsAsked,
|
|
835
|
+
plansCreated: acc.plansCreated,
|
|
836
|
+
todoItemsCreated: acc.todoItemsCreated
|
|
837
|
+
},
|
|
838
|
+
serviceTier: {
|
|
839
|
+
standardRequests: acc.standardRequests,
|
|
840
|
+
priorityRequests: acc.priorityRequests,
|
|
841
|
+
priorityPercentage
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function calculateCollaborationMetrics(entries) {
|
|
846
|
+
const acc = createCollaborationAccumulator();
|
|
847
|
+
for (const entry of entries) {
|
|
848
|
+
processCollaborationEntry(acc, entry);
|
|
849
|
+
}
|
|
850
|
+
return finalizeCollaborationMetrics(acc);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/utils/dates.ts
|
|
854
|
+
var DAY_NAMES = [
|
|
855
|
+
"Sunday",
|
|
856
|
+
"Monday",
|
|
857
|
+
"Tuesday",
|
|
858
|
+
"Wednesday",
|
|
859
|
+
"Thursday",
|
|
860
|
+
"Friday",
|
|
861
|
+
"Saturday"
|
|
862
|
+
];
|
|
863
|
+
var MONTH_NAMES = [
|
|
864
|
+
"January",
|
|
865
|
+
"February",
|
|
866
|
+
"March",
|
|
867
|
+
"April",
|
|
868
|
+
"May",
|
|
869
|
+
"June",
|
|
870
|
+
"July",
|
|
871
|
+
"August",
|
|
872
|
+
"September",
|
|
873
|
+
"October",
|
|
874
|
+
"November",
|
|
875
|
+
"December"
|
|
876
|
+
];
|
|
877
|
+
function getDayName(day) {
|
|
878
|
+
return DAY_NAMES[day] ?? "Unknown";
|
|
879
|
+
}
|
|
880
|
+
function getMonthName(month) {
|
|
881
|
+
return MONTH_NAMES[month] ?? "Unknown";
|
|
882
|
+
}
|
|
883
|
+
function parseTimestamp(timestamp) {
|
|
884
|
+
try {
|
|
885
|
+
const date = new Date(timestamp);
|
|
886
|
+
if (Number.isNaN(date.getTime())) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
return date;
|
|
890
|
+
} catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function toDateString(date) {
|
|
895
|
+
const parts = date.toISOString().split("T");
|
|
896
|
+
return parts[0] ?? "";
|
|
897
|
+
}
|
|
898
|
+
function getHour(date) {
|
|
899
|
+
return date.getHours();
|
|
900
|
+
}
|
|
901
|
+
function getDayOfWeek(date) {
|
|
902
|
+
return date.getDay();
|
|
903
|
+
}
|
|
904
|
+
function getMonth(date) {
|
|
905
|
+
return date.getMonth();
|
|
906
|
+
}
|
|
907
|
+
function calculateStreak(sortedDates) {
|
|
908
|
+
if (sortedDates.length === 0) {
|
|
909
|
+
return { longestStreak: 0, currentStreak: 0 };
|
|
910
|
+
}
|
|
911
|
+
const uniqueDates = [...new Set(sortedDates)].sort();
|
|
912
|
+
let longestStreak = 1;
|
|
913
|
+
let currentStreak = 1;
|
|
914
|
+
let tempStreak = 1;
|
|
915
|
+
const today = toDateString(/* @__PURE__ */ new Date());
|
|
916
|
+
const yesterday = toDateString(new Date(Date.now() - 24 * 60 * 60 * 1e3));
|
|
917
|
+
for (let i = 1; i < uniqueDates.length; i++) {
|
|
918
|
+
const prevDateStr = uniqueDates[i - 1];
|
|
919
|
+
const currDateStr = uniqueDates[i];
|
|
920
|
+
if (!(prevDateStr && currDateStr)) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const prevDate = new Date(prevDateStr);
|
|
924
|
+
const currDate = new Date(currDateStr);
|
|
925
|
+
const diffDays = Math.round(
|
|
926
|
+
(currDate.getTime() - prevDate.getTime()) / (24 * 60 * 60 * 1e3)
|
|
927
|
+
);
|
|
928
|
+
if (diffDays === 1) {
|
|
929
|
+
tempStreak += 1;
|
|
930
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
931
|
+
} else {
|
|
932
|
+
tempStreak = 1;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const lastDate = uniqueDates.at(-1) ?? "";
|
|
936
|
+
if (lastDate === today || lastDate === yesterday) {
|
|
937
|
+
currentStreak = 1;
|
|
938
|
+
for (let i = uniqueDates.length - 2; i >= 0; i--) {
|
|
939
|
+
const currDateStr = uniqueDates[i + 1];
|
|
940
|
+
const prevDateStr = uniqueDates[i];
|
|
941
|
+
if (!(currDateStr && prevDateStr)) {
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
const currDate = new Date(currDateStr);
|
|
945
|
+
const prevDate = new Date(prevDateStr);
|
|
946
|
+
const diffDays = Math.round(
|
|
947
|
+
(currDate.getTime() - prevDate.getTime()) / (24 * 60 * 60 * 1e3)
|
|
948
|
+
);
|
|
949
|
+
if (diffDays === 1) {
|
|
950
|
+
currentStreak += 1;
|
|
951
|
+
} else {
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
currentStreak = 0;
|
|
957
|
+
}
|
|
958
|
+
return { longestStreak, currentStreak };
|
|
959
|
+
}
|
|
960
|
+
function countDaysInRange(from, to) {
|
|
961
|
+
const diffTime = Math.abs(to.getTime() - from.getTime());
|
|
962
|
+
return Math.ceil(diffTime / (24 * 60 * 60 * 1e3)) + 1;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/metrics/core.ts
|
|
966
|
+
function createCoreAccumulator() {
|
|
967
|
+
return {
|
|
968
|
+
tokens: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
969
|
+
costs: [],
|
|
970
|
+
sessionMessages: /* @__PURE__ */ new Map(),
|
|
971
|
+
sessionProjects: /* @__PURE__ */ new Map(),
|
|
972
|
+
sessionDates: /* @__PURE__ */ new Map(),
|
|
973
|
+
userMessageCount: 0,
|
|
974
|
+
assistantMessageCount: 0,
|
|
975
|
+
activeDates: /* @__PURE__ */ new Set(),
|
|
976
|
+
longestSession: null
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function processCoreEntry(acc, entry) {
|
|
980
|
+
const sessionCount = acc.sessionMessages.get(entry.sessionId) ?? 0;
|
|
981
|
+
acc.sessionMessages.set(entry.sessionId, sessionCount + 1);
|
|
982
|
+
acc.sessionProjects.set(entry.sessionId, entry.projectPath);
|
|
983
|
+
const date = parseTimestamp(entry.timestamp);
|
|
984
|
+
if (date) {
|
|
985
|
+
acc.activeDates.add(toDateString(date));
|
|
986
|
+
if (!acc.sessionDates.has(entry.sessionId)) {
|
|
987
|
+
acc.sessionDates.set(entry.sessionId, date);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (isUserMessage(entry)) {
|
|
991
|
+
acc.userMessageCount += 1;
|
|
992
|
+
}
|
|
993
|
+
if (isAssistantMessage(entry)) {
|
|
994
|
+
acc.assistantMessageCount += 1;
|
|
995
|
+
const usage = entry.message.usage;
|
|
996
|
+
const model = entry.message.model;
|
|
997
|
+
acc.tokens.input += usage.input_tokens;
|
|
998
|
+
acc.tokens.output += usage.output_tokens;
|
|
999
|
+
acc.tokens.cacheCreation += usage.cache_creation_input_tokens ?? 0;
|
|
1000
|
+
acc.tokens.cacheRead += usage.cache_read_input_tokens ?? 0;
|
|
1001
|
+
const cost = calculateCost(model, usage);
|
|
1002
|
+
acc.costs.push(cost);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function finalizeCoreMetrics(acc, dateRange) {
|
|
1006
|
+
const totalCost = aggregateCosts(acc.costs);
|
|
1007
|
+
let longestSession = null;
|
|
1008
|
+
let maxMessages = 0;
|
|
1009
|
+
for (const [sessionId, messageCount] of acc.sessionMessages) {
|
|
1010
|
+
if (messageCount > maxMessages) {
|
|
1011
|
+
maxMessages = messageCount;
|
|
1012
|
+
longestSession = {
|
|
1013
|
+
id: sessionId,
|
|
1014
|
+
messageCount,
|
|
1015
|
+
project: acc.sessionProjects.get(sessionId) ?? "Unknown",
|
|
1016
|
+
date: acc.sessionDates.get(sessionId) ?? /* @__PURE__ */ new Date()
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const sortedDates = Array.from(acc.activeDates).sort();
|
|
1021
|
+
const { longestStreak, currentStreak } = calculateStreak(sortedDates);
|
|
1022
|
+
const firstDateStr = sortedDates[0];
|
|
1023
|
+
const lastDateStr = sortedDates.at(-1);
|
|
1024
|
+
const firstActiveDay = firstDateStr ? new Date(firstDateStr) : null;
|
|
1025
|
+
const lastActiveDay = lastDateStr ? new Date(lastDateStr) : null;
|
|
1026
|
+
const totalMessages = acc.userMessageCount + acc.assistantMessageCount;
|
|
1027
|
+
const sessionCount = acc.sessionMessages.size;
|
|
1028
|
+
return {
|
|
1029
|
+
totalTokens: {
|
|
1030
|
+
input: acc.tokens.input,
|
|
1031
|
+
output: acc.tokens.output,
|
|
1032
|
+
cacheCreation: acc.tokens.cacheCreation,
|
|
1033
|
+
cacheRead: acc.tokens.cacheRead,
|
|
1034
|
+
total: acc.tokens.input + acc.tokens.output + acc.tokens.cacheCreation + acc.tokens.cacheRead
|
|
1035
|
+
},
|
|
1036
|
+
estimatedCost: {
|
|
1037
|
+
total: totalCost.total,
|
|
1038
|
+
byCategory: {
|
|
1039
|
+
input: totalCost.input,
|
|
1040
|
+
output: totalCost.output,
|
|
1041
|
+
cacheCreation: totalCost.cacheCreation,
|
|
1042
|
+
cacheRead: totalCost.cacheRead
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
sessions: {
|
|
1046
|
+
total: sessionCount,
|
|
1047
|
+
averageMessages: sessionCount > 0 ? totalMessages / sessionCount : 0,
|
|
1048
|
+
longestSession
|
|
1049
|
+
},
|
|
1050
|
+
messages: {
|
|
1051
|
+
total: totalMessages,
|
|
1052
|
+
userMessages: acc.userMessageCount,
|
|
1053
|
+
assistantMessages: acc.assistantMessageCount,
|
|
1054
|
+
averagePerSession: sessionCount > 0 ? totalMessages / sessionCount : 0
|
|
1055
|
+
},
|
|
1056
|
+
activity: {
|
|
1057
|
+
daysActive: acc.activeDates.size,
|
|
1058
|
+
totalDaysInPeriod: countDaysInRange(dateRange.from, dateRange.to),
|
|
1059
|
+
longestStreak,
|
|
1060
|
+
currentStreak,
|
|
1061
|
+
firstActiveDay,
|
|
1062
|
+
lastActiveDay
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
function calculateCoreMetrics(entries, dateRange) {
|
|
1067
|
+
const acc = createCoreAccumulator();
|
|
1068
|
+
for (const entry of entries) {
|
|
1069
|
+
processCoreEntry(acc, entry);
|
|
1070
|
+
}
|
|
1071
|
+
return finalizeCoreMetrics(acc, dateRange);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/metrics/fun.ts
|
|
1075
|
+
function createFunAccumulator() {
|
|
1076
|
+
return {
|
|
1077
|
+
totalCharacters: 0,
|
|
1078
|
+
summaries: [],
|
|
1079
|
+
longestResponse: null,
|
|
1080
|
+
dailyActivity: /* @__PURE__ */ new Map(),
|
|
1081
|
+
versions: /* @__PURE__ */ new Map(),
|
|
1082
|
+
milestones: [],
|
|
1083
|
+
sessionCount: 0,
|
|
1084
|
+
messageCount: 0,
|
|
1085
|
+
tokenCount: 0
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function processFunEntry(acc, entry) {
|
|
1089
|
+
const date = parseTimestamp(entry.timestamp);
|
|
1090
|
+
const dateStr = date ? toDateString(date) : null;
|
|
1091
|
+
if (dateStr) {
|
|
1092
|
+
const daily = acc.dailyActivity.get(dateStr) ?? {
|
|
1093
|
+
messageCount: 0,
|
|
1094
|
+
tokensUsed: 0
|
|
1095
|
+
};
|
|
1096
|
+
daily.messageCount += 1;
|
|
1097
|
+
acc.dailyActivity.set(dateStr, daily);
|
|
1098
|
+
}
|
|
1099
|
+
if (isSummaryEntry(entry) && entry.summary) {
|
|
1100
|
+
acc.summaries.push(entry.summary);
|
|
1101
|
+
}
|
|
1102
|
+
if (isUserMessage(entry) && entry.version) {
|
|
1103
|
+
const existing = acc.versions.get(entry.version);
|
|
1104
|
+
if (existing) {
|
|
1105
|
+
existing.count += 1;
|
|
1106
|
+
} else if (date) {
|
|
1107
|
+
acc.versions.set(entry.version, { count: 1, firstSeen: date });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (isAssistantMessage(entry)) {
|
|
1111
|
+
acc.messageCount += 1;
|
|
1112
|
+
const usage = entry.message.usage;
|
|
1113
|
+
const tokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
1114
|
+
acc.tokenCount += tokens;
|
|
1115
|
+
if (dateStr) {
|
|
1116
|
+
const daily = acc.dailyActivity.get(dateStr);
|
|
1117
|
+
if (daily) {
|
|
1118
|
+
daily.tokensUsed += tokens;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
let responseChars = 0;
|
|
1122
|
+
for (const content of entry.message.content) {
|
|
1123
|
+
if (isTextContent(content)) {
|
|
1124
|
+
responseChars += content.text.length;
|
|
1125
|
+
acc.totalCharacters += content.text.length;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (date && responseChars > 0 && (!acc.longestResponse || responseChars > acc.longestResponse.characterCount)) {
|
|
1129
|
+
acc.longestResponse = {
|
|
1130
|
+
characterCount: responseChars,
|
|
1131
|
+
date,
|
|
1132
|
+
sessionId: entry.sessionId
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function checkMilestones(acc, sortedDates) {
|
|
1138
|
+
const milestones = [];
|
|
1139
|
+
const firstDate = sortedDates[0];
|
|
1140
|
+
if (firstDate) {
|
|
1141
|
+
milestones.push({
|
|
1142
|
+
type: "first_session",
|
|
1143
|
+
date: new Date(firstDate),
|
|
1144
|
+
description: "You started your Claude Code journey"
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
let cumulativeMessages = 0;
|
|
1148
|
+
let cumulativeTokens = 0;
|
|
1149
|
+
const milestonesHit = /* @__PURE__ */ new Set();
|
|
1150
|
+
for (const dateStr of sortedDates) {
|
|
1151
|
+
const daily = acc.dailyActivity.get(dateStr);
|
|
1152
|
+
if (!daily) {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
cumulativeMessages += daily.messageCount;
|
|
1156
|
+
cumulativeTokens += daily.tokensUsed;
|
|
1157
|
+
const date = new Date(dateStr);
|
|
1158
|
+
if (cumulativeMessages >= 100 && !milestonesHit.has("100_messages")) {
|
|
1159
|
+
milestonesHit.add("100_messages");
|
|
1160
|
+
milestones.push({
|
|
1161
|
+
type: "100_messages",
|
|
1162
|
+
date,
|
|
1163
|
+
description: "You exchanged 100 messages with Claude"
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
if (cumulativeMessages >= 1e3 && !milestonesHit.has("1000_messages")) {
|
|
1167
|
+
milestonesHit.add("1000_messages");
|
|
1168
|
+
milestones.push({
|
|
1169
|
+
type: "1000_messages",
|
|
1170
|
+
date,
|
|
1171
|
+
description: "You reached 1,000 messages!"
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
if (cumulativeMessages >= 1e4 && !milestonesHit.has("10000_messages")) {
|
|
1175
|
+
milestonesHit.add("10000_messages");
|
|
1176
|
+
milestones.push({
|
|
1177
|
+
type: "10000_messages",
|
|
1178
|
+
date,
|
|
1179
|
+
description: "A true power user: 10,000 messages!"
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
if (cumulativeTokens >= 1e6 && !milestonesHit.has("1m_tokens")) {
|
|
1183
|
+
milestonesHit.add("1m_tokens");
|
|
1184
|
+
milestones.push({
|
|
1185
|
+
type: "1m_tokens",
|
|
1186
|
+
date,
|
|
1187
|
+
description: "You processed 1 million tokens"
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
if (cumulativeTokens >= 1e7 && !milestonesHit.has("10m_tokens")) {
|
|
1191
|
+
milestonesHit.add("10m_tokens");
|
|
1192
|
+
milestones.push({
|
|
1193
|
+
type: "10m_tokens",
|
|
1194
|
+
date,
|
|
1195
|
+
description: "10 million tokens processed!"
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return milestones;
|
|
1200
|
+
}
|
|
1201
|
+
function finalizeFunMetrics(acc) {
|
|
1202
|
+
const wordsGenerated = Math.round(acc.totalCharacters / 5);
|
|
1203
|
+
const equivalentPages = Math.round(wordsGenerated / 250);
|
|
1204
|
+
const summaryWordCloud = buildWordCloud(acc.summaries, 50);
|
|
1205
|
+
let mostActiveDay = null;
|
|
1206
|
+
let maxMessages = 0;
|
|
1207
|
+
for (const [dateStr, data] of acc.dailyActivity) {
|
|
1208
|
+
if (data.messageCount > maxMessages) {
|
|
1209
|
+
maxMessages = data.messageCount;
|
|
1210
|
+
mostActiveDay = {
|
|
1211
|
+
date: new Date(dateStr),
|
|
1212
|
+
messageCount: data.messageCount,
|
|
1213
|
+
tokensUsed: data.tokensUsed
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const versions = Array.from(acc.versions.entries()).map(([version, data]) => ({
|
|
1218
|
+
version,
|
|
1219
|
+
messageCount: data.count,
|
|
1220
|
+
firstSeen: data.firstSeen
|
|
1221
|
+
})).sort((a, b) => b.messageCount - a.messageCount);
|
|
1222
|
+
const sortedDates = Array.from(acc.dailyActivity.keys()).sort();
|
|
1223
|
+
const milestones = checkMilestones(acc, sortedDates);
|
|
1224
|
+
return {
|
|
1225
|
+
charactersGenerated: acc.totalCharacters,
|
|
1226
|
+
wordsGenerated,
|
|
1227
|
+
equivalentPages,
|
|
1228
|
+
summaryWordCloud,
|
|
1229
|
+
longestResponse: acc.longestResponse,
|
|
1230
|
+
mostActiveDay,
|
|
1231
|
+
milestones,
|
|
1232
|
+
versions
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function calculateFunMetrics(entries) {
|
|
1236
|
+
const acc = createFunAccumulator();
|
|
1237
|
+
for (const entry of entries) {
|
|
1238
|
+
processFunEntry(acc, entry);
|
|
1239
|
+
}
|
|
1240
|
+
return finalizeFunMetrics(acc);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/metrics/models.ts
|
|
1244
|
+
function createModelAccumulator() {
|
|
1245
|
+
return {
|
|
1246
|
+
models: /* @__PURE__ */ new Map()
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function processModelEntry(acc, entry) {
|
|
1250
|
+
if (!isAssistantMessage(entry)) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const model = entry.message.model;
|
|
1254
|
+
const usage = entry.message.usage;
|
|
1255
|
+
const existing = acc.models.get(model) ?? {
|
|
1256
|
+
messageCount: 0,
|
|
1257
|
+
tokenCount: 0,
|
|
1258
|
+
cost: 0
|
|
1259
|
+
};
|
|
1260
|
+
const cost = calculateCost(model, usage);
|
|
1261
|
+
const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
1262
|
+
acc.models.set(model, {
|
|
1263
|
+
messageCount: existing.messageCount + 1,
|
|
1264
|
+
tokenCount: existing.tokenCount + totalTokens,
|
|
1265
|
+
cost: existing.cost + cost.total
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
function finalizeModelMetrics(acc) {
|
|
1269
|
+
const totalMessages = Array.from(acc.models.values()).reduce(
|
|
1270
|
+
(sum, m) => sum + m.messageCount,
|
|
1271
|
+
0
|
|
1272
|
+
);
|
|
1273
|
+
const totalCost = Array.from(acc.models.values()).reduce(
|
|
1274
|
+
(sum, m) => sum + m.cost,
|
|
1275
|
+
0
|
|
1276
|
+
);
|
|
1277
|
+
const modelsUsed = Array.from(acc.models.entries()).map(([model, data]) => ({
|
|
1278
|
+
model,
|
|
1279
|
+
displayName: getModelDisplayName(model),
|
|
1280
|
+
messageCount: data.messageCount,
|
|
1281
|
+
tokenCount: data.tokenCount,
|
|
1282
|
+
cost: data.cost,
|
|
1283
|
+
percentage: calculatePercentage(data.messageCount, totalMessages)
|
|
1284
|
+
})).sort((a, b) => b.messageCount - a.messageCount);
|
|
1285
|
+
const topModel = modelsUsed[0];
|
|
1286
|
+
const favoriteModel = topModel ? {
|
|
1287
|
+
model: topModel.model,
|
|
1288
|
+
displayName: topModel.displayName,
|
|
1289
|
+
messageCount: topModel.messageCount,
|
|
1290
|
+
percentage: topModel.percentage
|
|
1291
|
+
} : null;
|
|
1292
|
+
const costByModel = Array.from(acc.models.entries()).map(([model, data]) => ({
|
|
1293
|
+
model,
|
|
1294
|
+
displayName: getModelDisplayName(model),
|
|
1295
|
+
cost: data.cost,
|
|
1296
|
+
percentage: calculatePercentage(data.cost, totalCost)
|
|
1297
|
+
})).sort((a, b) => b.cost - a.cost);
|
|
1298
|
+
return {
|
|
1299
|
+
modelsUsed,
|
|
1300
|
+
favoriteModel,
|
|
1301
|
+
costByModel
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
function calculateModelMetrics(entries) {
|
|
1305
|
+
const acc = createModelAccumulator();
|
|
1306
|
+
for (const entry of entries) {
|
|
1307
|
+
processModelEntry(acc, entry);
|
|
1308
|
+
}
|
|
1309
|
+
return finalizeModelMetrics(acc);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/metrics/patterns.ts
|
|
1313
|
+
function createPatternAccumulator() {
|
|
1314
|
+
return {
|
|
1315
|
+
hourly: initHourlyDistribution(),
|
|
1316
|
+
weekly: initWeeklyDistribution(),
|
|
1317
|
+
monthly: initMonthlyDistribution()
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
function processPatternEntry(acc, entry) {
|
|
1321
|
+
const date = parseTimestamp(entry.timestamp);
|
|
1322
|
+
if (!date) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
incrementMap(acc.hourly, getHour(date));
|
|
1326
|
+
incrementMap(acc.weekly, getDayOfWeek(date));
|
|
1327
|
+
incrementMap(acc.monthly, getMonth(date));
|
|
1328
|
+
}
|
|
1329
|
+
function finalizePatternMetrics(acc) {
|
|
1330
|
+
const peakHourData = findMax(acc.hourly);
|
|
1331
|
+
const peakDayData = findMax(acc.weekly);
|
|
1332
|
+
const peakMonthData = findMax(acc.monthly);
|
|
1333
|
+
const hourlyDistribution = Array.from(acc.hourly.entries()).map(([hour, count]) => ({ hour, count })).sort((a, b) => a.hour - b.hour);
|
|
1334
|
+
const weeklyDistribution = Array.from(acc.weekly.entries()).map(([day, count]) => ({ day, dayName: getDayName(day), count })).sort((a, b) => a.day - b.day);
|
|
1335
|
+
const monthlyDistribution = Array.from(acc.monthly.entries()).map(([month, count]) => ({ month, monthName: getMonthName(month), count })).sort((a, b) => a.month - b.month);
|
|
1336
|
+
return {
|
|
1337
|
+
peakHour: {
|
|
1338
|
+
hour: typeof peakHourData?.key === "number" ? peakHourData.key : 0,
|
|
1339
|
+
messageCount: peakHourData?.count ?? 0
|
|
1340
|
+
},
|
|
1341
|
+
peakDayOfWeek: {
|
|
1342
|
+
day: typeof peakDayData?.key === "number" ? peakDayData.key : 0,
|
|
1343
|
+
dayName: getDayName(
|
|
1344
|
+
typeof peakDayData?.key === "number" ? peakDayData.key : 0
|
|
1345
|
+
),
|
|
1346
|
+
messageCount: peakDayData?.count ?? 0
|
|
1347
|
+
},
|
|
1348
|
+
busiestMonth: {
|
|
1349
|
+
month: typeof peakMonthData?.key === "number" ? peakMonthData.key : 0,
|
|
1350
|
+
monthName: getMonthName(
|
|
1351
|
+
typeof peakMonthData?.key === "number" ? peakMonthData.key : 0
|
|
1352
|
+
),
|
|
1353
|
+
messageCount: peakMonthData?.count ?? 0
|
|
1354
|
+
},
|
|
1355
|
+
hourlyDistribution,
|
|
1356
|
+
weeklyDistribution,
|
|
1357
|
+
monthlyDistribution
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
function calculatePatternMetrics(entries) {
|
|
1361
|
+
const acc = createPatternAccumulator();
|
|
1362
|
+
for (const entry of entries) {
|
|
1363
|
+
processPatternEntry(acc, entry);
|
|
1364
|
+
}
|
|
1365
|
+
return finalizePatternMetrics(acc);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/metrics/projects.ts
|
|
1369
|
+
function createProjectAccumulator() {
|
|
1370
|
+
return {
|
|
1371
|
+
projects: /* @__PURE__ */ new Map(),
|
|
1372
|
+
branches: /* @__PURE__ */ new Map()
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function processProjectEntry(acc, entry) {
|
|
1376
|
+
const projectPath = entry.projectPath;
|
|
1377
|
+
const projectData = acc.projects.get(projectPath) ?? {
|
|
1378
|
+
sessionIds: /* @__PURE__ */ new Set(),
|
|
1379
|
+
messageCount: 0,
|
|
1380
|
+
tokenCount: 0,
|
|
1381
|
+
lastActive: null
|
|
1382
|
+
};
|
|
1383
|
+
projectData.sessionIds.add(entry.sessionId);
|
|
1384
|
+
projectData.messageCount += 1;
|
|
1385
|
+
const date = parseTimestamp(entry.timestamp);
|
|
1386
|
+
if (date && (!projectData.lastActive || date > projectData.lastActive)) {
|
|
1387
|
+
projectData.lastActive = date;
|
|
1388
|
+
}
|
|
1389
|
+
if (isAssistantMessage(entry)) {
|
|
1390
|
+
const usage = entry.message.usage;
|
|
1391
|
+
projectData.tokenCount += usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
1392
|
+
}
|
|
1393
|
+
acc.projects.set(projectPath, projectData);
|
|
1394
|
+
if (isUserMessage(entry) || isAssistantMessage(entry)) {
|
|
1395
|
+
const gitBranch = "gitBranch" in entry ? entry.gitBranch : void 0;
|
|
1396
|
+
if (gitBranch) {
|
|
1397
|
+
const branchKey = `${projectPath}:${gitBranch}`;
|
|
1398
|
+
const branchData = acc.branches.get(branchKey) ?? {
|
|
1399
|
+
projectPath,
|
|
1400
|
+
messageCount: 0
|
|
1401
|
+
};
|
|
1402
|
+
branchData.messageCount += 1;
|
|
1403
|
+
acc.branches.set(branchKey, branchData);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function finalizeProjectMetrics(acc) {
|
|
1408
|
+
const totalMessages = Array.from(acc.projects.values()).reduce(
|
|
1409
|
+
(sum, p) => sum + p.messageCount,
|
|
1410
|
+
0
|
|
1411
|
+
);
|
|
1412
|
+
const projects = Array.from(acc.projects.entries()).map(([path, data]) => ({
|
|
1413
|
+
path,
|
|
1414
|
+
displayName: parseProjectName(path),
|
|
1415
|
+
sessionCount: data.sessionIds.size,
|
|
1416
|
+
messageCount: data.messageCount,
|
|
1417
|
+
tokenCount: data.tokenCount,
|
|
1418
|
+
lastActive: data.lastActive ?? /* @__PURE__ */ new Date()
|
|
1419
|
+
})).sort((a, b) => b.messageCount - a.messageCount);
|
|
1420
|
+
const firstProject = projects[0];
|
|
1421
|
+
const topProject = firstProject ? {
|
|
1422
|
+
path: firstProject.path,
|
|
1423
|
+
displayName: firstProject.displayName,
|
|
1424
|
+
sessionCount: firstProject.sessionCount,
|
|
1425
|
+
messageCount: firstProject.messageCount,
|
|
1426
|
+
percentageOfTotal: calculatePercentage(
|
|
1427
|
+
firstProject.messageCount,
|
|
1428
|
+
totalMessages
|
|
1429
|
+
)
|
|
1430
|
+
} : null;
|
|
1431
|
+
const branches = Array.from(acc.branches.entries()).map(([key, data]) => {
|
|
1432
|
+
const branchName = key.split(":").slice(1).join(":");
|
|
1433
|
+
return {
|
|
1434
|
+
name: branchName,
|
|
1435
|
+
projectPath: data.projectPath,
|
|
1436
|
+
messageCount: data.messageCount
|
|
1437
|
+
};
|
|
1438
|
+
}).sort((a, b) => b.messageCount - a.messageCount);
|
|
1439
|
+
return {
|
|
1440
|
+
projectsWorkedOn: acc.projects.size,
|
|
1441
|
+
projects,
|
|
1442
|
+
topProject,
|
|
1443
|
+
gitBranches: {
|
|
1444
|
+
total: acc.branches.size,
|
|
1445
|
+
branches: branches.slice(0, 20)
|
|
1446
|
+
// Top 20 branches
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function calculateProjectMetrics(entries) {
|
|
1451
|
+
const acc = createProjectAccumulator();
|
|
1452
|
+
for (const entry of entries) {
|
|
1453
|
+
processProjectEntry(acc, entry);
|
|
1454
|
+
}
|
|
1455
|
+
return finalizeProjectMetrics(acc);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/metrics/tools.ts
|
|
1459
|
+
var READING_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep"]);
|
|
1460
|
+
var WRITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit"]);
|
|
1461
|
+
var EXECUTING_TOOLS = /* @__PURE__ */ new Set(["Bash", "KillShell", "TaskOutput"]);
|
|
1462
|
+
var RESEARCHING_TOOLS = /* @__PURE__ */ new Set(["WebFetch", "WebSearch"]);
|
|
1463
|
+
var PLANNING_TOOLS = /* @__PURE__ */ new Set([
|
|
1464
|
+
"Task",
|
|
1465
|
+
"TodoWrite",
|
|
1466
|
+
"TodoRead",
|
|
1467
|
+
"ExitPlanMode",
|
|
1468
|
+
"EnterPlanMode",
|
|
1469
|
+
"AskUserQuestion"
|
|
1470
|
+
]);
|
|
1471
|
+
function createToolAccumulator() {
|
|
1472
|
+
return {
|
|
1473
|
+
tools: /* @__PURE__ */ new Map(),
|
|
1474
|
+
mcpTools: /* @__PURE__ */ new Map(),
|
|
1475
|
+
subagentTypes: /* @__PURE__ */ new Map(),
|
|
1476
|
+
taskToolCalls: 0
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function isMcpTool(name) {
|
|
1480
|
+
return name.startsWith("mcp__") || name.includes("::");
|
|
1481
|
+
}
|
|
1482
|
+
function parseMcpTool(name) {
|
|
1483
|
+
if (name.startsWith("mcp__")) {
|
|
1484
|
+
const parts = name.split("__");
|
|
1485
|
+
return {
|
|
1486
|
+
server: parts[1] ?? "unknown",
|
|
1487
|
+
tool: parts.slice(2).join("__")
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
if (name.includes("::")) {
|
|
1491
|
+
const [server, ...toolParts] = name.split("::");
|
|
1492
|
+
return {
|
|
1493
|
+
server: server ?? "unknown",
|
|
1494
|
+
tool: toolParts.join("::")
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
return { server: "unknown", tool: name };
|
|
1498
|
+
}
|
|
1499
|
+
function processToolEntry(acc, entry) {
|
|
1500
|
+
if (!isAssistantMessage(entry)) {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
for (const content of entry.message.content) {
|
|
1504
|
+
if (!isToolUseContent(content)) {
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const toolName = content.name;
|
|
1508
|
+
acc.tools.set(toolName, (acc.tools.get(toolName) ?? 0) + 1);
|
|
1509
|
+
if (isMcpTool(toolName)) {
|
|
1510
|
+
acc.mcpTools.set(toolName, (acc.mcpTools.get(toolName) ?? 0) + 1);
|
|
1511
|
+
}
|
|
1512
|
+
if (toolName === "Task") {
|
|
1513
|
+
acc.taskToolCalls += 1;
|
|
1514
|
+
const input = content.input;
|
|
1515
|
+
const subagentType = typeof input.subagent_type === "string" ? input.subagent_type : "unknown";
|
|
1516
|
+
acc.subagentTypes.set(
|
|
1517
|
+
subagentType,
|
|
1518
|
+
(acc.subagentTypes.get(subagentType) ?? 0) + 1
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function determineDeveloperStyle(breakdown) {
|
|
1524
|
+
const total = breakdown.reading + breakdown.writing + breakdown.executing + breakdown.researching + breakdown.planning;
|
|
1525
|
+
if (total === 0) {
|
|
1526
|
+
return { style: "balanced", description: "Just getting started" };
|
|
1527
|
+
}
|
|
1528
|
+
const percentages = {
|
|
1529
|
+
reading: breakdown.reading / total * 100,
|
|
1530
|
+
writing: breakdown.writing / total * 100,
|
|
1531
|
+
executing: breakdown.executing / total * 100,
|
|
1532
|
+
researching: breakdown.researching / total * 100,
|
|
1533
|
+
planning: breakdown.planning / total * 100
|
|
1534
|
+
};
|
|
1535
|
+
if (percentages.reading > 40) {
|
|
1536
|
+
return {
|
|
1537
|
+
style: "reader",
|
|
1538
|
+
description: "You prefer understanding code before changing it"
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
if (percentages.writing > 40) {
|
|
1542
|
+
return {
|
|
1543
|
+
style: "writer",
|
|
1544
|
+
description: "You dive straight into writing and editing code"
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
if (percentages.executing > 40) {
|
|
1548
|
+
return {
|
|
1549
|
+
style: "executor",
|
|
1550
|
+
description: "You love running commands and scripts"
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
if (percentages.researching > 30) {
|
|
1554
|
+
return {
|
|
1555
|
+
style: "researcher",
|
|
1556
|
+
description: "You gather information before acting"
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
if (percentages.planning > 30) {
|
|
1560
|
+
return {
|
|
1561
|
+
style: "planner",
|
|
1562
|
+
description: "You carefully plan and organize your work"
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
style: "balanced",
|
|
1567
|
+
description: "You have a well-rounded approach to coding"
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
function finalizeToolMetrics(acc) {
|
|
1571
|
+
const totalCalls = Array.from(acc.tools.values()).reduce(
|
|
1572
|
+
(sum, count) => sum + count,
|
|
1573
|
+
0
|
|
1574
|
+
);
|
|
1575
|
+
const toolDistribution = Array.from(acc.tools.entries()).map(([tool, count]) => ({
|
|
1576
|
+
tool,
|
|
1577
|
+
count,
|
|
1578
|
+
percentage: calculatePercentage(count, totalCalls)
|
|
1579
|
+
})).sort((a, b) => b.count - a.count);
|
|
1580
|
+
const topTools = toolDistribution.slice(0, 10).map((item, index) => ({
|
|
1581
|
+
tool: item.tool,
|
|
1582
|
+
count: item.count,
|
|
1583
|
+
rank: index + 1
|
|
1584
|
+
}));
|
|
1585
|
+
const subagentTypes = Array.from(acc.subagentTypes.entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count);
|
|
1586
|
+
const mcpTools = Array.from(acc.mcpTools.entries()).map(([name, count]) => {
|
|
1587
|
+
const { server, tool } = parseMcpTool(name);
|
|
1588
|
+
return { server, tool, count };
|
|
1589
|
+
}).sort((a, b) => b.count - a.count);
|
|
1590
|
+
const styleBreakdown = {
|
|
1591
|
+
reading: 0,
|
|
1592
|
+
writing: 0,
|
|
1593
|
+
executing: 0,
|
|
1594
|
+
researching: 0,
|
|
1595
|
+
planning: 0
|
|
1596
|
+
};
|
|
1597
|
+
for (const [tool, count] of acc.tools) {
|
|
1598
|
+
if (READING_TOOLS.has(tool)) {
|
|
1599
|
+
styleBreakdown.reading += count;
|
|
1600
|
+
} else if (WRITING_TOOLS.has(tool)) {
|
|
1601
|
+
styleBreakdown.writing += count;
|
|
1602
|
+
} else if (EXECUTING_TOOLS.has(tool)) {
|
|
1603
|
+
styleBreakdown.executing += count;
|
|
1604
|
+
} else if (RESEARCHING_TOOLS.has(tool)) {
|
|
1605
|
+
styleBreakdown.researching += count;
|
|
1606
|
+
} else if (PLANNING_TOOLS.has(tool)) {
|
|
1607
|
+
styleBreakdown.planning += count;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const { style, description } = determineDeveloperStyle(styleBreakdown);
|
|
1611
|
+
return {
|
|
1612
|
+
toolCallsTotal: totalCalls,
|
|
1613
|
+
toolDistribution,
|
|
1614
|
+
topTools,
|
|
1615
|
+
subagentUsage: {
|
|
1616
|
+
taskToolCalls: acc.taskToolCalls,
|
|
1617
|
+
subagentTypes
|
|
1618
|
+
},
|
|
1619
|
+
mcpTools,
|
|
1620
|
+
developerProfile: {
|
|
1621
|
+
primaryStyle: style,
|
|
1622
|
+
styleBreakdown,
|
|
1623
|
+
description
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
function calculateToolMetrics(entries) {
|
|
1628
|
+
const acc = createToolAccumulator();
|
|
1629
|
+
for (const entry of entries) {
|
|
1630
|
+
processToolEntry(acc, entry);
|
|
1631
|
+
}
|
|
1632
|
+
return finalizeToolMetrics(acc);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/metrics/index.ts
|
|
1636
|
+
function calculateAllMetrics(entries, dateRange) {
|
|
1637
|
+
const core = calculateCoreMetrics(entries, dateRange);
|
|
1638
|
+
const patterns = calculatePatternMetrics(entries);
|
|
1639
|
+
const models = calculateModelMetrics(entries);
|
|
1640
|
+
const tools = calculateToolMetrics(entries);
|
|
1641
|
+
const projects = calculateProjectMetrics(entries);
|
|
1642
|
+
const collaboration = calculateCollaborationMetrics(entries);
|
|
1643
|
+
const fun = calculateFunMetrics(entries);
|
|
1644
|
+
return {
|
|
1645
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
1646
|
+
period: {
|
|
1647
|
+
from: dateRange.from,
|
|
1648
|
+
to: dateRange.to,
|
|
1649
|
+
year: dateRange.from.getFullYear()
|
|
1650
|
+
},
|
|
1651
|
+
core,
|
|
1652
|
+
patterns,
|
|
1653
|
+
models,
|
|
1654
|
+
tools,
|
|
1655
|
+
projects,
|
|
1656
|
+
collaboration,
|
|
1657
|
+
fun
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/index.ts
|
|
1662
|
+
async function generateWrappedSummary(config = {}) {
|
|
1663
|
+
const startTime = Date.now();
|
|
1664
|
+
const warnings = [];
|
|
1665
|
+
const errors = [];
|
|
1666
|
+
const resolvedConfig = resolveConfig(config);
|
|
1667
|
+
let loadedData;
|
|
1668
|
+
try {
|
|
1669
|
+
loadedData = await loadClaudeData(resolvedConfig);
|
|
1670
|
+
} catch (e) {
|
|
1671
|
+
errors.push({
|
|
1672
|
+
code: "LOAD_ERROR",
|
|
1673
|
+
message: e instanceof Error ? e.message : "Failed to load Claude data",
|
|
1674
|
+
fatal: true
|
|
1675
|
+
});
|
|
1676
|
+
return {
|
|
1677
|
+
summary: createEmptySummary(resolvedConfig),
|
|
1678
|
+
warnings,
|
|
1679
|
+
errors,
|
|
1680
|
+
stats: {
|
|
1681
|
+
filesProcessed: 0,
|
|
1682
|
+
filesSkipped: 0,
|
|
1683
|
+
entriesProcessed: 0,
|
|
1684
|
+
entriesSkipped: 0,
|
|
1685
|
+
processingTimeMs: Date.now() - startTime
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
if (loadedData.entries.length === 0) {
|
|
1690
|
+
warnings.push({
|
|
1691
|
+
code: "NO_DATA",
|
|
1692
|
+
message: "No Claude Code data found for the specified period",
|
|
1693
|
+
context: {
|
|
1694
|
+
baseDir: resolvedConfig.baseDir,
|
|
1695
|
+
dateRange: resolvedConfig.dateRange
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
const summary = calculateAllMetrics(
|
|
1700
|
+
loadedData.entries,
|
|
1701
|
+
resolvedConfig.dateRange
|
|
1702
|
+
);
|
|
1703
|
+
return {
|
|
1704
|
+
summary,
|
|
1705
|
+
warnings,
|
|
1706
|
+
errors,
|
|
1707
|
+
stats: {
|
|
1708
|
+
filesProcessed: loadedData.sessions.length,
|
|
1709
|
+
filesSkipped: 0,
|
|
1710
|
+
entriesProcessed: loadedData.entries.length,
|
|
1711
|
+
entriesSkipped: 0,
|
|
1712
|
+
processingTimeMs: Date.now() - startTime
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function createEmptySummary(config) {
|
|
1717
|
+
const emptyDate = /* @__PURE__ */ new Date();
|
|
1718
|
+
return {
|
|
1719
|
+
generatedAt: emptyDate,
|
|
1720
|
+
period: {
|
|
1721
|
+
from: config.dateRange.from,
|
|
1722
|
+
to: config.dateRange.to,
|
|
1723
|
+
year: config.year
|
|
1724
|
+
},
|
|
1725
|
+
core: {
|
|
1726
|
+
totalTokens: {
|
|
1727
|
+
input: 0,
|
|
1728
|
+
output: 0,
|
|
1729
|
+
cacheCreation: 0,
|
|
1730
|
+
cacheRead: 0,
|
|
1731
|
+
total: 0
|
|
1732
|
+
},
|
|
1733
|
+
estimatedCost: {
|
|
1734
|
+
total: 0,
|
|
1735
|
+
byCategory: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 }
|
|
1736
|
+
},
|
|
1737
|
+
sessions: { total: 0, averageMessages: 0, longestSession: null },
|
|
1738
|
+
messages: {
|
|
1739
|
+
total: 0,
|
|
1740
|
+
userMessages: 0,
|
|
1741
|
+
assistantMessages: 0,
|
|
1742
|
+
averagePerSession: 0
|
|
1743
|
+
},
|
|
1744
|
+
activity: {
|
|
1745
|
+
daysActive: 0,
|
|
1746
|
+
totalDaysInPeriod: 0,
|
|
1747
|
+
longestStreak: 0,
|
|
1748
|
+
currentStreak: 0,
|
|
1749
|
+
firstActiveDay: null,
|
|
1750
|
+
lastActiveDay: null
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
patterns: {
|
|
1754
|
+
peakHour: { hour: 0, messageCount: 0 },
|
|
1755
|
+
peakDayOfWeek: { day: 0, dayName: "Sunday", messageCount: 0 },
|
|
1756
|
+
busiestMonth: { month: 0, monthName: "January", messageCount: 0 },
|
|
1757
|
+
hourlyDistribution: [],
|
|
1758
|
+
weeklyDistribution: [],
|
|
1759
|
+
monthlyDistribution: []
|
|
1760
|
+
},
|
|
1761
|
+
models: {
|
|
1762
|
+
modelsUsed: [],
|
|
1763
|
+
favoriteModel: null,
|
|
1764
|
+
costByModel: []
|
|
1765
|
+
},
|
|
1766
|
+
tools: {
|
|
1767
|
+
toolCallsTotal: 0,
|
|
1768
|
+
toolDistribution: [],
|
|
1769
|
+
topTools: [],
|
|
1770
|
+
subagentUsage: { taskToolCalls: 0, subagentTypes: [] },
|
|
1771
|
+
mcpTools: [],
|
|
1772
|
+
developerProfile: {
|
|
1773
|
+
primaryStyle: "balanced",
|
|
1774
|
+
styleBreakdown: {
|
|
1775
|
+
reading: 0,
|
|
1776
|
+
writing: 0,
|
|
1777
|
+
executing: 0,
|
|
1778
|
+
researching: 0,
|
|
1779
|
+
planning: 0
|
|
1780
|
+
},
|
|
1781
|
+
description: "No data yet"
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
projects: {
|
|
1785
|
+
projectsWorkedOn: 0,
|
|
1786
|
+
projects: [],
|
|
1787
|
+
topProject: null,
|
|
1788
|
+
gitBranches: { total: 0, branches: [] }
|
|
1789
|
+
},
|
|
1790
|
+
collaboration: {
|
|
1791
|
+
cacheEfficiency: {
|
|
1792
|
+
cacheHitRate: 0,
|
|
1793
|
+
cacheCreationRate: 0,
|
|
1794
|
+
estimatedSavings: 0
|
|
1795
|
+
},
|
|
1796
|
+
extendedThinking: { messagesWithThinking: 0, percentageOfTotal: 0 },
|
|
1797
|
+
interactivity: {
|
|
1798
|
+
questionsAsked: 0,
|
|
1799
|
+
plansCreated: 0,
|
|
1800
|
+
todoItemsCreated: 0
|
|
1801
|
+
},
|
|
1802
|
+
serviceTier: {
|
|
1803
|
+
standardRequests: 0,
|
|
1804
|
+
priorityRequests: 0,
|
|
1805
|
+
priorityPercentage: 0
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
fun: {
|
|
1809
|
+
charactersGenerated: 0,
|
|
1810
|
+
wordsGenerated: 0,
|
|
1811
|
+
equivalentPages: 0,
|
|
1812
|
+
summaryWordCloud: [],
|
|
1813
|
+
longestResponse: null,
|
|
1814
|
+
mostActiveDay: null,
|
|
1815
|
+
milestones: [],
|
|
1816
|
+
versions: []
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/url/params.ts
|
|
1822
|
+
var PARAMS = {
|
|
1823
|
+
// Core stats
|
|
1824
|
+
sessions: "s",
|
|
1825
|
+
tokens: "t",
|
|
1826
|
+
cost: "c",
|
|
1827
|
+
daysActive: "d",
|
|
1828
|
+
longestStreak: "ls",
|
|
1829
|
+
currentStreak: "cs",
|
|
1830
|
+
// Patterns
|
|
1831
|
+
peakHour: "ph",
|
|
1832
|
+
peakDay: "pd",
|
|
1833
|
+
// Models
|
|
1834
|
+
favoriteModel: "fm",
|
|
1835
|
+
modelBreakdown: "mb",
|
|
1836
|
+
// Tools
|
|
1837
|
+
topTools: "tt",
|
|
1838
|
+
developerStyle: "st",
|
|
1839
|
+
// Projects
|
|
1840
|
+
topProject: "tp",
|
|
1841
|
+
projectCount: "pc",
|
|
1842
|
+
// Fun
|
|
1843
|
+
wordsGenerated: "wg",
|
|
1844
|
+
firstActiveDay: "fad"};
|
|
1845
|
+
var MODEL_ABBREV = {
|
|
1846
|
+
// Claude 4.5 family
|
|
1847
|
+
"claude-opus-4-5-20251101": "o45",
|
|
1848
|
+
"claude-sonnet-4-5-20250929": "s45",
|
|
1849
|
+
"claude-haiku-4-5-20251015": "h45",
|
|
1850
|
+
// Claude 4.1 family
|
|
1851
|
+
"claude-opus-4-1-20250805": "o41",
|
|
1852
|
+
// Claude 4 family
|
|
1853
|
+
"claude-opus-4-20250514": "o4",
|
|
1854
|
+
"claude-sonnet-4-20250514": "s4",
|
|
1855
|
+
// Claude 3.7 family (hybrid reasoning)
|
|
1856
|
+
"claude-3-7-sonnet-20250219": "s37",
|
|
1857
|
+
"claude-3-7-sonnet-latest": "s37",
|
|
1858
|
+
// Claude 3.5 family
|
|
1859
|
+
"claude-3-5-sonnet-20241022": "s35",
|
|
1860
|
+
"claude-3-5-sonnet-20240620": "s35",
|
|
1861
|
+
"claude-3-5-haiku-20241022": "h35",
|
|
1862
|
+
// Claude 3 family
|
|
1863
|
+
"claude-3-opus-20240229": "o3",
|
|
1864
|
+
"claude-3-sonnet-20240229": "s3",
|
|
1865
|
+
"claude-3-haiku-20240307": "h3"
|
|
1866
|
+
};
|
|
1867
|
+
Object.fromEntries(
|
|
1868
|
+
Object.entries(MODEL_ABBREV).map(([k, v]) => [v, k])
|
|
1869
|
+
);
|
|
1870
|
+
var STYLE_ABBREV = {
|
|
1871
|
+
reader: "r",
|
|
1872
|
+
writer: "w",
|
|
1873
|
+
executor: "e",
|
|
1874
|
+
researcher: "rs",
|
|
1875
|
+
planner: "p",
|
|
1876
|
+
balanced: "b"
|
|
1877
|
+
};
|
|
1878
|
+
Object.fromEntries(
|
|
1879
|
+
Object.entries(STYLE_ABBREV).map(([k, v]) => [v, k])
|
|
1880
|
+
);
|
|
1881
|
+
var BASE_URL = "https://sleek.design/claude-code-wrapped-2025";
|
|
1882
|
+
|
|
1883
|
+
// src/url/encode.ts
|
|
1884
|
+
function formatCompact(num) {
|
|
1885
|
+
if (num >= 1e9) {
|
|
1886
|
+
return `${(num / 1e9).toFixed(1)}B`;
|
|
1887
|
+
}
|
|
1888
|
+
if (num >= 1e6) {
|
|
1889
|
+
return `${(num / 1e6).toFixed(1)}M`;
|
|
1890
|
+
}
|
|
1891
|
+
if (num >= 1e3) {
|
|
1892
|
+
return `${(num / 1e3).toFixed(1)}K`;
|
|
1893
|
+
}
|
|
1894
|
+
return num.toString();
|
|
1895
|
+
}
|
|
1896
|
+
function formatCost(cost) {
|
|
1897
|
+
return cost.toFixed(2).replace(/\.?0+$/, "");
|
|
1898
|
+
}
|
|
1899
|
+
function formatDate(date) {
|
|
1900
|
+
if (!date) {
|
|
1901
|
+
return "";
|
|
1902
|
+
}
|
|
1903
|
+
const year = date.getFullYear();
|
|
1904
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1905
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1906
|
+
return `${year}${month}${day}`;
|
|
1907
|
+
}
|
|
1908
|
+
function abbreviateModel(model) {
|
|
1909
|
+
return MODEL_ABBREV[model] ?? model.slice(0, 10);
|
|
1910
|
+
}
|
|
1911
|
+
function abbreviateStyle(style) {
|
|
1912
|
+
return STYLE_ABBREV[style] ?? style.slice(0, 2);
|
|
1913
|
+
}
|
|
1914
|
+
function encodeModelBreakdown(models) {
|
|
1915
|
+
return models.slice(0, 3).map((m) => `${abbreviateModel(m.model)}:${Math.round(m.percentage)}`).join(",");
|
|
1916
|
+
}
|
|
1917
|
+
function encodeTopTools(tools) {
|
|
1918
|
+
return tools.slice(0, 5).map((t) => t.tool).join(",");
|
|
1919
|
+
}
|
|
1920
|
+
function encodeToParams(summary) {
|
|
1921
|
+
const params = new URLSearchParams();
|
|
1922
|
+
params.set(PARAMS.sessions, summary.core.sessions.total.toString());
|
|
1923
|
+
params.set(PARAMS.tokens, formatCompact(summary.core.totalTokens.total));
|
|
1924
|
+
params.set(PARAMS.cost, formatCost(summary.core.estimatedCost.total));
|
|
1925
|
+
params.set(PARAMS.daysActive, summary.core.activity.daysActive.toString());
|
|
1926
|
+
params.set(
|
|
1927
|
+
PARAMS.longestStreak,
|
|
1928
|
+
summary.core.activity.longestStreak.toString()
|
|
1929
|
+
);
|
|
1930
|
+
params.set(
|
|
1931
|
+
PARAMS.currentStreak,
|
|
1932
|
+
summary.core.activity.currentStreak.toString()
|
|
1933
|
+
);
|
|
1934
|
+
params.set(PARAMS.peakHour, summary.patterns.peakHour.hour.toString());
|
|
1935
|
+
params.set(PARAMS.peakDay, summary.patterns.peakDayOfWeek.day.toString());
|
|
1936
|
+
if (summary.models.favoriteModel) {
|
|
1937
|
+
params.set(
|
|
1938
|
+
PARAMS.favoriteModel,
|
|
1939
|
+
abbreviateModel(summary.models.favoriteModel.model)
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
if (summary.models.modelsUsed.length > 0) {
|
|
1943
|
+
params.set(
|
|
1944
|
+
PARAMS.modelBreakdown,
|
|
1945
|
+
encodeModelBreakdown(summary.models.modelsUsed)
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
if (summary.tools.topTools.length > 0) {
|
|
1949
|
+
params.set(PARAMS.topTools, encodeTopTools(summary.tools.topTools));
|
|
1950
|
+
}
|
|
1951
|
+
params.set(
|
|
1952
|
+
PARAMS.developerStyle,
|
|
1953
|
+
abbreviateStyle(summary.tools.developerProfile.primaryStyle)
|
|
1954
|
+
);
|
|
1955
|
+
if (summary.projects.topProject) {
|
|
1956
|
+
params.set(PARAMS.topProject, summary.projects.topProject.displayName);
|
|
1957
|
+
}
|
|
1958
|
+
params.set(PARAMS.projectCount, summary.projects.projectsWorkedOn.toString());
|
|
1959
|
+
params.set(PARAMS.wordsGenerated, formatCompact(summary.fun.wordsGenerated));
|
|
1960
|
+
if (summary.core.activity.firstActiveDay) {
|
|
1961
|
+
params.set(
|
|
1962
|
+
PARAMS.firstActiveDay,
|
|
1963
|
+
formatDate(summary.core.activity.firstActiveDay)
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
return params;
|
|
1967
|
+
}
|
|
1968
|
+
function encodeToUrl(summary) {
|
|
1969
|
+
const params = encodeToParams(summary);
|
|
1970
|
+
return `${BASE_URL}?${params.toString()}`;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// src/cli.ts
|
|
1974
|
+
var main = defineCommand({
|
|
1975
|
+
meta: {
|
|
1976
|
+
name: "@sleekdesign/ccw25",
|
|
1977
|
+
version: "0.1.0",
|
|
1978
|
+
description: "Generate your Claude Code Wrapped 2025 summary"
|
|
1979
|
+
},
|
|
1980
|
+
args: {
|
|
1981
|
+
year: {
|
|
1982
|
+
type: "string",
|
|
1983
|
+
description: "Year to generate wrapped for (default: 2025)",
|
|
1984
|
+
default: "2025"
|
|
1985
|
+
},
|
|
1986
|
+
json: {
|
|
1987
|
+
type: "boolean",
|
|
1988
|
+
description: "Output raw JSON instead of URL",
|
|
1989
|
+
default: false
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
async run({ args }) {
|
|
1993
|
+
const year = Number.parseInt(args.year, 10);
|
|
1994
|
+
console.log("\n\u{1F381} Generating your Claude Code Wrapped 2025...\n");
|
|
1995
|
+
const result = await generateWrappedSummary({ year });
|
|
1996
|
+
if (result.errors.some((e) => e.fatal)) {
|
|
1997
|
+
console.error("\u274C Failed to generate wrapped summary:");
|
|
1998
|
+
for (const error of result.errors) {
|
|
1999
|
+
console.error(` ${error.message}`);
|
|
2000
|
+
}
|
|
2001
|
+
process.exit(1);
|
|
2002
|
+
}
|
|
2003
|
+
if (result.warnings.length > 0) {
|
|
2004
|
+
for (const warning of result.warnings) {
|
|
2005
|
+
console.warn(`\u26A0\uFE0F ${warning.message}`);
|
|
2006
|
+
}
|
|
2007
|
+
console.log("");
|
|
2008
|
+
}
|
|
2009
|
+
if (args.json) {
|
|
2010
|
+
console.log(JSON.stringify(result.summary, null, 2));
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const { summary } = result;
|
|
2014
|
+
console.log("\u{1F4CA} Your 2025 Stats:\n");
|
|
2015
|
+
console.log(
|
|
2016
|
+
` Sessions: ${summary.core.sessions.total.toLocaleString()}`
|
|
2017
|
+
);
|
|
2018
|
+
console.log(
|
|
2019
|
+
` Tokens: ${summary.core.totalTokens.total.toLocaleString()}`
|
|
2020
|
+
);
|
|
2021
|
+
console.log(
|
|
2022
|
+
` Cost: $${summary.core.estimatedCost.total.toFixed(2)}`
|
|
2023
|
+
);
|
|
2024
|
+
console.log(` Days Active: ${summary.core.activity.daysActive}`);
|
|
2025
|
+
console.log(` Streak: ${summary.core.activity.longestStreak} days`);
|
|
2026
|
+
console.log(
|
|
2027
|
+
` Style: ${summary.tools.developerProfile.primaryStyle.toUpperCase()}`
|
|
2028
|
+
);
|
|
2029
|
+
console.log("");
|
|
2030
|
+
const url = encodeToUrl(summary);
|
|
2031
|
+
console.log("\u2728 Your wrapped is ready!\n");
|
|
2032
|
+
console.log(`\u{1F517} ${url}
|
|
2033
|
+
`);
|
|
2034
|
+
console.log("Share it on social media! \u{1F680}\n");
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
runMain(main);
|