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/dist/cli.js CHANGED
@@ -44,10 +44,14 @@ function summarize(sessions) {
44
44
  };
45
45
  }
46
46
  function groupByRepo(sessions) {
47
- return groupBy(sessions, (session) => session.repoRoot, (session) => session.repoName);
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 ?? "Unknown 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 (!matchesAny(session.model, filters.model)) return false;
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 total = direct.reduce(
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
- return tokenUsageFromObject(codexLastUsage, false);
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 (!stateExists && sessionsExists) {
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
- return rows.map((row, index) => threadToUsage(row, index, sessionFiles, options));
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) ?? sessionBreakdown.source ?? sessionBreakdown.originator ?? threadSource;
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: stringValue2(row.id) ?? `codex-thread-${index}`,
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: dateValue2(row.created_at),
1507
- endedAt: dateValue2(row.updated_at),
1508
- model: stringValue2(row.model),
1509
- provider: stringValue2(row.model_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(dateValue2(row.created_at), dateValue2(row.updated_at)),
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 = void 0;
1565
- const surface = detectSurface(breakdown.source ?? breakdown.originator, breakdown.threadSource);
1566
- return {
1567
- id,
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(breakdown.source ?? breakdown.originator, breakdown.threadSource),
1570
- sourceAppRaw: breakdown.source ?? breakdown.originator ?? breakdown.threadSource,
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: void 0,
1578
- startedAt: void 0,
1579
- endedAt: void 0,
1678
+ title: breakdown.title,
1679
+ startedAt: breakdown.startedAt,
1680
+ endedAt: breakdown.endedAt,
1580
1681
  model,
1581
- provider: void 0,
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, "unknown_pricing"],
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 sessions = orderedFiles.map(({ file }, index) => claudeFileToUsage(file, index, options, seenUsageKeys));
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 claudeFileToUsage(file, index, options, seenUsageKeys) {
2064
- const breakdown = readClaudeBreakdown(file.filePath, seenUsageKeys);
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(breakdown.entrypoint),
2076
- sourceAppRaw: breakdown.entrypoint,
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(breakdown.entrypoint),
2325
+ detectedSurface: claudeSurface(entrypoint),
2107
2326
  surfaceConfidence: "medium",
2108
- surfaceReason: breakdown.entrypoint ? `Claude Code entrypoint: ${breakdown.entrypoint}` : "Claude Code transcript",
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 readClaudeBreakdown(filePath, seenUsageKeys) {
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, seenUsageKeys);
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, seenUsageKeys) {
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
- if (usage && usageTokens > 0 && !alreadySeenUsage(record, message, seenUsageKeys)) {
2179
- applyUsage(breakdown, usage);
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 alreadySeenUsage(record, message, seenUsageKeys) {
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 false;
2189
- const key = `${requestId}:${messageId}`;
2190
- if (seenUsageKeys.has(key)) return true;
2191
- seenUsageKeys.add(key);
2192
- return false;
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 desktop local agent";
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 fs6 from "node:fs";
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 = fs6.existsSync(target);
2497
- fs6.rmSync(target, { recursive: true, force: true });
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 fs7 from "node:fs";
2521
- import path6 from "node:path";
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 desktop local agent",
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 desktop local agent") return "local_agent";
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 filters = parseFilters(request.query);
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 = path6.dirname(fileURLToPath2(import.meta.url));
3360
- const packagedWebDist = path6.resolve(serverDist, "../web-dist");
3361
- const workspaceWebDist = path6.resolve(serverDist, "../../web/dist");
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) && path6.isAbsolute(inputPath) && fs7.existsSync(inputPath);
4479
+ return Boolean(inputPath) && path7.isAbsolute(inputPath) && fs8.existsSync(inputPath);
3383
4480
  }
3384
4481
  function readPackageVersion() {
3385
- const serverDist = path6.dirname(fileURLToPath2(import.meta.url));
3386
- const packagePath = path6.resolve(serverDist, "../package.json");
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(fs7.readFileSync(packagePath, "utf8"));
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 and Claude Code local data paths.");
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
  }