exe-watcher 0.2.67 → 0.2.69
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/README.md +1 -1
- package/dist/cli.js +195 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ npm install -g exe-watcher
|
|
|
31
31
|
exe-watcher report
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Version 0.2.
|
|
34
|
+
Version 0.2.69 explains that first-time history setup can take 10 minutes or more for large AI histories, with saved progress and recovery details. Large Claude history scans resume within the existing resource limits. Recorded API-equivalent costs and saved totals are preserved through refresh failures, restarts and day changes. AI time and the floating quota display are included. Terminal installs require Node 22.13 or newer; the Mac app includes its own compatible runtime. Download the matching app from [Releases](https://github.com/AskExe/exe-watcher/releases/latest) or [odd.community/watcher](https://odd.community/watcher). To run the current CLI from source:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
git clone https://github.com/AskExe/exe-watcher.git
|
package/dist/cli.js
CHANGED
|
@@ -2870,6 +2870,43 @@ import { promisify } from "util";
|
|
|
2870
2870
|
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
2871
2871
|
import { mkdir as mkdir8, readFile as readFile7, stat as stat5, writeFile as writeFile3, rename as rename6, unlink as unlink5, readdir as readdir3 } from "fs/promises";
|
|
2872
2872
|
import { join as join10 } from "path";
|
|
2873
|
+
function encodeEntry(value) {
|
|
2874
|
+
let bytes = 0;
|
|
2875
|
+
try {
|
|
2876
|
+
return JSON.stringify(value, (key2, item) => {
|
|
2877
|
+
bytes += key2 ? Buffer.byteLength(JSON.stringify(key2)) + 2 : 0;
|
|
2878
|
+
bytes += item && typeof item === "object" ? 2 : Buffer.byteLength(JSON.stringify(item) ?? "null");
|
|
2879
|
+
if (bytes > MAX_ENTRY) throw new Error("parsed entry limit");
|
|
2880
|
+
return item;
|
|
2881
|
+
});
|
|
2882
|
+
} catch {
|
|
2883
|
+
return void 0;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
function fileProgress(path, dir, cachePath, before) {
|
|
2887
|
+
let startingOffset = 0;
|
|
2888
|
+
return {
|
|
2889
|
+
async load(valid) {
|
|
2890
|
+
if (!before) return;
|
|
2891
|
+
try {
|
|
2892
|
+
const size = (await stat5(cachePath)).size;
|
|
2893
|
+
if (size > MAX_ENTRY) return;
|
|
2894
|
+
chargeRead(size);
|
|
2895
|
+
const saved = JSON.parse((await decompress(await readFile7(cachePath), { maxOutputLength: MAX_ENTRY })).toString("utf8"));
|
|
2896
|
+
if (saved.v !== VERSION2 || saved.fingerprint !== before || !Number.isSafeInteger(saved.offset) || saved.offset <= 0 || saved.offset > Number(before.split(":")[2]) || !valid(saved.state)) return;
|
|
2897
|
+
startingOffset = saved.offset;
|
|
2898
|
+
return { offset: saved.offset, state: saved.state };
|
|
2899
|
+
} catch (error) {
|
|
2900
|
+
if (error instanceof ResourceBudgetError) throw error;
|
|
2901
|
+
}
|
|
2902
|
+
},
|
|
2903
|
+
async save(state, offset) {
|
|
2904
|
+
if (!before || offset <= startingOffset || offset > Number(before.split(":")[2]) || await fingerprint(path) !== before) return;
|
|
2905
|
+
const payload = encodeEntry({ v: VERSION2, fingerprint: before, offset, state });
|
|
2906
|
+
if (payload) await storeParsedEntry(dir, cachePath, payload);
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2873
2910
|
async function fingerprint(path) {
|
|
2874
2911
|
try {
|
|
2875
2912
|
const s = await stat5(path);
|
|
@@ -2883,6 +2920,7 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
|
|
|
2883
2920
|
const dir = join10(getCacheDir(), "parsed-files");
|
|
2884
2921
|
const key2 = createHash3("sha256").update(`${VERSION2}:${kind}:${path}`).digest("hex");
|
|
2885
2922
|
const cachePath = join10(dir, `v${VERSION2}-${key2}.json.gz`);
|
|
2923
|
+
const progressPath = join10(dir, `v${VERSION2}-progress-${key2}.json.gz`);
|
|
2886
2924
|
const legacyKey = createHash3("sha256").update(`3:${kind}:${path}:${getPricingFingerprint()}`).digest("hex");
|
|
2887
2925
|
const legacyPath = join10(dir, `v3-${legacyKey}.json.gz`);
|
|
2888
2926
|
const fingerprintPath = kind === "opencode" ? path.slice(0, path.lastIndexOf(":")) : path;
|
|
@@ -2929,11 +2967,11 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
|
|
|
2929
2967
|
}
|
|
2930
2968
|
}
|
|
2931
2969
|
}
|
|
2932
|
-
const data = migrated ?? await parse();
|
|
2970
|
+
const data = migrated ?? await parse(fileProgress(fingerprintPath, dir, progressPath, before));
|
|
2933
2971
|
const hasUnpricedUsage = lockRecordedCosts(data);
|
|
2934
2972
|
if (!before || await fingerprint(fingerprintPath) !== before) return data;
|
|
2935
2973
|
await rememberRange(data);
|
|
2936
|
-
const payload =
|
|
2974
|
+
const payload = encodeEntry({
|
|
2937
2975
|
v: VERSION2,
|
|
2938
2976
|
fingerprint: before,
|
|
2939
2977
|
data,
|
|
@@ -2942,7 +2980,10 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
|
|
|
2942
2980
|
hasUnpricedUsage,
|
|
2943
2981
|
pricingFingerprint: getPricingFingerprint()
|
|
2944
2982
|
});
|
|
2945
|
-
if (
|
|
2983
|
+
if (payload) await storeParsedEntry(dir, cachePath, payload, progressPath);
|
|
2984
|
+
return data;
|
|
2985
|
+
}
|
|
2986
|
+
async function storeParsedEntry(dir, cachePath, payload, obsolete) {
|
|
2946
2987
|
await mkdir8(dir, { recursive: true });
|
|
2947
2988
|
const encoded = await compress(Buffer.from(payload));
|
|
2948
2989
|
const release = await acquireFileLock(join10(dir, "writes.lock"));
|
|
@@ -2985,7 +3026,7 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
|
|
|
2985
3026
|
if (!(await readFile7(cachePath).catch(() => Buffer.alloc(0))).equals(encoded)) {
|
|
2986
3027
|
const oldSize = (await stat5(cachePath).catch(() => null))?.size ?? 0;
|
|
2987
3028
|
let nextSize = total + encoded.length;
|
|
2988
|
-
if (!Number.isFinite(nextSize)) return
|
|
3029
|
+
if (!Number.isFinite(nextSize)) return;
|
|
2989
3030
|
if (nextSize > MAX_DISK) {
|
|
2990
3031
|
const rows2 = [];
|
|
2991
3032
|
for (const name of await readdir3(dir)) {
|
|
@@ -3001,25 +3042,33 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
|
|
|
3001
3042
|
});
|
|
3002
3043
|
nextSize -= row.size;
|
|
3003
3044
|
}
|
|
3004
|
-
if (nextSize > MAX_DISK) return
|
|
3045
|
+
if (nextSize > MAX_DISK) return;
|
|
3005
3046
|
}
|
|
3006
3047
|
const tmp = `${cachePath}.${randomUUID2()}.tmp`;
|
|
3007
3048
|
try {
|
|
3008
3049
|
await writeQuota(nextSize);
|
|
3009
3050
|
await writeFile3(tmp, encoded, { mode: 384 });
|
|
3010
3051
|
await rename6(tmp, cachePath);
|
|
3052
|
+
mutationCount++;
|
|
3011
3053
|
if (oldSize) await writeQuota(nextSize - oldSize);
|
|
3054
|
+
total = nextSize - oldSize;
|
|
3012
3055
|
} finally {
|
|
3013
3056
|
await unlink5(tmp).catch(() => {
|
|
3014
3057
|
});
|
|
3015
3058
|
}
|
|
3016
3059
|
}
|
|
3060
|
+
if (obsolete) {
|
|
3061
|
+
const size = (await stat5(obsolete).catch(() => null))?.size ?? 0;
|
|
3062
|
+
if (size) {
|
|
3063
|
+
await unlink5(obsolete);
|
|
3064
|
+
await writeQuota(Math.max(0, total - size));
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3017
3067
|
} finally {
|
|
3018
3068
|
await release();
|
|
3019
3069
|
}
|
|
3020
|
-
return data;
|
|
3021
3070
|
}
|
|
3022
|
-
var VERSION2, compress, decompress, MAX_ENTRY, MAX_DISK;
|
|
3071
|
+
var VERSION2, compress, decompress, MAX_ENTRY, MAX_DISK, mutationCount, parsedFileProgress;
|
|
3023
3072
|
var init_parsed_file_cache = __esm({
|
|
3024
3073
|
"src/parsed-file-cache.ts"() {
|
|
3025
3074
|
"use strict";
|
|
@@ -3034,6 +3083,8 @@ var init_parsed_file_cache = __esm({
|
|
|
3034
3083
|
decompress = promisify(gunzip);
|
|
3035
3084
|
MAX_ENTRY = 16 * 1024 * 1024;
|
|
3036
3085
|
MAX_DISK = 256 * 1024 * 1024;
|
|
3086
|
+
mutationCount = 0;
|
|
3087
|
+
parsedFileProgress = () => mutationCount;
|
|
3037
3088
|
}
|
|
3038
3089
|
});
|
|
3039
3090
|
|
|
@@ -3142,23 +3193,22 @@ function readSessionFileSync(filePath) {
|
|
|
3142
3193
|
closeSync(fd);
|
|
3143
3194
|
}
|
|
3144
3195
|
}
|
|
3145
|
-
async function* readSessionLines(filePath, startByte = 0, skipOversizedRecord) {
|
|
3196
|
+
async function* readSessionLines(filePath, startByte = 0, skipOversizedRecord, completedLine) {
|
|
3146
3197
|
const handle = await open5(filePath, "r").catch(() => null);
|
|
3147
3198
|
if (!handle) return;
|
|
3148
3199
|
try {
|
|
3149
3200
|
const size = (await handle.stat()).size;
|
|
3150
3201
|
if (size <= startByte) return;
|
|
3151
|
-
|
|
3152
|
-
const stream = handle.createReadStream({ encoding: "utf-8", start: startByte, end: size - 1, autoClose: false });
|
|
3202
|
+
const stream = handle.createReadStream({ start: startByte, end: size - 1, autoClose: false });
|
|
3153
3203
|
let pieces = [];
|
|
3154
3204
|
let lineBytes = 0;
|
|
3155
3205
|
let discarding = false;
|
|
3156
3206
|
const maxLineBytes = 32 * 1024 * 1024;
|
|
3157
3207
|
const append = (piece) => {
|
|
3158
3208
|
if (discarding) return;
|
|
3159
|
-
lineBytes +=
|
|
3209
|
+
lineBytes += piece.length;
|
|
3160
3210
|
if (lineBytes > maxLineBytes) {
|
|
3161
|
-
const prefix = (pieces.slice(0, 4)
|
|
3211
|
+
const prefix = Buffer.concat([...pieces.slice(0, 4), piece.subarray(0, 1024)]).toString("utf8").slice(0, 1024);
|
|
3162
3212
|
if (!skipOversizedRecord?.(prefix)) throw new ResourceBudgetError(`32 MiB JSONL record budget in ${filePath}`);
|
|
3163
3213
|
discarding = true;
|
|
3164
3214
|
pieces = [];
|
|
@@ -3166,23 +3216,31 @@ async function* readSessionLines(filePath, startByte = 0, skipOversizedRecord) {
|
|
|
3166
3216
|
}
|
|
3167
3217
|
pieces.push(piece);
|
|
3168
3218
|
};
|
|
3219
|
+
let readThrough = startByte;
|
|
3169
3220
|
try {
|
|
3170
3221
|
for await (const chunk of stream) {
|
|
3222
|
+
chargeRead(chunk.length);
|
|
3223
|
+
const chunkStart = readThrough;
|
|
3224
|
+
readThrough += chunk.length;
|
|
3171
3225
|
let start = 0;
|
|
3172
3226
|
let newline;
|
|
3173
|
-
while ((newline = chunk.indexOf(
|
|
3174
|
-
append(chunk.
|
|
3227
|
+
while ((newline = chunk.indexOf(10, start)) >= 0) {
|
|
3228
|
+
append(chunk.subarray(start, newline));
|
|
3175
3229
|
chargeRead(0);
|
|
3176
|
-
const line = discarding ? null : pieces.
|
|
3230
|
+
const line = discarding ? null : (pieces.length === 1 ? pieces[0] : Buffer.concat(pieces)).toString("utf8").replace(/\r$/, "");
|
|
3177
3231
|
pieces = [];
|
|
3178
3232
|
lineBytes = 0;
|
|
3179
3233
|
discarding = false;
|
|
3180
3234
|
if (line !== null) yield line;
|
|
3235
|
+
completedLine?.(chunkStart + newline + 1);
|
|
3181
3236
|
start = newline + 1;
|
|
3182
3237
|
}
|
|
3183
|
-
if (start < chunk.length) append(chunk.
|
|
3238
|
+
if (start < chunk.length) append(chunk.subarray(start));
|
|
3239
|
+
}
|
|
3240
|
+
if (pieces.length) {
|
|
3241
|
+
yield (pieces.length === 1 ? pieces[0] : Buffer.concat(pieces)).toString("utf8").replace(/\r$/, "");
|
|
3242
|
+
completedLine?.(readThrough);
|
|
3184
3243
|
}
|
|
3185
|
-
if (pieces.length) yield pieces.join("").replace(/\r$/, "");
|
|
3186
3244
|
} finally {
|
|
3187
3245
|
stream.destroy();
|
|
3188
3246
|
}
|
|
@@ -5466,71 +5524,95 @@ function parseApiCall(entry) {
|
|
|
5466
5524
|
deduplicationKey: msg.id ?? `claude:${entry.timestamp}`
|
|
5467
5525
|
};
|
|
5468
5526
|
}
|
|
5469
|
-
|
|
5527
|
+
function validTurnScan(value) {
|
|
5528
|
+
const x = value;
|
|
5529
|
+
const amount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
5530
|
+
const calls = (list) => Array.isArray(list) && list.every((c) => c && typeof c.deduplicationKey === "string" && typeof c.model === "string" && typeof c.timestamp === "string" && amount(c.costUSD) && c.usage && ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens"].every((key2) => amount(c.usage[key2])) && Array.isArray(c.tools) && Array.isArray(c.bashCommands));
|
|
5531
|
+
return !!x && Array.isArray(x.turns) && x.turns.every((t) => t && calls(t.assistantCalls) && typeof t.userMessage === "string" && typeof t.timestamp === "string" && typeof t.sessionId === "string") && calls(x.currentCalls) && typeof x.currentUserMessage === "string" && Number.isSafeInteger(x.currentUserSignals) && x.currentUserSignals >= 0 && typeof x.currentTimestamp === "string" && typeof x.currentSessionId === "string" && Number.isSafeInteger(x.lineNumber) && x.lineNumber >= 0;
|
|
5532
|
+
}
|
|
5533
|
+
async function parseTurns(filePath, progress) {
|
|
5534
|
+
const checkpoint = await progress.load(validTurnScan);
|
|
5535
|
+
const saved = checkpoint?.state;
|
|
5470
5536
|
const seenCalls = /* @__PURE__ */ new Map();
|
|
5471
|
-
const turns = [];
|
|
5472
|
-
let currentUserMessage = "";
|
|
5473
|
-
let currentUserSignals = 0;
|
|
5474
|
-
let currentCalls = [];
|
|
5475
|
-
let currentTimestamp = "";
|
|
5476
|
-
let currentSessionId = "";
|
|
5477
|
-
|
|
5478
|
-
for
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5537
|
+
const turns = saved?.turns ?? [];
|
|
5538
|
+
let currentUserMessage = saved?.currentUserMessage ?? "";
|
|
5539
|
+
let currentUserSignals = saved?.currentUserSignals ?? 0;
|
|
5540
|
+
let currentCalls = saved?.currentCalls ?? [];
|
|
5541
|
+
let currentTimestamp = saved?.currentTimestamp ?? "";
|
|
5542
|
+
let currentSessionId = saved?.currentSessionId ?? "";
|
|
5543
|
+
for (const turn of turns) for (const call of turn.assistantCalls) seenCalls.set(call.deduplicationKey, call);
|
|
5544
|
+
for (const call of currentCalls) seenCalls.set(call.deduplicationKey, call);
|
|
5545
|
+
let lineNumber = saved?.lineNumber ?? 0;
|
|
5546
|
+
let offset = checkpoint?.offset ?? 0;
|
|
5547
|
+
try {
|
|
5548
|
+
for await (const line of readSessionLines(filePath, offset, void 0, (next) => {
|
|
5549
|
+
offset = next;
|
|
5550
|
+
})) {
|
|
5551
|
+
chargeRecord();
|
|
5552
|
+
lineNumber++;
|
|
5553
|
+
const entry = parseJsonlLine(line);
|
|
5554
|
+
if (!entry) continue;
|
|
5555
|
+
if (entry.type === "user") {
|
|
5556
|
+
const text = getUserMessageText(entry);
|
|
5557
|
+
if (text.trim()) {
|
|
5558
|
+
if (currentCalls.length > 0) {
|
|
5559
|
+
turns.push({
|
|
5560
|
+
userMessage: currentUserMessage,
|
|
5561
|
+
userMessageSignals: currentUserSignals,
|
|
5562
|
+
assistantCalls: currentCalls,
|
|
5563
|
+
timestamp: currentTimestamp,
|
|
5564
|
+
sessionId: currentSessionId
|
|
5565
|
+
});
|
|
5566
|
+
}
|
|
5567
|
+
const compact = compactUserMessage(text);
|
|
5568
|
+
currentUserMessage = compact.userMessage;
|
|
5569
|
+
currentUserSignals = compact.userMessageSignals;
|
|
5570
|
+
currentCalls = [];
|
|
5571
|
+
currentTimestamp = entry.timestamp ?? "";
|
|
5572
|
+
currentSessionId = entry.sessionId ?? "";
|
|
5493
5573
|
}
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5574
|
+
} else if (entry.type === "assistant") {
|
|
5575
|
+
const msgId = getMessageId(entry);
|
|
5576
|
+
const call = parseApiCall(entry);
|
|
5577
|
+
if (!call) continue;
|
|
5578
|
+
const previous = msgId ? seenCalls.get(msgId) : void 0;
|
|
5579
|
+
if (previous) {
|
|
5580
|
+
for (const key2 of Object.keys(call.usage)) {
|
|
5581
|
+
previous.usage[key2] = Math.max(previous.usage[key2] ?? 0, call.usage[key2] ?? 0);
|
|
5582
|
+
}
|
|
5583
|
+
previous.tools = [.../* @__PURE__ */ new Set([...previous.tools, ...call.tools])];
|
|
5584
|
+
previous.mcpTools = extractMcpTools(previous.tools);
|
|
5585
|
+
previous.bashCommands = [.../* @__PURE__ */ new Set([...previous.bashCommands, ...call.bashCommands])];
|
|
5586
|
+
previous.hasAgentSpawn ||= call.hasAgentSpawn;
|
|
5587
|
+
previous.hasPlanMode ||= call.hasPlanMode;
|
|
5588
|
+
if (call.speed === "fast") previous.speed = "fast";
|
|
5589
|
+
previous.costUSD = calculateCost(
|
|
5590
|
+
previous.model,
|
|
5591
|
+
previous.usage.inputTokens,
|
|
5592
|
+
previous.usage.outputTokens,
|
|
5593
|
+
previous.usage.cacheCreationInputTokens,
|
|
5594
|
+
previous.usage.cacheReadInputTokens,
|
|
5595
|
+
previous.usage.webSearchRequests,
|
|
5596
|
+
previous.speed,
|
|
5597
|
+
previous.usage.inputTokens + previous.usage.cacheCreationInputTokens + previous.usage.cacheReadInputTokens,
|
|
5598
|
+
previous.usage.cacheCreation1hInputTokens
|
|
5599
|
+
);
|
|
5600
|
+
continue;
|
|
5509
5601
|
}
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
previous.hasAgentSpawn ||= call.hasAgentSpawn;
|
|
5514
|
-
previous.hasPlanMode ||= call.hasPlanMode;
|
|
5515
|
-
if (call.speed === "fast") previous.speed = "fast";
|
|
5516
|
-
previous.costUSD = calculateCost(
|
|
5517
|
-
previous.model,
|
|
5518
|
-
previous.usage.inputTokens,
|
|
5519
|
-
previous.usage.outputTokens,
|
|
5520
|
-
previous.usage.cacheCreationInputTokens,
|
|
5521
|
-
previous.usage.cacheReadInputTokens,
|
|
5522
|
-
previous.usage.webSearchRequests,
|
|
5523
|
-
previous.speed,
|
|
5524
|
-
previous.usage.inputTokens + previous.usage.cacheCreationInputTokens + previous.usage.cacheReadInputTokens,
|
|
5525
|
-
previous.usage.cacheCreation1hInputTokens
|
|
5526
|
-
);
|
|
5527
|
-
continue;
|
|
5602
|
+
if (msgId) seenCalls.set(msgId, call);
|
|
5603
|
+
else call.deduplicationKey = `${filePath}:${lineNumber}`;
|
|
5604
|
+
currentCalls.push(call);
|
|
5528
5605
|
}
|
|
5529
|
-
if (msgId) seenCalls.set(msgId, call);
|
|
5530
|
-
else call.deduplicationKey = `${filePath}:${lineNumber}`;
|
|
5531
|
-
chargeRecord();
|
|
5532
|
-
currentCalls.push(call);
|
|
5533
5606
|
}
|
|
5607
|
+
} catch (error) {
|
|
5608
|
+
if (error instanceof ResourceBudgetError) {
|
|
5609
|
+
try {
|
|
5610
|
+
await progress.save({ turns, currentCalls, currentUserMessage, currentUserSignals, currentTimestamp, currentSessionId, lineNumber }, offset);
|
|
5611
|
+
} catch {
|
|
5612
|
+
_parseWarnings.push("A historical transcript checkpoint could not be saved; its unfinished usage remains unknown.");
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
throw error;
|
|
5534
5616
|
}
|
|
5535
5617
|
if (currentCalls.length > 0) {
|
|
5536
5618
|
turns.push({
|
|
@@ -5635,7 +5717,7 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
|
|
|
5635
5717
|
} catch {
|
|
5636
5718
|
}
|
|
5637
5719
|
}
|
|
5638
|
-
const rawTurns = await cachedFileParse(filePath, "claude-turns", () => parseTurns(filePath));
|
|
5720
|
+
const rawTurns = await cachedFileParse(filePath, "claude-turns", (progress) => parseTurns(filePath, progress));
|
|
5639
5721
|
const sessionId = basename9(filePath, ".jsonl");
|
|
5640
5722
|
const turns = [];
|
|
5641
5723
|
for (const turn of rawTurns) {
|
|
@@ -6976,6 +7058,7 @@ function isValidCache(parsed) {
|
|
|
6976
7058
|
if (c.version !== DAILY_CACHE_VERSION && c.version !== 8) return false;
|
|
6977
7059
|
if (typeof c.scopeKey !== "string" || c.scopeKey.length === 0) return false;
|
|
6978
7060
|
if (!Array.isArray(c.days)) return false;
|
|
7061
|
+
if (c.historyScanProgress !== void 0 && (!Number.isSafeInteger(c.historyScanProgress) || c.historyScanProgress < 0)) return false;
|
|
6979
7062
|
if (c.missingProviders && (!Array.isArray(c.missingProviders) || !c.missingProviders.every((p) => typeof p === "string"))) return false;
|
|
6980
7063
|
if (c.historyBuild) {
|
|
6981
7064
|
const stage = c.historyBuild;
|
|
@@ -7054,6 +7137,7 @@ function addNewDays(cache, incoming, newestDate, options = {}) {
|
|
|
7054
7137
|
lastComputedDate: nextLast,
|
|
7055
7138
|
days: merged,
|
|
7056
7139
|
revalidatedAt: cache.revalidatedAt,
|
|
7140
|
+
historyScanProgress: cache.historyScanProgress,
|
|
7057
7141
|
backfill: cache.backfill,
|
|
7058
7142
|
partialDates: cache.partialDates
|
|
7059
7143
|
};
|
|
@@ -10980,7 +11064,8 @@ function buildPeriodSnapshots(cache, todayDays, today, windows, provider, names,
|
|
|
10980
11064
|
cache.historyBuild?.chunked,
|
|
10981
11065
|
cache.missingProviders,
|
|
10982
11066
|
cache.partialDates,
|
|
10983
|
-
cache.backfill
|
|
11067
|
+
cache.backfill,
|
|
11068
|
+
cache.historyScanProgress
|
|
10984
11069
|
])).digest("hex"),
|
|
10985
11070
|
message: window.key === "all" ? `Saved history through ${committed.at(-1).date}${live2.length ? ", plus today" : ""}. Some history is still missing; this is not the complete All total.` : `${hasData ? "Showing known usage" : "Recovering local usage"} for ${window.label}. ${covered} of ${required} historical days loaded${live2.some(recorded) ? ", plus today" : ""}. Missing days are unknown; this total is incomplete.`
|
|
10986
11071
|
} : void 0;
|
|
@@ -11132,6 +11217,7 @@ async function completeHistoryBuild(cache, start, end, providers, scan, save2, o
|
|
|
11132
11217
|
}
|
|
11133
11218
|
|
|
11134
11219
|
// src/cli.ts
|
|
11220
|
+
init_parsed_file_cache();
|
|
11135
11221
|
import { createInterface } from "readline";
|
|
11136
11222
|
import { createRequire as createRequire3 } from "module";
|
|
11137
11223
|
|
|
@@ -11511,20 +11597,31 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
11511
11597
|
const isAllProviders = pf === "all";
|
|
11512
11598
|
const dailyCacheScope = buildDailyCacheScopeKey(opts.project, opts.exclude);
|
|
11513
11599
|
const backfillWarnings = [];
|
|
11514
|
-
|
|
11600
|
+
let savedParserProgress = parsedFileProgress();
|
|
11601
|
+
const saveHistoryCache = async (c) => {
|
|
11602
|
+
const progress = parsedFileProgress();
|
|
11603
|
+
const advanced = progress > savedParserProgress;
|
|
11604
|
+
c.historyScanProgress = (c.historyScanProgress ?? 0) + progress - savedParserProgress;
|
|
11605
|
+
savedParserProgress = progress;
|
|
11606
|
+
if (c.backfill) {
|
|
11607
|
+
c.backfill.attempts = advanced ? 0 : Math.min(c.backfill.attempts, MAX_BLOCKED_CHUNK_ATTEMPTS);
|
|
11608
|
+
}
|
|
11609
|
+
await saveDailyCache(c);
|
|
11610
|
+
};
|
|
11611
|
+
const runChunkedBackfill = async (cache2, start, end, order, repair = false) => {
|
|
11515
11612
|
let c = cache2;
|
|
11516
11613
|
const chunks = planBackfillChunks(start, end);
|
|
11517
11614
|
if (order === "newest-first") chunks.reverse();
|
|
11518
11615
|
for (const chunk of chunks) {
|
|
11519
11616
|
const chunkDate = toDateString(chunk.start);
|
|
11520
11617
|
const coveredThrough = toDateString(chunk.end);
|
|
11521
|
-
if (shouldSkipBlockedChunk(c.backfill, chunkDate)) {
|
|
11618
|
+
if (!repair && shouldSkipBlockedChunk(c.backfill, chunkDate)) {
|
|
11522
11619
|
const blanks = fillMissingDays(chunk.start, chunk.end, []);
|
|
11523
11620
|
c = addNewDays(c, blanks, yesterdayStr, { coveredThrough });
|
|
11524
11621
|
c.partialDates = [.../* @__PURE__ */ new Set([...c.partialDates ?? [], ...blanks.map((d) => d.date)])];
|
|
11525
11622
|
c.backfill = null;
|
|
11526
11623
|
c.revalidatedAt = Date.now();
|
|
11527
|
-
await
|
|
11624
|
+
await saveHistoryCache(c);
|
|
11528
11625
|
const warn2 = `history for ${chunkDate} exceeded the scan budget ${MAX_BLOCKED_CHUNK_ATTEMPTS} times; recorded as incomplete so newer days can load`;
|
|
11529
11626
|
backfillWarnings.push(warn2);
|
|
11530
11627
|
process.stderr.write(`[exe-watcher] WARNING: ${warn2}
|
|
@@ -11536,14 +11633,15 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
11536
11633
|
const chunkProjects = filterProjectsByName(await parseAllSessions(chunkRange, "all", true), opts.project, opts.exclude);
|
|
11537
11634
|
const chunkDays = fillMissingDays(chunk.start, chunk.end, aggregateProjectsIntoDays(chunkProjects));
|
|
11538
11635
|
c = addNewDays(c, chunkDays, yesterdayStr, { coveredThrough });
|
|
11636
|
+
c.partialDates = (c.partialDates ?? []).filter((date2) => date2 < chunkDate || date2 > coveredThrough);
|
|
11539
11637
|
c.backfill = null;
|
|
11540
11638
|
c.revalidatedAt = Date.now();
|
|
11541
|
-
await
|
|
11639
|
+
await saveHistoryCache(c);
|
|
11542
11640
|
} catch (err) {
|
|
11543
11641
|
if (!(err instanceof ResourceBudgetError)) throw err;
|
|
11544
11642
|
c.backfill = nextBackfillCursor(c.backfill, chunkDate);
|
|
11545
11643
|
c.revalidatedAt = Date.now();
|
|
11546
|
-
await
|
|
11644
|
+
await saveHistoryCache(c);
|
|
11547
11645
|
const warn2 = `history backfill paused at ${chunkDate} (attempt ${c.backfill.attempts}): ${err.message}`;
|
|
11548
11646
|
backfillWarnings.push(warn2);
|
|
11549
11647
|
process.stderr.write(`[exe-watcher] WARNING: ${warn2}
|
|
@@ -11564,7 +11662,7 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
11564
11662
|
opts.project,
|
|
11565
11663
|
opts.exclude
|
|
11566
11664
|
)),
|
|
11567
|
-
|
|
11665
|
+
saveHistoryCache,
|
|
11568
11666
|
{ mergeExisting }
|
|
11569
11667
|
);
|
|
11570
11668
|
const pausedHistory = (c) => {
|
|
@@ -11574,6 +11672,7 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
11574
11672
|
};
|
|
11575
11673
|
const cache = await withScanBudget(() => withScanSlice(() => withDailyCacheLock(async () => {
|
|
11576
11674
|
let c = await loadDailyCache(dailyCacheScope);
|
|
11675
|
+
const repairDates = [...new Set(c.partialDates ?? [])].sort();
|
|
11577
11676
|
if (opts.period === "today" && opts.optimize === false) return c;
|
|
11578
11677
|
if (c.missingProviders?.length && c.days.length && !c.needsRepricing) {
|
|
11579
11678
|
const from = parseHistoryStartDate(c.days[0].date);
|
|
@@ -11619,6 +11718,17 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
11619
11718
|
c = await runChunkedBackfill(c, clampedNeeded, backEnd, "newest-first");
|
|
11620
11719
|
}
|
|
11621
11720
|
}
|
|
11721
|
+
if (!c.backfill || repairDates.includes(c.backfill.blockedDate)) {
|
|
11722
|
+
const selectedStart = toDateString(periodInfo.range.start);
|
|
11723
|
+
for (const date2 of repairDates) {
|
|
11724
|
+
if (date2 < selectedStart || date2 > yesterdayStr || !c.partialDates?.includes(date2)) continue;
|
|
11725
|
+
const start = parseHistoryStartDate(date2);
|
|
11726
|
+
const end = new Date(start);
|
|
11727
|
+
end.setHours(23, 59, 59, 999);
|
|
11728
|
+
c = await runChunkedBackfill(c, start, end, "oldest-first", true);
|
|
11729
|
+
if (c.backfill) break;
|
|
11730
|
+
}
|
|
11731
|
+
}
|
|
11622
11732
|
return c;
|
|
11623
11733
|
}), { maxMs: 1e4, maxBytes: 8 * 1024 ** 3, maxRecords: 2e6 }), 2e6, void 0, 8 * 1024 ** 3);
|
|
11624
11734
|
if (opts.optimize !== false || opts.period === "all" && !hasRecordedHistory(cache, toDateString(todayStart))) {
|
package/package.json
CHANGED