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/CHANGELOG.md +25 -1
- package/README.md +20 -5
- package/dist/cli.js +531 -170
- package/docs/screenshots/agent-friction.png +0 -0
- package/docs/screenshots/dashboard-overview.png +0 -0
- package/docs/screenshots/models-view.png +0 -0
- package/docs/screenshots/repo-detail.png +0 -0
- package/docs/screenshots/repos-view.png +0 -0
- package/docs/screenshots/session-detail.png +0 -0
- package/docs/screenshots/sessions-view.png +0 -0
- package/package.json +2 -1
- package/web-dist/assets/index-D9bkUUxw.css +1 -0
- package/web-dist/assets/index-L2zVlSNd.js +306 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-DAOannAp.css +0 -1
- package/web-dist/assets/index-DJs7VNSz.js +0 -306
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/
|
|
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
|
|
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 (!
|
|
383
|
+
if (!fs3.existsSync(localConfigPath)) {
|
|
327
384
|
return { config: {}, warnings: [] };
|
|
328
385
|
}
|
|
329
386
|
try {
|
|
330
|
-
return { config: JSON.parse(
|
|
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
|
|
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}${
|
|
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
|
|
865
|
-
import
|
|
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 =
|
|
871
|
-
let current = normalizeRoot(
|
|
934
|
+
function findGitRoot(cwd, fileSystem = fs4) {
|
|
935
|
+
let current = normalizeRoot(path4.resolve(cwd));
|
|
872
936
|
while (true) {
|
|
873
|
-
const gitPath =
|
|
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 =
|
|
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 =
|
|
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 ?? (
|
|
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(
|
|
919
|
-
const home = process.env.HOME ? normalizeRoot(
|
|
920
|
-
if (home && (resolved === home || resolved.startsWith(`${home}${
|
|
921
|
-
const relative =
|
|
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 ?
|
|
987
|
+
return first ? path4.join(home, first) : home;
|
|
924
988
|
}
|
|
925
|
-
const parts = resolved.split(
|
|
926
|
-
return `${
|
|
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 =
|
|
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 =
|
|
934
|
-
const headPath =
|
|
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
|
|
1009
|
+
return path4.join(repoRoot, ".git");
|
|
946
1010
|
}
|
|
947
|
-
return
|
|
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
|
|
1024
|
+
import fs5 from "node:fs";
|
|
961
1025
|
import os2 from "node:os";
|
|
962
|
-
import
|
|
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 ??
|
|
1404
|
-
const statePath =
|
|
1405
|
-
const sessionsPath =
|
|
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 =
|
|
1409
|
-
const sessionsExists =
|
|
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 ??
|
|
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 =
|
|
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] ?
|
|
1540
|
-
|
|
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 =
|
|
1615
|
+
let current = path5.resolve(start);
|
|
1551
1616
|
while (true) {
|
|
1552
|
-
const packagePath =
|
|
1553
|
-
if (
|
|
1617
|
+
const packagePath = path5.join(current, "package.json");
|
|
1618
|
+
if (fs5.existsSync(packagePath)) {
|
|
1554
1619
|
try {
|
|
1555
|
-
const packageJson = JSON.parse(
|
|
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 =
|
|
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 ??
|
|
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 ??
|
|
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 =
|
|
1812
|
+
const importsPath = path5.join(codexHome, "external_agent_session_imports.json");
|
|
1743
1813
|
const models = /* @__PURE__ */ new Map();
|
|
1744
|
-
if (!
|
|
1814
|
+
if (!fs5.existsSync(importsPath)) return models;
|
|
1745
1815
|
let parsed;
|
|
1746
1816
|
try {
|
|
1747
|
-
parsed = JSON.parse(
|
|
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 || !
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1820
|
-
const fullPath =
|
|
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 =
|
|
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 ?
|
|
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
|
|
2271
|
+
import fs6 from "node:fs";
|
|
2161
2272
|
import os3 from "node:os";
|
|
2162
|
-
import
|
|
2273
|
+
import path6 from "node:path";
|
|
2163
2274
|
function scanClaude(options) {
|
|
2164
2275
|
const claudeHome = options.claudeHome ?? defaultClaudeHome();
|
|
2165
|
-
const historyPath =
|
|
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 =
|
|
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 =
|
|
2177
|
-
if (!discovery.projectsPaths.some((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) =>
|
|
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 ? [
|
|
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) =>
|
|
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 (!
|
|
2345
|
+
if (!fs6.existsSync(projectsPath)) return;
|
|
2235
2346
|
try {
|
|
2236
|
-
for (const entry of
|
|
2347
|
+
for (const entry of fs6.readdirSync(projectsPath, { withFileTypes: true })) {
|
|
2237
2348
|
if (!entry.isDirectory()) continue;
|
|
2238
|
-
const projectDir =
|
|
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 (!
|
|
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(
|
|
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(
|
|
2257
|
-
const projectDir = hasProjectSegment ? parts.slice(0, projectsIndex + 2).join(
|
|
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
|
|
2378
|
+
for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
|
|
2268
2379
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
2269
|
-
const fullPath =
|
|
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 ?? (
|
|
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(
|
|
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 =
|
|
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(
|
|
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 `${
|
|
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 =
|
|
2724
|
+
const name = path6.basename(projectDir);
|
|
2614
2725
|
if (name.startsWith("-")) {
|
|
2615
|
-
return `${
|
|
2726
|
+
return `${path6.sep}${name.slice(1).split("-").filter(Boolean).join(path6.sep)}`;
|
|
2616
2727
|
}
|
|
2617
|
-
const relative =
|
|
2618
|
-
return relative && !relative.startsWith("..") ?
|
|
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] ??
|
|
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 ||
|
|
2757
|
+
const xdg = process.env.XDG_CONFIG_HOME || path6.join(home, ".config");
|
|
2647
2758
|
return [
|
|
2648
|
-
|
|
2649
|
-
|
|
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 =
|
|
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 (!
|
|
2776
|
+
if (!fs6.existsSync(historyPath)) return 0;
|
|
2666
2777
|
let entryCount = 0;
|
|
2667
2778
|
try {
|
|
2668
|
-
for (const line of
|
|
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
|
|
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
|
|
2846
|
+
import fs7 from "node:fs";
|
|
2736
2847
|
import os4 from "node:os";
|
|
2737
|
-
import
|
|
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 ??
|
|
2856
|
+
const cursorHome = options.cursorHome ?? path7.join(os4.homedir(), ".cursor");
|
|
2746
2857
|
const discovery = discoverCursorFiles(cursorHome, Boolean(options.cursorHome));
|
|
2747
|
-
const
|
|
2748
|
-
const
|
|
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 (!
|
|
2867
|
+
if (!fs7.existsSync(cursorHome)) {
|
|
2755
2868
|
discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
|
|
2756
2869
|
}
|
|
2757
|
-
if (!
|
|
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:
|
|
2779
|
-
stateExists:
|
|
2780
|
-
sessionsExists:
|
|
2781
|
-
sessionFileCount:
|
|
2782
|
-
databaseFileCount:
|
|
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 (!
|
|
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:
|
|
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 (!
|
|
2951
|
+
if (!fs7.existsSync(workspaceStoragePath)) return [];
|
|
2839
2952
|
const roots = [];
|
|
2840
2953
|
try {
|
|
2841
|
-
for (const workspace of
|
|
2954
|
+
for (const workspace of fs7.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
|
|
2842
2955
|
if (!workspace.isDirectory()) continue;
|
|
2843
|
-
roots.push(...cursorOwnedChildDirs(
|
|
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 (!
|
|
2964
|
+
if (!fs7.existsSync(root)) return [];
|
|
2852
2965
|
try {
|
|
2853
|
-
return
|
|
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 ??
|
|
2862
|
-
const globalStoragePath2 =
|
|
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:
|
|
2866
|
-
statePath:
|
|
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 =
|
|
2983
|
+
const globalStoragePath2 = path7.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
|
|
2871
2984
|
return {
|
|
2872
2985
|
globalStoragePath: globalStoragePath2,
|
|
2873
|
-
workspaceStoragePath:
|
|
2874
|
-
statePath:
|
|
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 ||
|
|
2878
|
-
const globalStoragePath =
|
|
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:
|
|
2882
|
-
statePath:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2900
|
-
const stat =
|
|
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(`${
|
|
2908
|
-
const bTranscript = b.includes(`${
|
|
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(
|
|
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 =
|
|
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 ?? `${
|
|
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(
|
|
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 `${
|
|
3412
|
+
return `${path7.sep}${projectId.slice(1).split("-").filter(Boolean).join(path7.sep)}`;
|
|
3281
3413
|
}
|
|
3282
|
-
if (projectId) return
|
|
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(`${
|
|
3289
|
-
if (lower.includes(`${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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 =
|
|
3559
|
-
|
|
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
|
|
3616
|
-
import
|
|
3617
|
-
import { fileURLToPath as
|
|
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 =
|
|
4457
|
-
const packagedWebDist =
|
|
4458
|
-
const workspaceWebDist =
|
|
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) &&
|
|
4781
|
+
return Boolean(inputPath) && path9.isAbsolute(inputPath) && fs9.existsSync(inputPath);
|
|
4480
4782
|
}
|
|
4481
4783
|
function readPackageVersion() {
|
|
4482
|
-
const serverDist =
|
|
4483
|
-
const packagePath =
|
|
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(
|
|
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
|
|
4495
|
-
|
|
4496
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
}
|