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