repospend 0.0.7 → 0.0.9

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // apps/server/src/cli.ts
4
- import { spawn } from "node:child_process";
4
+ import { spawn as spawn2 } from "node:child_process";
5
5
 
6
6
  // packages/core/src/aggregate.ts
7
7
  function summarize(sessions) {
@@ -195,7 +195,8 @@ function unique(values) {
195
195
  return [...new Set(values)].sort();
196
196
  }
197
197
 
198
- // packages/core/src/config.ts
198
+ // packages/core/src/cache.ts
199
+ import crypto from "node:crypto";
199
200
  import fs2 from "node:fs";
200
201
  import path2 from "node:path";
201
202
 
@@ -314,34 +315,97 @@ function isUsablePricing(pricing) {
314
315
  return typeof pricing?.inputPerMillion === "number" && pricing.inputPerMillion > 0 && typeof pricing.outputPerMillion === "number" && pricing.outputPerMillion > 0;
315
316
  }
316
317
 
318
+ // packages/core/src/cache.ts
319
+ var activeCacheVersion = "parse-v1";
320
+ var preparedCacheRoot;
321
+ function parseCacheRoot() {
322
+ return path2.join(repospendHome(), "cache", activeCacheVersion);
323
+ }
324
+ function readJsonCache(namespace, keyParts) {
325
+ const cachePath = cacheEntryPath(namespace, keyParts);
326
+ if (!cachePath || !fs2.existsSync(cachePath)) return void 0;
327
+ try {
328
+ return JSON.parse(fs2.readFileSync(cachePath, "utf8"));
329
+ } catch {
330
+ fs2.rmSync(cachePath, { force: true });
331
+ return void 0;
332
+ }
333
+ }
334
+ function writeJsonCache(namespace, keyParts, value) {
335
+ const cachePath = cacheEntryPath(namespace, keyParts);
336
+ if (!cachePath) return;
337
+ try {
338
+ fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
339
+ fs2.writeFileSync(cachePath, `${JSON.stringify(value)}
340
+ `);
341
+ } catch {
342
+ }
343
+ }
344
+ function clearRepoSpendCache() {
345
+ const target = path2.join(repospendHome(), "cache");
346
+ const removed = fs2.existsSync(target);
347
+ fs2.rmSync(target, { recursive: true, force: true });
348
+ preparedCacheRoot = void 0;
349
+ return { path: target, removed };
350
+ }
351
+ function cacheEntryPath(namespace, keyParts) {
352
+ prepareCacheRoot();
353
+ const safeNamespace = namespace.replace(/[^a-z0-9_-]/gi, "-");
354
+ const key = crypto.createHash("sha256").update(JSON.stringify(keyParts)).digest("hex");
355
+ return path2.join(parseCacheRoot(), safeNamespace, `${key}.json`);
356
+ }
357
+ function prepareCacheRoot() {
358
+ const root = path2.join(repospendHome(), "cache");
359
+ if (preparedCacheRoot === root) return;
360
+ preparedCacheRoot = root;
361
+ try {
362
+ fs2.mkdirSync(parseCacheRoot(), { recursive: true });
363
+ for (const entry of fs2.readdirSync(root, { withFileTypes: true })) {
364
+ if (entry.isDirectory() && entry.name !== activeCacheVersion) {
365
+ fs2.rmSync(path2.join(root, entry.name), { recursive: true, force: true });
366
+ }
367
+ }
368
+ } catch {
369
+ }
370
+ }
371
+
317
372
  // packages/core/src/config.ts
373
+ import fs3 from "node:fs";
374
+ import path3 from "node:path";
318
375
  function configPath() {
319
- return path2.join(repospendHome(), "config.json");
376
+ return path3.join(repospendHome(), "config.json");
320
377
  }
321
378
  function loadConfig() {
322
379
  return loadConfigWithWarnings().config;
323
380
  }
324
381
  function loadConfigWithWarnings() {
325
382
  const localConfigPath = configPath();
326
- if (!fs2.existsSync(localConfigPath)) {
383
+ if (!fs3.existsSync(localConfigPath)) {
327
384
  return { config: {}, warnings: [] };
328
385
  }
329
386
  try {
330
- return { config: JSON.parse(fs2.readFileSync(localConfigPath, "utf8")), warnings: [] };
387
+ return { config: JSON.parse(fs3.readFileSync(localConfigPath, "utf8")), warnings: [] };
331
388
  } catch (error) {
332
389
  const message = error instanceof Error ? error.message : String(error);
333
390
  return { config: {}, warnings: [`Unable to parse ${localConfigPath}: ${message}`] };
334
391
  }
335
392
  }
393
+ function saveConfig(config) {
394
+ const localConfigPath = configPath();
395
+ fs3.mkdirSync(path3.dirname(localConfigPath), { recursive: true });
396
+ fs3.writeFileSync(localConfigPath, `${JSON.stringify(config, null, 2)}
397
+ `);
398
+ return loadConfig();
399
+ }
336
400
  function normalizePathForMatch(inputPath) {
337
- return path2.resolve(inputPath).replace(/\/+$/, "");
401
+ return path3.resolve(inputPath).replace(/\/+$/, "");
338
402
  }
339
403
  function aliasForPath(repoRoot, config) {
340
404
  const resolvedRoot = normalizePathForMatch(repoRoot);
341
405
  const match = config.repos?.find(
342
406
  (repo) => repo.paths.some((repoPath) => {
343
407
  const candidate = normalizePathForMatch(repoPath);
344
- return resolvedRoot === candidate || resolvedRoot.startsWith(`${candidate}${path2.sep}`);
408
+ return resolvedRoot === candidate || resolvedRoot.startsWith(`${candidate}${path3.sep}`);
345
409
  })
346
410
  );
347
411
  return match?.name;
@@ -861,16 +925,16 @@ function isDashboardVisibleSession(session) {
861
925
  }
862
926
 
863
927
  // packages/core/src/repo.ts
864
- import fs3 from "node:fs";
865
- import path3 from "node:path";
928
+ import fs4 from "node:fs";
929
+ import path4 from "node:path";
866
930
  function normalizeRoot(p) {
867
931
  const stripped = p.startsWith("\\\\?\\") ? p.slice(4) : p;
868
932
  return stripped.replace(/^([a-zA-Z]):/, (_, letter) => letter.toUpperCase() + ":");
869
933
  }
870
- function findGitRoot(cwd, fileSystem = fs3) {
871
- let current = normalizeRoot(path3.resolve(cwd));
934
+ function findGitRoot(cwd, fileSystem = fs4) {
935
+ let current = normalizeRoot(path4.resolve(cwd));
872
936
  while (true) {
873
- const gitPath = path3.join(current, ".git");
937
+ const gitPath = path4.join(current, ".git");
874
938
  if (fileSystem.existsSync(gitPath)) {
875
939
  try {
876
940
  const stat = fileSystem.statSync(gitPath);
@@ -881,19 +945,19 @@ function findGitRoot(cwd, fileSystem = fs3) {
881
945
  return current;
882
946
  }
883
947
  }
884
- const parent = path3.dirname(current);
948
+ const parent = path4.dirname(current);
885
949
  if (parent === current) {
886
950
  return void 0;
887
951
  }
888
952
  current = parent;
889
953
  }
890
954
  }
891
- function resolveRepoInfo(cwd, config = {}, fileSystem = fs3) {
955
+ function resolveRepoInfo(cwd, config = {}, fileSystem = fs4) {
892
956
  const warnings = [];
893
957
  const gitRoot = safeFindGitRoot(cwd, fileSystem);
894
958
  const repoRoot = gitRoot ?? fallbackWorkspaceRoot(cwd);
895
959
  const alias = aliasForPath(repoRoot, config);
896
- const repoName = alias ?? (path3.basename(repoRoot) || "unknown-workspace");
960
+ const repoName = alias ?? (path4.basename(repoRoot) || "unknown-workspace");
897
961
  const gitMeta = gitRoot ? readGitMetadata(gitRoot, fileSystem) : { remote: void 0, branch: void 0 };
898
962
  if (!gitRoot) {
899
963
  warnings.push("repo_unverified_no_git_root");
@@ -915,23 +979,23 @@ function safeFindGitRoot(cwd, fileSystem) {
915
979
  }
916
980
  }
917
981
  function fallbackWorkspaceRoot(cwd) {
918
- const resolved = normalizeRoot(path3.resolve(cwd || process.cwd()));
919
- const home = process.env.HOME ? normalizeRoot(path3.resolve(process.env.HOME)) : void 0;
920
- if (home && (resolved === home || resolved.startsWith(`${home}${path3.sep}`))) {
921
- const relative = path3.relative(home, resolved).split(path3.sep).filter(Boolean);
982
+ const resolved = normalizeRoot(path4.resolve(cwd || process.cwd()));
983
+ const home = process.env.HOME ? normalizeRoot(path4.resolve(process.env.HOME)) : void 0;
984
+ if (home && (resolved === home || resolved.startsWith(`${home}${path4.sep}`))) {
985
+ const relative = path4.relative(home, resolved).split(path4.sep).filter(Boolean);
922
986
  const first = relative[0];
923
- return first ? path3.join(home, first) : home;
987
+ return first ? path4.join(home, first) : home;
924
988
  }
925
- const parts = resolved.split(path3.sep).filter(Boolean);
926
- return `${path3.sep}${parts.slice(0, Math.min(parts.length, 3)).join(path3.sep)}`;
989
+ const parts = resolved.split(path4.sep).filter(Boolean);
990
+ return `${path4.sep}${parts.slice(0, Math.min(parts.length, 3)).join(path4.sep)}`;
927
991
  }
928
992
  function readGitMetadata(repoRoot, fileSystem) {
929
- const gitPath = path3.join(repoRoot, ".git");
993
+ const gitPath = path4.join(repoRoot, ".git");
930
994
  try {
931
995
  const stat = fileSystem.statSync(gitPath);
932
996
  const gitDir = stat.isFile() ? resolveGitFile(repoRoot, fileSystem.readFileSync(gitPath, "utf8")) : gitPath;
933
- const configPath2 = path3.join(gitDir, "config");
934
- const headPath = path3.join(gitDir, "HEAD");
997
+ const configPath2 = path4.join(gitDir, "config");
998
+ const headPath = path4.join(gitDir, "HEAD");
935
999
  const remote = fileSystem.existsSync(configPath2) ? parseRemote(fileSystem.readFileSync(configPath2, "utf8")) : void 0;
936
1000
  const branch = fileSystem.existsSync(headPath) ? parseBranch(fileSystem.readFileSync(headPath, "utf8")) : void 0;
937
1001
  return { remote, branch };
@@ -942,9 +1006,9 @@ function readGitMetadata(repoRoot, fileSystem) {
942
1006
  function resolveGitFile(repoRoot, contents) {
943
1007
  const match = contents.match(/^gitdir:\s*(.+)$/m);
944
1008
  if (!match?.[1]) {
945
- return path3.join(repoRoot, ".git");
1009
+ return path4.join(repoRoot, ".git");
946
1010
  }
947
- return path3.resolve(repoRoot, match[1].trim());
1011
+ return path4.resolve(repoRoot, match[1].trim());
948
1012
  }
949
1013
  function parseRemote(config) {
950
1014
  const originMatch = config.match(/\[remote "origin"\][\s\S]*?\n\s*url\s*=\s*(.+)/);
@@ -957,9 +1021,9 @@ function parseBranch(head) {
957
1021
 
958
1022
  // packages/core/src/sources/codex.ts
959
1023
  import { spawnSync } from "node:child_process";
960
- import fs4 from "node:fs";
1024
+ import fs5 from "node:fs";
961
1025
  import os2 from "node:os";
962
- import path4 from "node:path";
1026
+ import path5 from "node:path";
963
1027
  import { fileURLToPath } from "node:url";
964
1028
  import Database from "better-sqlite3";
965
1029
 
@@ -1398,28 +1462,29 @@ function numberFromKeys(object, keys) {
1398
1462
  }
1399
1463
 
1400
1464
  // packages/core/src/sources/codex.ts
1465
+ var codexSessionCacheVersion = "codex-session-v1";
1401
1466
  var nativeSqliteRebuildAttempted = false;
1402
1467
  function scanCodex(options) {
1403
- const codexHome = options.codexHome ?? path4.join(os2.homedir(), ".codex");
1404
- const statePath = path4.join(codexHome, "state_5.sqlite");
1405
- const sessionsPath = path4.join(codexHome, "sessions");
1468
+ const codexHome = options.codexHome ?? path5.join(os2.homedir(), ".codex");
1469
+ const statePath = path5.join(codexHome, "state_5.sqlite");
1470
+ const sessionsPath = path5.join(codexHome, "sessions");
1406
1471
  const warnings = [];
1407
1472
  const sessions = [];
1408
- const stateExists = fs4.existsSync(statePath);
1409
- const sessionsExists = fs4.existsSync(sessionsPath);
1473
+ const stateExists = fs5.existsSync(statePath);
1474
+ const sessionsExists = fs5.existsSync(sessionsPath);
1410
1475
  if (!stateExists) {
1411
1476
  warnings.push(`Codex SQLite state not found at ${statePath}`);
1412
1477
  }
1413
1478
  if (!sessionsExists) {
1414
1479
  warnings.push(`Codex sessions directory not found at ${sessionsPath}`);
1415
1480
  }
1416
- const sessionFiles = sessionsExists ? indexSessionFiles(sessionsPath, warnings) : /* @__PURE__ */ new Map();
1481
+ const sessionFiles = sessionsExists ? indexSessionFiles(sessionsPath, warnings, options.scanWindow) : /* @__PURE__ */ new Map();
1417
1482
  if (stateExists) {
1418
1483
  const stateSessions = readThreads(statePath, sessionFiles, options, warnings);
1419
1484
  sessions.push(...stateSessions);
1420
1485
  }
1421
1486
  if (sessionsExists) {
1422
- sessions.push(...readStandaloneSessions(sessionFiles, options).filter((session) => !hasImportedSession(sessions, session)));
1487
+ sessions.push(...readStandaloneSessions(remainingStandaloneSessionFiles(sessionFiles, sessions), options).filter((session) => !hasImportedSession(sessions, session)));
1423
1488
  }
1424
1489
  return {
1425
1490
  source: {
@@ -1462,8 +1527,8 @@ function readThreads(statePath, sessionFiles, options, warnings) {
1462
1527
  warnings.push("Codex threads table has no recognized columns.");
1463
1528
  return [];
1464
1529
  }
1465
- const importedModels = readImportedModelMap(options.codexHome ?? path4.join(os2.homedir(), ".codex"), warnings);
1466
- const rows = db.prepare(`SELECT ${selectedColumns.map((column) => `"${column}"`).join(", ")} FROM threads`).all();
1530
+ const importedModels = readImportedModelMap(options.codexHome ?? path5.join(os2.homedir(), ".codex"), warnings);
1531
+ const rows = db.prepare(`SELECT ${selectedColumns.map((column) => `"${column}"`).join(", ")} FROM threads`).all().filter((row) => rowMatchesScanWindow(row, options.scanWindow));
1467
1532
  const sessions = rows.map((row, index) => threadToUsage(row, index, sessionFiles, options, importedModels));
1468
1533
  return attributeSubagentsToParentSurfaces(sessions, rows);
1469
1534
  } catch (error) {
@@ -1522,7 +1587,7 @@ function attemptNativeSqliteRebuild(warnings) {
1522
1587
  warnings.push("Codex SQLite native module mismatch detected, but RepoSpend could not locate its package root for an automatic rebuild.");
1523
1588
  return false;
1524
1589
  }
1525
- const packageManager = fs4.existsSync(path4.join(packageRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
1590
+ const packageManager = fs5.existsSync(path5.join(packageRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
1526
1591
  const result = spawnSync(packageManager, ["rebuild", "better-sqlite3"], {
1527
1592
  cwd: packageRoot,
1528
1593
  encoding: "utf8",
@@ -1536,8 +1601,8 @@ function attemptNativeSqliteRebuild(warnings) {
1536
1601
  }
1537
1602
  function findRepoSpendPackageRoot() {
1538
1603
  const starts = [
1539
- process.argv[1] ? path4.dirname(fs4.realpathSync(process.argv[1])) : void 0,
1540
- path4.dirname(fileURLToPath(import.meta.url)),
1604
+ process.argv[1] ? path5.dirname(fs5.realpathSync(process.argv[1])) : void 0,
1605
+ path5.dirname(fileURLToPath(import.meta.url)),
1541
1606
  process.cwd()
1542
1607
  ].filter((item) => Boolean(item));
1543
1608
  for (const start of starts) {
@@ -1547,17 +1612,17 @@ function findRepoSpendPackageRoot() {
1547
1612
  return void 0;
1548
1613
  }
1549
1614
  function findPackageRootNamedRepoSpend(start) {
1550
- let current = path4.resolve(start);
1615
+ let current = path5.resolve(start);
1551
1616
  while (true) {
1552
- const packagePath = path4.join(current, "package.json");
1553
- if (fs4.existsSync(packagePath)) {
1617
+ const packagePath = path5.join(current, "package.json");
1618
+ if (fs5.existsSync(packagePath)) {
1554
1619
  try {
1555
- const packageJson = JSON.parse(fs4.readFileSync(packagePath, "utf8"));
1620
+ const packageJson = JSON.parse(fs5.readFileSync(packagePath, "utf8"));
1556
1621
  if (packageJson.name === "repospend") return current;
1557
1622
  } catch {
1558
1623
  }
1559
1624
  }
1560
- const parent = path4.dirname(current);
1625
+ const parent = path5.dirname(current);
1561
1626
  if (parent === current) return void 0;
1562
1627
  current = parent;
1563
1628
  }
@@ -1619,7 +1684,7 @@ function threadToUsage(row, index, sessionFiles, options, importedModels = /* @_
1619
1684
  messageCount: sessionBreakdown.messageCount,
1620
1685
  rawTokenTotal: dbTokens || sessionBreakdown.totalTokens || void 0,
1621
1686
  warnings,
1622
- codexHome: options.codexHome ?? path4.join(os2.homedir(), ".codex"),
1687
+ codexHome: options.codexHome ?? path5.join(os2.homedir(), ".codex"),
1623
1688
  durationMs: durationMs(startedAt, endedAt),
1624
1689
  rawEventCount: sessionBreakdown.rawEventCount,
1625
1690
  parseStatus: sessionBreakdown.parseStatus,
@@ -1693,7 +1758,7 @@ function readStandaloneSessions(sessionFiles, options) {
1693
1758
  messageCount: breakdown.messageCount,
1694
1759
  rawTokenTotal: breakdown.totalTokens || void 0,
1695
1760
  warnings: [...repo.warnings, ...breakdown.warnings],
1696
- codexHome: options.codexHome ?? path4.join(os2.homedir(), ".codex"),
1761
+ codexHome: options.codexHome ?? path5.join(os2.homedir(), ".codex"),
1697
1762
  durationMs: durationMs(breakdown.startedAt, breakdown.endedAt),
1698
1763
  rawEventCount: breakdown.rawEventCount,
1699
1764
  parseStatus: breakdown.parseStatus,
@@ -1738,13 +1803,18 @@ function hasImportedSession(sessions, candidate) {
1738
1803
  return Boolean(candidatePath && sessionPath && candidatePath === sessionPath);
1739
1804
  });
1740
1805
  }
1806
+ function remainingStandaloneSessionFiles(sessionFiles, importedSessions) {
1807
+ const importedIds = new Set(importedSessions.map((session) => session.id).filter(Boolean));
1808
+ const importedPaths = new Set(importedSessions.map((session) => comparableSourcePath(session.sourcePath)).filter(Boolean));
1809
+ return new Map([...sessionFiles.entries()].filter(([id, sourcePath]) => !importedIds.has(id) && !importedPaths.has(comparableSourcePath(sourcePath))));
1810
+ }
1741
1811
  function readImportedModelMap(codexHome, warnings) {
1742
- const importsPath = path4.join(codexHome, "external_agent_session_imports.json");
1812
+ const importsPath = path5.join(codexHome, "external_agent_session_imports.json");
1743
1813
  const models = /* @__PURE__ */ new Map();
1744
- if (!fs4.existsSync(importsPath)) return models;
1814
+ if (!fs5.existsSync(importsPath)) return models;
1745
1815
  let parsed;
1746
1816
  try {
1747
- parsed = JSON.parse(fs4.readFileSync(importsPath, "utf8"));
1817
+ parsed = JSON.parse(fs5.readFileSync(importsPath, "utf8"));
1748
1818
  } catch (error) {
1749
1819
  warnings.push(`Unable to read Codex external import index at ${importsPath}: ${errorMessage(error)}`);
1750
1820
  return models;
@@ -1754,7 +1824,7 @@ function readImportedModelMap(codexHome, warnings) {
1754
1824
  const item = asRecord(record);
1755
1825
  const importedThreadId = stringValue2(item?.imported_thread_id);
1756
1826
  const sourcePath = normalizeVerbatimPath(stringValue2(item?.source_path));
1757
- if (!importedThreadId || !sourcePath || !fs4.existsSync(sourcePath)) continue;
1827
+ if (!importedThreadId || !sourcePath || !fs5.existsSync(sourcePath)) continue;
1758
1828
  const model = readImportedSourceModel(sourcePath, warnings);
1759
1829
  if (model) models.set(importedThreadId, { model, provider: providerForModel(model) });
1760
1830
  }
@@ -1764,7 +1834,7 @@ function readImportedSourceModel(sourcePath, warnings) {
1764
1834
  const counts = /* @__PURE__ */ new Map();
1765
1835
  let content;
1766
1836
  try {
1767
- content = fs4.readFileSync(sourcePath, "utf8");
1837
+ content = fs5.readFileSync(sourcePath, "utf8");
1768
1838
  } catch (error) {
1769
1839
  warnings.push(`Unable to read Codex external import source at ${sourcePath}: ${errorMessage(error)}`);
1770
1840
  return void 0;
@@ -1802,12 +1872,13 @@ function normalizeVerbatimPath(value) {
1802
1872
  function comparableSourcePath(value) {
1803
1873
  return normalizeVerbatimPath(value)?.replace(/\\/g, "/");
1804
1874
  }
1805
- function indexSessionFiles(sessionsPath, warnings) {
1875
+ function indexSessionFiles(sessionsPath, warnings, scanWindow) {
1806
1876
  const files = /* @__PURE__ */ new Map();
1807
1877
  try {
1808
1878
  walk(sessionsPath, (filePath) => {
1809
1879
  if (!/\.(jsonl|json|log)$/i.test(filePath)) return;
1810
- const id = path4.basename(filePath).replace(/\.(jsonl|json|log)$/i, "");
1880
+ if (!fileMayOverlapScanWindow(filePath, scanWindow)) return;
1881
+ const id = path5.basename(filePath).replace(/\.(jsonl|json|log)$/i, "");
1811
1882
  files.set(id, filePath);
1812
1883
  });
1813
1884
  } catch (error) {
@@ -1815,9 +1886,26 @@ function indexSessionFiles(sessionsPath, warnings) {
1815
1886
  }
1816
1887
  return files;
1817
1888
  }
1889
+ function rowMatchesScanWindow(row, scanWindow) {
1890
+ if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
1891
+ const updatedAt = timestampMs(row.updated_at);
1892
+ const createdAt = timestampMs(row.created_at);
1893
+ const latestTimestamp = updatedAt ?? createdAt;
1894
+ if (scanWindow.fromMs !== void 0 && latestTimestamp !== void 0 && latestTimestamp < scanWindow.fromMs) return false;
1895
+ if (scanWindow.toMs !== void 0 && createdAt !== void 0 && createdAt > scanWindow.toMs) return false;
1896
+ return true;
1897
+ }
1898
+ function fileMayOverlapScanWindow(filePath, scanWindow) {
1899
+ if (!scanWindow?.fromMs) return true;
1900
+ try {
1901
+ return fs5.statSync(filePath).mtimeMs >= scanWindow.fromMs;
1902
+ } catch {
1903
+ return true;
1904
+ }
1905
+ }
1818
1906
  function walk(root, onFile) {
1819
- for (const entry of fs4.readdirSync(root, { withFileTypes: true })) {
1820
- const fullPath = path4.join(root, entry.name);
1907
+ for (const entry of fs5.readdirSync(root, { withFileTypes: true })) {
1908
+ const fullPath = path5.join(root, entry.name);
1821
1909
  if (entry.isDirectory()) {
1822
1910
  walk(fullPath, onFile);
1823
1911
  } else if (entry.isFile()) {
@@ -1826,9 +1914,12 @@ function walk(root, onFile) {
1826
1914
  }
1827
1915
  }
1828
1916
  function readSessionBreakdown(filePath) {
1917
+ const cacheKey = sessionBreakdownCacheKey(filePath);
1918
+ const cached = cacheKey ? readJsonCache("codex-session", cacheKey) : void 0;
1919
+ if (cached) return cached;
1829
1920
  const breakdown = emptyBreakdown();
1830
1921
  try {
1831
- const text = fs4.readFileSync(filePath, "utf8");
1922
+ const text = fs5.readFileSync(filePath, "utf8");
1832
1923
  const parse = parseSessionRecords(text);
1833
1924
  const records = parse.records;
1834
1925
  breakdown.rawEventCount = records.length;
@@ -1860,8 +1951,22 @@ function readSessionBreakdown(filePath) {
1860
1951
  breakdown.parseErrors.push(message);
1861
1952
  breakdown.warnings.push(message);
1862
1953
  }
1954
+ if (cacheKey) writeJsonCache("codex-session", cacheKey, breakdown);
1863
1955
  return breakdown;
1864
1956
  }
1957
+ function sessionBreakdownCacheKey(filePath) {
1958
+ try {
1959
+ const stat = fs5.statSync(filePath);
1960
+ return {
1961
+ version: codexSessionCacheVersion,
1962
+ filePath: path5.resolve(filePath),
1963
+ size: stat.size,
1964
+ mtimeMs: stat.mtimeMs
1965
+ };
1966
+ } catch {
1967
+ return void 0;
1968
+ }
1969
+ }
1865
1970
  function parseSessionRecords(text) {
1866
1971
  const trimmed = text.trim();
1867
1972
  if (!trimmed) return { records: [], errors: [] };
@@ -1966,7 +2071,7 @@ function emptyBreakdown() {
1966
2071
  };
1967
2072
  }
1968
2073
  function fallbackCwdFromPath(filePath) {
1969
- return filePath ? path4.dirname(filePath) : process.cwd();
2074
+ return filePath ? path5.dirname(filePath) : process.cwd();
1970
2075
  }
1971
2076
  function applySessionMetadata(breakdown, record) {
1972
2077
  if (!record || typeof record !== "object") return;
@@ -2146,6 +2251,12 @@ function dateValue2(value) {
2146
2251
  }
2147
2252
  return void 0;
2148
2253
  }
2254
+ function timestampMs(value) {
2255
+ const date = dateValue2(value);
2256
+ if (!date) return void 0;
2257
+ const timestamp3 = new Date(date).getTime();
2258
+ return Number.isFinite(timestamp3) ? timestamp3 : void 0;
2259
+ }
2149
2260
  function errorMessage(error) {
2150
2261
  const message = error instanceof Error ? error.message : String(error);
2151
2262
  if (message.includes("NODE_MODULE_VERSION") && message.includes("better_sqlite3.node")) {
@@ -2157,24 +2268,24 @@ function errorMessage(error) {
2157
2268
  }
2158
2269
 
2159
2270
  // packages/core/src/sources/claude.ts
2160
- import fs5 from "node:fs";
2271
+ import fs6 from "node:fs";
2161
2272
  import os3 from "node:os";
2162
- import path5 from "node:path";
2273
+ import path6 from "node:path";
2163
2274
  function scanClaude(options) {
2164
2275
  const claudeHome = options.claudeHome ?? defaultClaudeHome();
2165
- const historyPath = path5.join(claudeHome, "history.jsonl");
2276
+ const historyPath = path6.join(claudeHome, "history.jsonl");
2166
2277
  const warnings = [];
2167
2278
  const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
2168
- const historyExists = fs5.existsSync(historyPath);
2169
- const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).sort((a, b) => a.mtimeMs - b.mtimeMs);
2279
+ const historyExists = fs6.existsSync(historyPath);
2280
+ const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).filter(({ mtimeMs }) => !options.scanWindow?.fromMs || mtimeMs >= options.scanWindow.fromMs).sort((a, b) => a.mtimeMs - b.mtimeMs);
2170
2281
  const usageSelection = selectClaudeUsageOccurrences(orderedFiles.map(({ file }) => file));
2171
2282
  const sessions = orderedFiles.flatMap(({ file }, index) => claudeFileToUsages(file, index, options, usageSelection));
2172
2283
  const historyEntryCount = countHistoryEntries(historyPath);
2173
2284
  const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
2174
2285
  const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
2175
2286
  const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_"))).length;
2176
- const projectsPath = path5.join(claudeHome, "projects");
2177
- if (!discovery.projectsPaths.some((candidate) => fs5.existsSync(candidate))) {
2287
+ const projectsPath = path6.join(claudeHome, "projects");
2288
+ if (!discovery.projectsPaths.some((candidate) => fs6.existsSync(candidate))) {
2178
2289
  warnings.push(`Claude Code projects directory not found at ${projectsPath}`);
2179
2290
  }
2180
2291
  return {
@@ -2193,7 +2304,7 @@ function scanClaude(options) {
2193
2304
  claudeHome,
2194
2305
  projectsPath,
2195
2306
  historyPath,
2196
- projectsExists: discovery.projectsPaths.some((candidate) => fs5.existsSync(candidate)),
2307
+ projectsExists: discovery.projectsPaths.some((candidate) => fs6.existsSync(candidate)),
2197
2308
  historyExists,
2198
2309
  sessionFileCount: discovery.files.length,
2199
2310
  projectDirCount: discovery.projectDirs.size,
@@ -2209,7 +2320,7 @@ function scanClaude(options) {
2209
2320
  function discoverClaudeSessionFiles(claudeHome, warnings, explicitHome) {
2210
2321
  const files = [];
2211
2322
  const projectDirs = /* @__PURE__ */ new Set();
2212
- const projectsPaths = explicitHome ? [path5.join(claudeHome, "projects")] : unique3([...configuredClaudeHomes(claudeHome).map((root) => path5.join(root, "projects")), path5.join(os3.homedir(), ".config", "claude", "projects")]);
2323
+ const projectsPaths = explicitHome ? [path6.join(claudeHome, "projects")] : unique3([...configuredClaudeHomes(claudeHome).map((root) => path6.join(root, "projects")), path6.join(os3.homedir(), ".config", "claude", "projects")]);
2213
2324
  for (const projectsPath of projectsPaths) {
2214
2325
  collectProjectFiles(projectsPath, files, projectDirs, warnings);
2215
2326
  }
@@ -2226,16 +2337,16 @@ function discoverClaudeSessionFiles(claudeHome, warnings, explicitHome) {
2226
2337
  }
2227
2338
  function configuredClaudeHomes(primaryHome) {
2228
2339
  if (process.env.CLAUDE_CONFIG_DIR) {
2229
- return process.env.CLAUDE_CONFIG_DIR.split(",").map((item) => item.trim()).filter(Boolean).map((item) => path5.resolve(item));
2340
+ return process.env.CLAUDE_CONFIG_DIR.split(",").map((item) => item.trim()).filter(Boolean).map((item) => path6.resolve(item));
2230
2341
  }
2231
2342
  return [primaryHome];
2232
2343
  }
2233
2344
  function collectProjectFiles(projectsPath, files, projectDirs, warnings) {
2234
- if (!fs5.existsSync(projectsPath)) return;
2345
+ if (!fs6.existsSync(projectsPath)) return;
2235
2346
  try {
2236
- for (const entry of fs5.readdirSync(projectsPath, { withFileTypes: true })) {
2347
+ for (const entry of fs6.readdirSync(projectsPath, { withFileTypes: true })) {
2237
2348
  if (!entry.isDirectory()) continue;
2238
- const projectDir = path5.join(projectsPath, entry.name);
2349
+ const projectDir = path6.join(projectsPath, entry.name);
2239
2350
  projectDirs.add(projectDir);
2240
2351
  walk2(projectDir, 5, (filePath) => {
2241
2352
  if (filePath.endsWith(".jsonl")) files.push({ filePath, projectDir, projectsPath });
@@ -2246,15 +2357,15 @@ function collectProjectFiles(projectsPath, files, projectDirs, warnings) {
2246
2357
  }
2247
2358
  }
2248
2359
  function collectDesktopSessionFiles(root, files, projectDirs, warnings) {
2249
- if (!fs5.existsSync(root)) return;
2360
+ if (!fs6.existsSync(root)) return;
2250
2361
  try {
2251
2362
  walk2(root, 8, (filePath) => {
2252
2363
  if (!filePath.endsWith(".jsonl")) return;
2253
- const parts = filePath.split(path5.sep);
2364
+ const parts = filePath.split(path6.sep);
2254
2365
  const projectsIndex = parts.lastIndexOf("projects");
2255
2366
  const hasProjectSegment = projectsIndex >= 0 && projectsIndex < parts.length - 2;
2256
- const projectsPath = hasProjectSegment ? parts.slice(0, projectsIndex + 1).join(path5.sep) || path5.sep : root;
2257
- const projectDir = hasProjectSegment ? parts.slice(0, projectsIndex + 2).join(path5.sep) || path5.sep : path5.dirname(filePath);
2367
+ const projectsPath = hasProjectSegment ? parts.slice(0, projectsIndex + 1).join(path6.sep) || path6.sep : root;
2368
+ const projectDir = hasProjectSegment ? parts.slice(0, projectsIndex + 2).join(path6.sep) || path6.sep : path6.dirname(filePath);
2258
2369
  projectDirs.add(projectDir);
2259
2370
  files.push({ filePath, projectDir, projectsPath });
2260
2371
  });
@@ -2264,9 +2375,9 @@ function collectDesktopSessionFiles(root, files, projectDirs, warnings) {
2264
2375
  }
2265
2376
  function walk2(root, maxDepth, onFile, depth = 0) {
2266
2377
  if (depth > maxDepth) return;
2267
- for (const entry of fs5.readdirSync(root, { withFileTypes: true })) {
2378
+ for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
2268
2379
  if (entry.name === "node_modules" || entry.name === ".git") continue;
2269
- const fullPath = path5.join(root, entry.name);
2380
+ const fullPath = path6.join(root, entry.name);
2270
2381
  if (entry.isDirectory()) {
2271
2382
  walk2(fullPath, maxDepth, onFile, depth + 1);
2272
2383
  } else if (entry.isFile()) {
@@ -2280,7 +2391,7 @@ function claudeFileToUsages(file, index, options, usageSelection) {
2280
2391
  return segments.map(({ breakdown: segment, suffix }) => claudeBreakdownToUsage(file, index, options, segment, suffix));
2281
2392
  }
2282
2393
  function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "") {
2283
- const id = breakdown.id ?? (path5.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
2394
+ const id = breakdown.id ?? (path6.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
2284
2395
  const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
2285
2396
  const cwd = breakdown.cwd ?? fallbackCwd;
2286
2397
  const repo = resolveRepoInfo(cwd, options.config);
@@ -2352,12 +2463,12 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
2352
2463
  };
2353
2464
  }
2354
2465
  function inferClaudeEntrypointFromPath(filePath) {
2355
- return filePath.split(path5.sep).includes("local-agent-mode-sessions") ? "local-agent" : void 0;
2466
+ return filePath.split(path6.sep).includes("local-agent-mode-sessions") ? "local-agent" : void 0;
2356
2467
  }
2357
2468
  function readClaudeBreakdown(filePath, usageSelection) {
2358
2469
  const breakdown = emptyBreakdown2();
2359
2470
  try {
2360
- const text = fs5.readFileSync(filePath, "utf8");
2471
+ const text = fs6.readFileSync(filePath, "utf8");
2361
2472
  const parse = parseJsonLines2(text);
2362
2473
  breakdown.rawEventCount = parse.records.length;
2363
2474
  breakdown.parseErrors = parse.errors;
@@ -2412,7 +2523,7 @@ function selectClaudeUsageOccurrences(files) {
2412
2523
  for (const file of files) {
2413
2524
  let parse;
2414
2525
  try {
2415
- parse = parseJsonLines2(fs5.readFileSync(file.filePath, "utf8"));
2526
+ parse = parseJsonLines2(fs6.readFileSync(file.filePath, "utf8"));
2416
2527
  } catch {
2417
2528
  continue;
2418
2529
  }
@@ -2446,7 +2557,7 @@ function usageDedupKey(record, message) {
2446
2557
  return `${requestId}:${messageId}`;
2447
2558
  }
2448
2559
  function occurrenceKey(filePath, recordIndex) {
2449
- return `${path5.resolve(filePath)}:${recordIndex}`;
2560
+ return `${path6.resolve(filePath)}:${recordIndex}`;
2450
2561
  }
2451
2562
  function cloneUsage(usage) {
2452
2563
  return {
@@ -2610,12 +2721,12 @@ function normalizeTimelineText2(text) {
2610
2721
  return text.replace(/\s+/g, " ").trim().slice(0, 1e3);
2611
2722
  }
2612
2723
  function inferProjectPath(projectDir, projectsPath) {
2613
- const name = path5.basename(projectDir);
2724
+ const name = path6.basename(projectDir);
2614
2725
  if (name.startsWith("-")) {
2615
- return `${path5.sep}${name.slice(1).split("-").filter(Boolean).join(path5.sep)}`;
2726
+ return `${path6.sep}${name.slice(1).split("-").filter(Boolean).join(path6.sep)}`;
2616
2727
  }
2617
- const relative = path5.relative(projectsPath, projectDir);
2618
- return relative && !relative.startsWith("..") ? path5.join(projectsPath, relative) : projectDir;
2728
+ const relative = path6.relative(projectsPath, projectDir);
2729
+ return relative && !relative.startsWith("..") ? path6.join(projectsPath, relative) : projectDir;
2619
2730
  }
2620
2731
  function inferOutcome2(breakdown) {
2621
2732
  if (breakdown.parseStatus === "failed") return "failed";
@@ -2639,20 +2750,20 @@ function claudeSourceAppLabel(entrypoint) {
2639
2750
  return "Claude Code";
2640
2751
  }
2641
2752
  function defaultClaudeHome() {
2642
- return process.env.CLAUDE_CONFIG_DIR?.split(",").map((item) => item.trim()).filter(Boolean)[0] ?? path5.join(os3.homedir(), ".claude");
2753
+ return process.env.CLAUDE_CONFIG_DIR?.split(",").map((item) => item.trim()).filter(Boolean)[0] ?? path6.join(os3.homedir(), ".claude");
2643
2754
  }
2644
2755
  function defaultDesktopSessionRoots() {
2645
2756
  const home = os3.homedir();
2646
- const xdg = process.env.XDG_CONFIG_HOME || path5.join(home, ".config");
2757
+ const xdg = process.env.XDG_CONFIG_HOME || path6.join(home, ".config");
2647
2758
  return [
2648
- path5.join(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions"),
2649
- path5.join(xdg, "Claude", "local-agent-mode-sessions")
2759
+ path6.join(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions"),
2760
+ path6.join(xdg, "Claude", "local-agent-mode-sessions")
2650
2761
  ];
2651
2762
  }
2652
2763
  function uniqueSessionFiles(files) {
2653
2764
  const seen = /* @__PURE__ */ new Set();
2654
2765
  return files.filter((file) => {
2655
- const key = path5.resolve(file.filePath);
2766
+ const key = path6.resolve(file.filePath);
2656
2767
  if (seen.has(key)) return false;
2657
2768
  seen.add(key);
2658
2769
  return true;
@@ -2662,10 +2773,10 @@ function unique3(values) {
2662
2773
  return [...new Set(values)];
2663
2774
  }
2664
2775
  function countHistoryEntries(historyPath) {
2665
- if (!fs5.existsSync(historyPath)) return 0;
2776
+ if (!fs6.existsSync(historyPath)) return 0;
2666
2777
  let entryCount = 0;
2667
2778
  try {
2668
- for (const line of fs5.readFileSync(historyPath, "utf8").split(/\r?\n/).filter(Boolean)) {
2779
+ for (const line of fs6.readFileSync(historyPath, "utf8").split(/\r?\n/).filter(Boolean)) {
2669
2780
  try {
2670
2781
  JSON.parse(line);
2671
2782
  entryCount += 1;
@@ -2725,16 +2836,16 @@ function errorMessage2(error) {
2725
2836
  }
2726
2837
  function safeMtimeMs(filePath) {
2727
2838
  try {
2728
- return fs5.statSync(filePath).mtimeMs;
2839
+ return fs6.statSync(filePath).mtimeMs;
2729
2840
  } catch {
2730
2841
  return 0;
2731
2842
  }
2732
2843
  }
2733
2844
 
2734
2845
  // packages/core/src/sources/cursor.ts
2735
- import fs6 from "node:fs";
2846
+ import fs7 from "node:fs";
2736
2847
  import os4 from "node:os";
2737
- import path6 from "node:path";
2848
+ import path7 from "node:path";
2738
2849
  import Database2 from "better-sqlite3";
2739
2850
  var parserVersion = "cursor-v1";
2740
2851
  var maxJsonlBytes = 20 * 1024 * 1024;
@@ -2742,19 +2853,21 @@ var maxDatabaseBytes = 100 * 1024 * 1024;
2742
2853
  var cursorStoragePrefixes = ["cursor.", "anysphere."];
2743
2854
  var interestingTerms = ["cursor", "composer", "aichat", "ai_chat", "aiservice", "agent-transcript", "prompttokenbreakdown"];
2744
2855
  function scanCursor(options) {
2745
- const cursorHome = options.cursorHome ?? path6.join(os4.homedir(), ".cursor");
2856
+ const cursorHome = options.cursorHome ?? path7.join(os4.homedir(), ".cursor");
2746
2857
  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));
2858
+ const jsonlFiles = discovery.jsonlFiles.filter((filePath) => fileMayOverlapScanWindow2(filePath, options.scanWindow));
2859
+ const databaseFiles = discovery.databaseFiles.filter((filePath) => fileMayOverlapScanWindow2(filePath, options.scanWindow));
2860
+ const jsonlCandidates = jsonlFiles.map((filePath) => cursorJsonlToCandidate(filePath, cursorHome));
2861
+ const databaseCandidates = databaseFiles.flatMap((filePath) => cursorDatabaseToCandidates(filePath, cursorHome, discovery.warnings));
2749
2862
  const candidates = dedupeCandidates([...jsonlCandidates, ...databaseCandidates]);
2750
2863
  const sessions = candidates.map((candidate, index) => candidateToUsage(candidate, index, options));
2751
2864
  const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
2752
2865
  const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
2753
2866
  const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
2754
- if (!fs6.existsSync(cursorHome)) {
2867
+ if (!fs7.existsSync(cursorHome)) {
2755
2868
  discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
2756
2869
  }
2757
- if (!fs6.existsSync(discovery.statePath)) {
2870
+ if (!fs7.existsSync(discovery.statePath)) {
2758
2871
  discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
2759
2872
  }
2760
2873
  return {
@@ -2775,11 +2888,11 @@ function scanCursor(options) {
2775
2888
  workspaceStoragePath: discovery.workspaceStoragePath,
2776
2889
  projectsPath: discovery.projectsPath,
2777
2890
  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,
2891
+ projectsExists: fs7.existsSync(discovery.projectsPath),
2892
+ stateExists: fs7.existsSync(discovery.statePath),
2893
+ sessionsExists: jsonlFiles.length > 0,
2894
+ sessionFileCount: jsonlFiles.length,
2895
+ databaseFileCount: databaseFiles.length,
2783
2896
  skippedFileCount: discovery.skippedFileCount,
2784
2897
  sessionsImported: sessions.length,
2785
2898
  parseFailureCount,
@@ -2797,7 +2910,7 @@ function discoverCursorFiles(cursorHome, explicitHome) {
2797
2910
  const databaseFiles = [];
2798
2911
  let skippedFileCount = 0;
2799
2912
  for (const root of roots) {
2800
- if (!fs6.existsSync(root)) continue;
2913
+ if (!fs7.existsSync(root)) continue;
2801
2914
  try {
2802
2915
  if (isReadableRegularFile(root, maxDatabaseBytes)) {
2803
2916
  databaseFiles.push(root);
@@ -2821,7 +2934,7 @@ function discoverCursorFiles(cursorHome, explicitHome) {
2821
2934
  }
2822
2935
  return {
2823
2936
  roots,
2824
- projectsPath: path6.join(cursorHome, "projects"),
2937
+ projectsPath: path7.join(cursorHome, "projects"),
2825
2938
  globalStoragePath: appPaths.globalStoragePath,
2826
2939
  workspaceStoragePath: appPaths.workspaceStoragePath,
2827
2940
  statePath: appPaths.statePath,
@@ -2835,12 +2948,12 @@ function cursorOwnedGlobalStorageRoots(globalStoragePath) {
2835
2948
  return cursorOwnedChildDirs(globalStoragePath);
2836
2949
  }
2837
2950
  function cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
2838
- if (!fs6.existsSync(workspaceStoragePath)) return [];
2951
+ if (!fs7.existsSync(workspaceStoragePath)) return [];
2839
2952
  const roots = [];
2840
2953
  try {
2841
- for (const workspace of fs6.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
2954
+ for (const workspace of fs7.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
2842
2955
  if (!workspace.isDirectory()) continue;
2843
- roots.push(...cursorOwnedChildDirs(path6.join(workspaceStoragePath, workspace.name)));
2956
+ roots.push(...cursorOwnedChildDirs(path7.join(workspaceStoragePath, workspace.name)));
2844
2957
  }
2845
2958
  } catch {
2846
2959
  return [];
@@ -2848,9 +2961,9 @@ function cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
2848
2961
  return roots;
2849
2962
  }
2850
2963
  function cursorOwnedChildDirs(root) {
2851
- if (!fs6.existsSync(root)) return [];
2964
+ if (!fs7.existsSync(root)) return [];
2852
2965
  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));
2966
+ return fs7.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && cursorStoragePrefixes.some((prefix) => entry.name.toLowerCase().startsWith(prefix))).map((entry) => path7.join(root, entry.name));
2854
2967
  } catch {
2855
2968
  return [];
2856
2969
  }
@@ -2858,35 +2971,35 @@ function cursorOwnedChildDirs(root) {
2858
2971
  function defaultCursorAppPaths() {
2859
2972
  const home = os4.homedir();
2860
2973
  if (process.platform === "win32") {
2861
- const appData = process.env.APPDATA ?? path6.join(home, "AppData", "Roaming");
2862
- const globalStoragePath2 = path6.join(appData, "Cursor", "User", "globalStorage");
2974
+ const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
2975
+ const globalStoragePath2 = path7.join(appData, "Cursor", "User", "globalStorage");
2863
2976
  return {
2864
2977
  globalStoragePath: globalStoragePath2,
2865
- workspaceStoragePath: path6.join(appData, "Cursor", "User", "workspaceStorage"),
2866
- statePath: path6.join(globalStoragePath2, "state.vscdb")
2978
+ workspaceStoragePath: path7.join(appData, "Cursor", "User", "workspaceStorage"),
2979
+ statePath: path7.join(globalStoragePath2, "state.vscdb")
2867
2980
  };
2868
2981
  }
2869
2982
  if (process.platform === "darwin") {
2870
- const globalStoragePath2 = path6.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
2983
+ const globalStoragePath2 = path7.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
2871
2984
  return {
2872
2985
  globalStoragePath: globalStoragePath2,
2873
- workspaceStoragePath: path6.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
2874
- statePath: path6.join(globalStoragePath2, "state.vscdb")
2986
+ workspaceStoragePath: path7.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
2987
+ statePath: path7.join(globalStoragePath2, "state.vscdb")
2875
2988
  };
2876
2989
  }
2877
- const xdg = process.env.XDG_CONFIG_HOME || path6.join(home, ".config");
2878
- const globalStoragePath = path6.join(xdg, "Cursor", "User", "globalStorage");
2990
+ const xdg = process.env.XDG_CONFIG_HOME || path7.join(home, ".config");
2991
+ const globalStoragePath = path7.join(xdg, "Cursor", "User", "globalStorage");
2879
2992
  return {
2880
2993
  globalStoragePath,
2881
- workspaceStoragePath: path6.join(xdg, "Cursor", "User", "workspaceStorage"),
2882
- statePath: path6.join(globalStoragePath, "state.vscdb")
2994
+ workspaceStoragePath: path7.join(xdg, "Cursor", "User", "workspaceStorage"),
2995
+ statePath: path7.join(globalStoragePath, "state.vscdb")
2883
2996
  };
2884
2997
  }
2885
2998
  function walk3(root, maxDepth, onFile, depth = 0) {
2886
2999
  if (depth > maxDepth) return;
2887
- for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
3000
+ for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
2888
3001
  if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "CachedData" || entry.name === "Cache") continue;
2889
- const fullPath = path6.join(root, entry.name);
3002
+ const fullPath = path7.join(root, entry.name);
2890
3003
  if (entry.isDirectory()) {
2891
3004
  walk3(fullPath, maxDepth, onFile, depth + 1);
2892
3005
  } else if (entry.isFile()) {
@@ -2896,22 +3009,34 @@ function walk3(root, maxDepth, onFile, depth = 0) {
2896
3009
  }
2897
3010
  function isReadableRegularFile(filePath, maxBytes) {
2898
3011
  try {
2899
- fs6.accessSync(filePath, fs6.constants.R_OK);
2900
- const stat = fs6.statSync(filePath);
3012
+ fs7.accessSync(filePath, fs7.constants.R_OK);
3013
+ const stat = fs7.statSync(filePath);
2901
3014
  return stat.isFile() && stat.size <= maxBytes;
2902
3015
  } catch {
2903
3016
  return false;
2904
3017
  }
2905
3018
  }
2906
3019
  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;
3020
+ const aTranscript = a.includes(`${path7.sep}agent-transcripts${path7.sep}`) ? 0 : 1;
3021
+ const bTranscript = b.includes(`${path7.sep}agent-transcripts${path7.sep}`) ? 0 : 1;
2909
3022
  return aTranscript - bTranscript || a.localeCompare(b);
2910
3023
  }
3024
+ function fileMayOverlapScanWindow2(filePath, scanWindow) {
3025
+ if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
3026
+ const inferredTimestamp = timestampMs2(inferDateFromCursorPath(filePath));
3027
+ if (scanWindow.fromMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp < scanWindow.fromMs) return false;
3028
+ if (scanWindow.toMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp > scanWindow.toMs) return false;
3029
+ if (scanWindow.fromMs === void 0) return true;
3030
+ try {
3031
+ return fs7.statSync(filePath).mtimeMs >= scanWindow.fromMs;
3032
+ } catch {
3033
+ return true;
3034
+ }
3035
+ }
2911
3036
  function cursorJsonlToCandidate(filePath, cursorHome) {
2912
3037
  const breakdown = emptyBreakdown3();
2913
3038
  try {
2914
- const parse = parseJsonLines3(fs6.readFileSync(filePath, "utf8"));
3039
+ const parse = parseJsonLines3(fs7.readFileSync(filePath, "utf8"));
2915
3040
  breakdown.rawEventCount = parse.records.length;
2916
3041
  breakdown.parseErrors = parse.errors;
2917
3042
  breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
@@ -2923,7 +3048,12 @@ function cursorJsonlToCandidate(filePath, cursorHome) {
2923
3048
  breakdown.parseErrors.push(message);
2924
3049
  breakdown.warnings.push(message);
2925
3050
  }
2926
- if (!breakdown.id) breakdown.id = path6.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
3051
+ if (!breakdown.id) breakdown.id = path7.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
3052
+ const fallbackDate = inferDateFromCursorPath(filePath) ?? fileModifiedDate(filePath);
3053
+ if (fallbackDate) {
3054
+ breakdown.startedAt ??= fallbackDate;
3055
+ breakdown.endedAt ??= fallbackDate;
3056
+ }
2927
3057
  return {
2928
3058
  sourcePath: filePath,
2929
3059
  surface: inferSurfaceFromPath(filePath),
@@ -3193,7 +3323,7 @@ function finalizeBreakdown(breakdown) {
3193
3323
  }
3194
3324
  function candidateToUsage(candidate, index, options) {
3195
3325
  const breakdown = candidate.breakdown;
3196
- const id = breakdown.id ?? `${path6.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
3326
+ const id = breakdown.id ?? `${path7.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
3197
3327
  const cwd = breakdown.cwd ?? candidate.fallbackCwd ?? process.cwd();
3198
3328
  const repo = resolveRepoInfo(cwd, options.config);
3199
3329
  const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
@@ -3272,21 +3402,81 @@ function candidateToUsage(candidate, index, options) {
3272
3402
  };
3273
3403
  }
3274
3404
  function inferProjectPathFromCursorPath(filePath, cursorHome) {
3275
- const parts = filePath.split(path6.sep);
3405
+ const parts = filePath.split(path7.sep);
3276
3406
  const projectsIndex = parts.lastIndexOf("projects");
3277
3407
  if (projectsIndex >= 0 && projectsIndex < parts.length - 1) {
3278
3408
  const projectId = parts[projectsIndex + 1];
3409
+ const decodedProjectPath = decodeCursorProjectId(projectId);
3410
+ if (decodedProjectPath) return decodedProjectPath;
3279
3411
  if (projectId?.startsWith("-")) {
3280
- return `${path6.sep}${projectId.slice(1).split("-").filter(Boolean).join(path6.sep)}`;
3412
+ return `${path7.sep}${projectId.slice(1).split("-").filter(Boolean).join(path7.sep)}`;
3281
3413
  }
3282
- if (projectId) return path6.join(cursorHome, "projects", projectId);
3414
+ if (projectId) return path7.join(cursorHome, "projects", projectId);
3283
3415
  }
3284
3416
  return void 0;
3285
3417
  }
3418
+ function inferDateFromCursorPath(filePath) {
3419
+ const parts = filePath.split(path7.sep);
3420
+ for (const part of parts) {
3421
+ if (!/^\d{10,13}$/.test(part)) continue;
3422
+ const date = dateValue4(Number(part));
3423
+ if (date) return date;
3424
+ }
3425
+ return void 0;
3426
+ }
3427
+ function timestampMs2(value) {
3428
+ if (!value) return void 0;
3429
+ const timestamp3 = new Date(value).getTime();
3430
+ return Number.isFinite(timestamp3) ? timestamp3 : void 0;
3431
+ }
3432
+ function fileModifiedDate(filePath) {
3433
+ try {
3434
+ return fs7.statSync(filePath).mtime.toISOString();
3435
+ } catch {
3436
+ return void 0;
3437
+ }
3438
+ }
3439
+ function decodeCursorProjectId(projectId) {
3440
+ if (!projectId) return void 0;
3441
+ const id = projectId.startsWith("-") ? projectId.slice(1) : projectId;
3442
+ const tokens2 = id.split("-").filter(Boolean);
3443
+ if (tokens2.length < 2) return void 0;
3444
+ const root = cursorProjectRoot(tokens2);
3445
+ if (!root) return void 0;
3446
+ const matched = matchEncodedPath(root.path, tokens2.slice(root.consumed));
3447
+ if (matched) return matched;
3448
+ return path7.join(root.path, ...tokens2.slice(root.consumed));
3449
+ }
3450
+ function cursorProjectRoot(tokens2) {
3451
+ if (tokens2[0] === "Users" && tokens2[1]) return { path: path7.join(path7.sep, "Users", tokens2[1]), consumed: 2 };
3452
+ if (tokens2[0] === "home" && tokens2[1]) return { path: path7.join(path7.sep, "home", tokens2[1]), consumed: 2 };
3453
+ if (tokens2[0] === "tmp") return { path: path7.join(path7.sep, "tmp"), consumed: 1 };
3454
+ if (/^[A-Za-z]$/.test(tokens2[0] ?? "") && tokens2[1]) return { path: `${tokens2[0]?.toUpperCase()}:\\`, consumed: 1 };
3455
+ return void 0;
3456
+ }
3457
+ function matchEncodedPath(root, tokens2) {
3458
+ if (!tokens2.length) return fs7.existsSync(root) ? root : void 0;
3459
+ if (!fs7.existsSync(root)) return void 0;
3460
+ try {
3461
+ for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
3462
+ if (!entry.isDirectory()) continue;
3463
+ const entryTokens = entry.name.split("-").filter(Boolean);
3464
+ if (!startsWithTokens(tokens2, entryTokens)) continue;
3465
+ const matched = matchEncodedPath(path7.join(root, entry.name), tokens2.slice(entryTokens.length));
3466
+ if (matched) return matched;
3467
+ }
3468
+ } catch {
3469
+ return void 0;
3470
+ }
3471
+ return void 0;
3472
+ }
3473
+ function startsWithTokens(tokens2, prefix) {
3474
+ return prefix.length <= tokens2.length && prefix.every((token, index) => tokens2[index] === token);
3475
+ }
3286
3476
  function inferSurfaceFromPath(filePath) {
3287
3477
  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";
3478
+ if (lower.includes(`${path7.sep}agent-transcripts${path7.sep}`) || lower.endsWith(".jsonl")) return "cli";
3479
+ if (lower.includes(`${path7.sep}workspaceStorage${path7.sep}`.toLowerCase()) || lower.includes(`${path7.sep}globalStorage${path7.sep}`.toLowerCase()) || lower.endsWith(".vscdb")) return "desktop";
3290
3480
  return "unknown";
3291
3481
  }
3292
3482
  function cursorSourceAppLabel(surface) {
@@ -3468,52 +3658,103 @@ function scanUsageSources(options) {
3468
3658
  const codex = scanCodex({
3469
3659
  ...options.codexHome ? { codexHome: options.codexHome } : {},
3470
3660
  ...options.config ? { config: options.config } : {},
3661
+ ...options.scanWindow ? { scanWindow: options.scanWindow } : {},
3471
3662
  pricing: options.pricing
3472
3663
  });
3473
3664
  const claude = scanClaude({
3474
3665
  ...options.claudeHome ? { claudeHome: options.claudeHome } : {},
3475
3666
  ...options.config ? { config: options.config } : {},
3667
+ ...options.scanWindow ? { scanWindow: options.scanWindow } : {},
3476
3668
  pricing: options.pricing
3477
3669
  });
3478
- const cursor = scanCursor({
3670
+ const cursorEnabled = options.config?.experimentalSources?.cursor === true || Boolean(options.cursorHome);
3671
+ const cursor = cursorEnabled ? scanCursor({
3479
3672
  ...options.cursorHome ? { cursorHome: options.cursorHome } : {},
3480
3673
  ...options.config ? { config: options.config } : {},
3674
+ ...options.scanWindow ? { scanWindow: options.scanWindow } : {},
3481
3675
  pricing: options.pricing
3482
- });
3676
+ }) : void 0;
3483
3677
  return {
3484
- sources: [codex.source, claude.source, cursor.source],
3485
- sourceStats: [codex.stats, claude.stats, cursor.stats],
3486
- sessions: [...codex.sessions, ...claude.sessions, ...cursor.sessions]
3678
+ sources: [codex.source, claude.source, ...cursor ? [cursor.source] : []],
3679
+ sourceStats: [codex.stats, claude.stats, ...cursor ? [cursor.stats] : []],
3680
+ sessions: [...codex.sessions, ...claude.sessions, ...cursor?.sessions ?? []]
3487
3681
  };
3488
3682
  }
3489
3683
 
3490
3684
  // apps/server/src/data.ts
3491
- import fs7 from "node:fs";
3685
+ import fs8 from "node:fs";
3686
+ import path8 from "node:path";
3687
+ import { spawn } from "node:child_process";
3688
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3492
3689
  var scanCacheTtlMs = 1e4;
3493
3690
  var scanCache;
3691
+ var warmScanInFlight = false;
3692
+ var warmScanChild;
3693
+ var warmedScanKeys = /* @__PURE__ */ new Set();
3494
3694
  function readDashboardData(filters = {}, options = {}) {
3495
- const raw = readRawUsageData();
3695
+ const raw = readRawUsageData(filters);
3496
3696
  return buildDashboardSnapshot({ ...raw, sessions: displaySessions(raw.sessions, options), filters });
3497
3697
  }
3498
3698
  function clearScanCache() {
3499
3699
  scanCache = void 0;
3700
+ warmScanInFlight = false;
3701
+ warmScanChild?.kill();
3702
+ warmScanChild = void 0;
3703
+ warmedScanKeys.clear();
3500
3704
  }
3501
- function readRawUsageData() {
3705
+ function readRawUsageData(filters = {}) {
3502
3706
  const cwd = runtimeCwd();
3503
- if (scanCache && scanCache.cwd === cwd && scanCache.expiresAt > Date.now()) {
3707
+ const { config, warnings: configWarnings } = loadConfigWithWarnings();
3708
+ const cacheKey = scanCacheKey({ cwd, filters, cursorEnabled: config.experimentalSources?.cursor === true });
3709
+ if (scanCache && scanCache.key === cacheKey && scanCache.expiresAt > Date.now()) {
3504
3710
  return scanCache.data;
3505
3711
  }
3506
- const { config, warnings: configWarnings } = loadConfigWithWarnings();
3507
3712
  const pricing = loadPricingTable(resolvePricingPath(config));
3508
- const scan = scanUsageSources({ config, pricing });
3713
+ const scanWindow = scanWindowFromFilters(filters);
3714
+ const scan = scanUsageSources({
3715
+ config,
3716
+ pricing,
3717
+ ...scanWindow ? { scanWindow } : {}
3718
+ });
3509
3719
  const data = {
3510
3720
  sources: scan.sources.map((source) => ({ ...source, warnings: [...source.warnings, ...configWarnings] })),
3511
3721
  sourceStats: scan.sourceStats,
3512
3722
  sessions: scan.sessions
3513
3723
  };
3514
- scanCache = { cwd, data, expiresAt: Date.now() + scanCacheTtlMs };
3724
+ scanCache = { key: cacheKey, data, expiresAt: Date.now() + scanCacheTtlMs };
3515
3725
  return data;
3516
3726
  }
3727
+ function warmDashboardCache(filters = {}, options = {}) {
3728
+ const cwd = runtimeCwd();
3729
+ const config = loadConfig();
3730
+ const key = scanCacheKey({ cwd, filters, cursorEnabled: config.experimentalSources?.cursor === true });
3731
+ if (scanCache?.key === key || warmedScanKeys.has(key)) return { started: false, key, reason: "already_warm" };
3732
+ if (warmScanInFlight) return { started: false, key, reason: "warm_scan_in_flight" };
3733
+ warmScanInFlight = true;
3734
+ let child;
3735
+ try {
3736
+ child = spawn(process.execPath, [...process.execArgv, cliEntryPath(), "__warm-cache", ...warmCacheArgs(filters, options)], {
3737
+ detached: true,
3738
+ env: process.env,
3739
+ stdio: "ignore"
3740
+ });
3741
+ } catch (error) {
3742
+ warmScanInFlight = false;
3743
+ return { started: false, key, reason: error instanceof Error ? error.message : String(error) };
3744
+ }
3745
+ warmScanChild = child;
3746
+ child.once("error", () => {
3747
+ warmScanInFlight = false;
3748
+ if (warmScanChild === child) warmScanChild = void 0;
3749
+ });
3750
+ child.once("exit", (code) => {
3751
+ warmScanInFlight = false;
3752
+ if (warmScanChild === child) warmScanChild = void 0;
3753
+ if (code === 0) warmedScanKeys.add(key);
3754
+ });
3755
+ child.unref();
3756
+ return { started: true, key };
3757
+ }
3517
3758
  function parseFilters(query) {
3518
3759
  const filters = {};
3519
3760
  const source = listQuery(query.source);
@@ -3543,6 +3784,20 @@ function readPricingData() {
3543
3784
  models: loadPricingTable(pricingPath)
3544
3785
  };
3545
3786
  }
3787
+ function readConfigData() {
3788
+ return {
3789
+ path: configPath(),
3790
+ config: loadConfig()
3791
+ };
3792
+ }
3793
+ function writeConfigData(nextConfig) {
3794
+ const saved = saveConfig(nextConfig);
3795
+ clearScanCache();
3796
+ return {
3797
+ path: configPath(),
3798
+ config: saved
3799
+ };
3800
+ }
3546
3801
  function writePricingData(models) {
3547
3802
  const config = loadConfig();
3548
3803
  const pricingPath = resolvePricingPath(config);
@@ -3555,11 +3810,16 @@ function writePricingData(models) {
3555
3810
  }
3556
3811
  function clearRepoSpendLocalData() {
3557
3812
  const target = repospendHome();
3558
- const removed = fs7.existsSync(target);
3559
- fs7.rmSync(target, { recursive: true, force: true });
3813
+ const removed = fs8.existsSync(target);
3814
+ fs8.rmSync(target, { recursive: true, force: true });
3560
3815
  clearScanCache();
3561
3816
  return { path: target, removed };
3562
3817
  }
3818
+ function clearRepoSpendParseCache() {
3819
+ const result = clearRepoSpendCache();
3820
+ clearScanCache();
3821
+ return result;
3822
+ }
3563
3823
  function exportJson() {
3564
3824
  return JSON.stringify(readDashboardData().sessions, null, 2);
3565
3825
  }
@@ -3587,6 +3847,35 @@ function sourceScopedAppLabel(session) {
3587
3847
  if (!shouldScopeApp(app)) return app;
3588
3848
  return `${sourceClientLabel(session.sourceClient)} on ${app}`;
3589
3849
  }
3850
+ function scanWindowFromFilters(filters) {
3851
+ const fromMs = filters.from ? lowerBound(filters.from) : void 0;
3852
+ const toMs = filters.to ? upperBound(filters.to) : void 0;
3853
+ const scanWindow = {};
3854
+ if (fromMs !== void 0) scanWindow.fromMs = fromMs;
3855
+ if (toMs !== void 0) scanWindow.toMs = toMs;
3856
+ return scanWindow.fromMs === void 0 && scanWindow.toMs === void 0 ? void 0 : scanWindow;
3857
+ }
3858
+ function scanCacheKey({ cwd, filters, cursorEnabled }) {
3859
+ return JSON.stringify({
3860
+ cwd,
3861
+ from: filters.from ?? "",
3862
+ to: filters.to ?? "",
3863
+ cursor: cursorEnabled
3864
+ });
3865
+ }
3866
+ function warmCacheArgs(filters, options) {
3867
+ const args2 = [];
3868
+ if (filters.from) args2.push("--from", filters.from);
3869
+ if (filters.to) args2.push("--to", filters.to);
3870
+ if (options.splitSourceApps) args2.push("--splitSourceApps");
3871
+ return args2;
3872
+ }
3873
+ function cliEntryPath() {
3874
+ const currentDir = path8.dirname(fileURLToPath2(import.meta.url));
3875
+ const sourceCli = path8.join(currentDir, "cli.ts");
3876
+ if (fs8.existsSync(sourceCli)) return sourceCli;
3877
+ return path8.join(currentDir, "cli.js");
3878
+ }
3590
3879
  function shouldScopeApp(app) {
3591
3880
  const normalized = app.toLowerCase();
3592
3881
  return normalized.includes("vs code") || normalized.includes("vscode") || normalized.includes("terminal") || normalized.includes("cli");
@@ -3612,9 +3901,9 @@ function runtimeCwd() {
3612
3901
  }
3613
3902
 
3614
3903
  // apps/server/src/server.ts
3615
- import fs8 from "node:fs";
3616
- import path7 from "node:path";
3617
- import { fileURLToPath as fileURLToPath2 } from "node:url";
3904
+ import fs9 from "node:fs";
3905
+ import path9 from "node:path";
3906
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
3618
3907
  import fastifyStatic from "@fastify/static";
3619
3908
  import Fastify from "fastify";
3620
3909
 
@@ -4427,6 +4716,7 @@ function createServer(options = {}) {
4427
4716
  const demoMode = demoModeEnabled();
4428
4717
  const data = demoMode ? readDemoDashboardData(filters, dashboardOptions) : readDashboardData(filters, dashboardOptions);
4429
4718
  const rtkGain = demoMode ? readDemoRtkGain() : await readRtkGain();
4719
+ const configData = readConfigData();
4430
4720
  return {
4431
4721
  ...data,
4432
4722
  appVersion: readPackageVersion(),
@@ -4435,6 +4725,8 @@ function createServer(options = {}) {
4435
4725
  info: pricingInfo,
4436
4726
  ...readPricingData()
4437
4727
  },
4728
+ config: configData.config,
4729
+ configPath: configData.path,
4438
4730
  rtkGain
4439
4731
  };
4440
4732
  });
@@ -4442,7 +4734,17 @@ function createServer(options = {}) {
4442
4734
  info: pricingInfo,
4443
4735
  ...writePricingData(validatePricingBody(request.body))
4444
4736
  }));
4737
+ app.put("/api/settings/config", async (request) => writeConfigData(validateConfigBody(request.body)));
4445
4738
  app.delete("/api/settings/local-data", async () => clearRepoSpendLocalData());
4739
+ app.delete("/api/settings/cache", async () => {
4740
+ if (demoModeEnabled()) return { path: "demo mode (cache disabled)", removed: false, demo: true };
4741
+ return clearRepoSpendParseCache();
4742
+ });
4743
+ app.post("/api/cache/warm", async (request) => {
4744
+ if (demoModeEnabled()) return { started: false, reason: "demo_mode" };
4745
+ const query = request.query;
4746
+ return warmDashboardCache(warmCacheFilters(), parseDashboardOptions(query));
4747
+ });
4446
4748
  app.get("/api/export.json", async (_request, reply) => {
4447
4749
  reply.header("content-type", "application/json; charset=utf-8");
4448
4750
  return exportJson();
@@ -4453,9 +4755,9 @@ function createServer(options = {}) {
4453
4755
  return exportCsv();
4454
4756
  });
4455
4757
  if (options.serveWeb !== false) {
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");
4758
+ const serverDist = path9.dirname(fileURLToPath3(import.meta.url));
4759
+ const packagedWebDist = path9.resolve(serverDist, "../web-dist");
4760
+ const workspaceWebDist = path9.resolve(serverDist, "../../web/dist");
4459
4761
  const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
4460
4762
  if (!fsExists(webDist)) {
4461
4763
  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.`);
@@ -4476,13 +4778,13 @@ function createServer(options = {}) {
4476
4778
  return app;
4477
4779
  }
4478
4780
  function fsExists(inputPath) {
4479
- return Boolean(inputPath) && path7.isAbsolute(inputPath) && fs8.existsSync(inputPath);
4781
+ return Boolean(inputPath) && path9.isAbsolute(inputPath) && fs9.existsSync(inputPath);
4480
4782
  }
4481
4783
  function readPackageVersion() {
4482
- const serverDist = path7.dirname(fileURLToPath2(import.meta.url));
4483
- const packagePath = path7.resolve(serverDist, "../package.json");
4784
+ const serverDist = path9.dirname(fileURLToPath3(import.meta.url));
4785
+ const packagePath = path9.resolve(serverDist, "../package.json");
4484
4786
  try {
4485
- const parsed = JSON.parse(fs8.readFileSync(packagePath, "utf8"));
4787
+ const parsed = JSON.parse(fs9.readFileSync(packagePath, "utf8"));
4486
4788
  return typeof parsed.version === "string" ? parsed.version : "unknown";
4487
4789
  } catch {
4488
4790
  return "unknown";
@@ -4491,9 +4793,28 @@ function readPackageVersion() {
4491
4793
  async function startServer(options = {}) {
4492
4794
  const port = options.port ?? Number(process.env.REPOSPEND_PORT ?? 2005);
4493
4795
  const host = options.host ?? "127.0.0.1";
4494
- const app = createServer(options);
4495
- await app.listen({ host, port });
4496
- return { app, url: `http://localhost:${port}` };
4796
+ const candidatePorts = portCandidates(port, options.portFallbackCount ?? 20);
4797
+ let lastError;
4798
+ for (const candidatePort of candidatePorts) {
4799
+ const app = createServer(options);
4800
+ try {
4801
+ await app.listen({ host, port: candidatePort });
4802
+ return { app, url: `http://localhost:${candidatePort}`, port: candidatePort };
4803
+ } catch (error) {
4804
+ await app.close().catch(() => void 0);
4805
+ lastError = error;
4806
+ if (!isAddressInUse(error)) throw error;
4807
+ }
4808
+ }
4809
+ throw lastError;
4810
+ }
4811
+ function portCandidates(startPort, fallbackCount) {
4812
+ if (!Number.isInteger(startPort) || startPort <= 0 || startPort > 65535) return [2005];
4813
+ const count2 = Math.max(0, Math.min(fallbackCount, 100));
4814
+ return Array.from({ length: count2 + 1 }, (_value, index) => startPort + index).filter((port) => port <= 65535);
4815
+ }
4816
+ function isAddressInUse(error) {
4817
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE");
4497
4818
  }
4498
4819
  function validatePricingBody(body) {
4499
4820
  if (!body || typeof body !== "object" || Array.isArray(body)) {
@@ -4525,6 +4846,32 @@ function validatePricingBody(body) {
4525
4846
  }
4526
4847
  return validated;
4527
4848
  }
4849
+ function validateConfigBody(body) {
4850
+ if (!body || typeof body !== "object" || Array.isArray(body)) return {};
4851
+ const input = body;
4852
+ const config = {};
4853
+ if (Array.isArray(input.repos)) config.repos = input.repos;
4854
+ if (Array.isArray(input.budgets)) config.budgets = input.budgets;
4855
+ if (typeof input.pricingPath === "string") config.pricingPath = input.pricingPath;
4856
+ const experimentalSources = input.experimentalSources;
4857
+ if (experimentalSources && typeof experimentalSources === "object" && !Array.isArray(experimentalSources)) {
4858
+ config.experimentalSources = {
4859
+ cursor: experimentalSources.cursor === true
4860
+ };
4861
+ }
4862
+ return config;
4863
+ }
4864
+ function warmCacheFilters() {
4865
+ const days = 30;
4866
+ const now = /* @__PURE__ */ new Date();
4867
+ return {
4868
+ from: dateOnly(new Date(now.getTime() - days * 24 * 60 * 60 * 1e3)),
4869
+ to: dateOnly(now)
4870
+ };
4871
+ }
4872
+ function dateOnly(value) {
4873
+ return value.toISOString().slice(0, 10);
4874
+ }
4528
4875
  function finiteNumber(value) {
4529
4876
  if (value === "" || value === null || value === void 0) return void 0;
4530
4877
  const parsed = typeof value === "number" ? value : Number(value);
@@ -4537,9 +4884,13 @@ try {
4537
4884
  if (command === "--help" || command === "-h" || command === "help") {
4538
4885
  printHelp();
4539
4886
  } else if (command === "serve") {
4540
- const { url } = await startServer({ port: Number(process.env.REPOSPEND_PORT ?? 2005), serveWeb: true });
4887
+ const requestedPort = Number(process.env.REPOSPEND_PORT ?? 2005);
4888
+ const { url, port } = await startServer({ port: requestedPort, serveWeb: true });
4889
+ if (port !== requestedPort) console.log(`Port ${requestedPort} is already in use; using ${port} instead.`);
4541
4890
  console.log(`RepoSpend dashboard: ${url}`);
4542
4891
  openBrowser(url);
4892
+ } else if (command === "__warm-cache") {
4893
+ readDashboardData(cliDateFilters(args), { splitSourceApps: args.includes("--splitSourceApps") });
4543
4894
  } else if (command === "scan") {
4544
4895
  const data = readDashboardData(cliFilters(args));
4545
4896
  console.log(JSON.stringify({ sources: data.sources, sessionCount: data.sessions.length, summary: data.summary }, null, 2));
@@ -4571,14 +4922,14 @@ function openBrowser(url) {
4571
4922
  if (process.env.REPOSPEND_NO_OPEN === "1" || process.env.CI === "true") return;
4572
4923
  const command2 = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
4573
4924
  const args2 = process.platform === "win32" ? ["/c", "start", "", url] : [url];
4574
- const child = spawn(command2, args2, { detached: true, stdio: "ignore" });
4925
+ const child = spawn2(command2, args2, { detached: true, stdio: "ignore" });
4575
4926
  child.on("error", () => {
4576
4927
  });
4577
4928
  child.unref();
4578
4929
  }
4579
4930
  function printGroups(groups) {
4580
4931
  if (!groups.length) {
4581
- console.log("No usage found. Scanned Codex, Claude Code, and Cursor local data paths.");
4932
+ console.log("No usage found. Scanned enabled local data paths.");
4582
4933
  return;
4583
4934
  }
4584
4935
  const rows = groups.map((group) => ({
@@ -4594,6 +4945,14 @@ function cliFilters(args2) {
4594
4945
  if (!source || source === "all") return {};
4595
4946
  return { source };
4596
4947
  }
4948
+ function cliDateFilters(args2) {
4949
+ const from = valueAfter(args2, "--from");
4950
+ const to = valueAfter(args2, "--to");
4951
+ return {
4952
+ ...from ? { from } : {},
4953
+ ...to ? { to } : {}
4954
+ };
4955
+ }
4597
4956
  function valueAfter(args2, flag) {
4598
4957
  const index = args2.indexOf(flag);
4599
4958
  return index >= 0 ? args2[index + 1] : void 0;
@@ -4611,5 +4970,7 @@ function printHelp() {
4611
4970
  repospend export --format csv
4612
4971
 
4613
4972
  Options:
4614
- --source all|codex|claude|cursor`);
4973
+ --source all|codex|claude|cursor
4974
+
4975
+ Cursor is experimental and only scans when enabled in ~/.repospend/config.json.`);
4615
4976
  }