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.
- package/dist/cli.js +1599 -895
- 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
|
|
14
|
-
import { homedir
|
|
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
|
|
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
|
|
29
|
-
import { open as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
425
|
-
import { open as
|
|
426
|
-
import { join as
|
|
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
|
|
644
|
+
return join14(getCacheDir(), CACHE_FILE);
|
|
430
645
|
}
|
|
431
646
|
async function getDbFingerprint(dbPath) {
|
|
432
647
|
try {
|
|
433
|
-
const s = await
|
|
434
|
-
|
|
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
|
|
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.
|
|
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
|
|
678
|
+
await mkdir9(dir, { recursive: true });
|
|
459
679
|
const cache = {
|
|
460
|
-
|
|
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
|
|
687
|
+
const handle = await open6(tmpPath, "w", 384);
|
|
467
688
|
try {
|
|
468
|
-
await handle.writeFile(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
797
|
+
return join15(homedir8(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
565
798
|
}
|
|
566
799
|
if (process.platform === "win32") {
|
|
567
|
-
return
|
|
800
|
+
return join15(homedir8(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
568
801
|
}
|
|
569
|
-
return
|
|
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
|
|
803
|
-
import { join as
|
|
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
|
|
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"] ??
|
|
810
|
-
return
|
|
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
|
|
815
|
-
return entries.filter((f) => f.startsWith("opencode") && f.endsWith(".db")).map((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
|
|
1069
|
+
"SELECT id FROM session LIMIT 1"
|
|
828
1070
|
);
|
|
829
1071
|
db.query(
|
|
830
|
-
"SELECT
|
|
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 ?
|
|
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
|
|
1280
|
+
import { createHash as createHash3 } from "crypto";
|
|
1029
1281
|
import { existsSync as existsSync2 } from "fs";
|
|
1030
|
-
import { readdir as
|
|
1031
|
-
import { join as
|
|
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
|
|
1287
|
+
return join17(homedir10(), ".cursor");
|
|
1036
1288
|
}
|
|
1037
1289
|
function getProjectsDir2(baseDir) {
|
|
1038
|
-
return
|
|
1290
|
+
return join17(baseDir, "projects");
|
|
1039
1291
|
}
|
|
1040
1292
|
function getAttributionDbPath(baseDir) {
|
|
1041
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1552
|
+
const transcriptDir = join17(projectsDir, entry.name, "agent-transcripts");
|
|
1299
1553
|
if (!existsSync2(transcriptDir)) continue;
|
|
1300
|
-
const transcriptEntries = await
|
|
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 =
|
|
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
|
-
|
|
1362
|
-
|
|
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
|
|
1711
|
+
return join2(homedir2(), "Applications");
|
|
1386
1712
|
}
|
|
1387
1713
|
async function exists(path) {
|
|
1388
1714
|
try {
|
|
1389
|
-
await
|
|
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 =
|
|
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(
|
|
1906
|
+
const stagingDir = await mkdtemp(join2(tmpdir(), "exe-watcher-menubar-"));
|
|
1581
1907
|
try {
|
|
1582
|
-
const archivePath =
|
|
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 =
|
|
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
|
|
1594
|
-
const backupPath = alreadyInstalled ?
|
|
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
|
|
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
|
|
1643
|
-
import { open as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
2352
|
-
import { basename as basename8, join as join16 } from "path";
|
|
2677
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
2353
2678
|
|
|
2354
|
-
// src/
|
|
2355
|
-
import {
|
|
2356
|
-
import {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
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
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
if (
|
|
2419
|
-
|
|
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
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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
|
|
2457
|
-
import { basename as basename2, join as
|
|
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"] ||
|
|
3167
|
+
return process.env["CLAUDE_CONFIG_DIR"] || join9(homedir4(), ".claude");
|
|
2475
3168
|
}
|
|
2476
3169
|
function getProjectsDir() {
|
|
2477
|
-
return
|
|
3170
|
+
return join9(getClaudeDir(), "projects");
|
|
2478
3171
|
}
|
|
2479
3172
|
function getDesktopSessionsDir() {
|
|
2480
|
-
if (process.platform === "darwin") return
|
|
2481
|
-
if (process.platform === "win32") return
|
|
2482
|
-
return
|
|
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
|
|
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 =
|
|
2492
|
-
const s = await
|
|
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
|
|
3188
|
+
const projectDirs = await readdir4(full).catch(() => []);
|
|
2496
3189
|
for (const pd of projectDirs) {
|
|
2497
|
-
const pdFull =
|
|
2498
|
-
const pdStat = await
|
|
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
|
|
3219
|
+
const entries = await readdir4(projectsDir);
|
|
2527
3220
|
for (const dirName of entries) {
|
|
2528
|
-
const dirPath =
|
|
2529
|
-
const dirStat = await
|
|
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
|
|
2552
|
-
import { basename as basename3, join as
|
|
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"] ??
|
|
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
|
|
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
|
|
3407
|
+
async function readCodexHeader(filePath) {
|
|
2595
3408
|
const entry = await readFirstLine(filePath);
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
return {
|
|
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 =
|
|
3414
|
+
const sessionsDir = join11(codexDir, "sessions");
|
|
2602
3415
|
const sources = [];
|
|
2603
3416
|
let years;
|
|
2604
3417
|
try {
|
|
2605
|
-
years = await
|
|
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 =
|
|
2612
|
-
const months = await
|
|
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 =
|
|
2616
|
-
const days = await
|
|
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 =
|
|
2620
|
-
const files = await
|
|
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 =
|
|
2624
|
-
const s = await
|
|
3436
|
+
const filePath = join11(dayDir, file);
|
|
3437
|
+
const s = await stat7(filePath).catch(() => null);
|
|
2625
3438
|
if (!s?.isFile()) continue;
|
|
2626
|
-
const
|
|
2627
|
-
if (!
|
|
2628
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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
|
|
3718
|
+
sessionDirs = await readdir6(sessionStateDir);
|
|
2903
3719
|
} catch {
|
|
2904
3720
|
return sources;
|
|
2905
3721
|
}
|
|
2906
3722
|
for (const sessionId of sessionDirs) {
|
|
2907
|
-
const eventsPath =
|
|
2908
|
-
const s = await
|
|
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(
|
|
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
|
-
|
|
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 ??
|
|
3790
|
+
return override ?? join13(homedir7(), ".pi", "agent", "sessions");
|
|
2974
3791
|
}
|
|
2975
3792
|
function getOmpSessionsDir(override) {
|
|
2976
|
-
return override ??
|
|
3793
|
+
return override ?? join13(homedir7(), ".omp", "agent", "sessions");
|
|
2977
3794
|
}
|
|
2978
3795
|
async function readFirstEntry(filePath) {
|
|
2979
|
-
const
|
|
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
|
|
3808
|
+
projectDirs = await readdir7(sessionsDir);
|
|
2994
3809
|
} catch {
|
|
2995
3810
|
return sources;
|
|
2996
3811
|
}
|
|
2997
3812
|
for (const dirName of projectDirs) {
|
|
2998
|
-
const dirPath =
|
|
2999
|
-
const dirStat = await
|
|
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
|
|
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 =
|
|
3010
|
-
const fileStat = await
|
|
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
|
-
|
|
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 =
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
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
|
-
|
|
3293
|
-
if (
|
|
3294
|
-
if (
|
|
3295
|
-
if (
|
|
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,
|
|
4127
|
+
function refineByKeywords(category, turn) {
|
|
3308
4128
|
if (category === "building") {
|
|
3309
|
-
if (DEBUG_KEYWORDS
|
|
4129
|
+
if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
|
|
3310
4130
|
return "building";
|
|
3311
4131
|
}
|
|
3312
4132
|
if (category === "research") {
|
|
3313
|
-
if (DEBUG_KEYWORDS
|
|
4133
|
+
if (messageMatches(turn, DEBUG_KEYWORDS)) return "debugging";
|
|
3314
4134
|
return "research";
|
|
3315
4135
|
}
|
|
3316
4136
|
return category;
|
|
3317
4137
|
}
|
|
3318
|
-
function classifyConversation(
|
|
3319
|
-
if (BRAINSTORM_KEYWORDS
|
|
3320
|
-
if (RESEARCH_KEYWORDS
|
|
3321
|
-
if (DEBUG_KEYWORDS
|
|
3322
|
-
if (FEATURE_KEYWORDS
|
|
3323
|
-
if (FILE_PATTERNS
|
|
3324
|
-
if (SCRIPT_PATTERNS
|
|
3325
|
-
if (URL_PATTERN
|
|
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
|
|
4173
|
+
category = classifyConversation(turn);
|
|
3354
4174
|
} else {
|
|
3355
4175
|
const toolCategory = classifyByToolPattern(turn);
|
|
3356
4176
|
if (toolCategory) {
|
|
3357
|
-
category = refineByKeywords(toolCategory, turn
|
|
4177
|
+
category = refineByKeywords(toolCategory, turn);
|
|
3358
4178
|
} else {
|
|
3359
|
-
category = classifyConversation(turn
|
|
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
|
|
4187
|
+
import { stat as stat12 } from "fs/promises";
|
|
3368
4188
|
import { existsSync as existsSync3, unlinkSync } from "fs";
|
|
3369
|
-
import { readFile as
|
|
3370
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4231
|
+
await rename8(tempPath, finalPath);
|
|
3412
4232
|
} catch (err) {
|
|
3413
4233
|
try {
|
|
3414
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
3692
|
-
const files = await
|
|
3693
|
-
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((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 =
|
|
3697
|
-
const subFiles = await
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3794
|
-
|
|
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
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
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 (
|
|
3819
|
-
|
|
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 =
|
|
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
|
|
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
|
|
4013
|
-
import { join as
|
|
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 =
|
|
4019
|
-
const pkg = JSON.parse(
|
|
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
|
|
5046
|
+
import { createHash as createHash4, randomBytes as randomBytes6 } from "crypto";
|
|
4172
5047
|
import { existsSync as existsSync4 } from "fs";
|
|
4173
|
-
import { mkdir as
|
|
4174
|
-
import { join as
|
|
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
|
|
5055
|
+
return join21(getCacheDir(), DAILY_CACHE_FILENAME);
|
|
4181
5056
|
}
|
|
4182
|
-
return
|
|
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-${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
5111
|
+
await rename9(tempPath, finalPath);
|
|
4236
5112
|
} catch (err) {
|
|
4237
5113
|
try {
|
|
4238
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
4362
|
-
const files = await
|
|
4363
|
-
const result = files.filter((f) => f.endsWith(".jsonl")).map((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 =
|
|
4367
|
-
const subFiles = await
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
4508
|
-
|
|
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(
|
|
4512
|
-
configPaths.push(
|
|
4513
|
-
configPaths.push(
|
|
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 =
|
|
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 :
|
|
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 =
|
|
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
|
|
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
|
|
5747
|
+
const entries = await readdir11(dir);
|
|
4824
5748
|
const names = [];
|
|
4825
5749
|
for (const entry of entries) {
|
|
4826
|
-
if (existsSync5(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
4996
|
-
return `${dr}:${
|
|
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
|
-
|
|
6057
|
+
init_fs_utils();
|
|
6058
|
+
import { readdir as readdir12 } from "fs/promises";
|
|
5134
6059
|
import { existsSync as existsSync6 } from "fs";
|
|
5135
|
-
import { join as
|
|
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
|
-
|
|
5158
|
-
|
|
5159
|
-
];
|
|
5160
|
-
if (projectPath) {
|
|
5161
|
-
configPaths.push(
|
|
5162
|
-
configPaths.push(
|
|
5163
|
-
configPaths.push(
|
|
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
|
-
|
|
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
|
|
5430
|
-
const
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
const
|
|
5439
|
-
|
|
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
|
-
|
|
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
|
|
5486
|
-
const
|
|
6122
|
+
async function scanMemoryFiles(projectPath) {
|
|
6123
|
+
const home = homedir12();
|
|
5487
6124
|
const files = [];
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
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
|
|
5503
|
-
const
|
|
5504
|
-
const
|
|
5505
|
-
|
|
5506
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
6956
|
-
const
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
if (reloadGenerationRef.current !== generation) return;
|
|
6963
|
-
|
|
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
|
-
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
7986
|
-
const statsPath =
|
|
7987
|
-
const pidPath =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|