repospend 0.0.5 → 0.0.7
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/CHANGELOG.md +22 -0
- package/README.md +144 -70
- package/dist/cli.js +1184 -87
- package/package.json +1 -1
- package/web-dist/assets/index-DAOannAp.css +1 -0
- package/web-dist/assets/index-DJs7VNSz.js +306 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-D6eYRahs.css +0 -1
- package/web-dist/assets/index-DcroX3EF.js +0 -260
package/dist/cli.js
CHANGED
|
@@ -44,10 +44,14 @@ function summarize(sessions) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
function groupByRepo(sessions) {
|
|
47
|
-
|
|
47
|
+
const groups = groupBy(sessions, (session) => session.repoRoot, (session) => session.repoName);
|
|
48
|
+
return groups.map((group) => ({
|
|
49
|
+
...group,
|
|
50
|
+
verified: sessions.some((s) => s.repoRoot === group.id && !s.warnings.includes("repo_unverified_no_git_root"))
|
|
51
|
+
}));
|
|
48
52
|
}
|
|
49
53
|
function groupByModel(sessions) {
|
|
50
|
-
return groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "
|
|
54
|
+
return groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "Model not recorded");
|
|
51
55
|
}
|
|
52
56
|
function groupBySourceApp(sessions) {
|
|
53
57
|
return groupBy(sessions, (session) => session.sourceApp, (session) => session.sourceApp);
|
|
@@ -67,6 +71,8 @@ function groupBy(sessions, keyFn, labelFn) {
|
|
|
67
71
|
id,
|
|
68
72
|
label: labelFn(session),
|
|
69
73
|
estimatedCostUsd: void 0,
|
|
74
|
+
knownCostSessions: 0,
|
|
75
|
+
unknownCostSessions: 0,
|
|
70
76
|
inputTokens: 0,
|
|
71
77
|
cachedInputTokens: 0,
|
|
72
78
|
outputTokens: 0,
|
|
@@ -86,6 +92,9 @@ function groupBy(sessions, keyFn, labelFn) {
|
|
|
86
92
|
group.warnings = unique([...group.warnings, ...session.warnings]);
|
|
87
93
|
if (session.estimatedCostUsd !== void 0) {
|
|
88
94
|
group.estimatedCostUsd = roundCurrency((group.estimatedCostUsd ?? 0) + session.estimatedCostUsd);
|
|
95
|
+
group.knownCostSessions += 1;
|
|
96
|
+
} else {
|
|
97
|
+
group.unknownCostSessions += 1;
|
|
89
98
|
}
|
|
90
99
|
groups.set(id, group);
|
|
91
100
|
}
|
|
@@ -344,7 +353,7 @@ function filterUsage(sessions, filters) {
|
|
|
344
353
|
if (!matchesAny(session.sourceClient, filters.source)) return false;
|
|
345
354
|
if (!matchesAny(session.sourceApp, filters.sourceApp)) return false;
|
|
346
355
|
if (!matchesRepo(session, filters.repo)) return false;
|
|
347
|
-
if (!
|
|
356
|
+
if (!matchesModel(session.model, filters.model)) return false;
|
|
348
357
|
const startedAt = timestamp(session.startedAt);
|
|
349
358
|
const hasDateFilter = Boolean(filters.from || filters.to);
|
|
350
359
|
if (hasDateFilter && startedAt === void 0) return false;
|
|
@@ -365,6 +374,11 @@ function matchesRepo(session, filter) {
|
|
|
365
374
|
if (!values.length) return true;
|
|
366
375
|
return values.includes(session.repoName) || values.includes(session.repoRoot);
|
|
367
376
|
}
|
|
377
|
+
function matchesModel(value, filter) {
|
|
378
|
+
const values = filterValues(filter);
|
|
379
|
+
if (!values.length) return true;
|
|
380
|
+
return value !== void 0 ? values.includes(value) : values.includes("unknown-model");
|
|
381
|
+
}
|
|
368
382
|
function filterValues(filter) {
|
|
369
383
|
if (!filter) return [];
|
|
370
384
|
return Array.isArray(filter) ? filter.filter(Boolean) : filter.split(",").map((value) => value.trim()).filter(Boolean);
|
|
@@ -843,14 +857,18 @@ function buildDashboardSnapshot(input) {
|
|
|
843
857
|
};
|
|
844
858
|
}
|
|
845
859
|
function isDashboardVisibleSession(session) {
|
|
846
|
-
return session.totalTokens > 0 || session.sourceClient === "claude";
|
|
860
|
+
return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor";
|
|
847
861
|
}
|
|
848
862
|
|
|
849
863
|
// packages/core/src/repo.ts
|
|
850
864
|
import fs3 from "node:fs";
|
|
851
865
|
import path3 from "node:path";
|
|
866
|
+
function normalizeRoot(p) {
|
|
867
|
+
const stripped = p.startsWith("\\\\?\\") ? p.slice(4) : p;
|
|
868
|
+
return stripped.replace(/^([a-zA-Z]):/, (_, letter) => letter.toUpperCase() + ":");
|
|
869
|
+
}
|
|
852
870
|
function findGitRoot(cwd, fileSystem = fs3) {
|
|
853
|
-
let current = path3.resolve(cwd);
|
|
871
|
+
let current = normalizeRoot(path3.resolve(cwd));
|
|
854
872
|
while (true) {
|
|
855
873
|
const gitPath = path3.join(current, ".git");
|
|
856
874
|
if (fileSystem.existsSync(gitPath)) {
|
|
@@ -897,8 +915,8 @@ function safeFindGitRoot(cwd, fileSystem) {
|
|
|
897
915
|
}
|
|
898
916
|
}
|
|
899
917
|
function fallbackWorkspaceRoot(cwd) {
|
|
900
|
-
const resolved = path3.resolve(cwd || process.cwd());
|
|
901
|
-
const home = process.env.HOME ? path3.resolve(process.env.HOME) : void 0;
|
|
918
|
+
const resolved = normalizeRoot(path3.resolve(cwd || process.cwd()));
|
|
919
|
+
const home = process.env.HOME ? normalizeRoot(path3.resolve(process.env.HOME)) : void 0;
|
|
902
920
|
if (home && (resolved === home || resolved.startsWith(`${home}${path3.sep}`))) {
|
|
903
921
|
const relative = path3.relative(home, resolved).split(path3.sep).filter(Boolean);
|
|
904
922
|
const first = relative[0];
|
|
@@ -1260,7 +1278,8 @@ function finalizeTokenAccumulator(accumulator) {
|
|
|
1260
1278
|
}
|
|
1261
1279
|
const direct = accumulator.directTokenUsages.filter((usage) => isValidTokenUsage(usage));
|
|
1262
1280
|
if (direct.length > 0) {
|
|
1263
|
-
const
|
|
1281
|
+
const filteredDirect = filterDuplicateDirectUsages(direct);
|
|
1282
|
+
const total = filteredDirect.reduce(
|
|
1264
1283
|
(sum2, usage) => ({
|
|
1265
1284
|
inputTokens: sum2.inputTokens + usage.inputTokens,
|
|
1266
1285
|
cachedInputTokens: sum2.cachedInputTokens + usage.cachedInputTokens,
|
|
@@ -1273,6 +1292,8 @@ function finalizeTokenAccumulator(accumulator) {
|
|
|
1273
1292
|
applyFinalUsage(accumulator, total);
|
|
1274
1293
|
accumulator.tokenAggregationMethod = "delta_sum";
|
|
1275
1294
|
accumulator.tokenConfidence = "high";
|
|
1295
|
+
const skipped = direct.length - filteredDirect.length;
|
|
1296
|
+
if (skipped > 0) accumulator.warnings.push("duplicate_or_stale_token_snapshots_skipped");
|
|
1276
1297
|
return;
|
|
1277
1298
|
}
|
|
1278
1299
|
accumulator.tokenAggregationMethod = "unknown";
|
|
@@ -1285,7 +1306,10 @@ function extractRecordTokens(record) {
|
|
|
1285
1306
|
const payloadInfo = firstObject2(payload?.info);
|
|
1286
1307
|
const codexLastUsage = firstObject2(payloadInfo?.last_token_usage);
|
|
1287
1308
|
if (codexLastUsage) {
|
|
1288
|
-
|
|
1309
|
+
const usage2 = tokenUsageFromObject(codexLastUsage, false);
|
|
1310
|
+
const cumulativeSnapshot = firstObject2(payloadInfo?.total_token_usage);
|
|
1311
|
+
const cumulativeUsage = cumulativeSnapshot ? tokenUsageFromObject(cumulativeSnapshot, true) : void 0;
|
|
1312
|
+
return usage2 ? { ...usage2, cumulativeSnapshot: cumulativeUsage ? stripSnapshot(cumulativeUsage) : void 0 } : void 0;
|
|
1289
1313
|
}
|
|
1290
1314
|
const codexTotalUsage = firstObject2(payloadInfo?.total_token_usage);
|
|
1291
1315
|
if (codexTotalUsage) {
|
|
@@ -1295,6 +1319,45 @@ function extractRecordTokens(record) {
|
|
|
1295
1319
|
const usage = firstObject2(object.usage, object.token_usage, object.tokens, response?.usage, payload?.usage, payload?.token_usage, payload?.tokens);
|
|
1296
1320
|
return tokenUsageFromObject(usage ?? object, false);
|
|
1297
1321
|
}
|
|
1322
|
+
function filterDuplicateDirectUsages(usages) {
|
|
1323
|
+
const filtered = [];
|
|
1324
|
+
let previous;
|
|
1325
|
+
for (const usage of usages) {
|
|
1326
|
+
const snapshot = usage.cumulativeSnapshot;
|
|
1327
|
+
if (snapshot && previous) {
|
|
1328
|
+
if (sameTokenUsage(snapshot, previous)) continue;
|
|
1329
|
+
if (!isMonotonicFrom(snapshot, previous) && looksLikeStaleRegression(snapshot, previous, usage)) continue;
|
|
1330
|
+
}
|
|
1331
|
+
filtered.push(usage);
|
|
1332
|
+
if (snapshot) previous = snapshot;
|
|
1333
|
+
}
|
|
1334
|
+
return filtered;
|
|
1335
|
+
}
|
|
1336
|
+
function sameTokenUsage(a, b) {
|
|
1337
|
+
return a.inputTokens === b.inputTokens && a.cachedInputTokens === b.cachedInputTokens && a.outputTokens === b.outputTokens && a.reasoningTokens === b.reasoningTokens && a.totalTokens === b.totalTokens;
|
|
1338
|
+
}
|
|
1339
|
+
function isMonotonicFrom(current, previous) {
|
|
1340
|
+
return current.inputTokens >= previous.inputTokens && current.cachedInputTokens >= previous.cachedInputTokens && current.outputTokens >= previous.outputTokens && current.reasoningTokens >= previous.reasoningTokens;
|
|
1341
|
+
}
|
|
1342
|
+
function looksLikeStaleRegression(current, previous, last) {
|
|
1343
|
+
const previousTotal = tokenBucketTotal(previous);
|
|
1344
|
+
const currentTotal = tokenBucketTotal(current);
|
|
1345
|
+
const lastTotal = tokenBucketTotal(last);
|
|
1346
|
+
if (previousTotal <= 0 || currentTotal <= 0 || lastTotal <= 0) return false;
|
|
1347
|
+
return currentTotal * 100 >= previousTotal * 98 || currentTotal * 4 >= previousTotal * 3 && currentTotal + lastTotal * 2 >= previousTotal;
|
|
1348
|
+
}
|
|
1349
|
+
function tokenBucketTotal(usage) {
|
|
1350
|
+
return usage.inputTokens + usage.cachedInputTokens + usage.outputTokens + usage.reasoningTokens;
|
|
1351
|
+
}
|
|
1352
|
+
function stripSnapshot(usage) {
|
|
1353
|
+
return {
|
|
1354
|
+
inputTokens: usage.inputTokens,
|
|
1355
|
+
cachedInputTokens: usage.cachedInputTokens,
|
|
1356
|
+
outputTokens: usage.outputTokens,
|
|
1357
|
+
reasoningTokens: usage.reasoningTokens,
|
|
1358
|
+
totalTokens: usage.totalTokens
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1298
1361
|
function tokenUsageFromObject(target, cumulative) {
|
|
1299
1362
|
const input = numberFromKeys(target, ["input_tokens", "inputTokens", "prompt_tokens", "promptTokens"]);
|
|
1300
1363
|
const cached = numberFromKeys(target, ["cached_input_tokens", "cachedInputTokens", "cached_tokens", "cachedTokens", "cache_read_input_tokens"]);
|
|
@@ -1354,12 +1417,9 @@ function scanCodex(options) {
|
|
|
1354
1417
|
if (stateExists) {
|
|
1355
1418
|
const stateSessions = readThreads(statePath, sessionFiles, options, warnings);
|
|
1356
1419
|
sessions.push(...stateSessions);
|
|
1357
|
-
if (!stateSessions.length && sessionsExists) {
|
|
1358
|
-
sessions.push(...readStandaloneSessions(sessionFiles, options));
|
|
1359
|
-
}
|
|
1360
1420
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
sessions.push(...readStandaloneSessions(sessionFiles, options));
|
|
1421
|
+
if (sessionsExists) {
|
|
1422
|
+
sessions.push(...readStandaloneSessions(sessionFiles, options).filter((session) => !hasImportedSession(sessions, session)));
|
|
1363
1423
|
}
|
|
1364
1424
|
return {
|
|
1365
1425
|
source: {
|
|
@@ -1402,8 +1462,10 @@ function readThreads(statePath, sessionFiles, options, warnings) {
|
|
|
1402
1462
|
warnings.push("Codex threads table has no recognized columns.");
|
|
1403
1463
|
return [];
|
|
1404
1464
|
}
|
|
1465
|
+
const importedModels = readImportedModelMap(options.codexHome ?? path4.join(os2.homedir(), ".codex"), warnings);
|
|
1405
1466
|
const rows = db.prepare(`SELECT ${selectedColumns.map((column) => `"${column}"`).join(", ")} FROM threads`).all();
|
|
1406
|
-
|
|
1467
|
+
const sessions = rows.map((row, index) => threadToUsage(row, index, sessionFiles, options, importedModels));
|
|
1468
|
+
return attributeSubagentsToParentSurfaces(sessions, rows);
|
|
1407
1469
|
} catch (error) {
|
|
1408
1470
|
warnings.push(`Unable to read Codex SQLite state at ${statePath}: ${errorMessage(error)}`);
|
|
1409
1471
|
return [];
|
|
@@ -1411,6 +1473,37 @@ function readThreads(statePath, sessionFiles, options, warnings) {
|
|
|
1411
1473
|
db?.close();
|
|
1412
1474
|
}
|
|
1413
1475
|
}
|
|
1476
|
+
function attributeSubagentsToParentSurfaces(sessions, rows) {
|
|
1477
|
+
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
1478
|
+
const parentIdsByChildId = /* @__PURE__ */ new Map();
|
|
1479
|
+
for (const row of rows) {
|
|
1480
|
+
const childId = stringValue2(row.id);
|
|
1481
|
+
const parentId = codexParentThreadId(stringValue2(row.source));
|
|
1482
|
+
if (childId && parentId) parentIdsByChildId.set(childId, parentId);
|
|
1483
|
+
}
|
|
1484
|
+
return sessions.map((session) => {
|
|
1485
|
+
if (session.sourceApp !== "Codex subagent") return session;
|
|
1486
|
+
const parent = sessionsById.get(parentIdsByChildId.get(session.id) ?? "");
|
|
1487
|
+
if (!parent || parent.sourceApp === "Codex subagent") return session;
|
|
1488
|
+
return {
|
|
1489
|
+
...session,
|
|
1490
|
+
sourceApp: parent.sourceApp,
|
|
1491
|
+
sourceAppRaw: session.sourceAppRaw,
|
|
1492
|
+
surfaceReason: `subagent attributed to parent thread ${parent.id}: ${parent.surfaceReason}`
|
|
1493
|
+
};
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
function codexParentThreadId(source) {
|
|
1497
|
+
if (!source?.trim().startsWith("{")) return void 0;
|
|
1498
|
+
try {
|
|
1499
|
+
const object = asRecord(JSON.parse(source));
|
|
1500
|
+
const subagent = asRecord(object?.subagent);
|
|
1501
|
+
const threadSpawn = asRecord(subagent?.thread_spawn);
|
|
1502
|
+
return stringValue2(threadSpawn?.parent_thread_id);
|
|
1503
|
+
} catch {
|
|
1504
|
+
return void 0;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1414
1507
|
function openCodexStateDatabase(statePath, warnings) {
|
|
1415
1508
|
try {
|
|
1416
1509
|
return new Database(statePath, { readonly: true, fileMustExist: true });
|
|
@@ -1475,24 +1568,30 @@ function isNativeSqliteMismatch(error) {
|
|
|
1475
1568
|
function exitLabel(status) {
|
|
1476
1569
|
return status === null ? "" : ` with exit code ${status}`;
|
|
1477
1570
|
}
|
|
1478
|
-
function threadToUsage(row, index, sessionFiles, options) {
|
|
1571
|
+
function threadToUsage(row, index, sessionFiles, options, importedModels = /* @__PURE__ */ new Map()) {
|
|
1572
|
+
const id = stringValue2(row.id) ?? `codex-thread-${index}`;
|
|
1479
1573
|
const sourcePath = stringValue2(row.rollout_path) ?? matchSessionPath(stringValue2(row.id), sessionFiles) ?? "";
|
|
1480
1574
|
const sessionBreakdown = sourcePath ? readSessionBreakdown(sourcePath) : emptyBreakdown();
|
|
1481
1575
|
const dbTokens = parseTokenTotal(row.tokens_used);
|
|
1482
|
-
const cwd = stringValue2(row.cwd) ?? fallbackCwdFromPath(sourcePath);
|
|
1576
|
+
const cwd = stringValue2(row.cwd) ?? sessionBreakdown.cwd ?? fallbackCwdFromPath(sourcePath);
|
|
1483
1577
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
1578
|
+
const inferredModel = stringValue2(row.model) || sessionBreakdown.model ? void 0 : importedModels.get(id);
|
|
1579
|
+
const model = stringValue2(row.model) ?? sessionBreakdown.model ?? inferredModel?.model;
|
|
1580
|
+
const provider = sessionBreakdown.provider ?? inferredModel?.provider ?? stringValue2(row.model_provider);
|
|
1484
1581
|
const inputTokens = sessionBreakdown.inputTokens;
|
|
1485
1582
|
const cachedInputTokens = sessionBreakdown.cachedInputTokens;
|
|
1486
1583
|
const outputTokens = sessionBreakdown.outputTokens;
|
|
1487
1584
|
const reasoningTokens = sessionBreakdown.reasoningTokens;
|
|
1488
1585
|
const totalTokens = sessionBreakdown.totalTokens || dbTokens || inputTokens + outputTokens + reasoningTokens;
|
|
1489
1586
|
const hasPricedBreakdown = inputTokens + cachedInputTokens + outputTokens + reasoningTokens > 0;
|
|
1490
|
-
const warnings = [...repo.warnings, ...sessionBreakdown.warnings];
|
|
1587
|
+
const warnings = [...repo.warnings, ...sessionBreakdown.warnings, ...inferredModel ? ["model_inferred_from_import_source"] : []];
|
|
1491
1588
|
const threadSource = stringValue2(row.thread_source) ?? sessionBreakdown.threadSource;
|
|
1492
|
-
const sourceAppRaw = stringValue2(row.source)
|
|
1589
|
+
const sourceAppRaw = codexSourceRaw(stringValue2(row.source), sessionBreakdown.source, sessionBreakdown.originator, threadSource);
|
|
1493
1590
|
const surface = detectSurface(sourceAppRaw, threadSource);
|
|
1591
|
+
const startedAt = dateValue2(row.created_at) ?? sessionBreakdown.startedAt;
|
|
1592
|
+
const endedAt = dateValue2(row.updated_at) ?? sessionBreakdown.endedAt;
|
|
1494
1593
|
const usage = {
|
|
1495
|
-
id
|
|
1594
|
+
id,
|
|
1496
1595
|
sourceClient: "codex",
|
|
1497
1596
|
sourceApp: codexSourceAppLabel(sourceAppRaw, threadSource),
|
|
1498
1597
|
sourceAppRaw,
|
|
@@ -1502,11 +1601,11 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1502
1601
|
cwd,
|
|
1503
1602
|
gitRemoteUrl: repo.gitRemoteUrl,
|
|
1504
1603
|
gitBranch: repo.gitBranch,
|
|
1505
|
-
title: stringValue2(row.title),
|
|
1506
|
-
startedAt
|
|
1507
|
-
endedAt
|
|
1508
|
-
model
|
|
1509
|
-
provider
|
|
1604
|
+
title: stringValue2(row.title) ?? sessionBreakdown.title,
|
|
1605
|
+
startedAt,
|
|
1606
|
+
endedAt,
|
|
1607
|
+
model,
|
|
1608
|
+
provider,
|
|
1510
1609
|
inputTokens,
|
|
1511
1610
|
cachedInputTokens,
|
|
1512
1611
|
outputTokens,
|
|
@@ -1521,7 +1620,7 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1521
1620
|
rawTokenTotal: dbTokens || sessionBreakdown.totalTokens || void 0,
|
|
1522
1621
|
warnings,
|
|
1523
1622
|
codexHome: options.codexHome ?? path4.join(os2.homedir(), ".codex"),
|
|
1524
|
-
durationMs: durationMs(
|
|
1623
|
+
durationMs: durationMs(startedAt, endedAt),
|
|
1525
1624
|
rawEventCount: sessionBreakdown.rawEventCount,
|
|
1526
1625
|
parseStatus: sessionBreakdown.parseStatus,
|
|
1527
1626
|
parseErrors: sessionBreakdown.parseErrors,
|
|
@@ -1559,26 +1658,28 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1559
1658
|
function readStandaloneSessions(sessionFiles, options) {
|
|
1560
1659
|
return [...sessionFiles.entries()].map(([id, sourcePath]) => {
|
|
1561
1660
|
const breakdown = readSessionBreakdown(sourcePath);
|
|
1562
|
-
const cwd = fallbackCwdFromPath(sourcePath);
|
|
1661
|
+
const cwd = breakdown.cwd ?? fallbackCwdFromPath(sourcePath);
|
|
1563
1662
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
1564
|
-
const model =
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1663
|
+
const model = breakdown.model;
|
|
1664
|
+
const provider = breakdown.provider ?? providerForModel(model ?? "");
|
|
1665
|
+
const sourceAppRaw = codexSourceRaw(void 0, breakdown.source, breakdown.originator, breakdown.threadSource);
|
|
1666
|
+
const surface = detectSurface(sourceAppRaw, breakdown.threadSource);
|
|
1667
|
+
const usage = {
|
|
1668
|
+
id: breakdown.id ?? id,
|
|
1568
1669
|
sourceClient: "codex",
|
|
1569
|
-
sourceApp: codexSourceAppLabel(
|
|
1570
|
-
sourceAppRaw
|
|
1670
|
+
sourceApp: codexSourceAppLabel(sourceAppRaw, breakdown.threadSource),
|
|
1671
|
+
sourceAppRaw,
|
|
1571
1672
|
sourcePath,
|
|
1572
1673
|
repoRoot: repo.repoRoot,
|
|
1573
1674
|
repoName: repo.repoName,
|
|
1574
1675
|
cwd,
|
|
1575
1676
|
gitRemoteUrl: repo.gitRemoteUrl,
|
|
1576
1677
|
gitBranch: repo.gitBranch,
|
|
1577
|
-
title:
|
|
1578
|
-
startedAt:
|
|
1579
|
-
endedAt:
|
|
1678
|
+
title: breakdown.title,
|
|
1679
|
+
startedAt: breakdown.startedAt,
|
|
1680
|
+
endedAt: breakdown.endedAt,
|
|
1580
1681
|
model,
|
|
1581
|
-
provider
|
|
1682
|
+
provider,
|
|
1582
1683
|
inputTokens: breakdown.inputTokens,
|
|
1583
1684
|
cachedInputTokens: breakdown.cachedInputTokens,
|
|
1584
1685
|
outputTokens: breakdown.outputTokens,
|
|
@@ -1591,8 +1692,9 @@ function readStandaloneSessions(sessionFiles, options) {
|
|
|
1591
1692
|
estimatedCostUsd: void 0,
|
|
1592
1693
|
messageCount: breakdown.messageCount,
|
|
1593
1694
|
rawTokenTotal: breakdown.totalTokens || void 0,
|
|
1594
|
-
warnings: [...repo.warnings, ...breakdown.warnings
|
|
1695
|
+
warnings: [...repo.warnings, ...breakdown.warnings],
|
|
1595
1696
|
codexHome: options.codexHome ?? path4.join(os2.homedir(), ".codex"),
|
|
1697
|
+
durationMs: durationMs(breakdown.startedAt, breakdown.endedAt),
|
|
1596
1698
|
rawEventCount: breakdown.rawEventCount,
|
|
1597
1699
|
parseStatus: breakdown.parseStatus,
|
|
1598
1700
|
parseErrors: breakdown.parseErrors,
|
|
@@ -1619,8 +1721,87 @@ function readStandaloneSessions(sessionFiles, options) {
|
|
|
1619
1721
|
sessionOutcome: inferOutcome(breakdown),
|
|
1620
1722
|
compaction: breakdown.compaction
|
|
1621
1723
|
};
|
|
1724
|
+
const hasPricedBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
|
|
1725
|
+
const cost = hasPricedBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
1726
|
+
return {
|
|
1727
|
+
...usage,
|
|
1728
|
+
estimatedCostUsd: cost,
|
|
1729
|
+
warnings: cost === void 0 ? [...usage.warnings, hasPricedBreakdown ? "unknown_pricing" : "missing_token_breakdown"] : usage.warnings
|
|
1730
|
+
};
|
|
1622
1731
|
});
|
|
1623
1732
|
}
|
|
1733
|
+
function hasImportedSession(sessions, candidate) {
|
|
1734
|
+
const candidatePath = comparableSourcePath(candidate.sourcePath);
|
|
1735
|
+
return sessions.some((session) => {
|
|
1736
|
+
if (session.id && session.id === candidate.id) return true;
|
|
1737
|
+
const sessionPath = comparableSourcePath(session.sourcePath);
|
|
1738
|
+
return Boolean(candidatePath && sessionPath && candidatePath === sessionPath);
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
function readImportedModelMap(codexHome, warnings) {
|
|
1742
|
+
const importsPath = path4.join(codexHome, "external_agent_session_imports.json");
|
|
1743
|
+
const models = /* @__PURE__ */ new Map();
|
|
1744
|
+
if (!fs4.existsSync(importsPath)) return models;
|
|
1745
|
+
let parsed;
|
|
1746
|
+
try {
|
|
1747
|
+
parsed = JSON.parse(fs4.readFileSync(importsPath, "utf8"));
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
warnings.push(`Unable to read Codex external import index at ${importsPath}: ${errorMessage(error)}`);
|
|
1750
|
+
return models;
|
|
1751
|
+
}
|
|
1752
|
+
const records = Array.isArray(asRecord(parsed)?.records) ? asRecord(parsed)?.records : [];
|
|
1753
|
+
for (const record of records) {
|
|
1754
|
+
const item = asRecord(record);
|
|
1755
|
+
const importedThreadId = stringValue2(item?.imported_thread_id);
|
|
1756
|
+
const sourcePath = normalizeVerbatimPath(stringValue2(item?.source_path));
|
|
1757
|
+
if (!importedThreadId || !sourcePath || !fs4.existsSync(sourcePath)) continue;
|
|
1758
|
+
const model = readImportedSourceModel(sourcePath, warnings);
|
|
1759
|
+
if (model) models.set(importedThreadId, { model, provider: providerForModel(model) });
|
|
1760
|
+
}
|
|
1761
|
+
return models;
|
|
1762
|
+
}
|
|
1763
|
+
function readImportedSourceModel(sourcePath, warnings) {
|
|
1764
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1765
|
+
let content;
|
|
1766
|
+
try {
|
|
1767
|
+
content = fs4.readFileSync(sourcePath, "utf8");
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
warnings.push(`Unable to read Codex external import source at ${sourcePath}: ${errorMessage(error)}`);
|
|
1770
|
+
return void 0;
|
|
1771
|
+
}
|
|
1772
|
+
for (const [index, line] of content.split(/\r?\n/).entries()) {
|
|
1773
|
+
if (!line.trim()) continue;
|
|
1774
|
+
try {
|
|
1775
|
+
const model = modelFromImportedRecord(JSON.parse(line));
|
|
1776
|
+
if (model) counts.set(model, (counts.get(model) ?? 0) + 1);
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
warnings.push(`Unable to parse Codex external import source ${sourcePath} line ${index + 1}: ${errorMessage(error)}`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0];
|
|
1782
|
+
}
|
|
1783
|
+
function modelFromImportedRecord(record) {
|
|
1784
|
+
const object = asRecord(record);
|
|
1785
|
+
const type = stringValue2(object?.type) ?? stringValue2(asRecord(object?.payload)?.type);
|
|
1786
|
+
const message = asRecord(object?.message) ?? asRecord(asRecord(object?.payload)?.message);
|
|
1787
|
+
const model = stringValue2(message?.model) ?? stringValue2(object?.model);
|
|
1788
|
+
if (!model || model === "<synthetic>") return void 0;
|
|
1789
|
+
if (type && type !== "assistant") return void 0;
|
|
1790
|
+
return model;
|
|
1791
|
+
}
|
|
1792
|
+
function providerForModel(model) {
|
|
1793
|
+
const normalized = model.toLowerCase();
|
|
1794
|
+
if (!normalized) return void 0;
|
|
1795
|
+
if (normalized.startsWith("claude")) return "anthropic";
|
|
1796
|
+
if (normalized.startsWith("gpt") || normalized.startsWith("o")) return "openai";
|
|
1797
|
+
return void 0;
|
|
1798
|
+
}
|
|
1799
|
+
function normalizeVerbatimPath(value) {
|
|
1800
|
+
return value?.replace(/^\\\\\?\\/, "");
|
|
1801
|
+
}
|
|
1802
|
+
function comparableSourcePath(value) {
|
|
1803
|
+
return normalizeVerbatimPath(value)?.replace(/\\/g, "/");
|
|
1804
|
+
}
|
|
1624
1805
|
function indexSessionFiles(sessionsPath, warnings) {
|
|
1625
1806
|
const files = /* @__PURE__ */ new Map();
|
|
1626
1807
|
try {
|
|
@@ -1657,6 +1838,7 @@ function readSessionBreakdown(filePath) {
|
|
|
1657
1838
|
let responseMessageCount = 0;
|
|
1658
1839
|
let lastUserMessageText;
|
|
1659
1840
|
for (const record of records) {
|
|
1841
|
+
applySessionBounds(breakdown, record);
|
|
1660
1842
|
applySessionMetadata(breakdown, record);
|
|
1661
1843
|
applyPromptTimeline(breakdown, record);
|
|
1662
1844
|
if (isCodexEventMessage(record)) eventMessageCount += 1;
|
|
@@ -1739,6 +1921,13 @@ function matchSessionPath(id, sessionFiles) {
|
|
|
1739
1921
|
}
|
|
1740
1922
|
function emptyBreakdown() {
|
|
1741
1923
|
return {
|
|
1924
|
+
id: void 0,
|
|
1925
|
+
cwd: void 0,
|
|
1926
|
+
title: void 0,
|
|
1927
|
+
startedAt: void 0,
|
|
1928
|
+
endedAt: void 0,
|
|
1929
|
+
model: void 0,
|
|
1930
|
+
provider: void 0,
|
|
1742
1931
|
inputTokens: 0,
|
|
1743
1932
|
cachedInputTokens: 0,
|
|
1744
1933
|
outputTokens: 0,
|
|
@@ -1783,10 +1972,26 @@ function applySessionMetadata(breakdown, record) {
|
|
|
1783
1972
|
if (!record || typeof record !== "object") return;
|
|
1784
1973
|
const object = record;
|
|
1785
1974
|
const payload = firstObject3(object.payload) ?? object;
|
|
1975
|
+
const collaborationMode = firstObject3(payload.collaboration_mode);
|
|
1976
|
+
const collaborationSettings = firstObject3(collaborationMode?.settings);
|
|
1977
|
+
breakdown.id ??= stringValue2(payload.id);
|
|
1978
|
+
breakdown.cwd ??= stringValue2(payload.cwd);
|
|
1979
|
+
breakdown.title ??= stringValue2(payload.title);
|
|
1980
|
+
breakdown.model ??= stringValue2(payload.model) ?? stringValue2(collaborationSettings?.model);
|
|
1981
|
+
breakdown.provider ??= stringValue2(payload.model_provider);
|
|
1786
1982
|
breakdown.source ??= stringValue2(payload.source);
|
|
1787
1983
|
breakdown.originator ??= stringValue2(payload.originator);
|
|
1788
1984
|
breakdown.threadSource ??= stringValue2(payload.thread_source);
|
|
1789
1985
|
}
|
|
1986
|
+
function applySessionBounds(breakdown, record) {
|
|
1987
|
+
if (!record || typeof record !== "object") return;
|
|
1988
|
+
const object = record;
|
|
1989
|
+
const payload = firstObject3(object.payload);
|
|
1990
|
+
const timestamp3 = dateValue2(object.timestamp) ?? dateValue2(payload?.timestamp);
|
|
1991
|
+
const sessionStartedAt = object.type === "session_meta" ? dateValue2(payload?.timestamp) ?? timestamp3 : timestamp3;
|
|
1992
|
+
if (sessionStartedAt && (!breakdown.startedAt || sessionStartedAt < breakdown.startedAt)) breakdown.startedAt = sessionStartedAt;
|
|
1993
|
+
if (timestamp3 && (!breakdown.endedAt || timestamp3 > breakdown.endedAt)) breakdown.endedAt = timestamp3;
|
|
1994
|
+
}
|
|
1790
1995
|
function isCodexEventMessage(record) {
|
|
1791
1996
|
if (!record || typeof record !== "object") return false;
|
|
1792
1997
|
const object = record;
|
|
@@ -1887,9 +2092,14 @@ function hasNearbyDuplicatePromptTimelineItem(items, item) {
|
|
|
1887
2092
|
return Math.abs(existingTime - itemTime) <= 5e3;
|
|
1888
2093
|
});
|
|
1889
2094
|
}
|
|
2095
|
+
function codexSourceRaw(source, sessionSource, originator, threadSource) {
|
|
2096
|
+
if (originator && /codex\s+desktop/i.test(originator)) return originator;
|
|
2097
|
+
return source ?? sessionSource ?? originator ?? threadSource;
|
|
2098
|
+
}
|
|
1890
2099
|
function detectSurface(source, threadSource) {
|
|
1891
2100
|
const raw = `${source ?? ""} ${threadSource ?? ""}`.toLowerCase();
|
|
1892
2101
|
if (threadSource === "subagent") return { surface: "codex_exec", confidence: "medium", reason: "thread_source is subagent" };
|
|
2102
|
+
if (raw.includes("codex desktop")) return { surface: "local_agent", confidence: "high", reason: "originator metadata identifies Codex Desktop" };
|
|
1893
2103
|
if (raw.includes("vscode")) return { surface: "vscode_extension", confidence: "high", reason: "source metadata includes vscode" };
|
|
1894
2104
|
if (raw.includes("cli") || raw.includes("terminal") || raw.includes("tui")) return { surface: "terminal_cli", confidence: "high", reason: "source metadata indicates CLI/terminal" };
|
|
1895
2105
|
if (raw.includes("exec")) return { surface: "codex_exec", confidence: "medium", reason: "source metadata includes exec" };
|
|
@@ -1913,9 +2123,13 @@ function durationMs(startedAt, endedAt) {
|
|
|
1913
2123
|
function stringValue2(value) {
|
|
1914
2124
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1915
2125
|
}
|
|
2126
|
+
function asRecord(value) {
|
|
2127
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2128
|
+
}
|
|
1916
2129
|
function codexSourceAppLabel(source, threadSource) {
|
|
1917
2130
|
if (threadSource === "subagent") return "Codex subagent";
|
|
1918
2131
|
if (!source) return "Codex";
|
|
2132
|
+
if (/codex\s+desktop/i.test(source)) return "Codex Desktop";
|
|
1919
2133
|
if (source === "vscode" || source === "codex_vscode" || source === "codex-vscode") return "VS Code";
|
|
1920
2134
|
if (source === "cli" || source === "terminal" || source === "codex-tui") return "Terminal";
|
|
1921
2135
|
if (source === "codex_app" || source === "codex-app") return "Codex app";
|
|
@@ -1952,9 +2166,9 @@ function scanClaude(options) {
|
|
|
1952
2166
|
const warnings = [];
|
|
1953
2167
|
const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
|
|
1954
2168
|
const historyExists = fs5.existsSync(historyPath);
|
|
1955
|
-
const seenUsageKeys = /* @__PURE__ */ new Set();
|
|
1956
2169
|
const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
1957
|
-
const
|
|
2170
|
+
const usageSelection = selectClaudeUsageOccurrences(orderedFiles.map(({ file }) => file));
|
|
2171
|
+
const sessions = orderedFiles.flatMap(({ file }, index) => claudeFileToUsages(file, index, options, usageSelection));
|
|
1958
2172
|
const historyEntryCount = countHistoryEntries(historyPath);
|
|
1959
2173
|
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
1960
2174
|
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
|
|
@@ -2060,20 +2274,25 @@ function walk2(root, maxDepth, onFile, depth = 0) {
|
|
|
2060
2274
|
}
|
|
2061
2275
|
}
|
|
2062
2276
|
}
|
|
2063
|
-
function
|
|
2064
|
-
const breakdown = readClaudeBreakdown(file.filePath,
|
|
2277
|
+
function claudeFileToUsages(file, index, options, usageSelection) {
|
|
2278
|
+
const breakdown = readClaudeBreakdown(file.filePath, usageSelection);
|
|
2279
|
+
const segments = segmentClaudeBreakdownByDayAndModel(breakdown);
|
|
2280
|
+
return segments.map(({ breakdown: segment, suffix }) => claudeBreakdownToUsage(file, index, options, segment, suffix));
|
|
2281
|
+
}
|
|
2282
|
+
function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "") {
|
|
2065
2283
|
const id = breakdown.id ?? (path5.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
|
|
2066
2284
|
const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
|
|
2067
2285
|
const cwd = breakdown.cwd ?? fallbackCwd;
|
|
2068
2286
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
2069
2287
|
const warnings = [...repo.warnings, ...breakdown.warnings];
|
|
2288
|
+
const entrypoint = breakdown.entrypoint ?? inferClaudeEntrypointFromPath(file.filePath);
|
|
2070
2289
|
if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
|
|
2071
2290
|
const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
|
|
2072
2291
|
const usage = {
|
|
2073
|
-
id
|
|
2292
|
+
id: `${id}${idSuffix}`,
|
|
2074
2293
|
sourceClient: "claude",
|
|
2075
|
-
sourceApp: claudeSourceAppLabel(
|
|
2076
|
-
sourceAppRaw:
|
|
2294
|
+
sourceApp: claudeSourceAppLabel(entrypoint),
|
|
2295
|
+
sourceAppRaw: entrypoint,
|
|
2077
2296
|
sourcePath: file.filePath,
|
|
2078
2297
|
repoRoot: repo.repoRoot,
|
|
2079
2298
|
repoName: repo.repoName,
|
|
@@ -2103,9 +2322,9 @@ function claudeFileToUsage(file, index, options, seenUsageKeys) {
|
|
|
2103
2322
|
rawEventCount: breakdown.rawEventCount,
|
|
2104
2323
|
parseStatus: breakdown.parseStatus,
|
|
2105
2324
|
parseErrors: breakdown.parseErrors,
|
|
2106
|
-
detectedSurface: claudeSurface(
|
|
2325
|
+
detectedSurface: claudeSurface(entrypoint),
|
|
2107
2326
|
surfaceConfidence: "medium",
|
|
2108
|
-
surfaceReason:
|
|
2327
|
+
surfaceReason: entrypoint ? `Claude Code entrypoint: ${entrypoint}` : "Claude Code transcript",
|
|
2109
2328
|
userPromptCount: breakdown.userPromptCount,
|
|
2110
2329
|
assistantMessageCount: breakdown.assistantMessageCount,
|
|
2111
2330
|
toolCallCount: breakdown.toolCallCount,
|
|
@@ -2132,7 +2351,10 @@ function claudeFileToUsage(file, index, options, seenUsageKeys) {
|
|
|
2132
2351
|
warnings: cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings
|
|
2133
2352
|
};
|
|
2134
2353
|
}
|
|
2135
|
-
function
|
|
2354
|
+
function inferClaudeEntrypointFromPath(filePath) {
|
|
2355
|
+
return filePath.split(path5.sep).includes("local-agent-mode-sessions") ? "local-agent" : void 0;
|
|
2356
|
+
}
|
|
2357
|
+
function readClaudeBreakdown(filePath, usageSelection) {
|
|
2136
2358
|
const breakdown = emptyBreakdown2();
|
|
2137
2359
|
try {
|
|
2138
2360
|
const text = fs5.readFileSync(filePath, "utf8");
|
|
@@ -2140,8 +2362,8 @@ function readClaudeBreakdown(filePath, seenUsageKeys) {
|
|
|
2140
2362
|
breakdown.rawEventCount = parse.records.length;
|
|
2141
2363
|
breakdown.parseErrors = parse.errors;
|
|
2142
2364
|
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
2143
|
-
for (const record of parse.records) {
|
|
2144
|
-
applyClaudeRecord(breakdown, record,
|
|
2365
|
+
for (const [recordIndex, record] of parse.records.entries()) {
|
|
2366
|
+
applyClaudeRecord(breakdown, record, usageSelection, occurrenceKey(filePath, recordIndex));
|
|
2145
2367
|
}
|
|
2146
2368
|
breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens;
|
|
2147
2369
|
} catch (error) {
|
|
@@ -2152,7 +2374,7 @@ function readClaudeBreakdown(filePath, seenUsageKeys) {
|
|
|
2152
2374
|
}
|
|
2153
2375
|
return breakdown;
|
|
2154
2376
|
}
|
|
2155
|
-
function applyClaudeRecord(breakdown, record,
|
|
2377
|
+
function applyClaudeRecord(breakdown, record, usageSelection, usageOccurrenceKey) {
|
|
2156
2378
|
const timestamp3 = dateValue3(record.timestamp);
|
|
2157
2379
|
if (timestamp3) {
|
|
2158
2380
|
breakdown.startedAt = minDate(breakdown.startedAt, timestamp3);
|
|
@@ -2175,21 +2397,70 @@ function applyClaudeRecord(breakdown, record, seenUsageKeys) {
|
|
|
2175
2397
|
const usageTokens = usage ? usageTotal(usage) : 0;
|
|
2176
2398
|
const model = normalizeClaudeModel(message.model);
|
|
2177
2399
|
if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
|
|
2178
|
-
|
|
2179
|
-
|
|
2400
|
+
const selectedUsage = usageTokens > 0 ? usageSelection.usageByOccurrence.get(usageOccurrenceKey) : void 0;
|
|
2401
|
+
if (selectedUsage) {
|
|
2402
|
+
applyUsage(breakdown, selectedUsage);
|
|
2403
|
+
breakdown.usageEvents.push({ timestamp: timestamp3, model, usage: selectedUsage });
|
|
2180
2404
|
}
|
|
2181
2405
|
applyContentSignals(breakdown, message.content);
|
|
2182
2406
|
}
|
|
2183
2407
|
if (looksFailedToolResult(record.toolUseResult)) breakdown.failedToolCallCount += 1;
|
|
2184
2408
|
}
|
|
2185
|
-
function
|
|
2409
|
+
function selectClaudeUsageOccurrences(files) {
|
|
2410
|
+
const usageByOccurrence = /* @__PURE__ */ new Map();
|
|
2411
|
+
const selectedByDedupKey = /* @__PURE__ */ new Map();
|
|
2412
|
+
for (const file of files) {
|
|
2413
|
+
let parse;
|
|
2414
|
+
try {
|
|
2415
|
+
parse = parseJsonLines2(fs5.readFileSync(file.filePath, "utf8"));
|
|
2416
|
+
} catch {
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
for (const [recordIndex, record] of parse.records.entries()) {
|
|
2420
|
+
const message = firstObject4(record.message);
|
|
2421
|
+
const usage = firstObject4(message?.usage);
|
|
2422
|
+
if (!message || !usage || usageTotal(usage) <= 0) continue;
|
|
2423
|
+
const occurrence = occurrenceKey(file.filePath, recordIndex);
|
|
2424
|
+
const dedupKey = usageDedupKey(record, message);
|
|
2425
|
+
if (!dedupKey) {
|
|
2426
|
+
usageByOccurrence.set(occurrence, usage);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
const selected = selectedByDedupKey.get(dedupKey);
|
|
2430
|
+
if (selected) {
|
|
2431
|
+
mergeUsageMax(selected.usage, usage);
|
|
2432
|
+
} else {
|
|
2433
|
+
selectedByDedupKey.set(dedupKey, { occurrence, usage: cloneUsage(usage) });
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
for (const selected of selectedByDedupKey.values()) {
|
|
2438
|
+
usageByOccurrence.set(selected.occurrence, selected.usage);
|
|
2439
|
+
}
|
|
2440
|
+
return { usageByOccurrence };
|
|
2441
|
+
}
|
|
2442
|
+
function usageDedupKey(record, message) {
|
|
2186
2443
|
const messageId = stringValue3(message.id);
|
|
2187
2444
|
const requestId = stringValue3(record.requestId);
|
|
2188
|
-
if (!messageId || !requestId) return
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
return
|
|
2445
|
+
if (!messageId || !requestId) return void 0;
|
|
2446
|
+
return `${requestId}:${messageId}`;
|
|
2447
|
+
}
|
|
2448
|
+
function occurrenceKey(filePath, recordIndex) {
|
|
2449
|
+
return `${path5.resolve(filePath)}:${recordIndex}`;
|
|
2450
|
+
}
|
|
2451
|
+
function cloneUsage(usage) {
|
|
2452
|
+
return {
|
|
2453
|
+
input_tokens: numberValue(usage.input_tokens),
|
|
2454
|
+
cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
|
|
2455
|
+
cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
|
|
2456
|
+
output_tokens: numberValue(usage.output_tokens)
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function mergeUsageMax(target, usage) {
|
|
2460
|
+
target.input_tokens = Math.max(numberValue(target.input_tokens), numberValue(usage.input_tokens));
|
|
2461
|
+
target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
|
|
2462
|
+
target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
|
|
2463
|
+
target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
|
|
2193
2464
|
}
|
|
2194
2465
|
function applyUsage(breakdown, usage) {
|
|
2195
2466
|
const input = numberValue(usage.input_tokens);
|
|
@@ -2202,6 +2473,54 @@ function applyUsage(breakdown, usage) {
|
|
|
2202
2473
|
breakdown.outputTokens += output;
|
|
2203
2474
|
breakdown.usageCount += 1;
|
|
2204
2475
|
}
|
|
2476
|
+
function segmentClaudeBreakdownByDayAndModel(breakdown) {
|
|
2477
|
+
const events = breakdown.usageEvents.filter((event) => event.timestamp);
|
|
2478
|
+
const keys = new Set(events.map((event) => segmentKey(event.timestamp, event.model)));
|
|
2479
|
+
if (keys.size <= 1) return [{ breakdown, suffix: "" }];
|
|
2480
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2481
|
+
for (const event of events) {
|
|
2482
|
+
const key = segmentKey(event.timestamp, event.model);
|
|
2483
|
+
const day = dateKey2(event.timestamp);
|
|
2484
|
+
const existing = groups.get(key) ?? { day, model: event.model, events: [] };
|
|
2485
|
+
existing.events.push(event);
|
|
2486
|
+
groups.set(key, existing);
|
|
2487
|
+
}
|
|
2488
|
+
return [...groups.values()].sort((a, b) => a.day.localeCompare(b.day) || (a.model ?? "").localeCompare(b.model ?? "")).map((group) => {
|
|
2489
|
+
const segment = cloneBreakdownMetadata(breakdown);
|
|
2490
|
+
segment.model = group.model ?? breakdown.model;
|
|
2491
|
+
segment.startedAt = group.events.map((event) => event.timestamp).filter(Boolean).sort()[0];
|
|
2492
|
+
segment.endedAt = group.events.map((event) => event.timestamp).filter(Boolean).sort().at(-1);
|
|
2493
|
+
segment.assistantMessageCount = group.events.length;
|
|
2494
|
+
segment.messageCount = group.events.length;
|
|
2495
|
+
segment.rawEventCount = group.events.length;
|
|
2496
|
+
for (const event of group.events) {
|
|
2497
|
+
applyUsage(segment, event.usage);
|
|
2498
|
+
segment.usageEvents.push(event);
|
|
2499
|
+
}
|
|
2500
|
+
segment.totalTokens = segment.inputTokens + segment.outputTokens;
|
|
2501
|
+
return { breakdown: segment, suffix: `#${group.day}${group.model ? `#${group.model}` : ""}` };
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
function cloneBreakdownMetadata(breakdown) {
|
|
2505
|
+
return {
|
|
2506
|
+
...emptyBreakdown2(),
|
|
2507
|
+
id: breakdown.id,
|
|
2508
|
+
title: breakdown.title,
|
|
2509
|
+
cwd: breakdown.cwd,
|
|
2510
|
+
gitBranch: breakdown.gitBranch,
|
|
2511
|
+
entrypoint: breakdown.entrypoint,
|
|
2512
|
+
version: breakdown.version,
|
|
2513
|
+
parseStatus: breakdown.parseStatus,
|
|
2514
|
+
parseErrors: [...breakdown.parseErrors],
|
|
2515
|
+
warnings: [...breakdown.warnings, "claude_session_split_by_activity_day"]
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
function segmentKey(timestamp3, model) {
|
|
2519
|
+
return `${dateKey2(timestamp3)}\0${model ?? ""}`;
|
|
2520
|
+
}
|
|
2521
|
+
function dateKey2(timestamp3) {
|
|
2522
|
+
return timestamp3?.slice(0, 10) ?? "unknown-date";
|
|
2523
|
+
}
|
|
2205
2524
|
function usageTotal(usage) {
|
|
2206
2525
|
return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + numberValue(usage.cache_creation_input_tokens) + numberValue(usage.output_tokens);
|
|
2207
2526
|
}
|
|
@@ -2261,7 +2580,8 @@ function emptyBreakdown2() {
|
|
|
2261
2580
|
parseStatus: "ok",
|
|
2262
2581
|
parseErrors: [],
|
|
2263
2582
|
promptTimeline: [],
|
|
2264
|
-
warnings: []
|
|
2583
|
+
warnings: [],
|
|
2584
|
+
usageEvents: []
|
|
2265
2585
|
};
|
|
2266
2586
|
}
|
|
2267
2587
|
function applyPromptTimeline2(breakdown, role, content, timestamp3) {
|
|
@@ -2308,13 +2628,13 @@ function inferOutcome2(breakdown) {
|
|
|
2308
2628
|
function claudeSurface(entrypoint) {
|
|
2309
2629
|
const value = entrypoint?.toLowerCase() ?? "";
|
|
2310
2630
|
if (value.includes("vscode")) return "vscode_extension";
|
|
2311
|
-
if (value.includes("local-agent")) return "local_agent";
|
|
2631
|
+
if (value.includes("local-agent") || value.includes("desktop")) return "local_agent";
|
|
2312
2632
|
return "terminal_cli";
|
|
2313
2633
|
}
|
|
2314
2634
|
function claudeSourceAppLabel(entrypoint) {
|
|
2315
2635
|
const value = entrypoint?.toLowerCase() ?? "";
|
|
2316
2636
|
if (value.includes("vscode")) return "VS Code";
|
|
2317
|
-
if (value.includes("local-agent")) return "Claude
|
|
2637
|
+
if (value.includes("local-agent") || value.includes("desktop")) return "Claude Desktop App";
|
|
2318
2638
|
if (value === "cli" || value.includes("terminal")) return "Terminal";
|
|
2319
2639
|
return "Claude Code";
|
|
2320
2640
|
}
|
|
@@ -2411,6 +2731,738 @@ function safeMtimeMs(filePath) {
|
|
|
2411
2731
|
}
|
|
2412
2732
|
}
|
|
2413
2733
|
|
|
2734
|
+
// packages/core/src/sources/cursor.ts
|
|
2735
|
+
import fs6 from "node:fs";
|
|
2736
|
+
import os4 from "node:os";
|
|
2737
|
+
import path6 from "node:path";
|
|
2738
|
+
import Database2 from "better-sqlite3";
|
|
2739
|
+
var parserVersion = "cursor-v1";
|
|
2740
|
+
var maxJsonlBytes = 20 * 1024 * 1024;
|
|
2741
|
+
var maxDatabaseBytes = 100 * 1024 * 1024;
|
|
2742
|
+
var cursorStoragePrefixes = ["cursor.", "anysphere."];
|
|
2743
|
+
var interestingTerms = ["cursor", "composer", "aichat", "ai_chat", "aiservice", "agent-transcript", "prompttokenbreakdown"];
|
|
2744
|
+
function scanCursor(options) {
|
|
2745
|
+
const cursorHome = options.cursorHome ?? path6.join(os4.homedir(), ".cursor");
|
|
2746
|
+
const discovery = discoverCursorFiles(cursorHome, Boolean(options.cursorHome));
|
|
2747
|
+
const jsonlCandidates = discovery.jsonlFiles.map((filePath) => cursorJsonlToCandidate(filePath, cursorHome));
|
|
2748
|
+
const databaseCandidates = discovery.databaseFiles.flatMap((filePath) => cursorDatabaseToCandidates(filePath, cursorHome, discovery.warnings));
|
|
2749
|
+
const candidates = dedupeCandidates([...jsonlCandidates, ...databaseCandidates]);
|
|
2750
|
+
const sessions = candidates.map((candidate, index) => candidateToUsage(candidate, index, options));
|
|
2751
|
+
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
2752
|
+
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
|
|
2753
|
+
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
|
|
2754
|
+
if (!fs6.existsSync(cursorHome)) {
|
|
2755
|
+
discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
|
|
2756
|
+
}
|
|
2757
|
+
if (!fs6.existsSync(discovery.statePath)) {
|
|
2758
|
+
discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
|
|
2759
|
+
}
|
|
2760
|
+
return {
|
|
2761
|
+
source: {
|
|
2762
|
+
id: "cursor",
|
|
2763
|
+
label: "Cursor",
|
|
2764
|
+
available: discovery.jsonlFiles.length > 0 || discovery.databaseFiles.length > 0,
|
|
2765
|
+
paths: unique4([cursorHome, discovery.projectsPath, discovery.statePath, discovery.workspaceStoragePath, ...discovery.roots]),
|
|
2766
|
+
warnings: discovery.warnings
|
|
2767
|
+
},
|
|
2768
|
+
sessions,
|
|
2769
|
+
stats: {
|
|
2770
|
+
sourceId: "cursor",
|
|
2771
|
+
sourceLabel: "Cursor",
|
|
2772
|
+
homePath: cursorHome,
|
|
2773
|
+
cursorHome,
|
|
2774
|
+
globalStoragePath: discovery.globalStoragePath,
|
|
2775
|
+
workspaceStoragePath: discovery.workspaceStoragePath,
|
|
2776
|
+
projectsPath: discovery.projectsPath,
|
|
2777
|
+
statePath: discovery.statePath,
|
|
2778
|
+
projectsExists: fs6.existsSync(discovery.projectsPath),
|
|
2779
|
+
stateExists: fs6.existsSync(discovery.statePath),
|
|
2780
|
+
sessionsExists: discovery.jsonlFiles.length > 0,
|
|
2781
|
+
sessionFileCount: discovery.jsonlFiles.length,
|
|
2782
|
+
databaseFileCount: discovery.databaseFiles.length,
|
|
2783
|
+
skippedFileCount: discovery.skippedFileCount,
|
|
2784
|
+
sessionsImported: sessions.length,
|
|
2785
|
+
parseFailureCount,
|
|
2786
|
+
unreadableFileCount,
|
|
2787
|
+
malformedFileCount,
|
|
2788
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
function discoverCursorFiles(cursorHome, explicitHome) {
|
|
2793
|
+
const warnings = [];
|
|
2794
|
+
const appPaths = defaultCursorAppPaths();
|
|
2795
|
+
const roots = explicitHome ? [cursorHome] : unique4([cursorHome, appPaths.statePath, ...cursorOwnedGlobalStorageRoots(appPaths.globalStoragePath), ...cursorOwnedWorkspaceStorageRoots(appPaths.workspaceStoragePath)]);
|
|
2796
|
+
const jsonlFiles = [];
|
|
2797
|
+
const databaseFiles = [];
|
|
2798
|
+
let skippedFileCount = 0;
|
|
2799
|
+
for (const root of roots) {
|
|
2800
|
+
if (!fs6.existsSync(root)) continue;
|
|
2801
|
+
try {
|
|
2802
|
+
if (isReadableRegularFile(root, maxDatabaseBytes)) {
|
|
2803
|
+
databaseFiles.push(root);
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
walk3(root, 10, (filePath) => {
|
|
2807
|
+
const lower = filePath.toLowerCase();
|
|
2808
|
+
if (lower.endsWith(".jsonl")) {
|
|
2809
|
+
if (isReadableRegularFile(filePath, maxJsonlBytes)) jsonlFiles.push(filePath);
|
|
2810
|
+
else skippedFileCount += 1;
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
if (/\.(sqlite|db|vscdb)$/i.test(filePath)) {
|
|
2814
|
+
if (isReadableRegularFile(filePath, maxDatabaseBytes)) databaseFiles.push(filePath);
|
|
2815
|
+
else skippedFileCount += 1;
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
warnings.push(`Unable to inspect Cursor path ${root}: ${errorMessage3(error)}`);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return {
|
|
2823
|
+
roots,
|
|
2824
|
+
projectsPath: path6.join(cursorHome, "projects"),
|
|
2825
|
+
globalStoragePath: appPaths.globalStoragePath,
|
|
2826
|
+
workspaceStoragePath: appPaths.workspaceStoragePath,
|
|
2827
|
+
statePath: appPaths.statePath,
|
|
2828
|
+
jsonlFiles: unique4(jsonlFiles).sort(cursorJsonlSort),
|
|
2829
|
+
databaseFiles: unique4(databaseFiles).sort(),
|
|
2830
|
+
skippedFileCount,
|
|
2831
|
+
warnings
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
function cursorOwnedGlobalStorageRoots(globalStoragePath) {
|
|
2835
|
+
return cursorOwnedChildDirs(globalStoragePath);
|
|
2836
|
+
}
|
|
2837
|
+
function cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
|
|
2838
|
+
if (!fs6.existsSync(workspaceStoragePath)) return [];
|
|
2839
|
+
const roots = [];
|
|
2840
|
+
try {
|
|
2841
|
+
for (const workspace of fs6.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
|
|
2842
|
+
if (!workspace.isDirectory()) continue;
|
|
2843
|
+
roots.push(...cursorOwnedChildDirs(path6.join(workspaceStoragePath, workspace.name)));
|
|
2844
|
+
}
|
|
2845
|
+
} catch {
|
|
2846
|
+
return [];
|
|
2847
|
+
}
|
|
2848
|
+
return roots;
|
|
2849
|
+
}
|
|
2850
|
+
function cursorOwnedChildDirs(root) {
|
|
2851
|
+
if (!fs6.existsSync(root)) return [];
|
|
2852
|
+
try {
|
|
2853
|
+
return fs6.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && cursorStoragePrefixes.some((prefix) => entry.name.toLowerCase().startsWith(prefix))).map((entry) => path6.join(root, entry.name));
|
|
2854
|
+
} catch {
|
|
2855
|
+
return [];
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
function defaultCursorAppPaths() {
|
|
2859
|
+
const home = os4.homedir();
|
|
2860
|
+
if (process.platform === "win32") {
|
|
2861
|
+
const appData = process.env.APPDATA ?? path6.join(home, "AppData", "Roaming");
|
|
2862
|
+
const globalStoragePath2 = path6.join(appData, "Cursor", "User", "globalStorage");
|
|
2863
|
+
return {
|
|
2864
|
+
globalStoragePath: globalStoragePath2,
|
|
2865
|
+
workspaceStoragePath: path6.join(appData, "Cursor", "User", "workspaceStorage"),
|
|
2866
|
+
statePath: path6.join(globalStoragePath2, "state.vscdb")
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
if (process.platform === "darwin") {
|
|
2870
|
+
const globalStoragePath2 = path6.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
|
|
2871
|
+
return {
|
|
2872
|
+
globalStoragePath: globalStoragePath2,
|
|
2873
|
+
workspaceStoragePath: path6.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
|
|
2874
|
+
statePath: path6.join(globalStoragePath2, "state.vscdb")
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
const xdg = process.env.XDG_CONFIG_HOME || path6.join(home, ".config");
|
|
2878
|
+
const globalStoragePath = path6.join(xdg, "Cursor", "User", "globalStorage");
|
|
2879
|
+
return {
|
|
2880
|
+
globalStoragePath,
|
|
2881
|
+
workspaceStoragePath: path6.join(xdg, "Cursor", "User", "workspaceStorage"),
|
|
2882
|
+
statePath: path6.join(globalStoragePath, "state.vscdb")
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
function walk3(root, maxDepth, onFile, depth = 0) {
|
|
2886
|
+
if (depth > maxDepth) return;
|
|
2887
|
+
for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
|
|
2888
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "CachedData" || entry.name === "Cache") continue;
|
|
2889
|
+
const fullPath = path6.join(root, entry.name);
|
|
2890
|
+
if (entry.isDirectory()) {
|
|
2891
|
+
walk3(fullPath, maxDepth, onFile, depth + 1);
|
|
2892
|
+
} else if (entry.isFile()) {
|
|
2893
|
+
onFile(fullPath);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
function isReadableRegularFile(filePath, maxBytes) {
|
|
2898
|
+
try {
|
|
2899
|
+
fs6.accessSync(filePath, fs6.constants.R_OK);
|
|
2900
|
+
const stat = fs6.statSync(filePath);
|
|
2901
|
+
return stat.isFile() && stat.size <= maxBytes;
|
|
2902
|
+
} catch {
|
|
2903
|
+
return false;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
function cursorJsonlSort(a, b) {
|
|
2907
|
+
const aTranscript = a.includes(`${path6.sep}agent-transcripts${path6.sep}`) ? 0 : 1;
|
|
2908
|
+
const bTranscript = b.includes(`${path6.sep}agent-transcripts${path6.sep}`) ? 0 : 1;
|
|
2909
|
+
return aTranscript - bTranscript || a.localeCompare(b);
|
|
2910
|
+
}
|
|
2911
|
+
function cursorJsonlToCandidate(filePath, cursorHome) {
|
|
2912
|
+
const breakdown = emptyBreakdown3();
|
|
2913
|
+
try {
|
|
2914
|
+
const parse = parseJsonLines3(fs6.readFileSync(filePath, "utf8"));
|
|
2915
|
+
breakdown.rawEventCount = parse.records.length;
|
|
2916
|
+
breakdown.parseErrors = parse.errors;
|
|
2917
|
+
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
2918
|
+
for (const record of parse.records) applyCursorRecord(breakdown, record);
|
|
2919
|
+
finalizeBreakdown(breakdown);
|
|
2920
|
+
} catch (error) {
|
|
2921
|
+
const message = `unable_to_read_cursor_jsonl:${errorMessage3(error)}`;
|
|
2922
|
+
breakdown.parseStatus = "failed";
|
|
2923
|
+
breakdown.parseErrors.push(message);
|
|
2924
|
+
breakdown.warnings.push(message);
|
|
2925
|
+
}
|
|
2926
|
+
if (!breakdown.id) breakdown.id = path6.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
|
|
2927
|
+
return {
|
|
2928
|
+
sourcePath: filePath,
|
|
2929
|
+
surface: inferSurfaceFromPath(filePath),
|
|
2930
|
+
fallbackCwd: inferProjectPathFromCursorPath(filePath, cursorHome),
|
|
2931
|
+
breakdown
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
function cursorDatabaseToCandidates(filePath, cursorHome, warnings) {
|
|
2935
|
+
const candidates = [];
|
|
2936
|
+
let db;
|
|
2937
|
+
try {
|
|
2938
|
+
db = new Database2(filePath, { readonly: true, fileMustExist: true });
|
|
2939
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
2940
|
+
for (const table of tables) {
|
|
2941
|
+
const columns = safeColumns(db, table.name, warnings);
|
|
2942
|
+
if (!columns.length) continue;
|
|
2943
|
+
const selectedColumns = columns.filter((column) => isLikelyTextColumn(column.name, column.type));
|
|
2944
|
+
if (!selectedColumns.length) continue;
|
|
2945
|
+
const rows = safeRows(db, table.name, selectedColumns.map((column) => column.name), warnings);
|
|
2946
|
+
for (const row of rows) {
|
|
2947
|
+
const match = candidateFromDatabaseRow(filePath, cursorHome, table.name, row);
|
|
2948
|
+
if (match) candidates.push(match);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${errorMessage3(error)}`);
|
|
2953
|
+
} finally {
|
|
2954
|
+
db?.close();
|
|
2955
|
+
}
|
|
2956
|
+
return candidates;
|
|
2957
|
+
}
|
|
2958
|
+
function safeColumns(db, table, warnings) {
|
|
2959
|
+
try {
|
|
2960
|
+
return db.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all();
|
|
2961
|
+
} catch (error) {
|
|
2962
|
+
warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${errorMessage3(error)}`);
|
|
2963
|
+
return [];
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
function safeRows(db, table, columns, warnings) {
|
|
2967
|
+
try {
|
|
2968
|
+
const select = columns.map(quoteIdentifier).join(", ");
|
|
2969
|
+
return db.prepare(`SELECT ${select} FROM ${quoteIdentifier(table)} LIMIT 500`).all();
|
|
2970
|
+
} catch (error) {
|
|
2971
|
+
warnings.push(`Unable to scan Cursor SQLite table ${table}: ${errorMessage3(error)}`);
|
|
2972
|
+
return [];
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
function quoteIdentifier(value) {
|
|
2976
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
2977
|
+
}
|
|
2978
|
+
function isLikelyTextColumn(name, type) {
|
|
2979
|
+
const normalized = `${name} ${type}`.toLowerCase();
|
|
2980
|
+
return type === "" || normalized.includes("text") || normalized.includes("char") || normalized.includes("json") || normalized.includes("blob") || interestingTerms.some((term) => normalized.includes(term));
|
|
2981
|
+
}
|
|
2982
|
+
function candidateFromDatabaseRow(filePath, cursorHome, table, row) {
|
|
2983
|
+
const records = [];
|
|
2984
|
+
const matchedKeys = [];
|
|
2985
|
+
const parseErrors = [];
|
|
2986
|
+
const textEntries = Object.entries(row).map(([key, value]) => ({ key, text: textValue(value) })).filter((entry) => Boolean(entry.text));
|
|
2987
|
+
const rowLooksInteresting = textEntries.some(({ key, text }) => looksCursorInteresting(key, text));
|
|
2988
|
+
if (!rowLooksInteresting) return void 0;
|
|
2989
|
+
for (const { key, text } of textEntries) {
|
|
2990
|
+
if (!looksCursorInteresting(key, text) && !text.trim().startsWith("{") && !text.trim().startsWith("[")) continue;
|
|
2991
|
+
matchedKeys.push(`${table}.${key}`);
|
|
2992
|
+
const parsed = parseJsonBlob(text);
|
|
2993
|
+
if (parsed.ok && Array.isArray(parsed.value)) records.push(...parsed.value);
|
|
2994
|
+
else if (parsed.ok) records.push(parsed.value);
|
|
2995
|
+
else records.push({ key, value: text.slice(0, 2e3) });
|
|
2996
|
+
if (!parsed.ok && text.trim().startsWith("{")) parseErrors.push(`sqlite_json_parse:${table}.${key}:${parsed.error}`);
|
|
2997
|
+
}
|
|
2998
|
+
if (!records.length) return void 0;
|
|
2999
|
+
const breakdown = emptyBreakdown3();
|
|
3000
|
+
breakdown.parseErrors = parseErrors;
|
|
3001
|
+
breakdown.parseStatus = parseErrors.length ? "partial" : "ok";
|
|
3002
|
+
breakdown.rawEventCount = records.length;
|
|
3003
|
+
breakdown.matchedKeys = unique4(matchedKeys);
|
|
3004
|
+
for (const record of records) applyCursorRecord(breakdown, record);
|
|
3005
|
+
finalizeBreakdown(breakdown);
|
|
3006
|
+
if (isCursorMetadataOnlyBreakdown(breakdown)) return void 0;
|
|
3007
|
+
return {
|
|
3008
|
+
sourcePath: `${filePath}#${table}`,
|
|
3009
|
+
surface: inferSurfaceFromPath(filePath),
|
|
3010
|
+
fallbackCwd: inferProjectPathFromCursorPath(filePath, cursorHome),
|
|
3011
|
+
breakdown
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
function isCursorMetadataOnlyBreakdown(breakdown) {
|
|
3015
|
+
return breakdown.messageCount === 0 && breakdown.usageCount === 0 && !breakdown.hasEstimatedPromptTokens && breakdown.costCount === 0;
|
|
3016
|
+
}
|
|
3017
|
+
function looksCursorInteresting(key, text) {
|
|
3018
|
+
const haystack = `${key}
|
|
3019
|
+
${text.slice(0, 4e3)}`.toLowerCase();
|
|
3020
|
+
return interestingTerms.some((term) => haystack.includes(term));
|
|
3021
|
+
}
|
|
3022
|
+
function parseJsonBlob(text) {
|
|
3023
|
+
const trimmed = text.trim();
|
|
3024
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return { ok: false, error: "not_json" };
|
|
3025
|
+
try {
|
|
3026
|
+
return { ok: true, value: JSON.parse(trimmed) };
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
return { ok: false, error: errorMessage3(error) };
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
function parseJsonLines3(text) {
|
|
3032
|
+
const records = [];
|
|
3033
|
+
const errors = [];
|
|
3034
|
+
text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter(({ line }) => Boolean(line)).forEach(({ line, index }) => {
|
|
3035
|
+
try {
|
|
3036
|
+
records.push(JSON.parse(line));
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage3(error)}`);
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
return { records, errors };
|
|
3042
|
+
}
|
|
3043
|
+
function applyCursorRecord(breakdown, record) {
|
|
3044
|
+
const objects = collectObjects(record, 6, 150);
|
|
3045
|
+
const root = firstObject5(record);
|
|
3046
|
+
const timestamp3 = firstValueByKeys(objects, ["timestamp", "createdAt", "created_at", "lastUpdatedAt", "conversationCheckpointLastUpdatedAt", "time", "date"]);
|
|
3047
|
+
const date = dateValue4(timestamp3);
|
|
3048
|
+
if (date) {
|
|
3049
|
+
breakdown.startedAt = minDate2(breakdown.startedAt, date);
|
|
3050
|
+
breakdown.endedAt = maxDate2(breakdown.endedAt, date);
|
|
3051
|
+
}
|
|
3052
|
+
breakdown.id ??= firstStringByKeys(objects, ["sessionId", "session_id", "conversationId", "conversation_id", "composerId", "composer_id", "chatId", "chat_id"]);
|
|
3053
|
+
breakdown.title ??= firstStringByKeys(objects, ["title", "name", "summary"]);
|
|
3054
|
+
breakdown.cwd ??= firstPathByKeys(objects, ["cwd", "workspacePath", "workspace_path", "projectPath", "project_path", "repoPath", "repo_path", "rootPath", "root_path"]);
|
|
3055
|
+
breakdown.model ??= firstStringByKeys(objects, ["model", "modelName", "model_name", "modelId", "model_id"]);
|
|
3056
|
+
const role = inferRole(objects, root);
|
|
3057
|
+
const text = extractMessageText3(objects, role);
|
|
3058
|
+
if (role === "user") breakdown.userPromptCount += 1;
|
|
3059
|
+
if (role === "assistant") breakdown.assistantMessageCount += 1;
|
|
3060
|
+
if (role === "user" || role === "assistant") {
|
|
3061
|
+
breakdown.messageCount += 1;
|
|
3062
|
+
if (text && breakdown.promptTimeline.length < 80) {
|
|
3063
|
+
breakdown.promptTimeline.push({ role, text: normalizeTimelineText3(text), timestamp: date });
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const usage = extractUsage(objects);
|
|
3067
|
+
if (usage.inputTokens + usage.cachedInputTokens + usage.outputTokens + usage.reasoningTokens + usage.totalTokens > 0) {
|
|
3068
|
+
breakdown.inputTokens += usage.inputTokens;
|
|
3069
|
+
breakdown.cachedInputTokens += usage.cachedInputTokens;
|
|
3070
|
+
breakdown.outputTokens += usage.outputTokens;
|
|
3071
|
+
breakdown.reasoningTokens += usage.reasoningTokens;
|
|
3072
|
+
breakdown.totalTokens += usage.totalTokens;
|
|
3073
|
+
breakdown.usageCount += 1;
|
|
3074
|
+
if (usage.estimatedPromptTokens) breakdown.hasEstimatedPromptTokens = true;
|
|
3075
|
+
}
|
|
3076
|
+
const cost = firstNumberByKeys(objects, ["costUsd", "cost_usd", "estimatedCostUsd", "estimated_cost_usd", "cost"]);
|
|
3077
|
+
if (cost !== void 0 && cost >= 0) {
|
|
3078
|
+
breakdown.estimatedCostUsd = roundCurrency2((breakdown.estimatedCostUsd ?? 0) + cost);
|
|
3079
|
+
breakdown.costCount += 1;
|
|
3080
|
+
}
|
|
3081
|
+
applyToolSignals(breakdown, objects);
|
|
3082
|
+
}
|
|
3083
|
+
function collectObjects(value, maxDepth, maxObjects, objects = []) {
|
|
3084
|
+
if (objects.length >= maxObjects || maxDepth < 0) return objects;
|
|
3085
|
+
if (!value || typeof value !== "object") return objects;
|
|
3086
|
+
if (Array.isArray(value)) {
|
|
3087
|
+
for (const item of value.slice(0, 80)) collectObjects(item, maxDepth - 1, maxObjects, objects);
|
|
3088
|
+
return objects;
|
|
3089
|
+
}
|
|
3090
|
+
const object = value;
|
|
3091
|
+
objects.push(object);
|
|
3092
|
+
for (const child of Object.values(object)) collectObjects(child, maxDepth - 1, maxObjects, objects);
|
|
3093
|
+
return objects;
|
|
3094
|
+
}
|
|
3095
|
+
function firstObject5(value) {
|
|
3096
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3097
|
+
}
|
|
3098
|
+
function firstStringByKeys(objects, keys) {
|
|
3099
|
+
for (const object of objects) {
|
|
3100
|
+
for (const key of keys) {
|
|
3101
|
+
const value = stringValue4(object[key]);
|
|
3102
|
+
if (value) return value;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
return void 0;
|
|
3106
|
+
}
|
|
3107
|
+
function firstValueByKeys(objects, keys) {
|
|
3108
|
+
for (const object of objects) {
|
|
3109
|
+
for (const key of keys) {
|
|
3110
|
+
if (object[key] !== void 0 && object[key] !== null && object[key] !== "") return object[key];
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
return void 0;
|
|
3114
|
+
}
|
|
3115
|
+
function firstPathByKeys(objects, keys) {
|
|
3116
|
+
const value = firstStringByKeys(objects, keys);
|
|
3117
|
+
if (!value) return void 0;
|
|
3118
|
+
if (value.startsWith("file://")) return value.replace(/^file:\/\//, "");
|
|
3119
|
+
return value;
|
|
3120
|
+
}
|
|
3121
|
+
function firstNumberByKeys(objects, keys) {
|
|
3122
|
+
for (const object of objects) {
|
|
3123
|
+
for (const key of keys) {
|
|
3124
|
+
const value = numberValue2(object[key]);
|
|
3125
|
+
if (value !== void 0) return value;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
return void 0;
|
|
3129
|
+
}
|
|
3130
|
+
function inferRole(objects, root) {
|
|
3131
|
+
const role = firstStringByKeys(objects, ["role", "author", "speaker", "from"])?.toLowerCase();
|
|
3132
|
+
if (role === "user" || role === "human") return "user";
|
|
3133
|
+
if (role === "assistant" || role === "ai" || role === "agent") return "assistant";
|
|
3134
|
+
const type = (stringValue4(root?.type) ?? stringValue4(root?.event) ?? "").toLowerCase();
|
|
3135
|
+
if (type.includes("user") || type.includes("prompt")) return "user";
|
|
3136
|
+
if (type.includes("assistant") || type.includes("response") || type.includes("completion")) return "assistant";
|
|
3137
|
+
return void 0;
|
|
3138
|
+
}
|
|
3139
|
+
function extractMessageText3(objects, role) {
|
|
3140
|
+
if (!role) return void 0;
|
|
3141
|
+
for (const object of objects) {
|
|
3142
|
+
for (const key of ["text", "content", "message", "prompt", "response", "answer"]) {
|
|
3143
|
+
const text = textFromUnknown(object[key]);
|
|
3144
|
+
if (text) return text;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
return void 0;
|
|
3148
|
+
}
|
|
3149
|
+
function textFromUnknown(value) {
|
|
3150
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
3151
|
+
if (Array.isArray(value)) {
|
|
3152
|
+
const parts = value.map(textFromUnknown).filter((item) => Boolean(item));
|
|
3153
|
+
return parts.length ? parts.join("\n") : void 0;
|
|
3154
|
+
}
|
|
3155
|
+
const object = firstObject5(value);
|
|
3156
|
+
if (!object) return void 0;
|
|
3157
|
+
return stringValue4(object.text) ?? stringValue4(object.content) ?? stringValue4(object.value);
|
|
3158
|
+
}
|
|
3159
|
+
function extractUsage(objects) {
|
|
3160
|
+
const usageObjects = objects.filter((object) => Object.keys(object).some((key) => key.toLowerCase().includes("token") || key.toLowerCase() === "usage"));
|
|
3161
|
+
const candidates = usageObjects.length ? usageObjects : objects;
|
|
3162
|
+
return {
|
|
3163
|
+
inputTokens: firstNumberByKeys(candidates, ["inputTokens", "input_tokens", "promptTokens", "prompt_tokens", "totalUsedTokens"]) ?? 0,
|
|
3164
|
+
cachedInputTokens: firstNumberByKeys(candidates, ["cachedInputTokens", "cached_input_tokens", "cacheReadInputTokens", "cache_read_input_tokens"]) ?? 0,
|
|
3165
|
+
outputTokens: firstNumberByKeys(candidates, ["outputTokens", "output_tokens", "completionTokens", "completion_tokens"]) ?? 0,
|
|
3166
|
+
reasoningTokens: firstNumberByKeys(candidates, ["reasoningTokens", "reasoning_tokens", "reasoningOutputTokens", "reasoning_output_tokens"]) ?? 0,
|
|
3167
|
+
totalTokens: firstNumberByKeys(candidates, ["totalTokens", "total_tokens", "tokens", "tokenCount", "token_count"]) ?? 0,
|
|
3168
|
+
estimatedPromptTokens: firstNumberByKeys(candidates, ["totalUsedTokens"]) !== void 0
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
function applyToolSignals(breakdown, objects) {
|
|
3172
|
+
const typeText = objects.flatMap((object) => [stringValue4(object.type), stringValue4(object.event), stringValue4(object.toolName), stringValue4(object.tool_name), stringValue4(object.name)]).filter((item) => Boolean(item)).join(" ").toLowerCase();
|
|
3173
|
+
if (typeText.includes("tool")) breakdown.toolCallCount += 1;
|
|
3174
|
+
if (typeText.includes("terminal") || typeText.includes("shell") || typeText.includes("command")) breakdown.shellCommandCount += 1;
|
|
3175
|
+
if (typeText.includes("read")) breakdown.fileReadCount += 1;
|
|
3176
|
+
if (typeText.includes("edit") || typeText.includes("write") || typeText.includes("apply")) breakdown.fileEditCount += 1;
|
|
3177
|
+
if (objects.some(looksFailed)) breakdown.failedToolCallCount += 1;
|
|
3178
|
+
}
|
|
3179
|
+
function looksFailed(object) {
|
|
3180
|
+
if (object.isError === true || object.is_error === true || object.error === true) return true;
|
|
3181
|
+
const status = stringValue4(object.status)?.toLowerCase();
|
|
3182
|
+
if (status === "failed" || status === "error") return true;
|
|
3183
|
+
const exitCode = numberValue2(object.exitCode ?? object.exit_code ?? object.statusCode ?? object.status_code);
|
|
3184
|
+
return exitCode !== void 0 && exitCode !== 0;
|
|
3185
|
+
}
|
|
3186
|
+
function finalizeBreakdown(breakdown) {
|
|
3187
|
+
if (breakdown.totalTokens === 0) {
|
|
3188
|
+
breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens;
|
|
3189
|
+
}
|
|
3190
|
+
if (breakdown.messageCount === 0) {
|
|
3191
|
+
breakdown.messageCount = breakdown.userPromptCount + breakdown.assistantMessageCount;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
function candidateToUsage(candidate, index, options) {
|
|
3195
|
+
const breakdown = candidate.breakdown;
|
|
3196
|
+
const id = breakdown.id ?? `${path6.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
|
|
3197
|
+
const cwd = breakdown.cwd ?? candidate.fallbackCwd ?? process.cwd();
|
|
3198
|
+
const repo = resolveRepoInfo(cwd, options.config);
|
|
3199
|
+
const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
|
|
3200
|
+
const tokenAggregationMethod = hasTokenBreakdown ? breakdown.hasEstimatedPromptTokens ? "estimated" : "direct_usage" : "unknown";
|
|
3201
|
+
const tokenWarnings = breakdown.hasEstimatedPromptTokens ? ["cursor_prompt_token_breakdown_estimate"] : [];
|
|
3202
|
+
const usage = {
|
|
3203
|
+
id,
|
|
3204
|
+
sourceClient: "cursor",
|
|
3205
|
+
sourceApp: cursorSourceAppLabel(candidate.surface),
|
|
3206
|
+
sourceAppRaw: candidate.surface,
|
|
3207
|
+
sourcePath: candidate.sourcePath,
|
|
3208
|
+
repoRoot: repo.repoRoot,
|
|
3209
|
+
repoName: repo.repoName,
|
|
3210
|
+
cwd,
|
|
3211
|
+
gitRemoteUrl: repo.gitRemoteUrl,
|
|
3212
|
+
gitBranch: repo.gitBranch,
|
|
3213
|
+
title: breakdown.title,
|
|
3214
|
+
startedAt: breakdown.startedAt,
|
|
3215
|
+
endedAt: breakdown.endedAt,
|
|
3216
|
+
model: breakdown.model,
|
|
3217
|
+
provider: providerForModel2(breakdown.model ?? ""),
|
|
3218
|
+
inputTokens: breakdown.inputTokens,
|
|
3219
|
+
cachedInputTokens: breakdown.cachedInputTokens,
|
|
3220
|
+
outputTokens: breakdown.outputTokens,
|
|
3221
|
+
reasoningTokens: breakdown.reasoningTokens,
|
|
3222
|
+
reasoningOutputTokens: breakdown.reasoningTokens,
|
|
3223
|
+
totalTokens: breakdown.totalTokens,
|
|
3224
|
+
tokenAggregationMethod,
|
|
3225
|
+
tokenConfidence: breakdown.hasEstimatedPromptTokens ? "low" : hasTokenBreakdown ? "medium" : "low",
|
|
3226
|
+
tokenSnapshotCount: breakdown.usageCount,
|
|
3227
|
+
estimatedCostUsd: void 0,
|
|
3228
|
+
messageCount: breakdown.messageCount,
|
|
3229
|
+
rawTokenTotal: breakdown.totalTokens || void 0,
|
|
3230
|
+
warnings: [...repo.warnings, ...breakdown.warnings, ...tokenWarnings, ...hasTokenBreakdown ? [] : ["missing_token_breakdown"]],
|
|
3231
|
+
durationMs: durationMs3(breakdown.startedAt, breakdown.endedAt),
|
|
3232
|
+
rawEventCount: breakdown.rawEventCount,
|
|
3233
|
+
parseStatus: breakdown.parseStatus,
|
|
3234
|
+
parseErrors: breakdown.parseErrors,
|
|
3235
|
+
detectedSurface: detectedSurface(candidate.surface),
|
|
3236
|
+
surfaceConfidence: candidate.surface === "unknown" ? "low" : "medium",
|
|
3237
|
+
surfaceReason: cursorSurfaceReason(candidate.surface),
|
|
3238
|
+
userPromptCount: breakdown.userPromptCount,
|
|
3239
|
+
assistantMessageCount: breakdown.assistantMessageCount,
|
|
3240
|
+
toolCallCount: breakdown.toolCallCount,
|
|
3241
|
+
shellCommandCount: breakdown.shellCommandCount,
|
|
3242
|
+
failedToolCallCount: breakdown.failedToolCallCount,
|
|
3243
|
+
nonZeroCommandEvents: 0,
|
|
3244
|
+
importantCommandFailures: breakdown.failedToolCallCount,
|
|
3245
|
+
harmlessNonZeroEvents: 0,
|
|
3246
|
+
exploratoryMisses: 0,
|
|
3247
|
+
repeatedFailureClusters: 0,
|
|
3248
|
+
commandIssueSeverity: breakdown.failedToolCallCount > 0 ? "warning" : "none",
|
|
3249
|
+
commandIssueImpact: breakdown.failedToolCallCount > 0 ? "medium" : "none",
|
|
3250
|
+
topFailureType: breakdown.failedToolCallCount > 0 ? "unknown" : void 0,
|
|
3251
|
+
commandIssueSamples: [],
|
|
3252
|
+
fileReadCount: breakdown.fileReadCount,
|
|
3253
|
+
fileEditCount: breakdown.fileEditCount,
|
|
3254
|
+
promptTimeline: breakdown.promptTimeline,
|
|
3255
|
+
sessionOutcome: inferOutcome3(breakdown),
|
|
3256
|
+
sourceMetadata: {
|
|
3257
|
+
parserVersion,
|
|
3258
|
+
cursor: {
|
|
3259
|
+
surface: candidate.surface,
|
|
3260
|
+
matchedKeys: breakdown.matchedKeys,
|
|
3261
|
+
costRecords: breakdown.costCount,
|
|
3262
|
+
estimatedPromptTokens: breakdown.hasEstimatedPromptTokens
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
const pricedCost = hasTokenBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
3267
|
+
const estimatedCostUsd = pricedCost ?? breakdown.estimatedCostUsd;
|
|
3268
|
+
return {
|
|
3269
|
+
...usage,
|
|
3270
|
+
estimatedCostUsd,
|
|
3271
|
+
warnings: estimatedCostUsd === void 0 ? [...usage.warnings, breakdown.hasEstimatedPromptTokens ? "cursor_cost_not_estimated_from_prompt_tokens" : hasTokenBreakdown ? "unknown_pricing" : "unknown_cost"] : usage.warnings
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
function inferProjectPathFromCursorPath(filePath, cursorHome) {
|
|
3275
|
+
const parts = filePath.split(path6.sep);
|
|
3276
|
+
const projectsIndex = parts.lastIndexOf("projects");
|
|
3277
|
+
if (projectsIndex >= 0 && projectsIndex < parts.length - 1) {
|
|
3278
|
+
const projectId = parts[projectsIndex + 1];
|
|
3279
|
+
if (projectId?.startsWith("-")) {
|
|
3280
|
+
return `${path6.sep}${projectId.slice(1).split("-").filter(Boolean).join(path6.sep)}`;
|
|
3281
|
+
}
|
|
3282
|
+
if (projectId) return path6.join(cursorHome, "projects", projectId);
|
|
3283
|
+
}
|
|
3284
|
+
return void 0;
|
|
3285
|
+
}
|
|
3286
|
+
function inferSurfaceFromPath(filePath) {
|
|
3287
|
+
const lower = filePath.toLowerCase();
|
|
3288
|
+
if (lower.includes(`${path6.sep}agent-transcripts${path6.sep}`) || lower.endsWith(".jsonl")) return "cli";
|
|
3289
|
+
if (lower.includes(`${path6.sep}workspaceStorage${path6.sep}`.toLowerCase()) || lower.includes(`${path6.sep}globalStorage${path6.sep}`.toLowerCase()) || lower.endsWith(".vscdb")) return "desktop";
|
|
3290
|
+
return "unknown";
|
|
3291
|
+
}
|
|
3292
|
+
function cursorSourceAppLabel(surface) {
|
|
3293
|
+
if (surface === "cli") return "Cursor CLI";
|
|
3294
|
+
if (surface === "desktop") return "Cursor Desktop";
|
|
3295
|
+
return "Cursor";
|
|
3296
|
+
}
|
|
3297
|
+
function detectedSurface(surface) {
|
|
3298
|
+
if (surface === "cli") return "terminal_cli";
|
|
3299
|
+
if (surface === "desktop") return "vscode_extension";
|
|
3300
|
+
return "unknown";
|
|
3301
|
+
}
|
|
3302
|
+
function cursorSurfaceReason(surface) {
|
|
3303
|
+
if (surface === "cli") return "Cursor local JSONL transcript";
|
|
3304
|
+
if (surface === "desktop") return "Cursor local SQLite/vscdb storage";
|
|
3305
|
+
return "Cursor local data";
|
|
3306
|
+
}
|
|
3307
|
+
function inferOutcome3(breakdown) {
|
|
3308
|
+
if (breakdown.parseStatus === "failed") return "failed";
|
|
3309
|
+
if (breakdown.failedToolCallCount > 0) return "partial";
|
|
3310
|
+
if (breakdown.fileEditCount > 0) return "completed";
|
|
3311
|
+
if (breakdown.shellCommandCount > 0) return "setup_debugging";
|
|
3312
|
+
if (breakdown.userPromptCount > 0 || breakdown.assistantMessageCount > 0) return "research_only";
|
|
3313
|
+
return "unknown";
|
|
3314
|
+
}
|
|
3315
|
+
function emptyBreakdown3() {
|
|
3316
|
+
return {
|
|
3317
|
+
id: void 0,
|
|
3318
|
+
title: void 0,
|
|
3319
|
+
cwd: void 0,
|
|
3320
|
+
startedAt: void 0,
|
|
3321
|
+
endedAt: void 0,
|
|
3322
|
+
model: void 0,
|
|
3323
|
+
inputTokens: 0,
|
|
3324
|
+
cachedInputTokens: 0,
|
|
3325
|
+
outputTokens: 0,
|
|
3326
|
+
reasoningTokens: 0,
|
|
3327
|
+
totalTokens: 0,
|
|
3328
|
+
usageCount: 0,
|
|
3329
|
+
estimatedCostUsd: void 0,
|
|
3330
|
+
costCount: 0,
|
|
3331
|
+
hasEstimatedPromptTokens: false,
|
|
3332
|
+
messageCount: 0,
|
|
3333
|
+
userPromptCount: 0,
|
|
3334
|
+
assistantMessageCount: 0,
|
|
3335
|
+
toolCallCount: 0,
|
|
3336
|
+
shellCommandCount: 0,
|
|
3337
|
+
failedToolCallCount: 0,
|
|
3338
|
+
fileReadCount: 0,
|
|
3339
|
+
fileEditCount: 0,
|
|
3340
|
+
rawEventCount: 0,
|
|
3341
|
+
parseStatus: "ok",
|
|
3342
|
+
parseErrors: [],
|
|
3343
|
+
promptTimeline: [],
|
|
3344
|
+
warnings: [],
|
|
3345
|
+
matchedKeys: []
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
function dedupeCandidates(candidates) {
|
|
3349
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3350
|
+
const pathOnly = [];
|
|
3351
|
+
for (const candidate of candidates) {
|
|
3352
|
+
const id = candidate.breakdown.id;
|
|
3353
|
+
if (!id) {
|
|
3354
|
+
pathOnly.push(candidate);
|
|
3355
|
+
continue;
|
|
3356
|
+
}
|
|
3357
|
+
const existing = byId.get(id);
|
|
3358
|
+
byId.set(id, existing ? mergeCursorCandidates(existing, candidate) : candidate);
|
|
3359
|
+
}
|
|
3360
|
+
return [...byId.values(), ...pathOnly];
|
|
3361
|
+
}
|
|
3362
|
+
function mergeCursorCandidates(a, b) {
|
|
3363
|
+
const primary = cursorCandidateScore(b) > cursorCandidateScore(a) ? b : a;
|
|
3364
|
+
const secondary = primary === a ? b : a;
|
|
3365
|
+
return {
|
|
3366
|
+
...primary,
|
|
3367
|
+
surface: primary.surface === "unknown" ? secondary.surface : primary.surface,
|
|
3368
|
+
fallbackCwd: primary.fallbackCwd ?? secondary.fallbackCwd,
|
|
3369
|
+
breakdown: mergeCursorBreakdowns(primary.breakdown, secondary.breakdown)
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
function cursorCandidateScore(candidate) {
|
|
3373
|
+
const breakdown = candidate.breakdown;
|
|
3374
|
+
const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
|
|
3375
|
+
return (hasTokenBreakdown ? 1e9 : 0) + breakdown.totalTokens + breakdown.messageCount * 1e3 + breakdown.rawEventCount;
|
|
3376
|
+
}
|
|
3377
|
+
function mergeCursorBreakdowns(primary, secondary) {
|
|
3378
|
+
return {
|
|
3379
|
+
...primary,
|
|
3380
|
+
title: primary.title ?? secondary.title,
|
|
3381
|
+
cwd: primary.cwd ?? secondary.cwd,
|
|
3382
|
+
startedAt: optionalMinDate(primary.startedAt, secondary.startedAt),
|
|
3383
|
+
endedAt: optionalMaxDate(primary.endedAt, secondary.endedAt),
|
|
3384
|
+
model: primary.model ?? secondary.model,
|
|
3385
|
+
estimatedCostUsd: primary.estimatedCostUsd ?? secondary.estimatedCostUsd,
|
|
3386
|
+
costCount: Math.max(primary.costCount, secondary.costCount),
|
|
3387
|
+
hasEstimatedPromptTokens: primary.hasEstimatedPromptTokens || secondary.hasEstimatedPromptTokens,
|
|
3388
|
+
messageCount: Math.max(primary.messageCount, secondary.messageCount),
|
|
3389
|
+
userPromptCount: Math.max(primary.userPromptCount, secondary.userPromptCount),
|
|
3390
|
+
assistantMessageCount: Math.max(primary.assistantMessageCount, secondary.assistantMessageCount),
|
|
3391
|
+
toolCallCount: Math.max(primary.toolCallCount, secondary.toolCallCount),
|
|
3392
|
+
shellCommandCount: Math.max(primary.shellCommandCount, secondary.shellCommandCount),
|
|
3393
|
+
failedToolCallCount: Math.max(primary.failedToolCallCount, secondary.failedToolCallCount),
|
|
3394
|
+
fileReadCount: Math.max(primary.fileReadCount, secondary.fileReadCount),
|
|
3395
|
+
fileEditCount: Math.max(primary.fileEditCount, secondary.fileEditCount),
|
|
3396
|
+
rawEventCount: Math.max(primary.rawEventCount, secondary.rawEventCount),
|
|
3397
|
+
parseStatus: mergeParseStatus(primary.parseStatus, secondary.parseStatus),
|
|
3398
|
+
parseErrors: unique4([...primary.parseErrors, ...secondary.parseErrors]),
|
|
3399
|
+
promptTimeline: primary.promptTimeline.length >= secondary.promptTimeline.length ? primary.promptTimeline : secondary.promptTimeline,
|
|
3400
|
+
warnings: unique4([...primary.warnings, ...secondary.warnings, "cursor_duplicate_session_merged"]),
|
|
3401
|
+
matchedKeys: unique4([...primary.matchedKeys, ...secondary.matchedKeys])
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
function mergeParseStatus(a, b) {
|
|
3405
|
+
if (a === "failed" || b === "failed") return "failed";
|
|
3406
|
+
if (a === "partial" || b === "partial") return "partial";
|
|
3407
|
+
return "ok";
|
|
3408
|
+
}
|
|
3409
|
+
function optionalMinDate(current, candidate) {
|
|
3410
|
+
return candidate ? minDate2(current, candidate) : current;
|
|
3411
|
+
}
|
|
3412
|
+
function optionalMaxDate(current, candidate) {
|
|
3413
|
+
return candidate ? maxDate2(current, candidate) : current;
|
|
3414
|
+
}
|
|
3415
|
+
function providerForModel2(model) {
|
|
3416
|
+
const normalized = model.toLowerCase();
|
|
3417
|
+
if (!normalized) return void 0;
|
|
3418
|
+
if (normalized.includes("claude")) return "anthropic";
|
|
3419
|
+
if (normalized.startsWith("gpt") || normalized.startsWith("o")) return "openai";
|
|
3420
|
+
return void 0;
|
|
3421
|
+
}
|
|
3422
|
+
function stringValue4(value) {
|
|
3423
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
3424
|
+
}
|
|
3425
|
+
function textValue(value) {
|
|
3426
|
+
if (typeof value === "string") return value;
|
|
3427
|
+
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
3428
|
+
return void 0;
|
|
3429
|
+
}
|
|
3430
|
+
function numberValue2(value) {
|
|
3431
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3432
|
+
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
|
3433
|
+
return void 0;
|
|
3434
|
+
}
|
|
3435
|
+
function dateValue4(value) {
|
|
3436
|
+
if (typeof value === "number" && Number.isFinite(value)) return new Date(value < 1e10 ? value * 1e3 : value).toISOString();
|
|
3437
|
+
if (typeof value !== "string") return void 0;
|
|
3438
|
+
const date = new Date(value);
|
|
3439
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
3440
|
+
}
|
|
3441
|
+
function minDate2(current, candidate) {
|
|
3442
|
+
return !current || candidate < current ? candidate : current;
|
|
3443
|
+
}
|
|
3444
|
+
function maxDate2(current, candidate) {
|
|
3445
|
+
return !current || candidate > current ? candidate : current;
|
|
3446
|
+
}
|
|
3447
|
+
function durationMs3(startedAt, endedAt) {
|
|
3448
|
+
if (!startedAt || !endedAt) return void 0;
|
|
3449
|
+
const start = new Date(startedAt).getTime();
|
|
3450
|
+
const end = new Date(endedAt).getTime();
|
|
3451
|
+
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : void 0;
|
|
3452
|
+
}
|
|
3453
|
+
function normalizeTimelineText3(text) {
|
|
3454
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 1e3);
|
|
3455
|
+
}
|
|
3456
|
+
function roundCurrency2(value) {
|
|
3457
|
+
return Number(value.toFixed(6));
|
|
3458
|
+
}
|
|
3459
|
+
function unique4(values) {
|
|
3460
|
+
return [...new Set(values.filter(Boolean))];
|
|
3461
|
+
}
|
|
3462
|
+
function errorMessage3(error) {
|
|
3463
|
+
return error instanceof Error ? error.message : String(error);
|
|
3464
|
+
}
|
|
3465
|
+
|
|
2414
3466
|
// packages/core/src/sources/index.ts
|
|
2415
3467
|
function scanUsageSources(options) {
|
|
2416
3468
|
const codex = scanCodex({
|
|
@@ -2423,20 +3475,25 @@ function scanUsageSources(options) {
|
|
|
2423
3475
|
...options.config ? { config: options.config } : {},
|
|
2424
3476
|
pricing: options.pricing
|
|
2425
3477
|
});
|
|
3478
|
+
const cursor = scanCursor({
|
|
3479
|
+
...options.cursorHome ? { cursorHome: options.cursorHome } : {},
|
|
3480
|
+
...options.config ? { config: options.config } : {},
|
|
3481
|
+
pricing: options.pricing
|
|
3482
|
+
});
|
|
2426
3483
|
return {
|
|
2427
|
-
sources: [codex.source, claude.source],
|
|
2428
|
-
sourceStats: [codex.stats, claude.stats],
|
|
2429
|
-
sessions: [...codex.sessions, ...claude.sessions]
|
|
3484
|
+
sources: [codex.source, claude.source, cursor.source],
|
|
3485
|
+
sourceStats: [codex.stats, claude.stats, cursor.stats],
|
|
3486
|
+
sessions: [...codex.sessions, ...claude.sessions, ...cursor.sessions]
|
|
2430
3487
|
};
|
|
2431
3488
|
}
|
|
2432
3489
|
|
|
2433
3490
|
// apps/server/src/data.ts
|
|
2434
|
-
import
|
|
3491
|
+
import fs7 from "node:fs";
|
|
2435
3492
|
var scanCacheTtlMs = 1e4;
|
|
2436
3493
|
var scanCache;
|
|
2437
|
-
function readDashboardData(filters = {}) {
|
|
3494
|
+
function readDashboardData(filters = {}, options = {}) {
|
|
2438
3495
|
const raw = readRawUsageData();
|
|
2439
|
-
return buildDashboardSnapshot({ ...raw, filters });
|
|
3496
|
+
return buildDashboardSnapshot({ ...raw, sessions: displaySessions(raw.sessions, options), filters });
|
|
2440
3497
|
}
|
|
2441
3498
|
function clearScanCache() {
|
|
2442
3499
|
scanCache = void 0;
|
|
@@ -2473,6 +3530,11 @@ function parseFilters(query) {
|
|
|
2473
3530
|
if (to) filters.to = to;
|
|
2474
3531
|
return filters;
|
|
2475
3532
|
}
|
|
3533
|
+
function parseDashboardOptions(query) {
|
|
3534
|
+
return {
|
|
3535
|
+
splitSourceApps: query.splitSourceApps === "true" || query.splitSourceApps === "1"
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
2476
3538
|
function readPricingData() {
|
|
2477
3539
|
const config = loadConfig();
|
|
2478
3540
|
const pricingPath = resolvePricingPath(config);
|
|
@@ -2493,8 +3555,8 @@ function writePricingData(models) {
|
|
|
2493
3555
|
}
|
|
2494
3556
|
function clearRepoSpendLocalData() {
|
|
2495
3557
|
const target = repospendHome();
|
|
2496
|
-
const removed =
|
|
2497
|
-
|
|
3558
|
+
const removed = fs7.existsSync(target);
|
|
3559
|
+
fs7.rmSync(target, { recursive: true, force: true });
|
|
2498
3560
|
clearScanCache();
|
|
2499
3561
|
return { path: target, removed };
|
|
2500
3562
|
}
|
|
@@ -2512,13 +3574,46 @@ function listQuery(value) {
|
|
|
2512
3574
|
const parsed = values.flatMap((item) => typeof item === "string" ? item.split(",") : []).map((item) => item.trim()).filter(Boolean);
|
|
2513
3575
|
return parsed.length ? parsed : void 0;
|
|
2514
3576
|
}
|
|
3577
|
+
function displaySessions(sessions, options) {
|
|
3578
|
+
if (!options.splitSourceApps) return sessions;
|
|
3579
|
+
return sessions.map((session) => {
|
|
3580
|
+
const sourceApp = sourceScopedAppLabel(session);
|
|
3581
|
+
return sourceApp === session.sourceApp ? session : { ...session, sourceApp };
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
function sourceScopedAppLabel(session) {
|
|
3585
|
+
const app = session.sourceApp || fallbackSurfaceLabel(session.detectedSurface);
|
|
3586
|
+
if (!app || app === "Unknown") return sourceClientLabel(session.sourceClient);
|
|
3587
|
+
if (!shouldScopeApp(app)) return app;
|
|
3588
|
+
return `${sourceClientLabel(session.sourceClient)} on ${app}`;
|
|
3589
|
+
}
|
|
3590
|
+
function shouldScopeApp(app) {
|
|
3591
|
+
const normalized = app.toLowerCase();
|
|
3592
|
+
return normalized.includes("vs code") || normalized.includes("vscode") || normalized.includes("terminal") || normalized.includes("cli");
|
|
3593
|
+
}
|
|
3594
|
+
function sourceClientLabel(source) {
|
|
3595
|
+
if (source === "codex") return "Codex";
|
|
3596
|
+
if (source === "claude") return "Claude Code";
|
|
3597
|
+
if (source === "gemini-cli") return "Gemini CLI";
|
|
3598
|
+
if (source === "opencode") return "OpenCode";
|
|
3599
|
+
if (source === "cursor") return "Cursor";
|
|
3600
|
+
return "Unknown";
|
|
3601
|
+
}
|
|
3602
|
+
function fallbackSurfaceLabel(surface) {
|
|
3603
|
+
if (surface === "terminal_cli") return "Terminal";
|
|
3604
|
+
if (surface === "vscode_extension") return "VS Code";
|
|
3605
|
+
if (surface === "local_agent") return "Desktop local agent";
|
|
3606
|
+
if (surface === "codex_exec") return "Codex";
|
|
3607
|
+
if (surface === "codex_app_cloud") return "Codex app";
|
|
3608
|
+
return "Unknown";
|
|
3609
|
+
}
|
|
2515
3610
|
function runtimeCwd() {
|
|
2516
3611
|
return process.env.INIT_CWD || process.cwd();
|
|
2517
3612
|
}
|
|
2518
3613
|
|
|
2519
3614
|
// apps/server/src/server.ts
|
|
2520
|
-
import
|
|
2521
|
-
import
|
|
3615
|
+
import fs8 from "node:fs";
|
|
3616
|
+
import path7 from "node:path";
|
|
2522
3617
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2523
3618
|
import fastifyStatic from "@fastify/static";
|
|
2524
3619
|
import Fastify from "fastify";
|
|
@@ -2534,11 +3629,11 @@ var demoPricing = {
|
|
|
2534
3629
|
function demoModeEnabled() {
|
|
2535
3630
|
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
2536
3631
|
}
|
|
2537
|
-
function readDemoDashboardData(filters = {}) {
|
|
3632
|
+
function readDemoDashboardData(filters = {}, options = {}) {
|
|
2538
3633
|
return buildDashboardSnapshot({
|
|
2539
3634
|
sources: demoSources,
|
|
2540
3635
|
sourceStats: demoSourceStats,
|
|
2541
|
-
sessions: demoSessions,
|
|
3636
|
+
sessions: displaySessions(demoSessions, options),
|
|
2542
3637
|
filters
|
|
2543
3638
|
});
|
|
2544
3639
|
}
|
|
@@ -2796,7 +3891,7 @@ var demoInputs = [
|
|
|
2796
3891
|
repo: "rivendell-dashboard",
|
|
2797
3892
|
title: "Refactor the council-of-elrond planner",
|
|
2798
3893
|
sourceClient: "claude",
|
|
2799
|
-
sourceApp: "Claude
|
|
3894
|
+
sourceApp: "Claude Desktop App",
|
|
2800
3895
|
sourceAppRaw: "claude-desktop-local-agent",
|
|
2801
3896
|
model: "claude-sonnet-4-6",
|
|
2802
3897
|
provider: "anthropic",
|
|
@@ -3106,7 +4201,7 @@ function detectedSurfaceForApp(sourceApp) {
|
|
|
3106
4201
|
if (sourceApp === "VS Code") return "vscode_extension";
|
|
3107
4202
|
if (sourceApp === "Terminal") return "terminal_cli";
|
|
3108
4203
|
if (sourceApp === "Codex app") return "codex_app_cloud";
|
|
3109
|
-
if (sourceApp === "Claude
|
|
4204
|
+
if (sourceApp === "Claude Desktop App") return "local_agent";
|
|
3110
4205
|
return "terminal_cli";
|
|
3111
4206
|
}
|
|
3112
4207
|
function timestamp2(hour, minute) {
|
|
@@ -3326,9 +4421,11 @@ function createServer(options = {}) {
|
|
|
3326
4421
|
const app = Fastify({ logger: false });
|
|
3327
4422
|
app.get("/api/health", async () => ({ ok: true, app: "RepoSpend", version: readPackageVersion() }));
|
|
3328
4423
|
app.get("/api/dashboard", async (request) => {
|
|
3329
|
-
const
|
|
4424
|
+
const query = request.query;
|
|
4425
|
+
const filters = parseFilters(query);
|
|
4426
|
+
const dashboardOptions = parseDashboardOptions(query);
|
|
3330
4427
|
const demoMode = demoModeEnabled();
|
|
3331
|
-
const data = demoMode ? readDemoDashboardData(filters) : readDashboardData(filters);
|
|
4428
|
+
const data = demoMode ? readDemoDashboardData(filters, dashboardOptions) : readDashboardData(filters, dashboardOptions);
|
|
3332
4429
|
const rtkGain = demoMode ? readDemoRtkGain() : await readRtkGain();
|
|
3333
4430
|
return {
|
|
3334
4431
|
...data,
|
|
@@ -3356,9 +4453,9 @@ function createServer(options = {}) {
|
|
|
3356
4453
|
return exportCsv();
|
|
3357
4454
|
});
|
|
3358
4455
|
if (options.serveWeb !== false) {
|
|
3359
|
-
const serverDist =
|
|
3360
|
-
const packagedWebDist =
|
|
3361
|
-
const workspaceWebDist =
|
|
4456
|
+
const serverDist = path7.dirname(fileURLToPath2(import.meta.url));
|
|
4457
|
+
const packagedWebDist = path7.resolve(serverDist, "../web-dist");
|
|
4458
|
+
const workspaceWebDist = path7.resolve(serverDist, "../../web/dist");
|
|
3362
4459
|
const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
|
|
3363
4460
|
if (!fsExists(webDist)) {
|
|
3364
4461
|
throw new Error(`RepoSpend web assets are missing. Expected ${packagedWebDist} in the published package, or ${workspaceWebDist} during local development. Run "pnpm pack:prepare" before packaging.`);
|
|
@@ -3379,13 +4476,13 @@ function createServer(options = {}) {
|
|
|
3379
4476
|
return app;
|
|
3380
4477
|
}
|
|
3381
4478
|
function fsExists(inputPath) {
|
|
3382
|
-
return Boolean(inputPath) &&
|
|
4479
|
+
return Boolean(inputPath) && path7.isAbsolute(inputPath) && fs8.existsSync(inputPath);
|
|
3383
4480
|
}
|
|
3384
4481
|
function readPackageVersion() {
|
|
3385
|
-
const serverDist =
|
|
3386
|
-
const packagePath =
|
|
4482
|
+
const serverDist = path7.dirname(fileURLToPath2(import.meta.url));
|
|
4483
|
+
const packagePath = path7.resolve(serverDist, "../package.json");
|
|
3387
4484
|
try {
|
|
3388
|
-
const parsed = JSON.parse(
|
|
4485
|
+
const parsed = JSON.parse(fs8.readFileSync(packagePath, "utf8"));
|
|
3389
4486
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
3390
4487
|
} catch {
|
|
3391
4488
|
return "unknown";
|
|
@@ -3481,7 +4578,7 @@ function openBrowser(url) {
|
|
|
3481
4578
|
}
|
|
3482
4579
|
function printGroups(groups) {
|
|
3483
4580
|
if (!groups.length) {
|
|
3484
|
-
console.log("No usage found. Scanned Codex
|
|
4581
|
+
console.log("No usage found. Scanned Codex, Claude Code, and Cursor local data paths.");
|
|
3485
4582
|
return;
|
|
3486
4583
|
}
|
|
3487
4584
|
const rows = groups.map((group) => ({
|
|
@@ -3514,5 +4611,5 @@ function printHelp() {
|
|
|
3514
4611
|
repospend export --format csv
|
|
3515
4612
|
|
|
3516
4613
|
Options:
|
|
3517
|
-
--source all|codex|claude`);
|
|
4614
|
+
--source all|codex|claude|cursor`);
|
|
3518
4615
|
}
|