exe-watcher 0.2.49 → 0.2.52

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.
Files changed (2) hide show
  1. package/dist/cli.js +1410 -888
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --max-old-space-size=256 --expose-gc
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
4
  var __esm = (fn, res) => function __init() {
@@ -10,13 +10,13 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/cache-dir.ts
13
- import { join as join2 } from "path";
14
- import { homedir as homedir2 } from "os";
13
+ import { join } from "path";
14
+ import { homedir } from "os";
15
15
  function getCacheDir() {
16
16
  const explicit = process.env["EXE_WATCHER_CACHE_DIR"];
17
17
  if (explicit) return explicit;
18
18
  const xdgCache = process.env["XDG_CACHE_HOME"];
19
- return join2(xdgCache || join2(homedir2(), ".cache"), "exe-watcher");
19
+ return join(xdgCache || join(homedir(), ".cache"), "exe-watcher");
20
20
  }
21
21
  var init_cache_dir = __esm({
22
22
  "src/cache-dir.ts"() {
@@ -24,11 +24,56 @@ var init_cache_dir = __esm({
24
24
  }
25
25
  });
26
26
 
27
+ // src/resource-budget.ts
28
+ import { getHeapStatistics } from "v8";
29
+ import { AsyncLocalStorage } from "async_hooks";
30
+ function withScanBudget(fn, maxRecords = 2e5, signal, maxBytes = 1024 * 1024 * 1024) {
31
+ if (scans.getStore()) return fn();
32
+ return scans.run({ bytes: 0, records: 0, started: Date.now(), maxRecords, signal, maxBytes }, fn);
33
+ }
34
+ function chargeRead(bytes) {
35
+ const budget = scans.getStore();
36
+ if (!budget) return;
37
+ if (budget.signal?.aborted) throw new ResourceBudgetError("scan cancelled");
38
+ budget.bytes += bytes;
39
+ if (budget.bytes > budget.maxBytes) throw new ResourceBudgetError(`${budget.maxBytes / 1024 ** 3} GiB read budget per scan`);
40
+ if (Date.now() - budget.started > 3e4) throw new ResourceBudgetError("30 second scan budget");
41
+ const heapBudget = Math.min(384 * 1024 * 1024, getHeapStatistics().heap_size_limit * 0.65);
42
+ if (process.memoryUsage().heapUsed > heapBudget) {
43
+ global.gc?.();
44
+ if (process.memoryUsage().heapUsed > heapBudget) throw new ResourceBudgetError("parser heap budget");
45
+ }
46
+ }
47
+ function chargeRecord() {
48
+ const budget = scans.getStore();
49
+ if (!budget) return;
50
+ if (++budget.records > budget.maxRecords) throw new ResourceBudgetError(`${budget.maxRecords.toLocaleString()} scanned usage records`);
51
+ if (budget.records % 256 === 0) chargeRead(0);
52
+ }
53
+ function checkFileSize(size, cap) {
54
+ if (size <= cap) return true;
55
+ if (scans.getStore()) throw new ResourceBudgetError(`file exceeds ${cap} bytes`);
56
+ return false;
57
+ }
58
+ var ResourceBudgetError, scans;
59
+ var init_resource_budget = __esm({
60
+ "src/resource-budget.ts"() {
61
+ "use strict";
62
+ ResourceBudgetError = class extends Error {
63
+ constructor(detail) {
64
+ super(`Resource limit reached (${detail}). Showing the last successful refresh; retry after background ingestion progresses.`);
65
+ this.name = "ResourceBudgetError";
66
+ }
67
+ };
68
+ scans = new AsyncLocalStorage();
69
+ }
70
+ });
71
+
27
72
  // src/models.ts
28
- import { readFile as readFile3, mkdir as mkdir5, rename as rename4, unlink as unlink2 } from "fs/promises";
29
- import { open as open3 } from "fs/promises";
73
+ import { readFile as readFile4, mkdir as mkdir6, rename as rename4, unlink as unlink3 } from "fs/promises";
74
+ import { open as open4 } from "fs/promises";
30
75
  import { join as join6 } from "path";
31
- import { randomBytes as randomBytes3 } from "crypto";
76
+ import { createHash, randomBytes as randomBytes3 } from "crypto";
32
77
  function getPricingWarnings() {
33
78
  return _pricingWarnings;
34
79
  }
@@ -72,10 +117,10 @@ async function fetchAndCachePricing() {
72
117
  for (const [name, costs] of fallbackStripped) {
73
118
  if (!pricing.has(name)) pricing.set(name, costs);
74
119
  }
75
- await mkdir5(getCacheDir(), { recursive: true });
120
+ await mkdir6(getCacheDir(), { recursive: true });
76
121
  const finalPath = getCachePath();
77
122
  const tmpPath = `${finalPath}.${randomBytes3(8).toString("hex")}.tmp`;
78
- const handle = await open3(tmpPath, "w", 384);
123
+ const handle = await open4(tmpPath, "w", 384);
79
124
  try {
80
125
  await handle.writeFile(JSON.stringify({
81
126
  timestamp: Date.now(),
@@ -89,7 +134,7 @@ async function fetchAndCachePricing() {
89
134
  await rename4(tmpPath, finalPath);
90
135
  } catch (err) {
91
136
  try {
92
- await unlink2(tmpPath);
137
+ await unlink3(tmpPath);
93
138
  } catch {
94
139
  }
95
140
  throw err;
@@ -97,7 +142,7 @@ async function fetchAndCachePricing() {
97
142
  try {
98
143
  const lkgPath = getLastKnownGoodPath();
99
144
  const lkgTmp = `${lkgPath}.${randomBytes3(8).toString("hex")}.tmp`;
100
- const lkgHandle = await open3(lkgTmp, "w", 384);
145
+ const lkgHandle = await open4(lkgTmp, "w", 384);
101
146
  try {
102
147
  await lkgHandle.writeFile(JSON.stringify({
103
148
  timestamp: Date.now(),
@@ -109,7 +154,7 @@ async function fetchAndCachePricing() {
109
154
  }
110
155
  await rename4(lkgTmp, lkgPath).catch(() => {
111
156
  try {
112
- unlink2(lkgTmp);
157
+ unlink3(lkgTmp);
113
158
  } catch {
114
159
  }
115
160
  });
@@ -119,7 +164,7 @@ async function fetchAndCachePricing() {
119
164
  }
120
165
  async function loadCachedPricing() {
121
166
  try {
122
- const raw = await readFile3(getCachePath(), "utf-8");
167
+ const raw = await readFile4(getCachePath(), "utf-8");
123
168
  const cached = JSON.parse(raw);
124
169
  if (Date.now() - cached.timestamp > CACHE_TTL_MS2) return null;
125
170
  return { map: new Map(Object.entries(cached.data)), timestamp: cached.timestamp };
@@ -129,7 +174,7 @@ async function loadCachedPricing() {
129
174
  }
130
175
  async function loadLastKnownGoodPricing() {
131
176
  try {
132
- const raw = await readFile3(getLastKnownGoodPath(), "utf-8");
177
+ const raw = await readFile4(getLastKnownGoodPath(), "utf-8");
133
178
  const cached = JSON.parse(raw);
134
179
  return { map: new Map(Object.entries(cached.data)), timestamp: cached.timestamp };
135
180
  } catch {
@@ -267,7 +312,15 @@ function applyContextTier(costs, inputTokens) {
267
312
  cacheReadCostPerToken: tier.cacheReadCostPerToken ?? costs.cacheReadCostPerToken
268
313
  };
269
314
  }
270
- var LITELLM_URL, CACHE_TTL_MS2, WEB_SEARCH_COST, LONG_CONTEXT_1M_THRESHOLD_TOKENS, GEMINI_LONG_CONTEXT_THRESHOLD_TOKENS, DIRECT_PROVIDER_PREFIXES, FALLBACK_PRICING, pricingCache, _pricingWarnings, _pricingLastUpdated, STALENESS_WARN_MS, BUILTIN_ALIASES, userAliases;
315
+ function getPricingFingerprint() {
316
+ if (!fingerprintValue || fingerprintPricing !== pricingCache || fingerprintAliases !== userAliases) {
317
+ fingerprintPricing = pricingCache;
318
+ fingerprintAliases = userAliases;
319
+ fingerprintValue = createHash("sha256").update(JSON.stringify([Array.from(pricingCache ?? []), FALLBACK_PRICING, userAliases])).digest("hex").slice(0, 16);
320
+ }
321
+ return fingerprintValue;
322
+ }
323
+ var LITELLM_URL, CACHE_TTL_MS2, WEB_SEARCH_COST, LONG_CONTEXT_1M_THRESHOLD_TOKENS, GEMINI_LONG_CONTEXT_THRESHOLD_TOKENS, DIRECT_PROVIDER_PREFIXES, FALLBACK_PRICING, pricingCache, _pricingWarnings, _pricingLastUpdated, STALENESS_WARN_MS, BUILTIN_ALIASES, userAliases, fingerprintPricing, fingerprintAliases, fingerprintValue;
271
324
  var init_models = __esm({
272
325
  "src/models.ts"() {
273
326
  "use strict";
@@ -376,6 +429,164 @@ var init_models = __esm({
376
429
  "anthropic--claude-4.5-haiku": "claude-haiku-4-5"
377
430
  };
378
431
  userAliases = {};
432
+ fingerprintValue = "";
433
+ }
434
+ });
435
+
436
+ // src/fs-utils.ts
437
+ import { open as open5 } from "fs/promises";
438
+ import { openSync, fstatSync, readSync, closeSync, constants } from "fs";
439
+ function verbose() {
440
+ return process.env.EXE_WATCHER_VERBOSE === "1";
441
+ }
442
+ function warn(msg) {
443
+ if (verbose()) process.stderr.write(`exe-watcher: ${msg}
444
+ `);
445
+ }
446
+ async function readSessionFirstLine(filePath) {
447
+ const handle = await open5(filePath, "r").catch(() => null);
448
+ if (!handle) return null;
449
+ try {
450
+ const chunks = [];
451
+ const buffer = Buffer.allocUnsafe(64 * 1024);
452
+ let offset = 0;
453
+ const maxHeaderBytes = 8 * 1024 * 1024;
454
+ while (offset < maxHeaderBytes) {
455
+ const length = Math.min(offset === 0 ? 4096 : buffer.length, maxHeaderBytes - offset);
456
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
457
+ chargeRead(bytesRead);
458
+ if (bytesRead === 0) return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
459
+ const chunk = buffer.subarray(0, bytesRead);
460
+ const newline = chunk.indexOf(10);
461
+ if (newline >= 0) {
462
+ chunks.push(chunk.subarray(0, newline));
463
+ return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
464
+ }
465
+ chunks.push(Buffer.from(chunk));
466
+ offset += bytesRead;
467
+ }
468
+ if (!checkFileSize(offset + 1, maxHeaderBytes)) return null;
469
+ return null;
470
+ } catch (err) {
471
+ if (err instanceof ResourceBudgetError) throw err;
472
+ warn(`header read failed for ${filePath}: ${err.code ?? "unknown"}`);
473
+ return null;
474
+ } finally {
475
+ await handle.close();
476
+ }
477
+ }
478
+ async function readSessionFile(filePath) {
479
+ const handle = await open5(filePath, "r").catch(() => null);
480
+ if (!handle) return null;
481
+ try {
482
+ const size = (await handle.stat()).size;
483
+ if (!checkFileSize(size, MAX_SESSION_FILE_BYTES)) {
484
+ warn(`skipped oversize file ${filePath}`);
485
+ return null;
486
+ }
487
+ chargeRead(size);
488
+ const buffer = Buffer.allocUnsafe(size);
489
+ let offset = 0;
490
+ while (offset < size) {
491
+ const { bytesRead } = await handle.read(buffer, offset, Math.min(64 * 1024, size - offset), offset);
492
+ if (!bytesRead) break;
493
+ offset += bytesRead;
494
+ }
495
+ return buffer.toString("utf-8", 0, offset);
496
+ } catch (err) {
497
+ if (err instanceof ResourceBudgetError) throw err;
498
+ warn(`read failed for ${filePath}`);
499
+ return null;
500
+ } finally {
501
+ await handle.close();
502
+ }
503
+ }
504
+ function readSessionFileSync(filePath) {
505
+ const O_NOFOLLOW = constants["O_NOFOLLOW"] ?? 0;
506
+ let fd;
507
+ try {
508
+ fd = openSync(filePath, constants.O_RDONLY | O_NOFOLLOW);
509
+ } catch (err) {
510
+ const code = err.code ?? "unknown";
511
+ if (code === "ELOOP") {
512
+ warn(`skipped symlink ${filePath}`);
513
+ return null;
514
+ }
515
+ warn(`open failed for ${filePath}: ${code}`);
516
+ return null;
517
+ }
518
+ try {
519
+ const size = fstatSync(fd).size;
520
+ if (!checkFileSize(size, MAX_SESSION_FILE_BYTES)) {
521
+ warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`);
522
+ return null;
523
+ }
524
+ chargeRead(size);
525
+ const buf = Buffer.allocUnsafe(size);
526
+ let offset = 0;
527
+ while (offset < size) {
528
+ const bytesRead = readSync(fd, buf, offset, size - offset, offset);
529
+ if (bytesRead === 0) break;
530
+ offset += bytesRead;
531
+ }
532
+ return buf.toString("utf-8", 0, offset);
533
+ } catch (err) {
534
+ if (err instanceof ResourceBudgetError) throw err;
535
+ warn(`read failed for ${filePath}: ${err.code ?? "unknown"}`);
536
+ return null;
537
+ } finally {
538
+ closeSync(fd);
539
+ }
540
+ }
541
+ async function* readSessionLines(filePath, startByte = 0) {
542
+ const handle = await open5(filePath, "r").catch(() => null);
543
+ if (!handle) return;
544
+ try {
545
+ const size = (await handle.stat()).size;
546
+ if (size <= startByte) return;
547
+ chargeRead(size - startByte);
548
+ const stream = handle.createReadStream({ encoding: "utf-8", start: startByte, end: size - 1, autoClose: false });
549
+ let pieces = [];
550
+ let lineBytes = 0;
551
+ const maxLineBytes = 32 * 1024 * 1024;
552
+ const append = (piece) => {
553
+ lineBytes += Buffer.byteLength(piece);
554
+ if (lineBytes > maxLineBytes) throw new ResourceBudgetError("32 MiB JSONL record budget");
555
+ pieces.push(piece);
556
+ };
557
+ try {
558
+ for await (const chunk of stream) {
559
+ let start = 0;
560
+ let newline;
561
+ while ((newline = chunk.indexOf("\n", start)) >= 0) {
562
+ append(chunk.slice(start, newline));
563
+ chargeRead(0);
564
+ const line = pieces.join("").replace(/\r$/, "");
565
+ pieces = [];
566
+ lineBytes = 0;
567
+ yield line;
568
+ start = newline + 1;
569
+ }
570
+ if (start < chunk.length) append(chunk.slice(start));
571
+ }
572
+ if (pieces.length) yield pieces.join("").replace(/\r$/, "");
573
+ } finally {
574
+ stream.destroy();
575
+ }
576
+ } catch (err) {
577
+ if (err instanceof ResourceBudgetError) throw err;
578
+ warn(`stream read failed for ${filePath}`);
579
+ } finally {
580
+ await handle.close();
581
+ }
582
+ }
583
+ var MAX_SESSION_FILE_BYTES, STREAM_THRESHOLD_BYTES;
584
+ var init_fs_utils = __esm({
585
+ "src/fs-utils.ts"() {
586
+ "use strict";
587
+ init_resource_budget();
588
+ MAX_SESSION_FILE_BYTES = 128 * 1024 * 1024;
589
+ STREAM_THRESHOLD_BYTES = 8 * 1024 * 1024;
379
590
  }
380
591
  });
381
592
 
@@ -421,17 +632,18 @@ var init_bash_utils = __esm({
421
632
  });
422
633
 
423
634
  // src/cursor-cache.ts
424
- import { readFile as readFile5, mkdir as mkdir6, stat as stat8, rename as rename5 } from "fs/promises";
425
- import { open as open4 } from "fs/promises";
426
- import { join as join11 } from "path";
635
+ import { readFile as readFile7, mkdir as mkdir8, stat as stat9, rename as rename6 } from "fs/promises";
636
+ import { open as open6 } from "fs/promises";
637
+ import { join as join13 } from "path";
427
638
  import { randomBytes as randomBytes4 } from "crypto";
428
639
  function getCachePath2() {
429
- return join11(getCacheDir(), CACHE_FILE);
640
+ return join13(getCacheDir(), CACHE_FILE);
430
641
  }
431
642
  async function getDbFingerprint(dbPath) {
432
643
  try {
433
- const s = await stat8(dbPath);
434
- return { mtimeMs: s.mtimeMs, size: s.size };
644
+ const s = await stat9(dbPath);
645
+ const wal = await stat9(`${dbPath}-wal`).catch(() => null);
646
+ return [dbPath, s.dev, s.ino, s.ctimeMs, s.mtimeMs, s.size, wal?.ino, wal?.ctimeMs, wal?.mtimeMs, wal?.size, getPricingFingerprint()].join(":");
435
647
  } catch {
436
648
  return null;
437
649
  }
@@ -440,37 +652,42 @@ async function readCachedResults(dbPath) {
440
652
  try {
441
653
  const fp = await getDbFingerprint(dbPath);
442
654
  if (!fp) return null;
443
- const raw = await readFile5(getCachePath2(), "utf-8");
655
+ const size = (await stat9(getCachePath2())).size;
656
+ if (size > 16 * 1024 * 1024) return null;
657
+ chargeRead(size);
658
+ const raw = await readFile7(getCachePath2(), "utf-8");
444
659
  const cache = JSON.parse(raw);
445
- if (cache.dbMtimeMs === fp.mtimeMs && cache.dbSizeBytes === fp.size) {
660
+ if (cache.fingerprint === fp) {
446
661
  return cache.calls;
447
662
  }
448
663
  return null;
449
- } catch {
664
+ } catch (err) {
665
+ if (err instanceof ResourceBudgetError) throw err;
450
666
  return null;
451
667
  }
452
668
  }
453
- async function writeCachedResults(dbPath, calls) {
669
+ async function writeCachedResults(dbPath, calls, expected) {
454
670
  try {
455
671
  const fp = await getDbFingerprint(dbPath);
456
- if (!fp) return;
672
+ if (!fp || expected !== void 0 && expected !== fp) return;
457
673
  const dir = getCacheDir();
458
- await mkdir6(dir, { recursive: true });
674
+ await mkdir8(dir, { recursive: true });
459
675
  const cache = {
460
- dbMtimeMs: fp.mtimeMs,
461
- dbSizeBytes: fp.size,
676
+ fingerprint: fp,
462
677
  calls
463
678
  };
679
+ const payload = JSON.stringify(cache);
680
+ if (Buffer.byteLength(payload) > 16 * 1024 * 1024) return;
464
681
  const finalPath = getCachePath2();
465
682
  const tmpPath = `${finalPath}.${randomBytes4(8).toString("hex")}.tmp`;
466
- const handle = await open4(tmpPath, "w", 384);
683
+ const handle = await open6(tmpPath, "w", 384);
467
684
  try {
468
- await handle.writeFile(JSON.stringify(cache), { encoding: "utf-8" });
685
+ await handle.writeFile(payload, { encoding: "utf-8" });
469
686
  await handle.sync();
470
687
  } finally {
471
688
  await handle.close();
472
689
  }
473
- await rename5(tmpPath, finalPath);
690
+ await rename6(tmpPath, finalPath);
474
691
  } catch {
475
692
  }
476
693
  }
@@ -478,6 +695,8 @@ var CACHE_FILE;
478
695
  var init_cursor_cache = __esm({
479
696
  "src/cursor-cache.ts"() {
480
697
  "use strict";
698
+ init_models();
699
+ init_resource_budget();
481
700
  init_cache_dir();
482
701
  CACHE_FILE = "cursor-results.json";
483
702
  }
@@ -532,7 +751,16 @@ function openDatabase(path) {
532
751
  const db = new DatabaseSync(path, { readOnly: true });
533
752
  return {
534
753
  query(sql, params = []) {
535
- return db.prepare(sql).all(...params);
754
+ const rows = [];
755
+ let bytes = 0;
756
+ for (const row of db.prepare(sql).iterate(...params)) {
757
+ const size = Buffer.byteLength(JSON.stringify(row));
758
+ bytes += size;
759
+ if (rows.length >= 1e5 || bytes > 32 * 1024 * 1024) throw new ResourceBudgetError("SQLite result budget");
760
+ chargeRead(size);
761
+ rows.push(row);
762
+ }
763
+ return rows;
536
764
  },
537
765
  close() {
538
766
  db.close();
@@ -543,6 +771,7 @@ var requireForSqlite, DatabaseSync, loadAttempted, loadError;
543
771
  var init_sqlite = __esm({
544
772
  "src/sqlite.ts"() {
545
773
  "use strict";
774
+ init_resource_budget();
546
775
  requireForSqlite = createRequire(import.meta.url);
547
776
  DatabaseSync = null;
548
777
  loadAttempted = false;
@@ -557,16 +786,16 @@ __export(cursor_exports, {
557
786
  cursor: () => cursor
558
787
  });
559
788
  import { existsSync } from "fs";
560
- import { join as join12 } from "path";
789
+ import { join as join14 } from "path";
561
790
  import { homedir as homedir8 } from "os";
562
791
  function getCursorDbPath() {
563
792
  if (process.platform === "darwin") {
564
- return join12(homedir8(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
793
+ return join14(homedir8(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
565
794
  }
566
795
  if (process.platform === "win32") {
567
- return join12(homedir8(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
796
+ return join14(homedir8(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
568
797
  }
569
- return join12(homedir8(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
798
+ return join14(homedir8(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
570
799
  }
571
800
  function extractLanguages(codeBlocksJson) {
572
801
  if (!codeBlocksJson) return [];
@@ -580,7 +809,8 @@ function extractLanguages(codeBlocksJson) {
580
809
  }
581
810
  }
582
811
  return [...langs];
583
- } catch {
812
+ } catch (error) {
813
+ if (error instanceof ResourceBudgetError) throw error;
584
814
  return [];
585
815
  }
586
816
  }
@@ -598,7 +828,8 @@ function validateSchema(db) {
598
828
  "SELECT COUNT(*) as cnt FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' LIMIT 1"
599
829
  );
600
830
  return rows.length > 0;
601
- } catch {
831
+ } catch (error) {
832
+ if (error instanceof ResourceBudgetError) throw error;
602
833
  return false;
603
834
  }
604
835
  }
@@ -612,7 +843,8 @@ function buildUserMessageMap(db, timeFloor) {
612
843
  existing.push(row.text);
613
844
  map.set(row.conversation_id, existing);
614
845
  }
615
- } catch {
846
+ } catch (error) {
847
+ if (error instanceof ResourceBudgetError) throw error;
616
848
  }
617
849
  return map;
618
850
  }
@@ -625,7 +857,8 @@ function parseBubbles(db, seenKeys) {
625
857
  let rows;
626
858
  try {
627
859
  rows = db.query(BUBBLE_QUERY_SINCE, [timeFloor]);
628
- } catch {
860
+ } catch (error) {
861
+ if (error instanceof ResourceBudgetError) throw error;
629
862
  return { calls: results };
630
863
  }
631
864
  for (const row of rows) {
@@ -668,7 +901,8 @@ function parseBubbles(db, seenKeys) {
668
901
  userMessage: userText,
669
902
  sessionId: conversationId
670
903
  });
671
- } catch {
904
+ } catch (error) {
905
+ if (error instanceof ResourceBudgetError) throw error;
672
906
  skipped++;
673
907
  }
674
908
  }
@@ -685,6 +919,7 @@ function createParser4(source, seenKeys) {
685
919
  process.stderr.write(getSqliteLoadError() + "\n");
686
920
  return;
687
921
  }
922
+ const before = await getDbFingerprint(source.path);
688
923
  const cached = await readCachedResults(source.path);
689
924
  if (cached) {
690
925
  for (const call of cached) {
@@ -698,6 +933,7 @@ function createParser4(source, seenKeys) {
698
933
  try {
699
934
  db = openDatabase(source.path);
700
935
  } catch (err) {
936
+ if (err instanceof ResourceBudgetError) throw err;
701
937
  process.stderr.write(`exe-watcher: cannot open Cursor database: ${err instanceof Error ? err.message : err}
702
938
  `);
703
939
  return;
@@ -708,7 +944,7 @@ function createParser4(source, seenKeys) {
708
944
  return;
709
945
  }
710
946
  const { calls } = parseBubbles(db, seenKeys);
711
- await writeCachedResults(source.path, calls);
947
+ await writeCachedResults(source.path, calls, before);
712
948
  for (const call of calls) {
713
949
  yield call;
714
950
  }
@@ -743,6 +979,7 @@ var CURSOR_DEFAULT_MODEL, modelDisplayNames4, BUBBLE_QUERY_BASE, USER_MESSAGES_Q
743
979
  var init_cursor = __esm({
744
980
  "src/providers/cursor.ts"() {
745
981
  "use strict";
982
+ init_resource_budget();
746
983
  init_models();
747
984
  init_cursor_cache();
748
985
  init_sqlite();
@@ -799,21 +1036,22 @@ __export(opencode_exports, {
799
1036
  createOpenCodeProvider: () => createOpenCodeProvider,
800
1037
  opencode: () => opencode
801
1038
  });
802
- import { readdir as readdir6 } from "fs/promises";
803
- import { join as join13 } from "path";
1039
+ import { readdir as readdir8 } from "fs/promises";
1040
+ import { join as join15 } from "path";
804
1041
  import { homedir as homedir9 } from "os";
805
1042
  function sanitize(dir) {
806
1043
  return dir.replace(/^\//, "").replace(/\//g, "-");
807
1044
  }
808
1045
  function getDataDir(dataDir) {
809
- const base = dataDir ?? process.env["XDG_DATA_HOME"] ?? join13(homedir9(), ".local", "share");
810
- return join13(base, "opencode");
1046
+ const base = dataDir ?? process.env["XDG_DATA_HOME"] ?? join15(homedir9(), ".local", "share");
1047
+ return join15(base, "opencode");
811
1048
  }
812
1049
  async function findDbFiles(dir) {
813
1050
  try {
814
- const entries = await readdir6(dir);
815
- return entries.filter((f) => f.startsWith("opencode") && f.endsWith(".db")).map((f) => join13(dir, f));
816
- } catch {
1051
+ const entries = await readdir8(dir);
1052
+ return entries.filter((f) => f.startsWith("opencode") && f.endsWith(".db")).map((f) => join15(dir, f));
1053
+ } catch (error) {
1054
+ if (error instanceof ResourceBudgetError) throw error;
817
1055
  return [];
818
1056
  }
819
1057
  }
@@ -824,13 +1062,14 @@ function parseTimestamp(raw) {
824
1062
  function validateSchema2(db) {
825
1063
  try {
826
1064
  db.query(
827
- "SELECT COUNT(*) as cnt FROM session LIMIT 1"
1065
+ "SELECT id FROM session LIMIT 1"
828
1066
  );
829
1067
  db.query(
830
- "SELECT COUNT(*) as cnt FROM message LIMIT 1"
1068
+ "SELECT id FROM message LIMIT 1"
831
1069
  );
832
1070
  return true;
833
- } catch {
1071
+ } catch (error) {
1072
+ if (error instanceof ResourceBudgetError) throw error;
834
1073
  return false;
835
1074
  }
836
1075
  }
@@ -872,7 +1111,8 @@ function createParser5(source, seenKeys) {
872
1111
  const list = partsByMsg.get(part.message_id) ?? [];
873
1112
  list.push(parsed);
874
1113
  partsByMsg.set(part.message_id, list);
875
- } catch {
1114
+ } catch (error) {
1115
+ if (error instanceof ResourceBudgetError) throw error;
876
1116
  }
877
1117
  }
878
1118
  let currentUserMessage = "";
@@ -880,7 +1120,8 @@ function createParser5(source, seenKeys) {
880
1120
  let data;
881
1121
  try {
882
1122
  data = JSON.parse(msg.data);
883
- } catch {
1123
+ } catch (error) {
1124
+ if (error instanceof ResourceBudgetError) throw error;
884
1125
  continue;
885
1126
  }
886
1127
  if (data.role === "user") {
@@ -949,10 +1190,15 @@ async function discoverFromDb(dbPath) {
949
1190
  let db;
950
1191
  try {
951
1192
  db = openDatabase(dbPath);
952
- } catch {
1193
+ } catch (error) {
1194
+ if (error instanceof ResourceBudgetError) throw error;
953
1195
  return [];
954
1196
  }
955
1197
  try {
1198
+ if (!validateSchema2(db)) {
1199
+ process.stderr.write("exe-watcher: OpenCode storage format not recognized. Update Watcher to read this database.\n");
1200
+ return [];
1201
+ }
956
1202
  const rows = db.query(
957
1203
  "SELECT id, directory, title, time_created FROM session WHERE time_archived IS NULL AND parent_id IS NULL ORDER BY time_created DESC"
958
1204
  );
@@ -961,7 +1207,8 @@ async function discoverFromDb(dbPath) {
961
1207
  project: row.directory ? sanitize(row.directory) : sanitize(row.title),
962
1208
  provider: "opencode"
963
1209
  }));
964
- } catch {
1210
+ } catch (error) {
1211
+ if (error instanceof ResourceBudgetError) throw error;
965
1212
  return [];
966
1213
  } finally {
967
1214
  db.close();
@@ -998,6 +1245,7 @@ var toolNameMap4, opencode;
998
1245
  var init_opencode = __esm({
999
1246
  "src/providers/opencode.ts"() {
1000
1247
  "use strict";
1248
+ init_resource_budget();
1001
1249
  init_models();
1002
1250
  init_bash_utils();
1003
1251
  init_sqlite();
@@ -1025,20 +1273,20 @@ __export(cursor_agent_exports, {
1025
1273
  createCursorAgentProvider: () => createCursorAgentProvider,
1026
1274
  cursor_agent: () => cursor_agent
1027
1275
  });
1028
- import { createHash as createHash2 } from "crypto";
1276
+ import { createHash as createHash3 } from "crypto";
1029
1277
  import { existsSync as existsSync2 } from "fs";
1030
- import { readdir as readdir7, readFile as readFile6, stat as stat9 } from "fs/promises";
1031
- import { join as join14, basename as basename7 } from "path";
1278
+ import { readdir as readdir9, stat as stat10 } from "fs/promises";
1279
+ import { join as join16, basename as basename7 } from "path";
1032
1280
  import { homedir as homedir10 } from "os";
1033
1281
  function getCursorAgentBaseDir(baseDirOverride) {
1034
1282
  if (baseDirOverride) return baseDirOverride;
1035
- return join14(homedir10(), ".cursor");
1283
+ return join16(homedir10(), ".cursor");
1036
1284
  }
1037
1285
  function getProjectsDir2(baseDir) {
1038
- return join14(baseDir, "projects");
1286
+ return join16(baseDir, "projects");
1039
1287
  }
1040
1288
  function getAttributionDbPath(baseDir) {
1041
- return join14(baseDir, "ai-tracking", "ai-code-tracking.db");
1289
+ return join16(baseDir, "ai-tracking", "ai-code-tracking.db");
1042
1290
  }
1043
1291
  function estimateTokens(charCount) {
1044
1292
  if (charCount <= 0) return 0;
@@ -1089,7 +1337,7 @@ function resolveModel3(raw) {
1089
1337
  function toConversationId(transcriptPath) {
1090
1338
  const filename = basename7(transcriptPath, ".txt");
1091
1339
  if (filename.length === 36 && UUID_LIKE.test(filename)) return filename;
1092
- return createHash2("sha1").update(transcriptPath).digest("hex").slice(0, 16);
1340
+ return createHash3("sha1").update(transcriptPath).digest("hex").slice(0, 16);
1093
1341
  }
1094
1342
  function extractUserQuery(userBlock) {
1095
1343
  const chunks = [];
@@ -1212,12 +1460,14 @@ function createParser6(source, seenKeys, dbPath, summariesByConversationId) {
1212
1460
  };
1213
1461
  summariesByConversationId[conversationId] = summary;
1214
1462
  }
1215
- } catch {
1463
+ } catch (error) {
1464
+ if (error instanceof ResourceBudgetError) throw error;
1216
1465
  summary = void 0;
1217
1466
  }
1218
1467
  }
1219
1468
  }
1220
- const transcript = await readFile6(source.path, "utf-8");
1469
+ const transcript = await readSessionFile(source.path);
1470
+ if (transcript === null) return;
1221
1471
  const parsed = parseTranscript(transcript);
1222
1472
  if (!parsed.recognized) {
1223
1473
  process.stderr.write(`exe-watcher: skipped ${basename7(source.path)}: unrecognized cursor-agent transcript format
@@ -1226,7 +1476,7 @@ function createParser6(source, seenKeys, dbPath, summariesByConversationId) {
1226
1476
  }
1227
1477
  let timestamp = summary?.updatedAt ?? null;
1228
1478
  if (!timestamp) {
1229
- const fileStat = await stat9(source.path);
1479
+ const fileStat = await stat10(source.path);
1230
1480
  timestamp = fileStat.mtime.toISOString();
1231
1481
  }
1232
1482
  const model = resolveModel3(summary?.model ?? null);
@@ -1290,18 +1540,18 @@ function createCursorAgentProvider(baseDirOverride) {
1290
1540
  },
1291
1541
  async discoverSessions() {
1292
1542
  if (!existsSync2(projectsDir)) return [];
1293
- const projectEntries = await readdir7(projectsDir, { withFileTypes: true });
1543
+ const projectEntries = await readdir9(projectsDir, { withFileTypes: true });
1294
1544
  const sources = [];
1295
1545
  for (const entry of projectEntries) {
1296
1546
  if (!entry.isDirectory()) continue;
1297
1547
  const projectId = prettifyProjectId(entry.name);
1298
- const transcriptDir = join14(projectsDir, entry.name, "agent-transcripts");
1548
+ const transcriptDir = join16(projectsDir, entry.name, "agent-transcripts");
1299
1549
  if (!existsSync2(transcriptDir)) continue;
1300
- const transcriptEntries = await readdir7(transcriptDir, { withFileTypes: true });
1550
+ const transcriptEntries = await readdir9(transcriptDir, { withFileTypes: true });
1301
1551
  for (const transcript of transcriptEntries) {
1302
1552
  if (!transcript.isFile()) continue;
1303
1553
  if (!transcript.name.endsWith(".txt")) continue;
1304
- const transcriptPath = join14(transcriptDir, transcript.name);
1554
+ const transcriptPath = join16(transcriptDir, transcript.name);
1305
1555
  sources.push({
1306
1556
  path: transcriptPath,
1307
1557
  project: projectId,
@@ -1320,6 +1570,8 @@ var CURSOR_AGENT_DEFAULT_MODEL, CHARS_PER_TOKEN, MAX_USER_TEXT_LENGTH, DIGITS_ON
1320
1570
  var init_cursor_agent = __esm({
1321
1571
  "src/providers/cursor-agent.ts"() {
1322
1572
  "use strict";
1573
+ init_resource_budget();
1574
+ init_fs_utils();
1323
1575
  init_models();
1324
1576
  init_sqlite();
1325
1577
  CURSOR_AGENT_DEFAULT_MODEL = "claude-sonnet-4-5";
@@ -1358,17 +1610,87 @@ var init_cursor_agent = __esm({
1358
1610
  });
1359
1611
 
1360
1612
  // src/cli.ts
1361
- import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1362
- import { join as join23 } from "path";
1613
+ init_cache_dir();
1614
+
1615
+ // src/file-lock.ts
1616
+ import { mkdir, open, readFile, stat, unlink } from "fs/promises";
1617
+ import { dirname } from "path";
1618
+ async function acquireFileLock(path, timeoutMs = 15e3) {
1619
+ await mkdir(dirname(path), { recursive: true });
1620
+ const token = `${process.pid}:${Date.now()}:${Math.random()}`;
1621
+ const started = Date.now();
1622
+ while (true) {
1623
+ try {
1624
+ const handle = await open(path, "wx", 384);
1625
+ await handle.writeFile(token);
1626
+ await handle.close();
1627
+ return async () => {
1628
+ if (await readFile(path, "utf8").catch(() => "") === token) await unlink(path).catch(() => {
1629
+ });
1630
+ };
1631
+ } catch (error) {
1632
+ if (error.code !== "EEXIST") throw error;
1633
+ }
1634
+ const reaperPath = `${path}.reaper`;
1635
+ const reaper = await open(reaperPath, "wx", 384).catch(() => null);
1636
+ if (reaper) {
1637
+ try {
1638
+ await reaper.writeFile(token);
1639
+ const owner = await readFile(path, "utf8").catch(() => "");
1640
+ const pid = Number(owner.split(":")[0]);
1641
+ let dead = false;
1642
+ if (pid > 0) {
1643
+ try {
1644
+ process.kill(pid, 0);
1645
+ } catch (e) {
1646
+ dead = e.code === "ESRCH";
1647
+ }
1648
+ } else {
1649
+ const s = await stat(path).catch(() => null);
1650
+ dead = !!s && Date.now() - s.mtimeMs > 5e3;
1651
+ }
1652
+ if (dead) await unlink(path).catch(() => {
1653
+ });
1654
+ } finally {
1655
+ await reaper.close();
1656
+ await unlink(reaperPath).catch(() => {
1657
+ });
1658
+ }
1659
+ } else {
1660
+ const owner = await readFile(reaperPath, "utf8").catch(() => "");
1661
+ const pid = Number(owner.split(":")[0]);
1662
+ let dead = false;
1663
+ if (pid > 0) {
1664
+ try {
1665
+ process.kill(pid, 0);
1666
+ } catch (e) {
1667
+ dead = e.code === "ESRCH";
1668
+ }
1669
+ } else {
1670
+ const s = await stat(reaperPath).catch(() => null);
1671
+ dead = !!s && Date.now() - s.mtimeMs > 5e3;
1672
+ }
1673
+ if (dead && await readFile(reaperPath, "utf8").catch(() => "") === owner) await unlink(reaperPath).catch(() => {
1674
+ });
1675
+ }
1676
+ if (Date.now() - started >= timeoutMs) throw new Error("Another Watcher scan is still running. Keeping the last successful refresh.");
1677
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
1678
+ }
1679
+ }
1680
+
1681
+ // src/cli.ts
1682
+ init_resource_budget();
1683
+ import { existsSync as existsSync7, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
1684
+ import { join as join24 } from "path";
1363
1685
  import { homedir as homedir14 } from "os";
1364
1686
  import { Command } from "commander";
1365
1687
 
1366
1688
  // src/menubar-installer.ts
1367
1689
  import { spawn } from "child_process";
1368
1690
  import { createWriteStream } from "fs";
1369
- import { cp, mkdir, mkdtemp, rename, rm, stat } from "fs/promises";
1370
- import { homedir, platform, tmpdir } from "os";
1371
- import { basename, join } from "path";
1691
+ import { cp, mkdir as mkdir2, mkdtemp, rename, rm, stat as stat2 } from "fs/promises";
1692
+ import { homedir as homedir2, platform, tmpdir } from "os";
1693
+ import { basename, join as join2 } from "path";
1372
1694
  import { pipeline } from "stream/promises";
1373
1695
  import { Readable } from "stream";
1374
1696
  var RELEASE_API_LATEST = "https://api.github.com/repos/AskExe/exe-watcher/releases/latest";
@@ -1382,11 +1704,11 @@ var DEFAULT_APP_EXIT_TIMEOUT_MS = 5e3;
1382
1704
  var DEFAULT_APP_LAUNCH_TIMEOUT_MS = 5e3;
1383
1705
  var POLL_INTERVAL_MS = 150;
1384
1706
  function userApplicationsDir() {
1385
- return join(homedir(), "Applications");
1707
+ return join2(homedir2(), "Applications");
1386
1708
  }
1387
1709
  async function exists(path) {
1388
1710
  try {
1389
- await stat(path);
1711
+ await stat2(path);
1390
1712
  return true;
1391
1713
  } catch {
1392
1714
  return false;
@@ -1567,7 +1889,7 @@ async function killRunningApp() {
1567
1889
  async function installMenubarApp(options = {}) {
1568
1890
  await ensureSupportedPlatform();
1569
1891
  const appsDir = userApplicationsDir();
1570
- const targetPath = join(appsDir, APP_BUNDLE_NAME);
1892
+ const targetPath = join2(appsDir, APP_BUNDLE_NAME);
1571
1893
  const alreadyInstalled = await exists(targetPath);
1572
1894
  if (alreadyInstalled && !options.force) {
1573
1895
  if (!await isAppRunning()) {
@@ -1577,21 +1899,21 @@ async function installMenubarApp(options = {}) {
1577
1899
  }
1578
1900
  console.log(options.version ? `Looking up Exe Watcher Menubar release ${options.version}...` : "Looking up the latest Exe Watcher Menubar release...");
1579
1901
  const asset = await fetchLatestReleaseAsset(options.version);
1580
- const stagingDir = await mkdtemp(join(tmpdir(), "exe-watcher-menubar-"));
1902
+ const stagingDir = await mkdtemp(join2(tmpdir(), "exe-watcher-menubar-"));
1581
1903
  try {
1582
- const archivePath = join(stagingDir, asset.name);
1904
+ const archivePath = join2(stagingDir, asset.name);
1583
1905
  console.log(`Downloading ${asset.name}...`);
1584
1906
  await downloadToFile(asset.browser_download_url, archivePath);
1585
1907
  console.log("Unpacking...");
1586
1908
  await runCommand("/usr/bin/unzip", ["-q", archivePath, "-d", stagingDir]);
1587
- const unpackedApp = join(stagingDir, APP_BUNDLE_NAME);
1909
+ const unpackedApp = join2(stagingDir, APP_BUNDLE_NAME);
1588
1910
  if (!await exists(unpackedApp)) {
1589
1911
  throw new Error(`Archive did not contain ${APP_BUNDLE_NAME}.`);
1590
1912
  }
1591
1913
  await runCommand("/usr/bin/xattr", ["-dr", "com.apple.quarantine", unpackedApp]).catch(() => {
1592
1914
  });
1593
- await mkdir(appsDir, { recursive: true });
1594
- const backupPath = alreadyInstalled ? join(appsDir, `${basename(APP_BUNDLE_NAME, ".app")}.backup-${Date.now()}.app`) : void 0;
1915
+ await mkdir2(appsDir, { recursive: true });
1916
+ const backupPath = alreadyInstalled ? join2(appsDir, `${basename(APP_BUNDLE_NAME, ".app")}.backup-${Date.now()}.app`) : void 0;
1595
1917
  let targetReplaced = false;
1596
1918
  try {
1597
1919
  if (alreadyInstalled && backupPath) {
@@ -1624,8 +1946,8 @@ async function installMenubarApp(options = {}) {
1624
1946
  }
1625
1947
 
1626
1948
  // src/export.ts
1627
- import { writeFile, mkdir as mkdir4, readdir, stat as stat2, rm as rm2 } from "fs/promises";
1628
- import { dirname, join as join5, resolve } from "path";
1949
+ import { writeFile, mkdir as mkdir5, readdir, stat as stat3, rm as rm2 } from "fs/promises";
1950
+ import { dirname as dirname2, join as join5, resolve } from "path";
1629
1951
 
1630
1952
  // src/types.ts
1631
1953
  var CATEGORY_LABELS = {
@@ -1639,14 +1961,14 @@ var CATEGORY_LABELS = {
1639
1961
 
1640
1962
  // src/currency.ts
1641
1963
  init_cache_dir();
1642
- import { readFile as readFile2, mkdir as mkdir3, rename as rename3, unlink } from "fs/promises";
1643
- import { open as open2 } from "fs/promises";
1964
+ import { readFile as readFile3, mkdir as mkdir4, rename as rename3, unlink as unlink2 } from "fs/promises";
1965
+ import { open as open3 } from "fs/promises";
1644
1966
  import { join as join4 } from "path";
1645
1967
  import { randomBytes as randomBytes2 } from "crypto";
1646
1968
 
1647
1969
  // src/config.ts
1648
- import { readFile, mkdir as mkdir2, rename as rename2 } from "fs/promises";
1649
- import { open } from "fs/promises";
1970
+ import { readFile as readFile2, mkdir as mkdir3, rename as rename2 } from "fs/promises";
1971
+ import { open as open2 } from "fs/promises";
1650
1972
  import { join as join3 } from "path";
1651
1973
  import { homedir as homedir3 } from "os";
1652
1974
  import { randomBytes } from "crypto";
@@ -1659,17 +1981,17 @@ function getConfigPath() {
1659
1981
  }
1660
1982
  async function readConfig() {
1661
1983
  try {
1662
- const raw = await readFile(getConfigPath(), "utf-8");
1984
+ const raw = await readFile2(getConfigPath(), "utf-8");
1663
1985
  return JSON.parse(raw);
1664
1986
  } catch {
1665
1987
  return {};
1666
1988
  }
1667
1989
  }
1668
1990
  async function saveConfig(config) {
1669
- await mkdir2(getConfigDir(), { recursive: true });
1991
+ await mkdir3(getConfigDir(), { recursive: true });
1670
1992
  const configPath = getConfigPath();
1671
1993
  const tmpPath = `${configPath}.${randomBytes(8).toString("hex")}.tmp`;
1672
- const handle = await open(tmpPath, "w", 384);
1994
+ const handle = await open2(tmpPath, "w", 384);
1673
1995
  try {
1674
1996
  await handle.writeFile(JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8" });
1675
1997
  await handle.sync();
@@ -1742,7 +2064,7 @@ async function fetchRate(code) {
1742
2064
  }
1743
2065
  async function loadCachedRate(code) {
1744
2066
  try {
1745
- const raw = await readFile2(getRateCachePath(), "utf-8");
2067
+ const raw = await readFile3(getRateCachePath(), "utf-8");
1746
2068
  const cached = JSON.parse(raw);
1747
2069
  if (typeof cached.code !== "string" || cached.code !== code) return null;
1748
2070
  if (typeof cached.timestamp !== "number" || !Number.isFinite(cached.timestamp)) return null;
@@ -1754,10 +2076,10 @@ async function loadCachedRate(code) {
1754
2076
  }
1755
2077
  }
1756
2078
  async function cacheRate(code, rate) {
1757
- await mkdir3(getCacheDir(), { recursive: true });
2079
+ await mkdir4(getCacheDir(), { recursive: true });
1758
2080
  const finalPath = getRateCachePath();
1759
2081
  const tmpPath = `${finalPath}.${randomBytes2(8).toString("hex")}.tmp`;
1760
- const handle = await open2(tmpPath, "w", 384);
2082
+ const handle = await open3(tmpPath, "w", 384);
1761
2083
  try {
1762
2084
  await handle.writeFile(JSON.stringify({ timestamp: Date.now(), code, rate }), { encoding: "utf-8" });
1763
2085
  await handle.sync();
@@ -1768,7 +2090,7 @@ async function cacheRate(code, rate) {
1768
2090
  await rename3(tmpPath, finalPath);
1769
2091
  } catch (err) {
1770
2092
  try {
1771
- await unlink(tmpPath);
2093
+ await unlink2(tmpPath);
1772
2094
  } catch {
1773
2095
  }
1774
2096
  throw err;
@@ -2274,7 +2596,7 @@ function buildReadme(periods) {
2274
2596
  }
2275
2597
  var EXPORT_MARKER_FILE = ".exe-watcher-export";
2276
2598
  async function isExeWatcherExportFolder(path) {
2277
- const markerStat = await stat2(join5(path, EXPORT_MARKER_FILE)).catch(() => null);
2599
+ const markerStat = await stat3(join5(path, EXPORT_MARKER_FILE)).catch(() => null);
2278
2600
  return markerStat?.isFile() ?? false;
2279
2601
  }
2280
2602
  async function clearExeWatcherExportFolder(path) {
@@ -2290,7 +2612,7 @@ async function exportCsv(periods, outputPath) {
2290
2612
  if (folder.toLowerCase().endsWith(".csv")) {
2291
2613
  folder = folder.slice(0, -4);
2292
2614
  }
2293
- const existingStat = await stat2(folder).catch(() => null);
2615
+ const existingStat = await stat3(folder).catch(() => null);
2294
2616
  if (existingStat?.isFile()) {
2295
2617
  throw new Error(`Refusing to overwrite existing file at ${folder}. Pass a directory path instead.`);
2296
2618
  }
@@ -2302,7 +2624,7 @@ async function exportCsv(periods, outputPath) {
2302
2624
  }
2303
2625
  await clearExeWatcherExportFolder(folder);
2304
2626
  }
2305
- await mkdir4(folder, { recursive: true });
2627
+ await mkdir5(folder, { recursive: true });
2306
2628
  await writeFile(join5(folder, EXPORT_MARKER_FILE), "", "utf-8");
2307
2629
  const dailyRows = periods.flatMap((p) => buildDailyRows(p.projects, p.label));
2308
2630
  const activityRows = periods.flatMap((p) => buildActivityRows(p.projects, p.label));
@@ -2339,7 +2661,7 @@ async function exportJson(periods, outputPath) {
2339
2661
  shellCommands: buildBashRows(thirtyDayProjects)
2340
2662
  };
2341
2663
  const target = resolve(outputPath.toLowerCase().endsWith(".json") ? outputPath : `${outputPath}.json`);
2342
- await mkdir4(dirname(target), { recursive: true });
2664
+ await mkdir5(dirname2(target), { recursive: true });
2343
2665
  await writeFile(target, JSON.stringify(data, null, 2), "utf-8");
2344
2666
  return target;
2345
2667
  }
@@ -2348,113 +2670,480 @@ async function exportJson(periods, outputPath) {
2348
2670
  init_models();
2349
2671
 
2350
2672
  // src/parser.ts
2351
- import { readdir as readdir8, stat as stat11 } from "fs/promises";
2352
- import { basename as basename8, join as join16 } from "path";
2673
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2353
2674
 
2354
- // src/fs-utils.ts
2355
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
2356
- import { createReadStream, openSync, fstatSync, readSync, closeSync, constants } from "fs";
2357
- import { createInterface } from "readline";
2358
- var MAX_SESSION_FILE_BYTES = 128 * 1024 * 1024;
2359
- var STREAM_THRESHOLD_BYTES = 8 * 1024 * 1024;
2360
- function verbose() {
2361
- return process.env.EXE_WATCHER_VERBOSE === "1";
2362
- }
2363
- function warn(msg) {
2364
- if (verbose()) process.stderr.write(`exe-watcher: ${msg}
2365
- `);
2366
- }
2367
- async function readViaStream(filePath) {
2368
- const chunks = [];
2369
- const stream = createReadStream(filePath, { encoding: "utf-8" });
2370
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
2371
- for await (const line of rl) chunks.push(line);
2372
- return chunks.join("\n");
2373
- }
2374
- async function readSessionFile(filePath) {
2375
- let size;
2376
- try {
2377
- size = (await stat3(filePath)).size;
2378
- } catch (err) {
2379
- warn(`stat failed for ${filePath}: ${err.code ?? "unknown"}`);
2380
- return null;
2381
- }
2382
- if (size > MAX_SESSION_FILE_BYTES) {
2383
- warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`);
2384
- return null;
2385
- }
2386
- try {
2387
- if (size >= STREAM_THRESHOLD_BYTES) return await readViaStream(filePath);
2388
- return await readFile4(filePath, "utf-8");
2389
- } catch (err) {
2390
- warn(`read failed for ${filePath}: ${err.code ?? "unknown"}`);
2391
- return null;
2392
- }
2393
- }
2394
- function readSessionFileSync(filePath) {
2395
- const O_NOFOLLOW = constants["O_NOFOLLOW"] ?? 0;
2396
- let fd;
2397
- try {
2398
- fd = openSync(filePath, constants.O_RDONLY | O_NOFOLLOW);
2399
- } catch (err) {
2400
- const code = err.code ?? "unknown";
2401
- if (code === "ELOOP") {
2402
- warn(`skipped symlink ${filePath}`);
2403
- return null;
2404
- }
2405
- warn(`open failed for ${filePath}: ${code}`);
2406
- return null;
2407
- }
2408
- try {
2409
- const size = fstatSync(fd).size;
2410
- if (size > MAX_SESSION_FILE_BYTES) {
2411
- warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`);
2412
- return null;
2675
+ // src/compare-stats.ts
2676
+ import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
2677
+ import { join as join7 } from "path";
2678
+ var PLANNING_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "EnterPlanMode", "ExitPlanMode"]);
2679
+ function aggregateModelStats(projects) {
2680
+ const byModel = /* @__PURE__ */ new Map();
2681
+ const ensure = (model) => {
2682
+ let s = byModel.get(model);
2683
+ if (!s) {
2684
+ s = { model, calls: 0, cost: 0, outputTokens: 0, inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTurns: 0, editTurns: 0, oneShotTurns: 0, retries: 0, selfCorrections: 0, editCost: 0, firstSeen: "", lastSeen: "" };
2685
+ byModel.set(model, s);
2413
2686
  }
2414
- const buf = Buffer.allocUnsafe(size);
2415
- let offset = 0;
2416
- while (offset < size) {
2417
- const bytesRead = readSync(fd, buf, offset, size - offset, offset);
2418
- if (bytesRead === 0) break;
2419
- offset += bytesRead;
2687
+ return s;
2688
+ };
2689
+ for (const project of projects) {
2690
+ for (const session of project.sessions) {
2691
+ if (session.reportSummary) {
2692
+ for (const row of Object.values(session.reportSummary.models)) mergeCounters(ensure(row.model), row);
2693
+ continue;
2694
+ }
2695
+ for (const turn of session.turns) {
2696
+ if (turn.assistantCalls.length === 0) continue;
2697
+ const primaryModel = turn.assistantCalls[0].model;
2698
+ if (primaryModel === "<synthetic>") continue;
2699
+ const ms = ensure(primaryModel);
2700
+ ms.totalTurns++;
2701
+ if (turn.hasEdits) {
2702
+ ms.editTurns++;
2703
+ if (turn.retries === 0) ms.oneShotTurns++;
2704
+ for (const c of turn.assistantCalls) {
2705
+ if (c.model !== "<synthetic>") ms.editCost += c.costUSD;
2706
+ }
2707
+ }
2708
+ ms.retries += turn.retries;
2709
+ for (const call of turn.assistantCalls) {
2710
+ if (call.model === "<synthetic>") continue;
2711
+ const cs = call.model === primaryModel ? ms : ensure(call.model);
2712
+ cs.calls++;
2713
+ cs.cost += call.costUSD;
2714
+ cs.outputTokens += call.usage.outputTokens;
2715
+ cs.inputTokens += call.usage.inputTokens;
2716
+ cs.cacheReadTokens += call.usage.cacheReadInputTokens;
2717
+ cs.cacheWriteTokens += call.usage.cacheCreationInputTokens;
2718
+ if (!cs.firstSeen || call.timestamp < cs.firstSeen) cs.firstSeen = call.timestamp;
2719
+ if (!cs.lastSeen || call.timestamp > cs.lastSeen) cs.lastSeen = call.timestamp;
2720
+ }
2721
+ }
2420
2722
  }
2421
- return buf.toString("utf-8", 0, offset);
2422
- } catch (err) {
2423
- warn(`read failed for ${filePath}: ${err.code ?? "unknown"}`);
2424
- return null;
2425
- } finally {
2426
- closeSync(fd);
2427
2723
  }
2724
+ return [...byModel.values()].sort((a, b) => b.cost - a.cost);
2428
2725
  }
2429
- async function* readSessionLines(filePath, startByte) {
2430
- let size;
2431
- try {
2432
- size = (await stat3(filePath)).size;
2433
- } catch (err) {
2434
- warn(`stat failed for ${filePath}: ${err.code ?? "unknown"}`);
2435
- return;
2436
- }
2437
- if (size > MAX_SESSION_FILE_BYTES) {
2438
- warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`);
2439
- return;
2440
- }
2441
- const stream = createReadStream(filePath, { encoding: "utf-8", start: startByte ?? 0 });
2442
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
2443
- try {
2444
- for await (const line of rl) yield line;
2445
- } catch (err) {
2446
- warn(`stream read failed for ${filePath}: ${err.code ?? "unknown"}`);
2726
+ var METRICS = [
2727
+ {
2728
+ section: "Performance",
2729
+ label: "One-shot rate",
2730
+ formatFn: "percent",
2731
+ higherIsBetter: true,
2732
+ compute: (s) => s.editTurns > 0 ? s.oneShotTurns / s.editTurns * 100 : null
2733
+ },
2734
+ {
2735
+ section: "Performance",
2736
+ label: "Retry rate",
2737
+ formatFn: "decimal",
2738
+ higherIsBetter: false,
2739
+ compute: (s) => s.editTurns > 0 ? s.retries / s.editTurns : null
2740
+ },
2741
+ {
2742
+ section: "Performance",
2743
+ label: "Self-correction",
2744
+ formatFn: "percent",
2745
+ higherIsBetter: false,
2746
+ compute: (s) => s.totalTurns > 0 ? s.selfCorrections / s.totalTurns * 100 : null
2747
+ },
2748
+ {
2749
+ section: "Efficiency",
2750
+ label: "Cost / call",
2751
+ formatFn: "cost",
2752
+ higherIsBetter: false,
2753
+ compute: (s) => s.calls > 0 ? s.cost / s.calls : null
2754
+ },
2755
+ {
2756
+ section: "Efficiency",
2757
+ label: "Cost / edit",
2758
+ formatFn: "cost",
2759
+ higherIsBetter: false,
2760
+ compute: (s) => s.editTurns > 0 ? s.editCost / s.editTurns : null
2761
+ },
2762
+ {
2763
+ section: "Efficiency",
2764
+ label: "Output tok / call",
2765
+ formatFn: "number",
2766
+ higherIsBetter: false,
2767
+ compute: (s) => s.calls > 0 ? Math.round(s.outputTokens / s.calls) : null
2768
+ },
2769
+ {
2770
+ section: "Efficiency",
2771
+ label: "Cache hit rate",
2772
+ formatFn: "percent",
2773
+ higherIsBetter: true,
2774
+ compute: (s) => {
2775
+ const total = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens;
2776
+ return total > 0 ? s.cacheReadTokens / total * 100 : null;
2777
+ }
2778
+ }
2779
+ ];
2780
+ function pickWinner(valueA, valueB, higherIsBetter) {
2781
+ if (valueA === null || valueB === null) return "none";
2782
+ if (valueA === valueB) return "tie";
2783
+ if (higherIsBetter) return valueA > valueB ? "a" : "b";
2784
+ return valueA < valueB ? "a" : "b";
2785
+ }
2786
+ function computeComparison(a, b) {
2787
+ return METRICS.map((m) => {
2788
+ const valueA = m.compute(a);
2789
+ const valueB = m.compute(b);
2790
+ return {
2791
+ section: m.section,
2792
+ label: m.label,
2793
+ valueA,
2794
+ valueB,
2795
+ formatFn: m.formatFn,
2796
+ winner: pickWinner(valueA, valueB, m.higherIsBetter)
2797
+ };
2798
+ });
2799
+ }
2800
+ function computeCategoryComparison(projects, modelA, modelB) {
2801
+ const mapA = /* @__PURE__ */ new Map();
2802
+ const mapB = /* @__PURE__ */ new Map();
2803
+ const ensure = (map, cat) => {
2804
+ let a = map.get(cat);
2805
+ if (!a) {
2806
+ a = { turns: 0, editTurns: 0, oneShotTurns: 0 };
2807
+ map.set(cat, a);
2808
+ }
2809
+ return a;
2810
+ };
2811
+ for (const project of projects) {
2812
+ for (const session of project.sessions) {
2813
+ if (session.reportSummary) {
2814
+ for (const [model, map] of [[modelA, mapA], [modelB, mapB]]) {
2815
+ for (const [category, row] of Object.entries(session.reportSummary.categories[model] ?? {})) mergeCounters(ensure(map, category), row);
2816
+ }
2817
+ continue;
2818
+ }
2819
+ for (const turn of session.turns) {
2820
+ if (turn.assistantCalls.length === 0) continue;
2821
+ const primary = turn.assistantCalls[0].model;
2822
+ if (primary !== modelA && primary !== modelB) continue;
2823
+ const acc = ensure(primary === modelA ? mapA : mapB, turn.category);
2824
+ acc.turns++;
2825
+ if (turn.hasEdits) {
2826
+ acc.editTurns++;
2827
+ if (turn.retries === 0) acc.oneShotTurns++;
2828
+ }
2829
+ }
2830
+ }
2831
+ }
2832
+ const allCats = /* @__PURE__ */ new Set([...mapA.keys(), ...mapB.keys()]);
2833
+ const result = [];
2834
+ for (const category of allCats) {
2835
+ const a = mapA.get(category);
2836
+ const b = mapB.get(category);
2837
+ if ((!a || a.editTurns === 0) && (!b || b.editTurns === 0)) continue;
2838
+ const rateA = a && a.editTurns > 0 ? a.oneShotTurns / a.editTurns * 100 : null;
2839
+ const rateB = b && b.editTurns > 0 ? b.oneShotTurns / b.editTurns * 100 : null;
2840
+ result.push({
2841
+ category,
2842
+ turnsA: a?.turns ?? 0,
2843
+ editTurnsA: a?.editTurns ?? 0,
2844
+ oneShotRateA: rateA,
2845
+ turnsB: b?.turns ?? 0,
2846
+ editTurnsB: b?.editTurns ?? 0,
2847
+ oneShotRateB: rateB,
2848
+ winner: pickWinner(rateA, rateB, true)
2849
+ });
2850
+ }
2851
+ return result.sort((a, b) => b.turnsA + b.turnsB - (a.turnsA + a.turnsB));
2852
+ }
2853
+ function computeWorkingStyle(projects, modelA, modelB) {
2854
+ const sA = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
2855
+ const sB = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
2856
+ for (const project of projects) {
2857
+ for (const session of project.sessions) {
2858
+ if (session.reportSummary) {
2859
+ if (session.reportSummary.styles[modelA]) mergeCounters(sA, session.reportSummary.styles[modelA]);
2860
+ if (session.reportSummary.styles[modelB]) mergeCounters(sB, session.reportSummary.styles[modelB]);
2861
+ continue;
2862
+ }
2863
+ for (const turn of session.turns) {
2864
+ if (turn.assistantCalls.length === 0) continue;
2865
+ const primary = turn.assistantCalls[0].model;
2866
+ if (primary !== modelA && primary !== modelB) continue;
2867
+ const s = primary === modelA ? sA : sB;
2868
+ s.totalTurns++;
2869
+ const turnTools = turn.assistantCalls.flatMap((c) => c.tools);
2870
+ if (turnTools.some((t) => PLANNING_TOOLS.has(t)) || turn.assistantCalls.some((c) => c.hasPlanMode)) {
2871
+ s.planModeUses++;
2872
+ }
2873
+ for (const call of turn.assistantCalls) {
2874
+ s.totalToolCalls += call.tools.length;
2875
+ if (call.hasAgentSpawn) s.agentSpawns++;
2876
+ if (call.speed === "fast") s.fastModeCalls++;
2877
+ }
2878
+ }
2879
+ }
2880
+ }
2881
+ const pct2 = (num, den) => den > 0 ? num / den * 100 : null;
2882
+ const avg = (num, den) => den > 0 ? num / den : null;
2883
+ return [
2884
+ { label: "Delegation rate", valueA: pct2(sA.agentSpawns, sA.totalTurns), valueB: pct2(sB.agentSpawns, sB.totalTurns), formatFn: "percent" },
2885
+ { label: "Planning rate", valueA: pct2(sA.planModeUses, sA.totalTurns), valueB: pct2(sB.planModeUses, sB.totalTurns), formatFn: "percent" },
2886
+ { label: "Avg tools / turn", valueA: avg(sA.totalToolCalls, sA.totalTurns), valueB: avg(sB.totalToolCalls, sB.totalTurns), formatFn: "decimal" },
2887
+ { label: "Fast mode usage", valueA: pct2(sA.fastModeCalls, sA.totalTurns), valueB: pct2(sB.fastModeCalls, sB.totalTurns), formatFn: "percent" }
2888
+ ];
2889
+ }
2890
+ var SELF_CORRECTION_PATTERNS = [
2891
+ /\bmy mistake\b/i,
2892
+ /\bmy bad\b/i,
2893
+ /\bmy apolog/i,
2894
+ /\bI apologize\b/i,
2895
+ /\bI was wrong\b/i,
2896
+ /\bI was incorrect\b/i,
2897
+ /\bI made (a |an )?(error|mistake)\b/i,
2898
+ /\bI incorrectly\b/i,
2899
+ /\bI mistakenly\b/i,
2900
+ /\bthat was (incorrect|wrong|an error)\b/i,
2901
+ /\blet me correct that\b/i,
2902
+ /\bI need to correct\b/i,
2903
+ /\byou're right[.,]? I/i,
2904
+ /\bsorry about that\b/i
2905
+ ];
2906
+ function extractText(content) {
2907
+ if (typeof content === "string") return content;
2908
+ if (!Array.isArray(content)) return "";
2909
+ return content.filter((b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ");
2910
+ }
2911
+ function isCompactFile(name) {
2912
+ return name.includes("compact");
2913
+ }
2914
+ async function collectJsonlFiles(sessionDir) {
2915
+ const entries = await readdir2(sessionDir, { withFileTypes: true });
2916
+ const files = [];
2917
+ for (const entry of entries) {
2918
+ if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
2919
+ files.push(join7(sessionDir, entry.name));
2920
+ } else if (entry.isDirectory() && entry.name === "subagents") {
2921
+ const subEntries = await readdir2(join7(sessionDir, entry.name), { withFileTypes: true });
2922
+ for (const sub of subEntries) {
2923
+ if (sub.isFile() && sub.name.endsWith(".jsonl") && !isCompactFile(sub.name)) {
2924
+ files.push(join7(sessionDir, entry.name, sub.name));
2925
+ }
2926
+ }
2927
+ }
2928
+ }
2929
+ return files;
2930
+ }
2931
+ async function scanSelfCorrections(projectDirs) {
2932
+ const counts = /* @__PURE__ */ new Map();
2933
+ const seen = /* @__PURE__ */ new Set();
2934
+ for (const dir of projectDirs) {
2935
+ let entries;
2936
+ try {
2937
+ entries = await readdir2(dir, { withFileTypes: true });
2938
+ } catch {
2939
+ continue;
2940
+ }
2941
+ const allFiles = [];
2942
+ for (const entry of entries) {
2943
+ if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
2944
+ allFiles.push(join7(dir, entry.name));
2945
+ } else if (entry.isDirectory()) {
2946
+ try {
2947
+ const sessionFiles = await collectJsonlFiles(join7(dir, entry.name));
2948
+ allFiles.push(...sessionFiles);
2949
+ } catch {
2950
+ continue;
2951
+ }
2952
+ }
2953
+ }
2954
+ for (const file of allFiles) {
2955
+ let raw;
2956
+ try {
2957
+ raw = await readFile5(file, "utf8");
2958
+ } catch {
2959
+ continue;
2960
+ }
2961
+ for (const line of raw.split("\n")) {
2962
+ const trimmed = line.trim();
2963
+ if (!trimmed) continue;
2964
+ let parsed;
2965
+ try {
2966
+ parsed = JSON.parse(trimmed);
2967
+ } catch {
2968
+ continue;
2969
+ }
2970
+ const rec = parsed;
2971
+ if (!rec || typeof rec !== "object" || rec["type"] !== "assistant") continue;
2972
+ const ts = rec["timestamp"];
2973
+ const msg = rec["message"];
2974
+ if (msg === null || typeof msg !== "object") continue;
2975
+ const msgRec = msg;
2976
+ const model = msgRec["model"];
2977
+ if (typeof model !== "string" || model === "<synthetic>") continue;
2978
+ const dedupeKey = `${model}:${ts}`;
2979
+ if (seen.has(dedupeKey)) continue;
2980
+ seen.add(dedupeKey);
2981
+ const text = extractText(msgRec["content"]);
2982
+ if (SELF_CORRECTION_PATTERNS.some((p) => p.test(text))) {
2983
+ counts.set(model, (counts.get(model) ?? 0) + 1);
2984
+ }
2985
+ }
2986
+ }
2987
+ }
2988
+ return counts;
2989
+ }
2990
+
2991
+ // src/report-summary.ts
2992
+ var planning = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "EnterPlanMode", "ExitPlanMode"]);
2993
+ function compactReportSession(session) {
2994
+ const report = { daily: /* @__PURE__ */ Object.create(null), models: /* @__PURE__ */ Object.create(null), categories: /* @__PURE__ */ Object.create(null), styles: /* @__PURE__ */ Object.create(null) };
2995
+ for (const model of aggregateModelStats([{ project: session.project, projectPath: "", sessions: [session], totalCostUSD: session.totalCostUSD, totalApiCalls: session.apiCalls }])) report.models[model.model] = model;
2996
+ for (const turn of session.turns) {
2997
+ if (turn.timestamp) {
2998
+ const day = dateKey(turn.timestamp);
2999
+ const d = report.daily[day] ??= { cost: 0, calls: 0 };
3000
+ d.cost += turn.assistantCalls.reduce((n, c) => n + c.costUSD, 0);
3001
+ d.calls += turn.assistantCalls.length;
3002
+ }
3003
+ const model = turn.assistantCalls[0]?.model;
3004
+ if (!model) continue;
3005
+ const cats = report.categories[model] ??= /* @__PURE__ */ Object.create(null);
3006
+ const cat = cats[turn.category] ??= { turns: 0, editTurns: 0, oneShotTurns: 0 };
3007
+ cat.turns++;
3008
+ if (turn.hasEdits) {
3009
+ cat.editTurns++;
3010
+ if (!turn.retries) cat.oneShotTurns++;
3011
+ }
3012
+ const style = report.styles[model] ??= { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
3013
+ style.totalTurns++;
3014
+ if (turn.assistantCalls.some((c) => c.hasPlanMode || c.tools.some((t) => planning.has(t)))) style.planModeUses++;
3015
+ for (const call of turn.assistantCalls) {
3016
+ style.totalToolCalls += call.tools.length;
3017
+ if (call.hasAgentSpawn) style.agentSpawns++;
3018
+ if (call.speed === "fast") style.fastModeCalls++;
3019
+ }
3020
+ }
3021
+ session.reportSummary = report;
3022
+ session.turns = [];
3023
+ return session;
3024
+ }
3025
+ function mergeReportSession(target, source) {
3026
+ mergeCounters(target, source);
3027
+ }
3028
+ function mergeCounters(target, source) {
3029
+ const dst = target;
3030
+ for (const [key, value] of Object.entries(source)) {
3031
+ const old = Object.hasOwn(dst, key) ? dst[key] : void 0;
3032
+ let next = old;
3033
+ if (typeof value === "number") next = (typeof old === "number" ? old : 0) + value;
3034
+ else if (typeof value === "string") {
3035
+ if (key === "firstSeen" || key === "firstTimestamp") next = !old || value && value < String(old) ? value : old;
3036
+ else if (key === "lastSeen" || key === "lastTimestamp") next = !old || value > String(old) ? value : old;
3037
+ else next = old ?? value;
3038
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
3039
+ next = old && typeof old === "object" ? old : /* @__PURE__ */ Object.create(null);
3040
+ mergeCounters(next, value);
3041
+ }
3042
+ Object.defineProperty(dst, key, { value: next, writable: true, enumerable: true, configurable: true });
3043
+ }
3044
+ }
3045
+
3046
+ // src/parsed-file-cache.ts
3047
+ init_cache_dir();
3048
+ init_models();
3049
+ init_resource_budget();
3050
+ import { createHash as createHash2, randomUUID } from "crypto";
3051
+ import { mkdir as mkdir7, readFile as readFile6, stat as stat4, writeFile as writeFile2, rename as rename5, unlink as unlink4, readdir as readdir3 } from "fs/promises";
3052
+ import { join as join8 } from "path";
3053
+ var VERSION = 1;
3054
+ var MAX_ENTRY = 16 * 1024 * 1024;
3055
+ var MAX_DISK = 256 * 1024 * 1024;
3056
+ var maintainedDir = "";
3057
+ async function fingerprint(path) {
3058
+ try {
3059
+ const s = await stat4(path);
3060
+ const wal = await stat4(`${path}-wal`).catch(() => null);
3061
+ return [s.dev, s.ino, s.size, s.mtimeMs, s.ctimeMs, wal?.size, wal?.mtimeMs].join(":");
3062
+ } catch {
3063
+ return null;
3064
+ }
3065
+ }
3066
+ async function cachedFileParse(path, kind, parse, warnings) {
3067
+ const dir = join8(getCacheDir(), "parsed-files");
3068
+ const key = createHash2("sha256").update(`${VERSION}:${kind}:${path}:${getPricingFingerprint()}`).digest("hex");
3069
+ const cachePath = join8(dir, `${key}.json`);
3070
+ const fingerprintPath = kind === "opencode" ? path.slice(0, path.lastIndexOf(":")) : path;
3071
+ const before = await fingerprint(fingerprintPath);
3072
+ if (before) {
3073
+ try {
3074
+ const s = await stat4(cachePath);
3075
+ if (s.size <= MAX_ENTRY) {
3076
+ chargeRead(s.size);
3077
+ const saved = JSON.parse(await readFile6(cachePath, "utf8"));
3078
+ if (saved.v === VERSION && saved.fingerprint === before && Array.isArray(saved.data)) {
3079
+ if (Array.isArray(saved.warnings)) warnings?.push(...saved.warnings.filter((w) => typeof w === "string"));
3080
+ return saved.data;
3081
+ }
3082
+ }
3083
+ } catch (err) {
3084
+ if (err instanceof ResourceBudgetError) throw err;
3085
+ }
3086
+ }
3087
+ const data = await parse();
3088
+ if (!before || await fingerprint(fingerprintPath) !== before) return data;
3089
+ const payload = JSON.stringify({ v: VERSION, fingerprint: before, data, warnings });
3090
+ if (Buffer.byteLength(payload) > MAX_ENTRY) return data;
3091
+ await mkdir7(dir, { recursive: true });
3092
+ const release = await acquireFileLock(join8(dir, "writes.lock"));
3093
+ try {
3094
+ const quotaPath = join8(dir, "quota");
3095
+ if (maintainedDir !== dir) {
3096
+ const files = await readdir3(dir);
3097
+ const rows = [];
3098
+ for (const name of files) {
3099
+ if (!name.endsWith(".json") && !name.endsWith(".tmp")) continue;
3100
+ const p = join8(dir, name), s = await stat4(p).catch(() => null);
3101
+ if (s) rows.push({ path: p, size: s.size, mtime: s.mtimeMs });
3102
+ }
3103
+ let total = rows.reduce((n, r) => n + r.size, 0);
3104
+ for (const row of rows.sort((a, b) => a.mtime - b.mtime)) {
3105
+ if (total <= MAX_DISK) break;
3106
+ await unlink4(row.path).catch(() => {
3107
+ });
3108
+ total -= row.size;
3109
+ }
3110
+ await writeFile2(quotaPath, String(total), { mode: 384 });
3111
+ maintainedDir = dir;
3112
+ }
3113
+ if (await readFile6(cachePath, "utf8").catch(() => "") !== payload) {
3114
+ const total = Number(await readFile6(quotaPath, "utf8").catch(() => String(MAX_DISK)));
3115
+ const oldSize = (await stat4(cachePath).catch(() => null))?.size ?? 0;
3116
+ const nextSize = total - oldSize + Buffer.byteLength(payload);
3117
+ if (!Number.isFinite(nextSize) || nextSize > MAX_DISK) return data;
3118
+ const tmp = `${cachePath}.${randomUUID()}.tmp`;
3119
+ try {
3120
+ await writeFile2(quotaPath, String(nextSize), { mode: 384 });
3121
+ await writeFile2(tmp, payload, { mode: 384 });
3122
+ await rename5(tmp, cachePath);
3123
+ } finally {
3124
+ await unlink4(tmp).catch(() => {
3125
+ });
3126
+ }
3127
+ }
2447
3128
  } finally {
2448
- stream.destroy();
3129
+ await release();
2449
3130
  }
3131
+ return data;
2450
3132
  }
2451
3133
 
2452
3134
  // src/parser.ts
3135
+ init_resource_budget();
3136
+ init_fs_utils();
2453
3137
  init_models();
3138
+ import { readdir as readdir10, stat as stat12 } from "fs/promises";
3139
+ import { basename as basename8, join as join18 } from "path";
3140
+
3141
+ // src/providers/index.ts
3142
+ init_resource_budget();
2454
3143
 
2455
3144
  // src/providers/claude.ts
2456
- import { readdir as readdir2, stat as stat4 } from "fs/promises";
2457
- import { basename as basename2, join as join7 } from "path";
3145
+ import { readdir as readdir4, stat as stat5 } from "fs/promises";
3146
+ import { basename as basename2, join as join9 } from "path";
2458
3147
  import { homedir as homedir4 } from "os";
2459
3148
  var shortNames = {
2460
3149
  "claude-opus-4-7": "Opus 4.7",
@@ -2471,31 +3160,31 @@ var shortNames = {
2471
3160
  "claude-3-5-haiku": "Haiku 3.5"
2472
3161
  };
2473
3162
  function getClaudeDir() {
2474
- return process.env["CLAUDE_CONFIG_DIR"] || join7(homedir4(), ".claude");
3163
+ return process.env["CLAUDE_CONFIG_DIR"] || join9(homedir4(), ".claude");
2475
3164
  }
2476
3165
  function getProjectsDir() {
2477
- return join7(getClaudeDir(), "projects");
3166
+ return join9(getClaudeDir(), "projects");
2478
3167
  }
2479
3168
  function getDesktopSessionsDir() {
2480
- if (process.platform === "darwin") return join7(homedir4(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
2481
- if (process.platform === "win32") return join7(homedir4(), "AppData", "Roaming", "Claude", "local-agent-mode-sessions");
2482
- return join7(homedir4(), ".config", "Claude", "local-agent-mode-sessions");
3169
+ if (process.platform === "darwin") return join9(homedir4(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
3170
+ if (process.platform === "win32") return join9(homedir4(), "AppData", "Roaming", "Claude", "local-agent-mode-sessions");
3171
+ return join9(homedir4(), ".config", "Claude", "local-agent-mode-sessions");
2483
3172
  }
2484
3173
  async function findDesktopProjectDirs(base) {
2485
3174
  const results = [];
2486
3175
  async function walk(dir, depth) {
2487
3176
  if (depth > 8) return;
2488
- const entries = await readdir2(dir).catch(() => []);
3177
+ const entries = await readdir4(dir).catch(() => []);
2489
3178
  for (const entry of entries) {
2490
3179
  if (entry === "node_modules" || entry === ".git") continue;
2491
- const full = join7(dir, entry);
2492
- const s = await stat4(full).catch(() => null);
3180
+ const full = join9(dir, entry);
3181
+ const s = await stat5(full).catch(() => null);
2493
3182
  if (!s?.isDirectory()) continue;
2494
3183
  if (entry === "projects") {
2495
- const projectDirs = await readdir2(full).catch(() => []);
3184
+ const projectDirs = await readdir4(full).catch(() => []);
2496
3185
  for (const pd of projectDirs) {
2497
- const pdFull = join7(full, pd);
2498
- const pdStat = await stat4(pdFull).catch(() => null);
3186
+ const pdFull = join9(full, pd);
3187
+ const pdStat = await stat5(pdFull).catch(() => null);
2499
3188
  if (pdStat?.isDirectory()) results.push(pdFull);
2500
3189
  }
2501
3190
  } else {
@@ -2523,10 +3212,10 @@ var claude = {
2523
3212
  const sources = [];
2524
3213
  const projectsDir = getProjectsDir();
2525
3214
  try {
2526
- const entries = await readdir2(projectsDir);
3215
+ const entries = await readdir4(projectsDir);
2527
3216
  for (const dirName of entries) {
2528
- const dirPath = join7(projectsDir, dirName);
2529
- const dirStat = await stat4(dirPath).catch(() => null);
3217
+ const dirPath = join9(projectsDir, dirName);
3218
+ const dirStat = await stat5(dirPath).catch(() => null);
2530
3219
  if (dirStat?.isDirectory()) {
2531
3220
  sources.push({ path: dirPath, project: dirName, provider: "claude" });
2532
3221
  }
@@ -2548,10 +3237,11 @@ var claude = {
2548
3237
  };
2549
3238
 
2550
3239
  // src/providers/codex.ts
2551
- import { readdir as readdir3, stat as stat5 } from "fs/promises";
2552
- import { basename as basename3, join as join8 } from "path";
2553
- import { homedir as homedir5 } from "os";
3240
+ init_fs_utils();
2554
3241
  init_models();
3242
+ import { readdir as readdir5, stat as stat6 } from "fs/promises";
3243
+ import { basename as basename3, join as join10 } from "path";
3244
+ import { homedir as homedir5 } from "os";
2555
3245
  var modelDisplayNames = {
2556
3246
  "gpt-5.5": "GPT-5.5",
2557
3247
  "gpt-5.4": "GPT-5.4",
@@ -2575,15 +3265,13 @@ var toolNameMap = {
2575
3265
  read_dir: "Glob"
2576
3266
  };
2577
3267
  function getCodexDir(override) {
2578
- return override ?? process.env["CODEX_HOME"] ?? join8(homedir5(), ".codex");
3268
+ return override ?? process.env["CODEX_HOME"] ?? join10(homedir5(), ".codex");
2579
3269
  }
2580
3270
  function sanitizeProject(cwd) {
2581
3271
  return cwd.replace(/^\//, "").replace(/\//g, "-");
2582
3272
  }
2583
3273
  async function readFirstLine(filePath) {
2584
- const content = await readSessionFile(filePath);
2585
- if (content === null) return null;
2586
- const line = content.split("\n")[0];
3274
+ const line = await readSessionFirstLine(filePath);
2587
3275
  if (!line?.trim()) return null;
2588
3276
  try {
2589
3277
  return JSON.parse(line);
@@ -2598,30 +3286,30 @@ async function isValidCodexSession(filePath) {
2598
3286
  return { valid, meta: valid ? entry : void 0 };
2599
3287
  }
2600
3288
  async function discoverSessionsInDir(codexDir) {
2601
- const sessionsDir = join8(codexDir, "sessions");
3289
+ const sessionsDir = join10(codexDir, "sessions");
2602
3290
  const sources = [];
2603
3291
  let years;
2604
3292
  try {
2605
- years = await readdir3(sessionsDir);
3293
+ years = await readdir5(sessionsDir);
2606
3294
  } catch {
2607
3295
  return sources;
2608
3296
  }
2609
3297
  for (const year of years) {
2610
3298
  if (!/^\d{4}$/.test(year)) continue;
2611
- const yearDir = join8(sessionsDir, year);
2612
- const months = await readdir3(yearDir).catch(() => []);
3299
+ const yearDir = join10(sessionsDir, year);
3300
+ const months = await readdir5(yearDir).catch(() => []);
2613
3301
  for (const month of months) {
2614
3302
  if (!/^\d{2}$/.test(month)) continue;
2615
- const monthDir = join8(yearDir, month);
2616
- const days = await readdir3(monthDir).catch(() => []);
3303
+ const monthDir = join10(yearDir, month);
3304
+ const days = await readdir5(monthDir).catch(() => []);
2617
3305
  for (const day of days) {
2618
3306
  if (!/^\d{2}$/.test(day)) continue;
2619
- const dayDir = join8(monthDir, day);
2620
- const files = await readdir3(dayDir).catch(() => []);
3307
+ const dayDir = join10(monthDir, day);
3308
+ const files = await readdir5(dayDir).catch(() => []);
2621
3309
  for (const file of files) {
2622
3310
  if (!file.startsWith("rollout-") || !file.endsWith(".jsonl")) continue;
2623
- const filePath = join8(dayDir, file);
2624
- const s = await stat5(filePath).catch(() => null);
3311
+ const filePath = join10(dayDir, file);
3312
+ const s = await stat6(filePath).catch(() => null);
2625
3313
  if (!s?.isFile()) continue;
2626
3314
  const { valid, meta } = await isValidCodexSession(filePath);
2627
3315
  if (!valid || !meta) continue;
@@ -2641,10 +3329,6 @@ function createParser(source, seenKeys) {
2641
3329
  return {
2642
3330
  warnings: parserWarnings,
2643
3331
  async *parse() {
2644
- const content = await readSessionFile(source.path);
2645
- if (content === null) return;
2646
- const lines = content.split("\n").filter((l) => l.trim());
2647
- if (lines.length === 0) return;
2648
3332
  let hasSessionMeta = false;
2649
3333
  let hasTokenCounts = false;
2650
3334
  let sessionModel;
@@ -2656,7 +3340,10 @@ function createParser(source, seenKeys) {
2656
3340
  let prevReasoning = 0;
2657
3341
  let pendingTools = [];
2658
3342
  let pendingUserMessage = "";
2659
- for (const line of lines) {
3343
+ let lineCount = 0;
3344
+ for await (const line of readSessionLines(source.path)) {
3345
+ if (!line.trim()) continue;
3346
+ lineCount++;
2660
3347
  let entry;
2661
3348
  try {
2662
3349
  entry = JSON.parse(line);
@@ -2760,10 +3447,10 @@ function createParser(source, seenKeys) {
2760
3447
  pendingUserMessage = "";
2761
3448
  }
2762
3449
  }
2763
- if (lines.length > 5 && !hasSessionMeta) {
3450
+ if (lineCount > 5 && !hasSessionMeta) {
2764
3451
  parserWarnings.push(`Codex session file format may have changed \u2014 missing session_meta (${basename3(source.path)})`);
2765
3452
  }
2766
- if (lines.length > 5 && hasSessionMeta && !hasTokenCounts) {
3453
+ if (lineCount > 5 && hasSessionMeta && !hasTokenCounts) {
2767
3454
  parserWarnings.push(`Codex session file format may have changed \u2014 no token_count events found (${basename3(source.path)})`);
2768
3455
  }
2769
3456
  }
@@ -2794,10 +3481,11 @@ function createCodexProvider(codexDir) {
2794
3481
  var codex = createCodexProvider();
2795
3482
 
2796
3483
  // src/providers/copilot.ts
2797
- import { readdir as readdir4, stat as stat6 } from "fs/promises";
2798
- import { basename as basename4, dirname as dirname2, join as join9 } from "path";
2799
- import { homedir as homedir6 } from "os";
3484
+ init_fs_utils();
2800
3485
  init_models();
3486
+ import { readdir as readdir6, stat as stat7 } from "fs/promises";
3487
+ import { basename as basename4, dirname as dirname3, join as join11 } from "path";
3488
+ import { homedir as homedir6 } from "os";
2801
3489
  var modelDisplayNames2 = {
2802
3490
  "gpt-4.1-nano": "GPT-4.1 Nano",
2803
3491
  "gpt-4.1-mini": "GPT-4.1 Mini",
@@ -2829,7 +3517,7 @@ var toolNameMap2 = {
2829
3517
  };
2830
3518
  var modelDisplayEntries2 = Object.entries(modelDisplayNames2).sort((a, b) => b[0].length - a[0].length);
2831
3519
  function getCopilotSessionStateDir(override) {
2832
- return override ?? join9(homedir6(), ".copilot", "session-state");
3520
+ return override ?? join11(homedir6(), ".copilot", "session-state");
2833
3521
  }
2834
3522
  function parseCwd(yaml) {
2835
3523
  const match = yaml.match(/^cwd:\s*(.+)$/m);
@@ -2840,13 +3528,13 @@ function parseCwd(yaml) {
2840
3528
  function createParser2(source, seenKeys) {
2841
3529
  return {
2842
3530
  async *parse() {
2843
- const content = await readSessionFile(source.path);
2844
- if (content === null) return;
2845
- const sessionId = basename4(dirname2(source.path));
2846
- const lines = content.split("\n").filter((l) => l.trim());
3531
+ const sessionId = basename4(dirname3(source.path));
2847
3532
  let currentModel = "";
2848
3533
  let pendingUserMessage = "";
2849
- for (const line of lines) {
3534
+ let lineCount = 0;
3535
+ for await (const line of readSessionLines(source.path)) {
3536
+ if (!line.trim()) continue;
3537
+ lineCount++;
2850
3538
  let event;
2851
3539
  try {
2852
3540
  event = JSON.parse(line);
@@ -2899,16 +3587,16 @@ async function discoverSessionsInDir2(sessionStateDir) {
2899
3587
  const sources = [];
2900
3588
  let sessionDirs;
2901
3589
  try {
2902
- sessionDirs = await readdir4(sessionStateDir);
3590
+ sessionDirs = await readdir6(sessionStateDir);
2903
3591
  } catch {
2904
3592
  return sources;
2905
3593
  }
2906
3594
  for (const sessionId of sessionDirs) {
2907
- const eventsPath = join9(sessionStateDir, sessionId, "events.jsonl");
2908
- const s = await stat6(eventsPath).catch(() => null);
3595
+ const eventsPath = join11(sessionStateDir, sessionId, "events.jsonl");
3596
+ const s = await stat7(eventsPath).catch(() => null);
2909
3597
  if (!s?.isFile()) continue;
2910
3598
  let project = sessionId;
2911
- const yaml = await readSessionFile(join9(sessionStateDir, sessionId, "workspace.yaml"));
3599
+ const yaml = await readSessionFile(join11(sessionStateDir, sessionId, "workspace.yaml"));
2912
3600
  if (yaml !== null) {
2913
3601
  const cwd = parseCwd(yaml);
2914
3602
  if (cwd) project = basename4(cwd);
@@ -2942,11 +3630,12 @@ function createCopilotProvider(sessionStateDir) {
2942
3630
  var copilot = createCopilotProvider();
2943
3631
 
2944
3632
  // src/providers/pi.ts
2945
- import { readdir as readdir5, stat as stat7 } from "fs/promises";
2946
- import { basename as basename6, join as join10 } from "path";
2947
- import { homedir as homedir7 } from "os";
3633
+ init_fs_utils();
2948
3634
  init_models();
2949
3635
  init_bash_utils();
3636
+ import { readdir as readdir7, stat as stat8 } from "fs/promises";
3637
+ import { basename as basename6, join as join12 } from "path";
3638
+ import { homedir as homedir7 } from "os";
2950
3639
  var modelDisplayNames3 = {
2951
3640
  "gpt-5.4": "GPT-5.4",
2952
3641
  "gpt-5.4-mini": "GPT-5.4 Mini",
@@ -2970,15 +3659,13 @@ var toolNameMap3 = {
2970
3659
  };
2971
3660
  var modelDisplayEntries3 = Object.entries(modelDisplayNames3).sort((a, b) => b[0].length - a[0].length);
2972
3661
  function getPiSessionsDir(override) {
2973
- return override ?? join10(homedir7(), ".pi", "agent", "sessions");
3662
+ return override ?? join12(homedir7(), ".pi", "agent", "sessions");
2974
3663
  }
2975
3664
  function getOmpSessionsDir(override) {
2976
- return override ?? join10(homedir7(), ".omp", "agent", "sessions");
3665
+ return override ?? join12(homedir7(), ".omp", "agent", "sessions");
2977
3666
  }
2978
3667
  async function readFirstEntry(filePath) {
2979
- const content = await readSessionFile(filePath);
2980
- if (content === null) return null;
2981
- const line = content.split("\n")[0];
3668
+ const line = await readSessionFirstLine(filePath);
2982
3669
  if (!line?.trim()) return null;
2983
3670
  try {
2984
3671
  return JSON.parse(line);
@@ -2990,24 +3677,24 @@ async function discoverSessionsInDir3(sessionsDir, providerName) {
2990
3677
  const sources = [];
2991
3678
  let projectDirs;
2992
3679
  try {
2993
- projectDirs = await readdir5(sessionsDir);
3680
+ projectDirs = await readdir7(sessionsDir);
2994
3681
  } catch {
2995
3682
  return sources;
2996
3683
  }
2997
3684
  for (const dirName of projectDirs) {
2998
- const dirPath = join10(sessionsDir, dirName);
2999
- const dirStat = await stat7(dirPath).catch(() => null);
3685
+ const dirPath = join12(sessionsDir, dirName);
3686
+ const dirStat = await stat8(dirPath).catch(() => null);
3000
3687
  if (!dirStat?.isDirectory()) continue;
3001
3688
  let files;
3002
3689
  try {
3003
- files = await readdir5(dirPath);
3690
+ files = await readdir7(dirPath);
3004
3691
  } catch {
3005
3692
  continue;
3006
3693
  }
3007
3694
  for (const file of files) {
3008
3695
  if (!file.endsWith(".jsonl")) continue;
3009
- const filePath = join10(dirPath, file);
3010
- const fileStat = await stat7(filePath).catch(() => null);
3696
+ const filePath = join12(dirPath, file);
3697
+ const fileStat = await stat8(filePath).catch(() => null);
3011
3698
  if (!fileStat?.isFile()) continue;
3012
3699
  const first = await readFirstEntry(filePath);
3013
3700
  if (!first || first.type !== "session") continue;
@@ -3020,12 +3707,12 @@ async function discoverSessionsInDir3(sessionsDir, providerName) {
3020
3707
  function createParser3(source, seenKeys) {
3021
3708
  return {
3022
3709
  async *parse() {
3023
- const content = await readSessionFile(source.path);
3024
- if (content === null) return;
3025
- const lines = content.split("\n").filter((l) => l.trim());
3026
3710
  let sessionId = basename6(source.path, ".jsonl");
3027
3711
  let pendingUserMessage = "";
3028
- for (const [lineIdx, line] of lines.entries()) {
3712
+ let lineIdx = -1;
3713
+ for await (const line of readSessionLines(source.path)) {
3714
+ if (!line.trim()) continue;
3715
+ lineIdx++;
3029
3716
  let entry;
3030
3717
  try {
3031
3718
  entry = JSON.parse(line);
@@ -3184,40 +3871,22 @@ var _lastDiscoveryWarnings = [];
3184
3871
  function getDiscoveryWarnings() {
3185
3872
  return _lastDiscoveryWarnings;
3186
3873
  }
3187
- var PROVIDER_TIMEOUT_MS = 5e3;
3188
- function withTimeout(promise, ms, label) {
3189
- return Promise.race([
3190
- promise.then((result) => ({ result, warning: null })),
3191
- new Promise(
3192
- (resolve2) => setTimeout(() => resolve2({ result: null, warning: `Provider "${label}" timed out after ${ms}ms \u2014 skipped` }), ms)
3193
- )
3194
- ]);
3195
- }
3196
3874
  async function discoverAllSessions(providerFilter) {
3197
3875
  _lastDiscoveryWarnings = [];
3198
3876
  const allProviders = await getAllProviders();
3199
3877
  const filtered = providerFilter && providerFilter !== "all" ? allProviders.filter((p) => p.name === providerFilter) : allProviders;
3200
- const results = await Promise.all(
3201
- filtered.map(async (provider) => {
3202
- try {
3203
- const { result, warning } = await withTimeout(
3204
- provider.discoverSessions(),
3205
- PROVIDER_TIMEOUT_MS,
3206
- provider.name
3207
- );
3208
- if (warning) {
3209
- _lastDiscoveryWarnings.push(warning);
3210
- return [];
3211
- }
3212
- return result ?? [];
3213
- } catch (err) {
3214
- const msg = err instanceof Error ? err.message : String(err);
3215
- _lastDiscoveryWarnings.push(`Provider "${provider.name}" discovery failed: ${msg}`);
3216
- return [];
3217
- }
3218
- })
3219
- );
3220
- return results.flat();
3878
+ const results = [];
3879
+ for (const provider of filtered) {
3880
+ chargeRead(0);
3881
+ try {
3882
+ results.push(...await provider.discoverSessions());
3883
+ } catch (err) {
3884
+ if (err instanceof ResourceBudgetError) throw err;
3885
+ const msg = err instanceof Error ? err.message : String(err);
3886
+ _lastDiscoveryWarnings.push(`Provider "${provider.name}" discovery failed: ${msg}`);
3887
+ }
3888
+ }
3889
+ return results;
3221
3890
  }
3222
3891
  async function getProvider(name) {
3223
3892
  if (name === "cursor") {
@@ -3242,11 +3911,35 @@ var BUILD_PATTERNS = /\b(npm\s+run\s+build|npm\s+publish|pip\s+install|docker|de
3242
3911
  var INSTALL_PATTERNS = /\b(npm\s+install|pip\s+install|brew\s+install|apt\s+install|cargo\s+add)\b/i;
3243
3912
  var DEBUG_KEYWORDS = /\b(fix|bug|error|broken|failing|crash|issue|debug|traceback|exception|stack\s*trace|not\s+working|wrong|unexpected|status\s+code|404|500|401|403)\b/i;
3244
3913
  var FEATURE_KEYWORDS = /\b(add|create|implement|new|build|feature|introduce|set\s*up|scaffold|generate|make\s+(?:a|me|the)|write\s+(?:a|me|the))\b/i;
3914
+ var REFACTOR_KEYWORDS = /\b(refactor|clean\s*up|rename|reorganize|simplify|extract|restructure|move|migrate|split)\b/i;
3245
3915
  var BRAINSTORM_KEYWORDS = /\b(brainstorm|idea|what\s+if|explore|think\s+about|approach|strategy|design|consider|how\s+should|what\s+would|opinion|suggest|recommend)\b/i;
3246
3916
  var RESEARCH_KEYWORDS = /\b(research|investigate|look\s+into|find\s+out|check|search|analyze|review|understand|explain|how\s+does|what\s+is|show\s+me|list|compare)\b/i;
3247
3917
  var FILE_PATTERNS = /\.(py|js|ts|tsx|jsx|json|yaml|yml|toml|sql|sh|go|rs|java|rb|php|css|html|md|csv|xml)\b/i;
3248
3918
  var SCRIPT_PATTERNS = /\b(run\s+\S+\.\w+|execute|scrip?t|curl|api\s+\S+|endpoint|request\s+url|fetch\s+\S+|query|database|db\s+\S+)\b/i;
3249
3919
  var URL_PATTERN = /https?:\/\/\S+/i;
3920
+ var MESSAGE_PATTERNS = [
3921
+ TEST_PATTERNS,
3922
+ GIT_PATTERNS,
3923
+ BUILD_PATTERNS,
3924
+ INSTALL_PATTERNS,
3925
+ DEBUG_KEYWORDS,
3926
+ FEATURE_KEYWORDS,
3927
+ REFACTOR_KEYWORDS,
3928
+ BRAINSTORM_KEYWORDS,
3929
+ RESEARCH_KEYWORDS,
3930
+ FILE_PATTERNS,
3931
+ SCRIPT_PATTERNS,
3932
+ URL_PATTERN
3933
+ ];
3934
+ function compactUserMessage(text, signals) {
3935
+ return {
3936
+ userMessage: Buffer.from(text.slice(0, 512)).toString("utf8"),
3937
+ userMessageSignals: signals ?? MESSAGE_PATTERNS.reduce((mask, pattern, index) => mask | (pattern.test(text) ? 1 << index : 0), 0)
3938
+ };
3939
+ }
3940
+ function messageMatches(turn, pattern) {
3941
+ return turn.userMessageSignals === void 0 ? pattern.test(turn.userMessage) : (turn.userMessageSignals & 1 << MESSAGE_PATTERNS.indexOf(pattern)) !== 0;
3942
+ }
3250
3943
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "FileEditTool", "FileWriteTool", "NotebookEdit", "cursor:edit"]);
3251
3944
  var READ_TOOLS = /* @__PURE__ */ new Set(["Read", "Grep", "Glob", "FileReadTool", "GrepTool", "GlobTool"]);
3252
3945
  var BASH_TOOLS = /* @__PURE__ */ new Set(["Bash", "BashTool", "PowerShellTool"]);
@@ -3289,11 +3982,10 @@ function classifyByToolPattern(turn) {
3289
3982
  const hasMcp = hasMcpTools(tools);
3290
3983
  const hasSkill = hasSkillTool(tools);
3291
3984
  if (hasBash && !hasEdits) {
3292
- const userMsg = turn.userMessage;
3293
- if (TEST_PATTERNS.test(userMsg)) return "testing";
3294
- if (GIT_PATTERNS.test(userMsg)) return "devops";
3295
- if (BUILD_PATTERNS.test(userMsg)) return "devops";
3296
- if (INSTALL_PATTERNS.test(userMsg)) return "devops";
3985
+ if (messageMatches(turn, TEST_PATTERNS)) return "testing";
3986
+ if (messageMatches(turn, GIT_PATTERNS)) return "devops";
3987
+ if (messageMatches(turn, BUILD_PATTERNS)) return "devops";
3988
+ if (messageMatches(turn, INSTALL_PATTERNS)) return "devops";
3297
3989
  }
3298
3990
  if (hasEdits) return "building";
3299
3991
  if (hasBash && hasReads) return "research";
@@ -3304,25 +3996,25 @@ function classifyByToolPattern(turn) {
3304
3996
  if (hasSkill) return "building";
3305
3997
  return null;
3306
3998
  }
3307
- function refineByKeywords(category, userMessage) {
3999
+ function refineByKeywords(category, turn) {
3308
4000
  if (category === "building") {
3309
- if (DEBUG_KEYWORDS.test(userMessage)) return "debugging";
4001
+ if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
3310
4002
  return "building";
3311
4003
  }
3312
4004
  if (category === "research") {
3313
- if (DEBUG_KEYWORDS.test(userMessage)) return "debugging";
4005
+ if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
3314
4006
  return "research";
3315
4007
  }
3316
4008
  return category;
3317
4009
  }
3318
- function classifyConversation(userMessage) {
3319
- if (BRAINSTORM_KEYWORDS.test(userMessage)) return "research";
3320
- if (RESEARCH_KEYWORDS.test(userMessage)) return "research";
3321
- if (DEBUG_KEYWORDS.test(userMessage)) return "debugging";
3322
- if (FEATURE_KEYWORDS.test(userMessage)) return "building";
3323
- if (FILE_PATTERNS.test(userMessage)) return "building";
3324
- if (SCRIPT_PATTERNS.test(userMessage)) return "building";
3325
- if (URL_PATTERN.test(userMessage)) return "research";
4010
+ function classifyConversation(turn) {
4011
+ if (messageMatches(turn, BRAINSTORM_KEYWORDS)) return "research";
4012
+ if (messageMatches(turn, RESEARCH_KEYWORDS)) return "research";
4013
+ if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
4014
+ if (messageMatches(turn, FEATURE_KEYWORDS)) return "building";
4015
+ if (messageMatches(turn, FILE_PATTERNS)) return "building";
4016
+ if (messageMatches(turn, SCRIPT_PATTERNS)) return "building";
4017
+ if (messageMatches(turn, URL_PATTERN)) return "research";
3326
4018
  return "research";
3327
4019
  }
3328
4020
  function countRetries(turn) {
@@ -3350,13 +4042,13 @@ function classifyTurn(turn) {
3350
4042
  const tools = getAllTools(turn);
3351
4043
  let category;
3352
4044
  if (tools.length === 0) {
3353
- category = classifyConversation(turn.userMessage);
4045
+ category = classifyConversation(turn);
3354
4046
  } else {
3355
4047
  const toolCategory = classifyByToolPattern(turn);
3356
4048
  if (toolCategory) {
3357
- category = refineByKeywords(toolCategory, turn.userMessage);
4049
+ category = refineByKeywords(toolCategory, turn);
3358
4050
  } else {
3359
- category = classifyConversation(turn.userMessage);
4051
+ category = classifyConversation(turn);
3360
4052
  }
3361
4053
  }
3362
4054
  return { ...turn, category, retries: countRetries(turn), hasEdits: turnHasEdits(turn) };
@@ -3364,10 +4056,10 @@ function classifyTurn(turn) {
3364
4056
 
3365
4057
  // src/session-index.ts
3366
4058
  init_cache_dir();
3367
- import { stat as stat10 } from "fs/promises";
4059
+ import { stat as stat11 } from "fs/promises";
3368
4060
  import { existsSync as existsSync3, unlinkSync } from "fs";
3369
- import { readFile as readFile7, mkdir as mkdir7, rename as rename6, unlink as unlink3, open as open5 } from "fs/promises";
3370
- import { join as join15 } from "path";
4061
+ import { readFile as readFile9, mkdir as mkdir9, rename as rename7, unlink as unlink5, open as open7 } from "fs/promises";
4062
+ import { join as join17 } from "path";
3371
4063
  import { randomBytes as randomBytes5 } from "crypto";
3372
4064
  var SESSION_INDEX_VERSION = 1;
3373
4065
  var INDEX_FILENAME = "session-index.json";
@@ -3376,12 +4068,12 @@ function emptyIndex() {
3376
4068
  return { v: SESSION_INDEX_VERSION, e: {} };
3377
4069
  }
3378
4070
  function indexPath() {
3379
- return join15(getCacheDir(), INDEX_FILENAME);
4071
+ return join17(getCacheDir(), INDEX_FILENAME);
3380
4072
  }
3381
4073
  async function loadSessionIndex() {
3382
4074
  if (currentIndex) return currentIndex;
3383
4075
  try {
3384
- const raw = await readFile7(indexPath(), "utf-8");
4076
+ const raw = await readFile9(indexPath(), "utf-8");
3385
4077
  const parsed = JSON.parse(raw);
3386
4078
  if (parsed.v !== SESSION_INDEX_VERSION) {
3387
4079
  currentIndex = emptyIndex();
@@ -3396,11 +4088,11 @@ async function loadSessionIndex() {
3396
4088
  }
3397
4089
  async function saveSessionIndex(index) {
3398
4090
  const dir = getCacheDir();
3399
- if (!existsSync3(dir)) await mkdir7(dir, { recursive: true });
4091
+ if (!existsSync3(dir)) await mkdir9(dir, { recursive: true });
3400
4092
  const finalPath = indexPath();
3401
4093
  const tempPath = `${finalPath}.${randomBytes5(8).toString("hex")}.tmp`;
3402
4094
  const payload = JSON.stringify(index);
3403
- const handle = await open5(tempPath, "w", 384);
4095
+ const handle = await open7(tempPath, "w", 384);
3404
4096
  try {
3405
4097
  await handle.writeFile(payload, { encoding: "utf-8" });
3406
4098
  await handle.sync();
@@ -3408,10 +4100,10 @@ async function saveSessionIndex(index) {
3408
4100
  await handle.close();
3409
4101
  }
3410
4102
  try {
3411
- await rename6(tempPath, finalPath);
4103
+ await rename7(tempPath, finalPath);
3412
4104
  } catch (err) {
3413
4105
  try {
3414
- await unlink3(tempPath);
4106
+ await unlink5(tempPath);
3415
4107
  } catch {
3416
4108
  }
3417
4109
  throw err;
@@ -3423,7 +4115,7 @@ async function checkSessionFile(filePath, index, dateRange) {
3423
4115
  if (!entry) return "parse";
3424
4116
  let fileStat;
3425
4117
  try {
3426
- fileStat = await stat10(filePath);
4118
+ fileStat = await stat11(filePath);
3427
4119
  } catch {
3428
4120
  return "parse";
3429
4121
  }
@@ -3434,7 +4126,9 @@ async function checkSessionFile(filePath, index, dateRange) {
3434
4126
  function recordParseResult(filePath, index, sizeBytes, mtimeMs, hasApiCalls) {
3435
4127
  const prev = index.e[filePath];
3436
4128
  const h = hasApiCalls ? 1 : prev?.h === 1 ? 1 : 0;
4129
+ if (prev?.s === sizeBytes && prev?.m === mtimeMs && prev?.h === h) return false;
3437
4130
  index.e[filePath] = { s: sizeBytes, m: mtimeMs, h };
4131
+ return true;
3438
4132
  }
3439
4133
  function pruneIndex(index, knownPaths) {
3440
4134
  let pruned = 0;
@@ -3449,6 +4143,7 @@ function pruneIndex(index, knownPaths) {
3449
4143
 
3450
4144
  // src/parser.ts
3451
4145
  init_bash_utils();
4146
+ var reportMode = new AsyncLocalStorage2();
3452
4147
  var _parseWarnings = [];
3453
4148
  function getParseWarnings() {
3454
4149
  return _parseWarnings;
@@ -3532,25 +4227,34 @@ function parseApiCall(entry) {
3532
4227
  deduplicationKey: msg.id ?? `claude:${entry.timestamp}`
3533
4228
  };
3534
4229
  }
3535
- function groupIntoTurns(entries, seenMsgIds) {
4230
+ async function parseTurns(filePath) {
4231
+ const seenMsgIds = /* @__PURE__ */ new Set();
3536
4232
  const turns = [];
3537
4233
  let currentUserMessage = "";
4234
+ let currentUserSignals = 0;
3538
4235
  let currentCalls = [];
3539
4236
  let currentTimestamp = "";
3540
4237
  let currentSessionId = "";
3541
- for (const entry of entries) {
4238
+ let lineNumber = 0;
4239
+ for await (const line of readSessionLines(filePath)) {
4240
+ lineNumber++;
4241
+ const entry = parseJsonlLine(line);
4242
+ if (!entry) continue;
3542
4243
  if (entry.type === "user") {
3543
4244
  const text = getUserMessageText(entry);
3544
4245
  if (text.trim()) {
3545
4246
  if (currentCalls.length > 0) {
3546
4247
  turns.push({
3547
4248
  userMessage: currentUserMessage,
4249
+ userMessageSignals: currentUserSignals,
3548
4250
  assistantCalls: currentCalls,
3549
4251
  timestamp: currentTimestamp,
3550
4252
  sessionId: currentSessionId
3551
4253
  });
3552
4254
  }
3553
- currentUserMessage = text;
4255
+ const compact = compactUserMessage(text);
4256
+ currentUserMessage = compact.userMessage;
4257
+ currentUserSignals = compact.userMessageSignals;
3554
4258
  currentCalls = [];
3555
4259
  currentTimestamp = entry.timestamp ?? "";
3556
4260
  currentSessionId = entry.sessionId ?? "";
@@ -3560,12 +4264,17 @@ function groupIntoTurns(entries, seenMsgIds) {
3560
4264
  if (msgId && seenMsgIds.has(msgId)) continue;
3561
4265
  if (msgId) seenMsgIds.add(msgId);
3562
4266
  const call = parseApiCall(entry);
3563
- if (call) currentCalls.push(call);
4267
+ if (call) {
4268
+ if (!msgId) call.deduplicationKey = `${filePath}:${lineNumber}`;
4269
+ chargeRecord();
4270
+ currentCalls.push(call);
4271
+ }
3564
4272
  }
3565
4273
  }
3566
4274
  if (currentCalls.length > 0) {
3567
4275
  turns.push({
3568
4276
  userMessage: currentUserMessage,
4277
+ userMessageSignals: currentUserSignals,
3569
4278
  assistantCalls: currentCalls,
3570
4279
  timestamp: currentTimestamp,
3571
4280
  sessionId: currentSessionId
@@ -3637,7 +4346,7 @@ function buildSessionSummary(sessionId, project, turns) {
3637
4346
  if (!lastTs || call.timestamp > lastTs) lastTs = call.timestamp;
3638
4347
  }
3639
4348
  }
3640
- return {
4349
+ const summary = {
3641
4350
  sessionId,
3642
4351
  project,
3643
4352
  firstTimestamp: firstTs || turns[0]?.timestamp || "",
@@ -3655,26 +4364,28 @@ function buildSessionSummary(sessionId, project, turns) {
3655
4364
  bashBreakdown,
3656
4365
  categoryBreakdown
3657
4366
  };
4367
+ return reportMode.getStore() ? compactReportSession(summary) : summary;
3658
4368
  }
3659
4369
  async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
3660
4370
  if (dateRange) {
3661
4371
  try {
3662
- const s = await stat11(filePath);
4372
+ const s = await stat12(filePath);
3663
4373
  if (s.mtimeMs < dateRange.start.getTime()) return null;
3664
4374
  } catch {
3665
4375
  }
3666
4376
  }
3667
- const entries = [];
3668
- let hasLines = false;
3669
- for await (const line of readSessionLines(filePath)) {
3670
- hasLines = true;
3671
- const entry = parseJsonlLine(line);
3672
- if (entry) entries.push(entry);
3673
- }
3674
- if (!hasLines) return null;
3675
- if (entries.length === 0) return null;
4377
+ const rawTurns = await cachedFileParse(filePath, "claude-turns", () => parseTurns(filePath));
3676
4378
  const sessionId = basename8(filePath, ".jsonl");
3677
- let turns = groupIntoTurns(entries, seenMsgIds);
4379
+ let turns = [];
4380
+ for (const turn of rawTurns) {
4381
+ const calls = turn.assistantCalls.filter((call) => {
4382
+ chargeRecord();
4383
+ if (seenMsgIds.has(call.deduplicationKey)) return false;
4384
+ seenMsgIds.add(call.deduplicationKey);
4385
+ return true;
4386
+ });
4387
+ if (calls.length) turns.push({ ...turn, ...compactUserMessage(turn.userMessage, turn.userMessageSignals), assistantCalls: calls });
4388
+ }
3678
4389
  if (dateRange) {
3679
4390
  turns = turns.filter((turn) => {
3680
4391
  if (turn.assistantCalls.length === 0) return false;
@@ -3688,15 +4399,15 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
3688
4399
  const classified = turns.map(classifyTurn);
3689
4400
  return buildSessionSummary(sessionId, project, classified);
3690
4401
  }
3691
- async function collectJsonlFiles(dirPath) {
3692
- const files = await readdir8(dirPath).catch(() => []);
3693
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => join16(dirPath, f));
4402
+ async function collectJsonlFiles2(dirPath) {
4403
+ const files = await readdir10(dirPath).catch(() => []);
4404
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => join18(dirPath, f));
3694
4405
  for (const entry of files) {
3695
4406
  if (entry.endsWith(".jsonl")) continue;
3696
- const subagentsPath = join16(dirPath, entry, "subagents");
3697
- const subFiles = await readdir8(subagentsPath).catch(() => []);
4407
+ const subagentsPath = join18(dirPath, entry, "subagents");
4408
+ const subFiles = await readdir10(subagentsPath).catch(() => []);
3698
4409
  for (const sf of subFiles) {
3699
- if (sf.endsWith(".jsonl")) jsonlFiles.push(join16(subagentsPath, sf));
4410
+ if (sf.endsWith(".jsonl")) jsonlFiles.push(join18(subagentsPath, sf));
3700
4411
  }
3701
4412
  }
3702
4413
  return jsonlFiles;
@@ -3707,14 +4418,14 @@ async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
3707
4418
  let indexDirty = false;
3708
4419
  const allJsonlPaths = /* @__PURE__ */ new Set();
3709
4420
  for (const { path: dirPath, name: dirName } of dirs) {
3710
- const jsonlFiles = await collectJsonlFiles(dirPath);
4421
+ const jsonlFiles = await collectJsonlFiles2(dirPath);
3711
4422
  for (const f of jsonlFiles) allJsonlPaths.add(f);
3712
4423
  for (const filePath of jsonlFiles) {
3713
4424
  const action = await checkSessionFile(filePath, sessionIndex, dateRange);
3714
4425
  if (action === "skip") continue;
3715
4426
  let preStat = null;
3716
4427
  try {
3717
- const s = await stat11(filePath);
4428
+ const s = await stat12(filePath);
3718
4429
  preStat = { size: s.size, mtimeMs: s.mtimeMs };
3719
4430
  } catch {
3720
4431
  }
@@ -3722,10 +4433,9 @@ async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
3722
4433
  const hasApiCalls = session !== null && session.apiCalls > 0;
3723
4434
  if (preStat && (hasApiCalls || !dateRange)) {
3724
4435
  try {
3725
- const postStat = await stat11(filePath);
4436
+ const postStat = await stat12(filePath);
3726
4437
  if (postStat.size === preStat.size && postStat.mtimeMs === preStat.mtimeMs) {
3727
- recordParseResult(filePath, sessionIndex, preStat.size, preStat.mtimeMs, hasApiCalls);
3728
- indexDirty = true;
4438
+ indexDirty = recordParseResult(filePath, sessionIndex, preStat.size, preStat.mtimeMs, hasApiCalls) || indexDirty;
3729
4439
  }
3730
4440
  } catch {
3731
4441
  }
@@ -3777,7 +4487,7 @@ function providerCallToTurn(call) {
3777
4487
  deduplicationKey: call.deduplicationKey
3778
4488
  };
3779
4489
  return {
3780
- userMessage: call.userMessage,
4490
+ ...compactUserMessage(call.userMessage, call.userMessageSignals),
3781
4491
  assistantCalls: [apiCall],
3782
4492
  timestamp: call.timestamp,
3783
4493
  sessionId: call.sessionId
@@ -3787,19 +4497,36 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3787
4497
  const provider = await getProvider(providerName);
3788
4498
  if (!provider) return [];
3789
4499
  const sessionMap = /* @__PURE__ */ new Map();
4500
+ const compactSessions = /* @__PURE__ */ new Map();
3790
4501
  for (const source of sources) {
3791
4502
  if (dateRange) {
3792
4503
  try {
3793
- const s = await stat11(source.path);
3794
- if (s.mtimeMs < dateRange.start.getTime()) continue;
4504
+ const s = await stat12(source.path);
4505
+ const wal = await stat12(`${source.path}-wal`).catch(() => null);
4506
+ if (Math.max(s.mtimeMs, wal?.mtimeMs ?? 0) < dateRange.start.getTime()) continue;
3795
4507
  } catch {
3796
4508
  }
3797
4509
  }
3798
- const parser = provider.createSessionParser(
3799
- { path: source.path, project: source.project, provider: providerName },
3800
- seenKeys
3801
- );
3802
- for await (const call of parser.parse()) {
4510
+ const warnings = [];
4511
+ const calls = await cachedFileParse(source.path, providerName, async () => {
4512
+ const parser = provider.createSessionParser(
4513
+ { path: source.path, project: source.project, provider: providerName },
4514
+ /* @__PURE__ */ new Set()
4515
+ );
4516
+ const result = [];
4517
+ for await (const call of parser.parse()) {
4518
+ chargeRecord();
4519
+ result.push({ ...call, ...compactUserMessage(call.userMessage, call.userMessageSignals) });
4520
+ }
4521
+ if (parser.warnings?.length) warnings.push(...parser.warnings);
4522
+ return result;
4523
+ }, warnings);
4524
+ _parseWarnings.push(...warnings);
4525
+ const touched = /* @__PURE__ */ new Set();
4526
+ for (const call of calls) {
4527
+ chargeRecord();
4528
+ if (seenKeys.has(call.deduplicationKey)) continue;
4529
+ seenKeys.add(call.deduplicationKey);
3803
4530
  if (dateRange) {
3804
4531
  if (!call.timestamp) continue;
3805
4532
  const ts = new Date(call.timestamp);
@@ -3808,6 +4535,7 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3808
4535
  const turn = providerCallToTurn(call);
3809
4536
  const classified = classifyTurn(turn);
3810
4537
  const key = `${providerName}:${call.sessionId}:${source.project}`;
4538
+ touched.add(key);
3811
4539
  const existing = sessionMap.get(key);
3812
4540
  if (existing) {
3813
4541
  existing.turns.push(classified);
@@ -3815,11 +4543,23 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3815
4543
  sessionMap.set(key, { project: source.project, turns: [classified] });
3816
4544
  }
3817
4545
  }
3818
- if (parser.warnings && parser.warnings.length > 0) {
3819
- _parseWarnings.push(...parser.warnings);
4546
+ if (reportMode.getStore()) {
4547
+ for (const key of touched) {
4548
+ const entry = sessionMap.get(key);
4549
+ const summary = buildSessionSummary(key.split(":")[1] ?? key, entry.project, entry.turns);
4550
+ const previous = compactSessions.get(key);
4551
+ if (previous) mergeReportSession(previous, summary);
4552
+ else compactSessions.set(key, summary);
4553
+ sessionMap.delete(key);
4554
+ }
3820
4555
  }
3821
4556
  }
3822
4557
  const projectMap = /* @__PURE__ */ new Map();
4558
+ for (const summary of compactSessions.values()) {
4559
+ const existing = projectMap.get(summary.project) ?? [];
4560
+ existing.push(summary);
4561
+ projectMap.set(summary.project, existing);
4562
+ }
3823
4563
  for (const [key, { project, turns }] of sessionMap) {
3824
4564
  const sessionId = key.split(":")[1] ?? key;
3825
4565
  const session = buildSessionSummary(sessionId, project, turns);
@@ -3842,7 +4582,7 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3842
4582
  return projects;
3843
4583
  }
3844
4584
  var CACHE_TTL_MS3 = 6e4;
3845
- var MAX_CACHE_ENTRIES = 10;
4585
+ var MAX_CACHE_ENTRIES = 1;
3846
4586
  var sessionCache = /* @__PURE__ */ new Map();
3847
4587
  var inFlightSourceContexts = /* @__PURE__ */ new Map();
3848
4588
  var SOURCE_CONTEXT_TTL_MS = 5e3;
@@ -3881,8 +4621,8 @@ async function getSourceContext(providerFilter) {
3881
4621
  }
3882
4622
  const promise = (async () => {
3883
4623
  const sources = await discoverAllSessions(providerFilter);
3884
- const fingerprint = buildCacheFingerprint(sources);
3885
- return { sources, fingerprint };
4624
+ const fingerprint2 = buildCacheFingerprint(sources);
4625
+ return { sources, fingerprint: fingerprint2 };
3886
4626
  })();
3887
4627
  inFlightSourceContexts.set(key, promise);
3888
4628
  try {
@@ -3924,13 +4664,17 @@ function filterProjectsByName(projects, include, exclude) {
3924
4664
  }
3925
4665
  return result;
3926
4666
  }
3927
- async function parseAllSessions(dateRange, providerFilter) {
4667
+ async function parseAllSessions(dateRange, providerFilter, summaryOnly = false, signal) {
4668
+ return reportMode.run(summaryOnly, () => withScanBudget(() => parseAllSessionsWithinBudget(dateRange, providerFilter), summaryOnly ? 2e6 : 2e5, signal, (summaryOnly ? 8 : 1) * 1024 ** 3));
4669
+ }
4670
+ async function parseAllSessionsWithinBudget(dateRange, providerFilter) {
3928
4671
  _parseWarnings = [];
3929
4672
  const sourceContext = await getSourceContext(providerFilter);
3930
4673
  const allSources = sourceContext.sources;
3931
- const key = cacheKey(dateRange, providerFilter, sourceContext.fingerprint);
4674
+ const key = `${reportMode.getStore() ? "summary" : "full"}:${cacheKey(dateRange, providerFilter, sourceContext.fingerprint)}`;
3932
4675
  const cached = sessionCache.get(key);
3933
4676
  if (cached && Date.now() - cached.ts < CACHE_TTL_MS3) return cached.data;
4677
+ sessionCache.clear();
3934
4678
  const seenMsgIds = /* @__PURE__ */ new Set();
3935
4679
  const seenKeys = /* @__PURE__ */ new Set();
3936
4680
  const claudeSources = allSources.filter((s) => s.provider === "claude");
@@ -3963,6 +4707,9 @@ async function parseAllSessions(dateRange, providerFilter) {
3963
4707
  cachePut(key, result);
3964
4708
  return result;
3965
4709
  }
4710
+ function getDiscoveredProviderNames() {
4711
+ return [...new Set([...resolvedSourceContexts.values()].flatMap((c) => c.result.sources.map((s) => s.provider)))];
4712
+ }
3966
4713
 
3967
4714
  // src/format.ts
3968
4715
  import chalk from "chalk";
@@ -4009,13 +4756,14 @@ function renderStatusBar(projects) {
4009
4756
  }
4010
4757
 
4011
4758
  // src/menubar-json.ts
4012
- import { readdirSync, readFileSync as readFileSync2 } from "fs";
4013
- import { join as join17, dirname as dirname3 } from "path";
4759
+ import { readdirSync, readFileSync } from "fs";
4760
+ import { join as join19, dirname as dirname4 } from "path";
4761
+ init_models();
4014
4762
  import { fileURLToPath } from "url";
4015
4763
  function getCliVersion() {
4016
4764
  try {
4017
- const pkgPath = join17(dirname3(fileURLToPath(import.meta.url)), "..", "package.json");
4018
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
4765
+ const pkgPath = join19(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
4766
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
4019
4767
  return pkg.version ?? "0.0.0";
4020
4768
  } catch {
4021
4769
  return "0.0.0";
@@ -4103,7 +4851,7 @@ function buildTopActivities(categories) {
4103
4851
  }));
4104
4852
  }
4105
4853
  function buildTopModels(models) {
4106
- return models.filter((m) => m.name !== SYNTHETIC_MODEL_NAME).slice(0, TOP_MODELS_LIMIT).map((m) => ({ name: m.name, cost: m.cost, calls: m.calls }));
4854
+ return models.filter((m) => m.name !== SYNTHETIC_MODEL_NAME).slice(0, TOP_MODELS_LIMIT).map((m) => ({ name: getShortModelName(m.name), cost: m.cost, calls: m.calls }));
4107
4855
  }
4108
4856
  function buildOptimize(optimize) {
4109
4857
  if (!optimize || optimize.findings.length === 0) {
@@ -4167,18 +4915,18 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
4167
4915
 
4168
4916
  // src/daily-cache.ts
4169
4917
  init_cache_dir();
4170
- import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
4918
+ import { createHash as createHash4, randomBytes as randomBytes6 } from "crypto";
4171
4919
  import { existsSync as existsSync4 } from "fs";
4172
- import { mkdir as mkdir8, open as open6, readFile as readFile8, rename as rename7, unlink as unlink4 } from "fs/promises";
4173
- import { join as join18 } from "path";
4920
+ import { mkdir as mkdir10, open as open8, readFile as readFile10, rename as rename8, unlink as unlink6 } from "fs/promises";
4921
+ import { join as join20 } from "path";
4174
4922
  var DAILY_CACHE_VERSION = 6;
4175
4923
  var DEFAULT_DAILY_CACHE_SCOPE = "global";
4176
4924
  var DAILY_CACHE_FILENAME = "daily-cache.json";
4177
4925
  function getCachePath3(scopeKey) {
4178
4926
  if (scopeKey === DEFAULT_DAILY_CACHE_SCOPE) {
4179
- return join18(getCacheDir(), DAILY_CACHE_FILENAME);
4927
+ return join20(getCacheDir(), DAILY_CACHE_FILENAME);
4180
4928
  }
4181
- return join18(getCacheDir(), `daily-cache-${scopeKey}.json`);
4929
+ return join20(getCacheDir(), `daily-cache-${scopeKey}.json`);
4182
4930
  }
4183
4931
  function emptyCache(scopeKey) {
4184
4932
  return { version: DAILY_CACHE_VERSION, scopeKey, lastComputedDate: null, days: [] };
@@ -4193,7 +4941,7 @@ function buildDailyCacheScopeKey(include, exclude) {
4193
4941
  return DEFAULT_DAILY_CACHE_SCOPE;
4194
4942
  }
4195
4943
  const raw = JSON.stringify({ include: normalizedInclude, exclude: normalizedExclude });
4196
- return `scope-${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
4944
+ return `scope-${createHash4("sha1").update(raw).digest("hex").slice(0, 12)}`;
4197
4945
  }
4198
4946
  function isValidCache(parsed) {
4199
4947
  if (!parsed || typeof parsed !== "object") return false;
@@ -4207,7 +4955,7 @@ async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
4207
4955
  const path = getCachePath3(scopeKey);
4208
4956
  if (!existsSync4(path)) return emptyCache(scopeKey);
4209
4957
  try {
4210
- const raw = await readFile8(path, "utf-8");
4958
+ const raw = await readFile10(path, "utf-8");
4211
4959
  const parsed = JSON.parse(raw);
4212
4960
  if (!isValidCache(parsed)) return emptyCache(scopeKey);
4213
4961
  if (parsed.scopeKey !== scopeKey) return emptyCache(scopeKey);
@@ -4218,12 +4966,13 @@ async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
4218
4966
  }
4219
4967
  async function saveDailyCache(cache) {
4220
4968
  const dir = getCacheDir();
4221
- if (!existsSync4(dir)) await mkdir8(dir, { recursive: true });
4969
+ if (!existsSync4(dir)) await mkdir10(dir, { recursive: true });
4222
4970
  const scopeKey = cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE;
4223
4971
  const finalPath = getCachePath3(scopeKey);
4224
4972
  const tempPath = `${finalPath}.${randomBytes6(8).toString("hex")}.tmp`;
4225
4973
  const payload = JSON.stringify({ ...cache, scopeKey });
4226
- const handle = await open6(tempPath, "w", 384);
4974
+ if (await readFile10(finalPath, "utf8").catch(() => "") === payload) return;
4975
+ const handle = await open8(tempPath, "w", 384);
4227
4976
  try {
4228
4977
  await handle.writeFile(payload, { encoding: "utf-8" });
4229
4978
  await handle.sync();
@@ -4231,10 +4980,10 @@ async function saveDailyCache(cache) {
4231
4980
  await handle.close();
4232
4981
  }
4233
4982
  try {
4234
- await rename7(tempPath, finalPath);
4983
+ await rename8(tempPath, finalPath);
4235
4984
  } catch (err) {
4236
4985
  try {
4237
- await unlink4(tempPath);
4986
+ await unlink6(tempPath);
4238
4987
  } catch {
4239
4988
  }
4240
4989
  throw err;
@@ -4252,7 +5001,8 @@ function addNewDays(cache, incoming, newestDate, options = {}) {
4252
5001
  version: DAILY_CACHE_VERSION,
4253
5002
  scopeKey: cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE,
4254
5003
  lastComputedDate: nextLast,
4255
- days: merged
5004
+ days: merged,
5005
+ revalidatedAt: cache.revalidatedAt
4256
5006
  };
4257
5007
  }
4258
5008
  function getDaysInRange(cache, start, end) {
@@ -4265,17 +5015,62 @@ function withDailyCacheLock(fn) {
4265
5015
  return next;
4266
5016
  }
4267
5017
 
5018
+ // src/latest-load.ts
5019
+ var LatestLoad = class {
5020
+ tail = Promise.resolve();
5021
+ generation = 0;
5022
+ controller;
5023
+ pending = 0;
5024
+ get busy() {
5025
+ return this.pending > 0;
5026
+ }
5027
+ cancel() {
5028
+ this.generation++;
5029
+ this.controller?.abort();
5030
+ }
5031
+ async run(load) {
5032
+ const generation = ++this.generation;
5033
+ this.controller?.abort();
5034
+ const previous = this.tail;
5035
+ this.pending++;
5036
+ const next = (async () => {
5037
+ await previous.catch(() => {
5038
+ });
5039
+ if (generation !== this.generation) return void 0;
5040
+ const controller = new AbortController();
5041
+ this.controller = controller;
5042
+ try {
5043
+ const result = await load(controller.signal);
5044
+ return generation === this.generation ? result : void 0;
5045
+ } catch (error) {
5046
+ if (generation !== this.generation) return void 0;
5047
+ throw error;
5048
+ } finally {
5049
+ if (this.controller === controller) this.controller = void 0;
5050
+ }
5051
+ })();
5052
+ this.tail = next;
5053
+ try {
5054
+ return await next;
5055
+ } finally {
5056
+ this.pending--;
5057
+ }
5058
+ }
5059
+ };
5060
+
4268
5061
  // src/dashboard.tsx
5062
+ init_resource_budget();
4269
5063
  import { homedir as homedir13 } from "os";
4270
5064
  import { useState as useState2, useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
4271
5065
  import { render as render2, Box as Box2, Text as Text2, useInput as useInput2, useApp as useApp2, useWindowSize } from "ink";
4272
5066
  init_models();
4273
5067
 
4274
5068
  // src/optimize.ts
5069
+ init_fs_utils();
4275
5070
  import chalk2 from "chalk";
4276
- import { readdir as readdir9, stat as stat12 } from "fs/promises";
5071
+ import { readdir as readdir11, stat as stat13 } from "fs/promises";
4277
5072
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4278
- import { basename as basename9, join as join19 } from "path";
5073
+ import { basename as basename9, join as join21 } from "path";
4279
5074
  import { homedir as homedir11 } from "os";
4280
5075
  var ORANGE = "#F59E0B";
4281
5076
  var DIM = "#666666";
@@ -4350,22 +5145,22 @@ var SHELL_PROFILES = [".zshrc", ".bashrc", ".bash_profile", ".profile"];
4350
5145
  var TOP_ITEMS_PREVIEW = 3;
4351
5146
  var GHOST_NAMES_PREVIEW = 5;
4352
5147
  var GHOST_CLEANUP_COMMANDS_LIMIT = 10;
4353
- var FILE_READ_CONCURRENCY = 16;
5148
+ var FILE_READ_CONCURRENCY = 1;
4354
5149
  var RESULT_CACHE_TTL_MS = 6e4;
4355
5150
  var RECENT_WINDOW_HOURS = 48;
4356
5151
  var RECENT_WINDOW_MS = RECENT_WINDOW_HOURS * 60 * 60 * 1e3;
4357
5152
  var DEFAULT_TREND_PERIOD_DAYS = 30;
4358
5153
  var DEFAULT_TREND_PERIOD_MS = DEFAULT_TREND_PERIOD_DAYS * 24 * 60 * 60 * 1e3;
4359
5154
  var IMPROVING_THRESHOLD = 0.5;
4360
- async function collectJsonlFiles2(dirPath) {
4361
- const files = await readdir9(dirPath).catch(() => []);
4362
- const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join19(dirPath, f));
5155
+ async function collectJsonlFiles3(dirPath) {
5156
+ const files = await readdir11(dirPath).catch(() => []);
5157
+ const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join21(dirPath, f));
4363
5158
  for (const entry of files) {
4364
5159
  if (entry.endsWith(".jsonl")) continue;
4365
- const subPath = join19(dirPath, entry, "subagents");
4366
- const subFiles = await readdir9(subPath).catch(() => []);
5160
+ const subPath = join21(dirPath, entry, "subagents");
5161
+ const subFiles = await readdir11(subPath).catch(() => []);
4367
5162
  for (const sf of subFiles) {
4368
- if (sf.endsWith(".jsonl")) result.push(join19(subPath, sf));
5163
+ if (sf.endsWith(".jsonl")) result.push(join21(subPath, sf));
4369
5164
  }
4370
5165
  }
4371
5166
  return result;
@@ -4373,7 +5168,7 @@ async function collectJsonlFiles2(dirPath) {
4373
5168
  async function isFileStaleForRange(filePath, range) {
4374
5169
  if (!range) return false;
4375
5170
  try {
4376
- const s = await stat12(filePath);
5171
+ const s = await stat13(filePath);
4377
5172
  return s.mtimeMs < range.start.getTime();
4378
5173
  } catch {
4379
5174
  return false;
@@ -4466,7 +5261,7 @@ async function scanSessions(dateRange) {
4466
5261
  const allUserMessages = [];
4467
5262
  const tasks = [];
4468
5263
  for (const source of sources) {
4469
- const files = await collectJsonlFiles2(source.path);
5264
+ const files = await collectJsonlFiles3(source.path);
4470
5265
  for (const file of files) {
4471
5266
  if (await isFileStaleForRange(file, dateRange)) continue;
4472
5267
  tasks.push({ file, project: source.project });
@@ -4503,13 +5298,13 @@ function isReadTool(name) {
4503
5298
  function loadMcpConfigs(projectCwds) {
4504
5299
  const servers = /* @__PURE__ */ new Map();
4505
5300
  const configPaths = [
4506
- join19(homedir11(), ".claude", "settings.json"),
4507
- join19(homedir11(), ".claude", "settings.local.json")
5301
+ join21(homedir11(), ".claude", "settings.json"),
5302
+ join21(homedir11(), ".claude", "settings.local.json")
4508
5303
  ];
4509
5304
  for (const cwd of projectCwds) {
4510
- configPaths.push(join19(cwd, ".mcp.json"));
4511
- configPaths.push(join19(cwd, ".claude", "settings.json"));
4512
- configPaths.push(join19(cwd, ".claude", "settings.local.json"));
5305
+ configPaths.push(join21(cwd, ".mcp.json"));
5306
+ configPaths.push(join21(cwd, ".claude", "settings.json"));
5307
+ configPaths.push(join21(cwd, ".claude", "settings.local.json"));
4513
5308
  }
4514
5309
  for (const p of configPaths) {
4515
5310
  if (!existsSync5(p)) continue;
@@ -4662,12 +5457,12 @@ function expandImports(filePath, seen, depth) {
4662
5457
  if (content === null) return { totalLines: 0, importedFiles: 0 };
4663
5458
  let totalLines = content.split("\n").length;
4664
5459
  let importedFiles = 0;
4665
- const dir = join19(filePath, "..");
5460
+ const dir = join21(filePath, "..");
4666
5461
  IMPORT_PATTERN.lastIndex = 0;
4667
5462
  for (const match of content.matchAll(IMPORT_PATTERN)) {
4668
5463
  const rawPath = match[1];
4669
5464
  if (!rawPath) continue;
4670
- const resolved = rawPath.startsWith("/") ? rawPath : join19(dir, rawPath);
5465
+ const resolved = rawPath.startsWith("/") ? rawPath : join21(dir, rawPath);
4671
5466
  if (!existsSync5(resolved)) continue;
4672
5467
  const nested = expandImports(resolved, seen, depth + 1);
4673
5468
  totalLines += nested.totalLines;
@@ -4679,7 +5474,7 @@ function detectBloatedClaudeMd(projectCwds) {
4679
5474
  const bloated = [];
4680
5475
  for (const cwd of projectCwds) {
4681
5476
  for (const name of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
4682
- const fullPath = join19(cwd, name);
5477
+ const fullPath = join21(cwd, name);
4683
5478
  if (!existsSync5(fullPath)) continue;
4684
5479
  const { totalLines, importedFiles } = expandImports(fullPath, /* @__PURE__ */ new Set(), 0);
4685
5480
  if (totalLines > CLAUDEMD_HEALTHY_LINES) {
@@ -4810,7 +5605,7 @@ function detectCacheBloat(apiCalls, projects, dateRange) {
4810
5605
  async function listMarkdownFiles(dir) {
4811
5606
  if (!existsSync5(dir)) return [];
4812
5607
  try {
4813
- const entries = await readdir9(dir);
5608
+ const entries = await readdir11(dir);
4814
5609
  return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(/\.md$/, ""));
4815
5610
  } catch {
4816
5611
  return [];
@@ -4819,10 +5614,10 @@ async function listMarkdownFiles(dir) {
4819
5614
  async function listSkillDirs(dir) {
4820
5615
  if (!existsSync5(dir)) return [];
4821
5616
  try {
4822
- const entries = await readdir9(dir);
5617
+ const entries = await readdir11(dir);
4823
5618
  const names = [];
4824
5619
  for (const entry of entries) {
4825
- if (existsSync5(join19(dir, entry, "SKILL.md"))) names.push(entry);
5620
+ if (existsSync5(join21(dir, entry, "SKILL.md"))) names.push(entry);
4826
5621
  }
4827
5622
  return names;
4828
5623
  } catch {
@@ -4830,7 +5625,7 @@ async function listSkillDirs(dir) {
4830
5625
  }
4831
5626
  }
4832
5627
  async function detectGhostAgents(calls) {
4833
- const defined = await listMarkdownFiles(join19(homedir11(), ".claude", "agents"));
5628
+ const defined = await listMarkdownFiles(join21(homedir11(), ".claude", "agents"));
4834
5629
  if (defined.length === 0) return null;
4835
5630
  const invoked = /* @__PURE__ */ new Set();
4836
5631
  for (const call of calls) {
@@ -4855,7 +5650,7 @@ async function detectGhostAgents(calls) {
4855
5650
  };
4856
5651
  }
4857
5652
  async function detectGhostSkills(calls) {
4858
- const defined = await listSkillDirs(join19(homedir11(), ".claude", "skills"));
5653
+ const defined = await listSkillDirs(join21(homedir11(), ".claude", "skills"));
4859
5654
  if (defined.length === 0) return null;
4860
5655
  const invoked = /* @__PURE__ */ new Set();
4861
5656
  for (const call of calls) {
@@ -4880,7 +5675,7 @@ async function detectGhostSkills(calls) {
4880
5675
  };
4881
5676
  }
4882
5677
  async function detectGhostCommands(userMessages) {
4883
- const defined = await listMarkdownFiles(join19(homedir11(), ".claude", "commands"));
5678
+ const defined = await listMarkdownFiles(join21(homedir11(), ".claude", "commands"));
4884
5679
  if (defined.length === 0) return null;
4885
5680
  const invoked = /* @__PURE__ */ new Set();
4886
5681
  for (const msg of userMessages) {
@@ -4908,7 +5703,7 @@ async function detectGhostCommands(userMessages) {
4908
5703
  }
4909
5704
  function readShellProfileLimit() {
4910
5705
  for (const profile of SHELL_PROFILES) {
4911
- const path = join19(homedir11(), profile);
5706
+ const path = join21(homedir11(), profile);
4912
5707
  if (!existsSync5(path)) continue;
4913
5708
  const content = readSessionFileSync(path);
4914
5709
  if (content === null) continue;
@@ -4991,8 +5786,8 @@ function computeInputCostRate(projects) {
4991
5786
  var resultCache = /* @__PURE__ */ new Map();
4992
5787
  function cacheKey2(projects, dateRange) {
4993
5788
  const dr = dateRange ? `${dateRange.start.getTime()}-${dateRange.end.getTime()}` : "all";
4994
- const fingerprint = projects.length + ":" + projects.reduce((s, p) => s + p.totalApiCalls, 0);
4995
- return `${dr}:${fingerprint}`;
5789
+ const fingerprint2 = projects.length + ":" + projects.reduce((s, p) => s + p.totalApiCalls, 0);
5790
+ return `${dr}:${fingerprint2}`;
4996
5791
  }
4997
5792
  async function scanAndDetect(projects, dateRange, preScanned) {
4998
5793
  if (projects.length === 0) {
@@ -5129,436 +5924,134 @@ async function runOptimize(projects, periodLabel, dateRange, preScanned) {
5129
5924
  }
5130
5925
 
5131
5926
  // src/context-budget.ts
5132
- import { readdir as readdir10 } from "fs/promises";
5927
+ init_fs_utils();
5928
+ import { readdir as readdir12 } from "fs/promises";
5133
5929
  import { existsSync as existsSync6 } from "fs";
5134
- import { join as join20 } from "path";
5135
- import { homedir as homedir12 } from "os";
5136
- var CHARS_PER_TOKEN2 = 4;
5137
- var SYSTEM_BASE_TOKENS = 10400;
5138
- var TOOL_TOKENS_OVERHEAD = 400;
5139
- var SKILL_FRONTMATTER_TOKENS = 80;
5140
- function estimateTokens2(text) {
5141
- return Math.ceil(text.length / CHARS_PER_TOKEN2);
5142
- }
5143
- async function readConfigFile(path) {
5144
- if (!existsSync6(path)) return null;
5145
- const raw = await readSessionFile(path);
5146
- if (raw === null) return null;
5147
- try {
5148
- return JSON.parse(raw);
5149
- } catch {
5150
- return null;
5151
- }
5152
- }
5153
- async function countMcpTools(projectPath) {
5154
- const home = homedir12();
5155
- const configPaths = [
5156
- join20(home, ".claude", "settings.json"),
5157
- join20(home, ".claude", "settings.local.json")
5158
- ];
5159
- if (projectPath) {
5160
- configPaths.push(join20(projectPath, ".mcp.json"));
5161
- configPaths.push(join20(projectPath, ".claude", "settings.json"));
5162
- configPaths.push(join20(projectPath, ".claude", "settings.local.json"));
5163
- }
5164
- const servers = /* @__PURE__ */ new Set();
5165
- let toolCount = 0;
5166
- for (const p of configPaths) {
5167
- const config = await readConfigFile(p);
5168
- if (!config) continue;
5169
- const mcpServers = config.mcpServers ?? {};
5170
- for (const name of Object.keys(mcpServers)) {
5171
- if (servers.has(name)) continue;
5172
- servers.add(name);
5173
- toolCount += 5;
5174
- }
5175
- }
5176
- return toolCount;
5177
- }
5178
- async function countSkills(projectPath) {
5179
- const dirs = [join20(homedir12(), ".claude", "skills")];
5180
- if (projectPath) dirs.push(join20(projectPath, ".claude", "skills"));
5181
- let count = 0;
5182
- for (const dir of dirs) {
5183
- if (!existsSync6(dir)) continue;
5184
- try {
5185
- const entries = await readdir10(dir);
5186
- for (const entry of entries) {
5187
- const skillFile = join20(dir, entry, "SKILL.md");
5188
- if (existsSync6(skillFile)) count++;
5189
- }
5190
- } catch {
5191
- continue;
5192
- }
5193
- }
5194
- return count;
5195
- }
5196
- async function scanMemoryFiles(projectPath) {
5197
- const home = homedir12();
5198
- const files = [];
5199
- const paths = [
5200
- { path: join20(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
5201
- ];
5202
- if (projectPath) {
5203
- paths.push({ path: join20(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
5204
- paths.push({ path: join20(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
5205
- paths.push({ path: join20(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
5206
- }
5207
- for (const { path, name } of paths) {
5208
- if (!existsSync6(path)) continue;
5209
- const content = await readSessionFile(path);
5210
- if (content === null) continue;
5211
- files.push({ name, tokens: estimateTokens2(content) });
5212
- }
5213
- return files;
5214
- }
5215
- async function estimateContextBudget(projectPath, modelContext = 1e6) {
5216
- const mcpToolCount = await countMcpTools(projectPath);
5217
- const skillCount = await countSkills(projectPath);
5218
- const memoryFiles = await scanMemoryFiles(projectPath);
5219
- const mcpTokens = mcpToolCount * TOOL_TOKENS_OVERHEAD;
5220
- const skillTokens = skillCount * SKILL_FRONTMATTER_TOKENS;
5221
- const memoryTokens = memoryFiles.reduce((s, f) => s + f.tokens, 0);
5222
- const total = SYSTEM_BASE_TOKENS + mcpTokens + skillTokens + memoryTokens;
5223
- return {
5224
- systemBase: SYSTEM_BASE_TOKENS,
5225
- mcpTools: { count: mcpToolCount, tokens: mcpTokens },
5226
- skills: { count: skillCount, tokens: skillTokens },
5227
- memory: { count: memoryFiles.length, tokens: memoryTokens, files: memoryFiles },
5228
- total,
5229
- modelContext
5230
- };
5231
- }
5232
- async function discoverProjectCwd(sessionDir) {
5233
- let files;
5234
- try {
5235
- files = (await readdir10(sessionDir)).filter((f) => f.endsWith(".jsonl"));
5236
- } catch {
5237
- return null;
5238
- }
5239
- if (files.length === 0) return null;
5240
- const firstFile = files[0];
5241
- if (!firstFile) return null;
5242
- const content = await readSessionFile(join20(sessionDir, firstFile));
5243
- if (content === null) return null;
5244
- for (const line of content.split("\n")) {
5245
- if (!line.trim()) continue;
5246
- try {
5247
- const entry = JSON.parse(line);
5248
- if (entry.cwd && typeof entry.cwd === "string") return entry.cwd;
5249
- } catch {
5250
- continue;
5251
- }
5252
- }
5253
- return null;
5254
- }
5255
-
5256
- // src/compare.tsx
5257
- import React, { useState, useEffect, useRef } from "react";
5258
- import { render, Box, Text, useInput, useApp, useStdout } from "ink";
5259
-
5260
- // src/compare-stats.ts
5261
- import { readdir as readdir11, readFile as readFile9 } from "fs/promises";
5262
- import { join as join21 } from "path";
5263
- var PLANNING_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "EnterPlanMode", "ExitPlanMode"]);
5264
- function aggregateModelStats(projects) {
5265
- const byModel = /* @__PURE__ */ new Map();
5266
- const ensure = (model) => {
5267
- let s = byModel.get(model);
5268
- if (!s) {
5269
- s = { model, calls: 0, cost: 0, outputTokens: 0, inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTurns: 0, editTurns: 0, oneShotTurns: 0, retries: 0, selfCorrections: 0, editCost: 0, firstSeen: "", lastSeen: "" };
5270
- byModel.set(model, s);
5271
- }
5272
- return s;
5273
- };
5274
- for (const project of projects) {
5275
- for (const session of project.sessions) {
5276
- for (const turn of session.turns) {
5277
- if (turn.assistantCalls.length === 0) continue;
5278
- const primaryModel = turn.assistantCalls[0].model;
5279
- if (primaryModel === "<synthetic>") continue;
5280
- const ms = ensure(primaryModel);
5281
- ms.totalTurns++;
5282
- if (turn.hasEdits) {
5283
- ms.editTurns++;
5284
- if (turn.retries === 0) ms.oneShotTurns++;
5285
- for (const c of turn.assistantCalls) {
5286
- if (c.model !== "<synthetic>") ms.editCost += c.costUSD;
5287
- }
5288
- }
5289
- ms.retries += turn.retries;
5290
- for (const call of turn.assistantCalls) {
5291
- if (call.model === "<synthetic>") continue;
5292
- const cs = call.model === primaryModel ? ms : ensure(call.model);
5293
- cs.calls++;
5294
- cs.cost += call.costUSD;
5295
- cs.outputTokens += call.usage.outputTokens;
5296
- cs.inputTokens += call.usage.inputTokens;
5297
- cs.cacheReadTokens += call.usage.cacheReadInputTokens;
5298
- cs.cacheWriteTokens += call.usage.cacheCreationInputTokens;
5299
- if (!cs.firstSeen || call.timestamp < cs.firstSeen) cs.firstSeen = call.timestamp;
5300
- if (!cs.lastSeen || call.timestamp > cs.lastSeen) cs.lastSeen = call.timestamp;
5301
- }
5302
- }
5303
- }
5304
- }
5305
- return [...byModel.values()].sort((a, b) => b.cost - a.cost);
5306
- }
5307
- var METRICS = [
5308
- {
5309
- section: "Performance",
5310
- label: "One-shot rate",
5311
- formatFn: "percent",
5312
- higherIsBetter: true,
5313
- compute: (s) => s.editTurns > 0 ? s.oneShotTurns / s.editTurns * 100 : null
5314
- },
5315
- {
5316
- section: "Performance",
5317
- label: "Retry rate",
5318
- formatFn: "decimal",
5319
- higherIsBetter: false,
5320
- compute: (s) => s.editTurns > 0 ? s.retries / s.editTurns : null
5321
- },
5322
- {
5323
- section: "Performance",
5324
- label: "Self-correction",
5325
- formatFn: "percent",
5326
- higherIsBetter: false,
5327
- compute: (s) => s.totalTurns > 0 ? s.selfCorrections / s.totalTurns * 100 : null
5328
- },
5329
- {
5330
- section: "Efficiency",
5331
- label: "Cost / call",
5332
- formatFn: "cost",
5333
- higherIsBetter: false,
5334
- compute: (s) => s.calls > 0 ? s.cost / s.calls : null
5335
- },
5336
- {
5337
- section: "Efficiency",
5338
- label: "Cost / edit",
5339
- formatFn: "cost",
5340
- higherIsBetter: false,
5341
- compute: (s) => s.editTurns > 0 ? s.editCost / s.editTurns : null
5342
- },
5343
- {
5344
- section: "Efficiency",
5345
- label: "Output tok / call",
5346
- formatFn: "number",
5347
- higherIsBetter: false,
5348
- compute: (s) => s.calls > 0 ? Math.round(s.outputTokens / s.calls) : null
5349
- },
5350
- {
5351
- section: "Efficiency",
5352
- label: "Cache hit rate",
5353
- formatFn: "percent",
5354
- higherIsBetter: true,
5355
- compute: (s) => {
5356
- const total = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens;
5357
- return total > 0 ? s.cacheReadTokens / total * 100 : null;
5358
- }
5359
- }
5360
- ];
5361
- function pickWinner(valueA, valueB, higherIsBetter) {
5362
- if (valueA === null || valueB === null) return "none";
5363
- if (valueA === valueB) return "tie";
5364
- if (higherIsBetter) return valueA > valueB ? "a" : "b";
5365
- return valueA < valueB ? "a" : "b";
5366
- }
5367
- function computeComparison(a, b) {
5368
- return METRICS.map((m) => {
5369
- const valueA = m.compute(a);
5370
- const valueB = m.compute(b);
5371
- return {
5372
- section: m.section,
5373
- label: m.label,
5374
- valueA,
5375
- valueB,
5376
- formatFn: m.formatFn,
5377
- winner: pickWinner(valueA, valueB, m.higherIsBetter)
5378
- };
5379
- });
5380
- }
5381
- function computeCategoryComparison(projects, modelA, modelB) {
5382
- const mapA = /* @__PURE__ */ new Map();
5383
- const mapB = /* @__PURE__ */ new Map();
5384
- const ensure = (map, cat) => {
5385
- let a = map.get(cat);
5386
- if (!a) {
5387
- a = { turns: 0, editTurns: 0, oneShotTurns: 0 };
5388
- map.set(cat, a);
5389
- }
5390
- return a;
5391
- };
5392
- for (const project of projects) {
5393
- for (const session of project.sessions) {
5394
- for (const turn of session.turns) {
5395
- if (turn.assistantCalls.length === 0) continue;
5396
- const primary = turn.assistantCalls[0].model;
5397
- if (primary !== modelA && primary !== modelB) continue;
5398
- const acc = ensure(primary === modelA ? mapA : mapB, turn.category);
5399
- acc.turns++;
5400
- if (turn.hasEdits) {
5401
- acc.editTurns++;
5402
- if (turn.retries === 0) acc.oneShotTurns++;
5403
- }
5404
- }
5405
- }
5930
+ import { join as join22 } from "path";
5931
+ import { homedir as homedir12 } from "os";
5932
+ var CHARS_PER_TOKEN2 = 4;
5933
+ var SYSTEM_BASE_TOKENS = 10400;
5934
+ var TOOL_TOKENS_OVERHEAD = 400;
5935
+ var SKILL_FRONTMATTER_TOKENS = 80;
5936
+ function estimateTokens2(text) {
5937
+ return Math.ceil(text.length / CHARS_PER_TOKEN2);
5938
+ }
5939
+ async function readConfigFile(path) {
5940
+ if (!existsSync6(path)) return null;
5941
+ const raw = await readSessionFile(path);
5942
+ if (raw === null) return null;
5943
+ try {
5944
+ return JSON.parse(raw);
5945
+ } catch {
5946
+ return null;
5406
5947
  }
5407
- const allCats = /* @__PURE__ */ new Set([...mapA.keys(), ...mapB.keys()]);
5408
- const result = [];
5409
- for (const category of allCats) {
5410
- const a = mapA.get(category);
5411
- const b = mapB.get(category);
5412
- if ((!a || a.editTurns === 0) && (!b || b.editTurns === 0)) continue;
5413
- const rateA = a && a.editTurns > 0 ? a.oneShotTurns / a.editTurns * 100 : null;
5414
- const rateB = b && b.editTurns > 0 ? b.oneShotTurns / b.editTurns * 100 : null;
5415
- result.push({
5416
- category,
5417
- turnsA: a?.turns ?? 0,
5418
- editTurnsA: a?.editTurns ?? 0,
5419
- oneShotRateA: rateA,
5420
- turnsB: b?.turns ?? 0,
5421
- editTurnsB: b?.editTurns ?? 0,
5422
- oneShotRateB: rateB,
5423
- winner: pickWinner(rateA, rateB, true)
5424
- });
5948
+ }
5949
+ async function countMcpTools(projectPath) {
5950
+ const home = homedir12();
5951
+ const configPaths = [
5952
+ join22(home, ".claude", "settings.json"),
5953
+ join22(home, ".claude", "settings.local.json")
5954
+ ];
5955
+ if (projectPath) {
5956
+ configPaths.push(join22(projectPath, ".mcp.json"));
5957
+ configPaths.push(join22(projectPath, ".claude", "settings.json"));
5958
+ configPaths.push(join22(projectPath, ".claude", "settings.local.json"));
5425
5959
  }
5426
- return result.sort((a, b) => b.turnsA + b.turnsB - (a.turnsA + a.turnsB));
5960
+ const servers = /* @__PURE__ */ new Set();
5961
+ let toolCount = 0;
5962
+ for (const p of configPaths) {
5963
+ const config = await readConfigFile(p);
5964
+ if (!config) continue;
5965
+ const mcpServers = config.mcpServers ?? {};
5966
+ for (const name of Object.keys(mcpServers)) {
5967
+ if (servers.has(name)) continue;
5968
+ servers.add(name);
5969
+ toolCount += 5;
5970
+ }
5971
+ }
5972
+ return toolCount;
5427
5973
  }
5428
- function computeWorkingStyle(projects, modelA, modelB) {
5429
- const sA = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
5430
- const sB = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
5431
- for (const project of projects) {
5432
- for (const session of project.sessions) {
5433
- for (const turn of session.turns) {
5434
- if (turn.assistantCalls.length === 0) continue;
5435
- const primary = turn.assistantCalls[0].model;
5436
- if (primary !== modelA && primary !== modelB) continue;
5437
- const s = primary === modelA ? sA : sB;
5438
- s.totalTurns++;
5439
- const turnTools = turn.assistantCalls.flatMap((c) => c.tools);
5440
- if (turnTools.some((t) => PLANNING_TOOLS.has(t)) || turn.assistantCalls.some((c) => c.hasPlanMode)) {
5441
- s.planModeUses++;
5442
- }
5443
- for (const call of turn.assistantCalls) {
5444
- s.totalToolCalls += call.tools.length;
5445
- if (call.hasAgentSpawn) s.agentSpawns++;
5446
- if (call.speed === "fast") s.fastModeCalls++;
5447
- }
5974
+ async function countSkills(projectPath) {
5975
+ const dirs = [join22(homedir12(), ".claude", "skills")];
5976
+ if (projectPath) dirs.push(join22(projectPath, ".claude", "skills"));
5977
+ let count = 0;
5978
+ for (const dir of dirs) {
5979
+ if (!existsSync6(dir)) continue;
5980
+ try {
5981
+ const entries = await readdir12(dir);
5982
+ for (const entry of entries) {
5983
+ const skillFile = join22(dir, entry, "SKILL.md");
5984
+ if (existsSync6(skillFile)) count++;
5448
5985
  }
5986
+ } catch {
5987
+ continue;
5449
5988
  }
5450
5989
  }
5451
- const pct2 = (num, den) => den > 0 ? num / den * 100 : null;
5452
- const avg = (num, den) => den > 0 ? num / den : null;
5453
- return [
5454
- { label: "Delegation rate", valueA: pct2(sA.agentSpawns, sA.totalTurns), valueB: pct2(sB.agentSpawns, sB.totalTurns), formatFn: "percent" },
5455
- { label: "Planning rate", valueA: pct2(sA.planModeUses, sA.totalTurns), valueB: pct2(sB.planModeUses, sB.totalTurns), formatFn: "percent" },
5456
- { label: "Avg tools / turn", valueA: avg(sA.totalToolCalls, sA.totalTurns), valueB: avg(sB.totalToolCalls, sB.totalTurns), formatFn: "decimal" },
5457
- { label: "Fast mode usage", valueA: pct2(sA.fastModeCalls, sA.totalTurns), valueB: pct2(sB.fastModeCalls, sB.totalTurns), formatFn: "percent" }
5458
- ];
5459
- }
5460
- var SELF_CORRECTION_PATTERNS = [
5461
- /\bmy mistake\b/i,
5462
- /\bmy bad\b/i,
5463
- /\bmy apolog/i,
5464
- /\bI apologize\b/i,
5465
- /\bI was wrong\b/i,
5466
- /\bI was incorrect\b/i,
5467
- /\bI made (a |an )?(error|mistake)\b/i,
5468
- /\bI incorrectly\b/i,
5469
- /\bI mistakenly\b/i,
5470
- /\bthat was (incorrect|wrong|an error)\b/i,
5471
- /\blet me correct that\b/i,
5472
- /\bI need to correct\b/i,
5473
- /\byou're right[.,]? I/i,
5474
- /\bsorry about that\b/i
5475
- ];
5476
- function extractText(content) {
5477
- if (typeof content === "string") return content;
5478
- if (!Array.isArray(content)) return "";
5479
- return content.filter((b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ");
5480
- }
5481
- function isCompactFile(name) {
5482
- return name.includes("compact");
5990
+ return count;
5483
5991
  }
5484
- async function collectJsonlFiles3(sessionDir) {
5485
- const entries = await readdir11(sessionDir, { withFileTypes: true });
5992
+ async function scanMemoryFiles(projectPath) {
5993
+ const home = homedir12();
5486
5994
  const files = [];
5487
- for (const entry of entries) {
5488
- if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
5489
- files.push(join21(sessionDir, entry.name));
5490
- } else if (entry.isDirectory() && entry.name === "subagents") {
5491
- const subEntries = await readdir11(join21(sessionDir, entry.name), { withFileTypes: true });
5492
- for (const sub of subEntries) {
5493
- if (sub.isFile() && sub.name.endsWith(".jsonl") && !isCompactFile(sub.name)) {
5494
- files.push(join21(sessionDir, entry.name, sub.name));
5495
- }
5496
- }
5497
- }
5995
+ const paths = [
5996
+ { path: join22(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
5997
+ ];
5998
+ if (projectPath) {
5999
+ paths.push({ path: join22(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
6000
+ paths.push({ path: join22(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
6001
+ paths.push({ path: join22(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
6002
+ }
6003
+ for (const { path, name } of paths) {
6004
+ if (!existsSync6(path)) continue;
6005
+ const content = await readSessionFile(path);
6006
+ if (content === null) continue;
6007
+ files.push({ name, tokens: estimateTokens2(content) });
5498
6008
  }
5499
6009
  return files;
5500
6010
  }
5501
- async function scanSelfCorrections(projectDirs) {
5502
- const counts = /* @__PURE__ */ new Map();
5503
- const seen = /* @__PURE__ */ new Set();
5504
- for (const dir of projectDirs) {
5505
- let entries;
6011
+ async function estimateContextBudget(projectPath, modelContext = 1e6) {
6012
+ const mcpToolCount = await countMcpTools(projectPath);
6013
+ const skillCount = await countSkills(projectPath);
6014
+ const memoryFiles = await scanMemoryFiles(projectPath);
6015
+ const mcpTokens = mcpToolCount * TOOL_TOKENS_OVERHEAD;
6016
+ const skillTokens = skillCount * SKILL_FRONTMATTER_TOKENS;
6017
+ const memoryTokens = memoryFiles.reduce((s, f) => s + f.tokens, 0);
6018
+ const total = SYSTEM_BASE_TOKENS + mcpTokens + skillTokens + memoryTokens;
6019
+ return {
6020
+ systemBase: SYSTEM_BASE_TOKENS,
6021
+ mcpTools: { count: mcpToolCount, tokens: mcpTokens },
6022
+ skills: { count: skillCount, tokens: skillTokens },
6023
+ memory: { count: memoryFiles.length, tokens: memoryTokens, files: memoryFiles },
6024
+ total,
6025
+ modelContext
6026
+ };
6027
+ }
6028
+ async function discoverProjectCwd(sessionDir) {
6029
+ let files;
6030
+ try {
6031
+ files = (await readdir12(sessionDir)).filter((f) => f.endsWith(".jsonl"));
6032
+ } catch {
6033
+ return null;
6034
+ }
6035
+ if (files.length === 0) return null;
6036
+ const firstFile = files[0];
6037
+ if (!firstFile) return null;
6038
+ const content = await readSessionFile(join22(sessionDir, firstFile));
6039
+ if (content === null) return null;
6040
+ for (const line of content.split("\n")) {
6041
+ if (!line.trim()) continue;
5506
6042
  try {
5507
- entries = await readdir11(dir, { withFileTypes: true });
6043
+ const entry = JSON.parse(line);
6044
+ if (entry.cwd && typeof entry.cwd === "string") return entry.cwd;
5508
6045
  } catch {
5509
6046
  continue;
5510
6047
  }
5511
- const allFiles = [];
5512
- for (const entry of entries) {
5513
- if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
5514
- allFiles.push(join21(dir, entry.name));
5515
- } else if (entry.isDirectory()) {
5516
- try {
5517
- const sessionFiles = await collectJsonlFiles3(join21(dir, entry.name));
5518
- allFiles.push(...sessionFiles);
5519
- } catch {
5520
- continue;
5521
- }
5522
- }
5523
- }
5524
- for (const file of allFiles) {
5525
- let raw;
5526
- try {
5527
- raw = await readFile9(file, "utf8");
5528
- } catch {
5529
- continue;
5530
- }
5531
- for (const line of raw.split("\n")) {
5532
- const trimmed = line.trim();
5533
- if (!trimmed) continue;
5534
- let parsed;
5535
- try {
5536
- parsed = JSON.parse(trimmed);
5537
- } catch {
5538
- continue;
5539
- }
5540
- const rec = parsed;
5541
- if (!rec || typeof rec !== "object" || rec["type"] !== "assistant") continue;
5542
- const ts = rec["timestamp"];
5543
- const msg = rec["message"];
5544
- if (msg === null || typeof msg !== "object") continue;
5545
- const msgRec = msg;
5546
- const model = msgRec["model"];
5547
- if (typeof model !== "string" || model === "<synthetic>") continue;
5548
- const dedupeKey = `${model}:${ts}`;
5549
- if (seen.has(dedupeKey)) continue;
5550
- seen.add(dedupeKey);
5551
- const text = extractText(msgRec["content"]);
5552
- if (SELF_CORRECTION_PATTERNS.some((p) => p.test(text))) {
5553
- counts.set(model, (counts.get(model) ?? 0) + 1);
5554
- }
5555
- }
5556
- }
5557
6048
  }
5558
- return counts;
6049
+ return null;
5559
6050
  }
5560
6051
 
5561
6052
  // src/compare.tsx
6053
+ import React, { useState, useEffect, useRef } from "react";
6054
+ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
5562
6055
  import { jsx, jsxs } from "react/jsx-runtime";
5563
6056
  var ORANGE2 = "#F59E0B";
5564
6057
  var GREEN2 = "#16A34A";
@@ -6139,7 +6632,7 @@ function planDisplayName(id) {
6139
6632
  }
6140
6633
 
6141
6634
  // src/dashboard.tsx
6142
- import { join as join22 } from "path";
6635
+ import { join as join23 } from "path";
6143
6636
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
6144
6637
  var PERIODS = ["today", "week", "30days", "month", "all"];
6145
6638
  var PERIOD_LABELS = {
@@ -6388,6 +6881,13 @@ function DailyActivity({ projects, days = 14, pw, bw }) {
6388
6881
  const dailyCalls = {};
6389
6882
  for (const project of projects) {
6390
6883
  for (const session of project.sessions) {
6884
+ if (session.reportSummary) {
6885
+ for (const [day, row] of Object.entries(session.reportSummary.daily)) {
6886
+ dailyCosts[day] = (dailyCosts[day] ?? 0) + row.cost;
6887
+ dailyCalls[day] = (dailyCalls[day] ?? 0) + row.calls;
6888
+ }
6889
+ continue;
6890
+ }
6391
6891
  for (const turn of session.turns) {
6392
6892
  if (!turn.timestamp) continue;
6393
6893
  const day = dateKey(turn.timestamp);
@@ -6789,15 +7289,15 @@ function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable,
6789
7289
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " month " }),
6790
7290
  /* @__PURE__ */ jsx2(Text2, { color: ORANGE3, bold: true, children: "5" }),
6791
7291
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " all time" }),
6792
- !isOptimize && optimizeAvailable && findingCount != null && findingCount > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
7292
+ !isOptimize && optimizeAvailable && /* @__PURE__ */ jsxs2(Fragment, { children: [
6793
7293
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
6794
7294
  /* @__PURE__ */ jsx2(Text2, { color: ORANGE3, bold: true, children: "o" }),
6795
7295
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " optimize" }),
6796
- /* @__PURE__ */ jsxs2(Text2, { color: "#DC2626", children: [
7296
+ findingCount ? /* @__PURE__ */ jsxs2(Text2, { color: "#DC2626", children: [
6797
7297
  " (",
6798
7298
  findingCount,
6799
7299
  ")"
6800
- ] })
7300
+ ] }) : null
6801
7301
  ] }),
6802
7302
  !isOptimize && compareAvailable && /* @__PURE__ */ jsxs2(Fragment, { children: [
6803
7303
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
@@ -6851,6 +7351,10 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6851
7351
  const [projects, setProjects] = useState2([]);
6852
7352
  const [initialLoading, setInitialLoading] = useState2(true);
6853
7353
  const [loading, setLoading] = useState2(false);
7354
+ const [loadError2, setLoadError] = useState2(null);
7355
+ const loader = useRef2(new LatestLoad());
7356
+ const retryAt = useRef2(0);
7357
+ const failures = useRef2(0);
6854
7358
  const [activeProvider, setActiveProvider] = useState2(initialProvider);
6855
7359
  const [detectedProviders, setDetectedProviders] = useState2([]);
6856
7360
  const [view, setView] = useState2("dashboard");
@@ -6875,103 +7379,88 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6875
7379
  return () => clearInterval(id);
6876
7380
  }, [initialLoading]);
6877
7381
  useEffect2(() => {
6878
- let cancelled = false;
6879
- async function boot() {
6880
- await loadPricing();
6881
- if (cancelled) return;
6882
- const range = getDateRange(initialPeriod);
6883
- const data = await parseAllSessions(range, initialProvider);
6884
- if (cancelled) return;
6885
- const filtered = filterProjectsByName(data, projectFilter, excludeFilter);
6886
- setProjects(filtered);
6887
- const usage = await getPlanUsageOrNull();
6888
- if (cancelled) return;
6889
- setPlanUsage(usage ?? void 0);
6890
- setInitialLoading(false);
6891
- }
6892
- boot();
6893
- return () => {
6894
- cancelled = true;
6895
- };
6896
- }, []);
6897
- useEffect2(() => {
6898
- let cancelled = false;
6899
- async function detect() {
6900
- const found = [];
6901
- for (const p of await getAllProviders()) {
6902
- const s = await p.discoverSessions();
6903
- if (s.length > 0) found.push(p.name);
6904
- }
6905
- if (!cancelled) setDetectedProviders(found);
6906
- }
6907
- detect();
7382
+ void reloadData(initialPeriod, initialProvider);
6908
7383
  return () => {
6909
- cancelled = true;
7384
+ loader.current.cancel();
7385
+ if (debounceRef.current) clearTimeout(debounceRef.current);
6910
7386
  };
6911
7387
  }, []);
6912
7388
  useEffect2(() => {
6913
7389
  let cancelled = false;
6914
7390
  async function loadBudgets() {
6915
- const claudeDir = join22(homedir13(), ".claude", "projects");
7391
+ const claudeDir = join23(homedir13(), ".claude", "projects");
6916
7392
  const budgets = /* @__PURE__ */ new Map();
6917
7393
  for (const project of projects.slice(0, 8)) {
6918
7394
  if (cancelled) return;
6919
- const cwd = await discoverProjectCwd(join22(claudeDir, project.project));
7395
+ const cwd = await discoverProjectCwd(join23(claudeDir, project.project));
6920
7396
  if (!cwd) continue;
6921
7397
  budgets.set(project.project, await estimateContextBudget(cwd));
6922
7398
  }
6923
7399
  if (!cancelled) setProjectBudgets(budgets);
6924
7400
  }
6925
- loadBudgets();
7401
+ void loadBudgets().catch(() => {
7402
+ });
6926
7403
  return () => {
6927
7404
  cancelled = true;
6928
7405
  };
6929
7406
  }, [projects]);
6930
7407
  useEffect2(() => {
6931
- if (!optimizeAvailable) {
7408
+ if (!optimizeAvailable || view !== "optimize" || loading || initialLoading || loadError2) {
6932
7409
  setOptimizeResult(null);
6933
7410
  return;
6934
7411
  }
6935
7412
  let cancelled = false;
7413
+ const controller = new AbortController();
6936
7414
  async function scan() {
6937
7415
  if (projects.length === 0) {
6938
7416
  setOptimizeResult(null);
6939
7417
  return;
6940
7418
  }
6941
- const result = await scanAndDetect(projects, getDateRange(period));
6942
- if (!cancelled) setOptimizeResult(result);
7419
+ const result = await loader.current.run((signal) => withScanBudget(() => scanAndDetect(projects, getDateRange(period)), 2e5, AbortSignal.any([signal, controller.signal])));
7420
+ if (!cancelled && result) setOptimizeResult(result);
6943
7421
  }
6944
- scan();
7422
+ void scan().catch((error) => {
7423
+ if (!cancelled) setLoadError(error instanceof Error ? error.message : String(error));
7424
+ });
6945
7425
  return () => {
6946
7426
  cancelled = true;
7427
+ controller.abort();
6947
7428
  };
6948
- }, [projects, period, optimizeAvailable]);
7429
+ }, [projects, period, optimizeAvailable, view, loading, initialLoading, loadError2]);
6949
7430
  const reloadData = useCallback(async (p, prov) => {
6950
7431
  const generation = ++reloadGenerationRef.current;
6951
7432
  setLoading(true);
6952
7433
  setOptimizeResult(null);
6953
7434
  try {
6954
- const range = getDateRange(p);
6955
- const data = await parseAllSessions(range, prov);
6956
- if (reloadGenerationRef.current !== generation) return;
6957
- const filteredProjects = filterProjectsByName(data, projectFilter, excludeFilter);
6958
- if (reloadGenerationRef.current !== generation) return;
6959
- setProjects(filteredProjects);
6960
- const usage = await getPlanUsageOrNull();
6961
- if (reloadGenerationRef.current !== generation) return;
6962
- setPlanUsage(usage ?? void 0);
7435
+ setLoadError(null);
7436
+ const result = await loader.current.run(async (signal) => {
7437
+ await loadPricing();
7438
+ const data = await parseAllSessions(getDateRange(p), prov, true, signal);
7439
+ if (signal.aborted) return void 0;
7440
+ return { projects: filterProjectsByName(data, projectFilter, excludeFilter), usage: await getPlanUsageOrNull(), providers: getDiscoveredProviderNames() };
7441
+ });
7442
+ if (!result || reloadGenerationRef.current !== generation) return;
7443
+ setProjects(result.projects);
7444
+ setDetectedProviders(result.providers);
7445
+ setPlanUsage(result.usage ?? void 0);
7446
+ failures.current = 0;
7447
+ retryAt.current = 0;
6963
7448
  } catch (error) {
6964
- console.error(error);
7449
+ if (reloadGenerationRef.current === generation) {
7450
+ setLoadError(error instanceof Error ? error.message : String(error));
7451
+ retryAt.current = Date.now() + Math.min(3e5, 3e4 * 2 ** Math.min(failures.current++, 4));
7452
+ }
6965
7453
  } finally {
6966
7454
  if (reloadGenerationRef.current === generation) {
6967
7455
  setLoading(false);
7456
+ setInitialLoading(false);
6968
7457
  }
6969
7458
  }
6970
7459
  }, [projectFilter, excludeFilter]);
6971
7460
  useEffect2(() => {
6972
7461
  if (!refreshSeconds || refreshSeconds <= 0) return;
6973
7462
  const id = setInterval(() => {
6974
- reloadData(period, activeProvider);
7463
+ if (!loader.current.busy && Date.now() >= retryAt.current) void reloadData(period, activeProvider);
6975
7464
  }, refreshSeconds * 1e3);
6976
7465
  return () => clearInterval(id);
6977
7466
  }, [refreshSeconds, period, activeProvider, reloadData]);
@@ -6991,14 +7480,20 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6991
7480
  }, [period, activeProvider, reloadData]);
6992
7481
  useInput2((input, key) => {
6993
7482
  if (input === "q") {
7483
+ loader.current.cancel();
6994
7484
  exit();
6995
7485
  return;
6996
7486
  }
6997
- if (input === "o" && findingCount > 0 && view === "dashboard" && optimizeAvailable) {
7487
+ if (input === "r") {
7488
+ void reloadData(period, activeProvider);
7489
+ return;
7490
+ }
7491
+ if (input === "o" && view === "dashboard" && optimizeAvailable) {
6998
7492
  setView("optimize");
6999
7493
  return;
7000
7494
  }
7001
7495
  if ((input === "b" || key.escape) && view === "optimize") {
7496
+ setLoadError(null);
7002
7497
  setView("dashboard");
7003
7498
  return;
7004
7499
  }
@@ -7027,6 +7522,15 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
7027
7522
  if (initialLoading) {
7028
7523
  return /* @__PURE__ */ jsx2(LoadingScreen, { width: dashWidth, dots: loadingDots });
7029
7524
  }
7525
+ if (loadError2 && !loading) {
7526
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
7527
+ /* @__PURE__ */ jsx2(PeriodTabs, { active: period, providerName: activeProvider }),
7528
+ /* @__PURE__ */ jsxs2(Panel, { title: "Report could not refresh", color: ORANGE3, width: dashWidth, children: [
7529
+ /* @__PURE__ */ jsx2(Text2, { children: loadError2 }),
7530
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Press r to retry, 1\u20135 to change period, or q to quit." })
7531
+ ] })
7532
+ ] });
7533
+ }
7030
7534
  if (loading) {
7031
7535
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
7032
7536
  /* @__PURE__ */ jsx2(PeriodTabs, { active: period, providerName: activeProvider, showProvider: view !== "compare" && multipleProviders }),
@@ -7048,7 +7552,7 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
7048
7552
  }
7049
7553
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
7050
7554
  /* @__PURE__ */ jsx2(PeriodTabs, { active: period, providerName: activeProvider, showProvider: multipleProviders && view !== "compare" }),
7051
- view === "compare" ? /* @__PURE__ */ jsx2(CompareView, { projects, onBack: () => setView("dashboard") }) : view === "optimize" && optimizeResult ? /* @__PURE__ */ jsx2(OptimizeView, { findings: optimizeResult.findings, costRate: optimizeResult.costRate, projects, label: PERIOD_LABELS[period], width: dashWidth, healthScore: optimizeResult.healthScore, healthGrade: optimizeResult.healthGrade }) : /* @__PURE__ */ jsx2(DashboardContent, { projects, period, columns, activeProvider, budgets: projectBudgets, planUsage }),
7555
+ view === "compare" ? /* @__PURE__ */ jsx2(CompareView, { projects, onBack: () => setView("dashboard") }) : view === "optimize" && optimizeResult ? /* @__PURE__ */ jsx2(OptimizeView, { findings: optimizeResult.findings, costRate: optimizeResult.costRate, projects, label: PERIOD_LABELS[period], width: dashWidth, healthScore: optimizeResult.healthScore, healthGrade: optimizeResult.healthGrade }) : view === "optimize" ? /* @__PURE__ */ jsx2(Panel, { title: "Optimization", color: ORANGE3, width: dashWidth, children: /* @__PURE__ */ jsx2(Text2, { children: "Scanning\u2026" }) }) : /* @__PURE__ */ jsx2(DashboardContent, { projects, period, columns, activeProvider, budgets: projectBudgets, planUsage }),
7052
7556
  view !== "compare" && /* @__PURE__ */ jsx2(StatusBar, { width: dashWidth, showProvider: multipleProviders, view, findingCount, optimizeAvailable, compareAvailable })
7053
7557
  ] });
7054
7558
  }
@@ -7070,7 +7574,7 @@ async function renderDashboard(period = "week", provider = "all", refreshSeconds
7070
7574
  } else {
7071
7575
  await loadPricing();
7072
7576
  const range = customRange ?? getDateRange(period);
7073
- const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter);
7577
+ const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider, true), projectFilter, excludeFilter);
7074
7578
  const planUsage = await getPlanUsageOrNull();
7075
7579
  const { unmount } = render2(/* @__PURE__ */ jsx2(StaticDashboard, { projects: filteredProjects, period, activeProvider: provider, planUsage: planUsage ?? void 0 }), { patchConsole: false });
7076
7580
  unmount();
@@ -7168,7 +7672,7 @@ function computeProgressiveBackfillStart({
7168
7672
  }
7169
7673
 
7170
7674
  // src/cli.ts
7171
- import { createInterface as createInterface2 } from "readline";
7675
+ import { createInterface } from "readline";
7172
7676
  import { createRequire as createRequire2 } from "module";
7173
7677
  var require2 = createRequire2(import.meta.url);
7174
7678
  var { version } = require2("../package.json");
@@ -7462,7 +7966,7 @@ program.command("status").description("Compact status output (today + week + mon
7462
7966
  const cache = await withDailyCacheLock(async () => {
7463
7967
  let c = await loadDailyCache(dailyCacheScope);
7464
7968
  const hadYesterday = c.days.some((d) => d.date >= yesterdayStr);
7465
- if (hadYesterday) {
7969
+ if (hadYesterday && (!c.revalidatedAt || Date.now() - c.revalidatedAt >= 5 * 6e4)) {
7466
7970
  const freshDays = c.days.filter((d) => d.date < yesterdayStr);
7467
7971
  const latestFresh = freshDays.length > 0 ? freshDays[freshDays.length - 1]?.date ?? null : null;
7468
7972
  c = { ...c, days: freshDays, lastComputedDate: latestFresh };
@@ -7483,6 +7987,7 @@ program.command("status").description("Compact status output (today + week + mon
7483
7987
  const gapDays = fillMissingDays(gapRange.start, gapRange.end, aggregateProjectsIntoDays(gapProjects));
7484
7988
  const coveredThrough = gapEnd.getTime() >= yesterdayEnd.getTime() ? yesterdayStr : void 0;
7485
7989
  c = addNewDays(c, gapDays, yesterdayStr, coveredThrough ? { coveredThrough } : void 0);
7990
+ c.revalidatedAt = Date.now();
7486
7991
  await saveDailyCache(c);
7487
7992
  }
7488
7993
  const updatedOldest = c.days.length > 0 ? c.days[0]?.date ?? null : null;
@@ -7496,6 +8001,7 @@ program.command("status").description("Compact status output (today + week + mon
7496
8001
  const backProjects = filterProjectsByName(await parseAllSessions(backRange, "all"), opts.project, opts.exclude);
7497
8002
  const backDays = fillMissingDays(backRange.start, backRange.end, aggregateProjectsIntoDays(backProjects));
7498
8003
  c = addNewDays(c, backDays, yesterdayStr);
8004
+ c.revalidatedAt = Date.now();
7499
8005
  await saveDailyCache(c);
7500
8006
  }
7501
8007
  }
@@ -7512,6 +8018,7 @@ program.command("status").description("Compact status output (today + week + mon
7512
8018
  allTodayProjects = await parseAllSessions(todayRange, "all");
7513
8019
  todayProjects = fp(allTodayProjects);
7514
8020
  } catch (err) {
8021
+ if (err instanceof ResourceBudgetError) throw err;
7515
8022
  const msg = err instanceof Error ? err.message : String(err);
7516
8023
  const warn2 = `parseAllSessions failed: ${msg}`;
7517
8024
  warnings.push(warn2);
@@ -7620,6 +8127,7 @@ program.command("status").description("Compact status output (today + week + mon
7620
8127
  const optimizeProjects = isAllProviders && opts.period === "today" ? todayProjects : fp(await parseAllSessions(scanRange, isAllProviders ? "all" : pf));
7621
8128
  optimize = await scanAndDetect(optimizeProjects, scanRange);
7622
8129
  } catch (err) {
8130
+ if (err instanceof ResourceBudgetError) throw err;
7623
8131
  const msg = err instanceof Error ? err.message : String(err);
7624
8132
  const warn2 = `optimize parse failed: ${msg}`;
7625
8133
  warnings.push(warn2);
@@ -7628,15 +8136,15 @@ program.command("status").description("Compact status output (today + week + mon
7628
8136
  optimize = await scanAndDetect(todayProjects, scanRange);
7629
8137
  }
7630
8138
  }
7631
- const exeOsDir = join23(homedir14(), ".exe-os");
8139
+ const exeOsDir = join24(homedir14(), ".exe-os");
7632
8140
  const exeOsDetected = existsSync7(exeOsDir);
7633
8141
  let agentStats = null;
7634
8142
  let statsFileAge = null;
7635
8143
  try {
7636
- const statsPath = join23(exeOsDir, "agent-stats.json");
8144
+ const statsPath = join24(exeOsDir, "agent-stats.json");
7637
8145
  if (existsSync7(statsPath)) {
7638
8146
  statsFileAge = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
7639
- const parsed = JSON.parse(readFileSync3(statsPath, "utf-8"));
8147
+ const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
7640
8148
  if (parsed && Array.isArray(parsed.agents) && typeof parsed.generated === "string") {
7641
8149
  agentStats = parsed;
7642
8150
  } else {
@@ -7644,6 +8152,7 @@ program.command("status").description("Compact status output (today + week + mon
7644
8152
  }
7645
8153
  }
7646
8154
  } catch (err) {
8155
+ if (err instanceof ResourceBudgetError) throw err;
7647
8156
  const msg = err instanceof Error ? err.message : String(err);
7648
8157
  process.stderr.write(`[exe-watcher] agent-stats.json found but failed to parse: ${msg}
7649
8158
  `);
@@ -7981,9 +8490,9 @@ program.command("compare").description("Compare two AI models side-by-side").opt
7981
8490
  await renderCompare(range, opts.provider);
7982
8491
  });
7983
8492
  program.command("agent-status").description("Diagnose exe-os agent data pipeline").action(() => {
7984
- const exeOsDir = join23(homedir14(), ".exe-os");
7985
- const statsPath = join23(exeOsDir, "agent-stats.json");
7986
- const pidPath = join23(exeOsDir, "exed.pid");
8493
+ const exeOsDir = join24(homedir14(), ".exe-os");
8494
+ const statsPath = join24(exeOsDir, "agent-stats.json");
8495
+ const pidPath = join24(exeOsDir, "exed.pid");
7987
8496
  const dirExists = existsSync7(exeOsDir);
7988
8497
  const statsExists = dirExists && existsSync7(statsPath);
7989
8498
  const pidExists = dirExists && existsSync7(pidPath);
@@ -7995,7 +8504,7 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
7995
8504
  } else {
7996
8505
  try {
7997
8506
  const ageSeconds = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
7998
- const raw = readFileSync3(statsPath, "utf-8");
8507
+ const raw = readFileSync2(statsPath, "utf-8");
7999
8508
  const parsed = JSON.parse(raw);
8000
8509
  if (parsed && Array.isArray(parsed.agents) && typeof parsed.generated === "string") {
8001
8510
  const agentCount = parsed.agents.length;
@@ -8008,18 +8517,27 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
8008
8517
  lines.push(`agent-stats.json: \u2717 invalid schema`);
8009
8518
  }
8010
8519
  } catch (err) {
8520
+ if (err instanceof ResourceBudgetError) throw err;
8011
8521
  const msg = err instanceof Error ? err.message : String(err);
8012
8522
  lines.push(`agent-stats.json: \u2717 parse error: ${msg}`);
8013
8523
  }
8014
8524
  }
8015
8525
  console.log(lines.join("\n"));
8016
8526
  });
8527
+ var releaseScan;
8528
+ program.hook("preAction", async (_root, command) => {
8529
+ if (command.name() === "status") releaseScan = await acquireFileLock(join24(getCacheDir(), "scan.lock"));
8530
+ });
8531
+ program.hook("postAction", async () => {
8532
+ await releaseScan?.();
8533
+ releaseScan = void 0;
8534
+ });
8017
8535
  if (process.argv.length <= 2 && process.platform === "darwin") {
8018
8536
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
8019
8537
  if (!isTTY) {
8020
8538
  program.outputHelp();
8021
8539
  } else {
8022
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
8540
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8023
8541
  rl.question("\n No subcommand given. Install the macOS menubar app? (y/N) ", (answer) => {
8024
8542
  rl.close();
8025
8543
  if (answer.trim().toLowerCase() === "y") {
@@ -8038,6 +8556,10 @@ if (process.argv.length <= 2 && process.platform === "darwin") {
8038
8556
  });
8039
8557
  }
8040
8558
  } else {
8041
- program.parse();
8559
+ program.parseAsync().catch(async (error) => {
8560
+ await releaseScan?.();
8561
+ console.error(error instanceof Error ? error.message : String(error));
8562
+ process.exitCode = 1;
8563
+ });
8042
8564
  }
8043
8565
  //# sourceMappingURL=cli.js.map