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