exe-watcher 0.2.16 → 0.2.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -25
- package/dist/cli.js +558 -189
- package/package.json +3 -2
- package/dist/cli.js.map +0 -1
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
|
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 =
|
|
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 ${
|
|
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.
|
|
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
|
|
1302
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
1398
|
-
|
|
1399
|
-
proc.on("
|
|
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
|
-
|
|
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("/
|
|
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
|
-
|
|
1437
|
-
|
|
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
|
-
|
|
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 (
|
|
1444
|
-
await
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
|
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
|
|
2180
|
-
import { basename as
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
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
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
3538
|
-
if (
|
|
3539
|
-
return
|
|
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
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3883
|
+
const result = await promise;
|
|
3884
|
+
resolvedSourceContexts.set(key, { result, ts: Date.now() });
|
|
3885
|
+
return result;
|
|
3551
3886
|
} finally {
|
|
3552
|
-
|
|
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
|
|
3815
|
-
import { existsSync as
|
|
3816
|
-
import { mkdir as
|
|
3817
|
-
import { join as
|
|
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
|
|
4173
|
+
return join18(getCacheDir(), DAILY_CACHE_FILENAME);
|
|
3824
4174
|
}
|
|
3825
|
-
return
|
|
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 (!
|
|
4202
|
+
if (!existsSync4(path)) return emptyCache(scopeKey);
|
|
3853
4203
|
try {
|
|
3854
|
-
const raw = await
|
|
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 (!
|
|
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}.${
|
|
4218
|
+
const tempPath = `${finalPath}.${randomBytes6(8).toString("hex")}.tmp`;
|
|
3869
4219
|
const payload = JSON.stringify({ ...cache, scopeKey });
|
|
3870
|
-
const handle = await
|
|
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
|
|
4228
|
+
await rename7(tempPath, finalPath);
|
|
3879
4229
|
} catch (err) {
|
|
3880
4230
|
try {
|
|
3881
|
-
await
|
|
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
|
|
3921
|
-
import { existsSync as
|
|
3922
|
-
import { basename as
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4150
|
-
|
|
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(
|
|
4154
|
-
configPaths.push(
|
|
4155
|
-
configPaths.push(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 :
|
|
4314
|
-
if (!
|
|
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 =
|
|
4326
|
-
if (!
|
|
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
|
|
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
|
|
4435
|
-
const low =
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
4555
|
-
if (!
|
|
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
|
-
|
|
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
|
|
4776
|
-
import { join as
|
|
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 (!
|
|
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
|
-
|
|
4799
|
-
|
|
5150
|
+
join20(home, ".claude", "settings.json"),
|
|
5151
|
+
join20(home, ".claude", "settings.local.json")
|
|
4800
5152
|
];
|
|
4801
5153
|
if (projectPath) {
|
|
4802
|
-
configPaths.push(
|
|
4803
|
-
configPaths.push(
|
|
4804
|
-
configPaths.push(
|
|
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 = [
|
|
4822
|
-
if (projectPath) dirs.push(
|
|
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 (!
|
|
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 =
|
|
4830
|
-
if (
|
|
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:
|
|
5194
|
+
{ path: join20(home, ".claude", "CLAUDE.md"), name: "~/.claude/CLAUDE.md" }
|
|
4843
5195
|
];
|
|
4844
5196
|
if (projectPath) {
|
|
4845
|
-
paths.push({ path:
|
|
4846
|
-
paths.push({ path:
|
|
4847
|
-
paths.push({ path:
|
|
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 (!
|
|
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
|
|
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
|
|
4902
|
-
import { join as
|
|
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(
|
|
5483
|
+
files.push(join21(sessionDir, entry.name));
|
|
5130
5484
|
} else if (entry.isDirectory() && entry.name === "subagents") {
|
|
5131
|
-
const subEntries = await readdir11(
|
|
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(
|
|
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(
|
|
5508
|
+
allFiles.push(join21(dir, entry.name));
|
|
5155
5509
|
} else if (entry.isDirectory()) {
|
|
5156
5510
|
try {
|
|
5157
|
-
const sessionFiles = await collectJsonlFiles3(
|
|
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
|
|
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
|
|
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]
|
|
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 =
|
|
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(
|
|
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]
|
|
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]
|
|
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]
|
|
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 =
|
|
7271
|
-
const exeOsDetected =
|
|
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 =
|
|
7276
|
-
if (
|
|
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(
|
|
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 =
|
|
7623
|
-
const statsPath =
|
|
7624
|
-
const pidPath =
|
|
7625
|
-
const dirExists =
|
|
7626
|
-
const statsExists = dirExists &&
|
|
7627
|
-
const pidExists = dirExists &&
|
|
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 =
|
|
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
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
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
|
-
|
|
7664
|
-
|
|
8027
|
+
process.exitCode = 1;
|
|
8028
|
+
});
|
|
8029
|
+
} else {
|
|
8030
|
+
program.outputHelp();
|
|
8031
|
+
}
|
|
8032
|
+
});
|
|
8033
|
+
}
|
|
7665
8034
|
} else {
|
|
7666
8035
|
program.parse();
|
|
7667
8036
|
}
|