exe-watcher 0.2.54 → 0.2.66
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/LICENSE +2 -2
- package/README.md +167 -65
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/cli.js +2110 -668
- package/package.json +6 -3
package/dist/cli.js
CHANGED
|
@@ -86,7 +86,7 @@ async function acquireFileLock(path, timeoutMs = 15e3) {
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
if (Date.now() - started >= timeoutMs) throw new Error("Another Watcher scan is still running. Keeping the last successful refresh.");
|
|
89
|
-
await new Promise((
|
|
89
|
+
await new Promise((resolve4) => setTimeout(resolve4, 50));
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
var init_file_lock = __esm({
|
|
@@ -98,6 +98,11 @@ var init_file_lock = __esm({
|
|
|
98
98
|
// src/resource-budget.ts
|
|
99
99
|
import { getHeapStatistics } from "v8";
|
|
100
100
|
import { AsyncLocalStorage } from "async_hooks";
|
|
101
|
+
function withScanSlice(fn, limits) {
|
|
102
|
+
const parent = scans.getStore();
|
|
103
|
+
if (!parent) return withScanBudget(() => withScanSlice(fn, limits));
|
|
104
|
+
return slices.run({ bytes: parent.bytes, records: parent.records, started: Date.now(), ...limits }, fn);
|
|
105
|
+
}
|
|
101
106
|
function effectiveMaxBytes(requested) {
|
|
102
107
|
const override = Number(process.env["EXE_WATCHER_MAX_SCAN_BYTES"]);
|
|
103
108
|
return Number.isFinite(override) && override > 0 ? Math.min(requested, override) : requested;
|
|
@@ -118,11 +123,17 @@ function chargeRead(bytes) {
|
|
|
118
123
|
global.gc?.();
|
|
119
124
|
if (process.memoryUsage().heapUsed > heapBudget) throw new ResourceBudgetError("parser heap budget");
|
|
120
125
|
}
|
|
126
|
+
const slice = slices.getStore();
|
|
127
|
+
if (slice && (budget.bytes - slice.bytes > slice.maxBytes || Date.now() - slice.started > slice.maxMs)) {
|
|
128
|
+
throw new ScanSliceLimitError("analysis slice budget");
|
|
129
|
+
}
|
|
121
130
|
}
|
|
122
131
|
function chargeRecord() {
|
|
123
132
|
const budget = scans.getStore();
|
|
124
133
|
if (!budget) return;
|
|
125
134
|
if (++budget.records > budget.maxRecords) throw new ResourceBudgetError(`${budget.maxRecords.toLocaleString()} scanned usage records`);
|
|
135
|
+
const slice = slices.getStore();
|
|
136
|
+
if (slice && budget.records - slice.records > slice.maxRecords) throw new ScanSliceLimitError("analysis record slice");
|
|
126
137
|
if (budget.records % 256 === 0) chargeRead(0);
|
|
127
138
|
}
|
|
128
139
|
function checkFileSize(size, cap) {
|
|
@@ -130,7 +141,7 @@ function checkFileSize(size, cap) {
|
|
|
130
141
|
if (scans.getStore()) throw new ResourceBudgetError(`file exceeds ${cap} bytes`);
|
|
131
142
|
return false;
|
|
132
143
|
}
|
|
133
|
-
var ResourceBudgetError, scans;
|
|
144
|
+
var ResourceBudgetError, ScanSliceLimitError, scans, slices;
|
|
134
145
|
var init_resource_budget = __esm({
|
|
135
146
|
"src/resource-budget.ts"() {
|
|
136
147
|
"use strict";
|
|
@@ -140,7 +151,10 @@ var init_resource_budget = __esm({
|
|
|
140
151
|
this.name = "ResourceBudgetError";
|
|
141
152
|
}
|
|
142
153
|
};
|
|
154
|
+
ScanSliceLimitError = class extends ResourceBudgetError {
|
|
155
|
+
};
|
|
143
156
|
scans = new AsyncLocalStorage();
|
|
157
|
+
slices = new AsyncLocalStorage();
|
|
144
158
|
}
|
|
145
159
|
});
|
|
146
160
|
|
|
@@ -579,19 +593,19 @@ function mergeReportSession(target, source) {
|
|
|
579
593
|
}
|
|
580
594
|
function mergeCounters(target, source) {
|
|
581
595
|
const dst = target;
|
|
582
|
-
for (const [
|
|
583
|
-
const old = Object.hasOwn(dst,
|
|
596
|
+
for (const [key2, value] of Object.entries(source)) {
|
|
597
|
+
const old = Object.hasOwn(dst, key2) ? dst[key2] : void 0;
|
|
584
598
|
let next = old;
|
|
585
599
|
if (typeof value === "number") next = (typeof old === "number" ? old : 0) + value;
|
|
586
600
|
else if (typeof value === "string") {
|
|
587
|
-
if (
|
|
588
|
-
else if (
|
|
601
|
+
if (key2 === "firstSeen" || key2 === "firstTimestamp") next = !old || value && value < String(old) ? value : old;
|
|
602
|
+
else if (key2 === "lastSeen" || key2 === "lastTimestamp") next = !old || value > String(old) ? value : old;
|
|
589
603
|
else next = old ?? value;
|
|
590
604
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
591
605
|
next = old && typeof old === "object" ? old : /* @__PURE__ */ Object.create(null);
|
|
592
606
|
mergeCounters(next, value);
|
|
593
607
|
}
|
|
594
|
-
Object.defineProperty(dst,
|
|
608
|
+
Object.defineProperty(dst, key2, { value: next, writable: true, enumerable: true, configurable: true });
|
|
595
609
|
}
|
|
596
610
|
}
|
|
597
611
|
var planning;
|
|
@@ -605,12 +619,12 @@ var init_report_summary = __esm({
|
|
|
605
619
|
});
|
|
606
620
|
|
|
607
621
|
// src/day-aggregator.ts
|
|
608
|
-
function formatDate(
|
|
609
|
-
return `${
|
|
622
|
+
function formatDate(date2) {
|
|
623
|
+
return `${date2.getFullYear()}-${String(date2.getMonth() + 1).padStart(2, "0")}-${String(date2.getDate()).padStart(2, "0")}`;
|
|
610
624
|
}
|
|
611
|
-
function emptyEntry(
|
|
625
|
+
function emptyEntry(date2) {
|
|
612
626
|
return {
|
|
613
|
-
date,
|
|
627
|
+
date: date2,
|
|
614
628
|
cost: 0,
|
|
615
629
|
calls: 0,
|
|
616
630
|
sessions: 0,
|
|
@@ -620,10 +634,10 @@ function emptyEntry(date) {
|
|
|
620
634
|
cacheWriteTokens: 0,
|
|
621
635
|
editTurns: 0,
|
|
622
636
|
oneShotTurns: 0,
|
|
623
|
-
models:
|
|
624
|
-
categories:
|
|
625
|
-
providers:
|
|
626
|
-
projects:
|
|
637
|
+
models: /* @__PURE__ */ Object.create(null),
|
|
638
|
+
categories: /* @__PURE__ */ Object.create(null),
|
|
639
|
+
providers: /* @__PURE__ */ Object.create(null),
|
|
640
|
+
projects: /* @__PURE__ */ Object.create(null)
|
|
627
641
|
};
|
|
628
642
|
}
|
|
629
643
|
function emptyProviderBreakdown() {
|
|
@@ -637,8 +651,8 @@ function emptyProviderBreakdown() {
|
|
|
637
651
|
cacheWriteTokens: 0,
|
|
638
652
|
editTurns: 0,
|
|
639
653
|
oneShotTurns: 0,
|
|
640
|
-
models:
|
|
641
|
-
categories:
|
|
654
|
+
models: /* @__PURE__ */ Object.create(null),
|
|
655
|
+
categories: /* @__PURE__ */ Object.create(null)
|
|
642
656
|
};
|
|
643
657
|
}
|
|
644
658
|
function dateKey(iso) {
|
|
@@ -646,16 +660,16 @@ function dateKey(iso) {
|
|
|
646
660
|
}
|
|
647
661
|
function aggregateProjectsIntoDays(projects) {
|
|
648
662
|
const byDate = /* @__PURE__ */ new Map();
|
|
649
|
-
const ensure = (
|
|
650
|
-
let d = byDate.get(
|
|
663
|
+
const ensure = (date2) => {
|
|
664
|
+
let d = byDate.get(date2);
|
|
651
665
|
if (!d) {
|
|
652
|
-
d = emptyEntry(
|
|
653
|
-
byDate.set(
|
|
666
|
+
d = emptyEntry(date2);
|
|
667
|
+
byDate.set(date2, d);
|
|
654
668
|
}
|
|
655
669
|
return d;
|
|
656
670
|
};
|
|
657
|
-
const ensureProvider = (
|
|
658
|
-
const day = ensure(
|
|
671
|
+
const ensureProvider = (date2, provider) => {
|
|
672
|
+
const day = ensure(date2);
|
|
659
673
|
let breakdown = day.providers[provider];
|
|
660
674
|
if (!breakdown) {
|
|
661
675
|
breakdown = emptyProviderBreakdown();
|
|
@@ -663,8 +677,8 @@ function aggregateProjectsIntoDays(projects) {
|
|
|
663
677
|
}
|
|
664
678
|
return breakdown;
|
|
665
679
|
};
|
|
666
|
-
const ensureProject = (
|
|
667
|
-
const day = ensure(
|
|
680
|
+
const ensureProject = (date2, projectName) => {
|
|
681
|
+
const day = ensure(date2);
|
|
668
682
|
let project = day.projects[projectName];
|
|
669
683
|
if (!project) {
|
|
670
684
|
project = { cost: 0, sessions: 0 };
|
|
@@ -730,7 +744,7 @@ function aggregateProjectsIntoDays(projects) {
|
|
|
730
744
|
for (const [provider, calls] of callsByProvider) {
|
|
731
745
|
providersSeenInSession.add(provider);
|
|
732
746
|
const providerTurnDay = ensureProvider(turnDate, provider);
|
|
733
|
-
const providerTurnCost = calls.reduce((
|
|
747
|
+
const providerTurnCost = calls.reduce((sum2, call) => sum2 + call.costUSD, 0);
|
|
734
748
|
providerTurnDay.editTurns += editTurns;
|
|
735
749
|
providerTurnDay.oneShotTurns += oneShotTurns;
|
|
736
750
|
const providerCategory = providerTurnDay.categories[turn.category] ?? { turns: 0, cost: 0, editTurns: 0, oneShotTurns: 0 };
|
|
@@ -799,8 +813,8 @@ function fillMissingDays(start, end, days) {
|
|
|
799
813
|
const cursor2 = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
800
814
|
const endDate = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
|
801
815
|
while (cursor2.getTime() <= endDate.getTime()) {
|
|
802
|
-
const
|
|
803
|
-
filled.push(byDate.get(
|
|
816
|
+
const key2 = formatDate(cursor2);
|
|
817
|
+
filled.push(byDate.get(key2) ?? emptyEntry(key2));
|
|
804
818
|
cursor2.setDate(cursor2.getDate() + 1);
|
|
805
819
|
}
|
|
806
820
|
return filled;
|
|
@@ -812,8 +826,8 @@ function buildPeriodDataFromDays(days, label) {
|
|
|
812
826
|
}
|
|
813
827
|
let cost = 0, calls = 0, sessions = 0;
|
|
814
828
|
let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0;
|
|
815
|
-
const catTotals =
|
|
816
|
-
const modelTotals =
|
|
829
|
+
const catTotals = /* @__PURE__ */ Object.create(null);
|
|
830
|
+
const modelTotals = /* @__PURE__ */ Object.create(null);
|
|
817
831
|
for (const d of days) {
|
|
818
832
|
cost += d.cost;
|
|
819
833
|
calls += d.calls;
|
|
@@ -2368,8 +2382,8 @@ function getShortModelName(model) {
|
|
|
2368
2382
|
"MiniMax-M2.7": "MiniMax M2.7"
|
|
2369
2383
|
};
|
|
2370
2384
|
if (Object.hasOwn(shortNames2, canonical)) return shortNames2[canonical];
|
|
2371
|
-
for (const [
|
|
2372
|
-
if (canonical.startsWith(
|
|
2385
|
+
for (const [key2, name] of Object.entries(shortNames2).sort((a, b) => b[0].length - a[0].length)) {
|
|
2386
|
+
if (canonical.startsWith(key2 + "-") && /^\d{4}-?\d{2}-?\d{2}$/.test(canonical.slice(key2.length + 1))) return name;
|
|
2373
2387
|
}
|
|
2374
2388
|
return canonical;
|
|
2375
2389
|
}
|
|
@@ -2385,7 +2399,7 @@ function mergeModelCosts(primary, fallback) {
|
|
|
2385
2399
|
};
|
|
2386
2400
|
}
|
|
2387
2401
|
function findPrefixMatch(entries, canonical) {
|
|
2388
|
-
const matches = [...entries].filter(([
|
|
2402
|
+
const matches = [...entries].filter(([key2]) => canonical.startsWith(key2 + "-") && /^\d{4}-?\d{2}-?\d{2}$/.test(canonical.slice(key2.length + 1))).sort(([a], [b]) => b.length - a.length);
|
|
2389
2403
|
if (matches.length) return matches[0][1];
|
|
2390
2404
|
return null;
|
|
2391
2405
|
}
|
|
@@ -2530,35 +2544,200 @@ var init_models = __esm({
|
|
|
2530
2544
|
}
|
|
2531
2545
|
});
|
|
2532
2546
|
|
|
2547
|
+
// src/discovery-cache.ts
|
|
2548
|
+
import { randomUUID } from "crypto";
|
|
2549
|
+
import { mkdir as mkdir7, readFile as readFile6, rename as rename5, stat as stat4, unlink as unlink4, writeFile as writeFile2 } from "fs/promises";
|
|
2550
|
+
import { join as join8 } from "path";
|
|
2551
|
+
function manifestPath() {
|
|
2552
|
+
return join8(getCacheDir(), MANIFEST);
|
|
2553
|
+
}
|
|
2554
|
+
function fingerprintOf(s) {
|
|
2555
|
+
return [s.dev, s.ino, s.size, s.mtimeMs, s.ctimeMs].join(":");
|
|
2556
|
+
}
|
|
2557
|
+
function emptyManifest() {
|
|
2558
|
+
return { v: VERSION, e: /* @__PURE__ */ Object.create(null) };
|
|
2559
|
+
}
|
|
2560
|
+
function sanitize(raw) {
|
|
2561
|
+
if (!raw || typeof raw !== "object") return emptyManifest();
|
|
2562
|
+
const parsed = raw;
|
|
2563
|
+
if (parsed.v !== VERSION || !parsed.e || typeof parsed.e !== "object") return emptyManifest();
|
|
2564
|
+
const out = emptyManifest();
|
|
2565
|
+
for (const [key2, value] of Object.entries(parsed.e)) {
|
|
2566
|
+
if (key2 === "__proto__" || key2 === "constructor" || key2 === "prototype") continue;
|
|
2567
|
+
if (!value || typeof value !== "object") continue;
|
|
2568
|
+
const entry = value;
|
|
2569
|
+
if (typeof entry.f !== "string" || typeof entry.t !== "number") continue;
|
|
2570
|
+
out.e[key2] = { f: entry.f, r: entry.r ?? null, t: entry.t };
|
|
2571
|
+
}
|
|
2572
|
+
return out;
|
|
2573
|
+
}
|
|
2574
|
+
async function load() {
|
|
2575
|
+
const path = manifestPath();
|
|
2576
|
+
if (loaded && loadedFrom === path) return loaded;
|
|
2577
|
+
loaded = emptyManifest();
|
|
2578
|
+
loadedFrom = path;
|
|
2579
|
+
dirty = false;
|
|
2580
|
+
try {
|
|
2581
|
+
const s = await stat4(path);
|
|
2582
|
+
if (s.size > MAX_MANIFEST_BYTES) return loaded;
|
|
2583
|
+
chargeRead(s.size);
|
|
2584
|
+
loaded = sanitize(JSON.parse(await readFile6(path, "utf8")));
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
if (err instanceof ResourceBudgetError) throw err;
|
|
2587
|
+
}
|
|
2588
|
+
return loaded;
|
|
2589
|
+
}
|
|
2590
|
+
function evict(manifest) {
|
|
2591
|
+
const cutoff = Date.now() - MAX_AGE_MS;
|
|
2592
|
+
for (const [key2, entry] of Object.entries(manifest.e)) {
|
|
2593
|
+
if (entry.t < cutoff) delete manifest.e[key2];
|
|
2594
|
+
}
|
|
2595
|
+
const keys = Object.keys(manifest.e);
|
|
2596
|
+
if (keys.length <= MAX_ENTRIES) return;
|
|
2597
|
+
keys.sort((a, b) => manifest.e[a].t - manifest.e[b].t);
|
|
2598
|
+
for (const key2 of keys.slice(0, keys.length - MAX_ENTRIES)) delete manifest.e[key2];
|
|
2599
|
+
}
|
|
2600
|
+
async function readDiscoveryFact(kind, path, fingerprint2) {
|
|
2601
|
+
const manifest = await load();
|
|
2602
|
+
const hit = manifest.e[`${kind}\0${path}`];
|
|
2603
|
+
if (!hit || hit.f !== fingerprint2) return void 0;
|
|
2604
|
+
if (Date.now() - hit.t > TOUCH_INTERVAL_MS) {
|
|
2605
|
+
hit.t = Date.now();
|
|
2606
|
+
dirty = true;
|
|
2607
|
+
}
|
|
2608
|
+
return hit.r;
|
|
2609
|
+
}
|
|
2610
|
+
async function rememberDiscoveryFact(kind, path, fingerprint2, value) {
|
|
2611
|
+
const manifest = await load();
|
|
2612
|
+
manifest.e[`${kind}\0${path}`] = { f: fingerprint2, r: value, t: Date.now() };
|
|
2613
|
+
dirty = true;
|
|
2614
|
+
}
|
|
2615
|
+
async function cachedDiscoveryHeader(kind, path, stats, compute) {
|
|
2616
|
+
const manifest = await load();
|
|
2617
|
+
const key2 = `${kind}\0${path}`;
|
|
2618
|
+
const fingerprint2 = fingerprintOf(stats);
|
|
2619
|
+
const hit = manifest.e[key2];
|
|
2620
|
+
if (hit && hit.f === fingerprint2) {
|
|
2621
|
+
const now = Date.now();
|
|
2622
|
+
if (now - hit.t > TOUCH_INTERVAL_MS) {
|
|
2623
|
+
hit.t = now;
|
|
2624
|
+
dirty = true;
|
|
2625
|
+
}
|
|
2626
|
+
return hit.r;
|
|
2627
|
+
}
|
|
2628
|
+
const value = await compute();
|
|
2629
|
+
const after = await stat4(path).catch(() => null);
|
|
2630
|
+
if (!after || fingerprintOf(after) !== fingerprint2) return value;
|
|
2631
|
+
manifest.e[key2] = { f: fingerprint2, r: value ?? null, t: Date.now() };
|
|
2632
|
+
dirty = true;
|
|
2633
|
+
return value;
|
|
2634
|
+
}
|
|
2635
|
+
async function flushDiscoveryCache() {
|
|
2636
|
+
if (!dirty || !loaded) return;
|
|
2637
|
+
const mine = loaded;
|
|
2638
|
+
dirty = false;
|
|
2639
|
+
const path = manifestPath();
|
|
2640
|
+
const dir = getCacheDir();
|
|
2641
|
+
try {
|
|
2642
|
+
await mkdir7(dir, { recursive: true });
|
|
2643
|
+
const release = await acquireFileLock(join8(dir, "discovery-index.lock"));
|
|
2644
|
+
try {
|
|
2645
|
+
const onDisk = await readFile6(path, "utf8").then((raw) => sanitize(JSON.parse(raw))).catch(() => emptyManifest());
|
|
2646
|
+
for (const [key2, entry] of Object.entries(mine.e)) {
|
|
2647
|
+
const other = onDisk.e[key2];
|
|
2648
|
+
if (!other || other.t <= entry.t) onDisk.e[key2] = entry;
|
|
2649
|
+
}
|
|
2650
|
+
evict(onDisk);
|
|
2651
|
+
const payload = JSON.stringify(onDisk);
|
|
2652
|
+
if (Buffer.byteLength(payload) > MAX_MANIFEST_BYTES) return;
|
|
2653
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
2654
|
+
try {
|
|
2655
|
+
await writeFile2(tmp, payload, { mode: 384 });
|
|
2656
|
+
await rename5(tmp, path);
|
|
2657
|
+
} finally {
|
|
2658
|
+
await unlink4(tmp).catch(() => {
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
loaded = onDisk;
|
|
2662
|
+
} finally {
|
|
2663
|
+
await release();
|
|
2664
|
+
}
|
|
2665
|
+
} catch {
|
|
2666
|
+
dirty = true;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
function clearDiscoveryCache() {
|
|
2670
|
+
loaded = null;
|
|
2671
|
+
loadedFrom = "";
|
|
2672
|
+
dirty = false;
|
|
2673
|
+
}
|
|
2674
|
+
var VERSION, MANIFEST, MAX_MANIFEST_BYTES, MAX_ENTRIES, MAX_AGE_MS, TOUCH_INTERVAL_MS, loaded, loadedFrom, dirty;
|
|
2675
|
+
var init_discovery_cache = __esm({
|
|
2676
|
+
"src/discovery-cache.ts"() {
|
|
2677
|
+
"use strict";
|
|
2678
|
+
init_cache_dir();
|
|
2679
|
+
init_file_lock();
|
|
2680
|
+
init_resource_budget();
|
|
2681
|
+
VERSION = 1;
|
|
2682
|
+
MANIFEST = "discovery-index.json";
|
|
2683
|
+
MAX_MANIFEST_BYTES = 16 * 1024 * 1024;
|
|
2684
|
+
MAX_ENTRIES = 2e4;
|
|
2685
|
+
MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2686
|
+
TOUCH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
2687
|
+
loaded = null;
|
|
2688
|
+
loadedFrom = "";
|
|
2689
|
+
dirty = false;
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2533
2693
|
// src/parsed-file-cache.ts
|
|
2534
2694
|
import { gzip, gunzip } from "zlib";
|
|
2535
2695
|
import { promisify } from "util";
|
|
2536
|
-
import { createHash as createHash2, randomUUID } from "crypto";
|
|
2537
|
-
import { mkdir as
|
|
2538
|
-
import { join as
|
|
2696
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
2697
|
+
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";
|
|
2698
|
+
import { join as join9 } from "path";
|
|
2539
2699
|
async function fingerprint(path) {
|
|
2540
2700
|
try {
|
|
2541
|
-
const s = await
|
|
2542
|
-
const wal = await
|
|
2701
|
+
const s = await stat5(path);
|
|
2702
|
+
const wal = await stat5(`${path}-wal`).catch(() => null);
|
|
2543
2703
|
return [s.dev, s.ino, s.size, s.mtimeMs, s.ctimeMs, wal?.size, wal?.mtimeMs].join(":");
|
|
2544
2704
|
} catch {
|
|
2545
2705
|
return null;
|
|
2546
2706
|
}
|
|
2547
2707
|
}
|
|
2548
|
-
async function cachedFileParse(path, kind, parse, warnings) {
|
|
2549
|
-
const dir =
|
|
2550
|
-
const
|
|
2551
|
-
const cachePath =
|
|
2708
|
+
async function cachedFileParse(path, kind, parse, warnings, range) {
|
|
2709
|
+
const dir = join9(getCacheDir(), "parsed-files");
|
|
2710
|
+
const key2 = createHash2("sha256").update(`${VERSION2}:${kind}:${path}:${getPricingFingerprint()}`).digest("hex");
|
|
2711
|
+
const cachePath = join9(dir, `v${VERSION2}-${key2}.json.gz`);
|
|
2552
2712
|
const fingerprintPath = kind === "opencode" ? path.slice(0, path.lastIndexOf(":")) : path;
|
|
2553
2713
|
const before = await fingerprint(fingerprintPath);
|
|
2714
|
+
const rangeKind = `usage-range:${VERSION2}:${kind}`;
|
|
2715
|
+
const bounds = range && before ? await readDiscoveryFact(rangeKind, path, before) : void 0;
|
|
2716
|
+
if (range && bounds && (bounds.last < range.start.getTime() || bounds.first > range.end.getTime())) {
|
|
2717
|
+
if (Array.isArray(bounds.warnings)) warnings?.push(...bounds.warnings);
|
|
2718
|
+
return [];
|
|
2719
|
+
}
|
|
2720
|
+
const rememberRange = async (data2) => {
|
|
2721
|
+
if (!range || !before || bounds || await fingerprint(fingerprintPath) !== before) return;
|
|
2722
|
+
let first = Infinity, last = -Infinity;
|
|
2723
|
+
for (const row of data2) {
|
|
2724
|
+
const timestamp2 = row?.timestamp;
|
|
2725
|
+
const value = typeof timestamp2 === "string" ? Date.parse(timestamp2) : NaN;
|
|
2726
|
+
if (!Number.isFinite(value)) return;
|
|
2727
|
+
first = Math.min(first, value);
|
|
2728
|
+
last = Math.max(last, value);
|
|
2729
|
+
}
|
|
2730
|
+
await rememberDiscoveryFact(rangeKind, path, before, { first: data2.length ? first : 0, last: data2.length ? last : -1, warnings });
|
|
2731
|
+
};
|
|
2554
2732
|
if (before) {
|
|
2555
2733
|
try {
|
|
2556
|
-
const s = await
|
|
2734
|
+
const s = await stat5(cachePath);
|
|
2557
2735
|
if (s.size <= MAX_ENTRY) {
|
|
2558
2736
|
chargeRead(s.size);
|
|
2559
|
-
const saved = JSON.parse((await decompress(await
|
|
2560
|
-
if (saved.v ===
|
|
2737
|
+
const saved = JSON.parse((await decompress(await readFile7(cachePath), { maxOutputLength: MAX_ENTRY })).toString("utf8"));
|
|
2738
|
+
if (saved.v === VERSION2 && saved.fingerprint === before && Array.isArray(saved.data)) {
|
|
2561
2739
|
if (Array.isArray(saved.warnings)) warnings?.push(...saved.warnings.filter((w) => typeof w === "string"));
|
|
2740
|
+
await rememberRange(saved.data);
|
|
2562
2741
|
return saved.data;
|
|
2563
2742
|
}
|
|
2564
2743
|
}
|
|
@@ -2568,65 +2747,77 @@ async function cachedFileParse(path, kind, parse, warnings) {
|
|
|
2568
2747
|
}
|
|
2569
2748
|
const data = await parse();
|
|
2570
2749
|
if (!before || await fingerprint(fingerprintPath) !== before) return data;
|
|
2571
|
-
|
|
2750
|
+
await rememberRange(data);
|
|
2751
|
+
const payload = JSON.stringify({ v: VERSION2, fingerprint: before, data, warnings });
|
|
2572
2752
|
if (Buffer.byteLength(payload) > MAX_ENTRY) return data;
|
|
2573
|
-
await
|
|
2753
|
+
await mkdir8(dir, { recursive: true });
|
|
2574
2754
|
const encoded = await compress(Buffer.from(payload));
|
|
2575
|
-
const release = await acquireFileLock(
|
|
2755
|
+
const release = await acquireFileLock(join9(dir, "writes.lock"));
|
|
2576
2756
|
try {
|
|
2577
|
-
const quotaPath =
|
|
2578
|
-
|
|
2757
|
+
const quotaPath = join9(dir, "quota");
|
|
2758
|
+
const quota = await readFile7(quotaPath, "utf8").catch(() => "");
|
|
2759
|
+
let total = /^\d+$/.test(quota) ? Number(quota) : NaN;
|
|
2760
|
+
const writeQuota = async (bytes) => {
|
|
2761
|
+
const tmp = `${quotaPath}.${randomUUID2()}.tmp`;
|
|
2762
|
+
try {
|
|
2763
|
+
await writeFile3(tmp, String(bytes), { mode: 384 });
|
|
2764
|
+
await rename6(tmp, quotaPath);
|
|
2765
|
+
} finally {
|
|
2766
|
+
await unlink5(tmp).catch(() => {
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
if (!Number.isSafeInteger(total) || total < 0 || total > MAX_DISK) {
|
|
2579
2771
|
const files = await readdir3(dir);
|
|
2580
2772
|
const rows2 = [];
|
|
2581
2773
|
for (const name of files) {
|
|
2582
2774
|
if (!name.endsWith(".json") && !name.endsWith(".json.gz") && !name.endsWith(".tmp")) continue;
|
|
2583
|
-
if (!name.startsWith(`v${
|
|
2584
|
-
await
|
|
2775
|
+
if (!name.startsWith(`v${VERSION2}-`)) {
|
|
2776
|
+
await unlink5(join9(dir, name)).catch(() => {
|
|
2585
2777
|
});
|
|
2586
2778
|
continue;
|
|
2587
2779
|
}
|
|
2588
|
-
const p =
|
|
2780
|
+
const p = join9(dir, name), s = await stat5(p).catch(() => null);
|
|
2589
2781
|
if (s) rows2.push({ path: p, size: s.size, mtime: s.mtimeMs });
|
|
2590
2782
|
}
|
|
2591
|
-
|
|
2783
|
+
total = rows2.reduce((n, r) => n + r.size, 0);
|
|
2592
2784
|
for (const row of rows2.sort((a, b) => a.mtime - b.mtime)) {
|
|
2593
2785
|
if (total <= MAX_DISK) break;
|
|
2594
|
-
await
|
|
2786
|
+
await unlink5(row.path).catch(() => {
|
|
2595
2787
|
});
|
|
2596
2788
|
total -= row.size;
|
|
2597
2789
|
}
|
|
2598
|
-
await
|
|
2599
|
-
maintainedDir = dir;
|
|
2790
|
+
await writeQuota(total);
|
|
2600
2791
|
}
|
|
2601
|
-
if (!(await
|
|
2602
|
-
const
|
|
2603
|
-
|
|
2604
|
-
let nextSize = total - oldSize + encoded.length;
|
|
2792
|
+
if (!(await readFile7(cachePath).catch(() => Buffer.alloc(0))).equals(encoded)) {
|
|
2793
|
+
const oldSize = (await stat5(cachePath).catch(() => null))?.size ?? 0;
|
|
2794
|
+
let nextSize = total + encoded.length;
|
|
2605
2795
|
if (!Number.isFinite(nextSize)) return data;
|
|
2606
2796
|
if (nextSize > MAX_DISK) {
|
|
2607
2797
|
const rows2 = [];
|
|
2608
2798
|
for (const name of await readdir3(dir)) {
|
|
2609
2799
|
if (!name.endsWith(".json.gz")) continue;
|
|
2610
|
-
const p =
|
|
2800
|
+
const p = join9(dir, name);
|
|
2611
2801
|
if (p === cachePath) continue;
|
|
2612
|
-
const s = await
|
|
2802
|
+
const s = await stat5(p).catch(() => null);
|
|
2613
2803
|
if (s) rows2.push({ path: p, size: s.size, mtime: s.mtimeMs });
|
|
2614
2804
|
}
|
|
2615
2805
|
for (const row of rows2.sort((a, b) => a.mtime - b.mtime)) {
|
|
2616
2806
|
if (nextSize <= MAX_DISK * 0.75) break;
|
|
2617
|
-
await
|
|
2807
|
+
await unlink5(row.path).catch(() => {
|
|
2618
2808
|
});
|
|
2619
2809
|
nextSize -= row.size;
|
|
2620
2810
|
}
|
|
2621
2811
|
if (nextSize > MAX_DISK) return data;
|
|
2622
2812
|
}
|
|
2623
|
-
const tmp = `${cachePath}.${
|
|
2813
|
+
const tmp = `${cachePath}.${randomUUID2()}.tmp`;
|
|
2624
2814
|
try {
|
|
2625
|
-
await
|
|
2626
|
-
await
|
|
2627
|
-
await
|
|
2815
|
+
await writeQuota(nextSize);
|
|
2816
|
+
await writeFile3(tmp, encoded, { mode: 384 });
|
|
2817
|
+
await rename6(tmp, cachePath);
|
|
2818
|
+
if (oldSize) await writeQuota(nextSize - oldSize);
|
|
2628
2819
|
} finally {
|
|
2629
|
-
await
|
|
2820
|
+
await unlink5(tmp).catch(() => {
|
|
2630
2821
|
});
|
|
2631
2822
|
}
|
|
2632
2823
|
}
|
|
@@ -2635,7 +2826,7 @@ async function cachedFileParse(path, kind, parse, warnings) {
|
|
|
2635
2826
|
}
|
|
2636
2827
|
return data;
|
|
2637
2828
|
}
|
|
2638
|
-
var
|
|
2829
|
+
var VERSION2, compress, decompress, MAX_ENTRY, MAX_DISK;
|
|
2639
2830
|
var init_parsed_file_cache = __esm({
|
|
2640
2831
|
"src/parsed-file-cache.ts"() {
|
|
2641
2832
|
"use strict";
|
|
@@ -2643,12 +2834,12 @@ var init_parsed_file_cache = __esm({
|
|
|
2643
2834
|
init_models();
|
|
2644
2835
|
init_resource_budget();
|
|
2645
2836
|
init_file_lock();
|
|
2646
|
-
|
|
2837
|
+
init_discovery_cache();
|
|
2838
|
+
VERSION2 = 3;
|
|
2647
2839
|
compress = promisify(gzip);
|
|
2648
2840
|
decompress = promisify(gunzip);
|
|
2649
2841
|
MAX_ENTRY = 16 * 1024 * 1024;
|
|
2650
2842
|
MAX_DISK = 256 * 1024 * 1024;
|
|
2651
|
-
maintainedDir = "";
|
|
2652
2843
|
}
|
|
2653
2844
|
});
|
|
2654
2845
|
|
|
@@ -2819,16 +3010,16 @@ var init_fs_utils = __esm({
|
|
|
2819
3010
|
});
|
|
2820
3011
|
|
|
2821
3012
|
// src/providers/claude.ts
|
|
2822
|
-
import { readdir as readdir4, stat as
|
|
2823
|
-
import { basename as basename2, join as
|
|
3013
|
+
import { readdir as readdir4, stat as stat6 } from "fs/promises";
|
|
3014
|
+
import { basename as basename2, join as join10, resolve as resolve2 } from "path";
|
|
2824
3015
|
import { homedir as homedir4 } from "os";
|
|
2825
3016
|
function getClaudeDir() {
|
|
2826
|
-
return process.env["CLAUDE_CONFIG_DIR"] ||
|
|
3017
|
+
return process.env["CLAUDE_CONFIG_DIR"] || join10(homedir4(), ".claude");
|
|
2827
3018
|
}
|
|
2828
3019
|
function getDesktopSessionsDir() {
|
|
2829
|
-
if (process.platform === "darwin") return
|
|
2830
|
-
if (process.platform === "win32") return
|
|
2831
|
-
return
|
|
3020
|
+
if (process.platform === "darwin") return join10(homedir4(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
|
|
3021
|
+
if (process.platform === "win32") return join10(homedir4(), "AppData", "Roaming", "Claude", "local-agent-mode-sessions");
|
|
3022
|
+
return join10(homedir4(), ".config", "Claude", "local-agent-mode-sessions");
|
|
2832
3023
|
}
|
|
2833
3024
|
async function findDesktopProjectDirs(base) {
|
|
2834
3025
|
const results = [];
|
|
@@ -2837,14 +3028,14 @@ async function findDesktopProjectDirs(base) {
|
|
|
2837
3028
|
const entries = await readdir4(dir).catch(() => []);
|
|
2838
3029
|
for (const entry of entries) {
|
|
2839
3030
|
if (entry === "node_modules" || entry === ".git") continue;
|
|
2840
|
-
const full =
|
|
2841
|
-
const s = await
|
|
3031
|
+
const full = join10(dir, entry);
|
|
3032
|
+
const s = await stat6(full).catch(() => null);
|
|
2842
3033
|
if (!s?.isDirectory()) continue;
|
|
2843
3034
|
if (entry === "projects") {
|
|
2844
3035
|
const projectDirs = await readdir4(full).catch(() => []);
|
|
2845
3036
|
for (const pd of projectDirs) {
|
|
2846
|
-
const pdFull =
|
|
2847
|
-
const pdStat = await
|
|
3037
|
+
const pdFull = join10(full, pd);
|
|
3038
|
+
const pdStat = await stat6(pdFull).catch(() => null);
|
|
2848
3039
|
if (pdStat?.isDirectory()) results.push(pdFull);
|
|
2849
3040
|
}
|
|
2850
3041
|
} else {
|
|
@@ -2884,8 +3075,8 @@ var init_claude = __esm({
|
|
|
2884
3075
|
displayName: "Claude",
|
|
2885
3076
|
modelDisplayName(model) {
|
|
2886
3077
|
const canonical = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
2887
|
-
for (const [
|
|
2888
|
-
if (canonical.startsWith(
|
|
3078
|
+
for (const [key2, name] of Object.entries(shortNames)) {
|
|
3079
|
+
if (canonical.startsWith(key2)) return name;
|
|
2889
3080
|
}
|
|
2890
3081
|
return canonical;
|
|
2891
3082
|
},
|
|
@@ -2897,12 +3088,12 @@ var init_claude = __esm({
|
|
|
2897
3088
|
const config = await readConfig();
|
|
2898
3089
|
const roots = /* @__PURE__ */ new Set([getClaudeDir(), ...config.claudeConfigDirs ?? []]);
|
|
2899
3090
|
for (const root of roots) {
|
|
2900
|
-
const projectsDir =
|
|
3091
|
+
const projectsDir = join10(root.startsWith("~/") ? join10(homedir4(), root.slice(2)) : resolve2(root), "projects");
|
|
2901
3092
|
try {
|
|
2902
3093
|
const entries = await readdir4(projectsDir);
|
|
2903
3094
|
for (const dirName of entries) {
|
|
2904
|
-
const dirPath =
|
|
2905
|
-
const dirStat = await
|
|
3095
|
+
const dirPath = join10(projectsDir, dirName);
|
|
3096
|
+
const dirStat = await stat6(dirPath).catch(() => null);
|
|
2906
3097
|
if (dirStat?.isDirectory()) {
|
|
2907
3098
|
sources.push({ path: dirPath, project: dirName, provider: "claude" });
|
|
2908
3099
|
}
|
|
@@ -2926,137 +3117,6 @@ var init_claude = __esm({
|
|
|
2926
3117
|
}
|
|
2927
3118
|
});
|
|
2928
3119
|
|
|
2929
|
-
// src/discovery-cache.ts
|
|
2930
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2931
|
-
import { mkdir as mkdir8, readFile as readFile7, rename as rename6, stat as stat6, unlink as unlink5, writeFile as writeFile3 } from "fs/promises";
|
|
2932
|
-
import { join as join10 } from "path";
|
|
2933
|
-
function manifestPath() {
|
|
2934
|
-
return join10(getCacheDir(), MANIFEST);
|
|
2935
|
-
}
|
|
2936
|
-
function fingerprintOf(s) {
|
|
2937
|
-
return [s.dev, s.ino, s.size, s.mtimeMs, s.ctimeMs].join(":");
|
|
2938
|
-
}
|
|
2939
|
-
function emptyManifest() {
|
|
2940
|
-
return { v: VERSION2, e: /* @__PURE__ */ Object.create(null) };
|
|
2941
|
-
}
|
|
2942
|
-
function sanitize(raw) {
|
|
2943
|
-
if (!raw || typeof raw !== "object") return emptyManifest();
|
|
2944
|
-
const parsed = raw;
|
|
2945
|
-
if (parsed.v !== VERSION2 || !parsed.e || typeof parsed.e !== "object") return emptyManifest();
|
|
2946
|
-
const out = emptyManifest();
|
|
2947
|
-
for (const [key, value] of Object.entries(parsed.e)) {
|
|
2948
|
-
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
2949
|
-
if (!value || typeof value !== "object") continue;
|
|
2950
|
-
const entry = value;
|
|
2951
|
-
if (typeof entry.f !== "string" || typeof entry.t !== "number") continue;
|
|
2952
|
-
out.e[key] = { f: entry.f, r: entry.r ?? null, t: entry.t };
|
|
2953
|
-
}
|
|
2954
|
-
return out;
|
|
2955
|
-
}
|
|
2956
|
-
async function load() {
|
|
2957
|
-
const path = manifestPath();
|
|
2958
|
-
if (loaded && loadedFrom === path) return loaded;
|
|
2959
|
-
loaded = emptyManifest();
|
|
2960
|
-
loadedFrom = path;
|
|
2961
|
-
dirty = false;
|
|
2962
|
-
try {
|
|
2963
|
-
const s = await stat6(path);
|
|
2964
|
-
if (s.size > MAX_MANIFEST_BYTES) return loaded;
|
|
2965
|
-
chargeRead(s.size);
|
|
2966
|
-
loaded = sanitize(JSON.parse(await readFile7(path, "utf8")));
|
|
2967
|
-
} catch (err) {
|
|
2968
|
-
if (err instanceof ResourceBudgetError) throw err;
|
|
2969
|
-
}
|
|
2970
|
-
return loaded;
|
|
2971
|
-
}
|
|
2972
|
-
function evict(manifest) {
|
|
2973
|
-
const cutoff = Date.now() - MAX_AGE_MS;
|
|
2974
|
-
for (const [key, entry] of Object.entries(manifest.e)) {
|
|
2975
|
-
if (entry.t < cutoff) delete manifest.e[key];
|
|
2976
|
-
}
|
|
2977
|
-
const keys = Object.keys(manifest.e);
|
|
2978
|
-
if (keys.length <= MAX_ENTRIES) return;
|
|
2979
|
-
keys.sort((a, b) => manifest.e[a].t - manifest.e[b].t);
|
|
2980
|
-
for (const key of keys.slice(0, keys.length - MAX_ENTRIES)) delete manifest.e[key];
|
|
2981
|
-
}
|
|
2982
|
-
async function cachedDiscoveryHeader(kind, path, stats, compute) {
|
|
2983
|
-
const manifest = await load();
|
|
2984
|
-
const key = `${kind}\0${path}`;
|
|
2985
|
-
const fingerprint2 = fingerprintOf(stats);
|
|
2986
|
-
const hit = manifest.e[key];
|
|
2987
|
-
if (hit && hit.f === fingerprint2) {
|
|
2988
|
-
const now = Date.now();
|
|
2989
|
-
if (now - hit.t > TOUCH_INTERVAL_MS) {
|
|
2990
|
-
hit.t = now;
|
|
2991
|
-
dirty = true;
|
|
2992
|
-
}
|
|
2993
|
-
return hit.r;
|
|
2994
|
-
}
|
|
2995
|
-
const value = await compute();
|
|
2996
|
-
const after = await stat6(path).catch(() => null);
|
|
2997
|
-
if (!after || fingerprintOf(after) !== fingerprint2) return value;
|
|
2998
|
-
manifest.e[key] = { f: fingerprint2, r: value ?? null, t: Date.now() };
|
|
2999
|
-
dirty = true;
|
|
3000
|
-
return value;
|
|
3001
|
-
}
|
|
3002
|
-
async function flushDiscoveryCache() {
|
|
3003
|
-
if (!dirty || !loaded) return;
|
|
3004
|
-
const mine = loaded;
|
|
3005
|
-
dirty = false;
|
|
3006
|
-
const path = manifestPath();
|
|
3007
|
-
const dir = getCacheDir();
|
|
3008
|
-
try {
|
|
3009
|
-
await mkdir8(dir, { recursive: true });
|
|
3010
|
-
const release = await acquireFileLock(join10(dir, "discovery-index.lock"));
|
|
3011
|
-
try {
|
|
3012
|
-
const onDisk = await readFile7(path, "utf8").then((raw) => sanitize(JSON.parse(raw))).catch(() => emptyManifest());
|
|
3013
|
-
for (const [key, entry] of Object.entries(mine.e)) {
|
|
3014
|
-
const other = onDisk.e[key];
|
|
3015
|
-
if (!other || other.t <= entry.t) onDisk.e[key] = entry;
|
|
3016
|
-
}
|
|
3017
|
-
evict(onDisk);
|
|
3018
|
-
const payload = JSON.stringify(onDisk);
|
|
3019
|
-
if (Buffer.byteLength(payload) > MAX_MANIFEST_BYTES) return;
|
|
3020
|
-
const tmp = `${path}.${randomUUID2()}.tmp`;
|
|
3021
|
-
try {
|
|
3022
|
-
await writeFile3(tmp, payload, { mode: 384 });
|
|
3023
|
-
await rename6(tmp, path);
|
|
3024
|
-
} finally {
|
|
3025
|
-
await unlink5(tmp).catch(() => {
|
|
3026
|
-
});
|
|
3027
|
-
}
|
|
3028
|
-
loaded = onDisk;
|
|
3029
|
-
} finally {
|
|
3030
|
-
await release();
|
|
3031
|
-
}
|
|
3032
|
-
} catch {
|
|
3033
|
-
dirty = true;
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
function clearDiscoveryCache() {
|
|
3037
|
-
loaded = null;
|
|
3038
|
-
loadedFrom = "";
|
|
3039
|
-
dirty = false;
|
|
3040
|
-
}
|
|
3041
|
-
var VERSION2, MANIFEST, MAX_MANIFEST_BYTES, MAX_ENTRIES, MAX_AGE_MS, TOUCH_INTERVAL_MS, loaded, loadedFrom, dirty;
|
|
3042
|
-
var init_discovery_cache = __esm({
|
|
3043
|
-
"src/discovery-cache.ts"() {
|
|
3044
|
-
"use strict";
|
|
3045
|
-
init_cache_dir();
|
|
3046
|
-
init_file_lock();
|
|
3047
|
-
init_resource_budget();
|
|
3048
|
-
VERSION2 = 1;
|
|
3049
|
-
MANIFEST = "discovery-index.json";
|
|
3050
|
-
MAX_MANIFEST_BYTES = 16 * 1024 * 1024;
|
|
3051
|
-
MAX_ENTRIES = 2e4;
|
|
3052
|
-
MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3053
|
-
TOUCH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
3054
|
-
loaded = null;
|
|
3055
|
-
loadedFrom = "";
|
|
3056
|
-
dirty = false;
|
|
3057
|
-
}
|
|
3058
|
-
});
|
|
3059
|
-
|
|
3060
3120
|
// src/providers/codex.ts
|
|
3061
3121
|
import { readdir as readdir5, stat as stat7 } from "fs/promises";
|
|
3062
3122
|
import { basename as basename3, join as join11 } from "path";
|
|
@@ -3224,8 +3284,8 @@ function createParser(source, seenKeys) {
|
|
|
3224
3284
|
if (totalTokens === 0) continue;
|
|
3225
3285
|
const uncachedInputTokens = Math.max(0, inputTokens - cachedInputTokens - cacheWriteTokens);
|
|
3226
3286
|
const model = resolveModel(entry.payload, sessionModel);
|
|
3227
|
-
const
|
|
3228
|
-
const dedupKey = `codex:${sessionId}:${
|
|
3287
|
+
const timestamp2 = entry.timestamp ?? "";
|
|
3288
|
+
const dedupKey = `codex:${sessionId}:${timestamp2}:${cumulativeTotal}`;
|
|
3229
3289
|
if (seenKeys.has(dedupKey)) continue;
|
|
3230
3290
|
seenKeys.add(dedupKey);
|
|
3231
3291
|
const totalPromptInputTokens = inputTokens;
|
|
@@ -3253,7 +3313,7 @@ function createParser(source, seenKeys) {
|
|
|
3253
3313
|
costUSD,
|
|
3254
3314
|
tools: pendingTools,
|
|
3255
3315
|
bashCommands: [],
|
|
3256
|
-
timestamp,
|
|
3316
|
+
timestamp: timestamp2,
|
|
3257
3317
|
speed: sessionSpeed,
|
|
3258
3318
|
deduplicationKey: dedupKey,
|
|
3259
3319
|
userMessage: pendingUserMessage,
|
|
@@ -3278,8 +3338,8 @@ function createCodexProvider(codexDir) {
|
|
|
3278
3338
|
name: "codex",
|
|
3279
3339
|
displayName: "Codex",
|
|
3280
3340
|
modelDisplayName(model) {
|
|
3281
|
-
for (const [
|
|
3282
|
-
if (model.startsWith(
|
|
3341
|
+
for (const [key2, name] of modelDisplayEntries) {
|
|
3342
|
+
if (model.startsWith(key2)) return name;
|
|
3283
3343
|
}
|
|
3284
3344
|
return model;
|
|
3285
3345
|
},
|
|
@@ -3430,8 +3490,8 @@ function createCopilotProvider(sessionStateDir) {
|
|
|
3430
3490
|
name: "copilot",
|
|
3431
3491
|
displayName: "Copilot",
|
|
3432
3492
|
modelDisplayName(model) {
|
|
3433
|
-
for (const [
|
|
3434
|
-
if (model ===
|
|
3493
|
+
for (const [key2, name] of modelDisplayEntries2) {
|
|
3494
|
+
if (model === key2 || model.startsWith(key2 + "-")) return name;
|
|
3435
3495
|
}
|
|
3436
3496
|
return model;
|
|
3437
3497
|
},
|
|
@@ -3619,7 +3679,7 @@ function createParser3(source, seenKeys) {
|
|
|
3619
3679
|
return typeof cmd === "string" ? extractBashCommands(cmd) : [];
|
|
3620
3680
|
});
|
|
3621
3681
|
const costUSD = calculateCost(model, input, output, cacheWrite, cacheRead, 0, "standard", input + cacheWrite + cacheRead);
|
|
3622
|
-
const
|
|
3682
|
+
const timestamp2 = entry.timestamp ?? "";
|
|
3623
3683
|
yield {
|
|
3624
3684
|
provider: source.provider,
|
|
3625
3685
|
model,
|
|
@@ -3633,7 +3693,7 @@ function createParser3(source, seenKeys) {
|
|
|
3633
3693
|
costUSD,
|
|
3634
3694
|
tools,
|
|
3635
3695
|
bashCommands,
|
|
3636
|
-
timestamp,
|
|
3696
|
+
timestamp: timestamp2,
|
|
3637
3697
|
speed: "standard",
|
|
3638
3698
|
deduplicationKey: dedupKey,
|
|
3639
3699
|
userMessage: pendingUserMessage,
|
|
@@ -3650,8 +3710,8 @@ function createPiProvider(sessionsDir) {
|
|
|
3650
3710
|
name: "pi",
|
|
3651
3711
|
displayName: "Pi",
|
|
3652
3712
|
modelDisplayName(model) {
|
|
3653
|
-
for (const [
|
|
3654
|
-
if (model.startsWith(
|
|
3713
|
+
for (const [key2, name] of modelDisplayEntries3) {
|
|
3714
|
+
if (model.startsWith(key2)) return name;
|
|
3655
3715
|
}
|
|
3656
3716
|
return model;
|
|
3657
3717
|
},
|
|
@@ -3672,8 +3732,8 @@ function createOmpProvider(sessionsDir) {
|
|
|
3672
3732
|
name: "omp",
|
|
3673
3733
|
displayName: "OMP",
|
|
3674
3734
|
modelDisplayName(model) {
|
|
3675
|
-
for (const [
|
|
3676
|
-
if (model.startsWith(
|
|
3735
|
+
for (const [key2, name] of modelDisplayEntries3) {
|
|
3736
|
+
if (model.startsWith(key2)) return name;
|
|
3677
3737
|
}
|
|
3678
3738
|
return model;
|
|
3679
3739
|
},
|
|
@@ -3722,37 +3782,168 @@ var init_pi = __esm({
|
|
|
3722
3782
|
}
|
|
3723
3783
|
});
|
|
3724
3784
|
|
|
3725
|
-
// src/
|
|
3726
|
-
import {
|
|
3727
|
-
import {
|
|
3728
|
-
import { join as join14 } from "path";
|
|
3729
|
-
import {
|
|
3730
|
-
function
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3785
|
+
// src/providers/kimi.ts
|
|
3786
|
+
import { createHash as createHash3 } from "crypto";
|
|
3787
|
+
import { readdir as readdir8, readFile as readFile8, stat as stat10 } from "fs/promises";
|
|
3788
|
+
import { basename as basename7, dirname as dirname4, join as join14 } from "path";
|
|
3789
|
+
import { homedir as homedir8 } from "os";
|
|
3790
|
+
function createKimiProvider(home) {
|
|
3791
|
+
const root = () => home ?? process.env["KIMI_CODE_HOME"] ?? join14(homedir8(), ".kimi-code");
|
|
3792
|
+
return {
|
|
3793
|
+
name: "kimi",
|
|
3794
|
+
displayName: "Kimi",
|
|
3795
|
+
modelDisplayName: (model) => model === "kimi-k3" ? "Kimi K3" : model,
|
|
3796
|
+
toolDisplayName: (tool) => tool,
|
|
3797
|
+
async discoverSessions() {
|
|
3798
|
+
const projects = /* @__PURE__ */ new Map();
|
|
3799
|
+
const catalog = join14(root(), "workspaces.json");
|
|
3800
|
+
try {
|
|
3801
|
+
const size = (await stat10(catalog)).size;
|
|
3802
|
+
if (size <= 2 * 1024 * 1024) {
|
|
3803
|
+
chargeRead(size);
|
|
3804
|
+
const data = JSON.parse(await readFile8(catalog, "utf8"));
|
|
3805
|
+
for (const [id, entry] of Object.entries(data.workspaces ?? {})) {
|
|
3806
|
+
if (entry && typeof entry === "object" && "root" in entry && typeof entry.root === "string") {
|
|
3807
|
+
projects.set(id, basename7(entry.root));
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
} catch (err) {
|
|
3812
|
+
if (err instanceof ResourceBudgetError) throw err;
|
|
3813
|
+
}
|
|
3814
|
+
const sources = [];
|
|
3815
|
+
async function walk(dir, project, depth) {
|
|
3816
|
+
if (depth > 8) return;
|
|
3817
|
+
chargeRead(0);
|
|
3818
|
+
const entries = await readdir8(dir, { withFileTypes: true }).catch(() => []);
|
|
3819
|
+
for (const entry of entries) {
|
|
3820
|
+
if (entry.name.startsWith(".")) continue;
|
|
3821
|
+
const path = join14(dir, entry.name);
|
|
3822
|
+
if (entry.isDirectory()) await walk(path, project, depth + 1);
|
|
3823
|
+
else if (entry.isFile() && entry.name === "wire.jsonl") sources.push({ path, project, provider: "kimi" });
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
const sessions = join14(root(), "sessions");
|
|
3827
|
+
for (const entry of await readdir8(sessions, { withFileTypes: true }).catch(() => [])) {
|
|
3828
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
3829
|
+
const project = projects.get(entry.name) ?? entry.name.replace(/^wd_/, "").replace(/_[a-f\d]{12}$/, "");
|
|
3830
|
+
await walk(join14(sessions, entry.name), project, 0);
|
|
3831
|
+
}
|
|
3832
|
+
return sources.sort((a, b) => a.path.localeCompare(b.path));
|
|
3833
|
+
},
|
|
3834
|
+
createSessionParser(source, seenKeys) {
|
|
3835
|
+
const warnings = [];
|
|
3836
|
+
return {
|
|
3837
|
+
warnings,
|
|
3838
|
+
async *parse() {
|
|
3839
|
+
const sessionDir = source.path.split("/").reverse().find((part) => part.startsWith("session_")) ?? basename7(dirname4(source.path));
|
|
3840
|
+
const agent = basename7(dirname4(source.path));
|
|
3841
|
+
for await (const line of readSessionLines(source.path)) {
|
|
3842
|
+
if (!/"type"\s*:\s*"usage\.record"/.test(line.slice(0, 256))) continue;
|
|
3843
|
+
let data;
|
|
3844
|
+
try {
|
|
3845
|
+
data = JSON.parse(line);
|
|
3846
|
+
} catch {
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
if (data.type !== "usage.record" || !data.usage || typeof data.usage !== "object") continue;
|
|
3850
|
+
const usage = data.usage;
|
|
3851
|
+
const fields2 = ["inputOther", "output", "inputCacheRead", "inputCacheCreation"];
|
|
3852
|
+
const values = fields2.map((field) => usage[field] ?? 0);
|
|
3853
|
+
if (values.some((value) => typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)) {
|
|
3854
|
+
if (!warnings.length) warnings.push("Kimi Code has invalid token counts; those records are excluded.");
|
|
3855
|
+
continue;
|
|
3856
|
+
}
|
|
3857
|
+
const [input, output, read, write] = values;
|
|
3858
|
+
if (input + output + read + write === 0) continue;
|
|
3859
|
+
if (typeof data.time !== "number" || !Number.isFinite(new Date(data.time).getTime()) || typeof data.model !== "string") {
|
|
3860
|
+
if (!warnings.length) warnings.push("Kimi Code has usage without a valid timestamp or model; those records are excluded.");
|
|
3861
|
+
continue;
|
|
3862
|
+
}
|
|
3863
|
+
const rawModel = data.model.replace(/^kimi-code\//, "");
|
|
3864
|
+
const model = ["k3", "k3-256k"].includes(rawModel) ? "kimi-k3" : rawModel;
|
|
3865
|
+
if (!getModelCosts(model)) {
|
|
3866
|
+
const warning = `No API pricing for model "${model}"; its tokens are counted but its cost is excluded. Configure a model alias only when the underlying API model is known.`;
|
|
3867
|
+
if (!warnings.includes(warning)) warnings.push(warning);
|
|
3868
|
+
}
|
|
3869
|
+
const key2 = "kimi:" + createHash3("sha256").update(JSON.stringify([
|
|
3870
|
+
data.agentId ?? agent,
|
|
3871
|
+
data.time,
|
|
3872
|
+
data.model,
|
|
3873
|
+
input,
|
|
3874
|
+
output,
|
|
3875
|
+
read,
|
|
3876
|
+
write
|
|
3877
|
+
])).digest("hex");
|
|
3878
|
+
if (seenKeys.has(key2)) continue;
|
|
3879
|
+
seenKeys.add(key2);
|
|
3880
|
+
yield {
|
|
3881
|
+
provider: "kimi",
|
|
3882
|
+
model,
|
|
3883
|
+
inputTokens: input,
|
|
3884
|
+
outputTokens: output,
|
|
3885
|
+
cacheCreationInputTokens: write,
|
|
3886
|
+
cacheReadInputTokens: read,
|
|
3887
|
+
cachedInputTokens: read,
|
|
3888
|
+
reasoningTokens: 0,
|
|
3889
|
+
webSearchRequests: 0,
|
|
3890
|
+
costUSD: calculateCost(model, input, output, write, read, 0, "standard", input + read + write),
|
|
3891
|
+
tools: [],
|
|
3892
|
+
bashCommands: [],
|
|
3893
|
+
timestamp: new Date(data.time).toISOString(),
|
|
3894
|
+
speed: "standard",
|
|
3895
|
+
deduplicationKey: key2,
|
|
3896
|
+
userMessage: "",
|
|
3897
|
+
sessionId: `${sessionDir}/${agent}`
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
var kimi;
|
|
3906
|
+
var init_kimi = __esm({
|
|
3907
|
+
"src/providers/kimi.ts"() {
|
|
3908
|
+
"use strict";
|
|
3909
|
+
init_fs_utils();
|
|
3910
|
+
init_models();
|
|
3911
|
+
init_resource_budget();
|
|
3912
|
+
kimi = createKimiProvider();
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
|
|
3916
|
+
// src/cursor-cache.ts
|
|
3917
|
+
import { readFile as readFile9, mkdir as mkdir9, stat as stat11, rename as rename7 } from "fs/promises";
|
|
3918
|
+
import { open as open6 } from "fs/promises";
|
|
3919
|
+
import { join as join15 } from "path";
|
|
3920
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
3921
|
+
function getCachePath2() {
|
|
3922
|
+
return join15(getCacheDir(), CACHE_FILE);
|
|
3923
|
+
}
|
|
3924
|
+
async function getDbFingerprint(dbPath) {
|
|
3925
|
+
try {
|
|
3926
|
+
const s = await stat11(dbPath);
|
|
3927
|
+
const wal = await stat11(`${dbPath}-wal`).catch(() => null);
|
|
3928
|
+
return [dbPath, s.dev, s.ino, s.ctimeMs, s.mtimeMs, s.size, wal?.ino, wal?.ctimeMs, wal?.mtimeMs, wal?.size, getPricingFingerprint()].join(":");
|
|
3929
|
+
} catch {
|
|
3930
|
+
return null;
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
async function readCachedResults(dbPath) {
|
|
3934
|
+
try {
|
|
3935
|
+
const fp = await getDbFingerprint(dbPath);
|
|
3936
|
+
if (!fp) return null;
|
|
3937
|
+
const size = (await stat11(getCachePath2())).size;
|
|
3938
|
+
if (size > 16 * 1024 * 1024) return null;
|
|
3939
|
+
chargeRead(size);
|
|
3940
|
+
const raw = await readFile9(getCachePath2(), "utf-8");
|
|
3941
|
+
const cache = JSON.parse(raw);
|
|
3942
|
+
if (cache.fingerprint === fp) {
|
|
3943
|
+
return cache.calls;
|
|
3944
|
+
}
|
|
3945
|
+
return null;
|
|
3946
|
+
} catch (err) {
|
|
3756
3947
|
if (err instanceof ResourceBudgetError) throw err;
|
|
3757
3948
|
return null;
|
|
3758
3949
|
}
|
|
@@ -3877,16 +4068,16 @@ __export(cursor_exports, {
|
|
|
3877
4068
|
cursor: () => cursor
|
|
3878
4069
|
});
|
|
3879
4070
|
import { existsSync } from "fs";
|
|
3880
|
-
import { join as
|
|
3881
|
-
import { homedir as
|
|
4071
|
+
import { join as join16 } from "path";
|
|
4072
|
+
import { homedir as homedir9 } from "os";
|
|
3882
4073
|
function getCursorDbPath() {
|
|
3883
4074
|
if (process.platform === "darwin") {
|
|
3884
|
-
return
|
|
4075
|
+
return join16(homedir9(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
3885
4076
|
}
|
|
3886
4077
|
if (process.platform === "win32") {
|
|
3887
|
-
return
|
|
4078
|
+
return join16(homedir9(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
3888
4079
|
}
|
|
3889
|
-
return
|
|
4080
|
+
return join16(homedir9(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
3890
4081
|
}
|
|
3891
4082
|
function extractLanguages(codeBlocksJson) {
|
|
3892
4083
|
if (!codeBlocksJson) return [];
|
|
@@ -3965,7 +4156,7 @@ function parseBubbles(db, seenKeys) {
|
|
|
3965
4156
|
const pricingModel = resolveModel2(row.model);
|
|
3966
4157
|
const displayModel = modelForDisplay(row.model);
|
|
3967
4158
|
const costUSD = calculateCost(pricingModel, inputTokens, outputTokens, 0, 0, 0);
|
|
3968
|
-
const
|
|
4159
|
+
const timestamp2 = createdAt || "";
|
|
3969
4160
|
const convMessages = userMessages.get(conversationId) ?? [];
|
|
3970
4161
|
const userQuestion = convMessages.length > 0 ? convMessages.shift() : "";
|
|
3971
4162
|
const assistantText = row.user_text ?? "";
|
|
@@ -3986,7 +4177,7 @@ function parseBubbles(db, seenKeys) {
|
|
|
3986
4177
|
costUSD,
|
|
3987
4178
|
tools: cursorTools,
|
|
3988
4179
|
bashCommands: [],
|
|
3989
|
-
timestamp,
|
|
4180
|
+
timestamp: timestamp2,
|
|
3990
4181
|
speed: "standard",
|
|
3991
4182
|
deduplicationKey: dedupKey,
|
|
3992
4183
|
userMessage: userText,
|
|
@@ -4125,27 +4316,31 @@ var init_cursor = __esm({
|
|
|
4125
4316
|
var opencode_exports = {};
|
|
4126
4317
|
__export(opencode_exports, {
|
|
4127
4318
|
createOpenCodeProvider: () => createOpenCodeProvider,
|
|
4319
|
+
getOpenCodeDbPaths: () => getOpenCodeDbPaths,
|
|
4128
4320
|
opencode: () => opencode
|
|
4129
4321
|
});
|
|
4130
|
-
import { readdir as
|
|
4131
|
-
import { join as
|
|
4132
|
-
import { homedir as
|
|
4322
|
+
import { readdir as readdir9 } from "fs/promises";
|
|
4323
|
+
import { join as join17 } from "path";
|
|
4324
|
+
import { homedir as homedir10 } from "os";
|
|
4133
4325
|
function sanitize2(dir) {
|
|
4134
4326
|
return dir.replace(/^\//, "").replace(/\//g, "-");
|
|
4135
4327
|
}
|
|
4136
4328
|
function getDataDir(dataDir) {
|
|
4137
|
-
const base = dataDir ?? process.env["XDG_DATA_HOME"] ??
|
|
4138
|
-
return
|
|
4329
|
+
const base = dataDir ?? process.env["XDG_DATA_HOME"] ?? join17(homedir10(), ".local", "share");
|
|
4330
|
+
return join17(base, "opencode");
|
|
4139
4331
|
}
|
|
4140
4332
|
async function findDbFiles(dir) {
|
|
4141
4333
|
try {
|
|
4142
|
-
const entries = await
|
|
4143
|
-
return entries.filter((f) => f.startsWith("opencode") && f.endsWith(".db")).map((f) =>
|
|
4334
|
+
const entries = await readdir9(dir);
|
|
4335
|
+
return entries.filter((f) => f.startsWith("opencode") && f.endsWith(".db")).map((f) => join17(dir, f));
|
|
4144
4336
|
} catch (error) {
|
|
4145
4337
|
if (error instanceof ResourceBudgetError) throw error;
|
|
4146
4338
|
return [];
|
|
4147
4339
|
}
|
|
4148
4340
|
}
|
|
4341
|
+
function getOpenCodeDbPaths() {
|
|
4342
|
+
return findDbFiles(getDataDir());
|
|
4343
|
+
}
|
|
4149
4344
|
function parseTimestamp(raw) {
|
|
4150
4345
|
const ms = raw < 1e12 ? raw * 1e3 : raw;
|
|
4151
4346
|
return new Date(ms).toISOString();
|
|
@@ -4366,20 +4561,20 @@ __export(cursor_agent_exports, {
|
|
|
4366
4561
|
createCursorAgentProvider: () => createCursorAgentProvider,
|
|
4367
4562
|
cursor_agent: () => cursor_agent
|
|
4368
4563
|
});
|
|
4369
|
-
import { createHash as
|
|
4564
|
+
import { createHash as createHash4 } from "crypto";
|
|
4370
4565
|
import { existsSync as existsSync2 } from "fs";
|
|
4371
|
-
import { readdir as
|
|
4372
|
-
import { join as
|
|
4373
|
-
import { homedir as
|
|
4566
|
+
import { readdir as readdir10, stat as stat12 } from "fs/promises";
|
|
4567
|
+
import { join as join18, basename as basename8 } from "path";
|
|
4568
|
+
import { homedir as homedir11 } from "os";
|
|
4374
4569
|
function getCursorAgentBaseDir(baseDirOverride) {
|
|
4375
4570
|
if (baseDirOverride) return baseDirOverride;
|
|
4376
|
-
return
|
|
4571
|
+
return join18(homedir11(), ".cursor");
|
|
4377
4572
|
}
|
|
4378
4573
|
function getProjectsDir(baseDir) {
|
|
4379
|
-
return
|
|
4574
|
+
return join18(baseDir, "projects");
|
|
4380
4575
|
}
|
|
4381
4576
|
function getAttributionDbPath(baseDir) {
|
|
4382
|
-
return
|
|
4577
|
+
return join18(baseDir, "ai-tracking", "ai-code-tracking.db");
|
|
4383
4578
|
}
|
|
4384
4579
|
function estimateTokens(charCount) {
|
|
4385
4580
|
if (charCount <= 0) return 0;
|
|
@@ -4428,9 +4623,9 @@ function resolveModel3(raw) {
|
|
|
4428
4623
|
return raw;
|
|
4429
4624
|
}
|
|
4430
4625
|
function toConversationId(transcriptPath) {
|
|
4431
|
-
const filename =
|
|
4626
|
+
const filename = basename8(transcriptPath, ".txt");
|
|
4432
4627
|
if (filename.length === 36 && UUID_LIKE.test(filename)) return filename;
|
|
4433
|
-
return
|
|
4628
|
+
return createHash4("sha1").update(transcriptPath).digest("hex").slice(0, 16);
|
|
4434
4629
|
}
|
|
4435
4630
|
function extractUserQuery(userBlock) {
|
|
4436
4631
|
const chunks = [];
|
|
@@ -4563,14 +4758,14 @@ function createParser6(source, seenKeys, dbPath, summariesByConversationId) {
|
|
|
4563
4758
|
if (transcript === null) return;
|
|
4564
4759
|
const parsed = parseTranscript(transcript);
|
|
4565
4760
|
if (!parsed.recognized) {
|
|
4566
|
-
process.stderr.write(`exe-watcher: skipped ${
|
|
4761
|
+
process.stderr.write(`exe-watcher: skipped ${basename8(source.path)}: unrecognized cursor-agent transcript format
|
|
4567
4762
|
`);
|
|
4568
4763
|
return;
|
|
4569
4764
|
}
|
|
4570
|
-
let
|
|
4571
|
-
if (!
|
|
4572
|
-
const fileStat = await
|
|
4573
|
-
|
|
4765
|
+
let timestamp2 = summary?.updatedAt ?? null;
|
|
4766
|
+
if (!timestamp2) {
|
|
4767
|
+
const fileStat = await stat12(source.path);
|
|
4768
|
+
timestamp2 = fileStat.mtime.toISOString();
|
|
4574
4769
|
}
|
|
4575
4770
|
const model = resolveModel3(summary?.model ?? null);
|
|
4576
4771
|
for (let turnIndex = 0; turnIndex < parsed.turns.length; turnIndex++) {
|
|
@@ -4602,7 +4797,7 @@ function createParser6(source, seenKeys, dbPath, summariesByConversationId) {
|
|
|
4602
4797
|
costUSD,
|
|
4603
4798
|
tools: turn.assistant.tools,
|
|
4604
4799
|
bashCommands: [],
|
|
4605
|
-
timestamp,
|
|
4800
|
+
timestamp: timestamp2,
|
|
4606
4801
|
speed: "standard",
|
|
4607
4802
|
deduplicationKey,
|
|
4608
4803
|
userMessage: turn.userMessage,
|
|
@@ -4633,18 +4828,18 @@ function createCursorAgentProvider(baseDirOverride) {
|
|
|
4633
4828
|
},
|
|
4634
4829
|
async discoverSessions() {
|
|
4635
4830
|
if (!existsSync2(projectsDir)) return [];
|
|
4636
|
-
const projectEntries = await
|
|
4831
|
+
const projectEntries = await readdir10(projectsDir, { withFileTypes: true });
|
|
4637
4832
|
const sources = [];
|
|
4638
4833
|
for (const entry of projectEntries) {
|
|
4639
4834
|
if (!entry.isDirectory()) continue;
|
|
4640
4835
|
const projectId = prettifyProjectId(entry.name);
|
|
4641
|
-
const transcriptDir =
|
|
4836
|
+
const transcriptDir = join18(projectsDir, entry.name, "agent-transcripts");
|
|
4642
4837
|
if (!existsSync2(transcriptDir)) continue;
|
|
4643
|
-
const transcriptEntries = await
|
|
4838
|
+
const transcriptEntries = await readdir10(transcriptDir, { withFileTypes: true });
|
|
4644
4839
|
for (const transcript of transcriptEntries) {
|
|
4645
4840
|
if (!transcript.isFile()) continue;
|
|
4646
4841
|
if (!transcript.name.endsWith(".txt")) continue;
|
|
4647
|
-
const transcriptPath =
|
|
4842
|
+
const transcriptPath = join18(transcriptDir, transcript.name);
|
|
4648
4843
|
sources.push({
|
|
4649
4844
|
path: transcriptPath,
|
|
4650
4845
|
project: projectId,
|
|
@@ -4788,13 +4983,14 @@ var init_providers = __esm({
|
|
|
4788
4983
|
init_codex();
|
|
4789
4984
|
init_copilot();
|
|
4790
4985
|
init_pi();
|
|
4986
|
+
init_kimi();
|
|
4791
4987
|
cursorProvider = null;
|
|
4792
4988
|
cursorLoadAttempted = false;
|
|
4793
4989
|
opencodeProvider = null;
|
|
4794
4990
|
opencodeLoadAttempted = false;
|
|
4795
4991
|
cursorAgentProvider = null;
|
|
4796
4992
|
cursorAgentLoadAttempted = false;
|
|
4797
|
-
coreProviders = [claude, codex, copilot, pi, omp];
|
|
4993
|
+
coreProviders = [claude, codex, copilot, pi, omp, kimi];
|
|
4798
4994
|
_lastDiscoveryWarnings = [];
|
|
4799
4995
|
}
|
|
4800
4996
|
});
|
|
@@ -4956,21 +5152,21 @@ var init_classifier = __esm({
|
|
|
4956
5152
|
});
|
|
4957
5153
|
|
|
4958
5154
|
// src/session-index.ts
|
|
4959
|
-
import { stat as
|
|
5155
|
+
import { stat as stat13 } from "fs/promises";
|
|
4960
5156
|
import { existsSync as existsSync3, unlinkSync } from "fs";
|
|
4961
|
-
import { readFile as
|
|
4962
|
-
import { join as
|
|
5157
|
+
import { readFile as readFile11, mkdir as mkdir10, rename as rename8, unlink as unlink6, open as open7 } from "fs/promises";
|
|
5158
|
+
import { join as join19 } from "path";
|
|
4963
5159
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
4964
5160
|
function emptyIndex() {
|
|
4965
5161
|
return { v: SESSION_INDEX_VERSION, e: {} };
|
|
4966
5162
|
}
|
|
4967
5163
|
function indexPath() {
|
|
4968
|
-
return
|
|
5164
|
+
return join19(getCacheDir(), INDEX_FILENAME);
|
|
4969
5165
|
}
|
|
4970
5166
|
async function loadSessionIndex() {
|
|
4971
5167
|
if (currentIndex) return currentIndex;
|
|
4972
5168
|
try {
|
|
4973
|
-
const raw = await
|
|
5169
|
+
const raw = await readFile11(indexPath(), "utf-8");
|
|
4974
5170
|
const parsed = JSON.parse(raw);
|
|
4975
5171
|
if (parsed.v !== SESSION_INDEX_VERSION) {
|
|
4976
5172
|
currentIndex = emptyIndex();
|
|
@@ -5012,7 +5208,7 @@ async function checkSessionFile(filePath, index, dateRange) {
|
|
|
5012
5208
|
if (!entry) return "parse";
|
|
5013
5209
|
let fileStat;
|
|
5014
5210
|
try {
|
|
5015
|
-
fileStat = await
|
|
5211
|
+
fileStat = await stat13(filePath);
|
|
5016
5212
|
} catch {
|
|
5017
5213
|
return "parse";
|
|
5018
5214
|
}
|
|
@@ -5066,8 +5262,8 @@ __export(parser_exports, {
|
|
|
5066
5262
|
parseAllSessions: () => parseAllSessions
|
|
5067
5263
|
});
|
|
5068
5264
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
5069
|
-
import { readdir as
|
|
5070
|
-
import { basename as
|
|
5265
|
+
import { readdir as readdir11, stat as stat14 } from "fs/promises";
|
|
5266
|
+
import { basename as basename9, join as join20 } from "path";
|
|
5071
5267
|
function getParseWarnings() {
|
|
5072
5268
|
return _parseWarnings;
|
|
5073
5269
|
}
|
|
@@ -5191,8 +5387,8 @@ async function parseTurns(filePath) {
|
|
|
5191
5387
|
if (!call) continue;
|
|
5192
5388
|
const previous = msgId ? seenCalls.get(msgId) : void 0;
|
|
5193
5389
|
if (previous) {
|
|
5194
|
-
for (const
|
|
5195
|
-
previous.usage[
|
|
5390
|
+
for (const key2 of Object.keys(call.usage)) {
|
|
5391
|
+
previous.usage[key2] = Math.max(previous.usage[key2] ?? 0, call.usage[key2] ?? 0);
|
|
5196
5392
|
}
|
|
5197
5393
|
previous.tools = [.../* @__PURE__ */ new Set([...previous.tools, ...call.tools])];
|
|
5198
5394
|
previous.mcpTools = extractMcpTools(previous.tools);
|
|
@@ -5317,13 +5513,13 @@ function buildSessionSummary(sessionId, project, turns) {
|
|
|
5317
5513
|
async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
|
|
5318
5514
|
if (dateRange) {
|
|
5319
5515
|
try {
|
|
5320
|
-
const s = await
|
|
5516
|
+
const s = await stat14(filePath);
|
|
5321
5517
|
if (s.mtimeMs < dateRange.start.getTime()) return null;
|
|
5322
5518
|
} catch {
|
|
5323
5519
|
}
|
|
5324
5520
|
}
|
|
5325
5521
|
const rawTurns = await cachedFileParse(filePath, "claude-turns", () => parseTurns(filePath));
|
|
5326
|
-
const sessionId =
|
|
5522
|
+
const sessionId = basename9(filePath, ".jsonl");
|
|
5327
5523
|
const turns = [];
|
|
5328
5524
|
for (const turn of rawTurns) {
|
|
5329
5525
|
const calls = turn.assistantCalls.filter((call) => {
|
|
@@ -5337,8 +5533,8 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
|
|
|
5337
5533
|
0
|
|
5338
5534
|
);
|
|
5339
5535
|
if (dateRange) {
|
|
5340
|
-
const
|
|
5341
|
-
if (!Number.isFinite(
|
|
5536
|
+
const timestamp2 = new Date(call.timestamp);
|
|
5537
|
+
if (!Number.isFinite(timestamp2.getTime()) || timestamp2 < dateRange.start || timestamp2 > dateRange.end) return false;
|
|
5342
5538
|
}
|
|
5343
5539
|
if (seenMsgIds.has(call.deduplicationKey)) return false;
|
|
5344
5540
|
seenMsgIds.add(call.deduplicationKey);
|
|
@@ -5356,14 +5552,14 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
|
|
|
5356
5552
|
return buildSessionSummary(sessionId, project, classified);
|
|
5357
5553
|
}
|
|
5358
5554
|
async function collectJsonlFiles2(dirPath) {
|
|
5359
|
-
const files = await
|
|
5360
|
-
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
5555
|
+
const files = await readdir11(dirPath).catch(() => []);
|
|
5556
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => join20(dirPath, f));
|
|
5361
5557
|
for (const entry of files) {
|
|
5362
5558
|
if (entry.endsWith(".jsonl")) continue;
|
|
5363
|
-
const subagentsPath =
|
|
5364
|
-
const subFiles = await
|
|
5559
|
+
const subagentsPath = join20(dirPath, entry, "subagents");
|
|
5560
|
+
const subFiles = await readdir11(subagentsPath).catch(() => []);
|
|
5365
5561
|
for (const sf of subFiles) {
|
|
5366
|
-
if (sf.endsWith(".jsonl")) jsonlFiles.push(
|
|
5562
|
+
if (sf.endsWith(".jsonl")) jsonlFiles.push(join20(subagentsPath, sf));
|
|
5367
5563
|
}
|
|
5368
5564
|
}
|
|
5369
5565
|
return jsonlFiles;
|
|
@@ -5381,7 +5577,7 @@ async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
|
|
|
5381
5577
|
if (action === "skip") continue;
|
|
5382
5578
|
let preStat = null;
|
|
5383
5579
|
try {
|
|
5384
|
-
const s = await
|
|
5580
|
+
const s = await stat14(filePath);
|
|
5385
5581
|
preStat = { size: s.size, mtimeMs: s.mtimeMs };
|
|
5386
5582
|
} catch {
|
|
5387
5583
|
}
|
|
@@ -5389,7 +5585,7 @@ async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
|
|
|
5389
5585
|
const hasApiCalls = session !== null && session.apiCalls > 0;
|
|
5390
5586
|
if (preStat && (hasApiCalls || !dateRange)) {
|
|
5391
5587
|
try {
|
|
5392
|
-
const postStat = await
|
|
5588
|
+
const postStat = await stat14(filePath);
|
|
5393
5589
|
if (postStat.size === preStat.size && postStat.mtimeMs === preStat.mtimeMs) {
|
|
5394
5590
|
indexDirty = recordParseResult(filePath, sessionIndex, preStat.size, preStat.mtimeMs, hasApiCalls) || indexDirty;
|
|
5395
5591
|
}
|
|
@@ -5465,8 +5661,8 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
|
|
|
5465
5661
|
for (const source of sources) {
|
|
5466
5662
|
if (dateRange) {
|
|
5467
5663
|
try {
|
|
5468
|
-
const s = await
|
|
5469
|
-
const wal = await
|
|
5664
|
+
const s = await stat14(source.path);
|
|
5665
|
+
const wal = await stat14(`${source.path}-wal`).catch(() => null);
|
|
5470
5666
|
if (Math.max(s.mtimeMs, wal?.mtimeMs ?? 0) < dateRange.start.getTime()) continue;
|
|
5471
5667
|
} catch {
|
|
5472
5668
|
}
|
|
@@ -5484,7 +5680,7 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
|
|
|
5484
5680
|
}
|
|
5485
5681
|
if (parser.warnings?.length) warnings.push(...parser.warnings);
|
|
5486
5682
|
return result;
|
|
5487
|
-
}, warnings);
|
|
5683
|
+
}, warnings, dateRange);
|
|
5488
5684
|
_parseWarnings.push(...warnings);
|
|
5489
5685
|
const touched = /* @__PURE__ */ new Set();
|
|
5490
5686
|
for (const call of calls) {
|
|
@@ -5498,23 +5694,23 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
|
|
|
5498
5694
|
}
|
|
5499
5695
|
const turn = providerCallToTurn(call);
|
|
5500
5696
|
const classified = classifyTurn(turn);
|
|
5501
|
-
const
|
|
5502
|
-
touched.add(
|
|
5503
|
-
const existing = sessionMap.get(
|
|
5697
|
+
const key2 = `${providerName}:${call.sessionId}:${source.project}`;
|
|
5698
|
+
touched.add(key2);
|
|
5699
|
+
const existing = sessionMap.get(key2);
|
|
5504
5700
|
if (existing) {
|
|
5505
5701
|
existing.turns.push(classified);
|
|
5506
5702
|
} else {
|
|
5507
|
-
sessionMap.set(
|
|
5703
|
+
sessionMap.set(key2, { project: source.project, turns: [classified] });
|
|
5508
5704
|
}
|
|
5509
5705
|
}
|
|
5510
5706
|
if (reportMode.getStore()) {
|
|
5511
|
-
for (const
|
|
5512
|
-
const entry = sessionMap.get(
|
|
5513
|
-
const summary = buildSessionSummary(
|
|
5514
|
-
const previous = compactSessions.get(
|
|
5707
|
+
for (const key2 of touched) {
|
|
5708
|
+
const entry = sessionMap.get(key2);
|
|
5709
|
+
const summary = buildSessionSummary(key2.split(":")[1] ?? key2, entry.project, entry.turns);
|
|
5710
|
+
const previous = compactSessions.get(key2);
|
|
5515
5711
|
if (previous) mergeReportSession(previous, summary);
|
|
5516
|
-
else compactSessions.set(
|
|
5517
|
-
sessionMap.delete(
|
|
5712
|
+
else compactSessions.set(key2, summary);
|
|
5713
|
+
sessionMap.delete(key2);
|
|
5518
5714
|
}
|
|
5519
5715
|
}
|
|
5520
5716
|
}
|
|
@@ -5524,8 +5720,8 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
|
|
|
5524
5720
|
existing.push(summary);
|
|
5525
5721
|
projectMap.set(summary.project, existing);
|
|
5526
5722
|
}
|
|
5527
|
-
for (const [
|
|
5528
|
-
const sessionId =
|
|
5723
|
+
for (const [key2, { project, turns }] of sessionMap) {
|
|
5724
|
+
const sessionId = key2.split(":")[1] ?? key2;
|
|
5529
5725
|
const session = buildSessionSummary(sessionId, project, turns);
|
|
5530
5726
|
if (session.apiCalls > 0) {
|
|
5531
5727
|
const existing = projectMap.get(project) ?? [];
|
|
@@ -5551,6 +5747,7 @@ function sourceContextCacheKey(providerFilter) {
|
|
|
5551
5747
|
claudeConfigDir: process.env["CLAUDE_CONFIG_DIR"] ?? "",
|
|
5552
5748
|
codexHome: process.env["CODEX_HOME"] ?? "",
|
|
5553
5749
|
xdgDataHome: process.env["XDG_DATA_HOME"] ?? "",
|
|
5750
|
+
kimiCodeHome: process.env["KIMI_CODE_HOME"] ?? "",
|
|
5554
5751
|
home: process.env["HOME"] ?? ""
|
|
5555
5752
|
});
|
|
5556
5753
|
}
|
|
@@ -5568,12 +5765,12 @@ function buildCacheFingerprint(sources) {
|
|
|
5568
5765
|
return `${sources.length}:${hash >>> 0}`;
|
|
5569
5766
|
}
|
|
5570
5767
|
async function getSourceContext(providerFilter) {
|
|
5571
|
-
const
|
|
5572
|
-
const resolved = resolvedSourceContexts.get(
|
|
5768
|
+
const key2 = sourceContextCacheKey(providerFilter);
|
|
5769
|
+
const resolved = resolvedSourceContexts.get(key2);
|
|
5573
5770
|
if (resolved && Date.now() - resolved.ts < SOURCE_CONTEXT_TTL_MS) {
|
|
5574
5771
|
return resolved.result;
|
|
5575
5772
|
}
|
|
5576
|
-
const inflight = inFlightSourceContexts.get(
|
|
5773
|
+
const inflight = inFlightSourceContexts.get(key2);
|
|
5577
5774
|
if (inflight) {
|
|
5578
5775
|
return inflight;
|
|
5579
5776
|
}
|
|
@@ -5582,16 +5779,16 @@ async function getSourceContext(providerFilter) {
|
|
|
5582
5779
|
const fingerprint2 = buildCacheFingerprint(sources);
|
|
5583
5780
|
return { sources, fingerprint: fingerprint2 };
|
|
5584
5781
|
})();
|
|
5585
|
-
inFlightSourceContexts.set(
|
|
5782
|
+
inFlightSourceContexts.set(key2, promise);
|
|
5586
5783
|
try {
|
|
5587
5784
|
const result = await promise;
|
|
5588
|
-
resolvedSourceContexts.set(
|
|
5785
|
+
resolvedSourceContexts.set(key2, { result, ts: Date.now() });
|
|
5589
5786
|
return result;
|
|
5590
5787
|
} finally {
|
|
5591
|
-
inFlightSourceContexts.delete(
|
|
5788
|
+
inFlightSourceContexts.delete(key2);
|
|
5592
5789
|
}
|
|
5593
5790
|
}
|
|
5594
|
-
function cachePut(
|
|
5791
|
+
function cachePut(key2, data) {
|
|
5595
5792
|
const now = Date.now();
|
|
5596
5793
|
for (const [k, v] of sessionCache) {
|
|
5597
5794
|
if (now - v.ts > CACHE_TTL_MS3) sessionCache.delete(k);
|
|
@@ -5600,7 +5797,7 @@ function cachePut(key, data) {
|
|
|
5600
5797
|
const oldest = [...sessionCache.entries()].sort((a, b) => a[1].ts - b[1].ts)[0];
|
|
5601
5798
|
if (oldest) sessionCache.delete(oldest[0]);
|
|
5602
5799
|
}
|
|
5603
|
-
sessionCache.set(
|
|
5800
|
+
sessionCache.set(key2, { data, ts: now });
|
|
5604
5801
|
}
|
|
5605
5802
|
function clearParserCaches() {
|
|
5606
5803
|
sessionCache.clear();
|
|
@@ -5645,14 +5842,20 @@ async function parseAllSessions(dateRange, providerFilter, summaryOnly = false,
|
|
|
5645
5842
|
}
|
|
5646
5843
|
return [...merged.values()].sort((a, b) => b.totalCostUSD - a.totalCostUSD);
|
|
5647
5844
|
}
|
|
5648
|
-
return reportMode.run(summaryOnly, () => withScanBudget(() =>
|
|
5845
|
+
return reportMode.run(summaryOnly, () => withScanBudget(async () => {
|
|
5846
|
+
try {
|
|
5847
|
+
return await parseAllSessionsWithinBudget(dateRange, providerFilter);
|
|
5848
|
+
} finally {
|
|
5849
|
+
await flushDiscoveryCache();
|
|
5850
|
+
}
|
|
5851
|
+
}, summaryOnly ? 2e6 : 2e5, signal, (summaryOnly ? 8 : 1) * 1024 ** 3));
|
|
5649
5852
|
}
|
|
5650
5853
|
async function parseAllSessionsWithinBudget(dateRange, providerFilter) {
|
|
5651
5854
|
_parseWarnings = [];
|
|
5652
5855
|
const sourceContext = await getSourceContext(providerFilter);
|
|
5653
5856
|
const allSources = sourceContext.sources;
|
|
5654
|
-
const
|
|
5655
|
-
const cached = sessionCache.get(
|
|
5857
|
+
const key2 = `${getPricingFingerprint()}:${reportMode.getStore() ? "summary" : "full"}:${cacheKey(dateRange, providerFilter, sourceContext.fingerprint)}`;
|
|
5858
|
+
const cached = sessionCache.get(key2);
|
|
5656
5859
|
if (cached && Date.now() - cached.ts < CACHE_TTL_MS3) return cached.data;
|
|
5657
5860
|
sessionCache.clear();
|
|
5658
5861
|
const seenMsgIds = /* @__PURE__ */ new Set();
|
|
@@ -5684,7 +5887,7 @@ async function parseAllSessionsWithinBudget(dateRange, providerFilter) {
|
|
|
5684
5887
|
}
|
|
5685
5888
|
}
|
|
5686
5889
|
const result = Array.from(mergedMap.values()).sort((a, b) => b.totalCostUSD - a.totalCostUSD);
|
|
5687
|
-
cachePut(
|
|
5890
|
+
cachePut(key2, result);
|
|
5688
5891
|
return result;
|
|
5689
5892
|
}
|
|
5690
5893
|
function getDiscoveredProviderNames() {
|
|
@@ -5723,8 +5926,8 @@ init_cache_dir();
|
|
|
5723
5926
|
init_file_lock();
|
|
5724
5927
|
init_resource_budget();
|
|
5725
5928
|
import { existsSync as existsSync7, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
|
|
5726
|
-
import { join as
|
|
5727
|
-
import { homedir as
|
|
5929
|
+
import { join as join30 } from "path";
|
|
5930
|
+
import { homedir as homedir17 } from "os";
|
|
5728
5931
|
import { Command } from "commander";
|
|
5729
5932
|
|
|
5730
5933
|
// src/menubar-installer.ts
|
|
@@ -5766,7 +5969,7 @@ async function ensureSupportedPlatform() {
|
|
|
5766
5969
|
}
|
|
5767
5970
|
}
|
|
5768
5971
|
async function sysProductVersion() {
|
|
5769
|
-
return new Promise((
|
|
5972
|
+
return new Promise((resolve4, reject) => {
|
|
5770
5973
|
const proc = spawn("/usr/bin/sw_vers", ["-productVersion"]);
|
|
5771
5974
|
let out = "";
|
|
5772
5975
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -5775,7 +5978,7 @@ async function sysProductVersion() {
|
|
|
5775
5978
|
proc.on("error", reject);
|
|
5776
5979
|
proc.on("close", (code) => {
|
|
5777
5980
|
if (code !== 0) reject(new Error(`sw_vers exited with ${code}`));
|
|
5778
|
-
else
|
|
5981
|
+
else resolve4(out.trim());
|
|
5779
5982
|
});
|
|
5780
5983
|
});
|
|
5781
5984
|
}
|
|
@@ -5829,27 +6032,27 @@ async function downloadToFile(url, destPath) {
|
|
|
5829
6032
|
await pipeline(nodeStream, createWriteStream(destPath));
|
|
5830
6033
|
}
|
|
5831
6034
|
async function runCommand(command, args) {
|
|
5832
|
-
return new Promise((
|
|
6035
|
+
return new Promise((resolve4, reject) => {
|
|
5833
6036
|
const proc = spawn(command, args, { stdio: "inherit" });
|
|
5834
6037
|
proc.on("error", reject);
|
|
5835
6038
|
proc.on("close", (code) => {
|
|
5836
|
-
if (code === 0)
|
|
6039
|
+
if (code === 0) resolve4();
|
|
5837
6040
|
else reject(new Error(`${command} exited with status ${code}`));
|
|
5838
6041
|
});
|
|
5839
6042
|
});
|
|
5840
6043
|
}
|
|
5841
6044
|
async function sleep(ms) {
|
|
5842
|
-
await new Promise((
|
|
6045
|
+
await new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5843
6046
|
}
|
|
5844
6047
|
async function captureCommand(command, args) {
|
|
5845
|
-
return new Promise((
|
|
6048
|
+
return new Promise((resolve4) => {
|
|
5846
6049
|
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
5847
6050
|
let stdout = "";
|
|
5848
6051
|
proc.stdout?.on("data", (chunk) => {
|
|
5849
6052
|
stdout += chunk.toString();
|
|
5850
6053
|
});
|
|
5851
|
-
proc.on("error", () =>
|
|
5852
|
-
proc.on("close", (code) =>
|
|
6054
|
+
proc.on("error", () => resolve4({ code: 1, stdout: "" }));
|
|
6055
|
+
proc.on("close", (code) => resolve4({ code: code ?? 1, stdout }));
|
|
5853
6056
|
});
|
|
5854
6057
|
}
|
|
5855
6058
|
function parsePids(output) {
|
|
@@ -5884,10 +6087,10 @@ async function waitForRunningApp(timeoutMs = timeoutFromEnv("EXE_WATCHER_APP_LAU
|
|
|
5884
6087
|
}
|
|
5885
6088
|
async function signalPids(signal, pids) {
|
|
5886
6089
|
if (pids.length === 0) return;
|
|
5887
|
-
await new Promise((
|
|
6090
|
+
await new Promise((resolve4) => {
|
|
5888
6091
|
const proc = spawn("/bin/kill", [`-${signal}`, ...pids.map(String)]);
|
|
5889
|
-
proc.on("close", () =>
|
|
5890
|
-
proc.on("error", () =>
|
|
6092
|
+
proc.on("close", () => resolve4());
|
|
6093
|
+
proc.on("error", () => resolve4());
|
|
5891
6094
|
});
|
|
5892
6095
|
}
|
|
5893
6096
|
async function movePath(source, dest) {
|
|
@@ -5947,7 +6150,7 @@ async function installMenubarApp(options = {}) {
|
|
|
5947
6150
|
console.log(`Downloading ${asset.name}...`);
|
|
5948
6151
|
await downloadToFile(asset.browser_download_url, archivePath);
|
|
5949
6152
|
console.log("Unpacking...");
|
|
5950
|
-
await runCommand("/usr/bin/
|
|
6153
|
+
await runCommand("/usr/bin/ditto", ["-x", "-k", archivePath, stagingDir]);
|
|
5951
6154
|
const unpackedApp = join2(stagingDir, APP_BUNDLE_NAME);
|
|
5952
6155
|
if (!await exists(unpackedApp)) {
|
|
5953
6156
|
throw new Error(`Archive did not contain ${APP_BUNDLE_NAME}.`);
|
|
@@ -6163,9 +6366,9 @@ function buildDailyRows(projects, period) {
|
|
|
6163
6366
|
}
|
|
6164
6367
|
}
|
|
6165
6368
|
const { code } = getCurrency();
|
|
6166
|
-
return Object.entries(daily).sort().map(([
|
|
6369
|
+
return Object.entries(daily).sort().map(([date2, d]) => ({
|
|
6167
6370
|
Period: period,
|
|
6168
|
-
Date:
|
|
6371
|
+
Date: date2,
|
|
6169
6372
|
[`Cost (${code})`]: round2(convertCost(d.cost)),
|
|
6170
6373
|
"API Calls": d.calls,
|
|
6171
6374
|
Sessions: d.sessions.size,
|
|
@@ -6454,19 +6657,19 @@ function renderStatusBar(projects) {
|
|
|
6454
6657
|
|
|
6455
6658
|
// src/menubar-json.ts
|
|
6456
6659
|
import { readdirSync, readFileSync } from "fs";
|
|
6457
|
-
import { join as
|
|
6660
|
+
import { join as join21, dirname as dirname5 } from "path";
|
|
6458
6661
|
init_models();
|
|
6459
6662
|
import { fileURLToPath } from "url";
|
|
6460
6663
|
function getCliVersion() {
|
|
6461
6664
|
try {
|
|
6462
|
-
const pkgPath =
|
|
6665
|
+
const pkgPath = join21(dirname5(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
6463
6666
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
6464
6667
|
return pkg.version ?? "0.0.0";
|
|
6465
6668
|
} catch {
|
|
6466
6669
|
return "0.0.0";
|
|
6467
6670
|
}
|
|
6468
6671
|
}
|
|
6469
|
-
var MIN_APP_VERSION = "0.2.
|
|
6672
|
+
var MIN_APP_VERSION = "0.2.59";
|
|
6470
6673
|
var TOP_ACTIVITIES_LIMIT = 20;
|
|
6471
6674
|
var TOP_MODELS_LIMIT = 20;
|
|
6472
6675
|
var TOP_FINDINGS_LIMIT = 10;
|
|
@@ -6485,7 +6688,7 @@ function estimateAgentCosts(stats) {
|
|
|
6485
6688
|
}
|
|
6486
6689
|
function buildProjectSpendFromDays(selectedDays, days24h, days7d, days30d) {
|
|
6487
6690
|
const aggregate = (days) => {
|
|
6488
|
-
const byName =
|
|
6691
|
+
const byName = /* @__PURE__ */ Object.create(null);
|
|
6489
6692
|
for (const day of days) {
|
|
6490
6693
|
for (const [projectName, totals] of Object.entries(day.projects)) {
|
|
6491
6694
|
const name = cleanProjectName(projectName);
|
|
@@ -6547,7 +6750,17 @@ function buildTopActivities(categories) {
|
|
|
6547
6750
|
}));
|
|
6548
6751
|
}
|
|
6549
6752
|
function buildTopModels(models) {
|
|
6550
|
-
|
|
6753
|
+
const merged = /* @__PURE__ */ new Map();
|
|
6754
|
+
for (const model of models) {
|
|
6755
|
+
if (model.name === SYNTHETIC_MODEL_NAME) continue;
|
|
6756
|
+
const name = getShortModelName(model.name);
|
|
6757
|
+
const row = merged.get(name) ?? { name, cost: 0, calls: 0, callsIncomplete: false };
|
|
6758
|
+
row.cost += model.cost;
|
|
6759
|
+
row.calls += model.calls;
|
|
6760
|
+
row.callsIncomplete ||= model.callsIncomplete === true;
|
|
6761
|
+
merged.set(name, row);
|
|
6762
|
+
}
|
|
6763
|
+
return [...merged.values()].sort((a, b) => b.cost - a.cost).slice(0, TOP_MODELS_LIMIT);
|
|
6551
6764
|
}
|
|
6552
6765
|
function buildOptimize(optimize) {
|
|
6553
6766
|
if (!optimize || optimize.findings.length === 0) {
|
|
@@ -6589,6 +6802,7 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
|
|
|
6589
6802
|
label: current.label,
|
|
6590
6803
|
cost: current.cost,
|
|
6591
6804
|
calls: current.calls,
|
|
6805
|
+
callsIncomplete: current.callsIncomplete === true,
|
|
6592
6806
|
sessions: current.sessions,
|
|
6593
6807
|
oneShotRate: aggregateOneShotRate(current.categories),
|
|
6594
6808
|
inputTokens: current.inputTokens,
|
|
@@ -6611,18 +6825,18 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
|
|
|
6611
6825
|
// src/daily-cache.ts
|
|
6612
6826
|
init_cache_dir();
|
|
6613
6827
|
init_models();
|
|
6614
|
-
import { createHash as
|
|
6828
|
+
import { createHash as createHash5, randomBytes as randomBytes6 } from "crypto";
|
|
6615
6829
|
import { existsSync as existsSync4 } from "fs";
|
|
6616
|
-
import { mkdir as mkdir11, open as open8, readFile as
|
|
6617
|
-
import { join as
|
|
6618
|
-
var DAILY_CACHE_VERSION =
|
|
6830
|
+
import { mkdir as mkdir11, open as open8, readFile as readFile12, rename as rename9, unlink as unlink7 } from "fs/promises";
|
|
6831
|
+
import { join as join22 } from "path";
|
|
6832
|
+
var DAILY_CACHE_VERSION = 9;
|
|
6619
6833
|
var DEFAULT_DAILY_CACHE_SCOPE = "global";
|
|
6620
6834
|
var DAILY_CACHE_FILENAME = "daily-cache.json";
|
|
6621
6835
|
function getCachePath3(scopeKey) {
|
|
6622
6836
|
if (scopeKey === DEFAULT_DAILY_CACHE_SCOPE) {
|
|
6623
|
-
return
|
|
6837
|
+
return join22(getCacheDir(), DAILY_CACHE_FILENAME);
|
|
6624
6838
|
}
|
|
6625
|
-
return
|
|
6839
|
+
return join22(getCacheDir(), `daily-cache-${scopeKey}.json`);
|
|
6626
6840
|
}
|
|
6627
6841
|
function emptyCache(scopeKey) {
|
|
6628
6842
|
return { version: DAILY_CACHE_VERSION, scopeKey, lastComputedDate: null, days: [] };
|
|
@@ -6637,26 +6851,46 @@ function buildDailyCacheScopeKey(include, exclude) {
|
|
|
6637
6851
|
return DEFAULT_DAILY_CACHE_SCOPE;
|
|
6638
6852
|
}
|
|
6639
6853
|
const raw = JSON.stringify({ include: normalizedInclude, exclude: normalizedExclude });
|
|
6640
|
-
return `scope-${
|
|
6854
|
+
return `scope-${createHash5("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
6641
6855
|
}
|
|
6642
6856
|
function isValidCache(parsed) {
|
|
6643
6857
|
if (!parsed || typeof parsed !== "object") return false;
|
|
6644
6858
|
const c = parsed;
|
|
6645
|
-
if (c.version !== DAILY_CACHE_VERSION) return false;
|
|
6859
|
+
if (c.version !== DAILY_CACHE_VERSION && c.version !== 8) return false;
|
|
6646
6860
|
if (typeof c.scopeKey !== "string" || c.scopeKey.length === 0) return false;
|
|
6647
6861
|
if (!Array.isArray(c.days)) return false;
|
|
6862
|
+
if (c.missingProviders && (!Array.isArray(c.missingProviders) || !c.missingProviders.every((p) => typeof p === "string"))) return false;
|
|
6863
|
+
if (c.historyBuild) {
|
|
6864
|
+
const stage = c.historyBuild;
|
|
6865
|
+
const validDate = (value) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value));
|
|
6866
|
+
if (!validDate(stage.start) || !validDate(stage.end) || stage.start > stage.end || !Array.isArray(stage.days) || !Array.isArray(stage.completedProviders) || !stage.completedProviders.every((p) => typeof p === "string") || stage.providers && (!Array.isArray(stage.providers) || !stage.providers.every((p) => typeof p === "string")) || stage.mergeExisting !== void 0 && typeof stage.mergeExisting !== "boolean" || stage.chunked && (typeof stage.chunked.provider !== "string" || !validDate(stage.chunked.nextDate))) return false;
|
|
6867
|
+
}
|
|
6648
6868
|
return true;
|
|
6649
6869
|
}
|
|
6650
6870
|
async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
|
|
6651
6871
|
const path = getCachePath3(scopeKey);
|
|
6652
6872
|
if (!existsSync4(path)) return emptyCache(scopeKey);
|
|
6653
6873
|
try {
|
|
6654
|
-
const raw = await
|
|
6874
|
+
const raw = await readFile12(path, "utf-8");
|
|
6655
6875
|
const parsed = JSON.parse(raw);
|
|
6656
6876
|
if (!isValidCache(parsed)) return emptyCache(scopeKey);
|
|
6657
6877
|
if (parsed.scopeKey !== scopeKey) return emptyCache(scopeKey);
|
|
6658
|
-
if (parsed.days.length && parsed.pricingFingerprint !== getPricingFingerprint()) {
|
|
6659
|
-
return {
|
|
6878
|
+
if ((parsed.days.length || parsed.historyBuild) && parsed.pricingFingerprint !== getPricingFingerprint()) {
|
|
6879
|
+
return {
|
|
6880
|
+
...parsed,
|
|
6881
|
+
version: DAILY_CACHE_VERSION,
|
|
6882
|
+
historyBuild: void 0,
|
|
6883
|
+
needsRepricing: true,
|
|
6884
|
+
previousHistoryStart: parsed.days[0]?.date ?? parsed.historyBuild?.start
|
|
6885
|
+
};
|
|
6886
|
+
}
|
|
6887
|
+
if (parsed.version !== DAILY_CACHE_VERSION) {
|
|
6888
|
+
return {
|
|
6889
|
+
...parsed,
|
|
6890
|
+
version: DAILY_CACHE_VERSION,
|
|
6891
|
+
historyBuild: void 0,
|
|
6892
|
+
missingProviders: parsed.days.length ? ["kimi"] : void 0
|
|
6893
|
+
};
|
|
6660
6894
|
}
|
|
6661
6895
|
return parsed;
|
|
6662
6896
|
} catch {
|
|
@@ -6670,7 +6904,7 @@ async function saveDailyCache(cache) {
|
|
|
6670
6904
|
const finalPath = getCachePath3(scopeKey);
|
|
6671
6905
|
const tempPath = `${finalPath}.${randomBytes6(8).toString("hex")}.tmp`;
|
|
6672
6906
|
const payload = JSON.stringify({ ...cache, scopeKey, pricingFingerprint: getPricingFingerprint() });
|
|
6673
|
-
if (await
|
|
6907
|
+
if (await readFile12(finalPath, "utf8").catch(() => "") === payload) return;
|
|
6674
6908
|
const handle = await open8(tempPath, "w", 384);
|
|
6675
6909
|
try {
|
|
6676
6910
|
await handle.writeFile(payload, { encoding: "utf-8" });
|
|
@@ -6724,41 +6958,41 @@ init_types();
|
|
|
6724
6958
|
// src/claude-recovery.ts
|
|
6725
6959
|
init_cache_dir();
|
|
6726
6960
|
init_models();
|
|
6727
|
-
import { readFile as
|
|
6728
|
-
import { createHash as
|
|
6729
|
-
import { homedir as
|
|
6730
|
-
import { join as
|
|
6961
|
+
import { readFile as readFile13, stat as stat16, mkdir as mkdir12, writeFile as writeFile4, rename as rename10, unlink as unlink8 } from "fs/promises";
|
|
6962
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
6963
|
+
import { homedir as homedir12 } from "os";
|
|
6964
|
+
import { join as join24 } from "path";
|
|
6731
6965
|
|
|
6732
6966
|
// src/history-start.ts
|
|
6733
6967
|
init_discovery_cache();
|
|
6734
6968
|
init_fs_utils();
|
|
6735
6969
|
init_parser();
|
|
6736
|
-
import { readdir as
|
|
6737
|
-
import { join as
|
|
6970
|
+
import { readdir as readdir12, stat as stat15 } from "fs/promises";
|
|
6971
|
+
import { join as join23 } from "path";
|
|
6738
6972
|
function parseHistoryStartDate(value) {
|
|
6739
6973
|
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
|
|
6740
6974
|
const [year, month, day] = value.split("-").map(Number);
|
|
6741
|
-
const
|
|
6742
|
-
return
|
|
6975
|
+
const date2 = new Date(year, month - 1, day);
|
|
6976
|
+
return date2.getFullYear() === year && date2.getMonth() === month - 1 && date2.getDate() === day ? date2 : null;
|
|
6743
6977
|
}
|
|
6744
6978
|
async function resolveHistoryStart(configured, cachedDates, now = /* @__PURE__ */ new Date()) {
|
|
6745
6979
|
const requestedStart = parseHistoryStartDate(configured);
|
|
6746
6980
|
if (requestedStart && requestedStart <= now) return requestedStart;
|
|
6747
6981
|
let earliest = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
6748
|
-
const include = (
|
|
6749
|
-
if (
|
|
6982
|
+
const include = (date2) => {
|
|
6983
|
+
if (date2 && Number.isFinite(date2.getTime()) && date2 < earliest) earliest = date2;
|
|
6750
6984
|
};
|
|
6751
6985
|
include(parseHistoryStartDate(configured));
|
|
6752
|
-
for (const
|
|
6986
|
+
for (const date2 of cachedDates) include(parseHistoryStartDate(date2));
|
|
6753
6987
|
const sources = await getDiscoveredSources();
|
|
6754
6988
|
for (const source of sources) {
|
|
6755
|
-
const sourceStat = await
|
|
6989
|
+
const sourceStat = await stat15(source.path).catch(() => null);
|
|
6756
6990
|
if (!sourceStat) continue;
|
|
6757
|
-
const paths = sourceStat.isDirectory() ? (await
|
|
6991
|
+
const paths = sourceStat.isDirectory() ? (await readdir12(source.path)).filter((name) => name.endsWith(".jsonl")).map((name) => join23(source.path, name)) : source.path.endsWith(".jsonl") ? [source.path] : [];
|
|
6758
6992
|
for (const path of paths) {
|
|
6759
|
-
const fileStat = await
|
|
6993
|
+
const fileStat = await stat15(path).catch(() => null);
|
|
6760
6994
|
if (!fileStat) continue;
|
|
6761
|
-
const
|
|
6995
|
+
const timestamp2 = await cachedDiscoveryHeader("history-start", path, fileStat, async () => {
|
|
6762
6996
|
try {
|
|
6763
6997
|
const entry = JSON.parse(await readSessionFirstLine(path) ?? "{}");
|
|
6764
6998
|
return typeof entry.timestamp === "string" ? entry.timestamp : null;
|
|
@@ -6766,7 +7000,7 @@ async function resolveHistoryStart(configured, cachedDates, now = /* @__PURE__ *
|
|
|
6766
7000
|
return null;
|
|
6767
7001
|
}
|
|
6768
7002
|
});
|
|
6769
|
-
if (
|
|
7003
|
+
if (timestamp2) include(new Date(timestamp2));
|
|
6770
7004
|
}
|
|
6771
7005
|
}
|
|
6772
7006
|
return new Date(earliest.getFullYear(), earliest.getMonth(), earliest.getDate());
|
|
@@ -6775,14 +7009,29 @@ async function resolveHistoryStart(configured, cachedDates, now = /* @__PURE__ *
|
|
|
6775
7009
|
// src/claude-recovery.ts
|
|
6776
7010
|
var fields = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"];
|
|
6777
7011
|
var emptyTokens = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
7012
|
+
function applyClaudeRecovery(data, recovery) {
|
|
7013
|
+
data.cost += recovery.costUSD;
|
|
7014
|
+
for (const field of fields) data[field] += recovery[field];
|
|
7015
|
+
data.callsIncomplete = true;
|
|
7016
|
+
for (const recovered of recovery.models) {
|
|
7017
|
+
const name = getShortModelName(recovered.model);
|
|
7018
|
+
const existing = data.models.find((model) => getShortModelName(model.name) === name);
|
|
7019
|
+
if (existing) {
|
|
7020
|
+
existing.cost += recovered.costUSD;
|
|
7021
|
+
existing.callsIncomplete = true;
|
|
7022
|
+
} else {
|
|
7023
|
+
data.models.push({ name, cost: recovered.costUSD, calls: 0, callsIncomplete: true });
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
}
|
|
6778
7027
|
async function readClaudeUsageSnapshot() {
|
|
6779
|
-
const root = process.env["CLAUDE_CONFIG_DIR"] ||
|
|
6780
|
-
const path =
|
|
6781
|
-
const savedPath =
|
|
7028
|
+
const root = process.env["CLAUDE_CONFIG_DIR"] || join24(homedir12(), ".claude");
|
|
7029
|
+
const path = join24(root, "stats-cache.json");
|
|
7030
|
+
const savedPath = join24(getCacheDir(), `claude-lifetime-${createHash6("sha256").update(root).digest("hex").slice(0, 16)}.json`);
|
|
6782
7031
|
const read = async (file) => {
|
|
6783
7032
|
try {
|
|
6784
|
-
if ((await
|
|
6785
|
-
const data = JSON.parse(await
|
|
7033
|
+
if ((await stat16(file)).size > 2 * 1024 ** 2) return null;
|
|
7034
|
+
const data = JSON.parse(await readFile13(file, "utf8"));
|
|
6786
7035
|
if (!data || typeof data.firstSessionDate !== "string" || !parseHistoryStartDate(data.lastComputedDate) || !data.modelUsage || typeof data.modelUsage !== "object") return null;
|
|
6787
7036
|
if (!Number.isFinite(new Date(data.firstSessionDate).getTime())) return null;
|
|
6788
7037
|
if (parseHistoryStartDate(data.lastComputedDate).getTime() > Date.now()) return null;
|
|
@@ -6800,13 +7049,13 @@ async function readClaudeUsageSnapshot() {
|
|
|
6800
7049
|
for (const [model, counters] of Object.entries(saved.modelUsage)) {
|
|
6801
7050
|
if (!counters || typeof counters !== "object") continue;
|
|
6802
7051
|
const row = combined.modelUsage[model] ??= {};
|
|
6803
|
-
for (const
|
|
6804
|
-
if (Number.isFinite(counters[
|
|
7052
|
+
for (const key2 of ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"]) {
|
|
7053
|
+
if (Number.isFinite(counters[key2]) && (counters[key2] ?? 0) >= 0) row[key2] = Math.max(row[key2] ?? 0, counters[key2] ?? 0);
|
|
6805
7054
|
}
|
|
6806
7055
|
}
|
|
6807
7056
|
}
|
|
6808
7057
|
const payload = JSON.stringify(combined);
|
|
6809
|
-
if (await
|
|
7058
|
+
if (await readFile13(savedPath, "utf8").catch(() => "") !== payload) {
|
|
6810
7059
|
await mkdir12(getCacheDir(), { recursive: true });
|
|
6811
7060
|
const tmp = `${savedPath}.${randomUUID3()}.tmp`;
|
|
6812
7061
|
try {
|
|
@@ -6860,7 +7109,7 @@ function recoverClaudeUsage(snapshot, recorded, rangeStart) {
|
|
|
6860
7109
|
source: "claude-stats-cache",
|
|
6861
7110
|
throughDate: snapshot.lastComputedDate,
|
|
6862
7111
|
...totals,
|
|
6863
|
-
costUSD: models.reduce((
|
|
7112
|
+
costUSD: models.reduce((sum2, row) => sum2 + row.costUSD, 0),
|
|
6864
7113
|
models,
|
|
6865
7114
|
note: "Includes historical Claude token counters missing from retained transcripts, reconciled per model and token type to avoid double-counting. Their dates, calls, projects, request sizes, cache TTL and speed are unavailable; recovered cost uses latest standard API rates, short-context rates and 5-minute cache writes. Time-based discounts cannot be reconstructed. Earlier usage without logs or counters remains unknown."
|
|
6866
7115
|
};
|
|
@@ -6932,7 +7181,7 @@ var LatestLoad = class {
|
|
|
6932
7181
|
// src/dashboard.tsx
|
|
6933
7182
|
init_resource_budget();
|
|
6934
7183
|
init_types();
|
|
6935
|
-
import { homedir as
|
|
7184
|
+
import { homedir as homedir16 } from "os";
|
|
6936
7185
|
import { useState as useState2, useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
6937
7186
|
import { render as render2, Box as Box2, Text as Text2, useInput as useInput2, useApp as useApp2, useWindowSize } from "ink";
|
|
6938
7187
|
init_parser();
|
|
@@ -6940,12 +7189,15 @@ init_models();
|
|
|
6940
7189
|
|
|
6941
7190
|
// src/optimize.ts
|
|
6942
7191
|
init_fs_utils();
|
|
7192
|
+
init_parsed_file_cache();
|
|
7193
|
+
init_discovery_cache();
|
|
7194
|
+
init_resource_budget();
|
|
6943
7195
|
init_providers();
|
|
6944
7196
|
import chalk2 from "chalk";
|
|
6945
|
-
import { readdir as
|
|
7197
|
+
import { readdir as readdir13, stat as stat17 } from "fs/promises";
|
|
6946
7198
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
6947
|
-
import { basename as
|
|
6948
|
-
import { homedir as
|
|
7199
|
+
import { basename as basename10, join as join25 } from "path";
|
|
7200
|
+
import { homedir as homedir13 } from "os";
|
|
6949
7201
|
var ORANGE = "#F59E0B";
|
|
6950
7202
|
var DIM = "#666666";
|
|
6951
7203
|
var GOLD = "#F5D76E";
|
|
@@ -7020,21 +7272,20 @@ var TOP_ITEMS_PREVIEW = 3;
|
|
|
7020
7272
|
var GHOST_NAMES_PREVIEW = 5;
|
|
7021
7273
|
var GHOST_CLEANUP_COMMANDS_LIMIT = 10;
|
|
7022
7274
|
var FILE_READ_CONCURRENCY = 1;
|
|
7023
|
-
var RESULT_CACHE_TTL_MS = 6e4;
|
|
7024
7275
|
var RECENT_WINDOW_HOURS = 48;
|
|
7025
7276
|
var RECENT_WINDOW_MS = RECENT_WINDOW_HOURS * 60 * 60 * 1e3;
|
|
7026
7277
|
var DEFAULT_TREND_PERIOD_DAYS = 30;
|
|
7027
7278
|
var DEFAULT_TREND_PERIOD_MS = DEFAULT_TREND_PERIOD_DAYS * 24 * 60 * 60 * 1e3;
|
|
7028
7279
|
var IMPROVING_THRESHOLD = 0.5;
|
|
7029
7280
|
async function collectJsonlFiles3(dirPath) {
|
|
7030
|
-
const files = await
|
|
7031
|
-
const result = files.filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
7281
|
+
const files = await readdir13(dirPath).catch(() => []);
|
|
7282
|
+
const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join25(dirPath, f));
|
|
7032
7283
|
for (const entry of files) {
|
|
7033
7284
|
if (entry.endsWith(".jsonl")) continue;
|
|
7034
|
-
const subPath =
|
|
7035
|
-
const subFiles = await
|
|
7285
|
+
const subPath = join25(dirPath, entry, "subagents");
|
|
7286
|
+
const subFiles = await readdir13(subPath).catch(() => []);
|
|
7036
7287
|
for (const sf of subFiles) {
|
|
7037
|
-
if (sf.endsWith(".jsonl")) result.push(
|
|
7288
|
+
if (sf.endsWith(".jsonl")) result.push(join25(subPath, sf));
|
|
7038
7289
|
}
|
|
7039
7290
|
}
|
|
7040
7291
|
return result;
|
|
@@ -7042,7 +7293,7 @@ async function collectJsonlFiles3(dirPath) {
|
|
|
7042
7293
|
async function isFileStaleForRange(filePath, range) {
|
|
7043
7294
|
if (!range) return false;
|
|
7044
7295
|
try {
|
|
7045
|
-
const s = await
|
|
7296
|
+
const s = await stat17(filePath);
|
|
7046
7297
|
return s.mtimeMs < range.start.getTime();
|
|
7047
7298
|
} catch {
|
|
7048
7299
|
return false;
|
|
@@ -7059,24 +7310,15 @@ async function runWithConcurrency(items, limit, worker) {
|
|
|
7059
7310
|
}
|
|
7060
7311
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => next()));
|
|
7061
7312
|
}
|
|
7062
|
-
function
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
const ts = new Date(timestamp);
|
|
7066
|
-
return ts >= range.start && ts <= range.end;
|
|
7313
|
+
function commandReferences(text) {
|
|
7314
|
+
COMMAND_PATTERN.lastIndex = 0;
|
|
7315
|
+
return [...text.matchAll(COMMAND_PATTERN)].map((m) => `/${(m[1] || m[2] || "").trim()}`);
|
|
7067
7316
|
}
|
|
7068
|
-
function
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
}
|
|
7072
|
-
async function scanJsonlFile(filePath, project, dateRange, recentCutoffMs = Date.now() - RECENT_WINDOW_MS) {
|
|
7073
|
-
const calls = [];
|
|
7074
|
-
const cwds = [];
|
|
7075
|
-
const apiCalls = [];
|
|
7076
|
-
const userMessages = [];
|
|
7077
|
-
const sessionId = basename9(filePath, ".jsonl");
|
|
7078
|
-
let lastVersion = "";
|
|
7317
|
+
async function readScanRecords(filePath, consume) {
|
|
7318
|
+
const records = [];
|
|
7319
|
+
let version2 = "";
|
|
7079
7320
|
for await (const line of readSessionLines(filePath)) {
|
|
7321
|
+
chargeRecord();
|
|
7080
7322
|
if (!line.trim()) continue;
|
|
7081
7323
|
let entry;
|
|
7082
7324
|
try {
|
|
@@ -7084,74 +7326,135 @@ async function scanJsonlFile(filePath, project, dateRange, recentCutoffMs = Date
|
|
|
7084
7326
|
} catch {
|
|
7085
7327
|
continue;
|
|
7086
7328
|
}
|
|
7087
|
-
if (entry
|
|
7088
|
-
|
|
7089
|
-
const
|
|
7090
|
-
|
|
7091
|
-
|
|
7329
|
+
if (!entry || typeof entry !== "object") continue;
|
|
7330
|
+
if (typeof entry.version === "string") version2 = entry.version;
|
|
7331
|
+
const record = {
|
|
7332
|
+
timestamp: typeof entry.timestamp === "string" ? entry.timestamp : "",
|
|
7333
|
+
cwd: typeof entry.cwd === "string" ? entry.cwd : void 0,
|
|
7334
|
+
calls: [],
|
|
7335
|
+
cacheCreationTokens: 0,
|
|
7336
|
+
version: version2,
|
|
7337
|
+
commands: []
|
|
7338
|
+
};
|
|
7339
|
+
const msg = entry.message;
|
|
7340
|
+
const content = msg?.content;
|
|
7092
7341
|
if (entry.type === "user") {
|
|
7093
|
-
if (
|
|
7094
|
-
const
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7342
|
+
if (typeof content === "string") record.commands.push(...commandReferences(content));
|
|
7343
|
+
else if (Array.isArray(content)) for (const block of content) {
|
|
7344
|
+
if (block?.type === "text" && typeof block.text === "string") record.commands.push(...commandReferences(block.text));
|
|
7345
|
+
}
|
|
7346
|
+
} else if (entry.type === "assistant") {
|
|
7347
|
+
const usage = msg?.usage;
|
|
7348
|
+
if (typeof usage?.cache_creation_input_tokens === "number") record.cacheCreationTokens = usage.cache_creation_input_tokens;
|
|
7349
|
+
if (Array.isArray(content)) for (const block of content) {
|
|
7350
|
+
if (block?.type !== "tool_use" || typeof block.name !== "string") continue;
|
|
7351
|
+
chargeRecord();
|
|
7352
|
+
const input = {};
|
|
7353
|
+
for (const key2 of ["file_path", "subagent_type", "skill", "name"]) {
|
|
7354
|
+
if (typeof block.input?.[key2] === "string") input[key2] = block.input[key2];
|
|
7103
7355
|
}
|
|
7356
|
+
record.calls.push({ name: block.name, input });
|
|
7104
7357
|
}
|
|
7105
|
-
continue;
|
|
7106
|
-
}
|
|
7107
|
-
if (entry.type !== "assistant") continue;
|
|
7108
|
-
if (!withinRange) continue;
|
|
7109
|
-
const msg = entry.message;
|
|
7110
|
-
const usage = msg?.usage;
|
|
7111
|
-
if (usage) {
|
|
7112
|
-
const cacheCreate = usage.cache_creation_input_tokens ?? 0;
|
|
7113
|
-
if (cacheCreate > 0) apiCalls.push({ cacheCreationTokens: cacheCreate, version: lastVersion, recent });
|
|
7114
7358
|
}
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
if (block.type !== "tool_use") continue;
|
|
7119
|
-
calls.push({
|
|
7120
|
-
name: block.name,
|
|
7121
|
-
input: block.input ?? {},
|
|
7122
|
-
sessionId,
|
|
7123
|
-
project,
|
|
7124
|
-
recent
|
|
7125
|
-
});
|
|
7359
|
+
if (record.cwd || record.calls.length || record.cacheCreationTokens > 0 || record.commands.length) {
|
|
7360
|
+
records.push(record);
|
|
7361
|
+
consume?.(record);
|
|
7126
7362
|
}
|
|
7127
7363
|
}
|
|
7364
|
+
return records;
|
|
7365
|
+
}
|
|
7366
|
+
function inRange(timestamp2, range) {
|
|
7367
|
+
if (!range) return true;
|
|
7368
|
+
if (!timestamp2) return false;
|
|
7369
|
+
const ts = new Date(timestamp2);
|
|
7370
|
+
return ts >= range.start && ts <= range.end;
|
|
7371
|
+
}
|
|
7372
|
+
function isRecent(timestamp2, cutoff) {
|
|
7373
|
+
if (!timestamp2) return false;
|
|
7374
|
+
return new Date(timestamp2).getTime() >= cutoff;
|
|
7375
|
+
}
|
|
7376
|
+
async function scanJsonlFile(filePath, project, dateRange, recentCutoffMs = Date.now() - RECENT_WINDOW_MS, sink) {
|
|
7377
|
+
const calls = [];
|
|
7378
|
+
const cwds = [];
|
|
7379
|
+
const apiCalls = [];
|
|
7380
|
+
const userMessages = [];
|
|
7381
|
+
const sessionId = basename10(filePath, ".jsonl");
|
|
7382
|
+
const consume = (record, replay) => {
|
|
7383
|
+
if (replay) chargeRecord();
|
|
7384
|
+
if (!inRange(record.timestamp, dateRange)) return;
|
|
7385
|
+
const recent = isRecent(record.timestamp, recentCutoffMs);
|
|
7386
|
+
if (record.cwd) {
|
|
7387
|
+
cwds.push(record.cwd);
|
|
7388
|
+
sink?.projectCwds.add(record.cwd);
|
|
7389
|
+
}
|
|
7390
|
+
userMessages.push(...record.commands);
|
|
7391
|
+
for (const command of record.commands) sink?.userMessages.push(command);
|
|
7392
|
+
if (record.cacheCreationTokens > 0) {
|
|
7393
|
+
const call = { cacheCreationTokens: record.cacheCreationTokens, version: record.version, recent };
|
|
7394
|
+
apiCalls.push(call);
|
|
7395
|
+
sink?.apiCalls.push(call);
|
|
7396
|
+
}
|
|
7397
|
+
for (const call of record.calls) {
|
|
7398
|
+
if (replay) chargeRecord();
|
|
7399
|
+
const observed = { ...call, sessionId, project, recent };
|
|
7400
|
+
calls.push(observed);
|
|
7401
|
+
sink?.toolCalls.push(observed);
|
|
7402
|
+
}
|
|
7403
|
+
};
|
|
7404
|
+
let parsed = false;
|
|
7405
|
+
const records = await cachedFileParse(filePath, "optimize-v1", () => {
|
|
7406
|
+
parsed = true;
|
|
7407
|
+
return readScanRecords(filePath, (record) => consume(record, false));
|
|
7408
|
+
}, void 0, dateRange);
|
|
7409
|
+
if (!parsed) for (const record of records) consume(record, true);
|
|
7128
7410
|
return { calls, cwds, apiCalls, userMessages };
|
|
7129
7411
|
}
|
|
7130
|
-
async function scanSessions(dateRange) {
|
|
7412
|
+
async function scanSessions(projects, dateRange, allowPartial = false) {
|
|
7131
7413
|
const sources = await discoverAllSessions("claude");
|
|
7414
|
+
const selected = /* @__PURE__ */ new Map();
|
|
7415
|
+
for (const project of projects) {
|
|
7416
|
+
const ids = selected.get(project.project) ?? /* @__PURE__ */ new Set();
|
|
7417
|
+
for (const session of project.sessions) ids.add(session.sessionId);
|
|
7418
|
+
selected.set(project.project, ids);
|
|
7419
|
+
}
|
|
7132
7420
|
const allCalls = [];
|
|
7133
7421
|
const allCwds = /* @__PURE__ */ new Set();
|
|
7134
7422
|
const allApiCalls = [];
|
|
7135
7423
|
const allUserMessages = [];
|
|
7424
|
+
const data = { toolCalls: allCalls, projectCwds: allCwds, apiCalls: allApiCalls, userMessages: allUserMessages };
|
|
7425
|
+
const recency = /* @__PURE__ */ new Map();
|
|
7426
|
+
for (const project of projects) for (const session of project.sessions) {
|
|
7427
|
+
const key2 = `${project.project}:${session.sessionId}`;
|
|
7428
|
+
recency.set(key2, Math.max(recency.get(key2) ?? 0, Date.parse(session.lastTimestamp ?? "") || 0));
|
|
7429
|
+
}
|
|
7136
7430
|
const tasks = [];
|
|
7137
7431
|
for (const source of sources) {
|
|
7432
|
+
chargeRead(0);
|
|
7433
|
+
const sessionIds = selected.get(source.project);
|
|
7434
|
+
if (!sessionIds) continue;
|
|
7138
7435
|
const files = await collectJsonlFiles3(source.path);
|
|
7139
7436
|
for (const file of files) {
|
|
7437
|
+
if (!sessionIds.has(basename10(file, ".jsonl"))) continue;
|
|
7140
7438
|
if (await isFileStaleForRange(file, dateRange)) continue;
|
|
7141
7439
|
tasks.push({ file, project: source.project });
|
|
7142
7440
|
}
|
|
7143
7441
|
}
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
allUserMessages.push(...userMessages);
|
|
7442
|
+
tasks.sort((a, b) => (recency.get(`${b.project}:${basename10(b.file, ".jsonl")}`) ?? 0) - (recency.get(`${a.project}:${basename10(a.file, ".jsonl")}`) ?? 0) || a.file.localeCompare(b.file));
|
|
7443
|
+
const coverage = { totalFiles: tasks.length, completedFiles: 0, partial: false };
|
|
7444
|
+
const scan = () => runWithConcurrency(tasks, FILE_READ_CONCURRENCY, async ({ file, project }) => {
|
|
7445
|
+
await scanJsonlFile(file, project, dateRange, void 0, data);
|
|
7446
|
+
coverage.completedFiles++;
|
|
7150
7447
|
});
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7448
|
+
if (allowPartial) {
|
|
7449
|
+
try {
|
|
7450
|
+
await withScanSlice(scan, { maxRecords: 1e5, maxBytes: 128 * 1024 ** 2, maxMs: 1e4 });
|
|
7451
|
+
} catch (error) {
|
|
7452
|
+
if (!(error instanceof ScanSliceLimitError)) throw error;
|
|
7453
|
+
coverage.partial = true;
|
|
7454
|
+
}
|
|
7455
|
+
data.coverage = coverage;
|
|
7456
|
+
} else await scan();
|
|
7457
|
+
return data;
|
|
7155
7458
|
}
|
|
7156
7459
|
function readJsonFile(path) {
|
|
7157
7460
|
const raw = readSessionFileSync(path);
|
|
@@ -7163,7 +7466,7 @@ function readJsonFile(path) {
|
|
|
7163
7466
|
}
|
|
7164
7467
|
}
|
|
7165
7468
|
function shortHomePath(absPath) {
|
|
7166
|
-
const home =
|
|
7469
|
+
const home = homedir13();
|
|
7167
7470
|
return absPath.startsWith(home) ? "~" + absPath.slice(home.length) : absPath;
|
|
7168
7471
|
}
|
|
7169
7472
|
function isReadTool(name) {
|
|
@@ -7172,13 +7475,13 @@ function isReadTool(name) {
|
|
|
7172
7475
|
function loadMcpConfigs(projectCwds) {
|
|
7173
7476
|
const servers = /* @__PURE__ */ new Map();
|
|
7174
7477
|
const configPaths = [
|
|
7175
|
-
|
|
7176
|
-
|
|
7478
|
+
join25(homedir13(), ".claude", "settings.json"),
|
|
7479
|
+
join25(homedir13(), ".claude", "settings.local.json")
|
|
7177
7480
|
];
|
|
7178
7481
|
for (const cwd of projectCwds) {
|
|
7179
|
-
configPaths.push(
|
|
7180
|
-
configPaths.push(
|
|
7181
|
-
configPaths.push(
|
|
7482
|
+
configPaths.push(join25(cwd, ".mcp.json"));
|
|
7483
|
+
configPaths.push(join25(cwd, ".claude", "settings.json"));
|
|
7484
|
+
configPaths.push(join25(cwd, ".claude", "settings.local.json"));
|
|
7182
7485
|
}
|
|
7183
7486
|
for (const p of configPaths) {
|
|
7184
7487
|
if (!existsSync5(p)) continue;
|
|
@@ -7247,9 +7550,9 @@ function detectDuplicateReads(calls, dateRange) {
|
|
|
7247
7550
|
if (!isReadTool(call.name)) continue;
|
|
7248
7551
|
const filePath = call.input.file_path;
|
|
7249
7552
|
if (!filePath || JUNK_PATTERN.test(filePath)) continue;
|
|
7250
|
-
const
|
|
7251
|
-
if (!sessionFiles.has(
|
|
7252
|
-
const fm = sessionFiles.get(
|
|
7553
|
+
const key2 = `${call.project}:${call.sessionId}`;
|
|
7554
|
+
if (!sessionFiles.has(key2)) sessionFiles.set(key2, /* @__PURE__ */ new Map());
|
|
7555
|
+
const fm = sessionFiles.get(key2);
|
|
7253
7556
|
const entry = fm.get(filePath) ?? { count: 0, recent: 0 };
|
|
7254
7557
|
entry.count++;
|
|
7255
7558
|
if (call.recent) entry.recent++;
|
|
@@ -7264,7 +7567,7 @@ function detectDuplicateReads(calls, dateRange) {
|
|
|
7264
7567
|
const extra = entry.count - 1;
|
|
7265
7568
|
totalDuplicates += extra;
|
|
7266
7569
|
if (entry.recent > 1) recentDuplicates += entry.recent - 1;
|
|
7267
|
-
const name =
|
|
7570
|
+
const name = basename10(file);
|
|
7268
7571
|
fileDupes.set(name, (fileDupes.get(name) ?? 0) + extra);
|
|
7269
7572
|
}
|
|
7270
7573
|
}
|
|
@@ -7331,12 +7634,12 @@ function expandImports(filePath, seen, depth) {
|
|
|
7331
7634
|
if (content === null) return { totalLines: 0, importedFiles: 0 };
|
|
7332
7635
|
let totalLines = content.split("\n").length;
|
|
7333
7636
|
let importedFiles = 0;
|
|
7334
|
-
const dir =
|
|
7637
|
+
const dir = join25(filePath, "..");
|
|
7335
7638
|
IMPORT_PATTERN.lastIndex = 0;
|
|
7336
7639
|
for (const match of content.matchAll(IMPORT_PATTERN)) {
|
|
7337
7640
|
const rawPath = match[1];
|
|
7338
7641
|
if (!rawPath) continue;
|
|
7339
|
-
const resolved = rawPath.startsWith("/") ? rawPath :
|
|
7642
|
+
const resolved = rawPath.startsWith("/") ? rawPath : join25(dir, rawPath);
|
|
7340
7643
|
if (!existsSync5(resolved)) continue;
|
|
7341
7644
|
const nested = expandImports(resolved, seen, depth + 1);
|
|
7342
7645
|
totalLines += nested.totalLines;
|
|
@@ -7348,7 +7651,7 @@ function detectBloatedClaudeMd(projectCwds) {
|
|
|
7348
7651
|
const bloated = [];
|
|
7349
7652
|
for (const cwd of projectCwds) {
|
|
7350
7653
|
for (const name of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
|
|
7351
|
-
const fullPath =
|
|
7654
|
+
const fullPath = join25(cwd, name);
|
|
7352
7655
|
if (!existsSync5(fullPath)) continue;
|
|
7353
7656
|
const { totalLines, importedFiles } = expandImports(fullPath, /* @__PURE__ */ new Set(), 0);
|
|
7354
7657
|
if (totalLines > CLAUDEMD_HEALTHY_LINES) {
|
|
@@ -7479,7 +7782,7 @@ function detectCacheBloat(apiCalls, projects, dateRange) {
|
|
|
7479
7782
|
async function listMarkdownFiles(dir) {
|
|
7480
7783
|
if (!existsSync5(dir)) return [];
|
|
7481
7784
|
try {
|
|
7482
|
-
const entries = await
|
|
7785
|
+
const entries = await readdir13(dir);
|
|
7483
7786
|
return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(/\.md$/, ""));
|
|
7484
7787
|
} catch {
|
|
7485
7788
|
return [];
|
|
@@ -7488,10 +7791,10 @@ async function listMarkdownFiles(dir) {
|
|
|
7488
7791
|
async function listSkillDirs(dir) {
|
|
7489
7792
|
if (!existsSync5(dir)) return [];
|
|
7490
7793
|
try {
|
|
7491
|
-
const entries = await
|
|
7794
|
+
const entries = await readdir13(dir);
|
|
7492
7795
|
const names = [];
|
|
7493
7796
|
for (const entry of entries) {
|
|
7494
|
-
if (existsSync5(
|
|
7797
|
+
if (existsSync5(join25(dir, entry, "SKILL.md"))) names.push(entry);
|
|
7495
7798
|
}
|
|
7496
7799
|
return names;
|
|
7497
7800
|
} catch {
|
|
@@ -7499,7 +7802,7 @@ async function listSkillDirs(dir) {
|
|
|
7499
7802
|
}
|
|
7500
7803
|
}
|
|
7501
7804
|
async function detectGhostAgents(calls) {
|
|
7502
|
-
const defined = await listMarkdownFiles(
|
|
7805
|
+
const defined = await listMarkdownFiles(join25(homedir13(), ".claude", "agents"));
|
|
7503
7806
|
if (defined.length === 0) return null;
|
|
7504
7807
|
const invoked = /* @__PURE__ */ new Set();
|
|
7505
7808
|
for (const call of calls) {
|
|
@@ -7524,7 +7827,7 @@ async function detectGhostAgents(calls) {
|
|
|
7524
7827
|
};
|
|
7525
7828
|
}
|
|
7526
7829
|
async function detectGhostSkills(calls) {
|
|
7527
|
-
const defined = await listSkillDirs(
|
|
7830
|
+
const defined = await listSkillDirs(join25(homedir13(), ".claude", "skills"));
|
|
7528
7831
|
if (defined.length === 0) return null;
|
|
7529
7832
|
const invoked = /* @__PURE__ */ new Set();
|
|
7530
7833
|
for (const call of calls) {
|
|
@@ -7549,7 +7852,7 @@ async function detectGhostSkills(calls) {
|
|
|
7549
7852
|
};
|
|
7550
7853
|
}
|
|
7551
7854
|
async function detectGhostCommands(userMessages) {
|
|
7552
|
-
const defined = await listMarkdownFiles(
|
|
7855
|
+
const defined = await listMarkdownFiles(join25(homedir13(), ".claude", "commands"));
|
|
7553
7856
|
if (defined.length === 0) return null;
|
|
7554
7857
|
const invoked = /* @__PURE__ */ new Set();
|
|
7555
7858
|
for (const msg of userMessages) {
|
|
@@ -7577,7 +7880,7 @@ async function detectGhostCommands(userMessages) {
|
|
|
7577
7880
|
}
|
|
7578
7881
|
function readShellProfileLimit() {
|
|
7579
7882
|
for (const profile of SHELL_PROFILES) {
|
|
7580
|
-
const path =
|
|
7883
|
+
const path = join25(homedir13(), profile);
|
|
7581
7884
|
if (!existsSync5(path)) continue;
|
|
7582
7885
|
const content = readSessionFileSync(path);
|
|
7583
7886
|
if (content === null) continue;
|
|
@@ -7657,28 +7960,26 @@ function computeInputCostRate(projects) {
|
|
|
7657
7960
|
if (totalTokens === 0 || totalCost === 0) return DEFAULT_COST_PER_TOKEN;
|
|
7658
7961
|
return totalCost * INPUT_COST_RATIO / totalTokens;
|
|
7659
7962
|
}
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7963
|
+
async function scanAndDetect(projects, dateRange, preScanned, options = {}) {
|
|
7964
|
+
try {
|
|
7965
|
+
return await withScanBudget(() => scanAndDetectWithinBudget(projects, dateRange, preScanned, options.allowPartial));
|
|
7966
|
+
} finally {
|
|
7967
|
+
await flushDiscoveryCache();
|
|
7968
|
+
}
|
|
7665
7969
|
}
|
|
7666
|
-
async function
|
|
7970
|
+
async function scanAndDetectWithinBudget(projects, dateRange, preScanned, allowPartial = false) {
|
|
7667
7971
|
if (projects.length === 0) {
|
|
7668
7972
|
return { findings: [], costRate: 0, healthScore: 100, healthGrade: "A" };
|
|
7669
7973
|
}
|
|
7670
|
-
const key = cacheKey2(projects, dateRange);
|
|
7671
|
-
const cached = resultCache.get(key);
|
|
7672
|
-
if (cached && Date.now() - cached.ts < RESULT_CACHE_TTL_MS) return cached.data;
|
|
7673
7974
|
const costRate = computeInputCostRate(projects);
|
|
7674
|
-
const { toolCalls, projectCwds, apiCalls, userMessages } = preScanned ? await preScanned : await scanSessions(dateRange);
|
|
7975
|
+
const { toolCalls, projectCwds, apiCalls, userMessages, coverage } = preScanned ? await preScanned : await scanSessions(projects, dateRange, allowPartial);
|
|
7675
7976
|
const findings = [];
|
|
7676
7977
|
const syncDetectors = [
|
|
7677
|
-
() => detectCacheBloat(apiCalls, projects, dateRange),
|
|
7978
|
+
() => coverage?.partial ? null : detectCacheBloat(apiCalls, projects, dateRange),
|
|
7678
7979
|
() => detectLowReadEditRatio(toolCalls),
|
|
7679
7980
|
() => detectJunkReads(toolCalls, dateRange),
|
|
7680
7981
|
() => detectDuplicateReads(toolCalls, dateRange),
|
|
7681
|
-
() => detectUnusedMcp(toolCalls, projects, projectCwds),
|
|
7982
|
+
() => coverage?.partial ? null : detectUnusedMcp(toolCalls, projects, projectCwds),
|
|
7682
7983
|
() => detectBloatedClaudeMd(projectCwds),
|
|
7683
7984
|
() => detectBashBloat()
|
|
7684
7985
|
];
|
|
@@ -7686,7 +7987,7 @@ async function scanAndDetect(projects, dateRange, preScanned) {
|
|
|
7686
7987
|
const finding = detect();
|
|
7687
7988
|
if (finding) findings.push(finding);
|
|
7688
7989
|
}
|
|
7689
|
-
const ghostResults = await Promise.all([
|
|
7990
|
+
const ghostResults = coverage?.partial ? [] : await Promise.all([
|
|
7690
7991
|
detectGhostAgents(toolCalls),
|
|
7691
7992
|
detectGhostSkills(toolCalls),
|
|
7692
7993
|
detectGhostCommands(userMessages)
|
|
@@ -7694,8 +7995,7 @@ async function scanAndDetect(projects, dateRange, preScanned) {
|
|
|
7694
7995
|
for (const f of ghostResults) if (f) findings.push(f);
|
|
7695
7996
|
findings.sort((a, b) => urgencyScore(b) - urgencyScore(a));
|
|
7696
7997
|
const { score, grade } = computeHealth(findings);
|
|
7697
|
-
const result = { findings, costRate, healthScore: score, healthGrade: grade };
|
|
7698
|
-
resultCache.set(key, { data: result, ts: Date.now() });
|
|
7998
|
+
const result = { findings, costRate, healthScore: score, healthGrade: grade, coverage };
|
|
7699
7999
|
return result;
|
|
7700
8000
|
}
|
|
7701
8001
|
var PANEL_WIDTH = 62;
|
|
@@ -7745,21 +8045,23 @@ function renderFinding(n, f, costRate) {
|
|
|
7745
8045
|
lines.push("");
|
|
7746
8046
|
return lines;
|
|
7747
8047
|
}
|
|
7748
|
-
function renderOptimize(findings, costRate, periodLabel, periodCost, sessionCount, callCount, healthScore, healthGrade) {
|
|
8048
|
+
function renderOptimize(findings, costRate, periodLabel, periodCost, sessionCount, callCount, healthScore, healthGrade, coverage) {
|
|
7749
8049
|
const lines = [];
|
|
7750
8050
|
lines.push("");
|
|
7751
8051
|
lines.push(` ${chalk2.bold.hex(ORANGE)("Exe Watcher config health")}${chalk2.dim(" " + periodLabel)}`);
|
|
8052
|
+
lines.push(chalk2.dim(" Claude Code recommendations. Changes are yours to apply."));
|
|
8053
|
+
if (coverage?.partial) lines.push(chalk2.hex(ORANGE)(` Partial analysis: ${coverage.completedFiles}/${coverage.totalFiles} logs fully checked; some records remain unchecked. Cleanup suggestions requiring complete history are omitted.`));
|
|
7752
8054
|
lines.push(chalk2.hex(DIM)(" " + SEP.repeat(PANEL_WIDTH)));
|
|
7753
8055
|
const issueSuffix = findings.length > 0 ? `, ${findings.length} issue${findings.length > 1 ? "s" : ""}` : "";
|
|
7754
8056
|
lines.push(" " + [
|
|
7755
8057
|
`${sessionCount} sessions`,
|
|
7756
8058
|
`${callCount.toLocaleString()} calls`,
|
|
7757
8059
|
chalk2.hex(GOLD)(formatCost(periodCost)),
|
|
7758
|
-
`Health: ${chalk2.bold.hex(GRADE_COLORS[healthGrade])(healthGrade)}${chalk2.dim(` (${healthScore}/100${issueSuffix})`)}`
|
|
8060
|
+
coverage?.partial ? "Partial check" : `Health: ${chalk2.bold.hex(GRADE_COLORS[healthGrade])(healthGrade)}${chalk2.dim(` (${healthScore}/100${issueSuffix})`)}`
|
|
7759
8061
|
].join(chalk2.hex(DIM)(" ")));
|
|
7760
8062
|
lines.push("");
|
|
7761
8063
|
if (findings.length === 0) {
|
|
7762
|
-
lines.push(chalk2.hex(GREEN)(" Nothing to fix. Your setup is lean."));
|
|
8064
|
+
lines.push(chalk2.hex(GREEN)(coverage?.partial ? " No recommendations in the checked records; unchecked history is unknown." : " Nothing to fix. Your setup is lean."));
|
|
7763
8065
|
lines.push("");
|
|
7764
8066
|
lines.push(chalk2.dim(" Exe Watcher optimize scans your Claude Code sessions and config for"));
|
|
7765
8067
|
lines.push(chalk2.dim(" token waste: junk directory reads, duplicate file reads, unused"));
|
|
@@ -7789,20 +8091,127 @@ async function runOptimize(projects, periodLabel, dateRange, preScanned) {
|
|
|
7789
8091
|
return;
|
|
7790
8092
|
}
|
|
7791
8093
|
process.stderr.write(chalk2.dim(" Analyzing your sessions...\n"));
|
|
7792
|
-
const { findings, costRate, healthScore, healthGrade } = await scanAndDetect(projects, dateRange, preScanned);
|
|
8094
|
+
const { findings, costRate, healthScore, healthGrade, coverage } = await scanAndDetect(projects, dateRange, preScanned, { allowPartial: true });
|
|
7793
8095
|
const sessions = projects.flatMap((p) => p.sessions);
|
|
7794
8096
|
const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0);
|
|
7795
8097
|
const callCount = projects.reduce((s, p) => s + p.totalApiCalls, 0);
|
|
7796
|
-
const output = renderOptimize(findings, costRate, periodLabel, periodCost, sessions.length, callCount, healthScore, healthGrade);
|
|
8098
|
+
const output = renderOptimize(findings, costRate, periodLabel, periodCost, sessions.length, callCount, healthScore, healthGrade, coverage);
|
|
7797
8099
|
console.log(output);
|
|
7798
8100
|
}
|
|
7799
8101
|
|
|
8102
|
+
// src/optimize-share.ts
|
|
8103
|
+
import { spawn as spawn2 } from "child_process";
|
|
8104
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
8105
|
+
import { stat as stat18, writeFile as writeFile5 } from "fs/promises";
|
|
8106
|
+
import { homedir as homedir14 } from "os";
|
|
8107
|
+
import { join as join26, resolve as resolve3 } from "path";
|
|
8108
|
+
function codeBlock(content, language) {
|
|
8109
|
+
const longest = (content.match(/`+/g) ?? []).reduce((max, run) => Math.max(max, run.length), 0);
|
|
8110
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
8111
|
+
return `${fence}${language}
|
|
8112
|
+
${content}
|
|
8113
|
+
${fence}`;
|
|
8114
|
+
}
|
|
8115
|
+
function formatOptimizeMarkdown(result, context, generatedAt = /* @__PURE__ */ new Date()) {
|
|
8116
|
+
const { findings, costRate, coverage } = result;
|
|
8117
|
+
const totalTokens = findings.reduce((sum2, finding) => sum2 + finding.tokensSaved, 0);
|
|
8118
|
+
const lines = [
|
|
8119
|
+
"# Exe Watcher optimizer recommendations",
|
|
8120
|
+
"",
|
|
8121
|
+
"Review these recommendations against the relevant projects and your Claude Code settings. Apply the appropriate fixes and explain what changed.",
|
|
8122
|
+
"",
|
|
8123
|
+
`Generated: ${generatedAt.toISOString()}`,
|
|
8124
|
+
`Period: ${context.periodLabel}`,
|
|
8125
|
+
"Provider: Claude Code",
|
|
8126
|
+
`Cost currency: ${getCurrency().code}`
|
|
8127
|
+
];
|
|
8128
|
+
if (context.projectFilter?.length) lines.push(`Included projects: ${context.projectFilter.join(", ")}`);
|
|
8129
|
+
if (context.excludeFilter?.length) lines.push(`Excluded projects: ${context.excludeFilter.join(", ")}`);
|
|
8130
|
+
lines.push("");
|
|
8131
|
+
if (coverage?.partial) {
|
|
8132
|
+
lines.push(
|
|
8133
|
+
`Partial analysis: ${coverage.completedFiles}/${coverage.totalFiles} logs fully checked; some records remain unchecked.`,
|
|
8134
|
+
"Cleanup suggestions requiring complete history and the overall health grade are omitted.",
|
|
8135
|
+
""
|
|
8136
|
+
);
|
|
8137
|
+
} else {
|
|
8138
|
+
lines.push(`Setup health: ${result.healthGrade} (${result.healthScore}/100).`, "");
|
|
8139
|
+
}
|
|
8140
|
+
lines.push(
|
|
8141
|
+
`Potential savings: ~${formatTokens(totalTokens)} tokens (~${formatCost(totalTokens * costRate)}).`,
|
|
8142
|
+
"Token and cost savings are estimates. Copying or saving does not apply any changes.",
|
|
8143
|
+
""
|
|
8144
|
+
);
|
|
8145
|
+
if (!findings.length) lines.push(coverage?.partial ? "No recommendations in the checked records; unchecked history is unknown." : "No recommendations found for this period.", "");
|
|
8146
|
+
findings.forEach((finding, index) => {
|
|
8147
|
+
const action = finding.fix;
|
|
8148
|
+
lines.push(
|
|
8149
|
+
`## ${index + 1}. ${finding.title}`,
|
|
8150
|
+
"",
|
|
8151
|
+
`Impact: ${finding.impact}${finding.trend === "improving" ? " \xB7 improving" : ""}`,
|
|
8152
|
+
"",
|
|
8153
|
+
finding.explanation,
|
|
8154
|
+
"",
|
|
8155
|
+
`Estimated savings: ~${formatTokens(finding.tokensSaved)} tokens (~${formatCost(finding.tokensSaved * costRate)}).`,
|
|
8156
|
+
"",
|
|
8157
|
+
"### Suggested fix",
|
|
8158
|
+
"",
|
|
8159
|
+
action.label,
|
|
8160
|
+
""
|
|
8161
|
+
);
|
|
8162
|
+
if (action.type === "file-content") lines.push("File:", codeBlock(action.path, "text"), "");
|
|
8163
|
+
lines.push(codeBlock(action.type === "file-content" ? action.content : action.text, action.type === "command" ? "sh" : "text"), "");
|
|
8164
|
+
});
|
|
8165
|
+
return lines.join("\n");
|
|
8166
|
+
}
|
|
8167
|
+
function sendToClipboard(command, args, markdown) {
|
|
8168
|
+
return new Promise((resolve4, reject) => {
|
|
8169
|
+
const child = spawn2(command, args, { stdio: ["pipe", "ignore", "ignore"], windowsHide: true });
|
|
8170
|
+
const timer = setTimeout(() => {
|
|
8171
|
+
child.kill("SIGKILL");
|
|
8172
|
+
reject(new Error("Clipboard timed out"));
|
|
8173
|
+
}, 3e3);
|
|
8174
|
+
const fail = (error) => {
|
|
8175
|
+
clearTimeout(timer);
|
|
8176
|
+
child.kill("SIGKILL");
|
|
8177
|
+
reject(error);
|
|
8178
|
+
};
|
|
8179
|
+
child.once("error", fail);
|
|
8180
|
+
child.stdin.on("error", fail);
|
|
8181
|
+
child.once("close", (code) => {
|
|
8182
|
+
clearTimeout(timer);
|
|
8183
|
+
if (code === 0) resolve4();
|
|
8184
|
+
else reject(new Error("Clipboard unavailable"));
|
|
8185
|
+
});
|
|
8186
|
+
child.stdin.end(process.platform === "win32" ? Buffer.concat([Buffer.from([255, 254]), Buffer.from(markdown, "utf16le")]) : Buffer.from(markdown, "utf8"));
|
|
8187
|
+
});
|
|
8188
|
+
}
|
|
8189
|
+
async function copyOptimizeMarkdown(markdown) {
|
|
8190
|
+
const commands = process.platform === "darwin" ? [["/usr/bin/pbcopy", []]] : process.platform === "win32" ? [["clip.exe", []]] : process.platform === "linux" ? [["wl-copy", ["--type", "text/plain;charset=utf-8"]], ["xclip", ["-selection", "clipboard"]]] : [];
|
|
8191
|
+
for (const [command, args] of commands) {
|
|
8192
|
+
try {
|
|
8193
|
+
await sendToClipboard(command, args, markdown);
|
|
8194
|
+
return;
|
|
8195
|
+
} catch {
|
|
8196
|
+
}
|
|
8197
|
+
}
|
|
8198
|
+
throw new Error("Could not copy. Press e to save a Markdown file instead.");
|
|
8199
|
+
}
|
|
8200
|
+
async function saveOptimizeMarkdown(markdown, directory) {
|
|
8201
|
+
const downloads = join26(homedir14(), "Downloads");
|
|
8202
|
+
const folder = directory ?? ((await stat18(downloads).catch(() => null))?.isDirectory() ? downloads : process.cwd());
|
|
8203
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
8204
|
+
const path = resolve3(folder, `exe-watcher-optimizer-${stamp}-${randomUUID4().slice(0, 8)}.md`);
|
|
8205
|
+
await writeFile5(path, markdown, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
8206
|
+
return path;
|
|
8207
|
+
}
|
|
8208
|
+
|
|
7800
8209
|
// src/context-budget.ts
|
|
7801
8210
|
init_fs_utils();
|
|
7802
|
-
import { readdir as
|
|
8211
|
+
import { readdir as readdir14 } from "fs/promises";
|
|
7803
8212
|
import { existsSync as existsSync6 } from "fs";
|
|
7804
|
-
import { join as
|
|
7805
|
-
import { homedir as
|
|
8213
|
+
import { join as join27 } from "path";
|
|
8214
|
+
import { homedir as homedir15 } from "os";
|
|
7806
8215
|
var CHARS_PER_TOKEN2 = 4;
|
|
7807
8216
|
var SYSTEM_BASE_TOKENS = 10400;
|
|
7808
8217
|
var TOOL_TOKENS_OVERHEAD = 400;
|
|
@@ -7821,15 +8230,15 @@ async function readConfigFile(path) {
|
|
|
7821
8230
|
}
|
|
7822
8231
|
}
|
|
7823
8232
|
async function countMcpTools(projectPath) {
|
|
7824
|
-
const home =
|
|
8233
|
+
const home = homedir15();
|
|
7825
8234
|
const configPaths = [
|
|
7826
|
-
|
|
7827
|
-
|
|
8235
|
+
join27(home, ".claude", "settings.json"),
|
|
8236
|
+
join27(home, ".claude", "settings.local.json")
|
|
7828
8237
|
];
|
|
7829
8238
|
if (projectPath) {
|
|
7830
|
-
configPaths.push(
|
|
7831
|
-
configPaths.push(
|
|
7832
|
-
configPaths.push(
|
|
8239
|
+
configPaths.push(join27(projectPath, ".mcp.json"));
|
|
8240
|
+
configPaths.push(join27(projectPath, ".claude", "settings.json"));
|
|
8241
|
+
configPaths.push(join27(projectPath, ".claude", "settings.local.json"));
|
|
7833
8242
|
}
|
|
7834
8243
|
const servers = /* @__PURE__ */ new Set();
|
|
7835
8244
|
let toolCount = 0;
|
|
@@ -7846,15 +8255,15 @@ async function countMcpTools(projectPath) {
|
|
|
7846
8255
|
return toolCount;
|
|
7847
8256
|
}
|
|
7848
8257
|
async function countSkills(projectPath) {
|
|
7849
|
-
const dirs = [
|
|
7850
|
-
if (projectPath) dirs.push(
|
|
8258
|
+
const dirs = [join27(homedir15(), ".claude", "skills")];
|
|
8259
|
+
if (projectPath) dirs.push(join27(projectPath, ".claude", "skills"));
|
|
7851
8260
|
let count = 0;
|
|
7852
8261
|
for (const dir of dirs) {
|
|
7853
8262
|
if (!existsSync6(dir)) continue;
|
|
7854
8263
|
try {
|
|
7855
|
-
const entries = await
|
|
8264
|
+
const entries = await readdir14(dir);
|
|
7856
8265
|
for (const entry of entries) {
|
|
7857
|
-
const skillFile =
|
|
8266
|
+
const skillFile = join27(dir, entry, "SKILL.md");
|
|
7858
8267
|
if (existsSync6(skillFile)) count++;
|
|
7859
8268
|
}
|
|
7860
8269
|
} catch {
|
|
@@ -7864,15 +8273,15 @@ async function countSkills(projectPath) {
|
|
|
7864
8273
|
return count;
|
|
7865
8274
|
}
|
|
7866
8275
|
async function scanMemoryFiles(projectPath) {
|
|
7867
|
-
const home =
|
|
8276
|
+
const home = homedir15();
|
|
7868
8277
|
const files = [];
|
|
7869
8278
|
const paths = [
|
|
7870
|
-
{ path:
|
|
8279
|
+
{ path: join27(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
|
|
7871
8280
|
];
|
|
7872
8281
|
if (projectPath) {
|
|
7873
|
-
paths.push({ path:
|
|
7874
|
-
paths.push({ path:
|
|
7875
|
-
paths.push({ path:
|
|
8282
|
+
paths.push({ path: join27(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
|
|
8283
|
+
paths.push({ path: join27(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
|
|
8284
|
+
paths.push({ path: join27(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
|
|
7876
8285
|
}
|
|
7877
8286
|
for (const { path, name } of paths) {
|
|
7878
8287
|
if (!existsSync6(path)) continue;
|
|
@@ -7902,14 +8311,14 @@ async function estimateContextBudget(projectPath, modelContext = 1e6) {
|
|
|
7902
8311
|
async function discoverProjectCwd(sessionDir) {
|
|
7903
8312
|
let files;
|
|
7904
8313
|
try {
|
|
7905
|
-
files = (await
|
|
8314
|
+
files = (await readdir14(sessionDir)).filter((f) => f.endsWith(".jsonl"));
|
|
7906
8315
|
} catch {
|
|
7907
8316
|
return null;
|
|
7908
8317
|
}
|
|
7909
8318
|
if (files.length === 0) return null;
|
|
7910
8319
|
const firstFile = files[0];
|
|
7911
8320
|
if (!firstFile) return null;
|
|
7912
|
-
const content = await readSessionFile(
|
|
8321
|
+
const content = await readSessionFile(join27(sessionDir, firstFile));
|
|
7913
8322
|
if (content === null) return null;
|
|
7914
8323
|
for (const line of content.split("\n")) {
|
|
7915
8324
|
if (!line.trim()) continue;
|
|
@@ -7975,20 +8384,20 @@ function ModelSelector({ models, onSelect, onBack }) {
|
|
|
7975
8384
|
const { exit } = useApp();
|
|
7976
8385
|
const [cursor2, setCursor] = useState(0);
|
|
7977
8386
|
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
7978
|
-
useInput((input,
|
|
8387
|
+
useInput((input, key2) => {
|
|
7979
8388
|
if (input === "q") {
|
|
7980
8389
|
exit();
|
|
7981
8390
|
return;
|
|
7982
8391
|
}
|
|
7983
|
-
if (
|
|
8392
|
+
if (key2.escape) {
|
|
7984
8393
|
onBack();
|
|
7985
8394
|
return;
|
|
7986
8395
|
}
|
|
7987
|
-
if (
|
|
8396
|
+
if (key2.upArrow) {
|
|
7988
8397
|
setCursor((c) => (c - 1 + models.length) % models.length);
|
|
7989
8398
|
return;
|
|
7990
8399
|
}
|
|
7991
|
-
if (
|
|
8400
|
+
if (key2.downArrow) {
|
|
7992
8401
|
setCursor((c) => (c + 1) % models.length);
|
|
7993
8402
|
return;
|
|
7994
8403
|
}
|
|
@@ -8004,7 +8413,7 @@ function ModelSelector({ models, onSelect, onBack }) {
|
|
|
8004
8413
|
});
|
|
8005
8414
|
return;
|
|
8006
8415
|
}
|
|
8007
|
-
if (
|
|
8416
|
+
if (key2.return && selected.size === 2) {
|
|
8008
8417
|
const indices = [...selected].sort((a, b) => a - b);
|
|
8009
8418
|
onSelect(models[indices[0]], models[indices[1]]);
|
|
8010
8419
|
}
|
|
@@ -8094,12 +8503,12 @@ function ComparisonResults({ modelA, modelB, rows: rows2, categories, workingSty
|
|
|
8094
8503
|
const nameB = shortName(modelB.model);
|
|
8095
8504
|
const lowDataA = modelA.calls < LOW_DATA_THRESHOLD;
|
|
8096
8505
|
const lowDataB = modelB.calls < LOW_DATA_THRESHOLD;
|
|
8097
|
-
useInput((input,
|
|
8506
|
+
useInput((input, key2) => {
|
|
8098
8507
|
if (input === "q") {
|
|
8099
8508
|
exit();
|
|
8100
8509
|
return;
|
|
8101
8510
|
}
|
|
8102
|
-
if (
|
|
8511
|
+
if (key2.escape) {
|
|
8103
8512
|
onBack();
|
|
8104
8513
|
return;
|
|
8105
8514
|
}
|
|
@@ -8274,14 +8683,14 @@ function CompareView({ projects, onBack }) {
|
|
|
8274
8683
|
cancelled = true;
|
|
8275
8684
|
};
|
|
8276
8685
|
}, [loadTrigger]);
|
|
8277
|
-
useInput((input,
|
|
8686
|
+
useInput((input, key2) => {
|
|
8278
8687
|
if (phase !== "select") return;
|
|
8279
8688
|
if (models.length < 2) {
|
|
8280
8689
|
if (input === "q") {
|
|
8281
8690
|
exit();
|
|
8282
8691
|
return;
|
|
8283
8692
|
}
|
|
8284
|
-
if (
|
|
8693
|
+
if (key2.escape) {
|
|
8285
8694
|
onBack();
|
|
8286
8695
|
return;
|
|
8287
8696
|
}
|
|
@@ -8408,7 +8817,7 @@ function projectMonthEnd(projects, periodStart, periodEnd, today, spent) {
|
|
|
8408
8817
|
if (Number.isNaN(ts.getTime())) continue;
|
|
8409
8818
|
if (ts < periodStart || ts > today) continue;
|
|
8410
8819
|
const dayKey = toLocalDateKey(ts);
|
|
8411
|
-
const turnCost = turn.assistantCalls.reduce((
|
|
8820
|
+
const turnCost = turn.assistantCalls.reduce((sum2, call) => sum2 + call.costUSD, 0);
|
|
8412
8821
|
dayCosts.set(dayKey, (dayCosts.get(dayKey) ?? 0) + turnCost);
|
|
8413
8822
|
}
|
|
8414
8823
|
}
|
|
@@ -8416,8 +8825,8 @@ function projectMonthEnd(projects, periodStart, periodEnd, today, spent) {
|
|
|
8416
8825
|
const elapsedDays = Math.max(1, diffCalendarDays(periodStart, today) + 1);
|
|
8417
8826
|
const elapsedDailyCosts = [];
|
|
8418
8827
|
for (let i = 0; i < elapsedDays; i++) {
|
|
8419
|
-
const
|
|
8420
|
-
elapsedDailyCosts.push(dayCosts.get(toLocalDateKey(
|
|
8828
|
+
const date2 = new Date(periodStart.getFullYear(), periodStart.getMonth(), periodStart.getDate() + i);
|
|
8829
|
+
elapsedDailyCosts.push(dayCosts.get(toLocalDateKey(date2)) ?? 0);
|
|
8421
8830
|
}
|
|
8422
8831
|
const trailingWindow = elapsedDailyCosts.slice(-7);
|
|
8423
8832
|
const medianDailyCost = median(trailingWindow);
|
|
@@ -8426,7 +8835,7 @@ function projectMonthEnd(projects, periodStart, periodEnd, today, spent) {
|
|
|
8426
8835
|
}
|
|
8427
8836
|
function getPlanUsageFromProjects(plan, projects, today = /* @__PURE__ */ new Date()) {
|
|
8428
8837
|
const { periodStart, periodEnd } = computePeriodFromResetDay(plan.resetDay, today);
|
|
8429
|
-
const spent = projects.reduce((
|
|
8838
|
+
const spent = projects.reduce((sum2, p) => sum2 + p.totalCostUSD, 0);
|
|
8430
8839
|
const budgetUsd = plan.monthlyUsd;
|
|
8431
8840
|
const percentUsed = budgetUsd > 0 ? spent / budgetUsd * 100 : 0;
|
|
8432
8841
|
const status = percentUsed > 100 ? "over" : percentUsed >= PLAN_NEAR_THRESHOLD_PCT ? "near" : "under";
|
|
@@ -8513,8 +8922,732 @@ function planDisplayName(id) {
|
|
|
8513
8922
|
}
|
|
8514
8923
|
}
|
|
8515
8924
|
|
|
8925
|
+
// src/owl-art.ts
|
|
8926
|
+
var OWL_FRAMES = {
|
|
8927
|
+
center: [
|
|
8928
|
+
" \u2584 \u2584",
|
|
8929
|
+
" \u2588\u2588\u2588\u2584 \u2584\u2588\u2588\u2588",
|
|
8930
|
+
" \u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2588",
|
|
8931
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8932
|
+
" \u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588 ",
|
|
8933
|
+
" \u2588\u2588 \u2584\u2588\u2584 \u2588\u2588 \u2584\u2588\u2584 \u2588\u2588 ",
|
|
8934
|
+
" \u2588\u2588 \u2588\u2584\u2588 \u2588\u2588 \u2588\u2584\u2588 \u2588\u2588 ",
|
|
8935
|
+
" \u2588\u2588\u2588 \u2580 \u2584\u2588\u2588\u2584 \u2580 \u2588\u2588\u2588 ",
|
|
8936
|
+
" \u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2588\u2588\u2588\u2588 ",
|
|
8937
|
+
" \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 ",
|
|
8938
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8939
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8940
|
+
" \u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588 ",
|
|
8941
|
+
" \u2588\u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588\u2588 ",
|
|
8942
|
+
" \u2580\u2588\u2588\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2588\u2588\u2580 ",
|
|
8943
|
+
" \u2580\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2580 ",
|
|
8944
|
+
" \u2584\u2588\u2588\u2588\u2588\u2580\u2580\u2580\u2580\u2588\u2588\u2588\u2588\u2584 ",
|
|
8945
|
+
" \u2580 \u2580 "
|
|
8946
|
+
],
|
|
8947
|
+
left: [
|
|
8948
|
+
" \u2584 \u2584",
|
|
8949
|
+
" \u2588\u2588\u2588\u2584 \u2584\u2588\u2588\u2588",
|
|
8950
|
+
" \u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2588",
|
|
8951
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8952
|
+
" \u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588 ",
|
|
8953
|
+
" \u2588\u2588 \u2584\u2588\u2584 \u2588\u2588\u2588\u2588\u2584 \u2588\u2588 ",
|
|
8954
|
+
" \u2588\u2588 \u2588\u2584\u2588 \u2588\u2588\u2588\u2584\u2588 \u2588\u2588 ",
|
|
8955
|
+
" \u2588\u2588\u2588 \u2580 \u2584\u2588\u2588\u2588\u2580 \u2588\u2588\u2588 ",
|
|
8956
|
+
" \u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2588\u2588\u2588\u2588 ",
|
|
8957
|
+
" \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 ",
|
|
8958
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8959
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8960
|
+
" \u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588 ",
|
|
8961
|
+
" \u2588\u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588\u2588 ",
|
|
8962
|
+
" \u2580\u2588\u2588\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2588\u2588\u2580 ",
|
|
8963
|
+
" \u2580\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2580 ",
|
|
8964
|
+
" \u2584\u2588\u2588\u2588\u2588\u2580\u2580\u2580\u2580\u2588\u2588\u2588\u2588\u2584 ",
|
|
8965
|
+
" \u2580 \u2580 "
|
|
8966
|
+
],
|
|
8967
|
+
right: [
|
|
8968
|
+
" \u2584 \u2584",
|
|
8969
|
+
" \u2588\u2588\u2588\u2584 \u2584\u2588\u2588\u2588",
|
|
8970
|
+
" \u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2588",
|
|
8971
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8972
|
+
" \u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588\u2588\u2580 \u2580\u2588\u2588\u2588 ",
|
|
8973
|
+
" \u2588\u2588 \u2584\u2588\u2588\u2588\u2588 \u2584\u2588\u2584 \u2588\u2588 ",
|
|
8974
|
+
" \u2588\u2588 \u2588\u2584\u2588\u2588\u2588 \u2588\u2584\u2588 \u2588\u2588 ",
|
|
8975
|
+
" \u2588\u2588\u2588 \u2580\u2588\u2588\u2588\u2584 \u2580 \u2588\u2588\u2588 ",
|
|
8976
|
+
" \u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2588\u2588\u2588\u2588 ",
|
|
8977
|
+
" \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 ",
|
|
8978
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8979
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8980
|
+
" \u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588 ",
|
|
8981
|
+
" \u2588\u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588\u2588 ",
|
|
8982
|
+
" \u2580\u2588\u2588\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2588\u2588\u2580 ",
|
|
8983
|
+
" \u2580\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2580 ",
|
|
8984
|
+
" \u2584\u2588\u2588\u2588\u2588\u2580\u2580\u2580\u2580\u2588\u2588\u2588\u2588\u2584 ",
|
|
8985
|
+
" \u2580 \u2580 "
|
|
8986
|
+
],
|
|
8987
|
+
blink: [
|
|
8988
|
+
" \u2584 \u2584",
|
|
8989
|
+
" \u2588\u2588\u2588\u2584 \u2584\u2588\u2588\u2588",
|
|
8990
|
+
" \u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2588\u2588\u2588",
|
|
8991
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8992
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8993
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8994
|
+
" \u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u2588 ",
|
|
8995
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8996
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8997
|
+
" \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 ",
|
|
8998
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
8999
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
|
|
9000
|
+
" \u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588 ",
|
|
9001
|
+
" \u2588\u2588 \u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580 \u2588\u2588 ",
|
|
9002
|
+
" \u2580\u2588\u2588\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2588\u2588\u2580 ",
|
|
9003
|
+
" \u2580\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2580 ",
|
|
9004
|
+
" \u2584\u2588\u2588\u2588\u2588\u2580\u2580\u2580\u2580\u2588\u2588\u2588\u2588\u2584 ",
|
|
9005
|
+
" \u2580 \u2580 "
|
|
9006
|
+
]
|
|
9007
|
+
};
|
|
9008
|
+
var OWL_LOOK = ["center", "center", "left", "left", "center", "right", "right", "center", "blink", "center"];
|
|
9009
|
+
function owlFrame(tick) {
|
|
9010
|
+
const i = (Math.floor(tick) % OWL_LOOK.length + OWL_LOOK.length) % OWL_LOOK.length;
|
|
9011
|
+
return OWL_FRAMES[OWL_LOOK[i]];
|
|
9012
|
+
}
|
|
9013
|
+
|
|
9014
|
+
// src/dashboard.tsx
|
|
9015
|
+
import { join as join29 } from "path";
|
|
9016
|
+
|
|
9017
|
+
// src/time-index.ts
|
|
9018
|
+
init_cache_dir();
|
|
9019
|
+
init_file_lock();
|
|
9020
|
+
init_resource_budget();
|
|
9021
|
+
import { createHash as createHash8, randomUUID as randomUUID5 } from "crypto";
|
|
9022
|
+
import { mkdir as mkdir13, open as open9, readFile as readFile14, rename as rename11, stat as stat19, unlink as unlink9, readdir as readdir15 } from "fs/promises";
|
|
9023
|
+
import { basename as basename11, dirname as dirname6, join as join28 } from "path";
|
|
9024
|
+
import { gzip as gzip2, gunzip as gunzip2 } from "zlib";
|
|
9025
|
+
import { promisify as promisify2 } from "util";
|
|
9026
|
+
|
|
9027
|
+
// src/time-tracking.ts
|
|
9028
|
+
import { createHash as createHash7 } from "crypto";
|
|
9029
|
+
var TIME_HISTORY_START = (/* @__PURE__ */ new Date("2025-07-01T00:00:00")).getTime();
|
|
9030
|
+
var TIME_PROVIDERS = ["claude", "codex", "cursor", "cursor-agent", "copilot", "opencode", "kimi", "pi", "omp"];
|
|
9031
|
+
function timestamp(value) {
|
|
9032
|
+
const n = typeof value === "number" ? value < 1e11 ? value * 1e3 : value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
9033
|
+
return Number.isFinite(n) && n >= TIME_HISTORY_START && n <= Date.now() + 864e5 ? n : void 0;
|
|
9034
|
+
}
|
|
9035
|
+
function add(list, start, end, estimated, recordId) {
|
|
9036
|
+
if (start === void 0 || end === void 0 || end <= start || end - start > 31 * 864e5) return;
|
|
9037
|
+
list.push(recordId ? [start, end, estimated, recordId] : [start, end, estimated]);
|
|
9038
|
+
}
|
|
9039
|
+
function prompt(state, at) {
|
|
9040
|
+
if (at === void 0) return;
|
|
9041
|
+
finishInferred(state);
|
|
9042
|
+
if (!state.child && state.lastEnd !== void 0 && at > state.lastEnd && at - state.lastEnd <= 5 * 6e4) {
|
|
9043
|
+
add(state.user, state.lastEnd, at, 1);
|
|
9044
|
+
}
|
|
9045
|
+
state.pending = at;
|
|
9046
|
+
state.lastAssistant = void 0;
|
|
9047
|
+
}
|
|
9048
|
+
function finishInferred(state) {
|
|
9049
|
+
if (state.pending !== void 0 && state.lastAssistant !== void 0) {
|
|
9050
|
+
if (state.lastAssistant - state.pending <= 30 * 6e4) add(state.ai, state.pending, state.lastAssistant, 1, state.pendingId);
|
|
9051
|
+
else state.dropped++;
|
|
9052
|
+
state.lastEnd = state.lastAssistant;
|
|
9053
|
+
}
|
|
9054
|
+
state.pending = void 0;
|
|
9055
|
+
state.pendingId = void 0;
|
|
9056
|
+
state.lastAssistant = void 0;
|
|
9057
|
+
}
|
|
9058
|
+
function complete(state, start, end, estimated = 0, recordId) {
|
|
9059
|
+
add(state.ai, start, end, estimated, recordId ?? state.pendingId);
|
|
9060
|
+
if (end !== void 0) state.lastEnd = end;
|
|
9061
|
+
state.pending = void 0;
|
|
9062
|
+
state.pendingId = void 0;
|
|
9063
|
+
state.lastAssistant = void 0;
|
|
9064
|
+
}
|
|
9065
|
+
function observeTime(provider, state, x) {
|
|
9066
|
+
if (!x || typeof x !== "object") return;
|
|
9067
|
+
const at = timestamp(x.timestamp ?? x.time);
|
|
9068
|
+
if (provider === "codex") {
|
|
9069
|
+
const p = x.payload ?? {};
|
|
9070
|
+
if (x.type === "session_meta" && typeof p.id === "string") {
|
|
9071
|
+
state.agent = `codex:${p.id}`;
|
|
9072
|
+
state.child = typeof p.source === "object" && p.source !== null && "subagent" in p.source;
|
|
9073
|
+
}
|
|
9074
|
+
if (x.type !== "event_msg") return;
|
|
9075
|
+
if (p.type === "task_started" || p.type === "user_message") {
|
|
9076
|
+
if (p.type === "task_started" || state.pending === void 0) {
|
|
9077
|
+
state.observed++;
|
|
9078
|
+
prompt(state, timestamp(p.started_at) ?? at);
|
|
9079
|
+
state.pendingId = typeof p.turn_id === "string" ? p.turn_id : void 0;
|
|
9080
|
+
}
|
|
9081
|
+
} else if (p.type === "agent_message" && p.phase !== "commentary") state.lastAssistant = at;
|
|
9082
|
+
else if (p.type === "task_complete" || p.type === "turn_aborted") {
|
|
9083
|
+
const end = timestamp(p.completed_at) ?? at;
|
|
9084
|
+
const duration = typeof p.duration_ms === "number" && Number.isFinite(p.duration_ms) && p.duration_ms > 0 ? p.duration_ms : void 0;
|
|
9085
|
+
const start = timestamp(p.started_at) ?? (end !== void 0 && duration ? end - duration : state.pending);
|
|
9086
|
+
complete(state, start, end, duration || timestamp(p.started_at) ? 0 : 1, typeof p.turn_id === "string" ? p.turn_id : void 0);
|
|
9087
|
+
}
|
|
9088
|
+
return;
|
|
9089
|
+
}
|
|
9090
|
+
if (provider === "claude") {
|
|
9091
|
+
if (typeof x.sessionId === "string") state.agent = `claude:${x.sessionId}${state.child ? ":" + state.agent.split(":").at(-1) : ""}`;
|
|
9092
|
+
if (x.type === "system" && x.subtype === "turn_duration") {
|
|
9093
|
+
const duration = typeof x.durationMs === "number" && Number.isFinite(x.durationMs) && x.durationMs > 0 ? x.durationMs : void 0;
|
|
9094
|
+
complete(state, duration && at !== void 0 ? at - duration : void 0, at, 0, typeof x.uuid === "string" ? x.uuid : void 0);
|
|
9095
|
+
} else if (x.type === "user" && !x.isMeta && (typeof x.message?.content === "string" || x.message?.content?.some?.((b) => b?.type === "text"))) {
|
|
9096
|
+
state.observed++;
|
|
9097
|
+
prompt(state, at);
|
|
9098
|
+
state.pendingId = typeof x.uuid === "string" ? x.uuid : void 0;
|
|
9099
|
+
} else if (x.type === "assistant") {
|
|
9100
|
+
state.observed++;
|
|
9101
|
+
state.lastAssistant = at ?? state.lastAssistant;
|
|
9102
|
+
}
|
|
9103
|
+
return;
|
|
9104
|
+
}
|
|
9105
|
+
if (provider === "copilot") {
|
|
9106
|
+
if (x.type === "assistant.turn_start") {
|
|
9107
|
+
state.observed++;
|
|
9108
|
+
state.pending = at;
|
|
9109
|
+
state.lastAssistant = void 0;
|
|
9110
|
+
state.pendingId = typeof x.data?.turnId === "string" ? x.data.turnId : typeof x.id === "string" ? x.id : void 0;
|
|
9111
|
+
}
|
|
9112
|
+
if (x.type === "assistant.turn_end") complete(state, state.pending, at);
|
|
9113
|
+
if (x.type === "user.message") {
|
|
9114
|
+
state.observed++;
|
|
9115
|
+
prompt(state, at);
|
|
9116
|
+
}
|
|
9117
|
+
if (x.type === "assistant.message") state.lastAssistant = at ?? state.lastAssistant;
|
|
9118
|
+
return;
|
|
9119
|
+
}
|
|
9120
|
+
if (provider === "kimi") {
|
|
9121
|
+
const event = x.message ?? x.event ?? x.params ?? x;
|
|
9122
|
+
const type = event.type ?? x.type;
|
|
9123
|
+
if (x.type === "turn.prompt") {
|
|
9124
|
+
state.observed++;
|
|
9125
|
+
prompt(state, at);
|
|
9126
|
+
} else if (type === "TurnBegin" || type === "turn.begin" || type === "turn.start") {
|
|
9127
|
+
state.observed++;
|
|
9128
|
+
prompt(state, at);
|
|
9129
|
+
} else if (type === "step.begin") {
|
|
9130
|
+
state.observed++;
|
|
9131
|
+
state.pending = at;
|
|
9132
|
+
state.lastAssistant = void 0;
|
|
9133
|
+
state.pendingId = typeof event.uuid === "string" ? `${event.uuid}:${at}` : void 0;
|
|
9134
|
+
} else if (type === "TurnEnd" || type === "turn.end" || type === "step.end") complete(state, state.pending, at);
|
|
9135
|
+
else if (x.type === "usage.record") state.observed++;
|
|
9136
|
+
return;
|
|
9137
|
+
}
|
|
9138
|
+
if (provider === "pi" || provider === "omp") {
|
|
9139
|
+
if (x.type === "session" && typeof x.id === "string") {
|
|
9140
|
+
state.agent = `${provider}:${x.id}`;
|
|
9141
|
+
if (x.parentSession) state.child = true;
|
|
9142
|
+
}
|
|
9143
|
+
if (x.type === "message" && x.message?.role === "user") {
|
|
9144
|
+
state.observed++;
|
|
9145
|
+
prompt(state, at);
|
|
9146
|
+
state.pendingId = typeof x.id === "string" ? `${x.id}:${at}` : void 0;
|
|
9147
|
+
} else if (x.type === "message" && x.message?.role === "assistant") {
|
|
9148
|
+
state.observed++;
|
|
9149
|
+
state.lastAssistant = at ?? state.lastAssistant;
|
|
9150
|
+
if (x.message.stopReason === "stop") {
|
|
9151
|
+
finishInferred(state);
|
|
9152
|
+
}
|
|
9153
|
+
}
|
|
9154
|
+
return;
|
|
9155
|
+
}
|
|
9156
|
+
if ((provider === "cursor" || provider === "opencode") && x.type === "user") {
|
|
9157
|
+
state.observed++;
|
|
9158
|
+
prompt(state, at);
|
|
9159
|
+
return;
|
|
9160
|
+
}
|
|
9161
|
+
if ((provider === "cursor-agent" || provider === "cursor") && x.type === "result") {
|
|
9162
|
+
state.observed++;
|
|
9163
|
+
const end = timestamp(x.timestamp ?? x.completed_at);
|
|
9164
|
+
if (typeof x.duration_ms === "number" && end !== void 0) complete(state, end - x.duration_ms, end);
|
|
9165
|
+
}
|
|
9166
|
+
}
|
|
9167
|
+
function unionIntervals(intervals, range) {
|
|
9168
|
+
const start = Math.max(TIME_HISTORY_START, range?.start.getTime() ?? TIME_HISTORY_START);
|
|
9169
|
+
const end = Math.min(range?.end.getTime() ?? Date.now(), Date.now());
|
|
9170
|
+
const sorted = intervals.filter((x) => Number.isFinite(x[0]) && Number.isFinite(x[1]) && x[1] > x[0]).map(([a, b, estimate]) => [Math.max(a, start), Math.min(b, end), estimate]).filter(([a, b]) => b > a).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
9171
|
+
const out = [];
|
|
9172
|
+
for (const row of sorted) {
|
|
9173
|
+
const last = out.at(-1);
|
|
9174
|
+
if (last && row[0] <= last[1]) {
|
|
9175
|
+
last[1] = Math.max(last[1], row[1]);
|
|
9176
|
+
last[2] = Math.max(last[2], row[2]);
|
|
9177
|
+
} else out.push([...row]);
|
|
9178
|
+
}
|
|
9179
|
+
return out;
|
|
9180
|
+
}
|
|
9181
|
+
var sum = (rows2) => rows2.reduce((n, [a, b]) => n + b - a, 0);
|
|
9182
|
+
function summarizeTime(index, range, provider = "all", include, exclude) {
|
|
9183
|
+
const matches = (name, terms) => terms?.some((t) => name.toLowerCase().includes(t.toLowerCase()));
|
|
9184
|
+
const files = Object.values(index.files).filter((f) => (provider === "all" || f.provider === provider) && (!include?.length || matches(f.project, include)) && !matches(f.project, exclude));
|
|
9185
|
+
const agents = /* @__PURE__ */ new Map(), estimatedAgents = /* @__PURE__ */ new Map(), user = [];
|
|
9186
|
+
const records = /* @__PURE__ */ new Map();
|
|
9187
|
+
const states = /* @__PURE__ */ new Map();
|
|
9188
|
+
for (const file of files) {
|
|
9189
|
+
const copies = [file.state, ...Object.values(file.state.children ?? {})].map((original) => {
|
|
9190
|
+
const state = { ...original, ai: [...original.ai] };
|
|
9191
|
+
finishInferred(state);
|
|
9192
|
+
for (const interval2 of state.ai) {
|
|
9193
|
+
if (!interval2[3]) continue;
|
|
9194
|
+
const key2 = `${file.provider}:${interval2[3]}`, prior = records.get(key2);
|
|
9195
|
+
if (!prior || interval2[2] < prior[2] || interval2[2] === prior[2] && interval2[1] - interval2[0] > prior[1] - prior[0]) records.set(key2, interval2);
|
|
9196
|
+
}
|
|
9197
|
+
return state;
|
|
9198
|
+
});
|
|
9199
|
+
states.set(file, copies);
|
|
9200
|
+
}
|
|
9201
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
9202
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
9203
|
+
let pendingFiles = 0;
|
|
9204
|
+
for (const file of files) {
|
|
9205
|
+
if (file.offset < file.size || file.unavailable) pendingFiles++;
|
|
9206
|
+
const row = byProvider.get(file.provider) ?? { files: 0, timedFiles: 0, agents: /* @__PURE__ */ new Map() };
|
|
9207
|
+
row.files++;
|
|
9208
|
+
let timed = false;
|
|
9209
|
+
for (const state of states.get(file)) {
|
|
9210
|
+
timed ||= unionIntervals(state.ai, range).length > 0;
|
|
9211
|
+
const unique = state.ai.filter((interval2) => {
|
|
9212
|
+
if (!interval2[3]) return true;
|
|
9213
|
+
const key2 = `${file.provider}:${interval2[3]}`;
|
|
9214
|
+
if (records.get(key2) !== interval2 || consumed.has(key2)) return false;
|
|
9215
|
+
consumed.add(key2);
|
|
9216
|
+
return true;
|
|
9217
|
+
});
|
|
9218
|
+
const ai = unionIntervals(unique, range);
|
|
9219
|
+
const inferred = estimatedAgents.get(state.agent) ?? [];
|
|
9220
|
+
inferred.push(...unique.filter((interval2) => interval2[2]));
|
|
9221
|
+
estimatedAgents.set(state.agent, inferred);
|
|
9222
|
+
for (const map of [agents, row.agents]) {
|
|
9223
|
+
const list = map.get(state.agent) ?? [];
|
|
9224
|
+
for (const interval2 of ai) list.push(interval2);
|
|
9225
|
+
map.set(state.agent, list);
|
|
9226
|
+
}
|
|
9227
|
+
for (const interval2 of state.user) user.push(interval2);
|
|
9228
|
+
}
|
|
9229
|
+
if (timed) row.timedFiles++;
|
|
9230
|
+
byProvider.set(file.provider, row);
|
|
9231
|
+
}
|
|
9232
|
+
const all = [...agents.values()].flatMap((v) => unionIntervals(v, range));
|
|
9233
|
+
const estimated = [...estimatedAgents.values()].flatMap((v) => unionIntervals(v, range));
|
|
9234
|
+
const busy = unionIntervals(all, range), human = unionIntervals(user, range);
|
|
9235
|
+
const pending = files.filter((f) => f.offset < f.size || f.unavailable);
|
|
9236
|
+
const waitingFiles = pending.filter((f) => f.waitingForAppend && !f.unavailable).length;
|
|
9237
|
+
const recovery = {
|
|
9238
|
+
runnableFiles: pending.length - waitingFiles,
|
|
9239
|
+
waitingFiles,
|
|
9240
|
+
pendingBytes: pending.reduce((n, f) => n + Math.max(0, f.size - f.offset), 0),
|
|
9241
|
+
token: createHash7("sha256").update(files.map((f) => `${f.identity}:${f.size}:${f.mtime}:${f.offset}:${f.dbOffset ?? 0}:${!!f.waitingForAppend}:${!!f.unavailable}`).join("|")).digest("hex")
|
|
9242
|
+
};
|
|
9243
|
+
return {
|
|
9244
|
+
indexedAt: index.updatedAt,
|
|
9245
|
+
recovery,
|
|
9246
|
+
since: new Date(Math.max(TIME_HISTORY_START, range.start.getTime())).toISOString(),
|
|
9247
|
+
through: range.end.toISOString(),
|
|
9248
|
+
aiRunningMs: all.length ? sum(all) : null,
|
|
9249
|
+
elapsedBusyMs: busy.length ? sum(busy) : null,
|
|
9250
|
+
userActiveMs: human.length ? sum(human) : null,
|
|
9251
|
+
estimatedAiMs: sum(estimated),
|
|
9252
|
+
coverage: all.length ? "partial" : "missing",
|
|
9253
|
+
pendingFiles,
|
|
9254
|
+
indexedFiles: files.length,
|
|
9255
|
+
firstRecordedAt: busy.length ? new Date(busy[0][0]).toISOString() : null,
|
|
9256
|
+
lastRecordedAt: busy.length ? new Date(busy.at(-1)[1]).toISOString() : null,
|
|
9257
|
+
providers: (provider === "all" ? TIME_PROVIDERS : [provider]).map((name) => {
|
|
9258
|
+
const row = byProvider.get(name);
|
|
9259
|
+
const duration = row ? [...row.agents.values()].reduce((n, a) => n + sum(unionIntervals(a, range)), 0) : 0;
|
|
9260
|
+
return { name, files: row?.files ?? 0, timedFiles: row?.timedFiles ?? 0, aiRunningMs: duration || null, status: duration ? "partial" : "missing" };
|
|
9261
|
+
}),
|
|
9262
|
+
notes: [
|
|
9263
|
+
"Retained local records only, since July 1, 2025. Missing history is unknown, not zero. Run time can include tools and waiting.",
|
|
9264
|
+
"Your time is an estimate of reply-to-next-prompt gaps up to 5 minutes, merged across chats. Longer idle gaps and subagents are excluded; unidentified automation may be included. It does not measure attention.",
|
|
9265
|
+
...estimated.length ? ["Some AI time is inferred from prompt/response timestamps, limited to spans up to 30 minutes. Longer unmeasured gaps stay unknown."] : [],
|
|
9266
|
+
...recovery.runnableFiles ? ["Timing recovery is in progress. Queued timing work continues in bounded steps; usage costs are unaffected."] : [],
|
|
9267
|
+
...waitingFiles ? ["Some logs end mid-record. Their incomplete records remain unknown until more data is saved."] : [],
|
|
9268
|
+
...files.some((f) => [f.state, ...Object.values(f.state.children ?? {})].some((s) => s.dropped)) ? ["Some timing records were incomplete, oversized or had unverified gaps; their missing time is unknown."] : [],
|
|
9269
|
+
...index.warnings
|
|
9270
|
+
]
|
|
9271
|
+
};
|
|
9272
|
+
}
|
|
9273
|
+
function formatDuration(ms) {
|
|
9274
|
+
if (ms === null) return "Unknown";
|
|
9275
|
+
if (ms < 6e4) return "<1 min";
|
|
9276
|
+
const minutes = Math.floor(ms / 6e4), hours = Math.floor(minutes / 60);
|
|
9277
|
+
return hours ? `${hours.toLocaleString()}h ${minutes % 60}m` : `${minutes}m`;
|
|
9278
|
+
}
|
|
9279
|
+
|
|
9280
|
+
// src/time-sqlite.ts
|
|
9281
|
+
init_sqlite();
|
|
9282
|
+
init_resource_budget();
|
|
9283
|
+
var PAGE = 1e3;
|
|
9284
|
+
function stateFor(file, agent) {
|
|
9285
|
+
const children = file.state.children ??= /* @__PURE__ */ Object.create(null);
|
|
9286
|
+
if (!Object.hasOwn(children, agent)) Object.defineProperty(children, agent, {
|
|
9287
|
+
value: { agent, ai: [], user: [], observed: 0, dropped: 0 },
|
|
9288
|
+
enumerable: true,
|
|
9289
|
+
writable: true
|
|
9290
|
+
});
|
|
9291
|
+
return children[agent];
|
|
9292
|
+
}
|
|
9293
|
+
function interval(state, startValue, endValue, id, estimated = 0) {
|
|
9294
|
+
const start = timestamp(startValue), end = timestamp(endValue);
|
|
9295
|
+
state.observed++;
|
|
9296
|
+
if (start !== void 0 && end !== void 0 && end > start && end - start <= 31 * 864e5) state.ai.push([start, end, estimated, id]);
|
|
9297
|
+
if (end !== void 0) state.lastEnd = end;
|
|
9298
|
+
}
|
|
9299
|
+
async function scanTimeDatabase(source, file) {
|
|
9300
|
+
const separator = source.path.lastIndexOf(":");
|
|
9301
|
+
const path = source.provider === "opencode" ? source.path.slice(0, separator) : source.path;
|
|
9302
|
+
const db = openDatabase(path);
|
|
9303
|
+
try {
|
|
9304
|
+
let rows2;
|
|
9305
|
+
if (source.provider === "cursor") {
|
|
9306
|
+
rows2 = db.query(`SELECT key,
|
|
9307
|
+
json_extract(value, '$.conversationId') AS conversation,
|
|
9308
|
+
json_extract(value, '$.type') AS role,
|
|
9309
|
+
json_extract(value, '$.createdAt') AS created,
|
|
9310
|
+
json_extract(value, '$.timingInfo.clientStartTime') AS started,
|
|
9311
|
+
COALESCE(json_extract(value, '$.timingInfo.clientEndTime'), json_extract(value, '$.timingInfo.clientSettleTime')) AS completed
|
|
9312
|
+
FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' ORDER BY rowid LIMIT ? OFFSET ?`, [PAGE, file.dbOffset ?? 0]);
|
|
9313
|
+
for (const row of rows2) {
|
|
9314
|
+
chargeRecord();
|
|
9315
|
+
const conversation = typeof row.conversation === "string" ? row.conversation : String(row.key).split(":")[1];
|
|
9316
|
+
const state = stateFor(file, `cursor:${conversation}`);
|
|
9317
|
+
if (row.role === 1) observeTime("cursor", state, { type: "user", timestamp: row.created });
|
|
9318
|
+
else interval(state, row.started, row.completed, `${String(row.key).split(":").at(-1)}:${String(row.started)}`);
|
|
9319
|
+
}
|
|
9320
|
+
} else {
|
|
9321
|
+
const hasParts = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'part'").length > 0;
|
|
9322
|
+
const evidence = hasParts ? `(SELECT MAX(part.time_created) FROM part WHERE part.message_id = message.id
|
|
9323
|
+
AND part.time_created >= message.time_created AND part.time_created <= message.time_created + 1800000)` : "NULL";
|
|
9324
|
+
rows2 = db.query(`SELECT message.id, session.parent_id AS parent,
|
|
9325
|
+
json_extract(data, '$.role') AS role,
|
|
9326
|
+
COALESCE(json_extract(data, '$.time.created'), message.time_created) AS started,
|
|
9327
|
+
json_extract(message.data, '$.time.completed') AS completed, ${evidence} AS observed_end
|
|
9328
|
+
FROM message JOIN session ON message.session_id = session.id
|
|
9329
|
+
WHERE message.session_id = ? ORDER BY message.time_created, message.id LIMIT ? OFFSET ?`, [source.path.slice(separator + 1), PAGE, file.dbOffset ?? 0]);
|
|
9330
|
+
const state = stateFor(file, `opencode:${source.path.slice(separator + 1)}`);
|
|
9331
|
+
for (const row of rows2) {
|
|
9332
|
+
chargeRecord();
|
|
9333
|
+
state.child = !!row.parent;
|
|
9334
|
+
if (row.role === "user") observeTime("opencode", state, { type: "user", timestamp: row.started });
|
|
9335
|
+
else if (row.role === "assistant") {
|
|
9336
|
+
const started = timestamp(row.started), completed = timestamp(row.completed);
|
|
9337
|
+
if (started !== void 0 && (completed === void 0 || completed < started || completed - started > 30 * 6e4)) {
|
|
9338
|
+
state.dropped++;
|
|
9339
|
+
const observed = timestamp(row.observed_end);
|
|
9340
|
+
if (observed !== void 0 && observed - started <= 30 * 6e4) interval(state, started, observed, String(row.id), 1);
|
|
9341
|
+
} else interval(state, row.started, row.completed, String(row.id));
|
|
9342
|
+
}
|
|
9343
|
+
}
|
|
9344
|
+
}
|
|
9345
|
+
file.dbOffset = (file.dbOffset ?? 0) + rows2.length;
|
|
9346
|
+
file.offset = rows2.length < PAGE ? file.size : 0;
|
|
9347
|
+
} finally {
|
|
9348
|
+
db.close();
|
|
9349
|
+
}
|
|
9350
|
+
}
|
|
9351
|
+
function expandOpenCodeTimingSources(source) {
|
|
9352
|
+
const path = source.path.slice(0, source.path.lastIndexOf(":"));
|
|
9353
|
+
const db = openDatabase(path);
|
|
9354
|
+
try {
|
|
9355
|
+
return db.query("SELECT id, directory FROM session LIMIT 20000").map((row) => ({
|
|
9356
|
+
provider: "opencode",
|
|
9357
|
+
path: `${path}:${String(row.id)}`,
|
|
9358
|
+
project: typeof row.directory === "string" ? row.directory.replace(/^\//, "").replace(/\//g, "-") : source.project
|
|
9359
|
+
}));
|
|
9360
|
+
} finally {
|
|
9361
|
+
db.close();
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
|
|
9365
|
+
// src/time-index.ts
|
|
9366
|
+
init_opencode();
|
|
9367
|
+
var compress2 = promisify2(gzip2);
|
|
9368
|
+
var decompress2 = promisify2(gunzip2);
|
|
9369
|
+
var MAX_INDEX = 32 * 1024 ** 2;
|
|
9370
|
+
var MAX_DISK2 = 16 * 1024 ** 2;
|
|
9371
|
+
var MAX_FILES = 2e4;
|
|
9372
|
+
var BLOCK = 2 * 1024 ** 2;
|
|
9373
|
+
var adapterVersion = (provider) => provider === "opencode" ? 2 : 1;
|
|
9374
|
+
var indexPath2 = () => join28(getCacheDir(), "time-index-v1.json.gz");
|
|
9375
|
+
var empty = () => ({ version: 1, files: /* @__PURE__ */ Object.create(null), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), warnings: [] });
|
|
9376
|
+
async function readTimeIndex() {
|
|
9377
|
+
try {
|
|
9378
|
+
if ((await stat19(indexPath2())).size > MAX_DISK2) throw new Error("Timing index exceeds its disk limit");
|
|
9379
|
+
const value = JSON.parse((await decompress2(await readFile14(indexPath2()), { maxOutputLength: MAX_INDEX })).toString("utf8"));
|
|
9380
|
+
if (value.version !== 1 || !value.files || typeof value.files !== "object" || Array.isArray(value.files) || Object.keys(value.files).length > MAX_FILES || !Array.isArray(value.warnings) || !value.warnings.every((x) => typeof x === "string") || typeof value.updatedAt !== "string") throw new Error("Invalid timing index");
|
|
9381
|
+
for (const row of Object.values(value.files)) {
|
|
9382
|
+
const interval2 = (x) => Array.isArray(x) && x.length >= 3 && typeof x[0] === "number" && Number.isFinite(x[0]) && typeof x[1] === "number" && Number.isFinite(x[1]) && (x[2] === 0 || x[2] === 1) && (x[3] === void 0 || typeof x[3] === "string");
|
|
9383
|
+
const state = (s) => s && typeof s.agent === "string" && Array.isArray(s.ai) && s.ai.every(interval2) && Array.isArray(s.user) && s.user.every(interval2);
|
|
9384
|
+
if (typeof row?.provider !== "string" || typeof row.project !== "string" || !state(row.state) || !Number.isSafeInteger(row.offset) || row.offset < 0 || !Number.isSafeInteger(row.size) || row.size < 0 || !Object.values(row.state.children ?? {}).every(state)) throw new Error("Invalid timing entry");
|
|
9385
|
+
}
|
|
9386
|
+
return value;
|
|
9387
|
+
} catch (error) {
|
|
9388
|
+
const result = empty();
|
|
9389
|
+
if (error.code !== "ENOENT") result.warnings.push("Saved timing index could not be read; timing coverage is unknown until recovered.");
|
|
9390
|
+
return result;
|
|
9391
|
+
}
|
|
9392
|
+
}
|
|
9393
|
+
async function save(index) {
|
|
9394
|
+
const data = JSON.stringify(index);
|
|
9395
|
+
if (Buffer.byteLength(data) > MAX_INDEX) throw new Error("Timing index reached its 32 MiB memory/disk snapshot limit; use a narrower history range.");
|
|
9396
|
+
const encoded = await compress2(Buffer.from(data));
|
|
9397
|
+
if (encoded.length > MAX_DISK2) throw new Error("Timing index reached its 16 MiB compressed disk limit.");
|
|
9398
|
+
const path = indexPath2(), temp = `${path}.${randomUUID5()}.tmp`;
|
|
9399
|
+
await mkdir13(getCacheDir(), { recursive: true });
|
|
9400
|
+
try {
|
|
9401
|
+
const handle = await open9(temp, "wx", 384);
|
|
9402
|
+
try {
|
|
9403
|
+
await handle.writeFile(encoded);
|
|
9404
|
+
await handle.sync();
|
|
9405
|
+
} finally {
|
|
9406
|
+
await handle.close();
|
|
9407
|
+
}
|
|
9408
|
+
await rename11(temp, path);
|
|
9409
|
+
} finally {
|
|
9410
|
+
await unlink9(temp).catch(() => {
|
|
9411
|
+
});
|
|
9412
|
+
}
|
|
9413
|
+
}
|
|
9414
|
+
async function expandSources(sources, warnings) {
|
|
9415
|
+
const out = [];
|
|
9416
|
+
const databases = /* @__PURE__ */ new Set();
|
|
9417
|
+
for (const source of sources) {
|
|
9418
|
+
chargeRead(0);
|
|
9419
|
+
if (source.provider === "opencode") {
|
|
9420
|
+
const path = source.path.slice(0, source.path.lastIndexOf(":"));
|
|
9421
|
+
if (!databases.has(path)) {
|
|
9422
|
+
databases.add(path);
|
|
9423
|
+
try {
|
|
9424
|
+
out.push(...expandOpenCodeTimingSources(source));
|
|
9425
|
+
} catch (error) {
|
|
9426
|
+
if (error instanceof ResourceBudgetError) throw error;
|
|
9427
|
+
warnings.push("An OpenCode database has no readable timing schema; its time is unknown.");
|
|
9428
|
+
}
|
|
9429
|
+
}
|
|
9430
|
+
continue;
|
|
9431
|
+
}
|
|
9432
|
+
if (source.provider !== "claude") {
|
|
9433
|
+
out.push(source);
|
|
9434
|
+
continue;
|
|
9435
|
+
}
|
|
9436
|
+
for (const entry of await readdir15(source.path, { withFileTypes: true }).catch(() => [])) {
|
|
9437
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push({ ...source, path: join28(source.path, entry.name) });
|
|
9438
|
+
else if (entry.isDirectory()) {
|
|
9439
|
+
const folder = join28(source.path, entry.name, "subagents");
|
|
9440
|
+
for (const agent of await readdir15(folder, { withFileTypes: true }).catch(() => [])) {
|
|
9441
|
+
if (agent.isFile() && agent.name.endsWith(".jsonl")) out.push({ ...source, path: join28(folder, agent.name) });
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
}
|
|
9446
|
+
return [...new Map(out.map((x) => [`${x.provider}:${x.path}`, x])).values()];
|
|
9447
|
+
}
|
|
9448
|
+
async function anchor(path, offset) {
|
|
9449
|
+
const handle = await open9(path, "r");
|
|
9450
|
+
try {
|
|
9451
|
+
const head = Buffer.alloc(Math.min(256, offset)), tail = Buffer.alloc(Math.min(256, offset));
|
|
9452
|
+
await handle.read(head, 0, head.length, 0);
|
|
9453
|
+
await handle.read(tail, 0, tail.length, Math.max(0, offset - tail.length));
|
|
9454
|
+
chargeRead(head.length + tail.length);
|
|
9455
|
+
return createHash8("sha256").update(head).update(tail).digest("hex");
|
|
9456
|
+
} finally {
|
|
9457
|
+
await handle.close();
|
|
9458
|
+
}
|
|
9459
|
+
}
|
|
9460
|
+
function fresh(source, identity, size, mtime, ctime) {
|
|
9461
|
+
const child = source.path.includes("/subagents/") || basename11(source.path).startsWith("agent-") || source.provider === "kimi" && basename11(dirname6(source.path)) !== "main";
|
|
9462
|
+
const kimiSession = source.path.split("/").find((part) => part.startsWith("session_")) ?? basename11(dirname6(dirname6(source.path)));
|
|
9463
|
+
const agent = source.provider === "copilot" ? basename11(dirname6(source.path)) : source.provider === "kimi" ? `${kimiSession}/${basename11(dirname6(source.path))}` : `${child ? basename11(dirname6(dirname6(source.path))) + "/" : ""}${basename11(source.path, ".jsonl")}`;
|
|
9464
|
+
return {
|
|
9465
|
+
provider: source.provider,
|
|
9466
|
+
project: source.project,
|
|
9467
|
+
identity,
|
|
9468
|
+
size,
|
|
9469
|
+
mtime,
|
|
9470
|
+
ctime,
|
|
9471
|
+
offset: 0,
|
|
9472
|
+
anchor: "",
|
|
9473
|
+
adapterVersion: adapterVersion(source.provider),
|
|
9474
|
+
state: { agent: `${source.provider}:${agent}`, child, ai: [], user: [], observed: 0, dropped: 0 }
|
|
9475
|
+
};
|
|
9476
|
+
}
|
|
9477
|
+
async function scanBlock(source, row) {
|
|
9478
|
+
row.waitingForAppend = false;
|
|
9479
|
+
const handle = await open9(source.path, "r");
|
|
9480
|
+
let consumed = 0;
|
|
9481
|
+
try {
|
|
9482
|
+
const length = Math.min(BLOCK, row.size - row.offset);
|
|
9483
|
+
chargeRead(length);
|
|
9484
|
+
const allocated = Buffer.allocUnsafe(length);
|
|
9485
|
+
const { bytesRead } = await handle.read(allocated, 0, length, row.offset);
|
|
9486
|
+
const buffer = allocated.subarray(0, bytesRead);
|
|
9487
|
+
let start = 0;
|
|
9488
|
+
for (; ; ) {
|
|
9489
|
+
const newline = buffer.indexOf(10, start);
|
|
9490
|
+
const atEnd = newline < 0;
|
|
9491
|
+
const end = atEnd ? bytesRead : newline;
|
|
9492
|
+
if (end > bytesRead) break;
|
|
9493
|
+
if (row.discarding) {
|
|
9494
|
+
consumed = atEnd ? bytesRead : end + 1;
|
|
9495
|
+
if (!atEnd) row.discarding = false;
|
|
9496
|
+
} else {
|
|
9497
|
+
const line = buffer.toString("utf8", start, end).trim();
|
|
9498
|
+
chargeRecord();
|
|
9499
|
+
if (line) {
|
|
9500
|
+
try {
|
|
9501
|
+
const entry = JSON.parse(line);
|
|
9502
|
+
observeTime(source.provider, row.state, entry);
|
|
9503
|
+
} catch {
|
|
9504
|
+
if (atEnd && row.offset + bytesRead >= row.size) {
|
|
9505
|
+
row.waitingForAppend = true;
|
|
9506
|
+
break;
|
|
9507
|
+
}
|
|
9508
|
+
if (atEnd && start > 0) {
|
|
9509
|
+
consumed = start;
|
|
9510
|
+
break;
|
|
9511
|
+
}
|
|
9512
|
+
if (!atEnd) row.state.dropped++;
|
|
9513
|
+
else {
|
|
9514
|
+
row.discarding = true;
|
|
9515
|
+
row.state.dropped++;
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
}
|
|
9519
|
+
consumed = atEnd ? bytesRead : end + 1;
|
|
9520
|
+
}
|
|
9521
|
+
if (atEnd) break;
|
|
9522
|
+
start = end + 1;
|
|
9523
|
+
if (start >= bytesRead) break;
|
|
9524
|
+
}
|
|
9525
|
+
} finally {
|
|
9526
|
+
row.offset += consumed;
|
|
9527
|
+
try {
|
|
9528
|
+
if (consumed) row.anchor = await anchor(source.path, row.offset);
|
|
9529
|
+
} finally {
|
|
9530
|
+
await handle.close();
|
|
9531
|
+
}
|
|
9532
|
+
}
|
|
9533
|
+
}
|
|
9534
|
+
async function refreshTimeIndex(options = {}) {
|
|
9535
|
+
let current;
|
|
9536
|
+
let release;
|
|
9537
|
+
try {
|
|
9538
|
+
release = await acquireFileLock(join28(getCacheDir(), "time-index.lock"), 100);
|
|
9539
|
+
} catch {
|
|
9540
|
+
current = await readTimeIndex();
|
|
9541
|
+
current.warnings.push("Another timing update is running; showing the saved timing snapshot.");
|
|
9542
|
+
return current;
|
|
9543
|
+
}
|
|
9544
|
+
current = await readTimeIndex();
|
|
9545
|
+
current.warnings = [];
|
|
9546
|
+
let changed = false;
|
|
9547
|
+
try {
|
|
9548
|
+
await withScanBudget(async () => {
|
|
9549
|
+
const sources = options.sources ?? [
|
|
9550
|
+
...await (await Promise.resolve().then(() => (init_parser(), parser_exports))).getDiscoveredSources(),
|
|
9551
|
+
...(await getOpenCodeDbPaths()).map((path) => ({ provider: "opencode", path: `${path}:timing-root`, project: "opencode" }))
|
|
9552
|
+
];
|
|
9553
|
+
const expanded = await expandSources(sources, current.warnings);
|
|
9554
|
+
const candidates = [];
|
|
9555
|
+
let fileCount = Object.keys(current.files).length;
|
|
9556
|
+
for (const source of expanded) {
|
|
9557
|
+
chargeRead(0);
|
|
9558
|
+
const database = source.provider === "opencode" ? source.path.slice(0, source.path.lastIndexOf(":")) : source.path;
|
|
9559
|
+
const s = await stat19(database).catch(() => null);
|
|
9560
|
+
if (!s) continue;
|
|
9561
|
+
const wal = ["cursor", "opencode"].includes(source.provider) ? await stat19(database + "-wal").catch(() => null) : null;
|
|
9562
|
+
candidates.push({ source, s, wal });
|
|
9563
|
+
}
|
|
9564
|
+
candidates.sort((a, b) => b.s.mtimeMs - a.s.mtimeMs || a.source.path.localeCompare(b.source.path));
|
|
9565
|
+
if (candidates.length > MAX_FILES) current.warnings.push("Some timing sources exceed the bounded file limit; their time remains unknown.");
|
|
9566
|
+
const deadline = Date.now() + (options.history ? 15e3 : 750);
|
|
9567
|
+
for (const { source, s, wal } of candidates) {
|
|
9568
|
+
const key2 = createHash8("sha256").update(`${source.provider}:${source.path}`).digest("hex");
|
|
9569
|
+
if (!current.files[key2] && fileCount < MAX_FILES) {
|
|
9570
|
+
current.files[key2] = fresh(source, `${s.dev}:${s.ino}:${wal?.size ?? 0}:${wal?.mtimeMs ?? 0}`, s.size, s.mtimeMs, s.ctimeMs);
|
|
9571
|
+
changed = true;
|
|
9572
|
+
fileCount++;
|
|
9573
|
+
} else if (current.files[key2] && ((current.files[key2].adapterVersion ?? 1) !== adapterVersion(source.provider) || current.files[key2].identity !== `${s.dev}:${s.ino}:${wal?.size ?? 0}:${wal?.mtimeMs ?? 0}` || current.files[key2].size !== s.size || current.files[key2].mtime !== s.mtimeMs || current.files[key2].ctime !== s.ctimeMs)) {
|
|
9574
|
+
current.files[key2].unavailable = true;
|
|
9575
|
+
changed = true;
|
|
9576
|
+
}
|
|
9577
|
+
}
|
|
9578
|
+
for (const { source, s, wal } of candidates) {
|
|
9579
|
+
chargeRead(0);
|
|
9580
|
+
const key2 = createHash8("sha256").update(`${source.provider}:${source.path}`).digest("hex");
|
|
9581
|
+
const identity = `${s.dev}:${s.ino}:${wal?.size ?? 0}:${wal?.mtimeMs ?? 0}`;
|
|
9582
|
+
let row = current.files[key2];
|
|
9583
|
+
const sameAdapter = (row?.adapterVersion ?? 1) === adapterVersion(source.provider);
|
|
9584
|
+
if (row && sameAdapter && !row.unavailable && row.identity === identity && row.size === s.size && row.mtime === s.mtimeMs && row.ctime === s.ctimeMs && (row.offset >= row.size || row.waitingForAppend)) continue;
|
|
9585
|
+
if (Date.now() >= deadline) break;
|
|
9586
|
+
if (!row && fileCount >= MAX_FILES) {
|
|
9587
|
+
current.warnings.push("Timing file limit reached; additional files remain unknown.");
|
|
9588
|
+
break;
|
|
9589
|
+
}
|
|
9590
|
+
const appended = row && sameAdapter && row.identity === identity && s.size > row.size && row.offset > 0 && row.anchor === await anchor(source.path, row.offset);
|
|
9591
|
+
const unchangedPartial = row && sameAdapter && row.identity === identity && s.size === row.size && row.mtime === s.mtimeMs && row.ctime === s.ctimeMs;
|
|
9592
|
+
if (!row) fileCount++;
|
|
9593
|
+
if (!row || !appended && !unchangedPartial) row = fresh(source, identity, s.size, s.mtimeMs, s.ctimeMs);
|
|
9594
|
+
row.size = s.size;
|
|
9595
|
+
row.mtime = s.mtimeMs;
|
|
9596
|
+
row.ctime = s.ctimeMs;
|
|
9597
|
+
current.files[key2] = row;
|
|
9598
|
+
changed = true;
|
|
9599
|
+
try {
|
|
9600
|
+
if (source.provider === "cursor" || source.provider === "opencode") await scanTimeDatabase(source, row);
|
|
9601
|
+
else if (source.provider === "cursor-agent" && !source.path.endsWith(".jsonl")) {
|
|
9602
|
+
row.offset = row.size;
|
|
9603
|
+
} else {
|
|
9604
|
+
const startedAt = row.offset;
|
|
9605
|
+
do {
|
|
9606
|
+
const before = row.offset;
|
|
9607
|
+
await scanBlock(source, row);
|
|
9608
|
+
if (row.offset === before) break;
|
|
9609
|
+
} while (row.offset < row.size && row.offset - startedAt < 16 * 1024 ** 2 && Date.now() < deadline);
|
|
9610
|
+
}
|
|
9611
|
+
row.state.user = unionIntervals(row.state.user);
|
|
9612
|
+
row.unavailable = false;
|
|
9613
|
+
} catch (error) {
|
|
9614
|
+
if (error instanceof ResourceBudgetError) {
|
|
9615
|
+
row.unavailable = true;
|
|
9616
|
+
throw error;
|
|
9617
|
+
}
|
|
9618
|
+
row.state.dropped++;
|
|
9619
|
+
row.offset = row.size;
|
|
9620
|
+
row.unavailable = false;
|
|
9621
|
+
current.warnings.push(`${source.provider} has a timing source that could not be read. Its missing timing remains unknown.`);
|
|
9622
|
+
}
|
|
9623
|
+
}
|
|
9624
|
+
}, options.history ? 2e6 : 2e5, options.signal, (options.history ? 256 : 16) * 1024 ** 2);
|
|
9625
|
+
} catch (error) {
|
|
9626
|
+
current.warnings = [`Timing update paused: ${error instanceof Error ? error.message : String(error)}`];
|
|
9627
|
+
} finally {
|
|
9628
|
+
if (changed) {
|
|
9629
|
+
current.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9630
|
+
try {
|
|
9631
|
+
await save(current);
|
|
9632
|
+
} catch (error) {
|
|
9633
|
+
current.warnings.push(String(error));
|
|
9634
|
+
}
|
|
9635
|
+
}
|
|
9636
|
+
await release();
|
|
9637
|
+
}
|
|
9638
|
+
return current;
|
|
9639
|
+
}
|
|
9640
|
+
async function getTimeReport(range, provider = "all", include, exclude, signal) {
|
|
9641
|
+
try {
|
|
9642
|
+
return summarizeTime(await refreshTimeIndex({ signal }), range, provider, include, exclude);
|
|
9643
|
+
} catch (error) {
|
|
9644
|
+
const index = await readTimeIndex();
|
|
9645
|
+
index.warnings.push(`Timing unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
9646
|
+
return summarizeTime(index, range, provider, include, exclude);
|
|
9647
|
+
}
|
|
9648
|
+
}
|
|
9649
|
+
|
|
8516
9650
|
// src/dashboard.tsx
|
|
8517
|
-
import { join as join26 } from "path";
|
|
8518
9651
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
8519
9652
|
var PERIODS = ["today", "week", "30days", "month", "all"];
|
|
8520
9653
|
var PERIOD_LABELS = {
|
|
@@ -8664,21 +9797,10 @@ function Panel({ title, color, children, width }) {
|
|
|
8664
9797
|
children
|
|
8665
9798
|
] });
|
|
8666
9799
|
}
|
|
8667
|
-
|
|
8668
|
-
" \u2584\u2584 \u2584\u2584 ",
|
|
8669
|
-
" \u2588\u2580\u2580\u2588\u2584\u2584\u2584\u2584\u2584\u2588\u2580\u2580\u2588 ",
|
|
8670
|
-
" \u2588 \u25CF \u25CF \u2588 ",
|
|
8671
|
-
" \u2588 \u25BC \u2588 ",
|
|
8672
|
-
" \u2580\u2584 \u2570\u2500\u2500\u2500\u256F \u2584\u2580 ",
|
|
8673
|
-
" \u2580\u2588\u2588\u2588\u2588\u2588\u2580 ",
|
|
8674
|
-
" \u2590\u2588\u2588\u2588\u2588\u2588\u258C ",
|
|
8675
|
-
" \u2588\u2588 \u2588\u2588 ",
|
|
8676
|
-
" \u2580\u2580 \u2580\u2580 "
|
|
8677
|
-
];
|
|
8678
|
-
function LoadingScreen({ width, dots }) {
|
|
9800
|
+
function LoadingScreen({ width, dots, tick }) {
|
|
8679
9801
|
const dotStr = ".".repeat(dots);
|
|
8680
9802
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, alignItems: "center", justifyContent: "center", paddingY: 2, children: [
|
|
8681
|
-
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", alignItems: "center", children:
|
|
9803
|
+
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", alignItems: "center", children: owlFrame(tick).map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: GOLD3, children: line }, i)) }),
|
|
8682
9804
|
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
8683
9805
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: PINK, children: "WATCHER" }),
|
|
8684
9806
|
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
@@ -8726,7 +9848,7 @@ function Overview({ projects, label, width, planUsage, recovery }) {
|
|
|
8726
9848
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
|
|
8727
9849
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: GOLD3, children: formatCost(totalCost) }),
|
|
8728
9850
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " cost " }),
|
|
8729
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, children: totalCalls.toLocaleString() }),
|
|
9851
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: recovery ? totalCalls ? `${totalCalls.toLocaleString()}+` : "unknown" : totalCalls.toLocaleString() }),
|
|
8730
9852
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " calls " }),
|
|
8731
9853
|
/* @__PURE__ */ jsx2(Text2, { bold: true, children: String(totalSessions) }),
|
|
8732
9854
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " sessions " }),
|
|
@@ -8802,7 +9924,7 @@ function DailyActivity({ projects, days = 14, pw, bw }) {
|
|
|
8802
9924
|
] }, day))
|
|
8803
9925
|
] });
|
|
8804
9926
|
}
|
|
8805
|
-
var _homeEncoded =
|
|
9927
|
+
var _homeEncoded = homedir16().replace(/\//g, "-");
|
|
8806
9928
|
function shortProject(encoded) {
|
|
8807
9929
|
let path = encoded.replace(/^-/, "");
|
|
8808
9930
|
if (path.startsWith(_homeEncoded.replace(/^-/, ""))) {
|
|
@@ -8983,8 +10105,8 @@ function TopSessions({ projects, pw, bw }) {
|
|
|
8983
10105
|
"calls".padStart(TOP_SESSIONS_CALLS_COL)
|
|
8984
10106
|
] }),
|
|
8985
10107
|
top.map((session, i) => {
|
|
8986
|
-
const
|
|
8987
|
-
const label = `${
|
|
10108
|
+
const date2 = session.firstTimestamp ? session.firstTimestamp.slice(0, TOP_SESSIONS_DATE_LEN) : "----------";
|
|
10109
|
+
const label = `${date2} ${shortProject(session.projectName)}`;
|
|
8988
10110
|
return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
|
|
8989
10111
|
/* @__PURE__ */ jsx2(HBar, { value: session.totalCostUSD, max: maxCost, width: bw }),
|
|
8990
10112
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
@@ -9116,7 +10238,7 @@ function FindingPanel({ index, finding, costRate, width }) {
|
|
|
9116
10238
|
] });
|
|
9117
10239
|
}
|
|
9118
10240
|
var GRADE_COLORS2 = { A: "#16A34A", B: "#16A34A", C: GOLD3, D: ORANGE3, F: "#DC2626" };
|
|
9119
|
-
function OptimizeView({ findings, costRate, projects, label, width, healthScore, healthGrade }) {
|
|
10241
|
+
function OptimizeView({ findings, costRate, projects, label, width, healthScore, healthGrade, coverage, shareStatus }) {
|
|
9120
10242
|
const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0);
|
|
9121
10243
|
const totalTokens = findings.reduce((s, f) => s + f.tokensSaved, 0);
|
|
9122
10244
|
const totalCost = totalTokens * costRate;
|
|
@@ -9130,15 +10252,34 @@ function OptimizeView({ findings, costRate, projects, label, width, healthScore,
|
|
|
9130
10252
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
9131
10253
|
" ",
|
|
9132
10254
|
label,
|
|
9133
|
-
"
|
|
10255
|
+
" "
|
|
9134
10256
|
] }),
|
|
9135
|
-
/* @__PURE__ */ jsx2(Text2, {
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
10257
|
+
coverage?.partial ? /* @__PURE__ */ jsx2(Text2, { color: ORANGE3, children: "Partial check" }) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
10258
|
+
"Setup: ",
|
|
10259
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: gradeColor, children: healthGrade }),
|
|
10260
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
10261
|
+
" (",
|
|
10262
|
+
healthScore,
|
|
10263
|
+
"/100)"
|
|
10264
|
+
] })
|
|
9140
10265
|
] })
|
|
9141
10266
|
] }),
|
|
10267
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Claude Code recommendations. Changes are yours to apply." }),
|
|
10268
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
10269
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: ORANGE3, children: "c" }),
|
|
10270
|
+
/* @__PURE__ */ jsx2(Text2, { children: " copy all " }),
|
|
10271
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: ORANGE3, children: "e" }),
|
|
10272
|
+
/* @__PURE__ */ jsx2(Text2, { children: " save .md" })
|
|
10273
|
+
] }),
|
|
10274
|
+
shareStatus && /* @__PURE__ */ jsx2(Text2, { color: shareStatus.error ? ORANGE3 : "#16A34A", children: shareStatus.text }),
|
|
10275
|
+
coverage?.partial && /* @__PURE__ */ jsxs2(Text2, { color: ORANGE3, children: [
|
|
10276
|
+
"Partial analysis: ",
|
|
10277
|
+
coverage.completedFiles,
|
|
10278
|
+
"/",
|
|
10279
|
+
coverage.totalFiles,
|
|
10280
|
+
" logs fully checked; some records remain unchecked."
|
|
10281
|
+
] }),
|
|
10282
|
+
coverage?.partial && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Cleanup suggestions requiring complete history are omitted." }),
|
|
9142
10283
|
/* @__PURE__ */ jsxs2(Text2, { color: "#16A34A", wrap: "truncate-end", children: [
|
|
9143
10284
|
"Savings: ~",
|
|
9144
10285
|
formatTokens(totalTokens),
|
|
@@ -9202,18 +10343,51 @@ function Row({ wide, width, children }) {
|
|
|
9202
10343
|
if (wide) return /* @__PURE__ */ jsx2(Box2, { width, children });
|
|
9203
10344
|
return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
9204
10345
|
}
|
|
9205
|
-
function
|
|
10346
|
+
function TimePanel({ time, width }) {
|
|
10347
|
+
return /* @__PURE__ */ jsxs2(Panel, { title: "AI time", color: GOLD3, width, children: [
|
|
10348
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
10349
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: "AI running " }),
|
|
10350
|
+
formatDuration(time?.aiRunningMs ?? null),
|
|
10351
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \xB7 added across agents" })
|
|
10352
|
+
] }),
|
|
10353
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
10354
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: "Elapsed busy " }),
|
|
10355
|
+
formatDuration(time?.elapsedBusyMs ?? null),
|
|
10356
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \xB7 overlapping time counted once" })
|
|
10357
|
+
] }),
|
|
10358
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
10359
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: "Your time \u2248 " }),
|
|
10360
|
+
formatDuration(time?.userActiveMs ?? null),
|
|
10361
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \xB7 interaction-gap estimate, not attention" })
|
|
10362
|
+
] }),
|
|
10363
|
+
/* @__PURE__ */ jsxs2(Text2, { color: ORANGE3, children: [
|
|
10364
|
+
time?.aiRunningMs != null ? "Partial history" : "Timing unknown",
|
|
10365
|
+
time?.pendingFiles ? ` \xB7 ${time.pendingFiles} files awaiting recovery` : "",
|
|
10366
|
+
time?.estimatedAiMs ? " \xB7 some AI time inferred" : ""
|
|
10367
|
+
] }),
|
|
10368
|
+
time && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
10369
|
+
"Providers without timed records: ",
|
|
10370
|
+
time.providers.filter((p) => p.aiRunningMs === null).map((p) => p.name).join(", ") || "none detected",
|
|
10371
|
+
". Missing time is unknown, not zero."
|
|
10372
|
+
] })
|
|
10373
|
+
] });
|
|
10374
|
+
}
|
|
10375
|
+
function DashboardContent({ projects, period, columns, activeProvider, budgets, planUsage, recovery, time }) {
|
|
9206
10376
|
const { dashWidth, wide, halfWidth, barWidth: barWidth2 } = getLayout(columns);
|
|
9207
10377
|
const isCursor = activeProvider === "cursor";
|
|
9208
|
-
if (projects.length === 0) return /* @__PURE__ */
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
10378
|
+
if (projects.length === 0) return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
10379
|
+
/* @__PURE__ */ jsx2(TimePanel, { time, width: dashWidth }),
|
|
10380
|
+
/* @__PURE__ */ jsx2(Panel, { title: "Exe Watcher", color: ORANGE3, width: dashWidth, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
10381
|
+
"No usage data found for ",
|
|
10382
|
+
PERIOD_LABELS[period],
|
|
10383
|
+
"."
|
|
10384
|
+
] }) })
|
|
10385
|
+
] });
|
|
9213
10386
|
const pw = wide ? halfWidth : dashWidth;
|
|
9214
10387
|
const days = period === "all" ? void 0 : period === "month" || period === "30days" ? 31 : 14;
|
|
9215
10388
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
|
|
9216
10389
|
/* @__PURE__ */ jsx2(Overview, { projects, label: PERIOD_LABELS[period], width: dashWidth, planUsage, recovery }),
|
|
10390
|
+
/* @__PURE__ */ jsx2(TimePanel, { time, width: dashWidth }),
|
|
9217
10391
|
/* @__PURE__ */ jsxs2(Row, { wide, width: dashWidth, children: [
|
|
9218
10392
|
/* @__PURE__ */ jsx2(DailyActivity, { projects, days, pw, bw: barWidth2 }),
|
|
9219
10393
|
/* @__PURE__ */ jsx2(ProjectBreakdown, { projects, pw, bw: barWidth2, budgets })
|
|
@@ -9237,6 +10411,7 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9237
10411
|
const [period, setPeriod] = useState2(initialPeriod);
|
|
9238
10412
|
const [projects, setProjects] = useState2([]);
|
|
9239
10413
|
const [recovery, setRecovery] = useState2(null);
|
|
10414
|
+
const [time, setTime] = useState2();
|
|
9240
10415
|
const [initialLoading, setInitialLoading] = useState2(true);
|
|
9241
10416
|
const [loading, setLoading] = useState2(false);
|
|
9242
10417
|
const [loadError2, setLoadError] = useState2(null);
|
|
@@ -9247,9 +10422,15 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9247
10422
|
const [detectedProviders, setDetectedProviders] = useState2([]);
|
|
9248
10423
|
const [view, setView] = useState2("dashboard");
|
|
9249
10424
|
const [optimizeResult, setOptimizeResult] = useState2(null);
|
|
10425
|
+
const [optimizeError, setOptimizeError] = useState2(null);
|
|
10426
|
+
const [optimizeAttempt, setOptimizeAttempt] = useState2(0);
|
|
10427
|
+
const [shareStatus, setShareStatus] = useState2(null);
|
|
10428
|
+
const sharing = useRef2(false);
|
|
10429
|
+
const shareGeneration = useRef2(0);
|
|
9250
10430
|
const [projectBudgets, setProjectBudgets] = useState2(/* @__PURE__ */ new Map());
|
|
9251
10431
|
const [planUsage, setPlanUsage] = useState2(void 0);
|
|
9252
10432
|
const [loadingDots, setLoadingDots] = useState2(1);
|
|
10433
|
+
const [loadingTick, setLoadingTick] = useState2(0);
|
|
9253
10434
|
const { columns } = useWindowSize();
|
|
9254
10435
|
const { dashWidth } = getLayout(columns);
|
|
9255
10436
|
const multipleProviders = detectedProviders.length > 1;
|
|
@@ -9261,9 +10442,34 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9261
10442
|
const debounceRef = useRef2(null);
|
|
9262
10443
|
const reloadGenerationRef = useRef2(0);
|
|
9263
10444
|
const findingCount = optimizeResult?.findings.length ?? 0;
|
|
10445
|
+
useEffect2(() => {
|
|
10446
|
+
shareGeneration.current++;
|
|
10447
|
+
setShareStatus(null);
|
|
10448
|
+
return () => {
|
|
10449
|
+
shareGeneration.current++;
|
|
10450
|
+
};
|
|
10451
|
+
}, [optimizeResult, period, view]);
|
|
10452
|
+
async function shareOptimize(action) {
|
|
10453
|
+
if (!optimizeResult || loading || loadError2 || optimizeError || sharing.current) return;
|
|
10454
|
+
sharing.current = true;
|
|
10455
|
+
const generation = shareGeneration.current;
|
|
10456
|
+
setShareStatus({ text: action === "c" ? "Copying\u2026" : "Saving\u2026" });
|
|
10457
|
+
try {
|
|
10458
|
+
const markdown = formatOptimizeMarkdown(optimizeResult, { periodLabel: PERIOD_LABELS[period], projectFilter, excludeFilter });
|
|
10459
|
+
const text = action === "c" ? (await copyOptimizeMarkdown(markdown), "Copied all recommendations. Paste into Claude Code.") : `Saved: ${await saveOptimizeMarkdown(markdown)}`;
|
|
10460
|
+
if (generation === shareGeneration.current) setShareStatus({ text });
|
|
10461
|
+
} catch (error) {
|
|
10462
|
+
if (generation === shareGeneration.current) setShareStatus({ text: action === "c" ? "Could not copy. Press e to save a Markdown file instead." : `Could not save: ${error instanceof Error ? error.message : String(error)}`, error: true });
|
|
10463
|
+
} finally {
|
|
10464
|
+
sharing.current = false;
|
|
10465
|
+
}
|
|
10466
|
+
}
|
|
9264
10467
|
useEffect2(() => {
|
|
9265
10468
|
if (!initialLoading) return;
|
|
9266
|
-
const id = setInterval(() =>
|
|
10469
|
+
const id = setInterval(() => {
|
|
10470
|
+
setLoadingDots((d) => d % 3 + 1);
|
|
10471
|
+
setLoadingTick((t) => t + 1);
|
|
10472
|
+
}, 400);
|
|
9267
10473
|
return () => clearInterval(id);
|
|
9268
10474
|
}, [initialLoading]);
|
|
9269
10475
|
useEffect2(() => {
|
|
@@ -9276,11 +10482,11 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9276
10482
|
useEffect2(() => {
|
|
9277
10483
|
let cancelled = false;
|
|
9278
10484
|
async function loadBudgets() {
|
|
9279
|
-
const claudeDir =
|
|
10485
|
+
const claudeDir = join29(homedir16(), ".claude", "projects");
|
|
9280
10486
|
const budgets = /* @__PURE__ */ new Map();
|
|
9281
10487
|
for (const project of projects.slice(0, 8)) {
|
|
9282
10488
|
if (cancelled) return;
|
|
9283
|
-
const cwd = await discoverProjectCwd(
|
|
10489
|
+
const cwd = await discoverProjectCwd(join29(claudeDir, project.project));
|
|
9284
10490
|
if (!cwd) continue;
|
|
9285
10491
|
budgets.set(project.project, await estimateContextBudget(cwd));
|
|
9286
10492
|
}
|
|
@@ -9297,28 +10503,26 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9297
10503
|
setOptimizeResult(null);
|
|
9298
10504
|
return;
|
|
9299
10505
|
}
|
|
10506
|
+
setOptimizeError(null);
|
|
9300
10507
|
let cancelled = false;
|
|
9301
10508
|
const controller = new AbortController();
|
|
9302
10509
|
async function scan() {
|
|
9303
|
-
|
|
9304
|
-
setOptimizeResult(null);
|
|
9305
|
-
return;
|
|
9306
|
-
}
|
|
9307
|
-
const result = await loader.current.run((signal) => withScanBudget(() => scanAndDetect(projects, getDateRange(period)), 2e5, AbortSignal.any([signal, controller.signal])));
|
|
10510
|
+
const result = await loader.current.run((signal) => withScanBudget(() => scanAndDetect(projects, getDateRange(period), void 0, { allowPartial: true }), 2e5, AbortSignal.any([signal, controller.signal])));
|
|
9308
10511
|
if (!cancelled && result) setOptimizeResult(result);
|
|
9309
10512
|
}
|
|
9310
10513
|
void scan().catch((error) => {
|
|
9311
|
-
if (!cancelled)
|
|
10514
|
+
if (!cancelled) setOptimizeError(error instanceof ResourceBudgetError ? "The analysis reached its safety limit. Completed session details are saved for your next attempt." : error instanceof Error ? error.message : String(error));
|
|
9312
10515
|
});
|
|
9313
10516
|
return () => {
|
|
9314
10517
|
cancelled = true;
|
|
9315
10518
|
controller.abort();
|
|
9316
10519
|
};
|
|
9317
|
-
}, [projects, period, optimizeAvailable, view, loading, initialLoading, loadError2]);
|
|
10520
|
+
}, [projects, period, optimizeAvailable, view, loading, initialLoading, loadError2, optimizeAttempt]);
|
|
9318
10521
|
const reloadData = useCallback(async (p, prov) => {
|
|
9319
10522
|
const generation = ++reloadGenerationRef.current;
|
|
9320
10523
|
setLoading(true);
|
|
9321
10524
|
setOptimizeResult(null);
|
|
10525
|
+
setOptimizeError(null);
|
|
9322
10526
|
try {
|
|
9323
10527
|
setLoadError(null);
|
|
9324
10528
|
const result = await loader.current.run(async (signal) => {
|
|
@@ -9326,11 +10530,13 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9326
10530
|
const data = await parseAllSessions(getDateRange(p), prov, true, signal);
|
|
9327
10531
|
if (signal.aborted) return void 0;
|
|
9328
10532
|
const recovery2 = p === "all" && (prov === "all" || prov === "claude") && !projectFilter?.length && !excludeFilter?.length ? await getClaudeRecoveryForRange(getDateRange(p), signal) : null;
|
|
9329
|
-
|
|
10533
|
+
const time2 = await getTimeReport(getDateRange(p), prov, projectFilter, excludeFilter, signal);
|
|
10534
|
+
return { recovery: recovery2, time: time2, projects: filterProjectsByName(data, projectFilter, excludeFilter), usage: await getPlanUsageOrNull(), providers: getDiscoveredProviderNames() };
|
|
9330
10535
|
});
|
|
9331
10536
|
if (!result || reloadGenerationRef.current !== generation) return;
|
|
9332
10537
|
setProjects(result.projects);
|
|
9333
10538
|
setRecovery(result.recovery);
|
|
10539
|
+
setTime(result.time);
|
|
9334
10540
|
setDetectedProviders(result.providers);
|
|
9335
10541
|
setPlanUsage(result.usage ?? void 0);
|
|
9336
10542
|
failures.current = 0;
|
|
@@ -9348,14 +10554,15 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9348
10554
|
}
|
|
9349
10555
|
}, [projectFilter, excludeFilter]);
|
|
9350
10556
|
useEffect2(() => {
|
|
9351
|
-
if (!refreshSeconds || refreshSeconds <= 0) return;
|
|
10557
|
+
if (!refreshSeconds || refreshSeconds <= 0 || view === "optimize") return;
|
|
9352
10558
|
const id = setInterval(() => {
|
|
9353
10559
|
if (!loader.current.busy && Date.now() >= retryAt.current) void reloadData(period, activeProvider);
|
|
9354
10560
|
}, refreshSeconds * 1e3);
|
|
9355
10561
|
return () => clearInterval(id);
|
|
9356
|
-
}, [refreshSeconds, period, activeProvider, reloadData]);
|
|
10562
|
+
}, [refreshSeconds, period, activeProvider, reloadData, view]);
|
|
9357
10563
|
const switchPeriod = useCallback((np) => {
|
|
9358
10564
|
if (np === period) return;
|
|
10565
|
+
setLoading(true);
|
|
9359
10566
|
setPeriod(np);
|
|
9360
10567
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
9361
10568
|
debounceRef.current = setTimeout(() => {
|
|
@@ -9368,22 +10575,29 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9368
10575
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
9369
10576
|
await reloadData(np, activeProvider);
|
|
9370
10577
|
}, [period, activeProvider, reloadData]);
|
|
9371
|
-
useInput2((input,
|
|
10578
|
+
useInput2((input, key2) => {
|
|
9372
10579
|
if (input === "q") {
|
|
9373
10580
|
loader.current.cancel();
|
|
9374
10581
|
exit();
|
|
9375
10582
|
return;
|
|
9376
10583
|
}
|
|
9377
10584
|
if (input === "r") {
|
|
9378
|
-
|
|
10585
|
+
if (view === "optimize" && !loading && !loadError2) {
|
|
10586
|
+
setOptimizeResult(null);
|
|
10587
|
+
setOptimizeAttempt((n) => n + 1);
|
|
10588
|
+
} else void reloadData(period, activeProvider);
|
|
9379
10589
|
return;
|
|
9380
10590
|
}
|
|
9381
10591
|
if (input === "o" && view === "dashboard" && optimizeAvailable) {
|
|
9382
10592
|
setView("optimize");
|
|
9383
10593
|
return;
|
|
9384
10594
|
}
|
|
9385
|
-
if ((input === "
|
|
9386
|
-
|
|
10595
|
+
if ((input === "c" || input === "e") && view === "optimize") {
|
|
10596
|
+
void shareOptimize(input);
|
|
10597
|
+
return;
|
|
10598
|
+
}
|
|
10599
|
+
if ((input === "b" || key2.escape) && view === "optimize") {
|
|
10600
|
+
setOptimizeError(null);
|
|
9387
10601
|
setView("dashboard");
|
|
9388
10602
|
return;
|
|
9389
10603
|
}
|
|
@@ -9401,8 +10615,8 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9401
10615
|
return;
|
|
9402
10616
|
}
|
|
9403
10617
|
const idx = PERIODS.indexOf(period);
|
|
9404
|
-
if (
|
|
9405
|
-
else if (
|
|
10618
|
+
if (key2.leftArrow) switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length] ?? "today");
|
|
10619
|
+
else if (key2.rightArrow || key2.tab) switchPeriod(PERIODS[(idx + 1) % PERIODS.length] ?? "today");
|
|
9406
10620
|
else if (input === "1") switchPeriodImmediate("today");
|
|
9407
10621
|
else if (input === "2") switchPeriodImmediate("week");
|
|
9408
10622
|
else if (input === "3") switchPeriodImmediate("30days");
|
|
@@ -9410,7 +10624,7 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9410
10624
|
else if (input === "5") switchPeriodImmediate("all");
|
|
9411
10625
|
});
|
|
9412
10626
|
if (initialLoading) {
|
|
9413
|
-
return /* @__PURE__ */ jsx2(LoadingScreen, { width: dashWidth, dots: loadingDots });
|
|
10627
|
+
return /* @__PURE__ */ jsx2(LoadingScreen, { width: dashWidth, dots: loadingDots, tick: loadingTick });
|
|
9414
10628
|
}
|
|
9415
10629
|
if (loadError2 && !loading) {
|
|
9416
10630
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
|
|
@@ -9442,16 +10656,19 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
|
|
|
9442
10656
|
}
|
|
9443
10657
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
|
|
9444
10658
|
/* @__PURE__ */ jsx2(PeriodTabs, { active: period, providerName: activeProvider, showProvider: multipleProviders && view !== "compare" }),
|
|
9445
|
-
view === "compare" ? /* @__PURE__ */ jsx2(CompareView, { projects, onBack: () => setView("dashboard") }) : view === "optimize" &&
|
|
10659
|
+
view === "compare" ? /* @__PURE__ */ jsx2(CompareView, { projects, onBack: () => setView("dashboard") }) : view === "optimize" && optimizeError ? /* @__PURE__ */ jsxs2(Panel, { title: "Optimize could not finish", color: ORANGE3, width: dashWidth, children: [
|
|
10660
|
+
/* @__PURE__ */ jsx2(Text2, { children: optimizeError }),
|
|
10661
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Your usage report is still available. Press b to go back, r to retry, or 1\u20135 for a shorter period." })
|
|
10662
|
+
] }) : 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, coverage: optimizeResult.coverage, shareStatus }) : 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, recovery, time }),
|
|
9446
10663
|
view !== "compare" && /* @__PURE__ */ jsx2(StatusBar, { width: dashWidth, showProvider: multipleProviders, view, findingCount, optimizeAvailable, compareAvailable })
|
|
9447
10664
|
] });
|
|
9448
10665
|
}
|
|
9449
|
-
function StaticDashboard({ projects, period, activeProvider, planUsage, recovery }) {
|
|
10666
|
+
function StaticDashboard({ projects, period, activeProvider, planUsage, recovery, time }) {
|
|
9450
10667
|
const { columns } = useWindowSize();
|
|
9451
10668
|
const { dashWidth } = getLayout(columns);
|
|
9452
10669
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: dashWidth, children: [
|
|
9453
10670
|
/* @__PURE__ */ jsx2(PeriodTabs, { active: period }),
|
|
9454
|
-
/* @__PURE__ */ jsx2(DashboardContent, { projects, period, columns, activeProvider, planUsage, recovery })
|
|
10671
|
+
/* @__PURE__ */ jsx2(DashboardContent, { projects, period, columns, activeProvider, planUsage, recovery, time })
|
|
9455
10672
|
] });
|
|
9456
10673
|
}
|
|
9457
10674
|
async function renderDashboard(period = "week", provider = "all", refreshSeconds, projectFilter, excludeFilter, customRange) {
|
|
@@ -9467,7 +10684,8 @@ async function renderDashboard(period = "week", provider = "all", refreshSeconds
|
|
|
9467
10684
|
const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider, true), projectFilter, excludeFilter);
|
|
9468
10685
|
const recovery = period === "all" && (provider === "all" || provider === "claude") && !projectFilter?.length && !excludeFilter?.length && !customRange ? await getClaudeRecoveryForRange(range) : null;
|
|
9469
10686
|
const planUsage = await getPlanUsageOrNull();
|
|
9470
|
-
const
|
|
10687
|
+
const time = await getTimeReport(range, provider, projectFilter, excludeFilter);
|
|
10688
|
+
const { unmount } = render2(/* @__PURE__ */ jsx2(StaticDashboard, { projects: filteredProjects, period, activeProvider: provider, planUsage: planUsage ?? void 0, recovery, time }), { patchConsole: false });
|
|
9471
10689
|
unmount();
|
|
9472
10690
|
}
|
|
9473
10691
|
}
|
|
@@ -9581,11 +10799,164 @@ function planBackfillChunks(start, end, chunkDays = DEFAULT_BACKFILL_CHUNK_DAYS)
|
|
|
9581
10799
|
}
|
|
9582
10800
|
return chunks;
|
|
9583
10801
|
}
|
|
9584
|
-
function nextBackfillCursor(prev,
|
|
9585
|
-
return prev && prev.blockedDate ===
|
|
10802
|
+
function nextBackfillCursor(prev, date2) {
|
|
10803
|
+
return prev && prev.blockedDate === date2 ? { blockedDate: date2, attempts: prev.attempts + 1 } : { blockedDate: date2, attempts: 1 };
|
|
10804
|
+
}
|
|
10805
|
+
function shouldSkipBlockedChunk(cursor2, date2, maxAttempts = MAX_BLOCKED_CHUNK_ATTEMPTS) {
|
|
10806
|
+
return !!cursor2 && cursor2.blockedDate === date2 && cursor2.attempts >= maxAttempts;
|
|
9586
10807
|
}
|
|
9587
|
-
|
|
9588
|
-
|
|
10808
|
+
|
|
10809
|
+
// src/period-snapshots.ts
|
|
10810
|
+
init_day_aggregator();
|
|
10811
|
+
init_models();
|
|
10812
|
+
function assertHistoryComplete(cache, start, today) {
|
|
10813
|
+
if (start >= today) return;
|
|
10814
|
+
const required = Math.round((Date.parse(today) - Date.parse(start)) / 864e5);
|
|
10815
|
+
const dates = new Set(cache.days.filter((d) => d.date >= start && d.date < today).map((d) => d.date));
|
|
10816
|
+
if (cache.needsRepricing || cache.historyBuild || cache.missingProviders?.length || cache.partialDates?.some((d) => d >= start && d < today) || dates.size !== required) {
|
|
10817
|
+
throw new Error("Historical usage is incomplete. The history rebuild has saved its progress; keeping the last successful total. Refresh again to resume.");
|
|
10818
|
+
}
|
|
10819
|
+
}
|
|
10820
|
+
function buildPeriodSnapshots(cache, todayDays, today, windows, provider, names, recovery, parseTimeMs, warnings) {
|
|
10821
|
+
const closed = cache.days.filter((d) => d.date < today);
|
|
10822
|
+
const live2 = todayDays.filter((d) => d.date === today);
|
|
10823
|
+
const dates = new Set(closed.map((d) => d.date));
|
|
10824
|
+
const partial = new Set(cache.partialDates ?? []);
|
|
10825
|
+
const days = [...closed, ...live2].sort((a, b) => a.date.localeCompare(b.date));
|
|
10826
|
+
const periodDays = (start) => days.filter((d) => d.date >= start);
|
|
10827
|
+
const week = windows.find((w) => w.key === "week");
|
|
10828
|
+
const month = windows.find((w) => w.key === "30days");
|
|
10829
|
+
const result = {};
|
|
10830
|
+
for (const window of windows) {
|
|
10831
|
+
const required = Math.max(0, Math.round((Date.parse(today) - Date.parse(window.start)) / 864e5));
|
|
10832
|
+
const covered = [...dates].filter((d) => d >= window.start && !partial.has(d)).length;
|
|
10833
|
+
if (window.key !== "today" && (cache.needsRepricing || cache.historyBuild || cache.missingProviders?.length || covered !== required)) continue;
|
|
10834
|
+
const selected = periodDays(window.start);
|
|
10835
|
+
const scoped = provider === "all" ? selected : filterDaysToProvider(selected, provider);
|
|
10836
|
+
const data = buildPeriodDataFromDays(scoped, window.label);
|
|
10837
|
+
const totals = {};
|
|
10838
|
+
for (const day of scoped) for (const [name, usage] of Object.entries(day.providers)) {
|
|
10839
|
+
totals[name] = (totals[name] ?? 0) + usage.cost;
|
|
10840
|
+
}
|
|
10841
|
+
const notes = [...warnings];
|
|
10842
|
+
for (const model of data.models) {
|
|
10843
|
+
if (getModelCosts(model.name)) continue;
|
|
10844
|
+
const note = `No API pricing for model "${model.name}"; its tokens are counted but its cost is excluded. Configure a model alias only when the underlying API model is known.`;
|
|
10845
|
+
if (!notes.includes(note)) notes.push(note);
|
|
10846
|
+
}
|
|
10847
|
+
if (window.key === "all" && recovery && (provider === "all" || provider === "claude")) {
|
|
10848
|
+
applyClaudeRecovery(data, recovery);
|
|
10849
|
+
totals.claude = (totals.claude ?? 0) + recovery.costUSD;
|
|
10850
|
+
if (!notes.includes(recovery.note)) notes.push(recovery.note);
|
|
10851
|
+
}
|
|
10852
|
+
const providers = provider === "all" ? Object.entries(totals).map(([name, cost]) => ({ name: names.get(name) ?? name, cost })) : [{ name: names.get(provider) ?? provider, cost: data.cost }];
|
|
10853
|
+
const built = buildMenubarPayload(data, providers, null);
|
|
10854
|
+
result[window.key] = {
|
|
10855
|
+
current: built.current,
|
|
10856
|
+
projectSpend: buildProjectSpendFromDays(selected, live2, periodDays(week.start), periodDays(month.start)),
|
|
10857
|
+
diagnostics: { daysCount: selected.length, parseTimeMs, warnings: notes },
|
|
10858
|
+
// Do not perpetually freshen historical validation with every daily update.
|
|
10859
|
+
historyValidatedAt: cache.revalidatedAt ?? 0
|
|
10860
|
+
};
|
|
10861
|
+
}
|
|
10862
|
+
return result;
|
|
10863
|
+
}
|
|
10864
|
+
|
|
10865
|
+
// src/history-build.ts
|
|
10866
|
+
init_day_aggregator();
|
|
10867
|
+
init_report_summary();
|
|
10868
|
+
init_resource_budget();
|
|
10869
|
+
var key = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
10870
|
+
var date = (s) => /* @__PURE__ */ new Date(`${s}T00:00:00`);
|
|
10871
|
+
async function completeHistoryBuild(cache, start, end, providers, scan, save2, options = {}) {
|
|
10872
|
+
const previous = cache.historyBuild;
|
|
10873
|
+
const stage = previous && !previous.partialDates?.length ? structuredClone(previous) : {
|
|
10874
|
+
start: previous?.start ?? key(start),
|
|
10875
|
+
end: previous?.end ?? key(end),
|
|
10876
|
+
completedProviders: [],
|
|
10877
|
+
days: [],
|
|
10878
|
+
providers: previous?.providers ?? providers,
|
|
10879
|
+
mergeExisting: previous?.mergeExisting ?? options.mergeExisting,
|
|
10880
|
+
previousBackfill: previous?.previousBackfill ?? cache.backfill
|
|
10881
|
+
};
|
|
10882
|
+
const scanProviders = stage.providers ?? providers;
|
|
10883
|
+
let current = { ...cache, historyBuild: stage };
|
|
10884
|
+
const first = date(stage.start);
|
|
10885
|
+
const last = date(stage.end);
|
|
10886
|
+
last.setHours(23, 59, 59, 999);
|
|
10887
|
+
const merge = (incoming) => {
|
|
10888
|
+
const byDate = new Map(stage.days.map((d) => [d.date, d]));
|
|
10889
|
+
for (const row of incoming) {
|
|
10890
|
+
if (row.date < stage.start || row.date > stage.end) continue;
|
|
10891
|
+
const previous2 = byDate.get(row.date);
|
|
10892
|
+
if (previous2) mergeCounters(previous2, row);
|
|
10893
|
+
else byDate.set(row.date, structuredClone(row));
|
|
10894
|
+
}
|
|
10895
|
+
stage.days = [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
10896
|
+
};
|
|
10897
|
+
for (const provider of scanProviders) {
|
|
10898
|
+
if (stage.completedProviders.includes(provider)) continue;
|
|
10899
|
+
if (!stage.chunked) {
|
|
10900
|
+
try {
|
|
10901
|
+
merge(await scan(provider, first, last));
|
|
10902
|
+
stage.completedProviders.push(provider);
|
|
10903
|
+
await save2(current);
|
|
10904
|
+
continue;
|
|
10905
|
+
} catch (err) {
|
|
10906
|
+
if (!(err instanceof ResourceBudgetError)) throw err;
|
|
10907
|
+
stage.chunked = { provider, nextDate: stage.start };
|
|
10908
|
+
await save2(current);
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
10911
|
+
if (stage.chunked.provider !== provider) throw new Error("Historical provider cursor mismatch");
|
|
10912
|
+
for (let day = date(stage.chunked.nextDate); day <= last; ) {
|
|
10913
|
+
const next = new Date(day);
|
|
10914
|
+
next.setDate(next.getDate() + 1);
|
|
10915
|
+
const dayEnd = new Date(next.getTime() - 1);
|
|
10916
|
+
const dayKey = key(day);
|
|
10917
|
+
try {
|
|
10918
|
+
const incoming = await scan(provider, day, dayEnd);
|
|
10919
|
+
merge(incoming.filter((row) => row.date === dayKey));
|
|
10920
|
+
} catch (err) {
|
|
10921
|
+
if (!(err instanceof ResourceBudgetError)) throw err;
|
|
10922
|
+
current.backfill = nextBackfillCursor(current.backfill, dayKey);
|
|
10923
|
+
await save2(current);
|
|
10924
|
+
return current;
|
|
10925
|
+
}
|
|
10926
|
+
current.backfill = null;
|
|
10927
|
+
stage.chunked.nextDate = key(next);
|
|
10928
|
+
await save2(current);
|
|
10929
|
+
day = next;
|
|
10930
|
+
}
|
|
10931
|
+
delete stage.chunked;
|
|
10932
|
+
stage.completedProviders.push(provider);
|
|
10933
|
+
await save2(current);
|
|
10934
|
+
}
|
|
10935
|
+
let rebuilt = fillMissingDays(first, last, stage.days);
|
|
10936
|
+
if (stage.mergeExisting) {
|
|
10937
|
+
const existing = new Map(cache.days.map((day) => [day.date, structuredClone(day)]));
|
|
10938
|
+
rebuilt = rebuilt.map((day) => {
|
|
10939
|
+
const previous2 = existing.get(day.date);
|
|
10940
|
+
if (!previous2) return day;
|
|
10941
|
+
mergeCounters(previous2, day);
|
|
10942
|
+
return previous2;
|
|
10943
|
+
});
|
|
10944
|
+
}
|
|
10945
|
+
current = {
|
|
10946
|
+
...current,
|
|
10947
|
+
...addNewDays(current, rebuilt, stage.end, { coveredThrough: stage.end }),
|
|
10948
|
+
historyBuild: void 0,
|
|
10949
|
+
revalidatedAt: Date.now(),
|
|
10950
|
+
partialDates: [.../* @__PURE__ */ new Set([...(cache.partialDates ?? []).filter((d) => stage.mergeExisting || d < stage.start || d > stage.end), ...stage.partialDates ?? []])],
|
|
10951
|
+
backfill: stage.mergeExisting ? stage.previousBackfill ?? null : null,
|
|
10952
|
+
missingProviders: (cache.missingProviders ?? []).filter((p) => !scanProviders.includes(p))
|
|
10953
|
+
};
|
|
10954
|
+
if (cache.needsRepricing) {
|
|
10955
|
+
delete current.needsRepricing;
|
|
10956
|
+
delete current.previousHistoryStart;
|
|
10957
|
+
}
|
|
10958
|
+
await save2(current);
|
|
10959
|
+
return current;
|
|
9589
10960
|
}
|
|
9590
10961
|
|
|
9591
10962
|
// src/cli.ts
|
|
@@ -9616,16 +10987,22 @@ function watchMenubarParent(parentPid, runtime = live) {
|
|
|
9616
10987
|
return () => clearInterval(timer);
|
|
9617
10988
|
}
|
|
9618
10989
|
|
|
10990
|
+
// src/time-snapshots.ts
|
|
10991
|
+
function buildTimeSnapshots(index, ranges, provider = "all", include, exclude) {
|
|
10992
|
+
const views = (scope) => Object.fromEntries(Object.entries(ranges).map(([key2, range]) => [key2, summarizeTime(index, range, scope, include, exclude)]));
|
|
10993
|
+
const periods = views(provider), allPeriods = provider === "all" ? void 0 : views("all");
|
|
10994
|
+
return { generated: (/* @__PURE__ */ new Date()).toISOString(), provider, periods, allPeriods, recovery: (allPeriods ?? periods)["all"].recovery };
|
|
10995
|
+
}
|
|
10996
|
+
|
|
9619
10997
|
// src/cli.ts
|
|
9620
10998
|
var require2 = createRequire2(import.meta.url);
|
|
9621
10999
|
var { version } = require2("../package.json");
|
|
9622
11000
|
var MS_PER_DAY4 = 24 * 60 * 60 * 1e3;
|
|
9623
11001
|
var configuredHistoryStart = null;
|
|
9624
|
-
function toDateString(
|
|
9625
|
-
return `${
|
|
11002
|
+
function toDateString(date2) {
|
|
11003
|
+
return `${date2.getFullYear()}-${String(date2.getMonth() + 1).padStart(2, "0")}-${String(date2.getDate()).padStart(2, "0")}`;
|
|
9626
11004
|
}
|
|
9627
|
-
function getDateRange2(period) {
|
|
9628
|
-
const now = /* @__PURE__ */ new Date();
|
|
11005
|
+
function getDateRange2(period, now = /* @__PURE__ */ new Date()) {
|
|
9629
11006
|
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
|
9630
11007
|
switch (period) {
|
|
9631
11008
|
case "today": {
|
|
@@ -9694,11 +11071,13 @@ async function runJsonReport(period, provider, project, exclude) {
|
|
|
9694
11071
|
const { range, label } = getDateRange2(period);
|
|
9695
11072
|
const projects = filterProjectsByName(await parseAllSessions(range, provider, true), project, exclude);
|
|
9696
11073
|
const report = buildJsonReport(projects, label, period);
|
|
11074
|
+
report.time = await getTimeReport(range, provider, project, exclude);
|
|
9697
11075
|
if (period === "all" && (provider === "all" || provider === "claude") && !project.length && !exclude.length) {
|
|
9698
11076
|
const recovery = await getClaudeRecoveryForRange(range);
|
|
9699
11077
|
if (recovery) {
|
|
9700
11078
|
const detail = report;
|
|
9701
11079
|
detail.historicalRecovery = recovery;
|
|
11080
|
+
report.overview.callsIncomplete = true;
|
|
9702
11081
|
report.overview.cost += convertCost(recovery.costUSD);
|
|
9703
11082
|
report.overview.tokens.input += recovery.inputTokens;
|
|
9704
11083
|
report.overview.tokens.output += recovery.outputTokens;
|
|
@@ -9708,12 +11087,13 @@ async function runJsonReport(period, provider, project, exclude) {
|
|
|
9708
11087
|
for (const row of recovery.models) {
|
|
9709
11088
|
const existing = report.models.find((m) => m.name === row.name);
|
|
9710
11089
|
if (existing) {
|
|
11090
|
+
existing.callsIncomplete = true;
|
|
9711
11091
|
existing.cost += convertCost(row.costUSD);
|
|
9712
11092
|
existing.inputTokens += row.inputTokens;
|
|
9713
11093
|
existing.outputTokens += row.outputTokens;
|
|
9714
11094
|
existing.cacheReadTokens += row.cacheReadTokens;
|
|
9715
11095
|
existing.cacheWriteTokens += row.cacheWriteTokens;
|
|
9716
|
-
} else report.models.push({ name: row.name, calls: 0, cost: convertCost(row.costUSD), inputTokens: row.inputTokens, outputTokens: row.outputTokens, cacheReadTokens: row.cacheReadTokens, cacheWriteTokens: row.cacheWriteTokens });
|
|
11096
|
+
} else report.models.push({ name: row.name, calls: 0, callsIncomplete: true, cost: convertCost(row.costUSD), inputTokens: row.inputTokens, outputTokens: row.outputTokens, cacheReadTokens: row.cacheReadTokens, cacheWriteTokens: row.cacheWriteTokens });
|
|
9717
11097
|
}
|
|
9718
11098
|
report.models.sort((a, b) => b.cost - a.cost);
|
|
9719
11099
|
report.warnings = [.../* @__PURE__ */ new Set([...report.warnings, ...getPricingWarnings()])];
|
|
@@ -9753,21 +11133,21 @@ function buildJsonReport(projects, period, periodKey) {
|
|
|
9753
11133
|
const cacheHitDenom = totalInput + totalCacheRead;
|
|
9754
11134
|
const cacheHitPercent2 = cacheHitDenom > 0 ? Math.round(totalCacheRead / cacheHitDenom * 1e3) / 10 : 0;
|
|
9755
11135
|
const dailyMap = /* @__PURE__ */ new Map();
|
|
9756
|
-
const addDaily = (
|
|
9757
|
-
const previous = dailyMap.get(
|
|
9758
|
-
dailyMap.set(
|
|
11136
|
+
const addDaily = (date2, cost, calls) => {
|
|
11137
|
+
const previous = dailyMap.get(date2) ?? { cost: 0, calls: 0 };
|
|
11138
|
+
dailyMap.set(date2, { cost: previous.cost + cost, calls: previous.calls + calls });
|
|
9759
11139
|
};
|
|
9760
11140
|
for (const session of sessions) {
|
|
9761
11141
|
if (session.reportSummary) {
|
|
9762
|
-
for (const [
|
|
11142
|
+
for (const [date2, day] of Object.entries(session.reportSummary.daily)) addDaily(date2, day.cost, day.calls);
|
|
9763
11143
|
} else {
|
|
9764
11144
|
for (const turn of session.turns) for (const call of turn.assistantCalls) {
|
|
9765
11145
|
if (call.timestamp) addDaily(dateKey(call.timestamp), call.costUSD, 1);
|
|
9766
11146
|
}
|
|
9767
11147
|
}
|
|
9768
11148
|
}
|
|
9769
|
-
const daily = [...dailyMap].sort(([a], [b]) => a.localeCompare(b)).map(([
|
|
9770
|
-
date,
|
|
11149
|
+
const daily = [...dailyMap].sort(([a], [b]) => a.localeCompare(b)).map(([date2, day]) => ({
|
|
11150
|
+
date: date2,
|
|
9771
11151
|
cost: convertCost(day.cost),
|
|
9772
11152
|
calls: day.calls
|
|
9773
11153
|
}));
|
|
@@ -9779,7 +11159,7 @@ function buildJsonReport(projects, period, periodKey) {
|
|
|
9779
11159
|
calls: p.totalApiCalls,
|
|
9780
11160
|
sessions: p.sessions.length
|
|
9781
11161
|
}));
|
|
9782
|
-
const modelMap =
|
|
11162
|
+
const modelMap = /* @__PURE__ */ Object.create(null);
|
|
9783
11163
|
for (const sess of sessions) {
|
|
9784
11164
|
for (const [model, d] of Object.entries(sess.modelBreakdown)) {
|
|
9785
11165
|
if (!modelMap[model]) {
|
|
@@ -9793,7 +11173,7 @@ function buildJsonReport(projects, period, periodKey) {
|
|
|
9793
11173
|
modelMap[model].cacheWriteTokens += d.tokens.cacheCreationInputTokens;
|
|
9794
11174
|
}
|
|
9795
11175
|
}
|
|
9796
|
-
const models = Object.entries(modelMap).sort(([, a], [, b]) => b.cost - a.cost).map(([name, { cost, ...rest }]) => ({ name, ...rest, cost: convertCost(cost) }));
|
|
11176
|
+
const models = Object.entries(modelMap).sort(([, a], [, b]) => b.cost - a.cost).map(([name, { cost, ...rest }]) => ({ name, ...rest, callsIncomplete: false, cost: convertCost(cost) }));
|
|
9797
11177
|
const catMap = {};
|
|
9798
11178
|
for (const sess of sessions) {
|
|
9799
11179
|
for (const [cat, d] of Object.entries(sess.categoryBreakdown)) {
|
|
@@ -9834,12 +11214,14 @@ function buildJsonReport(projects, period, periodKey) {
|
|
|
9834
11214
|
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9835
11215
|
currency: code,
|
|
9836
11216
|
costBasis: "api-equivalent",
|
|
11217
|
+
time: void 0,
|
|
9837
11218
|
warnings: [...getDiscoveryWarnings(), ...getPricingWarnings(), ...getParseWarnings()],
|
|
9838
11219
|
period,
|
|
9839
11220
|
periodKey,
|
|
9840
11221
|
overview: {
|
|
9841
11222
|
cost: convertCost(totalCostUSD),
|
|
9842
11223
|
calls: totalCalls,
|
|
11224
|
+
callsIncomplete: false,
|
|
9843
11225
|
sessions: totalSessions,
|
|
9844
11226
|
cacheHitPercent: cacheHitPercent2,
|
|
9845
11227
|
tokens: {
|
|
@@ -9859,7 +11241,7 @@ function buildJsonReport(projects, period, periodKey) {
|
|
|
9859
11241
|
topSessions
|
|
9860
11242
|
};
|
|
9861
11243
|
}
|
|
9862
|
-
program.command("report", { isDefault: true }).description("Interactive usage dashboard").option("-p, --period <period>", "Starting period: today, week, 30days, month, all", "week").option("--from <date>", "Start date (YYYY-MM-DD). Overrides --period when set").option("--to <date>", "End date (YYYY-MM-DD). Overrides --period when set").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").option("--format <format>", "Output format: tui, json", "tui").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--refresh <seconds>", "Auto-refresh interval in seconds (0 to disable)", parseInt, 30).action(async (opts) => {
|
|
11244
|
+
program.command("report", { isDefault: true }).description("Interactive usage dashboard").option("-p, --period <period>", "Starting period: today, week, 30days, month, all", "week").option("--from <date>", "Start date (YYYY-MM-DD). Overrides --period when set").option("--to <date>", "End date (YYYY-MM-DD). Overrides --period when set").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor, cursor-agent, copilot, opencode, kimi, pi, omp", "all").option("--format <format>", "Output format: tui, json", "tui").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--refresh <seconds>", "Auto-refresh interval in seconds (0 to disable)", parseInt, 30).action(async (opts) => {
|
|
9863
11245
|
let customRange = null;
|
|
9864
11246
|
try {
|
|
9865
11247
|
customRange = parseDateRangeFlags(opts.from, opts.to);
|
|
@@ -9880,7 +11262,9 @@ program.command("report", { isDefault: true }).description("Interactive usage da
|
|
|
9880
11262
|
opts.project,
|
|
9881
11263
|
opts.exclude
|
|
9882
11264
|
);
|
|
9883
|
-
|
|
11265
|
+
const report = buildJsonReport(projects, label, "custom");
|
|
11266
|
+
report.time = await getTimeReport(customRange, opts.provider, opts.project, opts.exclude);
|
|
11267
|
+
console.log(JSON.stringify(report, null, 2));
|
|
9884
11268
|
} else {
|
|
9885
11269
|
await runJsonReport(period, opts.provider, opts.project, opts.exclude);
|
|
9886
11270
|
}
|
|
@@ -9891,7 +11275,7 @@ program.command("report", { isDefault: true }).description("Interactive usage da
|
|
|
9891
11275
|
function buildPeriodData(label, projects) {
|
|
9892
11276
|
const sessions = projects.flatMap((p) => p.sessions);
|
|
9893
11277
|
const catTotals = {};
|
|
9894
|
-
const modelTotals =
|
|
11278
|
+
const modelTotals = /* @__PURE__ */ Object.create(null);
|
|
9895
11279
|
let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0;
|
|
9896
11280
|
for (const sess of sessions) {
|
|
9897
11281
|
inputTokens += sess.totalInputTokens;
|
|
@@ -9924,7 +11308,7 @@ function buildPeriodData(label, projects) {
|
|
|
9924
11308
|
models: Object.entries(modelTotals).sort(([, a], [, b]) => b.cost - a.cost).map(([name, d]) => ({ name, ...d }))
|
|
9925
11309
|
};
|
|
9926
11310
|
}
|
|
9927
|
-
program.command("status").description("Compact status output (today + week + month)").option("--format <format>", "Output format: terminal, menubar-json, json", "terminal").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--period <period>", "Primary period for menubar-json: today, week, 30days, month, all", "today").option("--no-optimize", "Skip optimize findings (menubar-json only, faster)").action(async (opts) => {
|
|
11311
|
+
program.command("status").description("Compact status output (today + week + month)").option("--format <format>", "Output format: terminal, menubar-json, json", "terminal").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor, cursor-agent, copilot, opencode, kimi, pi, omp", "all").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--period <period>", "Primary period for menubar-json: today, week, 30days, month, all", "today").option("--no-optimize", "Skip optimize findings (menubar-json only, faster)").action(async (opts) => {
|
|
9928
11312
|
await loadPricing();
|
|
9929
11313
|
const pf = opts.provider;
|
|
9930
11314
|
const fp = (p) => filterProjectsByName(p, opts.project, opts.exclude);
|
|
@@ -9985,25 +11369,47 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
9985
11369
|
}
|
|
9986
11370
|
return c;
|
|
9987
11371
|
};
|
|
11372
|
+
const providerNames = (await getAllProviders()).map((p) => p.name);
|
|
11373
|
+
const buildHistory2 = (c, start, end, scanProviders = providerNames, mergeExisting = false) => completeHistoryBuild(
|
|
11374
|
+
c,
|
|
11375
|
+
start,
|
|
11376
|
+
end,
|
|
11377
|
+
scanProviders,
|
|
11378
|
+
async (provider, from, through) => aggregateProjectsIntoDays(filterProjectsByName(
|
|
11379
|
+
await parseAllSessions({ start: from, end: through }, provider, true),
|
|
11380
|
+
opts.project,
|
|
11381
|
+
opts.exclude
|
|
11382
|
+
)),
|
|
11383
|
+
saveDailyCache,
|
|
11384
|
+
{ mergeExisting }
|
|
11385
|
+
);
|
|
11386
|
+
const pausedHistory = (c) => {
|
|
11387
|
+
if (!c.historyBuild) return false;
|
|
11388
|
+
backfillWarnings.push(`History rebuild paused at ${c.historyBuild.chunked?.nextDate ?? c.historyBuild.start}; completed scans are saved and will resume on the next historical refresh.`);
|
|
11389
|
+
return true;
|
|
11390
|
+
};
|
|
9988
11391
|
const cache = await withDailyCacheLock(async () => {
|
|
9989
11392
|
let c = await loadDailyCache(dailyCacheScope);
|
|
9990
11393
|
if (opts.period === "today" && opts.optimize === false) return c;
|
|
11394
|
+
if (c.missingProviders?.length && c.days.length && !c.needsRepricing) {
|
|
11395
|
+
const from = parseHistoryStartDate(c.days[0].date);
|
|
11396
|
+
const through = parseHistoryStartDate(c.days[c.days.length - 1].date);
|
|
11397
|
+
through.setHours(23, 59, 59, 999);
|
|
11398
|
+
c = await buildHistory2(c, from, through, c.missingProviders, true);
|
|
11399
|
+
if (pausedHistory(c)) return c;
|
|
11400
|
+
}
|
|
11401
|
+
if (c.historyBuild) {
|
|
11402
|
+
const from = parseHistoryStartDate(c.historyBuild.start);
|
|
11403
|
+
const through = parseHistoryStartDate(c.historyBuild.end);
|
|
11404
|
+
through.setHours(23, 59, 59, 999);
|
|
11405
|
+
c = await buildHistory2(c, from, through);
|
|
11406
|
+
if (pausedHistory(c)) return c;
|
|
11407
|
+
}
|
|
9991
11408
|
if (c.needsRepricing || opts.period === "all" && c.days.length === 0) {
|
|
9992
11409
|
const start = parseHistoryStartDate(c.previousHistoryStart) ?? historyStart;
|
|
9993
11410
|
if (start <= yesterdayEnd) {
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
c = addNewDays(c, fillMissingDays(start, yesterdayEnd, aggregateProjectsIntoDays(historicalProjects)), yesterdayStr);
|
|
9997
|
-
delete c.needsRepricing;
|
|
9998
|
-
delete c.previousHistoryStart;
|
|
9999
|
-
c.backfill = null;
|
|
10000
|
-
c.partialDates = [];
|
|
10001
|
-
c.revalidatedAt = Date.now();
|
|
10002
|
-
await saveDailyCache(c);
|
|
10003
|
-
} catch (err) {
|
|
10004
|
-
if (!(err instanceof ResourceBudgetError)) throw err;
|
|
10005
|
-
backfillWarnings.push(`History repricing will resume in smaller slices: ${err.message}`);
|
|
10006
|
-
}
|
|
11411
|
+
c = await buildHistory2(c, start, yesterdayEnd);
|
|
11412
|
+
if (pausedHistory(c)) return c;
|
|
10007
11413
|
}
|
|
10008
11414
|
}
|
|
10009
11415
|
const hadYesterday = c.days.some((d) => d.date >= yesterdayStr);
|
|
@@ -10037,6 +11443,7 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
10037
11443
|
}
|
|
10038
11444
|
return c;
|
|
10039
11445
|
});
|
|
11446
|
+
assertHistoryComplete(cache, toDateString(periodInfo.range.start), toDateString(todayStart));
|
|
10040
11447
|
let currentData;
|
|
10041
11448
|
const scanRange = periodInfo.range;
|
|
10042
11449
|
const todayRange = { start: todayStart, end: periodInfo.range.end };
|
|
@@ -10108,23 +11515,14 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
10108
11515
|
providers.push({ name: display, cost: currentData.cost });
|
|
10109
11516
|
}
|
|
10110
11517
|
let historicalRecovery = null;
|
|
10111
|
-
if (opts.period === "all" && (isAllProviders || pf === "claude") && !opts.project.length && !opts.exclude.length && !cache.partialDates?.length && !cache.backfill) {
|
|
11518
|
+
if ((opts.period === "all" || opts.optimize === false) && (isAllProviders || pf === "claude") && !opts.project.length && !opts.exclude.length && !cache.partialDates?.length && !cache.backfill) {
|
|
10112
11519
|
const snapshot = await readClaudeUsageSnapshot();
|
|
10113
11520
|
historicalRecovery = recoverClaudeUsage(snapshot, sumClaudeModelTokens([...cache.days, ...todayDays], snapshot?.lastComputedDate ?? ""), historyStart);
|
|
10114
|
-
if (historicalRecovery) {
|
|
10115
|
-
currentData
|
|
10116
|
-
currentData.inputTokens += historicalRecovery.inputTokens;
|
|
10117
|
-
currentData.outputTokens += historicalRecovery.outputTokens;
|
|
10118
|
-
currentData.cacheReadTokens += historicalRecovery.cacheReadTokens;
|
|
10119
|
-
currentData.cacheWriteTokens += historicalRecovery.cacheWriteTokens;
|
|
11521
|
+
if (historicalRecovery && opts.period === "all") {
|
|
11522
|
+
applyClaudeRecovery(currentData, historicalRecovery);
|
|
10120
11523
|
const claudeProvider = providers.find((p) => p.name.toLowerCase() === "claude");
|
|
10121
11524
|
if (claudeProvider) claudeProvider.cost += historicalRecovery.costUSD;
|
|
10122
11525
|
else providers.push({ name: "Claude", cost: historicalRecovery.costUSD });
|
|
10123
|
-
for (const row of historicalRecovery.models) {
|
|
10124
|
-
const existing = currentData.models.find((m) => m.name === row.model);
|
|
10125
|
-
if (existing) existing.cost += row.costUSD;
|
|
10126
|
-
else currentData.models.push({ name: row.model, calls: 0, cost: row.costUSD });
|
|
10127
|
-
}
|
|
10128
11526
|
warnings.push(historicalRecovery.note);
|
|
10129
11527
|
}
|
|
10130
11528
|
}
|
|
@@ -10177,7 +11575,7 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
10177
11575
|
let optimize = null;
|
|
10178
11576
|
if (opts.optimize !== false) {
|
|
10179
11577
|
try {
|
|
10180
|
-
const optimizeProjects = isAllProviders && opts.period === "today" ? todayProjects : fp(await parseAllSessions(scanRange, isAllProviders ? "all" : pf));
|
|
11578
|
+
const optimizeProjects = isAllProviders && opts.period === "today" ? todayProjects : fp(await parseAllSessions(scanRange, isAllProviders ? "all" : pf, true));
|
|
10181
11579
|
optimize = await scanAndDetect(optimizeProjects, scanRange);
|
|
10182
11580
|
} catch (err) {
|
|
10183
11581
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -10185,15 +11583,14 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
10185
11583
|
warnings.push(warn2);
|
|
10186
11584
|
process.stderr.write(`[exe-watcher] WARNING: ${warn2}
|
|
10187
11585
|
`);
|
|
10188
|
-
optimize = await scanAndDetect(todayProjects, scanRange);
|
|
10189
11586
|
}
|
|
10190
11587
|
}
|
|
10191
|
-
const exeOsDir =
|
|
11588
|
+
const exeOsDir = join30(homedir17(), ".exe-os");
|
|
10192
11589
|
const exeOsDetected = existsSync7(exeOsDir);
|
|
10193
11590
|
let agentStats = null;
|
|
10194
11591
|
let statsFileAge = null;
|
|
10195
11592
|
try {
|
|
10196
|
-
const statsPath =
|
|
11593
|
+
const statsPath = join30(exeOsDir, "agent-stats.json");
|
|
10197
11594
|
if (existsSync7(statsPath)) {
|
|
10198
11595
|
statsFileAge = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
|
|
10199
11596
|
const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
@@ -10214,8 +11611,8 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
10214
11611
|
}
|
|
10215
11612
|
let projectSpend = null;
|
|
10216
11613
|
try {
|
|
10217
|
-
const weekStartStr = toDateString(getDateRange2("week").range.start);
|
|
10218
|
-
const thirtyDayStartStr = toDateString(getDateRange2("30days").range.start);
|
|
11614
|
+
const weekStartStr = toDateString(getDateRange2("week", now).range.start);
|
|
11615
|
+
const thirtyDayStartStr = toDateString(getDateRange2("30days", now).range.start);
|
|
10219
11616
|
const todayOnlyDays = todayDays.filter((d) => d.date > yesterdayStr);
|
|
10220
11617
|
const days7d = [
|
|
10221
11618
|
...getDaysInRange(cache, weekStartStr, yesterdayStr),
|
|
@@ -10231,9 +11628,33 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
10231
11628
|
projectSpend = buildProjectSpendFromDays(selectedPeriodDays, todayOnlyDays, [], []);
|
|
10232
11629
|
}
|
|
10233
11630
|
warnings.push(...getDiscoveryWarnings(), ...getPricingWarnings(), ...getParseWarnings());
|
|
11631
|
+
for (const model of currentData.models) {
|
|
11632
|
+
if (getModelCosts(model.name)) continue;
|
|
11633
|
+
const warning = `No API pricing for model "${model.name}"; its tokens are counted but its cost is excluded. Configure a model alias only when the underlying API model is known.`;
|
|
11634
|
+
if (!warnings.includes(warning)) warnings.push(warning);
|
|
11635
|
+
}
|
|
10234
11636
|
const diagnostics = { daysCount, parseTimeMs, warnings };
|
|
10235
11637
|
const payload = buildMenubarPayload(currentData, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge, diagnostics);
|
|
10236
|
-
const
|
|
11638
|
+
const periodSnapshots = opts.optimize === false ? buildPeriodSnapshots(
|
|
11639
|
+
cache,
|
|
11640
|
+
todayDays,
|
|
11641
|
+
toDateString(todayStart),
|
|
11642
|
+
["today", "week", "30days", "month", "all"].map((key2) => {
|
|
11643
|
+
const { range, label } = getDateRange2(key2, now);
|
|
11644
|
+
return { key: key2, label, start: toDateString(key2 === "all" ? historyStart : range.start) };
|
|
11645
|
+
}),
|
|
11646
|
+
pf,
|
|
11647
|
+
displayNameByName,
|
|
11648
|
+
historicalRecovery,
|
|
11649
|
+
parseTimeMs,
|
|
11650
|
+
warnings
|
|
11651
|
+
) : void 0;
|
|
11652
|
+
const timeIndex = await refreshTimeIndex();
|
|
11653
|
+
const time = summarizeTime(timeIndex, getDateRange2(opts.period, now).range, pf, opts.project, opts.exclude);
|
|
11654
|
+
if (periodSnapshots) for (const [key2, snapshot] of Object.entries(periodSnapshots)) {
|
|
11655
|
+
snapshot.time = summarizeTime(timeIndex, getDateRange2(key2, now).range, pf, opts.project, opts.exclude);
|
|
11656
|
+
}
|
|
11657
|
+
const json = JSON.stringify({ ...payload, time, periodSnapshots, costBasis: "api-equivalent", pricing: getPricingMetadata(), historicalRecovery: opts.period === "all" ? historicalRecovery : null });
|
|
10237
11658
|
console.log(json);
|
|
10238
11659
|
return;
|
|
10239
11660
|
}
|
|
@@ -10256,21 +11677,21 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
10256
11677
|
const monthProjects = fp(await parseAllSessions(getDateRange2("month").range, pf));
|
|
10257
11678
|
console.log(renderStatusBar(monthProjects));
|
|
10258
11679
|
});
|
|
10259
|
-
program.command("today").description("Today's usage dashboard").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").option("--format <format>", "Output format: tui, json", "tui").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--refresh <seconds>", "Auto-refresh interval in seconds (0 to disable)", parseInt, 30).action(async (opts) => {
|
|
11680
|
+
program.command("today").description("Today's usage dashboard").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor, cursor-agent, copilot, opencode, kimi, pi, omp", "all").option("--format <format>", "Output format: tui, json", "tui").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--refresh <seconds>", "Auto-refresh interval in seconds (0 to disable)", parseInt, 30).action(async (opts) => {
|
|
10260
11681
|
if (opts.format === "json") {
|
|
10261
11682
|
await runJsonReport("today", opts.provider, opts.project, opts.exclude);
|
|
10262
11683
|
return;
|
|
10263
11684
|
}
|
|
10264
11685
|
await renderDashboard("today", opts.provider, opts.refresh, opts.project, opts.exclude);
|
|
10265
11686
|
});
|
|
10266
|
-
program.command("month").description("This month's usage dashboard").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").option("--format <format>", "Output format: tui, json", "tui").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--refresh <seconds>", "Auto-refresh interval in seconds (0 to disable)", parseInt, 30).action(async (opts) => {
|
|
11687
|
+
program.command("month").description("This month's usage dashboard").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor, cursor-agent, copilot, opencode, kimi, pi, omp", "all").option("--format <format>", "Output format: tui, json", "tui").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).option("--refresh <seconds>", "Auto-refresh interval in seconds (0 to disable)", parseInt, 30).action(async (opts) => {
|
|
10267
11688
|
if (opts.format === "json") {
|
|
10268
11689
|
await runJsonReport("month", opts.provider, opts.project, opts.exclude);
|
|
10269
11690
|
return;
|
|
10270
11691
|
}
|
|
10271
11692
|
await renderDashboard("month", opts.provider, opts.refresh, opts.project, opts.exclude);
|
|
10272
11693
|
});
|
|
10273
|
-
program.command("export").description("Export usage data to CSV or JSON (includes 1 day, 7 days, 30 days)").option("-f, --format <format>", "Export format: csv, json", "csv").option("-o, --output <path>", "Output file path").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).action(async (opts) => {
|
|
11694
|
+
program.command("export").description("Export usage data to CSV or JSON (includes 1 day, 7 days, 30 days)").option("-f, --format <format>", "Export format: csv, json", "csv").option("-o, --output <path>", "Output file path").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor, cursor-agent, copilot, opencode, kimi, pi, omp", "all").option("--project <name>", "Show only projects matching name (repeatable)", collect, []).option("--exclude <name>", "Exclude projects matching name (repeatable)", collect, []).action(async (opts) => {
|
|
10274
11695
|
await loadPricing();
|
|
10275
11696
|
const pf = opts.provider;
|
|
10276
11697
|
const fp = (p) => filterProjectsByName(p, opts.project, opts.exclude);
|
|
@@ -10529,22 +11950,43 @@ program.command("plan [action] [id]").description("Show or configure a subscript
|
|
|
10529
11950
|
console.log(` Config saved to ${getConfigFilePath()}
|
|
10530
11951
|
`);
|
|
10531
11952
|
});
|
|
10532
|
-
program.command("
|
|
11953
|
+
program.command("time").description("AI running time, elapsed busy time and estimated interaction time since July 1, 2025").option("-p, --period <period>", "today, week, 30days, month, all", "all").option("--provider <provider>", "Filter by coding client", "all").option("--project <names...>", "Include projects by name").option("--exclude <names...>", "Exclude projects by name").option("--recover", "Advance one bounded historical recovery pass; safe to resume").option("--snapshots", "Return timing-only period snapshots for the menu bar").option("--format <format>", "text or json", "text").action(async (opts) => {
|
|
11954
|
+
const index = await refreshTimeIndex({ history: opts.recover === true });
|
|
11955
|
+
if (opts.snapshots) {
|
|
11956
|
+
const now = /* @__PURE__ */ new Date();
|
|
11957
|
+
const ranges = Object.fromEntries(["today", "week", "30days", "month", "all"].map((key2) => [key2, getDateRange2(key2, now).range]));
|
|
11958
|
+
console.log(JSON.stringify(buildTimeSnapshots(index, ranges, opts.provider, opts.project, opts.exclude)));
|
|
11959
|
+
return;
|
|
11960
|
+
}
|
|
11961
|
+
const time = summarizeTime(index, getDateRange2(opts.period).range, opts.provider, opts.project, opts.exclude);
|
|
11962
|
+
if (opts.format === "json") console.log(JSON.stringify(time));
|
|
11963
|
+
else {
|
|
11964
|
+
console.log(`
|
|
11965
|
+
AI time \xB7 ${getDateRange2(opts.period).label}`);
|
|
11966
|
+
console.log(` AI running: ${formatDuration(time.aiRunningMs)} (added across agents)`);
|
|
11967
|
+
console.log(` Elapsed busy: ${formatDuration(time.elapsedBusyMs)} (overlap counted once)`);
|
|
11968
|
+
console.log(` Your time: ${formatDuration(time.userActiveMs)} (estimate)`);
|
|
11969
|
+
console.log(` Partial history \xB7 ${time.indexedFiles} files \xB7 ${time.pendingFiles} awaiting recovery`);
|
|
11970
|
+
for (const row of time.providers) console.log(` ${row.name}: ${formatDuration(row.aiRunningMs)} \xB7 ${row.timedFiles}/${row.files} files with timing in this period`);
|
|
11971
|
+
for (const note of time.notes) console.log(` ${note}`);
|
|
11972
|
+
console.log("");
|
|
11973
|
+
}
|
|
11974
|
+
});
|
|
11975
|
+
program.command("optimize").description("Find token waste and get exact fixes").option("-p, --period <period>", "Analysis period: today, week, 30days, month, all", "week").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor, cursor-agent, copilot, opencode, kimi, pi, omp", "all").action(async (opts) => {
|
|
10533
11976
|
await loadPricing();
|
|
10534
11977
|
const { range, label } = getDateRange2(opts.period);
|
|
10535
|
-
const
|
|
10536
|
-
|
|
10537
|
-
await runOptimize(projects, label, range, scanPromise);
|
|
11978
|
+
const projects = await parseAllSessions(range, opts.provider, true);
|
|
11979
|
+
await runOptimize(projects, label, range);
|
|
10538
11980
|
});
|
|
10539
|
-
program.command("compare").description("Compare two AI models side-by-side").option("-p, --period <period>", "Analysis period: today, week, 30days, month, all", "all").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").action(async (opts) => {
|
|
11981
|
+
program.command("compare").description("Compare two AI models side-by-side").option("-p, --period <period>", "Analysis period: today, week, 30days, month, all", "all").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor, cursor-agent, copilot, opencode, kimi, pi, omp", "all").action(async (opts) => {
|
|
10540
11982
|
await loadPricing();
|
|
10541
11983
|
const { range } = getDateRange2(opts.period);
|
|
10542
11984
|
await renderCompare(range, opts.provider);
|
|
10543
11985
|
});
|
|
10544
11986
|
program.command("agent-status").description("Diagnose exe-os agent data pipeline").action(() => {
|
|
10545
|
-
const exeOsDir =
|
|
10546
|
-
const statsPath =
|
|
10547
|
-
const pidPath =
|
|
11987
|
+
const exeOsDir = join30(homedir17(), ".exe-os");
|
|
11988
|
+
const statsPath = join30(exeOsDir, "agent-stats.json");
|
|
11989
|
+
const pidPath = join30(exeOsDir, "exed.pid");
|
|
10548
11990
|
const dirExists = existsSync7(exeOsDir);
|
|
10549
11991
|
const statsExists = dirExists && existsSync7(statsPath);
|
|
10550
11992
|
const pidExists = dirExists && existsSync7(pidPath);
|
|
@@ -10579,7 +12021,7 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
|
|
|
10579
12021
|
var releaseScan;
|
|
10580
12022
|
var stopParentWatch = watchMenubarParent(Number(process.env["EXE_WATCHER_PARENT_PID"]));
|
|
10581
12023
|
program.hook("preAction", async (_root, command) => {
|
|
10582
|
-
if (command.name() === "status") releaseScan = await acquireFileLock(
|
|
12024
|
+
if (command.name() === "status") releaseScan = await acquireFileLock(join30(getCacheDir(), "scan.lock"));
|
|
10583
12025
|
});
|
|
10584
12026
|
program.hook("postAction", async () => {
|
|
10585
12027
|
await releaseScan?.();
|