exe-watcher 0.2.16 → 0.2.46

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
@@ -29,9 +29,15 @@ import { readFile as readFile3, mkdir as mkdir5, rename as rename4, unlink as un
29
29
  import { open as open3 } from "fs/promises";
30
30
  import { join as join6 } from "path";
31
31
  import { randomBytes as randomBytes3 } from "crypto";
32
+ function getPricingWarnings() {
33
+ return _pricingWarnings;
34
+ }
32
35
  function getCachePath() {
33
36
  return join6(getCacheDir(), "litellm-pricing.json");
34
37
  }
38
+ function getLastKnownGoodPath() {
39
+ return join6(getCacheDir(), "pricing-cache.json");
40
+ }
35
41
  function parseLiteLLMEntry(entry) {
36
42
  if (entry.input_cost_per_token === void 0 || entry.output_cost_per_token === void 0) return null;
37
43
  return {
@@ -88,6 +94,27 @@ async function fetchAndCachePricing() {
88
94
  }
89
95
  throw err;
90
96
  }
97
+ try {
98
+ const lkgPath = getLastKnownGoodPath();
99
+ const lkgTmp = `${lkgPath}.${randomBytes3(8).toString("hex")}.tmp`;
100
+ const lkgHandle = await open3(lkgTmp, "w", 384);
101
+ try {
102
+ await lkgHandle.writeFile(JSON.stringify({
103
+ timestamp: Date.now(),
104
+ data: Object.fromEntries(pricing)
105
+ }), { encoding: "utf-8" });
106
+ await lkgHandle.sync();
107
+ } finally {
108
+ await lkgHandle.close();
109
+ }
110
+ await rename4(lkgTmp, lkgPath).catch(() => {
111
+ try {
112
+ unlink2(lkgTmp);
113
+ } catch {
114
+ }
115
+ });
116
+ } catch {
117
+ }
91
118
  return pricing;
92
119
  }
93
120
  async function loadCachedPricing() {
@@ -95,21 +122,45 @@ async function loadCachedPricing() {
95
122
  const raw = await readFile3(getCachePath(), "utf-8");
96
123
  const cached = JSON.parse(raw);
97
124
  if (Date.now() - cached.timestamp > CACHE_TTL_MS2) return null;
98
- return new Map(Object.entries(cached.data));
125
+ return { map: new Map(Object.entries(cached.data)), timestamp: cached.timestamp };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ async function loadLastKnownGoodPricing() {
131
+ try {
132
+ const raw = await readFile3(getLastKnownGoodPath(), "utf-8");
133
+ const cached = JSON.parse(raw);
134
+ return { map: new Map(Object.entries(cached.data)), timestamp: cached.timestamp };
99
135
  } catch {
100
136
  return null;
101
137
  }
102
138
  }
103
139
  async function loadPricing() {
140
+ _pricingWarnings = [];
104
141
  const cached = await loadCachedPricing();
105
142
  if (cached) {
106
- pricingCache = cached;
143
+ pricingCache = cached.map;
144
+ _pricingLastUpdated = cached.timestamp;
107
145
  return;
108
146
  }
109
147
  try {
110
148
  pricingCache = await fetchAndCachePricing();
149
+ _pricingLastUpdated = Date.now();
111
150
  } catch {
112
- pricingCache = new Map(Object.entries(FALLBACK_PRICING));
151
+ const lkg = await loadLastKnownGoodPricing();
152
+ if (lkg) {
153
+ pricingCache = lkg.map;
154
+ _pricingLastUpdated = lkg.timestamp;
155
+ const ageDays = Math.floor((Date.now() - lkg.timestamp) / (24 * 60 * 60 * 1e3));
156
+ if (Date.now() - lkg.timestamp > STALENESS_WARN_MS) {
157
+ _pricingWarnings.push(`Model pricing data may be outdated (last updated ${ageDays} days ago)`);
158
+ }
159
+ } else {
160
+ pricingCache = new Map(Object.entries(FALLBACK_PRICING));
161
+ _pricingLastUpdated = null;
162
+ _pricingWarnings.push("Using hardcoded fallback pricing \u2014 could not fetch or load cached pricing");
163
+ }
113
164
  }
114
165
  }
115
166
  function setModelAliases(aliases) {
@@ -214,7 +265,7 @@ function applyContextTier(costs, inputTokens) {
214
265
  cacheReadCostPerToken: tier.cacheReadCostPerToken ?? costs.cacheReadCostPerToken
215
266
  };
216
267
  }
217
- var LITELLM_URL, CACHE_TTL_MS2, WEB_SEARCH_COST, LONG_CONTEXT_1M_THRESHOLD_TOKENS, GEMINI_LONG_CONTEXT_THRESHOLD_TOKENS, DIRECT_PROVIDER_PREFIXES, FALLBACK_PRICING, pricingCache, BUILTIN_ALIASES, userAliases;
268
+ var LITELLM_URL, CACHE_TTL_MS2, WEB_SEARCH_COST, LONG_CONTEXT_1M_THRESHOLD_TOKENS, GEMINI_LONG_CONTEXT_THRESHOLD_TOKENS, DIRECT_PROVIDER_PREFIXES, FALLBACK_PRICING, pricingCache, _pricingWarnings, _pricingLastUpdated, STALENESS_WARN_MS, BUILTIN_ALIASES, userAliases;
218
269
  var init_models = __esm({
219
270
  "src/models.ts"() {
220
271
  "use strict";
@@ -308,6 +359,9 @@ var init_models = __esm({
308
359
  "MiniMax-M2.7": { inputCostPerToken: 3e-7, outputCostPerToken: 12e-7, cacheWriteCostPerToken: 375e-9, cacheReadCostPerToken: 6e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }
309
360
  };
310
361
  pricingCache = null;
362
+ _pricingWarnings = [];
363
+ _pricingLastUpdated = null;
364
+ STALENESS_WARN_MS = 7 * 24 * 60 * 60 * 1e3;
311
365
  BUILTIN_ALIASES = {
312
366
  "anthropic--claude-4.6-opus": "claude-opus-4-6",
313
367
  "anthropic--claude-4.6-sonnet": "claude-sonnet-4-6",
@@ -320,7 +374,7 @@ var init_models = __esm({
320
374
  });
321
375
 
322
376
  // src/bash-utils.ts
323
- import { basename as basename4 } from "path";
377
+ import { basename as basename5 } from "path";
324
378
  function stripQuotedStrings(command) {
325
379
  return command.replace(/"[^"]*"|'[^']*'/g, (match) => " ".repeat(match.length));
326
380
  }
@@ -347,7 +401,7 @@ function extractBashCommands(command) {
347
401
  const tokens = segment.split(/\s+/);
348
402
  let i = 0;
349
403
  while (i < tokens.length && /^\w+=/.test(tokens[i])) i++;
350
- const base = i < tokens.length ? basename4(tokens[i]) : "";
404
+ const base = i < tokens.length ? basename5(tokens[i]) : "";
351
405
  if (base && base !== "cd" && base !== "true" && base !== "false") {
352
406
  commands.push(base);
353
407
  }
@@ -456,7 +510,7 @@ Upgrade Node (https://nodejs.org) and run exe-watcher again.
456
510
  (underlying error: ${message})`;
457
511
  return false;
458
512
  } finally {
459
- restore();
513
+ setTimeout(restore, 0);
460
514
  }
461
515
  }
462
516
  function isSqliteAvailable() {
@@ -968,7 +1022,7 @@ __export(cursor_agent_exports, {
968
1022
  import { createHash as createHash2 } from "crypto";
969
1023
  import { existsSync as existsSync2 } from "fs";
970
1024
  import { readdir as readdir7, readFile as readFile6, stat as stat9 } from "fs/promises";
971
- import { join as join14, basename as basename6 } from "path";
1025
+ import { join as join14, basename as basename7 } from "path";
972
1026
  import { homedir as homedir10 } from "os";
973
1027
  function getCursorAgentBaseDir(baseDirOverride) {
974
1028
  if (baseDirOverride) return baseDirOverride;
@@ -1027,7 +1081,7 @@ function resolveModel3(raw) {
1027
1081
  return raw;
1028
1082
  }
1029
1083
  function toConversationId(transcriptPath) {
1030
- const filename = basename6(transcriptPath, ".txt");
1084
+ const filename = basename7(transcriptPath, ".txt");
1031
1085
  if (filename.length === 36 && UUID_LIKE.test(filename)) return filename;
1032
1086
  return createHash2("sha1").update(transcriptPath).digest("hex").slice(0, 16);
1033
1087
  }
@@ -1160,7 +1214,7 @@ function createParser6(source, seenKeys, dbPath, summariesByConversationId) {
1160
1214
  const transcript = await readFile6(source.path, "utf-8");
1161
1215
  const parsed = parseTranscript(transcript);
1162
1216
  if (!parsed.recognized) {
1163
- process.stderr.write(`exe-watcher: skipped ${basename6(source.path)}: unrecognized cursor-agent transcript format
1217
+ process.stderr.write(`exe-watcher: skipped ${basename7(source.path)}: unrecognized cursor-agent transcript format
1164
1218
  `);
1165
1219
  return;
1166
1220
  }
@@ -1221,7 +1275,7 @@ function createCursorAgentProvider(baseDirOverride) {
1221
1275
  name: "cursor-agent",
1222
1276
  displayName: "Cursor Agent",
1223
1277
  modelDisplayName(model) {
1224
- if (model === "default") return modelDisplayNames5.default;
1278
+ if (model === "default") return modelDisplayNames5["default"] ?? "Auto (Sonnet est.)";
1225
1279
  const label = modelDisplayNames5[model] ?? model;
1226
1280
  return `${label} (est.)`;
1227
1281
  },
@@ -1298,8 +1352,8 @@ var init_cursor_agent = __esm({
1298
1352
  });
1299
1353
 
1300
1354
  // src/cli.ts
1301
- import { existsSync as existsSync6, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
1302
- import { join as join21 } from "path";
1355
+ import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1356
+ import { join as join23 } from "path";
1303
1357
  import { homedir as homedir14 } from "os";
1304
1358
  import { Command } from "commander";
1305
1359
 
@@ -1308,15 +1362,19 @@ import { spawn } from "child_process";
1308
1362
  import { createWriteStream } from "fs";
1309
1363
  import { cp, mkdir, mkdtemp, rename, rm, stat } from "fs/promises";
1310
1364
  import { homedir, platform, tmpdir } from "os";
1311
- import { join } from "path";
1365
+ import { basename, join } from "path";
1312
1366
  import { pipeline } from "stream/promises";
1313
1367
  import { Readable } from "stream";
1314
- var RELEASE_API = "https://api.github.com/repos/AskExe/exe-watcher/releases/latest";
1368
+ var RELEASE_API_LATEST = "https://api.github.com/repos/AskExe/exe-watcher/releases/latest";
1369
+ var RELEASE_API_BY_TAG = "https://api.github.com/repos/AskExe/exe-watcher/releases/tags/";
1315
1370
  var APP_BUNDLE_NAME = "Watcher by EXE.app";
1316
1371
  var ASSET_PATTERN = /^ExeWatcherMenubar-.*\.zip$/;
1317
1372
  var APP_PROCESS_NAME = "ExeWatcherMenubar";
1318
1373
  var SUPPORTED_OS = "darwin";
1319
1374
  var MIN_MACOS_MAJOR = 14;
1375
+ var DEFAULT_APP_EXIT_TIMEOUT_MS = 5e3;
1376
+ var DEFAULT_APP_LAUNCH_TIMEOUT_MS = 5e3;
1377
+ var POLL_INTERVAL_MS = 150;
1320
1378
  function userApplicationsDir() {
1321
1379
  return join(homedir(), "Applications");
1322
1380
  }
@@ -1351,14 +1409,26 @@ async function sysProductVersion() {
1351
1409
  });
1352
1410
  });
1353
1411
  }
1354
- async function fetchLatestReleaseAsset() {
1355
- const response = await fetch(RELEASE_API, {
1412
+ async function fetchLatestReleaseAsset(version2) {
1413
+ const tag = version2 ? version2.startsWith("mac-") ? version2 : `mac-${version2.startsWith("v") ? version2 : `v${version2}`}` : void 0;
1414
+ const url = tag ? `${RELEASE_API_BY_TAG}${tag}` : RELEASE_API_LATEST;
1415
+ const response = await fetch(url, {
1356
1416
  headers: {
1357
1417
  // Identify the installer so GitHub's abuse heuristics treat us as a known client.
1358
1418
  "User-Agent": "exe-watcher-installer",
1359
1419
  Accept: "application/vnd.github+json"
1360
- }
1420
+ },
1421
+ signal: AbortSignal.timeout(1e4)
1361
1422
  });
1423
+ if (response.status === 403) {
1424
+ const resetHeader = response.headers.get("X-RateLimit-Reset");
1425
+ if (resetHeader) {
1426
+ const resetTimestamp = Number(resetHeader);
1427
+ const waitMinutes = Math.max(1, Math.ceil((resetTimestamp - Date.now() / 1e3) / 60));
1428
+ throw new Error(`GitHub API rate limit reached. Try again in ${waitMinutes} minutes.`);
1429
+ }
1430
+ throw new Error("GitHub API rate limit reached. Try again later.");
1431
+ }
1362
1432
  if (!response.ok) {
1363
1433
  throw new Error(`GitHub release lookup failed: HTTP ${response.status}`);
1364
1434
  }
@@ -1369,6 +1439,12 @@ async function fetchLatestReleaseAsset() {
1369
1439
  `No ${APP_BUNDLE_NAME} zip found in release ${body.tag_name}. Check https://github.com/AskExe/exe-watcher/releases.`
1370
1440
  );
1371
1441
  }
1442
+ const allowedPrefixes = ["https://github.com/", "https://objects.githubusercontent.com/"];
1443
+ if (!allowedPrefixes.some((prefix) => asset.browser_download_url.startsWith(prefix))) {
1444
+ throw new Error(
1445
+ `Untrusted download URL: ${asset.browser_download_url}. Expected a GitHub-hosted asset URL.`
1446
+ );
1447
+ }
1372
1448
  return asset;
1373
1449
  }
1374
1450
  async function downloadToFile(url, destPath) {
@@ -1392,20 +1468,96 @@ async function runCommand(command, args) {
1392
1468
  });
1393
1469
  });
1394
1470
  }
1395
- async function isAppRunning() {
1471
+ async function sleep(ms) {
1472
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
1473
+ }
1474
+ async function captureCommand(command, args) {
1396
1475
  return new Promise((resolve2) => {
1397
- const proc = spawn("/usr/bin/pgrep", ["-f", APP_PROCESS_NAME]);
1398
- proc.on("close", (code) => resolve2(code === 0));
1399
- proc.on("error", () => resolve2(false));
1476
+ const proc = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
1477
+ let stdout = "";
1478
+ proc.stdout?.on("data", (chunk) => {
1479
+ stdout += chunk.toString();
1480
+ });
1481
+ proc.on("error", () => resolve2({ code: 1, stdout: "" }));
1482
+ proc.on("close", (code) => resolve2({ code: code ?? 1, stdout }));
1400
1483
  });
1401
1484
  }
1402
- async function killRunningApp() {
1485
+ function parsePids(output) {
1486
+ return output.split(/\s+/).map((s) => Number(s)).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid);
1487
+ }
1488
+ async function runningAppPids() {
1489
+ const result = await captureCommand("/usr/bin/pgrep", ["-f", APP_PROCESS_NAME]);
1490
+ return result.code === 0 ? parsePids(result.stdout) : [];
1491
+ }
1492
+ async function isAppRunning() {
1493
+ return (await runningAppPids()).length > 0;
1494
+ }
1495
+ function timeoutFromEnv(name, fallback) {
1496
+ const value = Number(process.env[name] ?? "");
1497
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
1498
+ }
1499
+ async function waitForNoRunningApp(timeoutMs = timeoutFromEnv("EXE_WATCHER_APP_EXIT_TIMEOUT_MS", DEFAULT_APP_EXIT_TIMEOUT_MS)) {
1500
+ const deadline = Date.now() + timeoutMs;
1501
+ while (Date.now() < deadline) {
1502
+ if ((await runningAppPids()).length === 0) return true;
1503
+ await sleep(POLL_INTERVAL_MS);
1504
+ }
1505
+ return (await runningAppPids()).length === 0;
1506
+ }
1507
+ async function waitForRunningApp(timeoutMs = timeoutFromEnv("EXE_WATCHER_APP_LAUNCH_TIMEOUT_MS", DEFAULT_APP_LAUNCH_TIMEOUT_MS)) {
1508
+ const deadline = Date.now() + timeoutMs;
1509
+ while (Date.now() < deadline) {
1510
+ if ((await runningAppPids()).length > 0) return true;
1511
+ await sleep(POLL_INTERVAL_MS);
1512
+ }
1513
+ return (await runningAppPids()).length > 0;
1514
+ }
1515
+ async function signalPids(signal, pids) {
1516
+ if (pids.length === 0) return;
1403
1517
  await new Promise((resolve2) => {
1404
- const proc = spawn("/usr/bin/pkill", ["-f", APP_PROCESS_NAME]);
1518
+ const proc = spawn("/bin/kill", [`-${signal}`, ...pids.map(String)]);
1405
1519
  proc.on("close", () => resolve2());
1406
1520
  proc.on("error", () => resolve2());
1407
1521
  });
1408
1522
  }
1523
+ async function movePath(source, dest) {
1524
+ try {
1525
+ await rename(source, dest);
1526
+ } catch (err) {
1527
+ if (err.code === "EXDEV") {
1528
+ await cp(source, dest, { recursive: true });
1529
+ await rm(source, { recursive: true, force: true });
1530
+ } else {
1531
+ throw err;
1532
+ }
1533
+ }
1534
+ }
1535
+ async function restoreExistingApp(backupPath, targetPath) {
1536
+ if (!await exists(backupPath)) return;
1537
+ await killRunningApp().catch(() => {
1538
+ });
1539
+ await rm(targetPath, { recursive: true, force: true }).catch(() => {
1540
+ });
1541
+ await movePath(backupPath, targetPath);
1542
+ await runCommand("/usr/bin/open", [targetPath]).catch(() => {
1543
+ });
1544
+ await waitForRunningApp().catch(() => false);
1545
+ }
1546
+ async function killRunningApp() {
1547
+ const initialPids = await runningAppPids();
1548
+ if (initialPids.length === 0) return;
1549
+ console.log(`Stopping existing Exe Watcher Menubar (${initialPids.join(", ")})...`);
1550
+ await signalPids("TERM", initialPids);
1551
+ if (await waitForNoRunningApp()) return;
1552
+ const stubbornPids = await runningAppPids();
1553
+ if (stubbornPids.length > 0) {
1554
+ console.log(`Force-stopping stale Exe Watcher Menubar (${stubbornPids.join(", ")})...`);
1555
+ await signalPids("KILL", stubbornPids);
1556
+ }
1557
+ if (!await waitForNoRunningApp()) {
1558
+ throw new Error("Could not stop the existing Exe Watcher Menubar process. Please quit it and retry.");
1559
+ }
1560
+ }
1409
1561
  async function installMenubarApp(options = {}) {
1410
1562
  await ensureSupportedPlatform();
1411
1563
  const appsDir = userApplicationsDir();
@@ -1417,8 +1569,8 @@ async function installMenubarApp(options = {}) {
1417
1569
  }
1418
1570
  return { installedPath: targetPath, launched: true };
1419
1571
  }
1420
- console.log("Looking up the latest Exe Watcher Menubar release...");
1421
- const asset = await fetchLatestReleaseAsset();
1572
+ console.log(options.version ? `Looking up Exe Watcher Menubar release ${options.version}...` : "Looking up the latest Exe Watcher Menubar release...");
1573
+ const asset = await fetchLatestReleaseAsset(options.version);
1422
1574
  const stagingDir = await mkdtemp(join(tmpdir(), "exe-watcher-menubar-"));
1423
1575
  try {
1424
1576
  const archivePath = join(stagingDir, asset.name);
@@ -1433,23 +1585,33 @@ async function installMenubarApp(options = {}) {
1433
1585
  await runCommand("/usr/bin/xattr", ["-dr", "com.apple.quarantine", unpackedApp]).catch(() => {
1434
1586
  });
1435
1587
  await mkdir(appsDir, { recursive: true });
1436
- if (alreadyInstalled) {
1437
- await killRunningApp();
1438
- await rm(targetPath, { recursive: true, force: true });
1439
- }
1588
+ const backupPath = alreadyInstalled ? join(appsDir, `${basename(APP_BUNDLE_NAME, ".app")}.backup-${Date.now()}.app`) : void 0;
1589
+ let targetReplaced = false;
1440
1590
  try {
1441
- await rename(unpackedApp, targetPath);
1591
+ if (alreadyInstalled && backupPath) {
1592
+ await killRunningApp();
1593
+ await movePath(targetPath, backupPath);
1594
+ }
1595
+ await movePath(unpackedApp, targetPath);
1596
+ targetReplaced = true;
1597
+ console.log("Launching Exe Watcher Menubar...");
1598
+ await runCommand("/usr/bin/open", [targetPath]);
1599
+ if (!await waitForRunningApp()) {
1600
+ throw new Error("Installed Exe Watcher Menubar, but the app did not launch.");
1601
+ }
1602
+ if (backupPath) {
1603
+ await rm(backupPath, { recursive: true, force: true }).catch(() => {
1604
+ });
1605
+ }
1606
+ return { installedPath: targetPath, launched: true };
1442
1607
  } catch (err) {
1443
- if (err.code === "EXDEV") {
1444
- await cp(unpackedApp, targetPath, { recursive: true });
1445
- await rm(unpackedApp, { recursive: true, force: true });
1446
- } else {
1447
- throw err;
1608
+ if (backupPath && (targetReplaced || await exists(backupPath))) {
1609
+ await restoreExistingApp(backupPath, targetPath);
1610
+ const detail = err instanceof Error ? err.message : String(err);
1611
+ throw new Error(`${detail} Restored the previous working Exe Watcher Menubar; your menu bar app was not left broken.`);
1448
1612
  }
1613
+ throw err;
1449
1614
  }
1450
- console.log("Launching Exe Watcher Menubar...");
1451
- await runCommand("/usr/bin/open", [targetPath]);
1452
- return { installedPath: targetPath, launched: true };
1453
1615
  } finally {
1454
1616
  await rm(stagingDir, { recursive: true, force: true });
1455
1617
  }
@@ -1563,7 +1725,9 @@ function getRateCachePath() {
1563
1725
  return join4(getCacheDir(), "exchange-rate.json");
1564
1726
  }
1565
1727
  async function fetchRate(code) {
1566
- const response = await fetch(`${FRANKFURTER_URL}${code}`);
1728
+ const response = await fetch(`${FRANKFURTER_URL}${code}`, {
1729
+ signal: AbortSignal.timeout(5e3)
1730
+ });
1567
1731
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1568
1732
  const data = await response.json();
1569
1733
  const rate = data.rates?.[code];
@@ -1895,7 +2059,9 @@ function escCsv(s) {
1895
2059
  }
1896
2060
  function rowsToCsv(rows) {
1897
2061
  if (rows.length === 0) return "";
1898
- const headers = Object.keys(rows[0]);
2062
+ const firstRow = rows[0];
2063
+ if (!firstRow) return "";
2064
+ const headers = Object.keys(firstRow);
1899
2065
  const lines = [headers.map(escCsv).join(",")];
1900
2066
  for (const row of rows) {
1901
2067
  lines.push(headers.map((h) => escCsv(String(row[h] ?? ""))).join(","));
@@ -2176,8 +2342,8 @@ async function exportJson(periods, outputPath) {
2176
2342
  init_models();
2177
2343
 
2178
2344
  // src/parser.ts
2179
- import { readdir as readdir8, stat as stat10 } from "fs/promises";
2180
- import { basename as basename7, join as join15 } from "path";
2345
+ import { readdir as readdir8, stat as stat11 } from "fs/promises";
2346
+ import { basename as basename8, join as join16 } from "path";
2181
2347
 
2182
2348
  // src/fs-utils.ts
2183
2349
  import { readFile as readFile4, stat as stat3 } from "fs/promises";
@@ -2254,7 +2420,7 @@ function readSessionFileSync(filePath) {
2254
2420
  closeSync(fd);
2255
2421
  }
2256
2422
  }
2257
- async function* readSessionLines(filePath) {
2423
+ async function* readSessionLines(filePath, startByte) {
2258
2424
  let size;
2259
2425
  try {
2260
2426
  size = (await stat3(filePath)).size;
@@ -2266,7 +2432,7 @@ async function* readSessionLines(filePath) {
2266
2432
  warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`);
2267
2433
  return;
2268
2434
  }
2269
- const stream = createReadStream(filePath, { encoding: "utf-8" });
2435
+ const stream = createReadStream(filePath, { encoding: "utf-8", start: startByte ?? 0 });
2270
2436
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
2271
2437
  try {
2272
2438
  for await (const line of rl) yield line;
@@ -2282,7 +2448,7 @@ init_models();
2282
2448
 
2283
2449
  // src/providers/claude.ts
2284
2450
  import { readdir as readdir2, stat as stat4 } from "fs/promises";
2285
- import { basename, join as join7 } from "path";
2451
+ import { basename as basename2, join as join7 } from "path";
2286
2452
  import { homedir as homedir4 } from "os";
2287
2453
  var shortNames = {
2288
2454
  "claude-opus-4-7": "Opus 4.7",
@@ -2363,7 +2529,7 @@ var claude = {
2363
2529
  }
2364
2530
  const desktopDirs = await findDesktopProjectDirs(getDesktopSessionsDir());
2365
2531
  for (const dirPath of desktopDirs) {
2366
- sources.push({ path: dirPath, project: basename(dirPath), provider: "claude" });
2532
+ sources.push({ path: dirPath, project: basename2(dirPath), provider: "claude" });
2367
2533
  }
2368
2534
  return sources;
2369
2535
  },
@@ -2377,7 +2543,7 @@ var claude = {
2377
2543
 
2378
2544
  // src/providers/codex.ts
2379
2545
  import { readdir as readdir3, stat as stat5 } from "fs/promises";
2380
- import { basename as basename2, join as join8 } from "path";
2546
+ import { basename as basename3, join as join8 } from "path";
2381
2547
  import { homedir as homedir5 } from "os";
2382
2548
  init_models();
2383
2549
  var modelDisplayNames = {
@@ -2465,11 +2631,16 @@ function resolveModel(info, sessionModel) {
2465
2631
  return info?.info?.model ?? info?.info?.model_name ?? sessionModel ?? "gpt-5";
2466
2632
  }
2467
2633
  function createParser(source, seenKeys) {
2634
+ const parserWarnings = [];
2468
2635
  return {
2636
+ warnings: parserWarnings,
2469
2637
  async *parse() {
2470
2638
  const content = await readSessionFile(source.path);
2471
2639
  if (content === null) return;
2472
2640
  const lines = content.split("\n").filter((l) => l.trim());
2641
+ if (lines.length === 0) return;
2642
+ let hasSessionMeta = false;
2643
+ let hasTokenCounts = false;
2473
2644
  let sessionModel;
2474
2645
  let sessionId = "";
2475
2646
  let prevCumulativeTotal = 0;
@@ -2487,7 +2658,8 @@ function createParser(source, seenKeys) {
2487
2658
  continue;
2488
2659
  }
2489
2660
  if (entry.type === "session_meta") {
2490
- sessionId = entry.payload?.session_id ?? basename2(source.path, ".jsonl");
2661
+ hasSessionMeta = true;
2662
+ sessionId = entry.payload?.session_id ?? basename3(source.path, ".jsonl");
2491
2663
  sessionModel = entry.payload?.model;
2492
2664
  continue;
2493
2665
  }
@@ -2506,6 +2678,7 @@ function createParser(source, seenKeys) {
2506
2678
  continue;
2507
2679
  }
2508
2680
  if (entry.type === "event_msg" && entry.payload?.type === "token_count") {
2681
+ hasTokenCounts = true;
2509
2682
  const info = entry.payload.info;
2510
2683
  if (!info) continue;
2511
2684
  const cumulativeTotal = info.total_token_usage?.total_tokens ?? 0;
@@ -2581,6 +2754,12 @@ function createParser(source, seenKeys) {
2581
2754
  pendingUserMessage = "";
2582
2755
  }
2583
2756
  }
2757
+ if (lines.length > 5 && !hasSessionMeta) {
2758
+ parserWarnings.push(`Codex session file format may have changed \u2014 missing session_meta (${basename3(source.path)})`);
2759
+ }
2760
+ if (lines.length > 5 && hasSessionMeta && !hasTokenCounts) {
2761
+ parserWarnings.push(`Codex session file format may have changed \u2014 no token_count events found (${basename3(source.path)})`);
2762
+ }
2584
2763
  }
2585
2764
  };
2586
2765
  }
@@ -2610,7 +2789,7 @@ var codex = createCodexProvider();
2610
2789
 
2611
2790
  // src/providers/copilot.ts
2612
2791
  import { readdir as readdir4, stat as stat6 } from "fs/promises";
2613
- import { basename as basename3, dirname as dirname2, join as join9 } from "path";
2792
+ import { basename as basename4, dirname as dirname2, join as join9 } from "path";
2614
2793
  import { homedir as homedir6 } from "os";
2615
2794
  init_models();
2616
2795
  var modelDisplayNames2 = {
@@ -2657,7 +2836,7 @@ function createParser2(source, seenKeys) {
2657
2836
  async *parse() {
2658
2837
  const content = await readSessionFile(source.path);
2659
2838
  if (content === null) return;
2660
- const sessionId = basename3(dirname2(source.path));
2839
+ const sessionId = basename4(dirname2(source.path));
2661
2840
  const lines = content.split("\n").filter((l) => l.trim());
2662
2841
  let currentModel = "";
2663
2842
  let pendingUserMessage = "";
@@ -2726,7 +2905,7 @@ async function discoverSessionsInDir2(sessionStateDir) {
2726
2905
  const yaml = await readSessionFile(join9(sessionStateDir, sessionId, "workspace.yaml"));
2727
2906
  if (yaml !== null) {
2728
2907
  const cwd = parseCwd(yaml);
2729
- if (cwd) project = basename3(cwd);
2908
+ if (cwd) project = basename4(cwd);
2730
2909
  }
2731
2910
  sources.push({ path: eventsPath, project, provider: "copilot" });
2732
2911
  }
@@ -2758,7 +2937,7 @@ var copilot = createCopilotProvider();
2758
2937
 
2759
2938
  // src/providers/pi.ts
2760
2939
  import { readdir as readdir5, stat as stat7 } from "fs/promises";
2761
- import { basename as basename5, join as join10 } from "path";
2940
+ import { basename as basename6, join as join10 } from "path";
2762
2941
  import { homedir as homedir7 } from "os";
2763
2942
  init_models();
2764
2943
  init_bash_utils();
@@ -2827,7 +3006,7 @@ async function discoverSessionsInDir3(sessionsDir, providerName) {
2827
3006
  const first = await readFirstEntry(filePath);
2828
3007
  if (!first || first.type !== "session") continue;
2829
3008
  const cwd = first.cwd ?? dirName;
2830
- sources.push({ path: filePath, project: basename5(cwd), provider: providerName });
3009
+ sources.push({ path: filePath, project: basename6(cwd), provider: providerName });
2831
3010
  }
2832
3011
  }
2833
3012
  return sources;
@@ -2838,7 +3017,7 @@ function createParser3(source, seenKeys) {
2838
3017
  const content = await readSessionFile(source.path);
2839
3018
  if (content === null) return;
2840
3019
  const lines = content.split("\n").filter((l) => l.trim());
2841
- let sessionId = basename5(source.path, ".jsonl");
3020
+ let sessionId = basename6(source.path, ".jsonl");
2842
3021
  let pendingUserMessage = "";
2843
3022
  for (const [lineIdx, line] of lines.entries()) {
2844
3023
  let entry;
@@ -2995,15 +3174,44 @@ async function getAllProviders() {
2995
3174
  if (cursorAgent) all.push(cursorAgent);
2996
3175
  return all;
2997
3176
  }
3177
+ var _lastDiscoveryWarnings = [];
3178
+ function getDiscoveryWarnings() {
3179
+ return _lastDiscoveryWarnings;
3180
+ }
3181
+ var PROVIDER_TIMEOUT_MS = 5e3;
3182
+ function withTimeout(promise, ms, label) {
3183
+ return Promise.race([
3184
+ promise.then((result) => ({ result, warning: null })),
3185
+ new Promise(
3186
+ (resolve2) => setTimeout(() => resolve2({ result: null, warning: `Provider "${label}" timed out after ${ms}ms \u2014 skipped` }), ms)
3187
+ )
3188
+ ]);
3189
+ }
2998
3190
  async function discoverAllSessions(providerFilter) {
3191
+ _lastDiscoveryWarnings = [];
2999
3192
  const allProviders = await getAllProviders();
3000
3193
  const filtered = providerFilter && providerFilter !== "all" ? allProviders.filter((p) => p.name === providerFilter) : allProviders;
3001
- const all = [];
3002
- for (const provider of filtered) {
3003
- const sessions = await provider.discoverSessions();
3004
- all.push(...sessions);
3005
- }
3006
- return all;
3194
+ const results = await Promise.all(
3195
+ filtered.map(async (provider) => {
3196
+ try {
3197
+ const { result, warning } = await withTimeout(
3198
+ provider.discoverSessions(),
3199
+ PROVIDER_TIMEOUT_MS,
3200
+ provider.name
3201
+ );
3202
+ if (warning) {
3203
+ _lastDiscoveryWarnings.push(warning);
3204
+ return [];
3205
+ }
3206
+ return result ?? [];
3207
+ } catch (err) {
3208
+ const msg = err instanceof Error ? err.message : String(err);
3209
+ _lastDiscoveryWarnings.push(`Provider "${provider.name}" discovery failed: ${msg}`);
3210
+ return [];
3211
+ }
3212
+ })
3213
+ );
3214
+ return results.flat();
3007
3215
  }
3008
3216
  async function getProvider(name) {
3009
3217
  if (name === "cursor") {
@@ -3148,8 +3356,97 @@ function classifyTurn(turn) {
3148
3356
  return { ...turn, category, retries: countRetries(turn), hasEdits: turnHasEdits(turn) };
3149
3357
  }
3150
3358
 
3359
+ // src/session-index.ts
3360
+ init_cache_dir();
3361
+ import { stat as stat10 } from "fs/promises";
3362
+ import { existsSync as existsSync3, unlinkSync } from "fs";
3363
+ import { readFile as readFile7, mkdir as mkdir7, rename as rename6, unlink as unlink3, open as open5 } from "fs/promises";
3364
+ import { join as join15 } from "path";
3365
+ import { randomBytes as randomBytes5 } from "crypto";
3366
+ var SESSION_INDEX_VERSION = 1;
3367
+ var INDEX_FILENAME = "session-index.json";
3368
+ var currentIndex = null;
3369
+ function emptyIndex() {
3370
+ return { v: SESSION_INDEX_VERSION, e: {} };
3371
+ }
3372
+ function indexPath() {
3373
+ return join15(getCacheDir(), INDEX_FILENAME);
3374
+ }
3375
+ async function loadSessionIndex() {
3376
+ if (currentIndex) return currentIndex;
3377
+ try {
3378
+ const raw = await readFile7(indexPath(), "utf-8");
3379
+ const parsed = JSON.parse(raw);
3380
+ if (parsed.v !== SESSION_INDEX_VERSION) {
3381
+ currentIndex = emptyIndex();
3382
+ return currentIndex;
3383
+ }
3384
+ currentIndex = parsed;
3385
+ return currentIndex;
3386
+ } catch {
3387
+ currentIndex = emptyIndex();
3388
+ return currentIndex;
3389
+ }
3390
+ }
3391
+ async function saveSessionIndex(index) {
3392
+ const dir = getCacheDir();
3393
+ if (!existsSync3(dir)) await mkdir7(dir, { recursive: true });
3394
+ const finalPath = indexPath();
3395
+ const tempPath = `${finalPath}.${randomBytes5(8).toString("hex")}.tmp`;
3396
+ const payload = JSON.stringify(index);
3397
+ const handle = await open5(tempPath, "w", 384);
3398
+ try {
3399
+ await handle.writeFile(payload, { encoding: "utf-8" });
3400
+ await handle.sync();
3401
+ } finally {
3402
+ await handle.close();
3403
+ }
3404
+ try {
3405
+ await rename6(tempPath, finalPath);
3406
+ } catch (err) {
3407
+ try {
3408
+ await unlink3(tempPath);
3409
+ } catch {
3410
+ }
3411
+ throw err;
3412
+ }
3413
+ currentIndex = index;
3414
+ }
3415
+ async function checkSessionFile(filePath, index, dateRange) {
3416
+ const entry = index.e[filePath];
3417
+ if (!entry) return "parse";
3418
+ let fileStat;
3419
+ try {
3420
+ fileStat = await stat10(filePath);
3421
+ } catch {
3422
+ return "parse";
3423
+ }
3424
+ if (fileStat.size !== entry.s || fileStat.mtimeMs !== entry.m) return "parse";
3425
+ if (entry.h === 0) return "skip";
3426
+ return "parse";
3427
+ }
3428
+ function recordParseResult(filePath, index, sizeBytes, mtimeMs, hasApiCalls) {
3429
+ const prev = index.e[filePath];
3430
+ const h = hasApiCalls ? 1 : prev?.h === 1 ? 1 : 0;
3431
+ index.e[filePath] = { s: sizeBytes, m: mtimeMs, h };
3432
+ }
3433
+ function pruneIndex(index, knownPaths) {
3434
+ let pruned = 0;
3435
+ for (const path of Object.keys(index.e)) {
3436
+ if (!knownPaths.has(path)) {
3437
+ delete index.e[path];
3438
+ pruned++;
3439
+ }
3440
+ }
3441
+ return pruned;
3442
+ }
3443
+
3151
3444
  // src/parser.ts
3152
3445
  init_bash_utils();
3446
+ var _parseWarnings = [];
3447
+ function getParseWarnings() {
3448
+ return _parseWarnings;
3449
+ }
3153
3450
  function unsanitizePath(dirName) {
3154
3451
  return dirName.replace(/-/g, "/");
3155
3452
  }
@@ -3356,7 +3653,7 @@ function buildSessionSummary(sessionId, project, turns) {
3356
3653
  async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
3357
3654
  if (dateRange) {
3358
3655
  try {
3359
- const s = await stat10(filePath);
3656
+ const s = await stat11(filePath);
3360
3657
  if (s.mtimeMs < dateRange.start.getTime()) return null;
3361
3658
  } catch {
3362
3659
  }
@@ -3370,7 +3667,7 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
3370
3667
  }
3371
3668
  if (!hasLines) return null;
3372
3669
  if (entries.length === 0) return null;
3373
- const sessionId = basename7(filePath, ".jsonl");
3670
+ const sessionId = basename8(filePath, ".jsonl");
3374
3671
  let turns = groupIntoTurns(entries, seenMsgIds);
3375
3672
  if (dateRange) {
3376
3673
  turns = turns.filter((turn) => {
@@ -3387,23 +3684,46 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
3387
3684
  }
3388
3685
  async function collectJsonlFiles(dirPath) {
3389
3686
  const files = await readdir8(dirPath).catch(() => []);
3390
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => join15(dirPath, f));
3687
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => join16(dirPath, f));
3391
3688
  for (const entry of files) {
3392
3689
  if (entry.endsWith(".jsonl")) continue;
3393
- const subagentsPath = join15(dirPath, entry, "subagents");
3690
+ const subagentsPath = join16(dirPath, entry, "subagents");
3394
3691
  const subFiles = await readdir8(subagentsPath).catch(() => []);
3395
3692
  for (const sf of subFiles) {
3396
- if (sf.endsWith(".jsonl")) jsonlFiles.push(join15(subagentsPath, sf));
3693
+ if (sf.endsWith(".jsonl")) jsonlFiles.push(join16(subagentsPath, sf));
3397
3694
  }
3398
3695
  }
3399
3696
  return jsonlFiles;
3400
3697
  }
3401
3698
  async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
3699
+ const sessionIndex = await loadSessionIndex();
3402
3700
  const projectMap = /* @__PURE__ */ new Map();
3701
+ let indexDirty = false;
3702
+ const allJsonlPaths = /* @__PURE__ */ new Set();
3403
3703
  for (const { path: dirPath, name: dirName } of dirs) {
3404
3704
  const jsonlFiles = await collectJsonlFiles(dirPath);
3705
+ for (const f of jsonlFiles) allJsonlPaths.add(f);
3405
3706
  for (const filePath of jsonlFiles) {
3707
+ const action = await checkSessionFile(filePath, sessionIndex, dateRange);
3708
+ if (action === "skip") continue;
3709
+ let preStat = null;
3710
+ try {
3711
+ const s = await stat11(filePath);
3712
+ preStat = { size: s.size, mtimeMs: s.mtimeMs };
3713
+ } catch {
3714
+ }
3406
3715
  const session = await parseSessionFile(filePath, dirName, seenMsgIds, dateRange);
3716
+ const hasApiCalls = session !== null && session.apiCalls > 0;
3717
+ if (preStat && (hasApiCalls || !dateRange)) {
3718
+ try {
3719
+ const postStat = await stat11(filePath);
3720
+ if (postStat.size === preStat.size && postStat.mtimeMs === preStat.mtimeMs) {
3721
+ recordParseResult(filePath, sessionIndex, preStat.size, preStat.mtimeMs, hasApiCalls);
3722
+ indexDirty = true;
3723
+ }
3724
+ } catch {
3725
+ }
3726
+ }
3407
3727
  if (session && session.apiCalls > 0) {
3408
3728
  const existing = projectMap.get(dirName) ?? [];
3409
3729
  existing.push(session);
@@ -3411,13 +3731,19 @@ async function scanProjectDirs(dirs, seenMsgIds, dateRange) {
3411
3731
  }
3412
3732
  }
3413
3733
  }
3414
- return Array.from(projectMap.entries()).map(([dirName, sessions]) => ({
3734
+ const pruned = pruneIndex(sessionIndex, allJsonlPaths);
3735
+ if (pruned > 0) indexDirty = true;
3736
+ if (indexDirty) {
3737
+ await saveSessionIndex(sessionIndex);
3738
+ }
3739
+ const projects = Array.from(projectMap.entries()).map(([dirName, sessions]) => ({
3415
3740
  project: dirName,
3416
3741
  projectPath: unsanitizePath(dirName),
3417
3742
  sessions,
3418
3743
  totalCostUSD: sessions.reduce((s, sess) => s + sess.totalCostUSD, 0),
3419
3744
  totalApiCalls: sessions.reduce((s, sess) => s + sess.apiCalls, 0)
3420
3745
  }));
3746
+ return { projects, indexDirty };
3421
3747
  }
3422
3748
  function providerCallToTurn(call) {
3423
3749
  const tools = call.tools;
@@ -3458,7 +3784,7 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3458
3784
  for (const source of sources) {
3459
3785
  if (dateRange) {
3460
3786
  try {
3461
- const s = await stat10(source.path);
3787
+ const s = await stat11(source.path);
3462
3788
  if (s.mtimeMs < dateRange.start.getTime()) continue;
3463
3789
  } catch {
3464
3790
  }
@@ -3483,6 +3809,9 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3483
3809
  sessionMap.set(key, { project: source.project, turns: [classified] });
3484
3810
  }
3485
3811
  }
3812
+ if (parser.warnings && parser.warnings.length > 0) {
3813
+ _parseWarnings.push(...parser.warnings);
3814
+ }
3486
3815
  }
3487
3816
  const projectMap = /* @__PURE__ */ new Map();
3488
3817
  for (const [key, { project, turns }] of sessionMap) {
@@ -3510,6 +3839,8 @@ var CACHE_TTL_MS3 = 6e4;
3510
3839
  var MAX_CACHE_ENTRIES = 10;
3511
3840
  var sessionCache = /* @__PURE__ */ new Map();
3512
3841
  var inFlightSourceContexts = /* @__PURE__ */ new Map();
3842
+ var SOURCE_CONTEXT_TTL_MS = 5e3;
3843
+ var resolvedSourceContexts = /* @__PURE__ */ new Map();
3513
3844
  function sourceContextCacheKey(providerFilter) {
3514
3845
  return JSON.stringify({
3515
3846
  provider: providerFilter ?? "all",
@@ -3534,9 +3865,13 @@ function buildCacheFingerprint(sources) {
3534
3865
  }
3535
3866
  async function getSourceContext(providerFilter) {
3536
3867
  const key = sourceContextCacheKey(providerFilter);
3537
- const cached = inFlightSourceContexts.get(key);
3538
- if (cached) {
3539
- return cached;
3868
+ const resolved = resolvedSourceContexts.get(key);
3869
+ if (resolved && Date.now() - resolved.ts < SOURCE_CONTEXT_TTL_MS) {
3870
+ return resolved.result;
3871
+ }
3872
+ const inflight = inFlightSourceContexts.get(key);
3873
+ if (inflight) {
3874
+ return inflight;
3540
3875
  }
3541
3876
  const promise = (async () => {
3542
3877
  const sources = await discoverAllSessions(providerFilter);
@@ -3545,13 +3880,11 @@ async function getSourceContext(providerFilter) {
3545
3880
  })();
3546
3881
  inFlightSourceContexts.set(key, promise);
3547
3882
  try {
3548
- return await promise;
3549
- } catch (err) {
3550
- throw err;
3883
+ const result = await promise;
3884
+ resolvedSourceContexts.set(key, { result, ts: Date.now() });
3885
+ return result;
3551
3886
  } finally {
3552
- if (inFlightSourceContexts.get(key) === promise) {
3553
- inFlightSourceContexts.delete(key);
3554
- }
3887
+ inFlightSourceContexts.delete(key);
3555
3888
  }
3556
3889
  }
3557
3890
  function cachePut(key, data) {
@@ -3586,6 +3919,7 @@ function filterProjectsByName(projects, include, exclude) {
3586
3919
  return result;
3587
3920
  }
3588
3921
  async function parseAllSessions(dateRange, providerFilter) {
3922
+ _parseWarnings = [];
3589
3923
  const sourceContext = await getSourceContext(providerFilter);
3590
3924
  const allSources = sourceContext.sources;
3591
3925
  const key = cacheKey(dateRange, providerFilter, sourceContext.fingerprint);
@@ -3596,7 +3930,7 @@ async function parseAllSessions(dateRange, providerFilter) {
3596
3930
  const claudeSources = allSources.filter((s) => s.provider === "claude");
3597
3931
  const nonClaudeSources = allSources.filter((s) => s.provider !== "claude");
3598
3932
  const claudeDirs = claudeSources.map((s) => ({ path: s.path, name: s.project }));
3599
- const claudeProjects = await scanProjectDirs(claudeDirs, seenMsgIds, dateRange);
3933
+ const { projects: claudeProjects } = await scanProjectDirs(claudeDirs, seenMsgIds, dateRange);
3600
3934
  const providerGroups = /* @__PURE__ */ new Map();
3601
3935
  for (const source of nonClaudeSources) {
3602
3936
  const existing = providerGroups.get(source.provider) ?? [];
@@ -3669,6 +4003,19 @@ function renderStatusBar(projects) {
3669
4003
  }
3670
4004
 
3671
4005
  // src/menubar-json.ts
4006
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
4007
+ import { join as join17, dirname as dirname3 } from "path";
4008
+ import { fileURLToPath } from "url";
4009
+ function getCliVersion() {
4010
+ try {
4011
+ const pkgPath = join17(dirname3(fileURLToPath(import.meta.url)), "..", "package.json");
4012
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
4013
+ return pkg.version ?? "0.0.0";
4014
+ } catch {
4015
+ return "0.0.0";
4016
+ }
4017
+ }
4018
+ var MIN_APP_VERSION = "0.2.40";
3672
4019
  var TOP_ACTIVITIES_LIMIT = 20;
3673
4020
  var TOP_MODELS_LIMIT = 20;
3674
4021
  var TOP_FINDINGS_LIMIT = 10;
@@ -3785,7 +4132,10 @@ function buildHistory(daily) {
3785
4132
  }
3786
4133
  function buildMenubarPayload(current, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge, diagnostics) {
3787
4134
  return {
4135
+ schemaVersion: 2,
3788
4136
  generated: (/* @__PURE__ */ new Date()).toISOString(),
4137
+ cliVersion: getCliVersion(),
4138
+ minAppVersion: MIN_APP_VERSION,
3789
4139
  current: {
3790
4140
  label: current.label,
3791
4141
  cost: current.cost,
@@ -3811,18 +4161,18 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
3811
4161
 
3812
4162
  // src/daily-cache.ts
3813
4163
  init_cache_dir();
3814
- import { createHash as createHash3, randomBytes as randomBytes5 } from "crypto";
3815
- import { existsSync as existsSync3 } from "fs";
3816
- import { mkdir as mkdir7, open as open5, readFile as readFile7, rename as rename6, unlink as unlink3 } from "fs/promises";
3817
- import { join as join16 } from "path";
4164
+ import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
4165
+ import { existsSync as existsSync4 } from "fs";
4166
+ import { mkdir as mkdir8, open as open6, readFile as readFile8, rename as rename7, unlink as unlink4 } from "fs/promises";
4167
+ import { join as join18 } from "path";
3818
4168
  var DAILY_CACHE_VERSION = 6;
3819
4169
  var DEFAULT_DAILY_CACHE_SCOPE = "global";
3820
4170
  var DAILY_CACHE_FILENAME = "daily-cache.json";
3821
4171
  function getCachePath3(scopeKey) {
3822
4172
  if (scopeKey === DEFAULT_DAILY_CACHE_SCOPE) {
3823
- return join16(getCacheDir(), DAILY_CACHE_FILENAME);
4173
+ return join18(getCacheDir(), DAILY_CACHE_FILENAME);
3824
4174
  }
3825
- return join16(getCacheDir(), `daily-cache-${scopeKey}.json`);
4175
+ return join18(getCacheDir(), `daily-cache-${scopeKey}.json`);
3826
4176
  }
3827
4177
  function emptyCache(scopeKey) {
3828
4178
  return { version: DAILY_CACHE_VERSION, scopeKey, lastComputedDate: null, days: [] };
@@ -3849,9 +4199,9 @@ function isValidCache(parsed) {
3849
4199
  }
3850
4200
  async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
3851
4201
  const path = getCachePath3(scopeKey);
3852
- if (!existsSync3(path)) return emptyCache(scopeKey);
4202
+ if (!existsSync4(path)) return emptyCache(scopeKey);
3853
4203
  try {
3854
- const raw = await readFile7(path, "utf-8");
4204
+ const raw = await readFile8(path, "utf-8");
3855
4205
  const parsed = JSON.parse(raw);
3856
4206
  if (!isValidCache(parsed)) return emptyCache(scopeKey);
3857
4207
  if (parsed.scopeKey !== scopeKey) return emptyCache(scopeKey);
@@ -3862,12 +4212,12 @@ async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
3862
4212
  }
3863
4213
  async function saveDailyCache(cache) {
3864
4214
  const dir = getCacheDir();
3865
- if (!existsSync3(dir)) await mkdir7(dir, { recursive: true });
4215
+ if (!existsSync4(dir)) await mkdir8(dir, { recursive: true });
3866
4216
  const scopeKey = cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE;
3867
4217
  const finalPath = getCachePath3(scopeKey);
3868
- const tempPath = `${finalPath}.${randomBytes5(8).toString("hex")}.tmp`;
4218
+ const tempPath = `${finalPath}.${randomBytes6(8).toString("hex")}.tmp`;
3869
4219
  const payload = JSON.stringify({ ...cache, scopeKey });
3870
- const handle = await open5(tempPath, "w", 384);
4220
+ const handle = await open6(tempPath, "w", 384);
3871
4221
  try {
3872
4222
  await handle.writeFile(payload, { encoding: "utf-8" });
3873
4223
  await handle.sync();
@@ -3875,10 +4225,10 @@ async function saveDailyCache(cache) {
3875
4225
  await handle.close();
3876
4226
  }
3877
4227
  try {
3878
- await rename6(tempPath, finalPath);
4228
+ await rename7(tempPath, finalPath);
3879
4229
  } catch (err) {
3880
4230
  try {
3881
- await unlink3(tempPath);
4231
+ await unlink4(tempPath);
3882
4232
  } catch {
3883
4233
  }
3884
4234
  throw err;
@@ -3917,9 +4267,9 @@ init_models();
3917
4267
 
3918
4268
  // src/optimize.ts
3919
4269
  import chalk2 from "chalk";
3920
- import { readdir as readdir9, stat as stat11 } from "fs/promises";
3921
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
3922
- import { basename as basename8, join as join17 } from "path";
4270
+ import { readdir as readdir9, stat as stat12 } from "fs/promises";
4271
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4272
+ import { basename as basename9, join as join19 } from "path";
3923
4273
  import { homedir as homedir11 } from "os";
3924
4274
  var ORANGE = "#F59E0B";
3925
4275
  var DIM = "#666666";
@@ -4003,13 +4353,13 @@ var DEFAULT_TREND_PERIOD_MS = DEFAULT_TREND_PERIOD_DAYS * 24 * 60 * 60 * 1e3;
4003
4353
  var IMPROVING_THRESHOLD = 0.5;
4004
4354
  async function collectJsonlFiles2(dirPath) {
4005
4355
  const files = await readdir9(dirPath).catch(() => []);
4006
- const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join17(dirPath, f));
4356
+ const result = files.filter((f) => f.endsWith(".jsonl")).map((f) => join19(dirPath, f));
4007
4357
  for (const entry of files) {
4008
4358
  if (entry.endsWith(".jsonl")) continue;
4009
- const subPath = join17(dirPath, entry, "subagents");
4359
+ const subPath = join19(dirPath, entry, "subagents");
4010
4360
  const subFiles = await readdir9(subPath).catch(() => []);
4011
4361
  for (const sf of subFiles) {
4012
- if (sf.endsWith(".jsonl")) result.push(join17(subPath, sf));
4362
+ if (sf.endsWith(".jsonl")) result.push(join19(subPath, sf));
4013
4363
  }
4014
4364
  }
4015
4365
  return result;
@@ -4017,7 +4367,7 @@ async function collectJsonlFiles2(dirPath) {
4017
4367
  async function isFileStaleForRange(filePath, range) {
4018
4368
  if (!range) return false;
4019
4369
  try {
4020
- const s = await stat11(filePath);
4370
+ const s = await stat12(filePath);
4021
4371
  return s.mtimeMs < range.start.getTime();
4022
4372
  } catch {
4023
4373
  return false;
@@ -4028,7 +4378,8 @@ async function runWithConcurrency(items, limit, worker) {
4028
4378
  async function next() {
4029
4379
  while (idx < items.length) {
4030
4380
  const current = idx++;
4031
- await worker(items[current]);
4381
+ const item = items[current];
4382
+ if (item !== void 0) await worker(item);
4032
4383
  }
4033
4384
  }
4034
4385
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => next()));
@@ -4048,7 +4399,7 @@ async function scanJsonlFile(filePath, project, dateRange, recentCutoffMs = Date
4048
4399
  const cwds = [];
4049
4400
  const apiCalls = [];
4050
4401
  const userMessages = [];
4051
- const sessionId = basename8(filePath, ".jsonl");
4402
+ const sessionId = basename9(filePath, ".jsonl");
4052
4403
  let lastVersion = "";
4053
4404
  for await (const line of readSessionLines(filePath)) {
4054
4405
  if (!line.trim()) continue;
@@ -4146,16 +4497,16 @@ function isReadTool(name) {
4146
4497
  function loadMcpConfigs(projectCwds) {
4147
4498
  const servers = /* @__PURE__ */ new Map();
4148
4499
  const configPaths = [
4149
- join17(homedir11(), ".claude", "settings.json"),
4150
- join17(homedir11(), ".claude", "settings.local.json")
4500
+ join19(homedir11(), ".claude", "settings.json"),
4501
+ join19(homedir11(), ".claude", "settings.local.json")
4151
4502
  ];
4152
4503
  for (const cwd of projectCwds) {
4153
- configPaths.push(join17(cwd, ".mcp.json"));
4154
- configPaths.push(join17(cwd, ".claude", "settings.json"));
4155
- configPaths.push(join17(cwd, ".claude", "settings.local.json"));
4504
+ configPaths.push(join19(cwd, ".mcp.json"));
4505
+ configPaths.push(join19(cwd, ".claude", "settings.json"));
4506
+ configPaths.push(join19(cwd, ".claude", "settings.local.json"));
4156
4507
  }
4157
4508
  for (const p of configPaths) {
4158
- if (!existsSync4(p)) continue;
4509
+ if (!existsSync5(p)) continue;
4159
4510
  const config = readJsonFile(p);
4160
4511
  if (!config) continue;
4161
4512
  let mtime = 0;
@@ -4238,7 +4589,7 @@ function detectDuplicateReads(calls, dateRange) {
4238
4589
  const extra = entry.count - 1;
4239
4590
  totalDuplicates += extra;
4240
4591
  if (entry.recent > 1) recentDuplicates += entry.recent - 1;
4241
- const name = basename8(file);
4592
+ const name = basename9(file);
4242
4593
  fileDupes.set(name, (fileDupes.get(name) ?? 0) + extra);
4243
4594
  }
4244
4595
  }
@@ -4305,13 +4656,13 @@ function expandImports(filePath, seen, depth) {
4305
4656
  if (content === null) return { totalLines: 0, importedFiles: 0 };
4306
4657
  let totalLines = content.split("\n").length;
4307
4658
  let importedFiles = 0;
4308
- const dir = join17(filePath, "..");
4659
+ const dir = join19(filePath, "..");
4309
4660
  IMPORT_PATTERN.lastIndex = 0;
4310
4661
  for (const match of content.matchAll(IMPORT_PATTERN)) {
4311
4662
  const rawPath = match[1];
4312
4663
  if (!rawPath) continue;
4313
- const resolved = rawPath.startsWith("/") ? rawPath : join17(dir, rawPath);
4314
- if (!existsSync4(resolved)) continue;
4664
+ const resolved = rawPath.startsWith("/") ? rawPath : join19(dir, rawPath);
4665
+ if (!existsSync5(resolved)) continue;
4315
4666
  const nested = expandImports(resolved, seen, depth + 1);
4316
4667
  totalLines += nested.totalLines;
4317
4668
  importedFiles += 1 + nested.importedFiles;
@@ -4322,8 +4673,8 @@ function detectBloatedClaudeMd(projectCwds) {
4322
4673
  const bloated = [];
4323
4674
  for (const cwd of projectCwds) {
4324
4675
  for (const name of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
4325
- const fullPath = join17(cwd, name);
4326
- if (!existsSync4(fullPath)) continue;
4676
+ const fullPath = join19(cwd, name);
4677
+ if (!existsSync5(fullPath)) continue;
4327
4678
  const { totalLines, importedFiles } = expandImports(fullPath, /* @__PURE__ */ new Set(), 0);
4328
4679
  if (totalLines > CLAUDEMD_HEALTHY_LINES) {
4329
4680
  bloated.push({ path: `${shortHomePath(cwd)}/${name}`, expandedLines: totalLines, imports: importedFiles });
@@ -4342,7 +4693,7 @@ function detectBloatedClaudeMd(projectCwds) {
4342
4693
  return {
4343
4694
  title: `Your CLAUDE.md is too long`,
4344
4695
  explanation: `${list}. CLAUDE.md plus all @-imported files load into every API call. Trimming below ${CLAUDEMD_HEALTHY_LINES} lines saves ~${formatTokens(tokensSaved)} tokens per call.`,
4345
- impact: worst.expandedLines > CLAUDEMD_HIGH_THRESHOLD_LINES ? "high" : "medium",
4696
+ impact: (worst?.expandedLines ?? 0) > CLAUDEMD_HIGH_THRESHOLD_LINES ? "high" : "medium",
4346
4697
  tokensSaved,
4347
4698
  fix: {
4348
4699
  type: "paste",
@@ -4412,7 +4763,7 @@ function detectCacheBloat(apiCalls, projects, dateRange) {
4412
4763
  const median2 = sorted[Math.floor(sorted.length / 2)];
4413
4764
  const baseline = computeBudgetAwareCacheBaseline(projects);
4414
4765
  const bloatThreshold = baseline * CACHE_BLOAT_MULTIPLIER;
4415
- if (median2 < bloatThreshold) return null;
4766
+ if (median2 === void 0 || median2 < bloatThreshold) return null;
4416
4767
  const recentCalls = apiCalls.filter((c) => c.recent);
4417
4768
  const totalBloated = apiCalls.filter((c) => c.cacheCreationTokens > bloatThreshold).length;
4418
4769
  const recentBloated = recentCalls.filter((c) => c.cacheCreationTokens > bloatThreshold).length;
@@ -4431,15 +4782,15 @@ function detectCacheBloat(apiCalls, projects, dateRange) {
4431
4782
  const tokensSaved = excess * apiCalls.length;
4432
4783
  let versionNote = "";
4433
4784
  if (versionAvgs.length >= 2) {
4434
- const [high, ...rest] = versionAvgs;
4435
- const low = rest[rest.length - 1];
4436
- if (high.avg - low.avg > CACHE_VERSION_DIFF_THRESHOLD) {
4785
+ const high = versionAvgs[0];
4786
+ const low = versionAvgs[versionAvgs.length - 1];
4787
+ if (high && low && high.avg - low.avg > CACHE_VERSION_DIFF_THRESHOLD) {
4437
4788
  versionNote = ` Version ${high.version} averages ${formatTokens(high.avg)} vs ${low.version} at ${formatTokens(low.avg)}.`;
4438
4789
  }
4439
4790
  }
4440
4791
  return {
4441
4792
  title: "Session warmup is unusually large",
4442
- explanation: `Median cache_creation per call is ${formatTokens(median2)} tokens, about ${formatTokens(excess)} above your baseline of ${formatTokens(baseline)}.${versionNote}`,
4793
+ explanation: `Median cache_creation per call is ${formatTokens(median2 ?? 0)} tokens, about ${formatTokens(excess)} above your baseline of ${formatTokens(baseline)}.${versionNote}`,
4443
4794
  impact: excess > CACHE_EXCESS_HIGH_THRESHOLD ? "high" : "medium",
4444
4795
  tokensSaved,
4445
4796
  fix: {
@@ -4451,7 +4802,7 @@ function detectCacheBloat(apiCalls, projects, dateRange) {
4451
4802
  };
4452
4803
  }
4453
4804
  async function listMarkdownFiles(dir) {
4454
- if (!existsSync4(dir)) return [];
4805
+ if (!existsSync5(dir)) return [];
4455
4806
  try {
4456
4807
  const entries = await readdir9(dir);
4457
4808
  return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(/\.md$/, ""));
@@ -4460,12 +4811,12 @@ async function listMarkdownFiles(dir) {
4460
4811
  }
4461
4812
  }
4462
4813
  async function listSkillDirs(dir) {
4463
- if (!existsSync4(dir)) return [];
4814
+ if (!existsSync5(dir)) return [];
4464
4815
  try {
4465
4816
  const entries = await readdir9(dir);
4466
4817
  const names = [];
4467
4818
  for (const entry of entries) {
4468
- if (existsSync4(join17(dir, entry, "SKILL.md"))) names.push(entry);
4819
+ if (existsSync5(join19(dir, entry, "SKILL.md"))) names.push(entry);
4469
4820
  }
4470
4821
  return names;
4471
4822
  } catch {
@@ -4473,7 +4824,7 @@ async function listSkillDirs(dir) {
4473
4824
  }
4474
4825
  }
4475
4826
  async function detectGhostAgents(calls) {
4476
- const defined = await listMarkdownFiles(join17(homedir11(), ".claude", "agents"));
4827
+ const defined = await listMarkdownFiles(join19(homedir11(), ".claude", "agents"));
4477
4828
  if (defined.length === 0) return null;
4478
4829
  const invoked = /* @__PURE__ */ new Set();
4479
4830
  for (const call of calls) {
@@ -4498,7 +4849,7 @@ async function detectGhostAgents(calls) {
4498
4849
  };
4499
4850
  }
4500
4851
  async function detectGhostSkills(calls) {
4501
- const defined = await listSkillDirs(join17(homedir11(), ".claude", "skills"));
4852
+ const defined = await listSkillDirs(join19(homedir11(), ".claude", "skills"));
4502
4853
  if (defined.length === 0) return null;
4503
4854
  const invoked = /* @__PURE__ */ new Set();
4504
4855
  for (const call of calls) {
@@ -4523,7 +4874,7 @@ async function detectGhostSkills(calls) {
4523
4874
  };
4524
4875
  }
4525
4876
  async function detectGhostCommands(userMessages) {
4526
- const defined = await listMarkdownFiles(join17(homedir11(), ".claude", "commands"));
4877
+ const defined = await listMarkdownFiles(join19(homedir11(), ".claude", "commands"));
4527
4878
  if (defined.length === 0) return null;
4528
4879
  const invoked = /* @__PURE__ */ new Set();
4529
4880
  for (const msg of userMessages) {
@@ -4551,12 +4902,12 @@ async function detectGhostCommands(userMessages) {
4551
4902
  }
4552
4903
  function readShellProfileLimit() {
4553
4904
  for (const profile of SHELL_PROFILES) {
4554
- const path = join17(homedir11(), profile);
4555
- if (!existsSync4(path)) continue;
4905
+ const path = join19(homedir11(), profile);
4906
+ if (!existsSync5(path)) continue;
4556
4907
  const content = readSessionFileSync(path);
4557
4908
  if (content === null) continue;
4558
4909
  const match = content.match(/^\s*export\s+BASH_MAX_OUTPUT_LENGTH\s*=\s*['"]?(\d+)['"]?/m);
4559
- if (match) return parseInt(match[1], 10);
4910
+ if (match?.[1]) return parseInt(match[1], 10);
4560
4911
  }
4561
4912
  return null;
4562
4913
  }
@@ -4749,7 +5100,8 @@ function renderOptimize(findings, costRate, periodLabel, periodCost, sessionCoun
4749
5100
  lines.push(chalk2.hex(GREEN)(` Potential savings: ~${formatTokens(totalTokens)} tokens${costText}`));
4750
5101
  lines.push("");
4751
5102
  for (let i = 0; i < findings.length; i++) {
4752
- lines.push(...renderFinding(i + 1, findings[i], costRate));
5103
+ const finding = findings[i];
5104
+ if (finding) lines.push(...renderFinding(i + 1, finding, costRate));
4753
5105
  }
4754
5106
  lines.push(chalk2.hex(DIM)(" " + SEP.repeat(PANEL_WIDTH)));
4755
5107
  lines.push(chalk2.dim(" Estimates only."));
@@ -4772,8 +5124,8 @@ async function runOptimize(projects, periodLabel, dateRange, preScanned) {
4772
5124
 
4773
5125
  // src/context-budget.ts
4774
5126
  import { readdir as readdir10 } from "fs/promises";
4775
- import { existsSync as existsSync5 } from "fs";
4776
- import { join as join18 } from "path";
5127
+ import { existsSync as existsSync6 } from "fs";
5128
+ import { join as join20 } from "path";
4777
5129
  import { homedir as homedir12 } from "os";
4778
5130
  var CHARS_PER_TOKEN2 = 4;
4779
5131
  var SYSTEM_BASE_TOKENS = 10400;
@@ -4783,7 +5135,7 @@ function estimateTokens2(text) {
4783
5135
  return Math.ceil(text.length / CHARS_PER_TOKEN2);
4784
5136
  }
4785
5137
  async function readConfigFile(path) {
4786
- if (!existsSync5(path)) return null;
5138
+ if (!existsSync6(path)) return null;
4787
5139
  const raw = await readSessionFile(path);
4788
5140
  if (raw === null) return null;
4789
5141
  try {
@@ -4795,13 +5147,13 @@ async function readConfigFile(path) {
4795
5147
  async function countMcpTools(projectPath) {
4796
5148
  const home = homedir12();
4797
5149
  const configPaths = [
4798
- join18(home, ".claude", "settings.json"),
4799
- join18(home, ".claude", "settings.local.json")
5150
+ join20(home, ".claude", "settings.json"),
5151
+ join20(home, ".claude", "settings.local.json")
4800
5152
  ];
4801
5153
  if (projectPath) {
4802
- configPaths.push(join18(projectPath, ".mcp.json"));
4803
- configPaths.push(join18(projectPath, ".claude", "settings.json"));
4804
- configPaths.push(join18(projectPath, ".claude", "settings.local.json"));
5154
+ configPaths.push(join20(projectPath, ".mcp.json"));
5155
+ configPaths.push(join20(projectPath, ".claude", "settings.json"));
5156
+ configPaths.push(join20(projectPath, ".claude", "settings.local.json"));
4805
5157
  }
4806
5158
  const servers = /* @__PURE__ */ new Set();
4807
5159
  let toolCount = 0;
@@ -4818,16 +5170,16 @@ async function countMcpTools(projectPath) {
4818
5170
  return toolCount;
4819
5171
  }
4820
5172
  async function countSkills(projectPath) {
4821
- const dirs = [join18(homedir12(), ".claude", "skills")];
4822
- if (projectPath) dirs.push(join18(projectPath, ".claude", "skills"));
5173
+ const dirs = [join20(homedir12(), ".claude", "skills")];
5174
+ if (projectPath) dirs.push(join20(projectPath, ".claude", "skills"));
4823
5175
  let count = 0;
4824
5176
  for (const dir of dirs) {
4825
- if (!existsSync5(dir)) continue;
5177
+ if (!existsSync6(dir)) continue;
4826
5178
  try {
4827
5179
  const entries = await readdir10(dir);
4828
5180
  for (const entry of entries) {
4829
- const skillFile = join18(dir, entry, "SKILL.md");
4830
- if (existsSync5(skillFile)) count++;
5181
+ const skillFile = join20(dir, entry, "SKILL.md");
5182
+ if (existsSync6(skillFile)) count++;
4831
5183
  }
4832
5184
  } catch {
4833
5185
  continue;
@@ -4839,15 +5191,15 @@ async function scanMemoryFiles(projectPath) {
4839
5191
  const home = homedir12();
4840
5192
  const files = [];
4841
5193
  const paths = [
4842
- { path: join18(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
5194
+ { path: join20(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
4843
5195
  ];
4844
5196
  if (projectPath) {
4845
- paths.push({ path: join18(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
4846
- paths.push({ path: join18(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
4847
- paths.push({ path: join18(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
5197
+ paths.push({ path: join20(projectPath, "CLAUDE.md"), name: "CLAUDE.md" });
5198
+ paths.push({ path: join20(projectPath, ".claude", "CLAUDE.md"), name: ".claude/CLAUDE.md" });
5199
+ paths.push({ path: join20(projectPath, "CLAUDE.local.md"), name: "CLAUDE.local.md" });
4848
5200
  }
4849
5201
  for (const { path, name } of paths) {
4850
- if (!existsSync5(path)) continue;
5202
+ if (!existsSync6(path)) continue;
4851
5203
  const content = await readSessionFile(path);
4852
5204
  if (content === null) continue;
4853
5205
  files.push({ name, tokens: estimateTokens2(content) });
@@ -4879,7 +5231,9 @@ async function discoverProjectCwd(sessionDir) {
4879
5231
  return null;
4880
5232
  }
4881
5233
  if (files.length === 0) return null;
4882
- const content = await readSessionFile(join18(sessionDir, files[0]));
5234
+ const firstFile = files[0];
5235
+ if (!firstFile) return null;
5236
+ const content = await readSessionFile(join20(sessionDir, firstFile));
4883
5237
  if (content === null) return null;
4884
5238
  for (const line of content.split("\n")) {
4885
5239
  if (!line.trim()) continue;
@@ -4898,8 +5252,8 @@ import React, { useState, useEffect, useRef } from "react";
4898
5252
  import { render, Box, Text, useInput, useApp, useStdout } from "ink";
4899
5253
 
4900
5254
  // src/compare-stats.ts
4901
- import { readdir as readdir11, readFile as readFile8 } from "fs/promises";
4902
- import { join as join19 } from "path";
5255
+ import { readdir as readdir11, readFile as readFile9 } from "fs/promises";
5256
+ import { join as join21 } from "path";
4903
5257
  var PLANNING_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "EnterPlanMode", "ExitPlanMode"]);
4904
5258
  function aggregateModelStats(projects) {
4905
5259
  const byModel = /* @__PURE__ */ new Map();
@@ -5126,12 +5480,12 @@ async function collectJsonlFiles3(sessionDir) {
5126
5480
  const files = [];
5127
5481
  for (const entry of entries) {
5128
5482
  if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
5129
- files.push(join19(sessionDir, entry.name));
5483
+ files.push(join21(sessionDir, entry.name));
5130
5484
  } else if (entry.isDirectory() && entry.name === "subagents") {
5131
- const subEntries = await readdir11(join19(sessionDir, entry.name), { withFileTypes: true });
5485
+ const subEntries = await readdir11(join21(sessionDir, entry.name), { withFileTypes: true });
5132
5486
  for (const sub of subEntries) {
5133
5487
  if (sub.isFile() && sub.name.endsWith(".jsonl") && !isCompactFile(sub.name)) {
5134
- files.push(join19(sessionDir, entry.name, sub.name));
5488
+ files.push(join21(sessionDir, entry.name, sub.name));
5135
5489
  }
5136
5490
  }
5137
5491
  }
@@ -5151,10 +5505,10 @@ async function scanSelfCorrections(projectDirs) {
5151
5505
  const allFiles = [];
5152
5506
  for (const entry of entries) {
5153
5507
  if (entry.isFile() && entry.name.endsWith(".jsonl") && !isCompactFile(entry.name)) {
5154
- allFiles.push(join19(dir, entry.name));
5508
+ allFiles.push(join21(dir, entry.name));
5155
5509
  } else if (entry.isDirectory()) {
5156
5510
  try {
5157
- const sessionFiles = await collectJsonlFiles3(join19(dir, entry.name));
5511
+ const sessionFiles = await collectJsonlFiles3(join21(dir, entry.name));
5158
5512
  allFiles.push(...sessionFiles);
5159
5513
  } catch {
5160
5514
  continue;
@@ -5164,7 +5518,7 @@ async function scanSelfCorrections(projectDirs) {
5164
5518
  for (const file of allFiles) {
5165
5519
  let raw;
5166
5520
  try {
5167
- raw = await readFile8(file, "utf8");
5521
+ raw = await readFile9(file, "utf8");
5168
5522
  } catch {
5169
5523
  continue;
5170
5524
  }
@@ -5647,9 +6001,9 @@ function median(values) {
5647
6001
  const sorted = [...values].sort((a, b) => a - b);
5648
6002
  const mid = Math.floor(sorted.length / 2);
5649
6003
  if (sorted.length % 2 === 0) {
5650
- return (sorted[mid - 1] + sorted[mid]) / 2;
6004
+ return ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2;
5651
6005
  }
5652
- return sorted[mid];
6006
+ return sorted[mid] ?? 0;
5653
6007
  }
5654
6008
  function toLocalDateKey(d) {
5655
6009
  const year = d.getFullYear();
@@ -5779,7 +6133,7 @@ function planDisplayName(id) {
5779
6133
  }
5780
6134
 
5781
6135
  // src/dashboard.tsx
5782
- import { join as join20 } from "path";
6136
+ import { join as join22 } from "path";
5783
6137
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5784
6138
  var PERIODS = ["today", "week", "30days", "month", "all"];
5785
6139
  var PERIOD_LABELS = {
@@ -6227,7 +6581,7 @@ function TopSessions({ projects, pw, bw }) {
6227
6581
  if (top.length === 0) {
6228
6582
  return /* @__PURE__ */ jsx2(Panel, { title: "Top Sessions", color: PANEL_COLORS.sessions, width: pw, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No sessions" }) });
6229
6583
  }
6230
- const maxCost = top[0].totalCostUSD;
6584
+ const maxCost = top[0]?.totalCostUSD ?? 0;
6231
6585
  const nw = Math.max(8, pw - bw - TOP_SESSIONS_COST_COL - TOP_SESSIONS_CALLS_COL - 1 - PANEL_CHROME);
6232
6586
  return /* @__PURE__ */ jsxs2(Panel, { title: "Top Sessions", color: PANEL_COLORS.sessions, width: pw, children: [
6233
6587
  /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate-end", children: [
@@ -6552,11 +6906,11 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6552
6906
  useEffect2(() => {
6553
6907
  let cancelled = false;
6554
6908
  async function loadBudgets() {
6555
- const claudeDir = join20(homedir13(), ".claude", "projects");
6909
+ const claudeDir = join22(homedir13(), ".claude", "projects");
6556
6910
  const budgets = /* @__PURE__ */ new Map();
6557
6911
  for (const project of projects.slice(0, 8)) {
6558
6912
  if (cancelled) return;
6559
- const cwd = await discoverProjectCwd(join20(claudeDir, project.project));
6913
+ const cwd = await discoverProjectCwd(join22(claudeDir, project.project));
6560
6914
  if (!cwd) continue;
6561
6915
  budgets.set(project.project, await estimateContextBudget(cwd));
6562
6916
  }
@@ -6648,7 +7002,7 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6648
7002
  }
6649
7003
  if (input === "p" && multipleProviders && view !== "compare") {
6650
7004
  const opts = ["all", ...detectedProviders];
6651
- const next = opts[(opts.indexOf(activeProvider) + 1) % opts.length];
7005
+ const next = opts[(opts.indexOf(activeProvider) + 1) % opts.length] ?? "all";
6652
7006
  setActiveProvider(next);
6653
7007
  setView("dashboard");
6654
7008
  if (debounceRef.current) clearTimeout(debounceRef.current);
@@ -6656,8 +7010,8 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
6656
7010
  return;
6657
7011
  }
6658
7012
  const idx = PERIODS.indexOf(period);
6659
- if (key.leftArrow) switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length]);
6660
- else if (key.rightArrow || key.tab) switchPeriod(PERIODS[(idx + 1) % PERIODS.length]);
7013
+ if (key.leftArrow) switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length] ?? "today");
7014
+ else if (key.rightArrow || key.tab) switchPeriod(PERIODS[(idx + 1) % PERIODS.length] ?? "today");
6661
7015
  else if (input === "1") switchPeriodImmediate("today");
6662
7016
  else if (input === "2") switchPeriodImmediate("week");
6663
7017
  else if (input === "3") switchPeriodImmediate("30days");
@@ -6808,6 +7162,7 @@ function computeProgressiveBackfillStart({
6808
7162
  }
6809
7163
 
6810
7164
  // src/cli.ts
7165
+ import { createInterface as createInterface2 } from "readline";
6811
7166
  import { createRequire as createRequire2 } from "module";
6812
7167
  var require2 = createRequire2(import.meta.url);
6813
7168
  var { version } = require2("../package.json");
@@ -7103,10 +7458,10 @@ program.command("status").description("Compact status output (today + week + mon
7103
7458
  const hadYesterday = c.days.some((d) => d.date >= yesterdayStr);
7104
7459
  if (hadYesterday) {
7105
7460
  const freshDays = c.days.filter((d) => d.date < yesterdayStr);
7106
- const latestFresh = freshDays.length > 0 ? freshDays[freshDays.length - 1].date : null;
7461
+ const latestFresh = freshDays.length > 0 ? freshDays[freshDays.length - 1]?.date ?? null : null;
7107
7462
  c = { ...c, days: freshDays, lastComputedDate: latestFresh };
7108
7463
  }
7109
- const oldestCachedDate = c.days.length > 0 ? c.days[0].date : null;
7464
+ const oldestCachedDate = c.days.length > 0 ? c.days[0]?.date ?? null : null;
7110
7465
  const effectiveGapStart = computeProgressiveBackfillStart({
7111
7466
  lastComputedDate: c.lastComputedDate,
7112
7467
  oldestCachedDate,
@@ -7124,7 +7479,7 @@ program.command("status").description("Compact status output (today + week + mon
7124
7479
  c = addNewDays(c, gapDays, yesterdayStr, coveredThrough ? { coveredThrough } : void 0);
7125
7480
  await saveDailyCache(c);
7126
7481
  }
7127
- const updatedOldest = c.days.length > 0 ? c.days[0].date : null;
7482
+ const updatedOldest = c.days.length > 0 ? c.days[0]?.date ?? null : null;
7128
7483
  const neededStart = new Date(todayStart.getTime() - (selectedPeriodHistoryDays - 1) * MS_PER_DAY4);
7129
7484
  const fullBackfillStart = new Date(todayStart.getTime() - BACKFILL_DAYS * MS_PER_DAY4);
7130
7485
  const clampedNeeded = neededStart < fullBackfillStart ? fullBackfillStart : neededStart;
@@ -7267,15 +7622,15 @@ program.command("status").description("Compact status output (today + week + mon
7267
7622
  optimize = await scanAndDetect(todayProjects, scanRange);
7268
7623
  }
7269
7624
  }
7270
- const exeOsDir = join21(homedir14(), ".exe-os");
7271
- const exeOsDetected = existsSync6(exeOsDir);
7625
+ const exeOsDir = join23(homedir14(), ".exe-os");
7626
+ const exeOsDetected = existsSync7(exeOsDir);
7272
7627
  let agentStats = null;
7273
7628
  let statsFileAge = null;
7274
7629
  try {
7275
- const statsPath = join21(exeOsDir, "agent-stats.json");
7276
- if (existsSync6(statsPath)) {
7630
+ const statsPath = join23(exeOsDir, "agent-stats.json");
7631
+ if (existsSync7(statsPath)) {
7277
7632
  statsFileAge = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
7278
- const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
7633
+ const parsed = JSON.parse(readFileSync3(statsPath, "utf-8"));
7279
7634
  if (parsed && Array.isArray(parsed.agents) && typeof parsed.generated === "string") {
7280
7635
  agentStats = parsed;
7281
7636
  } else {
@@ -7308,6 +7663,7 @@ program.command("status").description("Compact status output (today + week + mon
7308
7663
  const todayOnlyDays = todayDays.filter((d) => d.date > yesterdayStr);
7309
7664
  projectSpend = buildProjectSpendFromDays(selectedPeriodDays, todayOnlyDays, [], []);
7310
7665
  }
7666
+ warnings.push(...getDiscoveryWarnings(), ...getPricingWarnings(), ...getParseWarnings());
7311
7667
  const diagnostics = { daysCount, parseTimeMs, warnings };
7312
7668
  const payload = buildMenubarPayload(currentData, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge, diagnostics);
7313
7669
  const json = JSON.stringify(payload);
@@ -7380,9 +7736,9 @@ program.command("export").description("Export usage data to CSV or JSON (include
7380
7736
  Exported (Today + 7 Days + 30 Days) to: ${savedPath}
7381
7737
  `);
7382
7738
  });
7383
- program.command("menubar").description("Install and launch the macOS menubar app (one command, no clone)").option("--force", "Reinstall even if an older copy is already in ~/Applications").action(async (opts) => {
7739
+ program.command("menubar").description("Install and launch the macOS menubar app (one command, no clone)").option("--force", "Reinstall even if an older copy is already in ~/Applications").option("--version <semver>", "Install a specific release version (e.g. v0.2.38) instead of latest").action(async (opts) => {
7384
7740
  try {
7385
- const result = await installMenubarApp({ force: opts.force });
7741
+ const result = await installMenubarApp({ force: opts.force, version: opts.version });
7386
7742
  console.log(`
7387
7743
  Ready. ${result.installedPath}
7388
7744
  `);
@@ -7619,12 +7975,12 @@ program.command("compare").description("Compare two AI models side-by-side").opt
7619
7975
  await renderCompare(range, opts.provider);
7620
7976
  });
7621
7977
  program.command("agent-status").description("Diagnose exe-os agent data pipeline").action(() => {
7622
- const exeOsDir = join21(homedir14(), ".exe-os");
7623
- const statsPath = join21(exeOsDir, "agent-stats.json");
7624
- const pidPath = join21(exeOsDir, "exed.pid");
7625
- const dirExists = existsSync6(exeOsDir);
7626
- const statsExists = dirExists && existsSync6(statsPath);
7627
- const pidExists = dirExists && existsSync6(pidPath);
7978
+ const exeOsDir = join23(homedir14(), ".exe-os");
7979
+ const statsPath = join23(exeOsDir, "agent-stats.json");
7980
+ const pidPath = join23(exeOsDir, "exed.pid");
7981
+ const dirExists = existsSync7(exeOsDir);
7982
+ const statsExists = dirExists && existsSync7(statsPath);
7983
+ const pidExists = dirExists && existsSync7(pidPath);
7628
7984
  const lines = [];
7629
7985
  lines.push(`exe-os directory: ${dirExists ? "\u2713 found" : "\u2717 not found"} (${exeOsDir})`);
7630
7986
  lines.push(`daemon pid file: ${pidExists ? "\u2713 found" : "\u2717 not found"}`);
@@ -7633,7 +7989,7 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
7633
7989
  } else {
7634
7990
  try {
7635
7991
  const ageSeconds = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
7636
- const raw = readFileSync2(statsPath, "utf-8");
7992
+ const raw = readFileSync3(statsPath, "utf-8");
7637
7993
  const parsed = JSON.parse(raw);
7638
7994
  if (parsed && Array.isArray(parsed.agents) && typeof parsed.generated === "string") {
7639
7995
  const agentCount = parsed.agents.length;
@@ -7653,15 +8009,28 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
7653
8009
  console.log(lines.join("\n"));
7654
8010
  });
7655
8011
  if (process.argv.length <= 2 && process.platform === "darwin") {
7656
- installMenubarApp().then((result) => {
7657
- console.log(`
7658
- Installed: ${result.installedPath}`);
7659
- if (result.launched) console.log(" Launched.");
7660
- }).catch((err) => {
7661
- console.error(`
8012
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
8013
+ if (!isTTY) {
8014
+ program.outputHelp();
8015
+ } else {
8016
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
8017
+ rl.question("\n No subcommand given. Install the macOS menubar app? (y/N) ", (answer) => {
8018
+ rl.close();
8019
+ if (answer.trim().toLowerCase() === "y") {
8020
+ installMenubarApp().then((result) => {
8021
+ console.log(`
8022
+ Ready. ${result.installedPath}
8023
+ `);
8024
+ }).catch((err) => {
8025
+ console.error(`
7662
8026
  ${err instanceof Error ? err.message : String(err)}`);
7663
- process.exitCode = 1;
7664
- });
8027
+ process.exitCode = 1;
8028
+ });
8029
+ } else {
8030
+ program.outputHelp();
8031
+ }
8032
+ });
8033
+ }
7665
8034
  } else {
7666
8035
  program.parse();
7667
8036
  }