exe-watcher 0.2.50 → 0.2.53

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 +1599 -895
  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,60 @@ 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 effectiveMaxBytes(requested) {
31
+ const override = Number(process.env["EXE_WATCHER_MAX_SCAN_BYTES"]);
32
+ return Number.isFinite(override) && override > 0 ? Math.min(requested, override) : requested;
33
+ }
34
+ function withScanBudget(fn, maxRecords = 2e5, signal, maxBytes = 1024 * 1024 * 1024) {
35
+ if (scans.getStore()) return fn();
36
+ return scans.run({ bytes: 0, records: 0, started: Date.now(), maxRecords, signal, maxBytes: effectiveMaxBytes(maxBytes) }, fn);
37
+ }
38
+ function chargeRead(bytes) {
39
+ const budget = scans.getStore();
40
+ if (!budget) return;
41
+ if (budget.signal?.aborted) throw new ResourceBudgetError("scan cancelled");
42
+ budget.bytes += bytes;
43
+ if (budget.bytes > budget.maxBytes) throw new ResourceBudgetError(`${budget.maxBytes / 1024 ** 3} GiB read budget per scan`);
44
+ if (Date.now() - budget.started > 3e4) throw new ResourceBudgetError("30 second scan budget");
45
+ const heapBudget = Math.min(384 * 1024 * 1024, getHeapStatistics().heap_size_limit * 0.65);
46
+ if (process.memoryUsage().heapUsed > heapBudget) {
47
+ global.gc?.();
48
+ if (process.memoryUsage().heapUsed > heapBudget) throw new ResourceBudgetError("parser heap budget");
49
+ }
50
+ }
51
+ function chargeRecord() {
52
+ const budget = scans.getStore();
53
+ if (!budget) return;
54
+ if (++budget.records > budget.maxRecords) throw new ResourceBudgetError(`${budget.maxRecords.toLocaleString()} scanned usage records`);
55
+ if (budget.records % 256 === 0) chargeRead(0);
56
+ }
57
+ function checkFileSize(size, cap) {
58
+ if (size <= cap) return true;
59
+ if (scans.getStore()) throw new ResourceBudgetError(`file exceeds ${cap} bytes`);
60
+ return false;
61
+ }
62
+ var ResourceBudgetError, scans;
63
+ var init_resource_budget = __esm({
64
+ "src/resource-budget.ts"() {
65
+ "use strict";
66
+ ResourceBudgetError = class extends Error {
67
+ constructor(detail) {
68
+ super(`Resource limit reached (${detail}). Showing the last successful refresh; retry after background ingestion progresses.`);
69
+ this.name = "ResourceBudgetError";
70
+ }
71
+ };
72
+ scans = new AsyncLocalStorage();
73
+ }
74
+ });
75
+
27
76
  // 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";
77
+ import { readFile as readFile4, mkdir as mkdir6, rename as rename4, unlink as unlink3 } from "fs/promises";
78
+ import { open as open4 } from "fs/promises";
30
79
  import { join as join6 } from "path";
31
- import { randomBytes as randomBytes3 } from "crypto";
80
+ import { createHash, randomBytes as randomBytes3 } from "crypto";
32
81
  function getPricingWarnings() {
33
82
  return _pricingWarnings;
34
83
  }
@@ -72,10 +121,10 @@ async function fetchAndCachePricing() {
72
121
  for (const [name, costs] of fallbackStripped) {
73
122
  if (!pricing.has(name)) pricing.set(name, costs);
74
123
  }
75
- await mkdir5(getCacheDir(), { recursive: true });
124
+ await mkdir6(getCacheDir(), { recursive: true });
76
125
  const finalPath = getCachePath();
77
126
  const tmpPath = `${finalPath}.${randomBytes3(8).toString("hex")}.tmp`;
78
- const handle = await open3(tmpPath, "w", 384);
127
+ const handle = await open4(tmpPath, "w", 384);
79
128
  try {
80
129
  await handle.writeFile(JSON.stringify({
81
130
  timestamp: Date.now(),
@@ -89,7 +138,7 @@ async function fetchAndCachePricing() {
89
138
  await rename4(tmpPath, finalPath);
90
139
  } catch (err) {
91
140
  try {
92
- await unlink2(tmpPath);
141
+ await unlink3(tmpPath);
93
142
  } catch {
94
143
  }
95
144
  throw err;
@@ -97,7 +146,7 @@ async function fetchAndCachePricing() {
97
146
  try {
98
147
  const lkgPath = getLastKnownGoodPath();
99
148
  const lkgTmp = `${lkgPath}.${randomBytes3(8).toString("hex")}.tmp`;
100
- const lkgHandle = await open3(lkgTmp, "w", 384);
149
+ const lkgHandle = await open4(lkgTmp, "w", 384);
101
150
  try {
102
151
  await lkgHandle.writeFile(JSON.stringify({
103
152
  timestamp: Date.now(),
@@ -109,7 +158,7 @@ async function fetchAndCachePricing() {
109
158
  }
110
159
  await rename4(lkgTmp, lkgPath).catch(() => {
111
160
  try {
112
- unlink2(lkgTmp);
161
+ unlink3(lkgTmp);
113
162
  } catch {
114
163
  }
115
164
  });
@@ -119,7 +168,7 @@ async function fetchAndCachePricing() {
119
168
  }
120
169
  async function loadCachedPricing() {
121
170
  try {
122
- const raw = await readFile3(getCachePath(), "utf-8");
171
+ const raw = await readFile4(getCachePath(), "utf-8");
123
172
  const cached = JSON.parse(raw);
124
173
  if (Date.now() - cached.timestamp > CACHE_TTL_MS2) return null;
125
174
  return { map: new Map(Object.entries(cached.data)), timestamp: cached.timestamp };
@@ -129,7 +178,7 @@ async function loadCachedPricing() {
129
178
  }
130
179
  async function loadLastKnownGoodPricing() {
131
180
  try {
132
- const raw = await readFile3(getLastKnownGoodPath(), "utf-8");
181
+ const raw = await readFile4(getLastKnownGoodPath(), "utf-8");
133
182
  const cached = JSON.parse(raw);
134
183
  return { map: new Map(Object.entries(cached.data)), timestamp: cached.timestamp };
135
184
  } catch {
@@ -267,7 +316,15 @@ function applyContextTier(costs, inputTokens) {
267
316
  cacheReadCostPerToken: tier.cacheReadCostPerToken ?? costs.cacheReadCostPerToken
268
317
  };
269
318
  }
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;
319
+ function getPricingFingerprint() {
320
+ if (!fingerprintValue || fingerprintPricing !== pricingCache || fingerprintAliases !== userAliases) {
321
+ fingerprintPricing = pricingCache;
322
+ fingerprintAliases = userAliases;
323
+ fingerprintValue = createHash("sha256").update(JSON.stringify([Array.from(pricingCache ?? []), FALLBACK_PRICING, userAliases])).digest("hex").slice(0, 16);
324
+ }
325
+ return fingerprintValue;
326
+ }
327
+ 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
328
  var init_models = __esm({
272
329
  "src/models.ts"() {
273
330
  "use strict";
@@ -376,6 +433,164 @@ var init_models = __esm({
376
433
  "anthropic--claude-4.5-haiku": "claude-haiku-4-5"
377
434
  };
378
435
  userAliases = {};
436
+ fingerprintValue = "";
437
+ }
438
+ });
439
+
440
+ // src/fs-utils.ts
441
+ import { open as open5 } from "fs/promises";
442
+ import { openSync, fstatSync, readSync, closeSync, constants } from "fs";
443
+ function verbose() {
444
+ return process.env.EXE_WATCHER_VERBOSE === "1";
445
+ }
446
+ function warn(msg) {
447
+ if (verbose()) process.stderr.write(`exe-watcher: ${msg}
448
+ `);
449
+ }
450
+ async function readSessionFirstLine(filePath) {
451
+ const handle = await open5(filePath, "r").catch(() => null);
452
+ if (!handle) return null;
453
+ try {
454
+ const chunks = [];
455
+ const buffer = Buffer.allocUnsafe(64 * 1024);
456
+ let offset = 0;
457
+ const maxHeaderBytes = 8 * 1024 * 1024;
458
+ while (offset < maxHeaderBytes) {
459
+ const length = Math.min(offset === 0 ? 4096 : buffer.length, maxHeaderBytes - offset);
460
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
461
+ chargeRead(bytesRead);
462
+ if (bytesRead === 0) return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
463
+ const chunk = buffer.subarray(0, bytesRead);
464
+ const newline = chunk.indexOf(10);
465
+ if (newline >= 0) {
466
+ chunks.push(chunk.subarray(0, newline));
467
+ return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
468
+ }
469
+ chunks.push(Buffer.from(chunk));
470
+ offset += bytesRead;
471
+ }
472
+ if (!checkFileSize(offset + 1, maxHeaderBytes)) return null;
473
+ return null;
474
+ } catch (err) {
475
+ if (err instanceof ResourceBudgetError) throw err;
476
+ warn(`header read failed for ${filePath}: ${err.code ?? "unknown"}`);
477
+ return null;
478
+ } finally {
479
+ await handle.close();
480
+ }
481
+ }
482
+ async function readSessionFile(filePath) {
483
+ const handle = await open5(filePath, "r").catch(() => null);
484
+ if (!handle) return null;
485
+ try {
486
+ const size = (await handle.stat()).size;
487
+ if (!checkFileSize(size, MAX_SESSION_FILE_BYTES)) {
488
+ warn(`skipped oversize file ${filePath}`);
489
+ return null;
490
+ }
491
+ chargeRead(size);
492
+ const buffer = Buffer.allocUnsafe(size);
493
+ let offset = 0;
494
+ while (offset < size) {
495
+ const { bytesRead } = await handle.read(buffer, offset, Math.min(64 * 1024, size - offset), offset);
496
+ if (!bytesRead) break;
497
+ offset += bytesRead;
498
+ }
499
+ return buffer.toString("utf-8", 0, offset);
500
+ } catch (err) {
501
+ if (err instanceof ResourceBudgetError) throw err;
502
+ warn(`read failed for ${filePath}`);
503
+ return null;
504
+ } finally {
505
+ await handle.close();
506
+ }
507
+ }
508
+ function readSessionFileSync(filePath) {
509
+ const O_NOFOLLOW = constants["O_NOFOLLOW"] ?? 0;
510
+ let fd;
511
+ try {
512
+ fd = openSync(filePath, constants.O_RDONLY | O_NOFOLLOW);
513
+ } catch (err) {
514
+ const code = err.code ?? "unknown";
515
+ if (code === "ELOOP") {
516
+ warn(`skipped symlink ${filePath}`);
517
+ return null;
518
+ }
519
+ warn(`open failed for ${filePath}: ${code}`);
520
+ return null;
521
+ }
522
+ try {
523
+ const size = fstatSync(fd).size;
524
+ if (!checkFileSize(size, MAX_SESSION_FILE_BYTES)) {
525
+ warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`);
526
+ return null;
527
+ }
528
+ chargeRead(size);
529
+ const buf = Buffer.allocUnsafe(size);
530
+ let offset = 0;
531
+ while (offset < size) {
532
+ const bytesRead = readSync(fd, buf, offset, size - offset, offset);
533
+ if (bytesRead === 0) break;
534
+ offset += bytesRead;
535
+ }
536
+ return buf.toString("utf-8", 0, offset);
537
+ } catch (err) {
538
+ if (err instanceof ResourceBudgetError) throw err;
539
+ warn(`read failed for ${filePath}: ${err.code ?? "unknown"}`);
540
+ return null;
541
+ } finally {
542
+ closeSync(fd);
543
+ }
544
+ }
545
+ async function* readSessionLines(filePath, startByte = 0) {
546
+ const handle = await open5(filePath, "r").catch(() => null);
547
+ if (!handle) return;
548
+ try {
549
+ const size = (await handle.stat()).size;
550
+ if (size <= startByte) return;
551
+ chargeRead(size - startByte);
552
+ const stream = handle.createReadStream({ encoding: "utf-8", start: startByte, end: size - 1, autoClose: false });
553
+ let pieces = [];
554
+ let lineBytes = 0;
555
+ const maxLineBytes = 32 * 1024 * 1024;
556
+ const append = (piece) => {
557
+ lineBytes += Buffer.byteLength(piece);
558
+ if (lineBytes > maxLineBytes) throw new ResourceBudgetError("32 MiB JSONL record budget");
559
+ pieces.push(piece);
560
+ };
561
+ try {
562
+ for await (const chunk of stream) {
563
+ let start = 0;
564
+ let newline;
565
+ while ((newline = chunk.indexOf("\n", start)) >= 0) {
566
+ append(chunk.slice(start, newline));
567
+ chargeRead(0);
568
+ const line = pieces.join("").replace(/\r$/, "");
569
+ pieces = [];
570
+ lineBytes = 0;
571
+ yield line;
572
+ start = newline + 1;
573
+ }
574
+ if (start < chunk.length) append(chunk.slice(start));
575
+ }
576
+ if (pieces.length) yield pieces.join("").replace(/\r$/, "");
577
+ } finally {
578
+ stream.destroy();
579
+ }
580
+ } catch (err) {
581
+ if (err instanceof ResourceBudgetError) throw err;
582
+ warn(`stream read failed for ${filePath}`);
583
+ } finally {
584
+ await handle.close();
585
+ }
586
+ }
587
+ var MAX_SESSION_FILE_BYTES, STREAM_THRESHOLD_BYTES;
588
+ var init_fs_utils = __esm({
589
+ "src/fs-utils.ts"() {
590
+ "use strict";
591
+ init_resource_budget();
592
+ MAX_SESSION_FILE_BYTES = 128 * 1024 * 1024;
593
+ STREAM_THRESHOLD_BYTES = 8 * 1024 * 1024;
379
594
  }
380
595
  });
381
596
 
@@ -421,17 +636,18 @@ var init_bash_utils = __esm({
421
636
  });
422
637
 
423
638
  // 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";
639
+ import { readFile as readFile8, mkdir as mkdir9, stat as stat10, rename as rename7 } from "fs/promises";
640
+ import { open as open6 } from "fs/promises";
641
+ import { join as join14 } from "path";
427
642
  import { randomBytes as randomBytes4 } from "crypto";
428
643
  function getCachePath2() {
429
- return join11(getCacheDir(), CACHE_FILE);
644
+ return join14(getCacheDir(), CACHE_FILE);
430
645
  }
431
646
  async function getDbFingerprint(dbPath) {
432
647
  try {
433
- const s = await stat8(dbPath);
434
- return { mtimeMs: s.mtimeMs, size: s.size };
648
+ const s = await stat10(dbPath);
649
+ const wal = await stat10(`${dbPath}-wal`).catch(() => null);
650
+ return [dbPath, s.dev, s.ino, s.ctimeMs, s.mtimeMs, s.size, wal?.ino, wal?.ctimeMs, wal?.mtimeMs, wal?.size, getPricingFingerprint()].join(":");
435
651
  } catch {
436
652
  return null;
437
653
  }
@@ -440,37 +656,42 @@ async function readCachedResults(dbPath) {
440
656
  try {
441
657
  const fp = await getDbFingerprint(dbPath);
442
658
  if (!fp) return null;
443
- const raw = await readFile5(getCachePath2(), "utf-8");
659
+ const size = (await stat10(getCachePath2())).size;
660
+ if (size > 16 * 1024 * 1024) return null;
661
+ chargeRead(size);
662
+ const raw = await readFile8(getCachePath2(), "utf-8");
444
663
  const cache = JSON.parse(raw);
445
- if (cache.dbMtimeMs === fp.mtimeMs && cache.dbSizeBytes === fp.size) {
664
+ if (cache.fingerprint === fp) {
446
665
  return cache.calls;
447
666
  }
448
667
  return null;
449
- } catch {
668
+ } catch (err) {
669
+ if (err instanceof ResourceBudgetError) throw err;
450
670
  return null;
451
671
  }
452
672
  }
453
- async function writeCachedResults(dbPath, calls) {
673
+ async function writeCachedResults(dbPath, calls, expected) {
454
674
  try {
455
675
  const fp = await getDbFingerprint(dbPath);
456
- if (!fp) return;
676
+ if (!fp || expected !== void 0 && expected !== fp) return;
457
677
  const dir = getCacheDir();
458
- await mkdir6(dir, { recursive: true });
678
+ await mkdir9(dir, { recursive: true });
459
679
  const cache = {
460
- dbMtimeMs: fp.mtimeMs,
461
- dbSizeBytes: fp.size,
680
+ fingerprint: fp,
462
681
  calls
463
682
  };
683
+ const payload = JSON.stringify(cache);
684
+ if (Buffer.byteLength(payload) > 16 * 1024 * 1024) return;
464
685
  const finalPath = getCachePath2();
465
686
  const tmpPath = `${finalPath}.${randomBytes4(8).toString("hex")}.tmp`;
466
- const handle = await open4(tmpPath, "w", 384);
687
+ const handle = await open6(tmpPath, "w", 384);
467
688
  try {
468
- await handle.writeFile(JSON.stringify(cache), { encoding: "utf-8" });
689
+ await handle.writeFile(payload, { encoding: "utf-8" });
469
690
  await handle.sync();
470
691
  } finally {
471
692
  await handle.close();
472
693
  }
473
- await rename5(tmpPath, finalPath);
694
+ await rename7(tmpPath, finalPath);
474
695
  } catch {
475
696
  }
476
697
  }
@@ -478,6 +699,8 @@ var CACHE_FILE;
478
699
  var init_cursor_cache = __esm({
479
700
  "src/cursor-cache.ts"() {
480
701
  "use strict";
702
+ init_models();
703
+ init_resource_budget();
481
704
  init_cache_dir();
482
705
  CACHE_FILE = "cursor-results.json";
483
706
  }
@@ -532,7 +755,16 @@ function openDatabase(path) {
532
755
  const db = new DatabaseSync(path, { readOnly: true });
533
756
  return {
534
757
  query(sql, params = []) {
535
- return db.prepare(sql).all(...params);
758
+ const rows = [];
759
+ let bytes = 0;
760
+ for (const row of db.prepare(sql).iterate(...params)) {
761
+ const size = Buffer.byteLength(JSON.stringify(row));
762
+ bytes += size;
763
+ if (rows.length >= 1e5 || bytes > 32 * 1024 * 1024) throw new ResourceBudgetError("SQLite result budget");
764
+ chargeRead(size);
765
+ rows.push(row);
766
+ }
767
+ return rows;
536
768
  },
537
769
  close() {
538
770
  db.close();
@@ -543,6 +775,7 @@ var requireForSqlite, DatabaseSync, loadAttempted, loadError;
543
775
  var init_sqlite = __esm({
544
776
  "src/sqlite.ts"() {
545
777
  "use strict";
778
+ init_resource_budget();
546
779
  requireForSqlite = createRequire(import.meta.url);
547
780
  DatabaseSync = null;
548
781
  loadAttempted = false;
@@ -557,16 +790,16 @@ __export(cursor_exports, {
557
790
  cursor: () => cursor
558
791
  });
559
792
  import { existsSync } from "fs";
560
- import { join as join12 } from "path";
793
+ import { join as join15 } from "path";
561
794
  import { homedir as homedir8 } from "os";
562
795
  function getCursorDbPath() {
563
796
  if (process.platform === "darwin") {
564
- return join12(homedir8(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
797
+ return join15(homedir8(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
565
798
  }
566
799
  if (process.platform === "win32") {
567
- return join12(homedir8(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
800
+ return join15(homedir8(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
568
801
  }
569
- return join12(homedir8(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
802
+ return join15(homedir8(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
570
803
  }
571
804
  function extractLanguages(codeBlocksJson) {
572
805
  if (!codeBlocksJson) return [];
@@ -580,7 +813,8 @@ function extractLanguages(codeBlocksJson) {
580
813
  }
581
814
  }
582
815
  return [...langs];
583
- } catch {
816
+ } catch (error) {
817
+ if (error instanceof ResourceBudgetError) throw error;
584
818
  return [];
585
819
  }
586
820
  }
@@ -598,7 +832,8 @@ function validateSchema(db) {
598
832
  "SELECT COUNT(*) as cnt FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' LIMIT 1"
599
833
  );
600
834
  return rows.length > 0;
601
- } catch {
835
+ } catch (error) {
836
+ if (error instanceof ResourceBudgetError) throw error;
602
837
  return false;
603
838
  }
604
839
  }
@@ -612,7 +847,8 @@ function buildUserMessageMap(db, timeFloor) {
612
847
  existing.push(row.text);
613
848
  map.set(row.conversation_id, existing);
614
849
  }
615
- } catch {
850
+ } catch (error) {
851
+ if (error instanceof ResourceBudgetError) throw error;
616
852
  }
617
853
  return map;
618
854
  }
@@ -625,7 +861,8 @@ function parseBubbles(db, seenKeys) {
625
861
  let rows;
626
862
  try {
627
863
  rows = db.query(BUBBLE_QUERY_SINCE, [timeFloor]);
628
- } catch {
864
+ } catch (error) {
865
+ if (error instanceof ResourceBudgetError) throw error;
629
866
  return { calls: results };
630
867
  }
631
868
  for (const row of rows) {
@@ -668,7 +905,8 @@ function parseBubbles(db, seenKeys) {
668
905
  userMessage: userText,
669
906
  sessionId: conversationId
670
907
  });
671
- } catch {
908
+ } catch (error) {
909
+ if (error instanceof ResourceBudgetError) throw error;
672
910
  skipped++;
673
911
  }
674
912
  }
@@ -685,6 +923,7 @@ function createParser4(source, seenKeys) {
685
923
  process.stderr.write(getSqliteLoadError() + "\n");
686
924
  return;
687
925
  }
926
+ const before = await getDbFingerprint(source.path);
688
927
  const cached = await readCachedResults(source.path);
689
928
  if (cached) {
690
929
  for (const call of cached) {
@@ -698,6 +937,7 @@ function createParser4(source, seenKeys) {
698
937
  try {
699
938
  db = openDatabase(source.path);
700
939
  } catch (err) {
940
+ if (err instanceof ResourceBudgetError) throw err;
701
941
  process.stderr.write(`exe-watcher: cannot open Cursor database: ${err instanceof Error ? err.message : err}
702
942
  `);
703
943
  return;
@@ -708,7 +948,7 @@ function createParser4(source, seenKeys) {
708
948
  return;
709
949
  }
710
950
  const { calls } = parseBubbles(db, seenKeys);
711
- await writeCachedResults(source.path, calls);
951
+ await writeCachedResults(source.path, calls, before);
712
952
  for (const call of calls) {
713
953
  yield call;
714
954
  }
@@ -743,6 +983,7 @@ var CURSOR_DEFAULT_MODEL, modelDisplayNames4, BUBBLE_QUERY_BASE, USER_MESSAGES_Q
743
983
  var init_cursor = __esm({
744
984
  "src/providers/cursor.ts"() {
745
985
  "use strict";
986
+ init_resource_budget();
746
987
  init_models();
747
988
  init_cursor_cache();
748
989
  init_sqlite();
@@ -799,21 +1040,22 @@ __export(opencode_exports, {
799
1040
  createOpenCodeProvider: () => createOpenCodeProvider,
800
1041
  opencode: () => opencode
801
1042
  });
802
- import { readdir as readdir6 } from "fs/promises";
803
- import { join as join13 } from "path";
1043
+ import { readdir as readdir8 } from "fs/promises";
1044
+ import { join as join16 } from "path";
804
1045
  import { homedir as homedir9 } from "os";
805
- function sanitize(dir) {
1046
+ function sanitize2(dir) {
806
1047
  return dir.replace(/^\//, "").replace(/\//g, "-");
807
1048
  }
808
1049
  function getDataDir(dataDir) {
809
- const base = dataDir ?? process.env["XDG_DATA_HOME"] ?? join13(homedir9(), ".local", "share");
810
- return join13(base, "opencode");
1050
+ const base = dataDir ?? process.env["XDG_DATA_HOME"] ?? join16(homedir9(), ".local", "share");
1051
+ return join16(base, "opencode");
811
1052
  }
812
1053
  async function findDbFiles(dir) {
813
1054
  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 {
1055
+ const entries = await readdir8(dir);
1056
+ return entries.filter((f) => f.startsWith("opencode") && f.endsWith(".db")).map((f) => join16(dir, f));
1057
+ } catch (error) {
1058
+ if (error instanceof ResourceBudgetError) throw error;
817
1059
  return [];
818
1060
  }
819
1061
  }
@@ -824,13 +1066,14 @@ function parseTimestamp(raw) {
824
1066
  function validateSchema2(db) {
825
1067
  try {
826
1068
  db.query(
827
- "SELECT COUNT(*) as cnt FROM session LIMIT 1"
1069
+ "SELECT id FROM session LIMIT 1"
828
1070
  );
829
1071
  db.query(
830
- "SELECT COUNT(*) as cnt FROM message LIMIT 1"
1072
+ "SELECT id FROM message LIMIT 1"
831
1073
  );
832
1074
  return true;
833
- } catch {
1075
+ } catch (error) {
1076
+ if (error instanceof ResourceBudgetError) throw error;
834
1077
  return false;
835
1078
  }
836
1079
  }
@@ -872,7 +1115,8 @@ function createParser5(source, seenKeys) {
872
1115
  const list = partsByMsg.get(part.message_id) ?? [];
873
1116
  list.push(parsed);
874
1117
  partsByMsg.set(part.message_id, list);
875
- } catch {
1118
+ } catch (error) {
1119
+ if (error instanceof ResourceBudgetError) throw error;
876
1120
  }
877
1121
  }
878
1122
  let currentUserMessage = "";
@@ -880,7 +1124,8 @@ function createParser5(source, seenKeys) {
880
1124
  let data;
881
1125
  try {
882
1126
  data = JSON.parse(msg.data);
883
- } catch {
1127
+ } catch (error) {
1128
+ if (error instanceof ResourceBudgetError) throw error;
884
1129
  continue;
885
1130
  }
886
1131
  if (data.role === "user") {
@@ -949,19 +1194,25 @@ async function discoverFromDb(dbPath) {
949
1194
  let db;
950
1195
  try {
951
1196
  db = openDatabase(dbPath);
952
- } catch {
1197
+ } catch (error) {
1198
+ if (error instanceof ResourceBudgetError) throw error;
953
1199
  return [];
954
1200
  }
955
1201
  try {
1202
+ if (!validateSchema2(db)) {
1203
+ process.stderr.write("exe-watcher: OpenCode storage format not recognized. Update Watcher to read this database.\n");
1204
+ return [];
1205
+ }
956
1206
  const rows = db.query(
957
1207
  "SELECT id, directory, title, time_created FROM session WHERE time_archived IS NULL AND parent_id IS NULL ORDER BY time_created DESC"
958
1208
  );
959
1209
  return rows.map((row) => ({
960
1210
  path: `${dbPath}:${row.id}`,
961
- project: row.directory ? sanitize(row.directory) : sanitize(row.title),
1211
+ project: row.directory ? sanitize2(row.directory) : sanitize2(row.title),
962
1212
  provider: "opencode"
963
1213
  }));
964
- } catch {
1214
+ } catch (error) {
1215
+ if (error instanceof ResourceBudgetError) throw error;
965
1216
  return [];
966
1217
  } finally {
967
1218
  db.close();
@@ -998,6 +1249,7 @@ var toolNameMap4, opencode;
998
1249
  var init_opencode = __esm({
999
1250
  "src/providers/opencode.ts"() {
1000
1251
  "use strict";
1252
+ init_resource_budget();
1001
1253
  init_models();
1002
1254
  init_bash_utils();
1003
1255
  init_sqlite();
@@ -1025,20 +1277,20 @@ __export(cursor_agent_exports, {
1025
1277
  createCursorAgentProvider: () => createCursorAgentProvider,
1026
1278
  cursor_agent: () => cursor_agent
1027
1279
  });
1028
- import { createHash as createHash2 } from "crypto";
1280
+ import { createHash as createHash3 } from "crypto";
1029
1281
  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";
1282
+ import { readdir as readdir9, stat as stat11 } from "fs/promises";
1283
+ import { join as join17, basename as basename7 } from "path";
1032
1284
  import { homedir as homedir10 } from "os";
1033
1285
  function getCursorAgentBaseDir(baseDirOverride) {
1034
1286
  if (baseDirOverride) return baseDirOverride;
1035
- return join14(homedir10(), ".cursor");
1287
+ return join17(homedir10(), ".cursor");
1036
1288
  }
1037
1289
  function getProjectsDir2(baseDir) {
1038
- return join14(baseDir, "projects");
1290
+ return join17(baseDir, "projects");
1039
1291
  }
1040
1292
  function getAttributionDbPath(baseDir) {
1041
- return join14(baseDir, "ai-tracking", "ai-code-tracking.db");
1293
+ return join17(baseDir, "ai-tracking", "ai-code-tracking.db");
1042
1294
  }
1043
1295
  function estimateTokens(charCount) {
1044
1296
  if (charCount <= 0) return 0;
@@ -1089,7 +1341,7 @@ function resolveModel3(raw) {
1089
1341
  function toConversationId(transcriptPath) {
1090
1342
  const filename = basename7(transcriptPath, ".txt");
1091
1343
  if (filename.length === 36 && UUID_LIKE.test(filename)) return filename;
1092
- return createHash2("sha1").update(transcriptPath).digest("hex").slice(0, 16);
1344
+ return createHash3("sha1").update(transcriptPath).digest("hex").slice(0, 16);
1093
1345
  }
1094
1346
  function extractUserQuery(userBlock) {
1095
1347
  const chunks = [];
@@ -1212,12 +1464,14 @@ function createParser6(source, seenKeys, dbPath, summariesByConversationId) {
1212
1464
  };
1213
1465
  summariesByConversationId[conversationId] = summary;
1214
1466
  }
1215
- } catch {
1467
+ } catch (error) {
1468
+ if (error instanceof ResourceBudgetError) throw error;
1216
1469
  summary = void 0;
1217
1470
  }
1218
1471
  }
1219
1472
  }
1220
- const transcript = await readFile6(source.path, "utf-8");
1473
+ const transcript = await readSessionFile(source.path);
1474
+ if (transcript === null) return;
1221
1475
  const parsed = parseTranscript(transcript);
1222
1476
  if (!parsed.recognized) {
1223
1477
  process.stderr.write(`exe-watcher: skipped ${basename7(source.path)}: unrecognized cursor-agent transcript format
@@ -1226,7 +1480,7 @@ function createParser6(source, seenKeys, dbPath, summariesByConversationId) {
1226
1480
  }
1227
1481
  let timestamp = summary?.updatedAt ?? null;
1228
1482
  if (!timestamp) {
1229
- const fileStat = await stat9(source.path);
1483
+ const fileStat = await stat11(source.path);
1230
1484
  timestamp = fileStat.mtime.toISOString();
1231
1485
  }
1232
1486
  const model = resolveModel3(summary?.model ?? null);
@@ -1290,18 +1544,18 @@ function createCursorAgentProvider(baseDirOverride) {
1290
1544
  },
1291
1545
  async discoverSessions() {
1292
1546
  if (!existsSync2(projectsDir)) return [];
1293
- const projectEntries = await readdir7(projectsDir, { withFileTypes: true });
1547
+ const projectEntries = await readdir9(projectsDir, { withFileTypes: true });
1294
1548
  const sources = [];
1295
1549
  for (const entry of projectEntries) {
1296
1550
  if (!entry.isDirectory()) continue;
1297
1551
  const projectId = prettifyProjectId(entry.name);
1298
- const transcriptDir = join14(projectsDir, entry.name, "agent-transcripts");
1552
+ const transcriptDir = join17(projectsDir, entry.name, "agent-transcripts");
1299
1553
  if (!existsSync2(transcriptDir)) continue;
1300
- const transcriptEntries = await readdir7(transcriptDir, { withFileTypes: true });
1554
+ const transcriptEntries = await readdir9(transcriptDir, { withFileTypes: true });
1301
1555
  for (const transcript of transcriptEntries) {
1302
1556
  if (!transcript.isFile()) continue;
1303
1557
  if (!transcript.name.endsWith(".txt")) continue;
1304
- const transcriptPath = join14(transcriptDir, transcript.name);
1558
+ const transcriptPath = join17(transcriptDir, transcript.name);
1305
1559
  sources.push({
1306
1560
  path: transcriptPath,
1307
1561
  project: projectId,
@@ -1320,6 +1574,8 @@ var CURSOR_AGENT_DEFAULT_MODEL, CHARS_PER_TOKEN, MAX_USER_TEXT_LENGTH, DIGITS_ON
1320
1574
  var init_cursor_agent = __esm({
1321
1575
  "src/providers/cursor-agent.ts"() {
1322
1576
  "use strict";
1577
+ init_resource_budget();
1578
+ init_fs_utils();
1323
1579
  init_models();
1324
1580
  init_sqlite();
1325
1581
  CURSOR_AGENT_DEFAULT_MODEL = "claude-sonnet-4-5";
@@ -1358,17 +1614,87 @@ var init_cursor_agent = __esm({
1358
1614
  });
1359
1615
 
1360
1616
  // src/cli.ts
1361
- import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1362
- import { join as join23 } from "path";
1617
+ init_cache_dir();
1618
+
1619
+ // src/file-lock.ts
1620
+ import { mkdir, open, readFile, stat, unlink } from "fs/promises";
1621
+ import { dirname } from "path";
1622
+ async function acquireFileLock(path, timeoutMs = 15e3) {
1623
+ await mkdir(dirname(path), { recursive: true });
1624
+ const token = `${process.pid}:${Date.now()}:${Math.random()}`;
1625
+ const started = Date.now();
1626
+ while (true) {
1627
+ try {
1628
+ const handle = await open(path, "wx", 384);
1629
+ await handle.writeFile(token);
1630
+ await handle.close();
1631
+ return async () => {
1632
+ if (await readFile(path, "utf8").catch(() => "") === token) await unlink(path).catch(() => {
1633
+ });
1634
+ };
1635
+ } catch (error) {
1636
+ if (error.code !== "EEXIST") throw error;
1637
+ }
1638
+ const reaperPath = `${path}.reaper`;
1639
+ const reaper = await open(reaperPath, "wx", 384).catch(() => null);
1640
+ if (reaper) {
1641
+ try {
1642
+ await reaper.writeFile(token);
1643
+ const owner = await readFile(path, "utf8").catch(() => "");
1644
+ const pid = Number(owner.split(":")[0]);
1645
+ let dead = false;
1646
+ if (pid > 0) {
1647
+ try {
1648
+ process.kill(pid, 0);
1649
+ } catch (e) {
1650
+ dead = e.code === "ESRCH";
1651
+ }
1652
+ } else {
1653
+ const s = await stat(path).catch(() => null);
1654
+ dead = !!s && Date.now() - s.mtimeMs > 5e3;
1655
+ }
1656
+ if (dead) await unlink(path).catch(() => {
1657
+ });
1658
+ } finally {
1659
+ await reaper.close();
1660
+ await unlink(reaperPath).catch(() => {
1661
+ });
1662
+ }
1663
+ } else {
1664
+ const owner = await readFile(reaperPath, "utf8").catch(() => "");
1665
+ const pid = Number(owner.split(":")[0]);
1666
+ let dead = false;
1667
+ if (pid > 0) {
1668
+ try {
1669
+ process.kill(pid, 0);
1670
+ } catch (e) {
1671
+ dead = e.code === "ESRCH";
1672
+ }
1673
+ } else {
1674
+ const s = await stat(reaperPath).catch(() => null);
1675
+ dead = !!s && Date.now() - s.mtimeMs > 5e3;
1676
+ }
1677
+ if (dead && await readFile(reaperPath, "utf8").catch(() => "") === owner) await unlink(reaperPath).catch(() => {
1678
+ });
1679
+ }
1680
+ if (Date.now() - started >= timeoutMs) throw new Error("Another Watcher scan is still running. Keeping the last successful refresh.");
1681
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
1682
+ }
1683
+ }
1684
+
1685
+ // src/cli.ts
1686
+ init_resource_budget();
1687
+ import { existsSync as existsSync7, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
1688
+ import { join as join25 } from "path";
1363
1689
  import { homedir as homedir14 } from "os";
1364
1690
  import { Command } from "commander";
1365
1691
 
1366
1692
  // src/menubar-installer.ts
1367
1693
  import { spawn } from "child_process";
1368
1694
  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";
1695
+ import { cp, mkdir as mkdir2, mkdtemp, rename, rm, stat as stat2 } from "fs/promises";
1696
+ import { homedir as homedir2, platform, tmpdir } from "os";
1697
+ import { basename, join as join2 } from "path";
1372
1698
  import { pipeline } from "stream/promises";
1373
1699
  import { Readable } from "stream";
1374
1700
  var RELEASE_API_LATEST = "https://api.github.com/repos/AskExe/exe-watcher/releases/latest";
@@ -1382,11 +1708,11 @@ var DEFAULT_APP_EXIT_TIMEOUT_MS = 5e3;
1382
1708
  var DEFAULT_APP_LAUNCH_TIMEOUT_MS = 5e3;
1383
1709
  var POLL_INTERVAL_MS = 150;
1384
1710
  function userApplicationsDir() {
1385
- return join(homedir(), "Applications");
1711
+ return join2(homedir2(), "Applications");
1386
1712
  }
1387
1713
  async function exists(path) {
1388
1714
  try {
1389
- await stat(path);
1715
+ await stat2(path);
1390
1716
  return true;
1391
1717
  } catch {
1392
1718
  return false;
@@ -1567,7 +1893,7 @@ async function killRunningApp() {
1567
1893
  async function installMenubarApp(options = {}) {
1568
1894
  await ensureSupportedPlatform();
1569
1895
  const appsDir = userApplicationsDir();
1570
- const targetPath = join(appsDir, APP_BUNDLE_NAME);
1896
+ const targetPath = join2(appsDir, APP_BUNDLE_NAME);
1571
1897
  const alreadyInstalled = await exists(targetPath);
1572
1898
  if (alreadyInstalled && !options.force) {
1573
1899
  if (!await isAppRunning()) {
@@ -1577,21 +1903,21 @@ async function installMenubarApp(options = {}) {
1577
1903
  }
1578
1904
  console.log(options.version ? `Looking up Exe Watcher Menubar release ${options.version}...` : "Looking up the latest Exe Watcher Menubar release...");
1579
1905
  const asset = await fetchLatestReleaseAsset(options.version);
1580
- const stagingDir = await mkdtemp(join(tmpdir(), "exe-watcher-menubar-"));
1906
+ const stagingDir = await mkdtemp(join2(tmpdir(), "exe-watcher-menubar-"));
1581
1907
  try {
1582
- const archivePath = join(stagingDir, asset.name);
1908
+ const archivePath = join2(stagingDir, asset.name);
1583
1909
  console.log(`Downloading ${asset.name}...`);
1584
1910
  await downloadToFile(asset.browser_download_url, archivePath);
1585
1911
  console.log("Unpacking...");
1586
1912
  await runCommand("/usr/bin/unzip", ["-q", archivePath, "-d", stagingDir]);
1587
- const unpackedApp = join(stagingDir, APP_BUNDLE_NAME);
1913
+ const unpackedApp = join2(stagingDir, APP_BUNDLE_NAME);
1588
1914
  if (!await exists(unpackedApp)) {
1589
1915
  throw new Error(`Archive did not contain ${APP_BUNDLE_NAME}.`);
1590
1916
  }
1591
1917
  await runCommand("/usr/bin/xattr", ["-dr", "com.apple.quarantine", unpackedApp]).catch(() => {
1592
1918
  });
1593
- await mkdir(appsDir, { recursive: true });
1594
- const backupPath = alreadyInstalled ? join(appsDir, `${basename(APP_BUNDLE_NAME, ".app")}.backup-${Date.now()}.app`) : void 0;
1919
+ await mkdir2(appsDir, { recursive: true });
1920
+ const backupPath = alreadyInstalled ? join2(appsDir, `${basename(APP_BUNDLE_NAME, ".app")}.backup-${Date.now()}.app`) : void 0;
1595
1921
  let targetReplaced = false;
1596
1922
  try {
1597
1923
  if (alreadyInstalled && backupPath) {
@@ -1624,8 +1950,8 @@ async function installMenubarApp(options = {}) {
1624
1950
  }
1625
1951
 
1626
1952
  // 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";
1953
+ import { writeFile, mkdir as mkdir5, readdir, stat as stat3, rm as rm2 } from "fs/promises";
1954
+ import { dirname as dirname2, join as join5, resolve } from "path";
1629
1955
 
1630
1956
  // src/types.ts
1631
1957
  var CATEGORY_LABELS = {
@@ -1639,14 +1965,14 @@ var CATEGORY_LABELS = {
1639
1965
 
1640
1966
  // src/currency.ts
1641
1967
  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";
1968
+ import { readFile as readFile3, mkdir as mkdir4, rename as rename3, unlink as unlink2 } from "fs/promises";
1969
+ import { open as open3 } from "fs/promises";
1644
1970
  import { join as join4 } from "path";
1645
1971
  import { randomBytes as randomBytes2 } from "crypto";
1646
1972
 
1647
1973
  // src/config.ts
1648
- import { readFile, mkdir as mkdir2, rename as rename2 } from "fs/promises";
1649
- import { open } from "fs/promises";
1974
+ import { readFile as readFile2, mkdir as mkdir3, rename as rename2 } from "fs/promises";
1975
+ import { open as open2 } from "fs/promises";
1650
1976
  import { join as join3 } from "path";
1651
1977
  import { homedir as homedir3 } from "os";
1652
1978
  import { randomBytes } from "crypto";
@@ -1659,17 +1985,17 @@ function getConfigPath() {
1659
1985
  }
1660
1986
  async function readConfig() {
1661
1987
  try {
1662
- const raw = await readFile(getConfigPath(), "utf-8");
1988
+ const raw = await readFile2(getConfigPath(), "utf-8");
1663
1989
  return JSON.parse(raw);
1664
1990
  } catch {
1665
1991
  return {};
1666
1992
  }
1667
1993
  }
1668
1994
  async function saveConfig(config) {
1669
- await mkdir2(getConfigDir(), { recursive: true });
1995
+ await mkdir3(getConfigDir(), { recursive: true });
1670
1996
  const configPath = getConfigPath();
1671
1997
  const tmpPath = `${configPath}.${randomBytes(8).toString("hex")}.tmp`;
1672
- const handle = await open(tmpPath, "w", 384);
1998
+ const handle = await open2(tmpPath, "w", 384);
1673
1999
  try {
1674
2000
  await handle.writeFile(JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8" });
1675
2001
  await handle.sync();
@@ -1742,7 +2068,7 @@ async function fetchRate(code) {
1742
2068
  }
1743
2069
  async function loadCachedRate(code) {
1744
2070
  try {
1745
- const raw = await readFile2(getRateCachePath(), "utf-8");
2071
+ const raw = await readFile3(getRateCachePath(), "utf-8");
1746
2072
  const cached = JSON.parse(raw);
1747
2073
  if (typeof cached.code !== "string" || cached.code !== code) return null;
1748
2074
  if (typeof cached.timestamp !== "number" || !Number.isFinite(cached.timestamp)) return null;
@@ -1754,10 +2080,10 @@ async function loadCachedRate(code) {
1754
2080
  }
1755
2081
  }
1756
2082
  async function cacheRate(code, rate) {
1757
- await mkdir3(getCacheDir(), { recursive: true });
2083
+ await mkdir4(getCacheDir(), { recursive: true });
1758
2084
  const finalPath = getRateCachePath();
1759
2085
  const tmpPath = `${finalPath}.${randomBytes2(8).toString("hex")}.tmp`;
1760
- const handle = await open2(tmpPath, "w", 384);
2086
+ const handle = await open3(tmpPath, "w", 384);
1761
2087
  try {
1762
2088
  await handle.writeFile(JSON.stringify({ timestamp: Date.now(), code, rate }), { encoding: "utf-8" });
1763
2089
  await handle.sync();
@@ -1768,7 +2094,7 @@ async function cacheRate(code, rate) {
1768
2094
  await rename3(tmpPath, finalPath);
1769
2095
  } catch (err) {
1770
2096
  try {
1771
- await unlink(tmpPath);
2097
+ await unlink2(tmpPath);
1772
2098
  } catch {
1773
2099
  }
1774
2100
  throw err;
@@ -2274,7 +2600,7 @@ function buildReadme(periods) {
2274
2600
  }
2275
2601
  var EXPORT_MARKER_FILE = ".exe-watcher-export";
2276
2602
  async function isExeWatcherExportFolder(path) {
2277
- const markerStat = await stat2(join5(path, EXPORT_MARKER_FILE)).catch(() => null);
2603
+ const markerStat = await stat3(join5(path, EXPORT_MARKER_FILE)).catch(() => null);
2278
2604
  return markerStat?.isFile() ?? false;
2279
2605
  }
2280
2606
  async function clearExeWatcherExportFolder(path) {
@@ -2290,7 +2616,7 @@ async function exportCsv(periods, outputPath) {
2290
2616
  if (folder.toLowerCase().endsWith(".csv")) {
2291
2617
  folder = folder.slice(0, -4);
2292
2618
  }
2293
- const existingStat = await stat2(folder).catch(() => null);
2619
+ const existingStat = await stat3(folder).catch(() => null);
2294
2620
  if (existingStat?.isFile()) {
2295
2621
  throw new Error(`Refusing to overwrite existing file at ${folder}. Pass a directory path instead.`);
2296
2622
  }
@@ -2302,7 +2628,7 @@ async function exportCsv(periods, outputPath) {
2302
2628
  }
2303
2629
  await clearExeWatcherExportFolder(folder);
2304
2630
  }
2305
- await mkdir4(folder, { recursive: true });
2631
+ await mkdir5(folder, { recursive: true });
2306
2632
  await writeFile(join5(folder, EXPORT_MARKER_FILE), "", "utf-8");
2307
2633
  const dailyRows = periods.flatMap((p) => buildDailyRows(p.projects, p.label));
2308
2634
  const activityRows = periods.flatMap((p) => buildActivityRows(p.projects, p.label));
@@ -2339,7 +2665,7 @@ async function exportJson(periods, outputPath) {
2339
2665
  shellCommands: buildBashRows(thirtyDayProjects)
2340
2666
  };
2341
2667
  const target = resolve(outputPath.toLowerCase().endsWith(".json") ? outputPath : `${outputPath}.json`);
2342
- await mkdir4(dirname(target), { recursive: true });
2668
+ await mkdir5(dirname2(target), { recursive: true });
2343
2669
  await writeFile(target, JSON.stringify(data, null, 2), "utf-8");
2344
2670
  return target;
2345
2671
  }
@@ -2348,113 +2674,480 @@ async function exportJson(periods, outputPath) {
2348
2674
  init_models();
2349
2675
 
2350
2676
  // src/parser.ts
2351
- import { readdir as readdir8, stat as stat11 } from "fs/promises";
2352
- import { basename as basename8, join as join16 } from "path";
2677
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2353
2678
 
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;
2679
+ // src/compare-stats.ts
2680
+ import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
2681
+ import { join as join7 } from "path";
2682
+ var PLANNING_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "EnterPlanMode", "ExitPlanMode"]);
2683
+ function aggregateModelStats(projects) {
2684
+ const byModel = /* @__PURE__ */ new Map();
2685
+ const ensure = (model) => {
2686
+ let s = byModel.get(model);
2687
+ if (!s) {
2688
+ 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: "" };
2689
+ byModel.set(model, s);
2413
2690
  }
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;
2691
+ return s;
2692
+ };
2693
+ for (const project of projects) {
2694
+ for (const session of project.sessions) {
2695
+ if (session.reportSummary) {
2696
+ for (const row of Object.values(session.reportSummary.models)) mergeCounters(ensure(row.model), row);
2697
+ continue;
2698
+ }
2699
+ for (const turn of session.turns) {
2700
+ if (turn.assistantCalls.length === 0) continue;
2701
+ const primaryModel = turn.assistantCalls[0].model;
2702
+ if (primaryModel === "<synthetic>") continue;
2703
+ const ms = ensure(primaryModel);
2704
+ ms.totalTurns++;
2705
+ if (turn.hasEdits) {
2706
+ ms.editTurns++;
2707
+ if (turn.retries === 0) ms.oneShotTurns++;
2708
+ for (const c of turn.assistantCalls) {
2709
+ if (c.model !== "<synthetic>") ms.editCost += c.costUSD;
2710
+ }
2711
+ }
2712
+ ms.retries += turn.retries;
2713
+ for (const call of turn.assistantCalls) {
2714
+ if (call.model === "<synthetic>") continue;
2715
+ const cs = call.model === primaryModel ? ms : ensure(call.model);
2716
+ cs.calls++;
2717
+ cs.cost += call.costUSD;
2718
+ cs.outputTokens += call.usage.outputTokens;
2719
+ cs.inputTokens += call.usage.inputTokens;
2720
+ cs.cacheReadTokens += call.usage.cacheReadInputTokens;
2721
+ cs.cacheWriteTokens += call.usage.cacheCreationInputTokens;
2722
+ if (!cs.firstSeen || call.timestamp < cs.firstSeen) cs.firstSeen = call.timestamp;
2723
+ if (!cs.lastSeen || call.timestamp > cs.lastSeen) cs.lastSeen = call.timestamp;
2724
+ }
2725
+ }
2420
2726
  }
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
2727
  }
2728
+ return [...byModel.values()].sort((a, b) => b.cost - a.cost);
2428
2729
  }
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"}`);
2447
- } finally {
2448
- stream.destroy();
2449
- }
2730
+ var METRICS = [
2731
+ {
2732
+ section: "Performance",
2733
+ label: "One-shot rate",
2734
+ formatFn: "percent",
2735
+ higherIsBetter: true,
2736
+ compute: (s) => s.editTurns > 0 ? s.oneShotTurns / s.editTurns * 100 : null
2737
+ },
2738
+ {
2739
+ section: "Performance",
2740
+ label: "Retry rate",
2741
+ formatFn: "decimal",
2742
+ higherIsBetter: false,
2743
+ compute: (s) => s.editTurns > 0 ? s.retries / s.editTurns : null
2744
+ },
2745
+ {
2746
+ section: "Performance",
2747
+ label: "Self-correction",
2748
+ formatFn: "percent",
2749
+ higherIsBetter: false,
2750
+ compute: (s) => s.totalTurns > 0 ? s.selfCorrections / s.totalTurns * 100 : null
2751
+ },
2752
+ {
2753
+ section: "Efficiency",
2754
+ label: "Cost / call",
2755
+ formatFn: "cost",
2756
+ higherIsBetter: false,
2757
+ compute: (s) => s.calls > 0 ? s.cost / s.calls : null
2758
+ },
2759
+ {
2760
+ section: "Efficiency",
2761
+ label: "Cost / edit",
2762
+ formatFn: "cost",
2763
+ higherIsBetter: false,
2764
+ compute: (s) => s.editTurns > 0 ? s.editCost / s.editTurns : null
2765
+ },
2766
+ {
2767
+ section: "Efficiency",
2768
+ label: "Output tok / call",
2769
+ formatFn: "number",
2770
+ higherIsBetter: false,
2771
+ compute: (s) => s.calls > 0 ? Math.round(s.outputTokens / s.calls) : null
2772
+ },
2773
+ {
2774
+ section: "Efficiency",
2775
+ label: "Cache hit rate",
2776
+ formatFn: "percent",
2777
+ higherIsBetter: true,
2778
+ compute: (s) => {
2779
+ const total = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens;
2780
+ return total > 0 ? s.cacheReadTokens / total * 100 : null;
2781
+ }
2782
+ }
2783
+ ];
2784
+ function pickWinner(valueA, valueB, higherIsBetter) {
2785
+ if (valueA === null || valueB === null) return "none";
2786
+ if (valueA === valueB) return "tie";
2787
+ if (higherIsBetter) return valueA > valueB ? "a" : "b";
2788
+ return valueA < valueB ? "a" : "b";
2789
+ }
2790
+ function computeComparison(a, b) {
2791
+ return METRICS.map((m) => {
2792
+ const valueA = m.compute(a);
2793
+ const valueB = m.compute(b);
2794
+ return {
2795
+ section: m.section,
2796
+ label: m.label,
2797
+ valueA,
2798
+ valueB,
2799
+ formatFn: m.formatFn,
2800
+ winner: pickWinner(valueA, valueB, m.higherIsBetter)
2801
+ };
2802
+ });
2803
+ }
2804
+ function computeCategoryComparison(projects, modelA, modelB) {
2805
+ const mapA = /* @__PURE__ */ new Map();
2806
+ const mapB = /* @__PURE__ */ new Map();
2807
+ const ensure = (map, cat) => {
2808
+ let a = map.get(cat);
2809
+ if (!a) {
2810
+ a = { turns: 0, editTurns: 0, oneShotTurns: 0 };
2811
+ map.set(cat, a);
2812
+ }
2813
+ return a;
2814
+ };
2815
+ for (const project of projects) {
2816
+ for (const session of project.sessions) {
2817
+ if (session.reportSummary) {
2818
+ for (const [model, map] of [[modelA, mapA], [modelB, mapB]]) {
2819
+ for (const [category, row] of Object.entries(session.reportSummary.categories[model] ?? {})) mergeCounters(ensure(map, category), row);
2820
+ }
2821
+ continue;
2822
+ }
2823
+ for (const turn of session.turns) {
2824
+ if (turn.assistantCalls.length === 0) continue;
2825
+ const primary = turn.assistantCalls[0].model;
2826
+ if (primary !== modelA && primary !== modelB) continue;
2827
+ const acc = ensure(primary === modelA ? mapA : mapB, turn.category);
2828
+ acc.turns++;
2829
+ if (turn.hasEdits) {
2830
+ acc.editTurns++;
2831
+ if (turn.retries === 0) acc.oneShotTurns++;
2832
+ }
2833
+ }
2834
+ }
2835
+ }
2836
+ const allCats = /* @__PURE__ */ new Set([...mapA.keys(), ...mapB.keys()]);
2837
+ const result = [];
2838
+ for (const category of allCats) {
2839
+ const a = mapA.get(category);
2840
+ const b = mapB.get(category);
2841
+ if ((!a || a.editTurns === 0) && (!b || b.editTurns === 0)) continue;
2842
+ const rateA = a && a.editTurns > 0 ? a.oneShotTurns / a.editTurns * 100 : null;
2843
+ const rateB = b && b.editTurns > 0 ? b.oneShotTurns / b.editTurns * 100 : null;
2844
+ result.push({
2845
+ category,
2846
+ turnsA: a?.turns ?? 0,
2847
+ editTurnsA: a?.editTurns ?? 0,
2848
+ oneShotRateA: rateA,
2849
+ turnsB: b?.turns ?? 0,
2850
+ editTurnsB: b?.editTurns ?? 0,
2851
+ oneShotRateB: rateB,
2852
+ winner: pickWinner(rateA, rateB, true)
2853
+ });
2854
+ }
2855
+ return result.sort((a, b) => b.turnsA + b.turnsB - (a.turnsA + a.turnsB));
2856
+ }
2857
+ function computeWorkingStyle(projects, modelA, modelB) {
2858
+ const sA = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
2859
+ const sB = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
2860
+ for (const project of projects) {
2861
+ for (const session of project.sessions) {
2862
+ if (session.reportSummary) {
2863
+ if (session.reportSummary.styles[modelA]) mergeCounters(sA, session.reportSummary.styles[modelA]);
2864
+ if (session.reportSummary.styles[modelB]) mergeCounters(sB, session.reportSummary.styles[modelB]);
2865
+ continue;
2866
+ }
2867
+ for (const turn of session.turns) {
2868
+ if (turn.assistantCalls.length === 0) continue;
2869
+ const primary = turn.assistantCalls[0].model;
2870
+ if (primary !== modelA && primary !== modelB) continue;
2871
+ const s = primary === modelA ? sA : sB;
2872
+ s.totalTurns++;
2873
+ const turnTools = turn.assistantCalls.flatMap((c) => c.tools);
2874
+ if (turnTools.some((t) => PLANNING_TOOLS.has(t)) || turn.assistantCalls.some((c) => c.hasPlanMode)) {
2875
+ s.planModeUses++;
2876
+ }
2877
+ for (const call of turn.assistantCalls) {
2878
+ s.totalToolCalls += call.tools.length;
2879
+ if (call.hasAgentSpawn) s.agentSpawns++;
2880
+ if (call.speed === "fast") s.fastModeCalls++;
2881
+ }
2882
+ }
2883
+ }
2884
+ }
2885
+ const pct2 = (num, den) => den > 0 ? num / den * 100 : null;
2886
+ const avg = (num, den) => den > 0 ? num / den : null;
2887
+ return [
2888
+ { label: "Delegation rate", valueA: pct2(sA.agentSpawns, sA.totalTurns), valueB: pct2(sB.agentSpawns, sB.totalTurns), formatFn: "percent" },
2889
+ { label: "Planning rate", valueA: pct2(sA.planModeUses, sA.totalTurns), valueB: pct2(sB.planModeUses, sB.totalTurns), formatFn: "percent" },
2890
+ { label: "Avg tools / turn", valueA: avg(sA.totalToolCalls, sA.totalTurns), valueB: avg(sB.totalToolCalls, sB.totalTurns), formatFn: "decimal" },
2891
+ { label: "Fast mode usage", valueA: pct2(sA.fastModeCalls, sA.totalTurns), valueB: pct2(sB.fastModeCalls, sB.totalTurns), formatFn: "percent" }
2892
+ ];
2893
+ }
2894
+ var SELF_CORRECTION_PATTERNS = [
2895
+ /\bmy mistake\b/i,
2896
+ /\bmy bad\b/i,
2897
+ /\bmy apolog/i,
2898
+ /\bI apologize\b/i,
2899
+ /\bI was wrong\b/i,
2900
+ /\bI was incorrect\b/i,
2901
+ /\bI made (a |an )?(error|mistake)\b/i,
2902
+ /\bI incorrectly\b/i,
2903
+ /\bI mistakenly\b/i,
2904
+ /\bthat was (incorrect|wrong|an error)\b/i,
2905
+ /\blet me correct that\b/i,
2906
+ /\bI need to correct\b/i,
2907
+ /\byou're right[.,]? I/i,
2908
+ /\bsorry about that\b/i
2909
+ ];
2910
+ function extractText(content) {
2911
+ if (typeof content === "string") return content;
2912
+ if (!Array.isArray(content)) return "";
2913
+ return content.filter((b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ");
2914
+ }
2915
+ function isCompactFile(name) {
2916
+ return name.includes("compact");
2917
+ }
2918
+ async function collectJsonlFiles(sessionDir) {
2919
+ const entries = await readdir2(sessionDir, { withFileTypes: true });
2920
+ const files = [];
2921
+ for (const entry of entries) {
2922
+ if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
2923
+ files.push(join7(sessionDir, entry.name));
2924
+ } else if (entry.isDirectory() && entry.name === "subagents") {
2925
+ const subEntries = await readdir2(join7(sessionDir, entry.name), { withFileTypes: true });
2926
+ for (const sub of subEntries) {
2927
+ if (sub.isFile() && sub.name.endsWith(".jsonl") && !isCompactFile(sub.name)) {
2928
+ files.push(join7(sessionDir, entry.name, sub.name));
2929
+ }
2930
+ }
2931
+ }
2932
+ }
2933
+ return files;
2934
+ }
2935
+ async function scanSelfCorrections(projectDirs) {
2936
+ const counts = /* @__PURE__ */ new Map();
2937
+ const seen = /* @__PURE__ */ new Set();
2938
+ for (const dir of projectDirs) {
2939
+ let entries;
2940
+ try {
2941
+ entries = await readdir2(dir, { withFileTypes: true });
2942
+ } catch {
2943
+ continue;
2944
+ }
2945
+ const allFiles = [];
2946
+ for (const entry of entries) {
2947
+ if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
2948
+ allFiles.push(join7(dir, entry.name));
2949
+ } else if (entry.isDirectory()) {
2950
+ try {
2951
+ const sessionFiles = await collectJsonlFiles(join7(dir, entry.name));
2952
+ allFiles.push(...sessionFiles);
2953
+ } catch {
2954
+ continue;
2955
+ }
2956
+ }
2957
+ }
2958
+ for (const file of allFiles) {
2959
+ let raw;
2960
+ try {
2961
+ raw = await readFile5(file, "utf8");
2962
+ } catch {
2963
+ continue;
2964
+ }
2965
+ for (const line of raw.split("\n")) {
2966
+ const trimmed = line.trim();
2967
+ if (!trimmed) continue;
2968
+ let parsed;
2969
+ try {
2970
+ parsed = JSON.parse(trimmed);
2971
+ } catch {
2972
+ continue;
2973
+ }
2974
+ const rec = parsed;
2975
+ if (!rec || typeof rec !== "object" || rec["type"] !== "assistant") continue;
2976
+ const ts = rec["timestamp"];
2977
+ const msg = rec["message"];
2978
+ if (msg === null || typeof msg !== "object") continue;
2979
+ const msgRec = msg;
2980
+ const model = msgRec["model"];
2981
+ if (typeof model !== "string" || model === "<synthetic>") continue;
2982
+ const dedupeKey = `${model}:${ts}`;
2983
+ if (seen.has(dedupeKey)) continue;
2984
+ seen.add(dedupeKey);
2985
+ const text = extractText(msgRec["content"]);
2986
+ if (SELF_CORRECTION_PATTERNS.some((p) => p.test(text))) {
2987
+ counts.set(model, (counts.get(model) ?? 0) + 1);
2988
+ }
2989
+ }
2990
+ }
2991
+ }
2992
+ return counts;
2993
+ }
2994
+
2995
+ // src/report-summary.ts
2996
+ var planning = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "EnterPlanMode", "ExitPlanMode"]);
2997
+ function compactReportSession(session) {
2998
+ const report = { daily: /* @__PURE__ */ Object.create(null), models: /* @__PURE__ */ Object.create(null), categories: /* @__PURE__ */ Object.create(null), styles: /* @__PURE__ */ Object.create(null) };
2999
+ for (const model of aggregateModelStats([{ project: session.project, projectPath: "", sessions: [session], totalCostUSD: session.totalCostUSD, totalApiCalls: session.apiCalls }])) report.models[model.model] = model;
3000
+ for (const turn of session.turns) {
3001
+ if (turn.timestamp) {
3002
+ const day = dateKey(turn.timestamp);
3003
+ const d = report.daily[day] ??= { cost: 0, calls: 0 };
3004
+ d.cost += turn.assistantCalls.reduce((n, c) => n + c.costUSD, 0);
3005
+ d.calls += turn.assistantCalls.length;
3006
+ }
3007
+ const model = turn.assistantCalls[0]?.model;
3008
+ if (!model) continue;
3009
+ const cats = report.categories[model] ??= /* @__PURE__ */ Object.create(null);
3010
+ const cat = cats[turn.category] ??= { turns: 0, editTurns: 0, oneShotTurns: 0 };
3011
+ cat.turns++;
3012
+ if (turn.hasEdits) {
3013
+ cat.editTurns++;
3014
+ if (!turn.retries) cat.oneShotTurns++;
3015
+ }
3016
+ const style = report.styles[model] ??= { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
3017
+ style.totalTurns++;
3018
+ if (turn.assistantCalls.some((c) => c.hasPlanMode || c.tools.some((t) => planning.has(t)))) style.planModeUses++;
3019
+ for (const call of turn.assistantCalls) {
3020
+ style.totalToolCalls += call.tools.length;
3021
+ if (call.hasAgentSpawn) style.agentSpawns++;
3022
+ if (call.speed === "fast") style.fastModeCalls++;
3023
+ }
3024
+ }
3025
+ session.reportSummary = report;
3026
+ session.turns = [];
3027
+ return session;
3028
+ }
3029
+ function mergeReportSession(target, source) {
3030
+ mergeCounters(target, source);
3031
+ }
3032
+ function mergeCounters(target, source) {
3033
+ const dst = target;
3034
+ for (const [key, value] of Object.entries(source)) {
3035
+ const old = Object.hasOwn(dst, key) ? dst[key] : void 0;
3036
+ let next = old;
3037
+ if (typeof value === "number") next = (typeof old === "number" ? old : 0) + value;
3038
+ else if (typeof value === "string") {
3039
+ if (key === "firstSeen" || key === "firstTimestamp") next = !old || value && value < String(old) ? value : old;
3040
+ else if (key === "lastSeen" || key === "lastTimestamp") next = !old || value > String(old) ? value : old;
3041
+ else next = old ?? value;
3042
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
3043
+ next = old && typeof old === "object" ? old : /* @__PURE__ */ Object.create(null);
3044
+ mergeCounters(next, value);
3045
+ }
3046
+ Object.defineProperty(dst, key, { value: next, writable: true, enumerable: true, configurable: true });
3047
+ }
3048
+ }
3049
+
3050
+ // src/parsed-file-cache.ts
3051
+ init_cache_dir();
3052
+ init_models();
3053
+ init_resource_budget();
3054
+ import { createHash as createHash2, randomUUID } from "crypto";
3055
+ 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";
3056
+ import { join as join8 } from "path";
3057
+ var VERSION = 1;
3058
+ var MAX_ENTRY = 16 * 1024 * 1024;
3059
+ var MAX_DISK = 256 * 1024 * 1024;
3060
+ var maintainedDir = "";
3061
+ async function fingerprint(path) {
3062
+ try {
3063
+ const s = await stat4(path);
3064
+ const wal = await stat4(`${path}-wal`).catch(() => null);
3065
+ return [s.dev, s.ino, s.size, s.mtimeMs, s.ctimeMs, wal?.size, wal?.mtimeMs].join(":");
3066
+ } catch {
3067
+ return null;
3068
+ }
3069
+ }
3070
+ async function cachedFileParse(path, kind, parse, warnings) {
3071
+ const dir = join8(getCacheDir(), "parsed-files");
3072
+ const key = createHash2("sha256").update(`${VERSION}:${kind}:${path}:${getPricingFingerprint()}`).digest("hex");
3073
+ const cachePath = join8(dir, `${key}.json`);
3074
+ const fingerprintPath = kind === "opencode" ? path.slice(0, path.lastIndexOf(":")) : path;
3075
+ const before = await fingerprint(fingerprintPath);
3076
+ if (before) {
3077
+ try {
3078
+ const s = await stat4(cachePath);
3079
+ if (s.size <= MAX_ENTRY) {
3080
+ chargeRead(s.size);
3081
+ const saved = JSON.parse(await readFile6(cachePath, "utf8"));
3082
+ if (saved.v === VERSION && saved.fingerprint === before && Array.isArray(saved.data)) {
3083
+ if (Array.isArray(saved.warnings)) warnings?.push(...saved.warnings.filter((w) => typeof w === "string"));
3084
+ return saved.data;
3085
+ }
3086
+ }
3087
+ } catch (err) {
3088
+ if (err instanceof ResourceBudgetError) throw err;
3089
+ }
3090
+ }
3091
+ const data = await parse();
3092
+ if (!before || await fingerprint(fingerprintPath) !== before) return data;
3093
+ const payload = JSON.stringify({ v: VERSION, fingerprint: before, data, warnings });
3094
+ if (Buffer.byteLength(payload) > MAX_ENTRY) return data;
3095
+ await mkdir7(dir, { recursive: true });
3096
+ const release = await acquireFileLock(join8(dir, "writes.lock"));
3097
+ try {
3098
+ const quotaPath = join8(dir, "quota");
3099
+ if (maintainedDir !== dir) {
3100
+ const files = await readdir3(dir);
3101
+ const rows = [];
3102
+ for (const name of files) {
3103
+ if (!name.endsWith(".json") && !name.endsWith(".tmp")) continue;
3104
+ const p = join8(dir, name), s = await stat4(p).catch(() => null);
3105
+ if (s) rows.push({ path: p, size: s.size, mtime: s.mtimeMs });
3106
+ }
3107
+ let total = rows.reduce((n, r) => n + r.size, 0);
3108
+ for (const row of rows.sort((a, b) => a.mtime - b.mtime)) {
3109
+ if (total <= MAX_DISK) break;
3110
+ await unlink4(row.path).catch(() => {
3111
+ });
3112
+ total -= row.size;
3113
+ }
3114
+ await writeFile2(quotaPath, String(total), { mode: 384 });
3115
+ maintainedDir = dir;
3116
+ }
3117
+ if (await readFile6(cachePath, "utf8").catch(() => "") !== payload) {
3118
+ const total = Number(await readFile6(quotaPath, "utf8").catch(() => String(MAX_DISK)));
3119
+ const oldSize = (await stat4(cachePath).catch(() => null))?.size ?? 0;
3120
+ const nextSize = total - oldSize + Buffer.byteLength(payload);
3121
+ if (!Number.isFinite(nextSize) || nextSize > MAX_DISK) return data;
3122
+ const tmp = `${cachePath}.${randomUUID()}.tmp`;
3123
+ try {
3124
+ await writeFile2(quotaPath, String(nextSize), { mode: 384 });
3125
+ await writeFile2(tmp, payload, { mode: 384 });
3126
+ await rename5(tmp, cachePath);
3127
+ } finally {
3128
+ await unlink4(tmp).catch(() => {
3129
+ });
3130
+ }
3131
+ }
3132
+ } finally {
3133
+ await release();
3134
+ }
3135
+ return data;
2450
3136
  }
2451
3137
 
2452
3138
  // src/parser.ts
3139
+ init_resource_budget();
3140
+ init_fs_utils();
2453
3141
  init_models();
3142
+ import { readdir as readdir10, stat as stat13 } from "fs/promises";
3143
+ import { basename as basename8, join as join19 } from "path";
3144
+
3145
+ // src/providers/index.ts
3146
+ init_resource_budget();
2454
3147
 
2455
3148
  // 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";
3149
+ import { readdir as readdir4, stat as stat5 } from "fs/promises";
3150
+ import { basename as basename2, join as join9 } from "path";
2458
3151
  import { homedir as homedir4 } from "os";
2459
3152
  var shortNames = {
2460
3153
  "claude-opus-4-7": "Opus 4.7",
@@ -2471,31 +3164,31 @@ var shortNames = {
2471
3164
  "claude-3-5-haiku": "Haiku 3.5"
2472
3165
  };
2473
3166
  function getClaudeDir() {
2474
- return process.env["CLAUDE_CONFIG_DIR"] || join7(homedir4(), ".claude");
3167
+ return process.env["CLAUDE_CONFIG_DIR"] || join9(homedir4(), ".claude");
2475
3168
  }
2476
3169
  function getProjectsDir() {
2477
- return join7(getClaudeDir(), "projects");
3170
+ return join9(getClaudeDir(), "projects");
2478
3171
  }
2479
3172
  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");
3173
+ if (process.platform === "darwin") return join9(homedir4(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
3174
+ if (process.platform === "win32") return join9(homedir4(), "AppData", "Roaming", "Claude", "local-agent-mode-sessions");
3175
+ return join9(homedir4(), ".config", "Claude", "local-agent-mode-sessions");
2483
3176
  }
2484
3177
  async function findDesktopProjectDirs(base) {
2485
3178
  const results = [];
2486
3179
  async function walk(dir, depth) {
2487
3180
  if (depth > 8) return;
2488
- const entries = await readdir2(dir).catch(() => []);
3181
+ const entries = await readdir4(dir).catch(() => []);
2489
3182
  for (const entry of entries) {
2490
3183
  if (entry === "node_modules" || entry === ".git") continue;
2491
- const full = join7(dir, entry);
2492
- const s = await stat4(full).catch(() => null);
3184
+ const full = join9(dir, entry);
3185
+ const s = await stat5(full).catch(() => null);
2493
3186
  if (!s?.isDirectory()) continue;
2494
3187
  if (entry === "projects") {
2495
- const projectDirs = await readdir2(full).catch(() => []);
3188
+ const projectDirs = await readdir4(full).catch(() => []);
2496
3189
  for (const pd of projectDirs) {
2497
- const pdFull = join7(full, pd);
2498
- const pdStat = await stat4(pdFull).catch(() => null);
3190
+ const pdFull = join9(full, pd);
3191
+ const pdStat = await stat5(pdFull).catch(() => null);
2499
3192
  if (pdStat?.isDirectory()) results.push(pdFull);
2500
3193
  }
2501
3194
  } else {
@@ -2523,10 +3216,10 @@ var claude = {
2523
3216
  const sources = [];
2524
3217
  const projectsDir = getProjectsDir();
2525
3218
  try {
2526
- const entries = await readdir2(projectsDir);
3219
+ const entries = await readdir4(projectsDir);
2527
3220
  for (const dirName of entries) {
2528
- const dirPath = join7(projectsDir, dirName);
2529
- const dirStat = await stat4(dirPath).catch(() => null);
3221
+ const dirPath = join9(projectsDir, dirName);
3222
+ const dirStat = await stat5(dirPath).catch(() => null);
2530
3223
  if (dirStat?.isDirectory()) {
2531
3224
  sources.push({ path: dirPath, project: dirName, provider: "claude" });
2532
3225
  }
@@ -2548,9 +3241,131 @@ var claude = {
2548
3241
  };
2549
3242
 
2550
3243
  // 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";
3244
+ import { readdir as readdir5, stat as stat7 } from "fs/promises";
3245
+ import { basename as basename3, join as join11 } from "path";
2553
3246
  import { homedir as homedir5 } from "os";
3247
+
3248
+ // src/discovery-cache.ts
3249
+ init_cache_dir();
3250
+ import { randomUUID as randomUUID2 } from "crypto";
3251
+ import { mkdir as mkdir8, readFile as readFile7, rename as rename6, stat as stat6, unlink as unlink5, writeFile as writeFile3 } from "fs/promises";
3252
+ import { join as join10 } from "path";
3253
+ init_resource_budget();
3254
+ var VERSION2 = 1;
3255
+ var MANIFEST = "discovery-index.json";
3256
+ var MAX_MANIFEST_BYTES = 16 * 1024 * 1024;
3257
+ var MAX_ENTRIES = 2e4;
3258
+ var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
3259
+ var TOUCH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
3260
+ var loaded = null;
3261
+ var loadedFrom = "";
3262
+ var dirty = false;
3263
+ function manifestPath() {
3264
+ return join10(getCacheDir(), MANIFEST);
3265
+ }
3266
+ function fingerprintOf(s) {
3267
+ return [s.dev, s.ino, s.size, s.mtimeMs, s.ctimeMs].join(":");
3268
+ }
3269
+ function emptyManifest() {
3270
+ return { v: VERSION2, e: /* @__PURE__ */ Object.create(null) };
3271
+ }
3272
+ function sanitize(raw) {
3273
+ if (!raw || typeof raw !== "object") return emptyManifest();
3274
+ const parsed = raw;
3275
+ if (parsed.v !== VERSION2 || !parsed.e || typeof parsed.e !== "object") return emptyManifest();
3276
+ const out = emptyManifest();
3277
+ for (const [key, value] of Object.entries(parsed.e)) {
3278
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
3279
+ if (!value || typeof value !== "object") continue;
3280
+ const entry = value;
3281
+ if (typeof entry.f !== "string" || typeof entry.t !== "number") continue;
3282
+ out.e[key] = { f: entry.f, r: entry.r ?? null, t: entry.t };
3283
+ }
3284
+ return out;
3285
+ }
3286
+ async function load() {
3287
+ const path = manifestPath();
3288
+ if (loaded && loadedFrom === path) return loaded;
3289
+ loaded = emptyManifest();
3290
+ loadedFrom = path;
3291
+ dirty = false;
3292
+ try {
3293
+ const s = await stat6(path);
3294
+ if (s.size > MAX_MANIFEST_BYTES) return loaded;
3295
+ chargeRead(s.size);
3296
+ loaded = sanitize(JSON.parse(await readFile7(path, "utf8")));
3297
+ } catch (err) {
3298
+ if (err instanceof ResourceBudgetError) throw err;
3299
+ }
3300
+ return loaded;
3301
+ }
3302
+ function evict(manifest) {
3303
+ const cutoff = Date.now() - MAX_AGE_MS;
3304
+ for (const [key, entry] of Object.entries(manifest.e)) {
3305
+ if (entry.t < cutoff) delete manifest.e[key];
3306
+ }
3307
+ const keys = Object.keys(manifest.e);
3308
+ if (keys.length <= MAX_ENTRIES) return;
3309
+ keys.sort((a, b) => manifest.e[a].t - manifest.e[b].t);
3310
+ for (const key of keys.slice(0, keys.length - MAX_ENTRIES)) delete manifest.e[key];
3311
+ }
3312
+ async function cachedDiscoveryHeader(kind, path, stats, compute) {
3313
+ const manifest = await load();
3314
+ const key = `${kind}\0${path}`;
3315
+ const fingerprint2 = fingerprintOf(stats);
3316
+ const hit = manifest.e[key];
3317
+ if (hit && hit.f === fingerprint2) {
3318
+ const now = Date.now();
3319
+ if (now - hit.t > TOUCH_INTERVAL_MS) {
3320
+ hit.t = now;
3321
+ dirty = true;
3322
+ }
3323
+ return hit.r;
3324
+ }
3325
+ const value = await compute();
3326
+ const after = await stat6(path).catch(() => null);
3327
+ if (!after || fingerprintOf(after) !== fingerprint2) return value;
3328
+ manifest.e[key] = { f: fingerprint2, r: value ?? null, t: Date.now() };
3329
+ dirty = true;
3330
+ return value;
3331
+ }
3332
+ async function flushDiscoveryCache() {
3333
+ if (!dirty || !loaded) return;
3334
+ const mine = loaded;
3335
+ dirty = false;
3336
+ const path = manifestPath();
3337
+ const dir = getCacheDir();
3338
+ try {
3339
+ await mkdir8(dir, { recursive: true });
3340
+ const release = await acquireFileLock(join10(dir, "discovery-index.lock"));
3341
+ try {
3342
+ const onDisk = await readFile7(path, "utf8").then((raw) => sanitize(JSON.parse(raw))).catch(() => emptyManifest());
3343
+ for (const [key, entry] of Object.entries(mine.e)) {
3344
+ const other = onDisk.e[key];
3345
+ if (!other || other.t <= entry.t) onDisk.e[key] = entry;
3346
+ }
3347
+ evict(onDisk);
3348
+ const payload = JSON.stringify(onDisk);
3349
+ if (Buffer.byteLength(payload) > MAX_MANIFEST_BYTES) return;
3350
+ const tmp = `${path}.${randomUUID2()}.tmp`;
3351
+ try {
3352
+ await writeFile3(tmp, payload, { mode: 384 });
3353
+ await rename6(tmp, path);
3354
+ } finally {
3355
+ await unlink5(tmp).catch(() => {
3356
+ });
3357
+ }
3358
+ loaded = onDisk;
3359
+ } finally {
3360
+ await release();
3361
+ }
3362
+ } catch {
3363
+ dirty = true;
3364
+ }
3365
+ }
3366
+
3367
+ // src/providers/codex.ts
3368
+ init_fs_utils();
2554
3369
  init_models();
2555
3370
  var modelDisplayNames = {
2556
3371
  "gpt-5.5": "GPT-5.5",
@@ -2575,15 +3390,13 @@ var toolNameMap = {
2575
3390
  read_dir: "Glob"
2576
3391
  };
2577
3392
  function getCodexDir(override) {
2578
- return override ?? process.env["CODEX_HOME"] ?? join8(homedir5(), ".codex");
3393
+ return override ?? process.env["CODEX_HOME"] ?? join11(homedir5(), ".codex");
2579
3394
  }
2580
3395
  function sanitizeProject(cwd) {
2581
3396
  return cwd.replace(/^\//, "").replace(/\//g, "-");
2582
3397
  }
2583
3398
  async function readFirstLine(filePath) {
2584
- const content = await readSessionFile(filePath);
2585
- if (content === null) return null;
2586
- const line = content.split("\n")[0];
3399
+ const line = await readSessionFirstLine(filePath);
2587
3400
  if (!line?.trim()) return null;
2588
3401
  try {
2589
3402
  return JSON.parse(line);
@@ -2591,42 +3404,41 @@ async function readFirstLine(filePath) {
2591
3404
  return null;
2592
3405
  }
2593
3406
  }
2594
- async function isValidCodexSession(filePath) {
3407
+ async function readCodexHeader(filePath) {
2595
3408
  const entry = await readFirstLine(filePath);
2596
- if (!entry) return { valid: false };
2597
- const valid = entry.type === "session_meta" && typeof entry.payload?.originator === "string" && entry.payload.originator.toLowerCase().startsWith("codex");
2598
- return { valid, meta: valid ? entry : void 0 };
3409
+ const valid = entry?.type === "session_meta" && typeof entry.payload?.originator === "string" && entry.payload.originator.toLowerCase().startsWith("codex");
3410
+ if (!valid) return null;
3411
+ return { project: sanitizeProject(entry.payload?.cwd ?? "unknown") };
2599
3412
  }
2600
3413
  async function discoverSessionsInDir(codexDir) {
2601
- const sessionsDir = join8(codexDir, "sessions");
3414
+ const sessionsDir = join11(codexDir, "sessions");
2602
3415
  const sources = [];
2603
3416
  let years;
2604
3417
  try {
2605
- years = await readdir3(sessionsDir);
3418
+ years = await readdir5(sessionsDir);
2606
3419
  } catch {
2607
3420
  return sources;
2608
3421
  }
2609
3422
  for (const year of years) {
2610
3423
  if (!/^\d{4}$/.test(year)) continue;
2611
- const yearDir = join8(sessionsDir, year);
2612
- const months = await readdir3(yearDir).catch(() => []);
3424
+ const yearDir = join11(sessionsDir, year);
3425
+ const months = await readdir5(yearDir).catch(() => []);
2613
3426
  for (const month of months) {
2614
3427
  if (!/^\d{2}$/.test(month)) continue;
2615
- const monthDir = join8(yearDir, month);
2616
- const days = await readdir3(monthDir).catch(() => []);
3428
+ const monthDir = join11(yearDir, month);
3429
+ const days = await readdir5(monthDir).catch(() => []);
2617
3430
  for (const day of days) {
2618
3431
  if (!/^\d{2}$/.test(day)) continue;
2619
- const dayDir = join8(monthDir, day);
2620
- const files = await readdir3(dayDir).catch(() => []);
3432
+ const dayDir = join11(monthDir, day);
3433
+ const files = await readdir5(dayDir).catch(() => []);
2621
3434
  for (const file of files) {
2622
3435
  if (!file.startsWith("rollout-") || !file.endsWith(".jsonl")) continue;
2623
- const filePath = join8(dayDir, file);
2624
- const s = await stat5(filePath).catch(() => null);
3436
+ const filePath = join11(dayDir, file);
3437
+ const s = await stat7(filePath).catch(() => null);
2625
3438
  if (!s?.isFile()) continue;
2626
- const { valid, meta } = await isValidCodexSession(filePath);
2627
- if (!valid || !meta) continue;
2628
- const cwd = meta.payload?.cwd ?? "unknown";
2629
- sources.push({ path: filePath, project: sanitizeProject(cwd), provider: "codex" });
3439
+ const header = await cachedDiscoveryHeader("codex", filePath, s, () => readCodexHeader(filePath));
3440
+ if (!header) continue;
3441
+ sources.push({ path: filePath, project: header.project, provider: "codex" });
2630
3442
  }
2631
3443
  }
2632
3444
  }
@@ -2641,10 +3453,6 @@ function createParser(source, seenKeys) {
2641
3453
  return {
2642
3454
  warnings: parserWarnings,
2643
3455
  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
3456
  let hasSessionMeta = false;
2649
3457
  let hasTokenCounts = false;
2650
3458
  let sessionModel;
@@ -2656,7 +3464,10 @@ function createParser(source, seenKeys) {
2656
3464
  let prevReasoning = 0;
2657
3465
  let pendingTools = [];
2658
3466
  let pendingUserMessage = "";
2659
- for (const line of lines) {
3467
+ let lineCount = 0;
3468
+ for await (const line of readSessionLines(source.path)) {
3469
+ if (!line.trim()) continue;
3470
+ lineCount++;
2660
3471
  let entry;
2661
3472
  try {
2662
3473
  entry = JSON.parse(line);
@@ -2760,10 +3571,10 @@ function createParser(source, seenKeys) {
2760
3571
  pendingUserMessage = "";
2761
3572
  }
2762
3573
  }
2763
- if (lines.length > 5 && !hasSessionMeta) {
3574
+ if (lineCount > 5 && !hasSessionMeta) {
2764
3575
  parserWarnings.push(`Codex session file format may have changed \u2014 missing session_meta (${basename3(source.path)})`);
2765
3576
  }
2766
- if (lines.length > 5 && hasSessionMeta && !hasTokenCounts) {
3577
+ if (lineCount > 5 && hasSessionMeta && !hasTokenCounts) {
2767
3578
  parserWarnings.push(`Codex session file format may have changed \u2014 no token_count events found (${basename3(source.path)})`);
2768
3579
  }
2769
3580
  }
@@ -2784,7 +3595,11 @@ function createCodexProvider(codexDir) {
2784
3595
  return toolNameMap[rawTool] ?? rawTool;
2785
3596
  },
2786
3597
  async discoverSessions() {
2787
- return discoverSessionsInDir(dir);
3598
+ try {
3599
+ return await discoverSessionsInDir(dir);
3600
+ } finally {
3601
+ await flushDiscoveryCache();
3602
+ }
2788
3603
  },
2789
3604
  createSessionParser(source, seenKeys) {
2790
3605
  return createParser(source, seenKeys);
@@ -2794,10 +3609,11 @@ function createCodexProvider(codexDir) {
2794
3609
  var codex = createCodexProvider();
2795
3610
 
2796
3611
  // 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";
3612
+ init_fs_utils();
2800
3613
  init_models();
3614
+ import { readdir as readdir6, stat as stat8 } from "fs/promises";
3615
+ import { basename as basename4, dirname as dirname3, join as join12 } from "path";
3616
+ import { homedir as homedir6 } from "os";
2801
3617
  var modelDisplayNames2 = {
2802
3618
  "gpt-4.1-nano": "GPT-4.1 Nano",
2803
3619
  "gpt-4.1-mini": "GPT-4.1 Mini",
@@ -2829,7 +3645,7 @@ var toolNameMap2 = {
2829
3645
  };
2830
3646
  var modelDisplayEntries2 = Object.entries(modelDisplayNames2).sort((a, b) => b[0].length - a[0].length);
2831
3647
  function getCopilotSessionStateDir(override) {
2832
- return override ?? join9(homedir6(), ".copilot", "session-state");
3648
+ return override ?? join12(homedir6(), ".copilot", "session-state");
2833
3649
  }
2834
3650
  function parseCwd(yaml) {
2835
3651
  const match = yaml.match(/^cwd:\s*(.+)$/m);
@@ -2840,13 +3656,13 @@ function parseCwd(yaml) {
2840
3656
  function createParser2(source, seenKeys) {
2841
3657
  return {
2842
3658
  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());
3659
+ const sessionId = basename4(dirname3(source.path));
2847
3660
  let currentModel = "";
2848
3661
  let pendingUserMessage = "";
2849
- for (const line of lines) {
3662
+ let lineCount = 0;
3663
+ for await (const line of readSessionLines(source.path)) {
3664
+ if (!line.trim()) continue;
3665
+ lineCount++;
2850
3666
  let event;
2851
3667
  try {
2852
3668
  event = JSON.parse(line);
@@ -2899,16 +3715,16 @@ async function discoverSessionsInDir2(sessionStateDir) {
2899
3715
  const sources = [];
2900
3716
  let sessionDirs;
2901
3717
  try {
2902
- sessionDirs = await readdir4(sessionStateDir);
3718
+ sessionDirs = await readdir6(sessionStateDir);
2903
3719
  } catch {
2904
3720
  return sources;
2905
3721
  }
2906
3722
  for (const sessionId of sessionDirs) {
2907
- const eventsPath = join9(sessionStateDir, sessionId, "events.jsonl");
2908
- const s = await stat6(eventsPath).catch(() => null);
3723
+ const eventsPath = join12(sessionStateDir, sessionId, "events.jsonl");
3724
+ const s = await stat8(eventsPath).catch(() => null);
2909
3725
  if (!s?.isFile()) continue;
2910
3726
  let project = sessionId;
2911
- const yaml = await readSessionFile(join9(sessionStateDir, sessionId, "workspace.yaml"));
3727
+ const yaml = await readSessionFile(join12(sessionStateDir, sessionId, "workspace.yaml"));
2912
3728
  if (yaml !== null) {
2913
3729
  const cwd = parseCwd(yaml);
2914
3730
  if (cwd) project = basename4(cwd);
@@ -2942,11 +3758,12 @@ function createCopilotProvider(sessionStateDir) {
2942
3758
  var copilot = createCopilotProvider();
2943
3759
 
2944
3760
  // 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";
3761
+ init_fs_utils();
2948
3762
  init_models();
2949
3763
  init_bash_utils();
3764
+ import { readdir as readdir7, stat as stat9 } from "fs/promises";
3765
+ import { basename as basename6, join as join13 } from "path";
3766
+ import { homedir as homedir7 } from "os";
2950
3767
  var modelDisplayNames3 = {
2951
3768
  "gpt-5.4": "GPT-5.4",
2952
3769
  "gpt-5.4-mini": "GPT-5.4 Mini",
@@ -2970,15 +3787,13 @@ var toolNameMap3 = {
2970
3787
  };
2971
3788
  var modelDisplayEntries3 = Object.entries(modelDisplayNames3).sort((a, b) => b[0].length - a[0].length);
2972
3789
  function getPiSessionsDir(override) {
2973
- return override ?? join10(homedir7(), ".pi", "agent", "sessions");
3790
+ return override ?? join13(homedir7(), ".pi", "agent", "sessions");
2974
3791
  }
2975
3792
  function getOmpSessionsDir(override) {
2976
- return override ?? join10(homedir7(), ".omp", "agent", "sessions");
3793
+ return override ?? join13(homedir7(), ".omp", "agent", "sessions");
2977
3794
  }
2978
3795
  async function readFirstEntry(filePath) {
2979
- const content = await readSessionFile(filePath);
2980
- if (content === null) return null;
2981
- const line = content.split("\n")[0];
3796
+ const line = await readSessionFirstLine(filePath);
2982
3797
  if (!line?.trim()) return null;
2983
3798
  try {
2984
3799
  return JSON.parse(line);
@@ -2990,24 +3805,24 @@ async function discoverSessionsInDir3(sessionsDir, providerName) {
2990
3805
  const sources = [];
2991
3806
  let projectDirs;
2992
3807
  try {
2993
- projectDirs = await readdir5(sessionsDir);
3808
+ projectDirs = await readdir7(sessionsDir);
2994
3809
  } catch {
2995
3810
  return sources;
2996
3811
  }
2997
3812
  for (const dirName of projectDirs) {
2998
- const dirPath = join10(sessionsDir, dirName);
2999
- const dirStat = await stat7(dirPath).catch(() => null);
3813
+ const dirPath = join13(sessionsDir, dirName);
3814
+ const dirStat = await stat9(dirPath).catch(() => null);
3000
3815
  if (!dirStat?.isDirectory()) continue;
3001
3816
  let files;
3002
3817
  try {
3003
- files = await readdir5(dirPath);
3818
+ files = await readdir7(dirPath);
3004
3819
  } catch {
3005
3820
  continue;
3006
3821
  }
3007
3822
  for (const file of files) {
3008
3823
  if (!file.endsWith(".jsonl")) continue;
3009
- const filePath = join10(dirPath, file);
3010
- const fileStat = await stat7(filePath).catch(() => null);
3824
+ const filePath = join13(dirPath, file);
3825
+ const fileStat = await stat9(filePath).catch(() => null);
3011
3826
  if (!fileStat?.isFile()) continue;
3012
3827
  const first = await readFirstEntry(filePath);
3013
3828
  if (!first || first.type !== "session") continue;
@@ -3020,12 +3835,12 @@ async function discoverSessionsInDir3(sessionsDir, providerName) {
3020
3835
  function createParser3(source, seenKeys) {
3021
3836
  return {
3022
3837
  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
3838
  let sessionId = basename6(source.path, ".jsonl");
3027
3839
  let pendingUserMessage = "";
3028
- for (const [lineIdx, line] of lines.entries()) {
3840
+ let lineIdx = -1;
3841
+ for await (const line of readSessionLines(source.path)) {
3842
+ if (!line.trim()) continue;
3843
+ lineIdx++;
3029
3844
  let entry;
3030
3845
  try {
3031
3846
  entry = JSON.parse(line);
@@ -3184,40 +3999,22 @@ var _lastDiscoveryWarnings = [];
3184
3999
  function getDiscoveryWarnings() {
3185
4000
  return _lastDiscoveryWarnings;
3186
4001
  }
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
4002
  async function discoverAllSessions(providerFilter) {
3197
4003
  _lastDiscoveryWarnings = [];
3198
4004
  const allProviders = await getAllProviders();
3199
4005
  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();
4006
+ const results = [];
4007
+ for (const provider of filtered) {
4008
+ chargeRead(0);
4009
+ try {
4010
+ results.push(...await provider.discoverSessions());
4011
+ } catch (err) {
4012
+ if (err instanceof ResourceBudgetError) throw err;
4013
+ const msg = err instanceof Error ? err.message : String(err);
4014
+ _lastDiscoveryWarnings.push(`Provider "${provider.name}" discovery failed: ${msg}`);
4015
+ }
4016
+ }
4017
+ return results;
3221
4018
  }
3222
4019
  async function getProvider(name) {
3223
4020
  if (name === "cursor") {
@@ -3242,11 +4039,35 @@ var BUILD_PATTERNS = /\b(npm\s+run\s+build|npm\s+publish|pip\s+install|docker|de
3242
4039
  var INSTALL_PATTERNS = /\b(npm\s+install|pip\s+install|brew\s+install|apt\s+install|cargo\s+add)\b/i;
3243
4040
  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
4041
  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;
4042
+ var REFACTOR_KEYWORDS = /\b(refactor|clean\s*up|rename|reorganize|simplify|extract|restructure|move|migrate|split)\b/i;
3245
4043
  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
4044
  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
4045
  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
4046
  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
4047
  var URL_PATTERN = /https?:\/\/\S+/i;
4048
+ var MESSAGE_PATTERNS = [
4049
+ TEST_PATTERNS,
4050
+ GIT_PATTERNS,
4051
+ BUILD_PATTERNS,
4052
+ INSTALL_PATTERNS,
4053
+ DEBUG_KEYWORDS,
4054
+ FEATURE_KEYWORDS,
4055
+ REFACTOR_KEYWORDS,
4056
+ BRAINSTORM_KEYWORDS,
4057
+ RESEARCH_KEYWORDS,
4058
+ FILE_PATTERNS,
4059
+ SCRIPT_PATTERNS,
4060
+ URL_PATTERN
4061
+ ];
4062
+ function compactUserMessage(text, signals) {
4063
+ return {
4064
+ userMessage: Buffer.from(text.slice(0, 512)).toString("utf8"),
4065
+ userMessageSignals: signals ?? MESSAGE_PATTERNS.reduce((mask, pattern, index) => mask | (pattern.test(text) ? 1 << index : 0), 0)
4066
+ };
4067
+ }
4068
+ function messageMatches(turn, pattern) {
4069
+ return turn.userMessageSignals === void 0 ? pattern.test(turn.userMessage) : (turn.userMessageSignals & 1 << MESSAGE_PATTERNS.indexOf(pattern)) !== 0;
4070
+ }
3250
4071
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "FileEditTool", "FileWriteTool", "NotebookEdit", "cursor:edit"]);
3251
4072
  var READ_TOOLS = /* @__PURE__ */ new Set(["Read", "Grep", "Glob", "FileReadTool", "GrepTool", "GlobTool"]);
3252
4073
  var BASH_TOOLS = /* @__PURE__ */ new Set(["Bash", "BashTool", "PowerShellTool"]);
@@ -3289,11 +4110,10 @@ function classifyByToolPattern(turn) {
3289
4110
  const hasMcp = hasMcpTools(tools);
3290
4111
  const hasSkill = hasSkillTool(tools);
3291
4112
  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";
4113
+ if (messageMatches(turn, TEST_PATTERNS)) return "testing";
4114
+ if (messageMatches(turn, GIT_PATTERNS)) return "devops";
4115
+ if (messageMatches(turn, BUILD_PATTERNS)) return "devops";
4116
+ if (messageMatches(turn, INSTALL_PATTERNS)) return "devops";
3297
4117
  }
3298
4118
  if (hasEdits) return "building";
3299
4119
  if (hasBash && hasReads) return "research";
@@ -3304,25 +4124,25 @@ function classifyByToolPattern(turn) {
3304
4124
  if (hasSkill) return "building";
3305
4125
  return null;
3306
4126
  }
3307
- function refineByKeywords(category, userMessage) {
4127
+ function refineByKeywords(category, turn) {
3308
4128
  if (category === "building") {
3309
- if (DEBUG_KEYWORDS.test(userMessage)) return "debugging";
4129
+ if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
3310
4130
  return "building";
3311
4131
  }
3312
4132
  if (category === "research") {
3313
- if (DEBUG_KEYWORDS.test(userMessage)) return "debugging";
4133
+ if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
3314
4134
  return "research";
3315
4135
  }
3316
4136
  return category;
3317
4137
  }
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";
4138
+ function classifyConversation(turn) {
4139
+ if (messageMatches(turn, BRAINSTORM_KEYWORDS)) return "research";
4140
+ if (messageMatches(turn, RESEARCH_KEYWORDS)) return "research";
4141
+ if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
4142
+ if (messageMatches(turn, FEATURE_KEYWORDS)) return "building";
4143
+ if (messageMatches(turn, FILE_PATTERNS)) return "building";
4144
+ if (messageMatches(turn, SCRIPT_PATTERNS)) return "building";
4145
+ if (messageMatches(turn, URL_PATTERN)) return "research";
3326
4146
  return "research";
3327
4147
  }
3328
4148
  function countRetries(turn) {
@@ -3350,13 +4170,13 @@ function classifyTurn(turn) {
3350
4170
  const tools = getAllTools(turn);
3351
4171
  let category;
3352
4172
  if (tools.length === 0) {
3353
- category = classifyConversation(turn.userMessage);
4173
+ category = classifyConversation(turn);
3354
4174
  } else {
3355
4175
  const toolCategory = classifyByToolPattern(turn);
3356
4176
  if (toolCategory) {
3357
- category = refineByKeywords(toolCategory, turn.userMessage);
4177
+ category = refineByKeywords(toolCategory, turn);
3358
4178
  } else {
3359
- category = classifyConversation(turn.userMessage);
4179
+ category = classifyConversation(turn);
3360
4180
  }
3361
4181
  }
3362
4182
  return { ...turn, category, retries: countRetries(turn), hasEdits: turnHasEdits(turn) };
@@ -3364,10 +4184,10 @@ function classifyTurn(turn) {
3364
4184
 
3365
4185
  // src/session-index.ts
3366
4186
  init_cache_dir();
3367
- import { stat as stat10 } from "fs/promises";
4187
+ import { stat as stat12 } from "fs/promises";
3368
4188
  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";
4189
+ import { readFile as readFile10, mkdir as mkdir10, rename as rename8, unlink as unlink6, open as open7 } from "fs/promises";
4190
+ import { join as join18 } from "path";
3371
4191
  import { randomBytes as randomBytes5 } from "crypto";
3372
4192
  var SESSION_INDEX_VERSION = 1;
3373
4193
  var INDEX_FILENAME = "session-index.json";
@@ -3376,12 +4196,12 @@ function emptyIndex() {
3376
4196
  return { v: SESSION_INDEX_VERSION, e: {} };
3377
4197
  }
3378
4198
  function indexPath() {
3379
- return join15(getCacheDir(), INDEX_FILENAME);
4199
+ return join18(getCacheDir(), INDEX_FILENAME);
3380
4200
  }
3381
4201
  async function loadSessionIndex() {
3382
4202
  if (currentIndex) return currentIndex;
3383
4203
  try {
3384
- const raw = await readFile7(indexPath(), "utf-8");
4204
+ const raw = await readFile10(indexPath(), "utf-8");
3385
4205
  const parsed = JSON.parse(raw);
3386
4206
  if (parsed.v !== SESSION_INDEX_VERSION) {
3387
4207
  currentIndex = emptyIndex();
@@ -3396,11 +4216,11 @@ async function loadSessionIndex() {
3396
4216
  }
3397
4217
  async function saveSessionIndex(index) {
3398
4218
  const dir = getCacheDir();
3399
- if (!existsSync3(dir)) await mkdir7(dir, { recursive: true });
4219
+ if (!existsSync3(dir)) await mkdir10(dir, { recursive: true });
3400
4220
  const finalPath = indexPath();
3401
4221
  const tempPath = `${finalPath}.${randomBytes5(8).toString("hex")}.tmp`;
3402
4222
  const payload = JSON.stringify(index);
3403
- const handle = await open5(tempPath, "w", 384);
4223
+ const handle = await open7(tempPath, "w", 384);
3404
4224
  try {
3405
4225
  await handle.writeFile(payload, { encoding: "utf-8" });
3406
4226
  await handle.sync();
@@ -3408,10 +4228,10 @@ async function saveSessionIndex(index) {
3408
4228
  await handle.close();
3409
4229
  }
3410
4230
  try {
3411
- await rename6(tempPath, finalPath);
4231
+ await rename8(tempPath, finalPath);
3412
4232
  } catch (err) {
3413
4233
  try {
3414
- await unlink3(tempPath);
4234
+ await unlink6(tempPath);
3415
4235
  } catch {
3416
4236
  }
3417
4237
  throw err;
@@ -3423,7 +4243,7 @@ async function checkSessionFile(filePath, index, dateRange) {
3423
4243
  if (!entry) return "parse";
3424
4244
  let fileStat;
3425
4245
  try {
3426
- fileStat = await stat10(filePath);
4246
+ fileStat = await stat12(filePath);
3427
4247
  } catch {
3428
4248
  return "parse";
3429
4249
  }
@@ -3434,7 +4254,9 @@ async function checkSessionFile(filePath, index, dateRange) {
3434
4254
  function recordParseResult(filePath, index, sizeBytes, mtimeMs, hasApiCalls) {
3435
4255
  const prev = index.e[filePath];
3436
4256
  const h = hasApiCalls ? 1 : prev?.h === 1 ? 1 : 0;
4257
+ if (prev?.s === sizeBytes && prev?.m === mtimeMs && prev?.h === h) return false;
3437
4258
  index.e[filePath] = { s: sizeBytes, m: mtimeMs, h };
4259
+ return true;
3438
4260
  }
3439
4261
  function pruneIndex(index, knownPaths) {
3440
4262
  let pruned = 0;
@@ -3449,6 +4271,7 @@ function pruneIndex(index, knownPaths) {
3449
4271
 
3450
4272
  // src/parser.ts
3451
4273
  init_bash_utils();
4274
+ var reportMode = new AsyncLocalStorage2();
3452
4275
  var _parseWarnings = [];
3453
4276
  function getParseWarnings() {
3454
4277
  return _parseWarnings;
@@ -3532,25 +4355,34 @@ function parseApiCall(entry) {
3532
4355
  deduplicationKey: msg.id ?? `claude:${entry.timestamp}`
3533
4356
  };
3534
4357
  }
3535
- function groupIntoTurns(entries, seenMsgIds) {
4358
+ async function parseTurns(filePath) {
4359
+ const seenMsgIds = /* @__PURE__ */ new Set();
3536
4360
  const turns = [];
3537
4361
  let currentUserMessage = "";
4362
+ let currentUserSignals = 0;
3538
4363
  let currentCalls = [];
3539
4364
  let currentTimestamp = "";
3540
4365
  let currentSessionId = "";
3541
- for (const entry of entries) {
4366
+ let lineNumber = 0;
4367
+ for await (const line of readSessionLines(filePath)) {
4368
+ lineNumber++;
4369
+ const entry = parseJsonlLine(line);
4370
+ if (!entry) continue;
3542
4371
  if (entry.type === "user") {
3543
4372
  const text = getUserMessageText(entry);
3544
4373
  if (text.trim()) {
3545
4374
  if (currentCalls.length > 0) {
3546
4375
  turns.push({
3547
4376
  userMessage: currentUserMessage,
4377
+ userMessageSignals: currentUserSignals,
3548
4378
  assistantCalls: currentCalls,
3549
4379
  timestamp: currentTimestamp,
3550
4380
  sessionId: currentSessionId
3551
4381
  });
3552
4382
  }
3553
- currentUserMessage = text;
4383
+ const compact = compactUserMessage(text);
4384
+ currentUserMessage = compact.userMessage;
4385
+ currentUserSignals = compact.userMessageSignals;
3554
4386
  currentCalls = [];
3555
4387
  currentTimestamp = entry.timestamp ?? "";
3556
4388
  currentSessionId = entry.sessionId ?? "";
@@ -3560,12 +4392,17 @@ function groupIntoTurns(entries, seenMsgIds) {
3560
4392
  if (msgId && seenMsgIds.has(msgId)) continue;
3561
4393
  if (msgId) seenMsgIds.add(msgId);
3562
4394
  const call = parseApiCall(entry);
3563
- if (call) currentCalls.push(call);
4395
+ if (call) {
4396
+ if (!msgId) call.deduplicationKey = `${filePath}:${lineNumber}`;
4397
+ chargeRecord();
4398
+ currentCalls.push(call);
4399
+ }
3564
4400
  }
3565
4401
  }
3566
4402
  if (currentCalls.length > 0) {
3567
4403
  turns.push({
3568
4404
  userMessage: currentUserMessage,
4405
+ userMessageSignals: currentUserSignals,
3569
4406
  assistantCalls: currentCalls,
3570
4407
  timestamp: currentTimestamp,
3571
4408
  sessionId: currentSessionId
@@ -3637,7 +4474,7 @@ function buildSessionSummary(sessionId, project, turns) {
3637
4474
  if (!lastTs || call.timestamp > lastTs) lastTs = call.timestamp;
3638
4475
  }
3639
4476
  }
3640
- return {
4477
+ const summary = {
3641
4478
  sessionId,
3642
4479
  project,
3643
4480
  firstTimestamp: firstTs || turns[0]?.timestamp || "",
@@ -3655,26 +4492,28 @@ function buildSessionSummary(sessionId, project, turns) {
3655
4492
  bashBreakdown,
3656
4493
  categoryBreakdown
3657
4494
  };
4495
+ return reportMode.getStore() ? compactReportSession(summary) : summary;
3658
4496
  }
3659
4497
  async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
3660
4498
  if (dateRange) {
3661
4499
  try {
3662
- const s = await stat11(filePath);
4500
+ const s = await stat13(filePath);
3663
4501
  if (s.mtimeMs < dateRange.start.getTime()) return null;
3664
4502
  } catch {
3665
4503
  }
3666
4504
  }
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;
4505
+ const rawTurns = await cachedFileParse(filePath, "claude-turns", () => parseTurns(filePath));
3676
4506
  const sessionId = basename8(filePath, ".jsonl");
3677
- let turns = groupIntoTurns(entries, seenMsgIds);
4507
+ let turns = [];
4508
+ for (const turn of rawTurns) {
4509
+ const calls = turn.assistantCalls.filter((call) => {
4510
+ chargeRecord();
4511
+ if (seenMsgIds.has(call.deduplicationKey)) return false;
4512
+ seenMsgIds.add(call.deduplicationKey);
4513
+ return true;
4514
+ });
4515
+ if (calls.length) turns.push({ ...turn, ...compactUserMessage(turn.userMessage, turn.userMessageSignals), assistantCalls: calls });
4516
+ }
3678
4517
  if (dateRange) {
3679
4518
  turns = turns.filter((turn) => {
3680
4519
  if (turn.assistantCalls.length === 0) return false;
@@ -3688,15 +4527,15 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
3688
4527
  const classified = turns.map(classifyTurn);
3689
4528
  return buildSessionSummary(sessionId, project, classified);
3690
4529
  }
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));
4530
+ async function collectJsonlFiles2(dirPath) {
4531
+ const files = await readdir10(dirPath).catch(() => []);
4532
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => join19(dirPath, f));
3694
4533
  for (const entry of files) {
3695
4534
  if (entry.endsWith(".jsonl")) continue;
3696
- const subagentsPath = join16(dirPath, entry, "subagents");
3697
- const subFiles = await readdir8(subagentsPath).catch(() => []);
4535
+ const subagentsPath = join19(dirPath, entry, "subagents");
4536
+ const subFiles = await readdir10(subagentsPath).catch(() => []);
3698
4537
  for (const sf of subFiles) {
3699
- if (sf.endsWith(".jsonl")) jsonlFiles.push(join16(subagentsPath, sf));
4538
+ if (sf.endsWith(".jsonl")) jsonlFiles.push(join19(subagentsPath, sf));
3700
4539
  }
3701
4540
  }
3702
4541
  return jsonlFiles;
@@ -3707,14 +4546,14 @@ async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
3707
4546
  let indexDirty = false;
3708
4547
  const allJsonlPaths = /* @__PURE__ */ new Set();
3709
4548
  for (const { path: dirPath, name: dirName } of dirs) {
3710
- const jsonlFiles = await collectJsonlFiles(dirPath);
4549
+ const jsonlFiles = await collectJsonlFiles2(dirPath);
3711
4550
  for (const f of jsonlFiles) allJsonlPaths.add(f);
3712
4551
  for (const filePath of jsonlFiles) {
3713
4552
  const action = await checkSessionFile(filePath, sessionIndex, dateRange);
3714
4553
  if (action === "skip") continue;
3715
4554
  let preStat = null;
3716
4555
  try {
3717
- const s = await stat11(filePath);
4556
+ const s = await stat13(filePath);
3718
4557
  preStat = { size: s.size, mtimeMs: s.mtimeMs };
3719
4558
  } catch {
3720
4559
  }
@@ -3722,10 +4561,9 @@ async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
3722
4561
  const hasApiCalls = session !== null && session.apiCalls > 0;
3723
4562
  if (preStat && (hasApiCalls || !dateRange)) {
3724
4563
  try {
3725
- const postStat = await stat11(filePath);
4564
+ const postStat = await stat13(filePath);
3726
4565
  if (postStat.size === preStat.size && postStat.mtimeMs === preStat.mtimeMs) {
3727
- recordParseResult(filePath, sessionIndex, preStat.size, preStat.mtimeMs, hasApiCalls);
3728
- indexDirty = true;
4566
+ indexDirty = recordParseResult(filePath, sessionIndex, preStat.size, preStat.mtimeMs, hasApiCalls) || indexDirty;
3729
4567
  }
3730
4568
  } catch {
3731
4569
  }
@@ -3777,7 +4615,7 @@ function providerCallToTurn(call) {
3777
4615
  deduplicationKey: call.deduplicationKey
3778
4616
  };
3779
4617
  return {
3780
- userMessage: call.userMessage,
4618
+ ...compactUserMessage(call.userMessage, call.userMessageSignals),
3781
4619
  assistantCalls: [apiCall],
3782
4620
  timestamp: call.timestamp,
3783
4621
  sessionId: call.sessionId
@@ -3787,19 +4625,36 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3787
4625
  const provider = await getProvider(providerName);
3788
4626
  if (!provider) return [];
3789
4627
  const sessionMap = /* @__PURE__ */ new Map();
4628
+ const compactSessions = /* @__PURE__ */ new Map();
3790
4629
  for (const source of sources) {
3791
4630
  if (dateRange) {
3792
4631
  try {
3793
- const s = await stat11(source.path);
3794
- if (s.mtimeMs < dateRange.start.getTime()) continue;
4632
+ const s = await stat13(source.path);
4633
+ const wal = await stat13(`${source.path}-wal`).catch(() => null);
4634
+ if (Math.max(s.mtimeMs, wal?.mtimeMs ?? 0) < dateRange.start.getTime()) continue;
3795
4635
  } catch {
3796
4636
  }
3797
4637
  }
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()) {
4638
+ const warnings = [];
4639
+ const calls = await cachedFileParse(source.path, providerName, async () => {
4640
+ const parser = provider.createSessionParser(
4641
+ { path: source.path, project: source.project, provider: providerName },
4642
+ /* @__PURE__ */ new Set()
4643
+ );
4644
+ const result = [];
4645
+ for await (const call of parser.parse()) {
4646
+ chargeRecord();
4647
+ result.push({ ...call, ...compactUserMessage(call.userMessage, call.userMessageSignals) });
4648
+ }
4649
+ if (parser.warnings?.length) warnings.push(...parser.warnings);
4650
+ return result;
4651
+ }, warnings);
4652
+ _parseWarnings.push(...warnings);
4653
+ const touched = /* @__PURE__ */ new Set();
4654
+ for (const call of calls) {
4655
+ chargeRecord();
4656
+ if (seenKeys.has(call.deduplicationKey)) continue;
4657
+ seenKeys.add(call.deduplicationKey);
3803
4658
  if (dateRange) {
3804
4659
  if (!call.timestamp) continue;
3805
4660
  const ts = new Date(call.timestamp);
@@ -3808,6 +4663,7 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3808
4663
  const turn = providerCallToTurn(call);
3809
4664
  const classified = classifyTurn(turn);
3810
4665
  const key = `${providerName}:${call.sessionId}:${source.project}`;
4666
+ touched.add(key);
3811
4667
  const existing = sessionMap.get(key);
3812
4668
  if (existing) {
3813
4669
  existing.turns.push(classified);
@@ -3815,11 +4671,23 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3815
4671
  sessionMap.set(key, { project: source.project, turns: [classified] });
3816
4672
  }
3817
4673
  }
3818
- if (parser.warnings && parser.warnings.length > 0) {
3819
- _parseWarnings.push(...parser.warnings);
4674
+ if (reportMode.getStore()) {
4675
+ for (const key of touched) {
4676
+ const entry = sessionMap.get(key);
4677
+ const summary = buildSessionSummary(key.split(":")[1] ?? key, entry.project, entry.turns);
4678
+ const previous = compactSessions.get(key);
4679
+ if (previous) mergeReportSession(previous, summary);
4680
+ else compactSessions.set(key, summary);
4681
+ sessionMap.delete(key);
4682
+ }
3820
4683
  }
3821
4684
  }
3822
4685
  const projectMap = /* @__PURE__ */ new Map();
4686
+ for (const summary of compactSessions.values()) {
4687
+ const existing = projectMap.get(summary.project) ?? [];
4688
+ existing.push(summary);
4689
+ projectMap.set(summary.project, existing);
4690
+ }
3823
4691
  for (const [key, { project, turns }] of sessionMap) {
3824
4692
  const sessionId = key.split(":")[1] ?? key;
3825
4693
  const session = buildSessionSummary(sessionId, project, turns);
@@ -3842,7 +4710,7 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3842
4710
  return projects;
3843
4711
  }
3844
4712
  var CACHE_TTL_MS3 = 6e4;
3845
- var MAX_CACHE_ENTRIES = 10;
4713
+ var MAX_CACHE_ENTRIES = 1;
3846
4714
  var sessionCache = /* @__PURE__ */ new Map();
3847
4715
  var inFlightSourceContexts = /* @__PURE__ */ new Map();
3848
4716
  var SOURCE_CONTEXT_TTL_MS = 5e3;
@@ -3881,8 +4749,8 @@ async function getSourceContext(providerFilter) {
3881
4749
  }
3882
4750
  const promise = (async () => {
3883
4751
  const sources = await discoverAllSessions(providerFilter);
3884
- const fingerprint = buildCacheFingerprint(sources);
3885
- return { sources, fingerprint };
4752
+ const fingerprint2 = buildCacheFingerprint(sources);
4753
+ return { sources, fingerprint: fingerprint2 };
3886
4754
  })();
3887
4755
  inFlightSourceContexts.set(key, promise);
3888
4756
  try {
@@ -3924,13 +4792,17 @@ function filterProjectsByName(projects, include, exclude) {
3924
4792
  }
3925
4793
  return result;
3926
4794
  }
3927
- async function parseAllSessions(dateRange, providerFilter) {
4795
+ async function parseAllSessions(dateRange, providerFilter, summaryOnly = false, signal) {
4796
+ return reportMode.run(summaryOnly, () => withScanBudget(() => parseAllSessionsWithinBudget(dateRange, providerFilter), summaryOnly ? 2e6 : 2e5, signal, (summaryOnly ? 8 : 1) * 1024 ** 3));
4797
+ }
4798
+ async function parseAllSessionsWithinBudget(dateRange, providerFilter) {
3928
4799
  _parseWarnings = [];
3929
4800
  const sourceContext = await getSourceContext(providerFilter);
3930
4801
  const allSources = sourceContext.sources;
3931
- const key = cacheKey(dateRange, providerFilter, sourceContext.fingerprint);
4802
+ const key = `${reportMode.getStore() ? "summary" : "full"}:${cacheKey(dateRange, providerFilter, sourceContext.fingerprint)}`;
3932
4803
  const cached = sessionCache.get(key);
3933
4804
  if (cached && Date.now() - cached.ts < CACHE_TTL_MS3) return cached.data;
4805
+ sessionCache.clear();
3934
4806
  const seenMsgIds = /* @__PURE__ */ new Set();
3935
4807
  const seenKeys = /* @__PURE__ */ new Set();
3936
4808
  const claudeSources = allSources.filter((s) => s.provider === "claude");
@@ -3963,6 +4835,9 @@ async function parseAllSessions(dateRange, providerFilter) {
3963
4835
  cachePut(key, result);
3964
4836
  return result;
3965
4837
  }
4838
+ function getDiscoveredProviderNames() {
4839
+ return [...new Set([...resolvedSourceContexts.values()].flatMap((c) => c.result.sources.map((s) => s.provider)))];
4840
+ }
3966
4841
 
3967
4842
  // src/format.ts
3968
4843
  import chalk from "chalk";
@@ -4009,14 +4884,14 @@ function renderStatusBar(projects) {
4009
4884
  }
4010
4885
 
4011
4886
  // src/menubar-json.ts
4012
- import { readdirSync, readFileSync as readFileSync2 } from "fs";
4013
- import { join as join17, dirname as dirname3 } from "path";
4887
+ import { readdirSync, readFileSync } from "fs";
4888
+ import { join as join20, dirname as dirname4 } from "path";
4014
4889
  init_models();
4015
4890
  import { fileURLToPath } from "url";
4016
4891
  function getCliVersion() {
4017
4892
  try {
4018
- const pkgPath = join17(dirname3(fileURLToPath(import.meta.url)), "..", "package.json");
4019
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
4893
+ const pkgPath = join20(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
4894
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
4020
4895
  return pkg.version ?? "0.0.0";
4021
4896
  } catch {
4022
4897
  return "0.0.0";
@@ -4168,18 +5043,18 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
4168
5043
 
4169
5044
  // src/daily-cache.ts
4170
5045
  init_cache_dir();
4171
- import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
5046
+ import { createHash as createHash4, randomBytes as randomBytes6 } from "crypto";
4172
5047
  import { existsSync as existsSync4 } from "fs";
4173
- import { mkdir as mkdir8, open as open6, readFile as readFile8, rename as rename7, unlink as unlink4 } from "fs/promises";
4174
- import { join as join18 } from "path";
5048
+ import { mkdir as mkdir11, open as open8, readFile as readFile11, rename as rename9, unlink as unlink7 } from "fs/promises";
5049
+ import { join as join21 } from "path";
4175
5050
  var DAILY_CACHE_VERSION = 6;
4176
5051
  var DEFAULT_DAILY_CACHE_SCOPE = "global";
4177
5052
  var DAILY_CACHE_FILENAME = "daily-cache.json";
4178
5053
  function getCachePath3(scopeKey) {
4179
5054
  if (scopeKey === DEFAULT_DAILY_CACHE_SCOPE) {
4180
- return join18(getCacheDir(), DAILY_CACHE_FILENAME);
5055
+ return join21(getCacheDir(), DAILY_CACHE_FILENAME);
4181
5056
  }
4182
- return join18(getCacheDir(), `daily-cache-${scopeKey}.json`);
5057
+ return join21(getCacheDir(), `daily-cache-${scopeKey}.json`);
4183
5058
  }
4184
5059
  function emptyCache(scopeKey) {
4185
5060
  return { version: DAILY_CACHE_VERSION, scopeKey, lastComputedDate: null, days: [] };
@@ -4194,7 +5069,7 @@ function buildDailyCacheScopeKey(include, exclude) {
4194
5069
  return DEFAULT_DAILY_CACHE_SCOPE;
4195
5070
  }
4196
5071
  const raw = JSON.stringify({ include: normalizedInclude, exclude: normalizedExclude });
4197
- return `scope-${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
5072
+ return `scope-${createHash4("sha1").update(raw).digest("hex").slice(0, 12)}`;
4198
5073
  }
4199
5074
  function isValidCache(parsed) {
4200
5075
  if (!parsed || typeof parsed !== "object") return false;
@@ -4208,7 +5083,7 @@ async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
4208
5083
  const path = getCachePath3(scopeKey);
4209
5084
  if (!existsSync4(path)) return emptyCache(scopeKey);
4210
5085
  try {
4211
- const raw = await readFile8(path, "utf-8");
5086
+ const raw = await readFile11(path, "utf-8");
4212
5087
  const parsed = JSON.parse(raw);
4213
5088
  if (!isValidCache(parsed)) return emptyCache(scopeKey);
4214
5089
  if (parsed.scopeKey !== scopeKey) return emptyCache(scopeKey);
@@ -4219,12 +5094,13 @@ async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
4219
5094
  }
4220
5095
  async function saveDailyCache(cache) {
4221
5096
  const dir = getCacheDir();
4222
- if (!existsSync4(dir)) await mkdir8(dir, { recursive: true });
5097
+ if (!existsSync4(dir)) await mkdir11(dir, { recursive: true });
4223
5098
  const scopeKey = cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE;
4224
5099
  const finalPath = getCachePath3(scopeKey);
4225
5100
  const tempPath = `${finalPath}.${randomBytes6(8).toString("hex")}.tmp`;
4226
5101
  const payload = JSON.stringify({ ...cache, scopeKey });
4227
- const handle = await open6(tempPath, "w", 384);
5102
+ if (await readFile11(finalPath, "utf8").catch(() => "") === payload) return;
5103
+ const handle = await open8(tempPath, "w", 384);
4228
5104
  try {
4229
5105
  await handle.writeFile(payload, { encoding: "utf-8" });
4230
5106
  await handle.sync();
@@ -4232,10 +5108,10 @@ async function saveDailyCache(cache) {
4232
5108
  await handle.close();
4233
5109
  }
4234
5110
  try {
4235
- await rename7(tempPath, finalPath);
5111
+ await rename9(tempPath, finalPath);
4236
5112
  } catch (err) {
4237
5113
  try {
4238
- await unlink4(tempPath);
5114
+ await unlink7(tempPath);
4239
5115
  } catch {
4240
5116
  }
4241
5117
  throw err;
@@ -4253,7 +5129,10 @@ function addNewDays(cache, incoming, newestDate, options = {}) {
4253
5129
  version: DAILY_CACHE_VERSION,
4254
5130
  scopeKey: cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE,
4255
5131
  lastComputedDate: nextLast,
4256
- days: merged
5132
+ days: merged,
5133
+ revalidatedAt: cache.revalidatedAt,
5134
+ backfill: cache.backfill,
5135
+ partialDates: cache.partialDates
4257
5136
  };
4258
5137
  }
4259
5138
  function getDaysInRange(cache, start, end) {
@@ -4266,17 +5145,62 @@ function withDailyCacheLock(fn) {
4266
5145
  return next;
4267
5146
  }
4268
5147
 
5148
+ // src/latest-load.ts
5149
+ var LatestLoad = class {
5150
+ tail = Promise.resolve();
5151
+ generation = 0;
5152
+ controller;
5153
+ pending = 0;
5154
+ get busy() {
5155
+ return this.pending > 0;
5156
+ }
5157
+ cancel() {
5158
+ this.generation++;
5159
+ this.controller?.abort();
5160
+ }
5161
+ async run(load2) {
5162
+ const generation = ++this.generation;
5163
+ this.controller?.abort();
5164
+ const previous = this.tail;
5165
+ this.pending++;
5166
+ const next = (async () => {
5167
+ await previous.catch(() => {
5168
+ });
5169
+ if (generation !== this.generation) return void 0;
5170
+ const controller = new AbortController();
5171
+ this.controller = controller;
5172
+ try {
5173
+ const result = await load2(controller.signal);
5174
+ return generation === this.generation ? result : void 0;
5175
+ } catch (error) {
5176
+ if (generation !== this.generation) return void 0;
5177
+ throw error;
5178
+ } finally {
5179
+ if (this.controller === controller) this.controller = void 0;
5180
+ }
5181
+ })();
5182
+ this.tail = next;
5183
+ try {
5184
+ return await next;
5185
+ } finally {
5186
+ this.pending--;
5187
+ }
5188
+ }
5189
+ };
5190
+
4269
5191
  // src/dashboard.tsx
5192
+ init_resource_budget();
4270
5193
  import { homedir as homedir13 } from "os";
4271
5194
  import { useState as useState2, useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
4272
5195
  import { render as render2, Box as Box2, Text as Text2, useInput as useInput2, useApp as useApp2, useWindowSize } from "ink";
4273
5196
  init_models();
4274
5197
 
4275
5198
  // src/optimize.ts
5199
+ init_fs_utils();
4276
5200
  import chalk2 from "chalk";
4277
- import { readdir as readdir9, stat as stat12 } from "fs/promises";
5201
+ import { readdir as readdir11, stat as stat14 } from "fs/promises";
4278
5202
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4279
- import { basename as basename9, join as join19 } from "path";
5203
+ import { basename as basename9, join as join22 } from "path";
4280
5204
  import { homedir as homedir11 } from "os";
4281
5205
  var ORANGE = "#F59E0B";
4282
5206
  var DIM = "#666666";
@@ -4351,22 +5275,22 @@ var SHELL_PROFILES = [".zshrc", ".bashrc", ".bash_profile", ".profile"];
4351
5275
  var TOP_ITEMS_PREVIEW = 3;
4352
5276
  var GHOST_NAMES_PREVIEW = 5;
4353
5277
  var GHOST_CLEANUP_COMMANDS_LIMIT = 10;
4354
- var FILE_READ_CONCURRENCY = 16;
5278
+ var FILE_READ_CONCURRENCY = 1;
4355
5279
  var RESULT_CACHE_TTL_MS = 6e4;
4356
5280
  var RECENT_WINDOW_HOURS = 48;
4357
5281
  var RECENT_WINDOW_MS = RECENT_WINDOW_HOURS * 60 * 60 * 1e3;
4358
5282
  var DEFAULT_TREND_PERIOD_DAYS = 30;
4359
5283
  var DEFAULT_TREND_PERIOD_MS = DEFAULT_TREND_PERIOD_DAYS * 24 * 60 * 60 * 1e3;
4360
5284
  var IMPROVING_THRESHOLD = 0.5;
4361
- async function collectJsonlFiles2(dirPath) {
4362
- const files = await readdir9(dirPath).catch(() => []);
4363
- const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join19(dirPath, f));
5285
+ async function collectJsonlFiles3(dirPath) {
5286
+ const files = await readdir11(dirPath).catch(() => []);
5287
+ const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join22(dirPath, f));
4364
5288
  for (const entry of files) {
4365
5289
  if (entry.endsWith(".jsonl")) continue;
4366
- const subPath = join19(dirPath, entry, "subagents");
4367
- const subFiles = await readdir9(subPath).catch(() => []);
5290
+ const subPath = join22(dirPath, entry, "subagents");
5291
+ const subFiles = await readdir11(subPath).catch(() => []);
4368
5292
  for (const sf of subFiles) {
4369
- if (sf.endsWith(".jsonl")) result.push(join19(subPath, sf));
5293
+ if (sf.endsWith(".jsonl")) result.push(join22(subPath, sf));
4370
5294
  }
4371
5295
  }
4372
5296
  return result;
@@ -4374,7 +5298,7 @@ async function collectJsonlFiles2(dirPath) {
4374
5298
  async function isFileStaleForRange(filePath, range) {
4375
5299
  if (!range) return false;
4376
5300
  try {
4377
- const s = await stat12(filePath);
5301
+ const s = await stat14(filePath);
4378
5302
  return s.mtimeMs < range.start.getTime();
4379
5303
  } catch {
4380
5304
  return false;
@@ -4467,7 +5391,7 @@ async function scanSessions(dateRange) {
4467
5391
  const allUserMessages = [];
4468
5392
  const tasks = [];
4469
5393
  for (const source of sources) {
4470
- const files = await collectJsonlFiles2(source.path);
5394
+ const files = await collectJsonlFiles3(source.path);
4471
5395
  for (const file of files) {
4472
5396
  if (await isFileStaleForRange(file, dateRange)) continue;
4473
5397
  tasks.push({ file, project: source.project });
@@ -4504,13 +5428,13 @@ function isReadTool(name) {
4504
5428
  function loadMcpConfigs(projectCwds) {
4505
5429
  const servers = /* @__PURE__ */ new Map();
4506
5430
  const configPaths = [
4507
- join19(homedir11(), ".claude", "settings.json"),
4508
- join19(homedir11(), ".claude", "settings.local.json")
5431
+ join22(homedir11(), ".claude", "settings.json"),
5432
+ join22(homedir11(), ".claude", "settings.local.json")
4509
5433
  ];
4510
5434
  for (const cwd of projectCwds) {
4511
- configPaths.push(join19(cwd, ".mcp.json"));
4512
- configPaths.push(join19(cwd, ".claude", "settings.json"));
4513
- configPaths.push(join19(cwd, ".claude", "settings.local.json"));
5435
+ configPaths.push(join22(cwd, ".mcp.json"));
5436
+ configPaths.push(join22(cwd, ".claude", "settings.json"));
5437
+ configPaths.push(join22(cwd, ".claude", "settings.local.json"));
4514
5438
  }
4515
5439
  for (const p of configPaths) {
4516
5440
  if (!existsSync5(p)) continue;
@@ -4663,12 +5587,12 @@ function expandImports(filePath, seen, depth) {
4663
5587
  if (content === null) return { totalLines: 0, importedFiles: 0 };
4664
5588
  let totalLines = content.split("\n").length;
4665
5589
  let importedFiles = 0;
4666
- const dir = join19(filePath, "..");
5590
+ const dir = join22(filePath, "..");
4667
5591
  IMPORT_PATTERN.lastIndex = 0;
4668
5592
  for (const match of content.matchAll(IMPORT_PATTERN)) {
4669
5593
  const rawPath = match[1];
4670
5594
  if (!rawPath) continue;
4671
- const resolved = rawPath.startsWith("/") ? rawPath : join19(dir, rawPath);
5595
+ const resolved = rawPath.startsWith("/") ? rawPath : join22(dir, rawPath);
4672
5596
  if (!existsSync5(resolved)) continue;
4673
5597
  const nested = expandImports(resolved, seen, depth + 1);
4674
5598
  totalLines += nested.totalLines;
@@ -4680,7 +5604,7 @@ function detectBloatedClaudeMd(projectCwds) {
4680
5604
  const bloated = [];
4681
5605
  for (const cwd of projectCwds) {
4682
5606
  for (const name of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
4683
- const fullPath = join19(cwd, name);
5607
+ const fullPath = join22(cwd, name);
4684
5608
  if (!existsSync5(fullPath)) continue;
4685
5609
  const { totalLines, importedFiles } = expandImports(fullPath, /* @__PURE__ */ new Set(), 0);
4686
5610
  if (totalLines > CLAUDEMD_HEALTHY_LINES) {
@@ -4811,7 +5735,7 @@ function detectCacheBloat(apiCalls, projects, dateRange) {
4811
5735
  async function listMarkdownFiles(dir) {
4812
5736
  if (!existsSync5(dir)) return [];
4813
5737
  try {
4814
- const entries = await readdir9(dir);
5738
+ const entries = await readdir11(dir);
4815
5739
  return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(/\.md$/, ""));
4816
5740
  } catch {
4817
5741
  return [];
@@ -4820,10 +5744,10 @@ async function listMarkdownFiles(dir) {
4820
5744
  async function listSkillDirs(dir) {
4821
5745
  if (!existsSync5(dir)) return [];
4822
5746
  try {
4823
- const entries = await readdir9(dir);
5747
+ const entries = await readdir11(dir);
4824
5748
  const names = [];
4825
5749
  for (const entry of entries) {
4826
- if (existsSync5(join19(dir, entry, "SKILL.md"))) names.push(entry);
5750
+ if (existsSync5(join22(dir, entry, "SKILL.md"))) names.push(entry);
4827
5751
  }
4828
5752
  return names;
4829
5753
  } catch {
@@ -4831,7 +5755,7 @@ async function listSkillDirs(dir) {
4831
5755
  }
4832
5756
  }
4833
5757
  async function detectGhostAgents(calls) {
4834
- const defined = await listMarkdownFiles(join19(homedir11(), ".claude", "agents"));
5758
+ const defined = await listMarkdownFiles(join22(homedir11(), ".claude", "agents"));
4835
5759
  if (defined.length === 0) return null;
4836
5760
  const invoked = /* @__PURE__ */ new Set();
4837
5761
  for (const call of calls) {
@@ -4856,7 +5780,7 @@ async function detectGhostAgents(calls) {
4856
5780
  };
4857
5781
  }
4858
5782
  async function detectGhostSkills(calls) {
4859
- const defined = await listSkillDirs(join19(homedir11(), ".claude", "skills"));
5783
+ const defined = await listSkillDirs(join22(homedir11(), ".claude", "skills"));
4860
5784
  if (defined.length === 0) return null;
4861
5785
  const invoked = /* @__PURE__ */ new Set();
4862
5786
  for (const call of calls) {
@@ -4881,7 +5805,7 @@ async function detectGhostSkills(calls) {
4881
5805
  };
4882
5806
  }
4883
5807
  async function detectGhostCommands(userMessages) {
4884
- const defined = await listMarkdownFiles(join19(homedir11(), ".claude", "commands"));
5808
+ const defined = await listMarkdownFiles(join22(homedir11(), ".claude", "commands"));
4885
5809
  if (defined.length === 0) return null;
4886
5810
  const invoked = /* @__PURE__ */ new Set();
4887
5811
  for (const msg of userMessages) {
@@ -4909,7 +5833,7 @@ async function detectGhostCommands(userMessages) {
4909
5833
  }
4910
5834
  function readShellProfileLimit() {
4911
5835
  for (const profile of SHELL_PROFILES) {
4912
- const path = join19(homedir11(), profile);
5836
+ const path = join22(homedir11(), profile);
4913
5837
  if (!existsSync5(path)) continue;
4914
5838
  const content = readSessionFileSync(path);
4915
5839
  if (content === null) continue;
@@ -4992,8 +5916,8 @@ function computeInputCostRate(projects) {
4992
5916
  var resultCache = /* @__PURE__ */ new Map();
4993
5917
  function cacheKey2(projects, dateRange) {
4994
5918
  const dr = dateRange ? `${dateRange.start.getTime()}-${dateRange.end.getTime()}` : "all";
4995
- const fingerprint = projects.length + ":" + projects.reduce((s, p) => s + p.totalApiCalls, 0);
4996
- return `${dr}:${fingerprint}`;
5919
+ const fingerprint2 = projects.length + ":" + projects.reduce((s, p) => s + p.totalApiCalls, 0);
5920
+ return `${dr}:${fingerprint2}`;
4997
5921
  }
4998
5922
  async function scanAndDetect(projects, dateRange, preScanned) {
4999
5923
  if (projects.length === 0) {
@@ -5130,9 +6054,10 @@ async function runOptimize(projects, periodLabel, dateRange, preScanned) {
5130
6054
  }
5131
6055
 
5132
6056
  // src/context-budget.ts
5133
- import { readdir as readdir10 } from "fs/promises";
6057
+ init_fs_utils();
6058
+ import { readdir as readdir12 } from "fs/promises";
5134
6059
  import { existsSync as existsSync6 } from "fs";
5135
- import { join as join20 } from "path";
6060
+ import { join as join23 } from "path";
5136
6061
  import { homedir as homedir12 } from "os";
5137
6062
  var CHARS_PER_TOKEN2 = 4;
5138
6063
  var SYSTEM_BASE_TOKENS = 10400;
@@ -5150,416 +6075,113 @@ async function readConfigFile(path) {
5150
6075
  } catch {
5151
6076
  return null;
5152
6077
  }
5153
- }
5154
- async function countMcpTools(projectPath) {
5155
- const home = homedir12();
5156
- const configPaths = [
5157
- join20(home, ".claude", "settings.json"),
5158
- join20(home, ".claude", "settings.local.json")
5159
- ];
5160
- if (projectPath) {
5161
- configPaths.push(join20(projectPath, ".mcp.json"));
5162
- configPaths.push(join20(projectPath, ".claude", "settings.json"));
5163
- configPaths.push(join20(projectPath, ".claude", "settings.local.json"));
5164
- }
5165
- const servers = /* @__PURE__ */ new Set();
5166
- let toolCount = 0;
5167
- for (const p of configPaths) {
5168
- const config = await readConfigFile(p);
5169
- if (!config) continue;
5170
- const mcpServers = config.mcpServers ?? {};
5171
- for (const name of Object.keys(mcpServers)) {
5172
- if (servers.has(name)) continue;
5173
- servers.add(name);
5174
- toolCount += 5;
5175
- }
5176
- }
5177
- return toolCount;
5178
- }
5179
- async function countSkills(projectPath) {
5180
- const dirs = [join20(homedir12(), ".claude", "skills")];
5181
- if (projectPath) dirs.push(join20(projectPath, ".claude", "skills"));
5182
- let count = 0;
5183
- for (const dir of dirs) {
5184
- if (!existsSync6(dir)) continue;
5185
- try {
5186
- const entries = await readdir10(dir);
5187
- for (const entry of entries) {
5188
- const skillFile = join20(dir, entry, "SKILL.md");
5189
- if (existsSync6(skillFile)) count++;
5190
- }
5191
- } catch {
5192
- continue;
5193
- }
5194
- }
5195
- return count;
5196
- }
5197
- async function scanMemoryFiles(projectPath) {
5198
- const home = homedir12();
5199
- const files = [];
5200
- const paths = [
5201
- { path: join20(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
5202
- ];
5203
- if (projectPath) {
5204
- paths.push({ path: join20(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
5205
- paths.push({ path: join20(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
5206
- paths.push({ path: join20(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
5207
- }
5208
- for (const { path, name } of paths) {
5209
- if (!existsSync6(path)) continue;
5210
- const content = await readSessionFile(path);
5211
- if (content === null) continue;
5212
- files.push({ name, tokens: estimateTokens2(content) });
5213
- }
5214
- return files;
5215
- }
5216
- async function estimateContextBudget(projectPath, modelContext = 1e6) {
5217
- const mcpToolCount = await countMcpTools(projectPath);
5218
- const skillCount = await countSkills(projectPath);
5219
- const memoryFiles = await scanMemoryFiles(projectPath);
5220
- const mcpTokens = mcpToolCount * TOOL_TOKENS_OVERHEAD;
5221
- const skillTokens = skillCount * SKILL_FRONTMATTER_TOKENS;
5222
- const memoryTokens = memoryFiles.reduce((s, f) => s + f.tokens, 0);
5223
- const total = SYSTEM_BASE_TOKENS + mcpTokens + skillTokens + memoryTokens;
5224
- return {
5225
- systemBase: SYSTEM_BASE_TOKENS,
5226
- mcpTools: { count: mcpToolCount, tokens: mcpTokens },
5227
- skills: { count: skillCount, tokens: skillTokens },
5228
- memory: { count: memoryFiles.length, tokens: memoryTokens, files: memoryFiles },
5229
- total,
5230
- modelContext
5231
- };
5232
- }
5233
- async function discoverProjectCwd(sessionDir) {
5234
- let files;
5235
- try {
5236
- files = (await readdir10(sessionDir)).filter((f) => f.endsWith(".jsonl"));
5237
- } catch {
5238
- return null;
5239
- }
5240
- if (files.length === 0) return null;
5241
- const firstFile = files[0];
5242
- if (!firstFile) return null;
5243
- const content = await readSessionFile(join20(sessionDir, firstFile));
5244
- if (content === null) return null;
5245
- for (const line of content.split("\n")) {
5246
- if (!line.trim()) continue;
5247
- try {
5248
- const entry = JSON.parse(line);
5249
- if (entry.cwd && typeof entry.cwd === "string") return entry.cwd;
5250
- } catch {
5251
- continue;
5252
- }
5253
- }
5254
- return null;
5255
- }
5256
-
5257
- // src/compare.tsx
5258
- import React, { useState, useEffect, useRef } from "react";
5259
- import { render, Box, Text, useInput, useApp, useStdout } from "ink";
5260
-
5261
- // src/compare-stats.ts
5262
- import { readdir as readdir11, readFile as readFile9 } from "fs/promises";
5263
- import { join as join21 } from "path";
5264
- var PLANNING_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "EnterPlanMode", "ExitPlanMode"]);
5265
- function aggregateModelStats(projects) {
5266
- const byModel = /* @__PURE__ */ new Map();
5267
- const ensure = (model) => {
5268
- let s = byModel.get(model);
5269
- if (!s) {
5270
- 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: "" };
5271
- byModel.set(model, s);
5272
- }
5273
- return s;
5274
- };
5275
- for (const project of projects) {
5276
- for (const session of project.sessions) {
5277
- for (const turn of session.turns) {
5278
- if (turn.assistantCalls.length === 0) continue;
5279
- const primaryModel = turn.assistantCalls[0].model;
5280
- if (primaryModel === "<synthetic>") continue;
5281
- const ms = ensure(primaryModel);
5282
- ms.totalTurns++;
5283
- if (turn.hasEdits) {
5284
- ms.editTurns++;
5285
- if (turn.retries === 0) ms.oneShotTurns++;
5286
- for (const c of turn.assistantCalls) {
5287
- if (c.model !== "<synthetic>") ms.editCost += c.costUSD;
5288
- }
5289
- }
5290
- ms.retries += turn.retries;
5291
- for (const call of turn.assistantCalls) {
5292
- if (call.model === "<synthetic>") continue;
5293
- const cs = call.model === primaryModel ? ms : ensure(call.model);
5294
- cs.calls++;
5295
- cs.cost += call.costUSD;
5296
- cs.outputTokens += call.usage.outputTokens;
5297
- cs.inputTokens += call.usage.inputTokens;
5298
- cs.cacheReadTokens += call.usage.cacheReadInputTokens;
5299
- cs.cacheWriteTokens += call.usage.cacheCreationInputTokens;
5300
- if (!cs.firstSeen || call.timestamp < cs.firstSeen) cs.firstSeen = call.timestamp;
5301
- if (!cs.lastSeen || call.timestamp > cs.lastSeen) cs.lastSeen = call.timestamp;
5302
- }
5303
- }
5304
- }
5305
- }
5306
- return [...byModel.values()].sort((a, b) => b.cost - a.cost);
5307
- }
5308
- var METRICS = [
5309
- {
5310
- section: "Performance",
5311
- label: "One-shot rate",
5312
- formatFn: "percent",
5313
- higherIsBetter: true,
5314
- compute: (s) => s.editTurns > 0 ? s.oneShotTurns / s.editTurns * 100 : null
5315
- },
5316
- {
5317
- section: "Performance",
5318
- label: "Retry rate",
5319
- formatFn: "decimal",
5320
- higherIsBetter: false,
5321
- compute: (s) => s.editTurns > 0 ? s.retries / s.editTurns : null
5322
- },
5323
- {
5324
- section: "Performance",
5325
- label: "Self-correction",
5326
- formatFn: "percent",
5327
- higherIsBetter: false,
5328
- compute: (s) => s.totalTurns > 0 ? s.selfCorrections / s.totalTurns * 100 : null
5329
- },
5330
- {
5331
- section: "Efficiency",
5332
- label: "Cost / call",
5333
- formatFn: "cost",
5334
- higherIsBetter: false,
5335
- compute: (s) => s.calls > 0 ? s.cost / s.calls : null
5336
- },
5337
- {
5338
- section: "Efficiency",
5339
- label: "Cost / edit",
5340
- formatFn: "cost",
5341
- higherIsBetter: false,
5342
- compute: (s) => s.editTurns > 0 ? s.editCost / s.editTurns : null
5343
- },
5344
- {
5345
- section: "Efficiency",
5346
- label: "Output tok / call",
5347
- formatFn: "number",
5348
- higherIsBetter: false,
5349
- compute: (s) => s.calls > 0 ? Math.round(s.outputTokens / s.calls) : null
5350
- },
5351
- {
5352
- section: "Efficiency",
5353
- label: "Cache hit rate",
5354
- formatFn: "percent",
5355
- higherIsBetter: true,
5356
- compute: (s) => {
5357
- const total = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens;
5358
- return total > 0 ? s.cacheReadTokens / total * 100 : null;
5359
- }
5360
- }
5361
- ];
5362
- function pickWinner(valueA, valueB, higherIsBetter) {
5363
- if (valueA === null || valueB === null) return "none";
5364
- if (valueA === valueB) return "tie";
5365
- if (higherIsBetter) return valueA > valueB ? "a" : "b";
5366
- return valueA < valueB ? "a" : "b";
5367
- }
5368
- function computeComparison(a, b) {
5369
- return METRICS.map((m) => {
5370
- const valueA = m.compute(a);
5371
- const valueB = m.compute(b);
5372
- return {
5373
- section: m.section,
5374
- label: m.label,
5375
- valueA,
5376
- valueB,
5377
- formatFn: m.formatFn,
5378
- winner: pickWinner(valueA, valueB, m.higherIsBetter)
5379
- };
5380
- });
5381
- }
5382
- function computeCategoryComparison(projects, modelA, modelB) {
5383
- const mapA = /* @__PURE__ */ new Map();
5384
- const mapB = /* @__PURE__ */ new Map();
5385
- const ensure = (map, cat) => {
5386
- let a = map.get(cat);
5387
- if (!a) {
5388
- a = { turns: 0, editTurns: 0, oneShotTurns: 0 };
5389
- map.set(cat, a);
5390
- }
5391
- return a;
5392
- };
5393
- for (const project of projects) {
5394
- for (const session of project.sessions) {
5395
- for (const turn of session.turns) {
5396
- if (turn.assistantCalls.length === 0) continue;
5397
- const primary = turn.assistantCalls[0].model;
5398
- if (primary !== modelA && primary !== modelB) continue;
5399
- const acc = ensure(primary === modelA ? mapA : mapB, turn.category);
5400
- acc.turns++;
5401
- if (turn.hasEdits) {
5402
- acc.editTurns++;
5403
- if (turn.retries === 0) acc.oneShotTurns++;
5404
- }
5405
- }
5406
- }
5407
- }
5408
- const allCats = /* @__PURE__ */ new Set([...mapA.keys(), ...mapB.keys()]);
5409
- const result = [];
5410
- for (const category of allCats) {
5411
- const a = mapA.get(category);
5412
- const b = mapB.get(category);
5413
- if ((!a || a.editTurns === 0) && (!b || b.editTurns === 0)) continue;
5414
- const rateA = a && a.editTurns > 0 ? a.oneShotTurns / a.editTurns * 100 : null;
5415
- const rateB = b && b.editTurns > 0 ? b.oneShotTurns / b.editTurns * 100 : null;
5416
- result.push({
5417
- category,
5418
- turnsA: a?.turns ?? 0,
5419
- editTurnsA: a?.editTurns ?? 0,
5420
- oneShotRateA: rateA,
5421
- turnsB: b?.turns ?? 0,
5422
- editTurnsB: b?.editTurns ?? 0,
5423
- oneShotRateB: rateB,
5424
- winner: pickWinner(rateA, rateB, true)
5425
- });
6078
+ }
6079
+ async function countMcpTools(projectPath) {
6080
+ const home = homedir12();
6081
+ const configPaths = [
6082
+ join23(home, ".claude", "settings.json"),
6083
+ join23(home, ".claude", "settings.local.json")
6084
+ ];
6085
+ if (projectPath) {
6086
+ configPaths.push(join23(projectPath, ".mcp.json"));
6087
+ configPaths.push(join23(projectPath, ".claude", "settings.json"));
6088
+ configPaths.push(join23(projectPath, ".claude", "settings.local.json"));
5426
6089
  }
5427
- return result.sort((a, b) => b.turnsA + b.turnsB - (a.turnsA + a.turnsB));
6090
+ const servers = /* @__PURE__ */ new Set();
6091
+ let toolCount = 0;
6092
+ for (const p of configPaths) {
6093
+ const config = await readConfigFile(p);
6094
+ if (!config) continue;
6095
+ const mcpServers = config.mcpServers ?? {};
6096
+ for (const name of Object.keys(mcpServers)) {
6097
+ if (servers.has(name)) continue;
6098
+ servers.add(name);
6099
+ toolCount += 5;
6100
+ }
6101
+ }
6102
+ return toolCount;
5428
6103
  }
5429
- function computeWorkingStyle(projects, modelA, modelB) {
5430
- const sA = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
5431
- const sB = { totalTurns: 0, agentSpawns: 0, planModeUses: 0, totalToolCalls: 0, fastModeCalls: 0 };
5432
- for (const project of projects) {
5433
- for (const session of project.sessions) {
5434
- for (const turn of session.turns) {
5435
- if (turn.assistantCalls.length === 0) continue;
5436
- const primary = turn.assistantCalls[0].model;
5437
- if (primary !== modelA && primary !== modelB) continue;
5438
- const s = primary === modelA ? sA : sB;
5439
- s.totalTurns++;
5440
- const turnTools = turn.assistantCalls.flatMap((c) => c.tools);
5441
- if (turnTools.some((t) => PLANNING_TOOLS.has(t)) || turn.assistantCalls.some((c) => c.hasPlanMode)) {
5442
- s.planModeUses++;
5443
- }
5444
- for (const call of turn.assistantCalls) {
5445
- s.totalToolCalls += call.tools.length;
5446
- if (call.hasAgentSpawn) s.agentSpawns++;
5447
- if (call.speed === "fast") s.fastModeCalls++;
5448
- }
6104
+ async function countSkills(projectPath) {
6105
+ const dirs = [join23(homedir12(), ".claude", "skills")];
6106
+ if (projectPath) dirs.push(join23(projectPath, ".claude", "skills"));
6107
+ let count = 0;
6108
+ for (const dir of dirs) {
6109
+ if (!existsSync6(dir)) continue;
6110
+ try {
6111
+ const entries = await readdir12(dir);
6112
+ for (const entry of entries) {
6113
+ const skillFile = join23(dir, entry, "SKILL.md");
6114
+ if (existsSync6(skillFile)) count++;
5449
6115
  }
6116
+ } catch {
6117
+ continue;
5450
6118
  }
5451
6119
  }
5452
- const pct2 = (num, den) => den > 0 ? num / den * 100 : null;
5453
- const avg = (num, den) => den > 0 ? num / den : null;
5454
- return [
5455
- { label: "Delegation rate", valueA: pct2(sA.agentSpawns, sA.totalTurns), valueB: pct2(sB.agentSpawns, sB.totalTurns), formatFn: "percent" },
5456
- { label: "Planning rate", valueA: pct2(sA.planModeUses, sA.totalTurns), valueB: pct2(sB.planModeUses, sB.totalTurns), formatFn: "percent" },
5457
- { label: "Avg tools / turn", valueA: avg(sA.totalToolCalls, sA.totalTurns), valueB: avg(sB.totalToolCalls, sB.totalTurns), formatFn: "decimal" },
5458
- { label: "Fast mode usage", valueA: pct2(sA.fastModeCalls, sA.totalTurns), valueB: pct2(sB.fastModeCalls, sB.totalTurns), formatFn: "percent" }
5459
- ];
5460
- }
5461
- var SELF_CORRECTION_PATTERNS = [
5462
- /\bmy mistake\b/i,
5463
- /\bmy bad\b/i,
5464
- /\bmy apolog/i,
5465
- /\bI apologize\b/i,
5466
- /\bI was wrong\b/i,
5467
- /\bI was incorrect\b/i,
5468
- /\bI made (a |an )?(error|mistake)\b/i,
5469
- /\bI incorrectly\b/i,
5470
- /\bI mistakenly\b/i,
5471
- /\bthat was (incorrect|wrong|an error)\b/i,
5472
- /\blet me correct that\b/i,
5473
- /\bI need to correct\b/i,
5474
- /\byou're right[.,]? I/i,
5475
- /\bsorry about that\b/i
5476
- ];
5477
- function extractText(content) {
5478
- if (typeof content === "string") return content;
5479
- if (!Array.isArray(content)) return "";
5480
- return content.filter((b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ");
5481
- }
5482
- function isCompactFile(name) {
5483
- return name.includes("compact");
6120
+ return count;
5484
6121
  }
5485
- async function collectJsonlFiles3(sessionDir) {
5486
- const entries = await readdir11(sessionDir, { withFileTypes: true });
6122
+ async function scanMemoryFiles(projectPath) {
6123
+ const home = homedir12();
5487
6124
  const files = [];
5488
- for (const entry of entries) {
5489
- if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
5490
- files.push(join21(sessionDir, entry.name));
5491
- } else if (entry.isDirectory() && entry.name === "subagents") {
5492
- const subEntries = await readdir11(join21(sessionDir, entry.name), { withFileTypes: true });
5493
- for (const sub of subEntries) {
5494
- if (sub.isFile() && sub.name.endsWith(".jsonl") && !isCompactFile(sub.name)) {
5495
- files.push(join21(sessionDir, entry.name, sub.name));
5496
- }
5497
- }
5498
- }
6125
+ const paths = [
6126
+ { path: join23(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
6127
+ ];
6128
+ if (projectPath) {
6129
+ paths.push({ path: join23(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
6130
+ paths.push({ path: join23(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
6131
+ paths.push({ path: join23(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
6132
+ }
6133
+ for (const { path, name } of paths) {
6134
+ if (!existsSync6(path)) continue;
6135
+ const content = await readSessionFile(path);
6136
+ if (content === null) continue;
6137
+ files.push({ name, tokens: estimateTokens2(content) });
5499
6138
  }
5500
6139
  return files;
5501
6140
  }
5502
- async function scanSelfCorrections(projectDirs) {
5503
- const counts = /* @__PURE__ */ new Map();
5504
- const seen = /* @__PURE__ */ new Set();
5505
- for (const dir of projectDirs) {
5506
- let entries;
6141
+ async function estimateContextBudget(projectPath, modelContext = 1e6) {
6142
+ const mcpToolCount = await countMcpTools(projectPath);
6143
+ const skillCount = await countSkills(projectPath);
6144
+ const memoryFiles = await scanMemoryFiles(projectPath);
6145
+ const mcpTokens = mcpToolCount * TOOL_TOKENS_OVERHEAD;
6146
+ const skillTokens = skillCount * SKILL_FRONTMATTER_TOKENS;
6147
+ const memoryTokens = memoryFiles.reduce((s, f) => s + f.tokens, 0);
6148
+ const total = SYSTEM_BASE_TOKENS + mcpTokens + skillTokens + memoryTokens;
6149
+ return {
6150
+ systemBase: SYSTEM_BASE_TOKENS,
6151
+ mcpTools: { count: mcpToolCount, tokens: mcpTokens },
6152
+ skills: { count: skillCount, tokens: skillTokens },
6153
+ memory: { count: memoryFiles.length, tokens: memoryTokens, files: memoryFiles },
6154
+ total,
6155
+ modelContext
6156
+ };
6157
+ }
6158
+ async function discoverProjectCwd(sessionDir) {
6159
+ let files;
6160
+ try {
6161
+ files = (await readdir12(sessionDir)).filter((f) => f.endsWith(".jsonl"));
6162
+ } catch {
6163
+ return null;
6164
+ }
6165
+ if (files.length === 0) return null;
6166
+ const firstFile = files[0];
6167
+ if (!firstFile) return null;
6168
+ const content = await readSessionFile(join23(sessionDir, firstFile));
6169
+ if (content === null) return null;
6170
+ for (const line of content.split("\n")) {
6171
+ if (!line.trim()) continue;
5507
6172
  try {
5508
- entries = await readdir11(dir, { withFileTypes: true });
6173
+ const entry = JSON.parse(line);
6174
+ if (entry.cwd && typeof entry.cwd === "string") return entry.cwd;
5509
6175
  } catch {
5510
6176
  continue;
5511
6177
  }
5512
- const allFiles = [];
5513
- for (const entry of entries) {
5514
- if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
5515
- allFiles.push(join21(dir, entry.name));
5516
- } else if (entry.isDirectory()) {
5517
- try {
5518
- const sessionFiles = await collectJsonlFiles3(join21(dir, entry.name));
5519
- allFiles.push(...sessionFiles);
5520
- } catch {
5521
- continue;
5522
- }
5523
- }
5524
- }
5525
- for (const file of allFiles) {
5526
- let raw;
5527
- try {
5528
- raw = await readFile9(file, "utf8");
5529
- } catch {
5530
- continue;
5531
- }
5532
- for (const line of raw.split("\n")) {
5533
- const trimmed = line.trim();
5534
- if (!trimmed) continue;
5535
- let parsed;
5536
- try {
5537
- parsed = JSON.parse(trimmed);
5538
- } catch {
5539
- continue;
5540
- }
5541
- const rec = parsed;
5542
- if (!rec || typeof rec !== "object" || rec["type"] !== "assistant") continue;
5543
- const ts = rec["timestamp"];
5544
- const msg = rec["message"];
5545
- if (msg === null || typeof msg !== "object") continue;
5546
- const msgRec = msg;
5547
- const model = msgRec["model"];
5548
- if (typeof model !== "string" || model === "<synthetic>") continue;
5549
- const dedupeKey = `${model}:${ts}`;
5550
- if (seen.has(dedupeKey)) continue;
5551
- seen.add(dedupeKey);
5552
- const text = extractText(msgRec["content"]);
5553
- if (SELF_CORRECTION_PATTERNS.some((p) => p.test(text))) {
5554
- counts.set(model, (counts.get(model) ?? 0) + 1);
5555
- }
5556
- }
5557
- }
5558
6178
  }
5559
- return counts;
6179
+ return null;
5560
6180
  }
5561
6181
 
5562
6182
  // src/compare.tsx
6183
+ import React, { useState, useEffect, useRef } from "react";
6184
+ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
5563
6185
  import { jsx, jsxs } from "react/jsx-runtime";
5564
6186
  var ORANGE2 = "#F59E0B";
5565
6187
  var GREEN2 = "#16A34A";
@@ -6140,7 +6762,7 @@ function planDisplayName(id) {
6140
6762
  }
6141
6763
 
6142
6764
  // src/dashboard.tsx
6143
- import { join as join22 } from "path";
6765
+ import { join as join24 } from "path";
6144
6766
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
6145
6767
  var PERIODS = ["today", "week", "30days", "month", "all"];
6146
6768
  var PERIOD_LABELS = {
@@ -6389,6 +7011,13 @@ function DailyActivity({ projects, days = 14, pw, bw }) {
6389
7011
  const dailyCalls = {};
6390
7012
  for (const project of projects) {
6391
7013
  for (const session of project.sessions) {
7014
+ if (session.reportSummary) {
7015
+ for (const [day, row] of Object.entries(session.reportSummary.daily)) {
7016
+ dailyCosts[day] = (dailyCosts[day] ?? 0) + row.cost;
7017
+ dailyCalls[day] = (dailyCalls[day] ?? 0) + row.calls;
7018
+ }
7019
+ continue;
7020
+ }
6392
7021
  for (const turn of session.turns) {
6393
7022
  if (!turn.timestamp) continue;
6394
7023
  const day = dateKey(turn.timestamp);
@@ -6790,15 +7419,15 @@ function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable,
6790
7419
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " month " }),
6791
7420
  /* @__PURE__ */ jsx2(Text2, { color: ORANGE3, bold: true, children: "5" }),
6792
7421
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " all time" }),
6793
- !isOptimize && optimizeAvailable && findingCount != null && findingCount > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
7422
+ !isOptimize && optimizeAvailable && /* @__PURE__ */ jsxs2(Fragment, { children: [
6794
7423
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
6795
7424
  /* @__PURE__ */ jsx2(Text2, { color: ORANGE3, bold: true, children: "o" }),
6796
7425
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " optimize" }),
6797
- /* @__PURE__ */ jsxs2(Text2, { color: "#DC2626", children: [
7426
+ findingCount ? /* @__PURE__ */ jsxs2(Text2, { color: "#DC2626", children: [
6798
7427
  " (",
6799
7428
  findingCount,
6800
7429
  ")"
6801
- ] })
7430
+ ] }) : null
6802
7431
  ] }),
6803
7432
  !isOptimize && compareAvailable && /* @__PURE__ */ jsxs2(Fragment, { children: [
6804
7433
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
@@ -6852,6 +7481,10 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6852
7481
  const [projects, setProjects] = useState2([]);
6853
7482
  const [initialLoading, setInitialLoading] = useState2(true);
6854
7483
  const [loading, setLoading] = useState2(false);
7484
+ const [loadError2, setLoadError] = useState2(null);
7485
+ const loader = useRef2(new LatestLoad());
7486
+ const retryAt = useRef2(0);
7487
+ const failures = useRef2(0);
6855
7488
  const [activeProvider, setActiveProvider] = useState2(initialProvider);
6856
7489
  const [detectedProviders, setDetectedProviders] = useState2([]);
6857
7490
  const [view, setView] = useState2("dashboard");
@@ -6876,103 +7509,88 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6876
7509
  return () => clearInterval(id);
6877
7510
  }, [initialLoading]);
6878
7511
  useEffect2(() => {
6879
- let cancelled = false;
6880
- async function boot() {
6881
- await loadPricing();
6882
- if (cancelled) return;
6883
- const range = getDateRange(initialPeriod);
6884
- const data = await parseAllSessions(range, initialProvider);
6885
- if (cancelled) return;
6886
- const filtered = filterProjectsByName(data, projectFilter, excludeFilter);
6887
- setProjects(filtered);
6888
- const usage = await getPlanUsageOrNull();
6889
- if (cancelled) return;
6890
- setPlanUsage(usage ?? void 0);
6891
- setInitialLoading(false);
6892
- }
6893
- boot();
6894
- return () => {
6895
- cancelled = true;
6896
- };
6897
- }, []);
6898
- useEffect2(() => {
6899
- let cancelled = false;
6900
- async function detect() {
6901
- const found = [];
6902
- for (const p of await getAllProviders()) {
6903
- const s = await p.discoverSessions();
6904
- if (s.length > 0) found.push(p.name);
6905
- }
6906
- if (!cancelled) setDetectedProviders(found);
6907
- }
6908
- detect();
7512
+ void reloadData(initialPeriod, initialProvider);
6909
7513
  return () => {
6910
- cancelled = true;
7514
+ loader.current.cancel();
7515
+ if (debounceRef.current) clearTimeout(debounceRef.current);
6911
7516
  };
6912
7517
  }, []);
6913
7518
  useEffect2(() => {
6914
7519
  let cancelled = false;
6915
7520
  async function loadBudgets() {
6916
- const claudeDir = join22(homedir13(), ".claude", "projects");
7521
+ const claudeDir = join24(homedir13(), ".claude", "projects");
6917
7522
  const budgets = /* @__PURE__ */ new Map();
6918
7523
  for (const project of projects.slice(0, 8)) {
6919
7524
  if (cancelled) return;
6920
- const cwd = await discoverProjectCwd(join22(claudeDir, project.project));
7525
+ const cwd = await discoverProjectCwd(join24(claudeDir, project.project));
6921
7526
  if (!cwd) continue;
6922
7527
  budgets.set(project.project, await estimateContextBudget(cwd));
6923
7528
  }
6924
7529
  if (!cancelled) setProjectBudgets(budgets);
6925
7530
  }
6926
- loadBudgets();
7531
+ void loadBudgets().catch(() => {
7532
+ });
6927
7533
  return () => {
6928
7534
  cancelled = true;
6929
7535
  };
6930
7536
  }, [projects]);
6931
7537
  useEffect2(() => {
6932
- if (!optimizeAvailable) {
7538
+ if (!optimizeAvailable || view !== "optimize" || loading || initialLoading || loadError2) {
6933
7539
  setOptimizeResult(null);
6934
7540
  return;
6935
7541
  }
6936
7542
  let cancelled = false;
7543
+ const controller = new AbortController();
6937
7544
  async function scan() {
6938
7545
  if (projects.length === 0) {
6939
7546
  setOptimizeResult(null);
6940
7547
  return;
6941
7548
  }
6942
- const result = await scanAndDetect(projects, getDateRange(period));
6943
- if (!cancelled) setOptimizeResult(result);
7549
+ const result = await loader.current.run((signal) => withScanBudget(() => scanAndDetect(projects, getDateRange(period)), 2e5, AbortSignal.any([signal, controller.signal])));
7550
+ if (!cancelled && result) setOptimizeResult(result);
6944
7551
  }
6945
- scan();
7552
+ void scan().catch((error) => {
7553
+ if (!cancelled) setLoadError(error instanceof Error ? error.message : String(error));
7554
+ });
6946
7555
  return () => {
6947
7556
  cancelled = true;
7557
+ controller.abort();
6948
7558
  };
6949
- }, [projects, period, optimizeAvailable]);
7559
+ }, [projects, period, optimizeAvailable, view, loading, initialLoading, loadError2]);
6950
7560
  const reloadData = useCallback(async (p, prov) => {
6951
7561
  const generation = ++reloadGenerationRef.current;
6952
7562
  setLoading(true);
6953
7563
  setOptimizeResult(null);
6954
7564
  try {
6955
- const range = getDateRange(p);
6956
- const data = await parseAllSessions(range, prov);
6957
- if (reloadGenerationRef.current !== generation) return;
6958
- const filteredProjects = filterProjectsByName(data, projectFilter, excludeFilter);
6959
- if (reloadGenerationRef.current !== generation) return;
6960
- setProjects(filteredProjects);
6961
- const usage = await getPlanUsageOrNull();
6962
- if (reloadGenerationRef.current !== generation) return;
6963
- setPlanUsage(usage ?? void 0);
7565
+ setLoadError(null);
7566
+ const result = await loader.current.run(async (signal) => {
7567
+ await loadPricing();
7568
+ const data = await parseAllSessions(getDateRange(p), prov, true, signal);
7569
+ if (signal.aborted) return void 0;
7570
+ return { projects: filterProjectsByName(data, projectFilter, excludeFilter), usage: await getPlanUsageOrNull(), providers: getDiscoveredProviderNames() };
7571
+ });
7572
+ if (!result || reloadGenerationRef.current !== generation) return;
7573
+ setProjects(result.projects);
7574
+ setDetectedProviders(result.providers);
7575
+ setPlanUsage(result.usage ?? void 0);
7576
+ failures.current = 0;
7577
+ retryAt.current = 0;
6964
7578
  } catch (error) {
6965
- console.error(error);
7579
+ if (reloadGenerationRef.current === generation) {
7580
+ setLoadError(error instanceof Error ? error.message : String(error));
7581
+ retryAt.current = Date.now() + Math.min(3e5, 3e4 * 2 ** Math.min(failures.current++, 4));
7582
+ }
6966
7583
  } finally {
6967
7584
  if (reloadGenerationRef.current === generation) {
6968
7585
  setLoading(false);
7586
+ setInitialLoading(false);
6969
7587
  }
6970
7588
  }
6971
7589
  }, [projectFilter, excludeFilter]);
6972
7590
  useEffect2(() => {
6973
7591
  if (!refreshSeconds || refreshSeconds <= 0) return;
6974
7592
  const id = setInterval(() => {
6975
- reloadData(period, activeProvider);
7593
+ if (!loader.current.busy && Date.now() >= retryAt.current) void reloadData(period, activeProvider);
6976
7594
  }, refreshSeconds * 1e3);
6977
7595
  return () => clearInterval(id);
6978
7596
  }, [refreshSeconds, period, activeProvider, reloadData]);
@@ -6992,14 +7610,20 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6992
7610
  }, [period, activeProvider, reloadData]);
6993
7611
  useInput2((input, key) => {
6994
7612
  if (input === "q") {
7613
+ loader.current.cancel();
6995
7614
  exit();
6996
7615
  return;
6997
7616
  }
6998
- if (input === "o" && findingCount > 0 && view === "dashboard" && optimizeAvailable) {
7617
+ if (input === "r") {
7618
+ void reloadData(period, activeProvider);
7619
+ return;
7620
+ }
7621
+ if (input === "o" && view === "dashboard" && optimizeAvailable) {
6999
7622
  setView("optimize");
7000
7623
  return;
7001
7624
  }
7002
7625
  if ((input === "b" || key.escape) && view === "optimize") {
7626
+ setLoadError(null);
7003
7627
  setView("dashboard");
7004
7628
  return;
7005
7629
  }
@@ -7028,6 +7652,15 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
7028
7652
  if (initialLoading) {
7029
7653
  return /* @__PURE__ */ jsx2(LoadingScreen, { width: dashWidth, dots: loadingDots });
7030
7654
  }
7655
+ if (loadError2 && !loading) {
7656
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
7657
+ /* @__PURE__ */ jsx2(PeriodTabs, { active: period, providerName: activeProvider }),
7658
+ /* @__PURE__ */ jsxs2(Panel, { title: "Report could not refresh", color: ORANGE3, width: dashWidth, children: [
7659
+ /* @__PURE__ */ jsx2(Text2, { children: loadError2 }),
7660
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Press r to retry, 1\u20135 to change period, or q to quit." })
7661
+ ] })
7662
+ ] });
7663
+ }
7031
7664
  if (loading) {
7032
7665
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
7033
7666
  /* @__PURE__ */ jsx2(PeriodTabs, { active: period, providerName: activeProvider, showProvider: view !== "compare" && multipleProviders }),
@@ -7049,7 +7682,7 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
7049
7682
  }
7050
7683
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
7051
7684
  /* @__PURE__ */ jsx2(PeriodTabs, { active: period, providerName: activeProvider, showProvider: multipleProviders && view !== "compare" }),
7052
- 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 }),
7685
+ 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 }),
7053
7686
  view !== "compare" && /* @__PURE__ */ jsx2(StatusBar, { width: dashWidth, showProvider: multipleProviders, view, findingCount, optimizeAvailable, compareAvailable })
7054
7687
  ] });
7055
7688
  }
@@ -7071,7 +7704,7 @@ async function renderDashboard(period = "week", provider = "all", refreshSeconds
7071
7704
  } else {
7072
7705
  await loadPricing();
7073
7706
  const range = customRange ?? getDateRange(period);
7074
- const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter);
7707
+ const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider, true), projectFilter, excludeFilter);
7075
7708
  const planUsage = await getPlanUsageOrNull();
7076
7709
  const { unmount } = render2(/* @__PURE__ */ jsx2(StaticDashboard, { projects: filteredProjects, period, activeProvider: provider, planUsage: planUsage ?? void 0 }), { patchConsole: false });
7077
7710
  unmount();
@@ -7167,9 +7800,32 @@ function computeProgressiveBackfillStart({
7167
7800
  }
7168
7801
  return gapStart;
7169
7802
  }
7803
+ var DEFAULT_BACKFILL_CHUNK_DAYS = 1;
7804
+ var MAX_BLOCKED_CHUNK_ATTEMPTS = 2;
7805
+ function planBackfillChunks(start, end, chunkDays = DEFAULT_BACKFILL_CHUNK_DAYS) {
7806
+ const size = Math.max(1, Math.floor(chunkDays));
7807
+ const chunks = [];
7808
+ if (start.getTime() > end.getTime()) return chunks;
7809
+ let cursor2 = new Date(start.getFullYear(), start.getMonth(), start.getDate());
7810
+ if (cursor2.getTime() < start.getTime()) cursor2 = new Date(start.getTime());
7811
+ while (cursor2.getTime() <= end.getTime()) {
7812
+ const dayStart = new Date(cursor2.getFullYear(), cursor2.getMonth(), cursor2.getDate());
7813
+ const lastDay = new Date(dayStart.getFullYear(), dayStart.getMonth(), dayStart.getDate() + size - 1);
7814
+ const chunkEnd = new Date(lastDay.getFullYear(), lastDay.getMonth(), lastDay.getDate() + 1).getTime() - 1;
7815
+ chunks.push({ start: new Date(cursor2.getTime()), end: new Date(Math.min(chunkEnd, end.getTime())) });
7816
+ cursor2 = new Date(dayStart.getFullYear(), dayStart.getMonth(), dayStart.getDate() + size);
7817
+ }
7818
+ return chunks;
7819
+ }
7820
+ function nextBackfillCursor(prev, date) {
7821
+ return prev && prev.blockedDate === date ? { blockedDate: date, attempts: prev.attempts + 1 } : { blockedDate: date, attempts: 1 };
7822
+ }
7823
+ function shouldSkipBlockedChunk(cursor2, date, maxAttempts = MAX_BLOCKED_CHUNK_ATTEMPTS) {
7824
+ return !!cursor2 && cursor2.blockedDate === date && cursor2.attempts >= maxAttempts;
7825
+ }
7170
7826
 
7171
7827
  // src/cli.ts
7172
- import { createInterface as createInterface2 } from "readline";
7828
+ import { createInterface } from "readline";
7173
7829
  import { createRequire as createRequire2 } from "module";
7174
7830
  var require2 = createRequire2(import.meta.url);
7175
7831
  var { version } = require2("../package.json");
@@ -7460,10 +8116,53 @@ program.command("status").description("Compact status output (today + week + mon
7460
8116
  const yesterdayStr = toDateString(new Date(todayStart.getTime() - MS_PER_DAY4));
7461
8117
  const isAllProviders = pf === "all";
7462
8118
  const dailyCacheScope = buildDailyCacheScopeKey(opts.project, opts.exclude);
8119
+ const backfillWarnings = [];
8120
+ const runChunkedBackfill = async (cache2, start, end, order) => {
8121
+ let c = cache2;
8122
+ const chunks = planBackfillChunks(start, end);
8123
+ if (order === "newest-first") chunks.reverse();
8124
+ for (const chunk of chunks) {
8125
+ const chunkDate = toDateString(chunk.start);
8126
+ const coveredThrough = toDateString(chunk.end);
8127
+ if (shouldSkipBlockedChunk(c.backfill, chunkDate)) {
8128
+ const blanks = fillMissingDays(chunk.start, chunk.end, []);
8129
+ c = addNewDays(c, blanks, yesterdayStr, { coveredThrough });
8130
+ c.partialDates = [.../* @__PURE__ */ new Set([...c.partialDates ?? [], ...blanks.map((d) => d.date)])];
8131
+ c.backfill = null;
8132
+ c.revalidatedAt = Date.now();
8133
+ await saveDailyCache(c);
8134
+ const warn2 = `history for ${chunkDate} exceeded the scan budget ${MAX_BLOCKED_CHUNK_ATTEMPTS} times; recorded as incomplete so newer days can load`;
8135
+ backfillWarnings.push(warn2);
8136
+ process.stderr.write(`[exe-watcher] WARNING: ${warn2}
8137
+ `);
8138
+ continue;
8139
+ }
8140
+ try {
8141
+ const chunkRange = { start: chunk.start, end: chunk.end };
8142
+ const chunkProjects = filterProjectsByName(await parseAllSessions(chunkRange, "all"), opts.project, opts.exclude);
8143
+ const chunkDays = fillMissingDays(chunk.start, chunk.end, aggregateProjectsIntoDays(chunkProjects));
8144
+ c = addNewDays(c, chunkDays, yesterdayStr, { coveredThrough });
8145
+ c.backfill = null;
8146
+ c.revalidatedAt = Date.now();
8147
+ await saveDailyCache(c);
8148
+ } catch (err) {
8149
+ if (!(err instanceof ResourceBudgetError)) throw err;
8150
+ c.backfill = nextBackfillCursor(c.backfill, chunkDate);
8151
+ c.revalidatedAt = Date.now();
8152
+ await saveDailyCache(c);
8153
+ const warn2 = `history backfill paused at ${chunkDate} (attempt ${c.backfill.attempts}): ${err.message}`;
8154
+ backfillWarnings.push(warn2);
8155
+ process.stderr.write(`[exe-watcher] WARNING: ${warn2}
8156
+ `);
8157
+ break;
8158
+ }
8159
+ }
8160
+ return c;
8161
+ };
7463
8162
  const cache = await withDailyCacheLock(async () => {
7464
8163
  let c = await loadDailyCache(dailyCacheScope);
7465
8164
  const hadYesterday = c.days.some((d) => d.date >= yesterdayStr);
7466
- if (hadYesterday) {
8165
+ if (hadYesterday && (!c.revalidatedAt || Date.now() - c.revalidatedAt >= 5 * 6e4)) {
7467
8166
  const freshDays = c.days.filter((d) => d.date < yesterdayStr);
7468
8167
  const latestFresh = freshDays.length > 0 ? freshDays[freshDays.length - 1]?.date ?? null : null;
7469
8168
  c = { ...c, days: freshDays, lastComputedDate: latestFresh };
@@ -7479,12 +8178,7 @@ program.command("status").description("Compact status output (today + week + mon
7479
8178
  });
7480
8179
  const gapEnd = oldestCachedDate && effectiveGapStart.getTime() < new Date(oldestCachedDate).getTime() ? new Date(new Date(oldestCachedDate).getTime() - MS_PER_DAY4) : yesterdayEnd;
7481
8180
  if (effectiveGapStart.getTime() <= gapEnd.getTime()) {
7482
- const gapRange = { start: effectiveGapStart, end: gapEnd };
7483
- const gapProjects = filterProjectsByName(await parseAllSessions(gapRange, "all"), opts.project, opts.exclude);
7484
- const gapDays = fillMissingDays(gapRange.start, gapRange.end, aggregateProjectsIntoDays(gapProjects));
7485
- const coveredThrough = gapEnd.getTime() >= yesterdayEnd.getTime() ? yesterdayStr : void 0;
7486
- c = addNewDays(c, gapDays, yesterdayStr, coveredThrough ? { coveredThrough } : void 0);
7487
- await saveDailyCache(c);
8181
+ c = await runChunkedBackfill(c, effectiveGapStart, gapEnd, "oldest-first");
7488
8182
  }
7489
8183
  const updatedOldest = c.days.length > 0 ? c.days[0]?.date ?? null : null;
7490
8184
  const neededStart = new Date(todayStart.getTime() - (selectedPeriodHistoryDays - 1) * MS_PER_DAY4);
@@ -7493,11 +8187,7 @@ program.command("status").description("Compact status output (today + week + mon
7493
8187
  if (updatedOldest && clampedNeeded.getTime() < new Date(updatedOldest).getTime()) {
7494
8188
  const backEnd = new Date(new Date(updatedOldest).getTime() - MS_PER_DAY4);
7495
8189
  if (clampedNeeded.getTime() <= backEnd.getTime()) {
7496
- const backRange = { start: clampedNeeded, end: backEnd };
7497
- const backProjects = filterProjectsByName(await parseAllSessions(backRange, "all"), opts.project, opts.exclude);
7498
- const backDays = fillMissingDays(backRange.start, backRange.end, aggregateProjectsIntoDays(backProjects));
7499
- c = addNewDays(c, backDays, yesterdayStr);
7500
- await saveDailyCache(c);
8190
+ c = await runChunkedBackfill(c, clampedNeeded, backEnd, "newest-first");
7501
8191
  }
7502
8192
  }
7503
8193
  return c;
@@ -7505,7 +8195,7 @@ program.command("status").description("Compact status output (today + week + mon
7505
8195
  let currentData;
7506
8196
  const scanRange = periodInfo.range;
7507
8197
  const todayRange = { start: todayStart, end: periodInfo.range.end };
7508
- const warnings = [];
8198
+ const warnings = [...backfillWarnings];
7509
8199
  const parseStart = Date.now();
7510
8200
  let todayProjects;
7511
8201
  let allTodayProjects;
@@ -7629,15 +8319,15 @@ program.command("status").description("Compact status output (today + week + mon
7629
8319
  optimize = await scanAndDetect(todayProjects, scanRange);
7630
8320
  }
7631
8321
  }
7632
- const exeOsDir = join23(homedir14(), ".exe-os");
8322
+ const exeOsDir = join25(homedir14(), ".exe-os");
7633
8323
  const exeOsDetected = existsSync7(exeOsDir);
7634
8324
  let agentStats = null;
7635
8325
  let statsFileAge = null;
7636
8326
  try {
7637
- const statsPath = join23(exeOsDir, "agent-stats.json");
8327
+ const statsPath = join25(exeOsDir, "agent-stats.json");
7638
8328
  if (existsSync7(statsPath)) {
7639
8329
  statsFileAge = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
7640
- const parsed = JSON.parse(readFileSync3(statsPath, "utf-8"));
8330
+ const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
7641
8331
  if (parsed && Array.isArray(parsed.agents) && typeof parsed.generated === "string") {
7642
8332
  agentStats = parsed;
7643
8333
  } else {
@@ -7645,6 +8335,7 @@ program.command("status").description("Compact status output (today + week + mon
7645
8335
  }
7646
8336
  }
7647
8337
  } catch (err) {
8338
+ if (err instanceof ResourceBudgetError) throw err;
7648
8339
  const msg = err instanceof Error ? err.message : String(err);
7649
8340
  process.stderr.write(`[exe-watcher] agent-stats.json found but failed to parse: ${msg}
7650
8341
  `);
@@ -7982,9 +8673,9 @@ program.command("compare").description("Compare two AI models side-by-side").opt
7982
8673
  await renderCompare(range, opts.provider);
7983
8674
  });
7984
8675
  program.command("agent-status").description("Diagnose exe-os agent data pipeline").action(() => {
7985
- const exeOsDir = join23(homedir14(), ".exe-os");
7986
- const statsPath = join23(exeOsDir, "agent-stats.json");
7987
- const pidPath = join23(exeOsDir, "exed.pid");
8676
+ const exeOsDir = join25(homedir14(), ".exe-os");
8677
+ const statsPath = join25(exeOsDir, "agent-stats.json");
8678
+ const pidPath = join25(exeOsDir, "exed.pid");
7988
8679
  const dirExists = existsSync7(exeOsDir);
7989
8680
  const statsExists = dirExists && existsSync7(statsPath);
7990
8681
  const pidExists = dirExists && existsSync7(pidPath);
@@ -7996,7 +8687,7 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
7996
8687
  } else {
7997
8688
  try {
7998
8689
  const ageSeconds = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
7999
- const raw = readFileSync3(statsPath, "utf-8");
8690
+ const raw = readFileSync2(statsPath, "utf-8");
8000
8691
  const parsed = JSON.parse(raw);
8001
8692
  if (parsed && Array.isArray(parsed.agents) && typeof parsed.generated === "string") {
8002
8693
  const agentCount = parsed.agents.length;
@@ -8009,18 +8700,27 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
8009
8700
  lines.push(`agent-stats.json: \u2717 invalid schema`);
8010
8701
  }
8011
8702
  } catch (err) {
8703
+ if (err instanceof ResourceBudgetError) throw err;
8012
8704
  const msg = err instanceof Error ? err.message : String(err);
8013
8705
  lines.push(`agent-stats.json: \u2717 parse error: ${msg}`);
8014
8706
  }
8015
8707
  }
8016
8708
  console.log(lines.join("\n"));
8017
8709
  });
8710
+ var releaseScan;
8711
+ program.hook("preAction", async (_root, command) => {
8712
+ if (command.name() === "status") releaseScan = await acquireFileLock(join25(getCacheDir(), "scan.lock"));
8713
+ });
8714
+ program.hook("postAction", async () => {
8715
+ await releaseScan?.();
8716
+ releaseScan = void 0;
8717
+ });
8018
8718
  if (process.argv.length <= 2 && process.platform === "darwin") {
8019
8719
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
8020
8720
  if (!isTTY) {
8021
8721
  program.outputHelp();
8022
8722
  } else {
8023
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
8723
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8024
8724
  rl.question("\n No subcommand given. Install the macOS menubar app? (y/N) ", (answer) => {
8025
8725
  rl.close();
8026
8726
  if (answer.trim().toLowerCase() === "y") {
@@ -8039,6 +8739,10 @@ if (process.argv.length <= 2 && process.platform === "darwin") {
8039
8739
  });
8040
8740
  }
8041
8741
  } else {
8042
- program.parse();
8742
+ program.parseAsync().catch(async (error) => {
8743
+ await releaseScan?.();
8744
+ console.error(error instanceof Error ? error.message : String(error));
8745
+ process.exitCode = 1;
8746
+ });
8043
8747
  }
8044
8748
  //# sourceMappingURL=cli.js.map