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/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((resolve3) => setTimeout(resolve3, 50));
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 [key, value] of Object.entries(source)) {
583
- const old = Object.hasOwn(dst, key) ? dst[key] : void 0;
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 (key === "firstSeen" || key === "firstTimestamp") next = !old || value && value < String(old) ? value : old;
588
- else if (key === "lastSeen" || key === "lastTimestamp") next = !old || value > String(old) ? value : old;
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, key, { value: next, writable: true, enumerable: true, configurable: true });
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(date) {
609
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
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(date) {
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 = (date) => {
650
- let d = byDate.get(date);
663
+ const ensure = (date2) => {
664
+ let d = byDate.get(date2);
651
665
  if (!d) {
652
- d = emptyEntry(date);
653
- byDate.set(date, d);
666
+ d = emptyEntry(date2);
667
+ byDate.set(date2, d);
654
668
  }
655
669
  return d;
656
670
  };
657
- const ensureProvider = (date, provider) => {
658
- const day = ensure(date);
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 = (date, projectName) => {
667
- const day = ensure(date);
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((sum, call) => sum + call.costUSD, 0);
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 key = formatDate(cursor2);
803
- filled.push(byDate.get(key) ?? emptyEntry(key));
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 [key, name] of Object.entries(shortNames2).sort((a, b) => b[0].length - a[0].length)) {
2372
- if (canonical.startsWith(key + "-") && /^\d{4}-?\d{2}-?\d{2}$/.test(canonical.slice(key.length + 1))) return name;
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(([key]) => canonical.startsWith(key + "-") && /^\d{4}-?\d{2}-?\d{2}$/.test(canonical.slice(key.length + 1))).sort(([a], [b]) => b.length - a.length);
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 mkdir7, readFile as readFile6, stat as stat4, writeFile as writeFile2, rename as rename5, unlink as unlink4, readdir as readdir3 } from "fs/promises";
2538
- import { join as join8 } from "path";
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 stat4(path);
2542
- const wal = await stat4(`${path}-wal`).catch(() => null);
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 = join8(getCacheDir(), "parsed-files");
2550
- const key = createHash2("sha256").update(`${VERSION}:${kind}:${path}:${getPricingFingerprint()}`).digest("hex");
2551
- const cachePath = join8(dir, `v${VERSION}-${key}.json.gz`);
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 stat4(cachePath);
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 readFile6(cachePath), { maxOutputLength: MAX_ENTRY })).toString("utf8"));
2560
- if (saved.v === VERSION && saved.fingerprint === before && Array.isArray(saved.data)) {
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
- const payload = JSON.stringify({ v: VERSION, fingerprint: before, data, warnings });
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 mkdir7(dir, { recursive: true });
2753
+ await mkdir8(dir, { recursive: true });
2574
2754
  const encoded = await compress(Buffer.from(payload));
2575
- const release = await acquireFileLock(join8(dir, "writes.lock"));
2755
+ const release = await acquireFileLock(join9(dir, "writes.lock"));
2576
2756
  try {
2577
- const quotaPath = join8(dir, "quota");
2578
- if (maintainedDir !== dir) {
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${VERSION}-`)) {
2584
- await unlink4(join8(dir, name)).catch(() => {
2775
+ if (!name.startsWith(`v${VERSION2}-`)) {
2776
+ await unlink5(join9(dir, name)).catch(() => {
2585
2777
  });
2586
2778
  continue;
2587
2779
  }
2588
- const p = join8(dir, name), s = await stat4(p).catch(() => null);
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
- let total = rows2.reduce((n, r) => n + r.size, 0);
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 unlink4(row.path).catch(() => {
2786
+ await unlink5(row.path).catch(() => {
2595
2787
  });
2596
2788
  total -= row.size;
2597
2789
  }
2598
- await writeFile2(quotaPath, String(total), { mode: 384 });
2599
- maintainedDir = dir;
2790
+ await writeQuota(total);
2600
2791
  }
2601
- if (!(await readFile6(cachePath).catch(() => Buffer.alloc(0))).equals(encoded)) {
2602
- const total = Number(await readFile6(quotaPath, "utf8").catch(() => String(MAX_DISK)));
2603
- const oldSize = (await stat4(cachePath).catch(() => null))?.size ?? 0;
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 = join8(dir, name);
2800
+ const p = join9(dir, name);
2611
2801
  if (p === cachePath) continue;
2612
- const s = await stat4(p).catch(() => null);
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 unlink4(row.path).catch(() => {
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}.${randomUUID()}.tmp`;
2813
+ const tmp = `${cachePath}.${randomUUID2()}.tmp`;
2624
2814
  try {
2625
- await writeFile2(quotaPath, String(nextSize), { mode: 384 });
2626
- await writeFile2(tmp, encoded, { mode: 384 });
2627
- await rename5(tmp, cachePath);
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 unlink4(tmp).catch(() => {
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 VERSION, compress, decompress, MAX_ENTRY, MAX_DISK, maintainedDir;
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
- VERSION = 3;
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 stat5 } from "fs/promises";
2823
- import { basename as basename2, join as join9, resolve as resolve2 } from "path";
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"] || join9(homedir4(), ".claude");
3017
+ return process.env["CLAUDE_CONFIG_DIR"] || join10(homedir4(), ".claude");
2827
3018
  }
2828
3019
  function getDesktopSessionsDir() {
2829
- if (process.platform === "darwin") return join9(homedir4(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
2830
- if (process.platform === "win32") return join9(homedir4(), "AppData", "Roaming", "Claude", "local-agent-mode-sessions");
2831
- return join9(homedir4(), ".config", "Claude", "local-agent-mode-sessions");
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 = join9(dir, entry);
2841
- const s = await stat5(full).catch(() => null);
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 = join9(full, pd);
2847
- const pdStat = await stat5(pdFull).catch(() => null);
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 [key, name] of Object.entries(shortNames)) {
2888
- if (canonical.startsWith(key)) return name;
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 = join9(root.startsWith("~/") ? join9(homedir4(), root.slice(2)) : resolve2(root), "projects");
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 = join9(projectsDir, dirName);
2905
- const dirStat = await stat5(dirPath).catch(() => null);
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 timestamp = entry.timestamp ?? "";
3228
- const dedupKey = `codex:${sessionId}:${timestamp}:${cumulativeTotal}`;
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 [key, name] of modelDisplayEntries) {
3282
- if (model.startsWith(key)) return name;
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 [key, name] of modelDisplayEntries2) {
3434
- if (model === key || model.startsWith(key + "-")) return name;
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 timestamp = entry.timestamp ?? "";
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 [key, name] of modelDisplayEntries3) {
3654
- if (model.startsWith(key)) return name;
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 [key, name] of modelDisplayEntries3) {
3676
- if (model.startsWith(key)) return name;
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/cursor-cache.ts
3726
- import { readFile as readFile8, mkdir as mkdir9, stat as stat10, rename as rename7 } from "fs/promises";
3727
- import { open as open6 } from "fs/promises";
3728
- import { join as join14 } from "path";
3729
- import { randomBytes as randomBytes4 } from "crypto";
3730
- function getCachePath2() {
3731
- return join14(getCacheDir(), CACHE_FILE);
3732
- }
3733
- async function getDbFingerprint(dbPath) {
3734
- try {
3735
- const s = await stat10(dbPath);
3736
- const wal = await stat10(`${dbPath}-wal`).catch(() => null);
3737
- return [dbPath, s.dev, s.ino, s.ctimeMs, s.mtimeMs, s.size, wal?.ino, wal?.ctimeMs, wal?.mtimeMs, wal?.size, getPricingFingerprint()].join(":");
3738
- } catch {
3739
- return null;
3740
- }
3741
- }
3742
- async function readCachedResults(dbPath) {
3743
- try {
3744
- const fp = await getDbFingerprint(dbPath);
3745
- if (!fp) return null;
3746
- const size = (await stat10(getCachePath2())).size;
3747
- if (size > 16 * 1024 * 1024) return null;
3748
- chargeRead(size);
3749
- const raw = await readFile8(getCachePath2(), "utf-8");
3750
- const cache = JSON.parse(raw);
3751
- if (cache.fingerprint === fp) {
3752
- return cache.calls;
3753
- }
3754
- return null;
3755
- } catch (err) {
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 join15 } from "path";
3881
- import { homedir as homedir8 } from "os";
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 join15(homedir8(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
4075
+ return join16(homedir9(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
3885
4076
  }
3886
4077
  if (process.platform === "win32") {
3887
- return join15(homedir8(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
4078
+ return join16(homedir9(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
3888
4079
  }
3889
- return join15(homedir8(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
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 timestamp = createdAt || "";
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 readdir8 } from "fs/promises";
4131
- import { join as join16 } from "path";
4132
- import { homedir as homedir9 } from "os";
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"] ?? join16(homedir9(), ".local", "share");
4138
- return join16(base, "opencode");
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 readdir8(dir);
4143
- return entries.filter((f) => f.startsWith("opencode") && f.endsWith(".db")).map((f) => join16(dir, 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 createHash3 } from "crypto";
4564
+ import { createHash as createHash4 } from "crypto";
4370
4565
  import { existsSync as existsSync2 } from "fs";
4371
- import { readdir as readdir9, stat as stat11 } from "fs/promises";
4372
- import { join as join17, basename as basename7 } from "path";
4373
- import { homedir as homedir10 } from "os";
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 join17(homedir10(), ".cursor");
4571
+ return join18(homedir11(), ".cursor");
4377
4572
  }
4378
4573
  function getProjectsDir(baseDir) {
4379
- return join17(baseDir, "projects");
4574
+ return join18(baseDir, "projects");
4380
4575
  }
4381
4576
  function getAttributionDbPath(baseDir) {
4382
- return join17(baseDir, "ai-tracking", "ai-code-tracking.db");
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 = basename7(transcriptPath, ".txt");
4626
+ const filename = basename8(transcriptPath, ".txt");
4432
4627
  if (filename.length === 36 && UUID_LIKE.test(filename)) return filename;
4433
- return createHash3("sha1").update(transcriptPath).digest("hex").slice(0, 16);
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 ${basename7(source.path)}: unrecognized cursor-agent transcript format
4761
+ process.stderr.write(`exe-watcher: skipped ${basename8(source.path)}: unrecognized cursor-agent transcript format
4567
4762
  `);
4568
4763
  return;
4569
4764
  }
4570
- let timestamp = summary?.updatedAt ?? null;
4571
- if (!timestamp) {
4572
- const fileStat = await stat11(source.path);
4573
- timestamp = fileStat.mtime.toISOString();
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 readdir9(projectsDir, { withFileTypes: true });
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 = join17(projectsDir, entry.name, "agent-transcripts");
4836
+ const transcriptDir = join18(projectsDir, entry.name, "agent-transcripts");
4642
4837
  if (!existsSync2(transcriptDir)) continue;
4643
- const transcriptEntries = await readdir9(transcriptDir, { withFileTypes: true });
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 = join17(transcriptDir, transcript.name);
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 stat12 } from "fs/promises";
5155
+ import { stat as stat13 } from "fs/promises";
4960
5156
  import { existsSync as existsSync3, unlinkSync } from "fs";
4961
- import { readFile as readFile10, mkdir as mkdir10, rename as rename8, unlink as unlink6, open as open7 } from "fs/promises";
4962
- import { join as join18 } from "path";
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 join18(getCacheDir(), INDEX_FILENAME);
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 readFile10(indexPath(), "utf-8");
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 stat12(filePath);
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 readdir10, stat as stat13 } from "fs/promises";
5070
- import { basename as basename8, join as join19 } from "path";
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 key of Object.keys(call.usage)) {
5195
- previous.usage[key] = Math.max(previous.usage[key] ?? 0, call.usage[key] ?? 0);
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 stat13(filePath);
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 = basename8(filePath, ".jsonl");
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 timestamp = new Date(call.timestamp);
5341
- if (!Number.isFinite(timestamp.getTime()) || timestamp < dateRange.start || timestamp > dateRange.end) return false;
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 readdir10(dirPath).catch(() => []);
5360
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => join19(dirPath, 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 = join19(dirPath, entry, "subagents");
5364
- const subFiles = await readdir10(subagentsPath).catch(() => []);
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(join19(subagentsPath, sf));
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 stat13(filePath);
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 stat13(filePath);
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 stat13(source.path);
5469
- const wal = await stat13(`${source.path}-wal`).catch(() => null);
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 key = `${providerName}:${call.sessionId}:${source.project}`;
5502
- touched.add(key);
5503
- const existing = sessionMap.get(key);
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(key, { project: source.project, turns: [classified] });
5703
+ sessionMap.set(key2, { project: source.project, turns: [classified] });
5508
5704
  }
5509
5705
  }
5510
5706
  if (reportMode.getStore()) {
5511
- for (const key of touched) {
5512
- const entry = sessionMap.get(key);
5513
- const summary = buildSessionSummary(key.split(":")[1] ?? key, entry.project, entry.turns);
5514
- const previous = compactSessions.get(key);
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(key, summary);
5517
- sessionMap.delete(key);
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 [key, { project, turns }] of sessionMap) {
5528
- const sessionId = key.split(":")[1] ?? key;
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 key = sourceContextCacheKey(providerFilter);
5572
- const resolved = resolvedSourceContexts.get(key);
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(key);
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(key, promise);
5782
+ inFlightSourceContexts.set(key2, promise);
5586
5783
  try {
5587
5784
  const result = await promise;
5588
- resolvedSourceContexts.set(key, { result, ts: Date.now() });
5785
+ resolvedSourceContexts.set(key2, { result, ts: Date.now() });
5589
5786
  return result;
5590
5787
  } finally {
5591
- inFlightSourceContexts.delete(key);
5788
+ inFlightSourceContexts.delete(key2);
5592
5789
  }
5593
5790
  }
5594
- function cachePut(key, data) {
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(key, { data, ts: now });
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(() => parseAllSessionsWithinBudget(dateRange, providerFilter), summaryOnly ? 2e6 : 2e5, signal, (summaryOnly ? 8 : 1) * 1024 ** 3));
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 key = `${getPricingFingerprint()}:${reportMode.getStore() ? "summary" : "full"}:${cacheKey(dateRange, providerFilter, sourceContext.fingerprint)}`;
5655
- const cached = sessionCache.get(key);
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(key, result);
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 join27 } from "path";
5727
- import { homedir as homedir15 } from "os";
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((resolve3, reject) => {
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 resolve3(out.trim());
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((resolve3, reject) => {
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) resolve3();
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((resolve3) => setTimeout(resolve3, ms));
6045
+ await new Promise((resolve4) => setTimeout(resolve4, ms));
5843
6046
  }
5844
6047
  async function captureCommand(command, args) {
5845
- return new Promise((resolve3) => {
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", () => resolve3({ code: 1, stdout: "" }));
5852
- proc.on("close", (code) => resolve3({ code: code ?? 1, stdout }));
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((resolve3) => {
6090
+ await new Promise((resolve4) => {
5888
6091
  const proc = spawn("/bin/kill", [`-${signal}`, ...pids.map(String)]);
5889
- proc.on("close", () => resolve3());
5890
- proc.on("error", () => resolve3());
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/unzip", ["-q", archivePath, "-d", stagingDir]);
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(([date, d]) => ({
6369
+ return Object.entries(daily).sort().map(([date2, d]) => ({
6167
6370
  Period: period,
6168
- Date: 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 join20, dirname as dirname4 } from "path";
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 = join20(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
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.40";
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
- return models.filter((m) => m.name !== SYNTHETIC_MODEL_NAME).sort((a, b) => b.cost - a.cost).slice(0, TOP_MODELS_LIMIT).map((m) => ({ name: getShortModelName(m.name), cost: m.cost, calls: m.calls }));
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 createHash4, randomBytes as randomBytes6 } from "crypto";
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 readFile11, rename as rename9, unlink as unlink7 } from "fs/promises";
6617
- import { join as join21 } from "path";
6618
- var DAILY_CACHE_VERSION = 8;
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 join21(getCacheDir(), DAILY_CACHE_FILENAME);
6837
+ return join22(getCacheDir(), DAILY_CACHE_FILENAME);
6624
6838
  }
6625
- return join21(getCacheDir(), `daily-cache-${scopeKey}.json`);
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-${createHash4("sha1").update(raw).digest("hex").slice(0, 12)}`;
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 readFile11(path, "utf-8");
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 { ...emptyCache(scopeKey), needsRepricing: true, previousHistoryStart: parsed.days[0]?.date };
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 readFile11(finalPath, "utf8").catch(() => "") === payload) return;
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 readFile12, stat as stat15, mkdir as mkdir12, writeFile as writeFile4, rename as rename10, unlink as unlink8 } from "fs/promises";
6728
- import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
6729
- import { homedir as homedir11 } from "os";
6730
- import { join as join23 } from "path";
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 readdir11, stat as stat14 } from "fs/promises";
6737
- import { join as join22 } from "path";
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 date = new Date(year, month - 1, day);
6742
- return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ? date : null;
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 = (date) => {
6749
- if (date && Number.isFinite(date.getTime()) && date < earliest) earliest = date;
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 date of cachedDates) include(parseHistoryStartDate(date));
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 stat14(source.path).catch(() => null);
6989
+ const sourceStat = await stat15(source.path).catch(() => null);
6756
6990
  if (!sourceStat) continue;
6757
- const paths = sourceStat.isDirectory() ? (await readdir11(source.path)).filter((name) => name.endsWith(".jsonl")).map((name) => join22(source.path, name)) : source.path.endsWith(".jsonl") ? [source.path] : [];
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 stat14(path).catch(() => null);
6993
+ const fileStat = await stat15(path).catch(() => null);
6760
6994
  if (!fileStat) continue;
6761
- const timestamp = await cachedDiscoveryHeader("history-start", path, fileStat, async () => {
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 (timestamp) include(new Date(timestamp));
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"] || join23(homedir11(), ".claude");
6780
- const path = join23(root, "stats-cache.json");
6781
- const savedPath = join23(getCacheDir(), `claude-lifetime-${createHash5("sha256").update(root).digest("hex").slice(0, 16)}.json`);
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 stat15(file)).size > 2 * 1024 ** 2) return null;
6785
- const data = JSON.parse(await readFile12(file, "utf8"));
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 key of ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"]) {
6804
- if (Number.isFinite(counters[key]) && (counters[key] ?? 0) >= 0) row[key] = Math.max(row[key] ?? 0, counters[key] ?? 0);
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 readFile12(savedPath, "utf8").catch(() => "") !== payload) {
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((sum, row) => sum + row.costUSD, 0),
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 homedir14 } from "os";
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 readdir12, stat as stat16 } from "fs/promises";
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 basename9, join as join24 } from "path";
6948
- import { homedir as homedir12 } from "os";
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 readdir12(dirPath).catch(() => []);
7031
- const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join24(dirPath, 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 = join24(dirPath, entry, "subagents");
7035
- const subFiles = await readdir12(subPath).catch(() => []);
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(join24(subPath, sf));
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 stat16(filePath);
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 inRange(timestamp, range) {
7063
- if (!range) return true;
7064
- if (!timestamp) return false;
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 isRecent(timestamp, cutoff) {
7069
- if (!timestamp) return false;
7070
- return new Date(timestamp).getTime() >= cutoff;
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.version && typeof entry.version === "string") lastVersion = entry.version;
7088
- const ts = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
7089
- const withinRange = inRange(ts, dateRange);
7090
- const recent = isRecent(ts, recentCutoffMs);
7091
- if (entry.cwd && typeof entry.cwd === "string" && withinRange) cwds.push(entry.cwd);
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 (!withinRange) continue;
7094
- const msg2 = entry.message;
7095
- const msgContent = msg2?.content;
7096
- if (typeof msgContent === "string") {
7097
- userMessages.push(msgContent);
7098
- } else if (Array.isArray(msgContent)) {
7099
- for (const block of msgContent) {
7100
- if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
7101
- userMessages.push(block.text);
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
- const blocks = msg?.content;
7116
- if (!Array.isArray(blocks)) continue;
7117
- for (const block of blocks) {
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
- await runWithConcurrency(tasks, FILE_READ_CONCURRENCY, async ({ file, project }) => {
7145
- const { calls, cwds, apiCalls, userMessages } = await scanJsonlFile(file, project, dateRange);
7146
- allCalls.push(...calls);
7147
- for (const cwd of cwds) allCwds.add(cwd);
7148
- allApiCalls.push(...apiCalls);
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
- return { toolCalls: allCalls, projectCwds: allCwds, apiCalls: allApiCalls, userMessages: allUserMessages };
7152
- }
7153
- function prewarmOptimizeScan(dateRange) {
7154
- return scanSessions(dateRange);
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 = homedir12();
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
- join24(homedir12(), ".claude", "settings.json"),
7176
- join24(homedir12(), ".claude", "settings.local.json")
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(join24(cwd, ".mcp.json"));
7180
- configPaths.push(join24(cwd, ".claude", "settings.json"));
7181
- configPaths.push(join24(cwd, ".claude", "settings.local.json"));
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 key = `${call.project}:${call.sessionId}`;
7251
- if (!sessionFiles.has(key)) sessionFiles.set(key, /* @__PURE__ */ new Map());
7252
- const fm = sessionFiles.get(key);
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 = basename9(file);
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 = join24(filePath, "..");
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 : join24(dir, 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 = join24(cwd, name);
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 readdir12(dir);
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 readdir12(dir);
7794
+ const entries = await readdir13(dir);
7492
7795
  const names = [];
7493
7796
  for (const entry of entries) {
7494
- if (existsSync5(join24(dir, entry, "SKILL.md"))) names.push(entry);
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(join24(homedir12(), ".claude", "agents"));
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(join24(homedir12(), ".claude", "skills"));
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(join24(homedir12(), ".claude", "commands"));
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 = join24(homedir12(), profile);
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
- var resultCache = /* @__PURE__ */ new Map();
7661
- function cacheKey2(projects, dateRange) {
7662
- const dr = dateRange ? `${dateRange.start.getTime()}-${dateRange.end.getTime()}` : "all";
7663
- const fingerprint2 = projects.length + ":" + projects.reduce((s, p) => s + p.totalApiCalls, 0);
7664
- return `${dr}:${fingerprint2}`;
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 scanAndDetect(projects, dateRange, preScanned) {
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 readdir13 } from "fs/promises";
8211
+ import { readdir as readdir14 } from "fs/promises";
7803
8212
  import { existsSync as existsSync6 } from "fs";
7804
- import { join as join25 } from "path";
7805
- import { homedir as homedir13 } from "os";
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 = homedir13();
8233
+ const home = homedir15();
7825
8234
  const configPaths = [
7826
- join25(home, ".claude", "settings.json"),
7827
- join25(home, ".claude", "settings.local.json")
8235
+ join27(home, ".claude", "settings.json"),
8236
+ join27(home, ".claude", "settings.local.json")
7828
8237
  ];
7829
8238
  if (projectPath) {
7830
- configPaths.push(join25(projectPath, ".mcp.json"));
7831
- configPaths.push(join25(projectPath, ".claude", "settings.json"));
7832
- configPaths.push(join25(projectPath, ".claude", "settings.local.json"));
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 = [join25(homedir13(), ".claude", "skills")];
7850
- if (projectPath) dirs.push(join25(projectPath, ".claude", "skills"));
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 readdir13(dir);
8264
+ const entries = await readdir14(dir);
7856
8265
  for (const entry of entries) {
7857
- const skillFile = join25(dir, entry, "SKILL.md");
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 = homedir13();
8276
+ const home = homedir15();
7868
8277
  const files = [];
7869
8278
  const paths = [
7870
- { path: join25(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
8279
+ { path: join27(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
7871
8280
  ];
7872
8281
  if (projectPath) {
7873
- paths.push({ path: join25(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
7874
- paths.push({ path: join25(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
7875
- paths.push({ path: join25(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
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 readdir13(sessionDir)).filter((f) => f.endsWith(".jsonl"));
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(join25(sessionDir, firstFile));
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, key) => {
8387
+ useInput((input, key2) => {
7979
8388
  if (input === "q") {
7980
8389
  exit();
7981
8390
  return;
7982
8391
  }
7983
- if (key.escape) {
8392
+ if (key2.escape) {
7984
8393
  onBack();
7985
8394
  return;
7986
8395
  }
7987
- if (key.upArrow) {
8396
+ if (key2.upArrow) {
7988
8397
  setCursor((c) => (c - 1 + models.length) % models.length);
7989
8398
  return;
7990
8399
  }
7991
- if (key.downArrow) {
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 (key.return && selected.size === 2) {
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, key) => {
8506
+ useInput((input, key2) => {
8098
8507
  if (input === "q") {
8099
8508
  exit();
8100
8509
  return;
8101
8510
  }
8102
- if (key.escape) {
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, key) => {
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 (key.escape) {
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((sum, call) => sum + call.costUSD, 0);
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 date = new Date(periodStart.getFullYear(), periodStart.getMonth(), periodStart.getDate() + i);
8420
- elapsedDailyCosts.push(dayCosts.get(toLocalDateKey(date)) ?? 0);
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((sum, p) => sum + p.totalCostUSD, 0);
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
- var OWL_ART = [
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: OWL_ART.map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: i < 5 ? GOLD3 : PINK, children: line }, i)) }),
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 = homedir14().replace(/\//g, "-");
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 date = session.firstTimestamp ? session.firstTimestamp.slice(0, TOP_SESSIONS_DATE_LEN) : "----------";
8987
- const label = `${date} ${shortProject(session.projectName)}`;
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
- " Setup: "
10255
+ " "
9134
10256
  ] }),
9135
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: gradeColor, children: healthGrade }),
9136
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
9137
- " (",
9138
- healthScore,
9139
- "/100)"
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 DashboardContent({ projects, period, columns, activeProvider, budgets, planUsage, recovery }) {
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__ */ jsx2(Panel, { title: "Exe Watcher", color: ORANGE3, width: dashWidth, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
9209
- "No usage data found for ",
9210
- PERIOD_LABELS[period],
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(() => setLoadingDots((d) => d % 3 + 1), 400);
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 = join26(homedir14(), ".claude", "projects");
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(join26(claudeDir, project.project));
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
- if (projects.length === 0) {
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) setLoadError(error instanceof Error ? error.message : String(error));
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
- return { recovery: recovery2, projects: filterProjectsByName(data, projectFilter, excludeFilter), usage: await getPlanUsageOrNull(), providers: getDiscoveredProviderNames() };
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, key) => {
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
- void reloadData(period, activeProvider);
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 === "b" || key.escape) && view === "optimize") {
9386
- setLoadError(null);
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 (key.leftArrow) switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length] ?? "today");
9405
- else if (key.rightArrow || key.tab) switchPeriod(PERIODS[(idx + 1) % PERIODS.length] ?? "today");
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" && optimizeResult ? /* @__PURE__ */ jsx2(OptimizeView, { findings: optimizeResult.findings, costRate: optimizeResult.costRate, projects, label: PERIOD_LABELS[period], width: dashWidth, healthScore: optimizeResult.healthScore, healthGrade: optimizeResult.healthGrade }) : view === "optimize" ? /* @__PURE__ */ jsx2(Panel, { title: "Optimization", color: ORANGE3, width: dashWidth, children: /* @__PURE__ */ jsx2(Text2, { children: "Scanning\u2026" }) }) : /* @__PURE__ */ jsx2(DashboardContent, { projects, period, columns, activeProvider, budgets: projectBudgets, planUsage, recovery }),
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 { unmount } = render2(/* @__PURE__ */ jsx2(StaticDashboard, { projects: filteredProjects, period, activeProvider: provider, planUsage: planUsage ?? void 0, recovery }), { patchConsole: false });
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, date) {
9585
- return prev && prev.blockedDate === date ? { blockedDate: date, attempts: prev.attempts + 1 } : { blockedDate: date, attempts: 1 };
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
- function shouldSkipBlockedChunk(cursor2, date, maxAttempts = MAX_BLOCKED_CHUNK_ATTEMPTS) {
9588
- return !!cursor2 && cursor2.blockedDate === date && cursor2.attempts >= maxAttempts;
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(date) {
9625
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
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 = (date, cost, calls) => {
9757
- const previous = dailyMap.get(date) ?? { cost: 0, calls: 0 };
9758
- dailyMap.set(date, { cost: previous.cost + cost, calls: previous.calls + calls });
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 [date, day] of Object.entries(session.reportSummary.daily)) addDaily(date, day.cost, day.calls);
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(([date, day]) => ({
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
- console.log(JSON.stringify(buildJsonReport(projects, label, "custom"), null, 2));
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
- try {
9995
- const historicalProjects = filterProjectsByName(await parseAllSessions({ start, end: yesterdayEnd }, "all", true), opts.project, opts.exclude);
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.cost += historicalRecovery.costUSD;
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 = join27(homedir15(), ".exe-os");
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 = join27(exeOsDir, "agent-stats.json");
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 json = JSON.stringify({ ...payload, costBasis: "api-equivalent", pricing: getPricingMetadata(), historicalRecovery });
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("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", "all").action(async (opts) => {
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 scanPromise = prewarmOptimizeScan(range);
10536
- const projects = await parseAllSessions(range, opts.provider);
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 = join27(homedir15(), ".exe-os");
10546
- const statsPath = join27(exeOsDir, "agent-stats.json");
10547
- const pidPath = join27(exeOsDir, "exed.pid");
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(join27(getCacheDir(), "scan.lock"));
12024
+ if (command.name() === "status") releaseScan = await acquireFileLock(join30(getCacheDir(), "scan.lock"));
10583
12025
  });
10584
12026
  program.hook("postAction", async () => {
10585
12027
  await releaseScan?.();