exe-watcher 0.2.2 → 0.2.3
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 +416 -100
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -48,12 +48,23 @@ async function fetchAndCachePricing() {
|
|
|
48
48
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
49
49
|
const data = await response.json();
|
|
50
50
|
const pricing = /* @__PURE__ */ new Map();
|
|
51
|
+
const preferredStripped = /* @__PURE__ */ new Map();
|
|
52
|
+
const fallbackStripped = /* @__PURE__ */ new Map();
|
|
51
53
|
for (const [name, entry] of Object.entries(data)) {
|
|
52
54
|
const costs = parseLiteLLMEntry(entry);
|
|
53
55
|
if (!costs) continue;
|
|
54
56
|
pricing.set(name, costs);
|
|
55
57
|
const stripped = name.replace(/^[^/]+\//, "");
|
|
56
|
-
if (stripped
|
|
58
|
+
if (stripped === name) continue;
|
|
59
|
+
const isDirectProvider = DIRECT_PROVIDER_PREFIXES.some((prefix) => name.toLowerCase().startsWith(prefix));
|
|
60
|
+
const target = isDirectProvider ? preferredStripped : fallbackStripped;
|
|
61
|
+
if (!target.has(stripped)) target.set(stripped, costs);
|
|
62
|
+
}
|
|
63
|
+
for (const [name, costs] of preferredStripped) {
|
|
64
|
+
if (!pricing.has(name)) pricing.set(name, costs);
|
|
65
|
+
}
|
|
66
|
+
for (const [name, costs] of fallbackStripped) {
|
|
67
|
+
if (!pricing.has(name)) pricing.set(name, costs);
|
|
57
68
|
}
|
|
58
69
|
await mkdir5(getCacheDir(), { recursive: true });
|
|
59
70
|
const finalPath = getCachePath();
|
|
@@ -114,18 +125,17 @@ function getCanonicalName(model) {
|
|
|
114
125
|
}
|
|
115
126
|
function getModelCosts(model) {
|
|
116
127
|
const canonical = resolveAlias(getCanonicalName(model));
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
128
|
+
const exactFetched = pricingCache?.get(canonical) ?? null;
|
|
129
|
+
const exactFallback = Object.hasOwn(FALLBACK_PRICING, canonical) ? FALLBACK_PRICING[canonical] : null;
|
|
130
|
+
const exactMatch = mergeModelCosts(exactFetched, exactFallback);
|
|
131
|
+
if (exactMatch) return exactMatch;
|
|
132
|
+
const prefixFetched = findPrefixMatch(pricingCache ?? /* @__PURE__ */ new Map(), canonical);
|
|
133
|
+
const prefixFallback = findPrefixMatch(new Map(Object.entries(FALLBACK_PRICING)), canonical);
|
|
134
|
+
return mergeModelCosts(prefixFetched, prefixFallback);
|
|
126
135
|
}
|
|
127
136
|
function calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, webSearchRequests, speed = "standard") {
|
|
128
|
-
const
|
|
137
|
+
const baseCosts = getModelCosts(model);
|
|
138
|
+
const costs = applyContextTier(baseCosts, inputTokens);
|
|
129
139
|
if (!costs) return 0;
|
|
130
140
|
const multiplier = speed === "fast" ? costs.fastMultiplier : 1;
|
|
131
141
|
return multiplier * (inputTokens * costs.inputCostPerToken + outputTokens * costs.outputCostPerToken + cacheCreationTokens * costs.cacheWriteCostPerToken + cacheReadTokens * costs.cacheReadCostPerToken + webSearchRequests * costs.webSearchCostPerRequest);
|
|
@@ -151,10 +161,17 @@ function getShortModelName(model) {
|
|
|
151
161
|
"gpt-4.1-mini": "GPT-4.1 Mini",
|
|
152
162
|
"gpt-4.1": "GPT-4.1",
|
|
153
163
|
"gpt-5.5": "GPT-5.5",
|
|
164
|
+
"gpt-5.5-pro": "GPT-5.5 Pro",
|
|
165
|
+
"gpt-5.4-pro": "GPT-5.4 Pro",
|
|
166
|
+
"gpt-5.4-nano": "GPT-5.4 Nano",
|
|
154
167
|
"gpt-5.4-mini": "GPT-5.4 Mini",
|
|
155
168
|
"gpt-5.4": "GPT-5.4",
|
|
156
169
|
"gpt-5.3-codex-spark": "GPT-5.3 Codex Spark",
|
|
157
170
|
"gpt-5.3-codex": "GPT-5.3 Codex",
|
|
171
|
+
"gpt-5.2": "GPT-5.2",
|
|
172
|
+
"gpt-5.1": "GPT-5.1",
|
|
173
|
+
"gpt-5-codex": "GPT-5-Codex",
|
|
174
|
+
"gpt-5-nano": "GPT-5 Nano",
|
|
158
175
|
"gpt-5-mini": "GPT-5 Mini",
|
|
159
176
|
"gpt-5": "GPT-5",
|
|
160
177
|
"gemini-2.5-pro": "Gemini 2.5 Pro",
|
|
@@ -163,12 +180,41 @@ function getShortModelName(model) {
|
|
|
163
180
|
"MiniMax-M2.7-highspeed": "MiniMax M2.7 Highspeed",
|
|
164
181
|
"MiniMax-M2.7": "MiniMax M2.7"
|
|
165
182
|
};
|
|
166
|
-
|
|
183
|
+
if (Object.hasOwn(shortNames2, canonical)) return shortNames2[canonical];
|
|
184
|
+
for (const [key, name] of Object.entries(shortNames2).sort((a, b) => b[0].length - a[0].length)) {
|
|
167
185
|
if (canonical.startsWith(key)) return name;
|
|
168
186
|
}
|
|
169
187
|
return canonical;
|
|
170
188
|
}
|
|
171
|
-
|
|
189
|
+
function mergeModelCosts(primary, fallback) {
|
|
190
|
+
if (!primary && !fallback) return null;
|
|
191
|
+
if (!primary) return fallback ?? null;
|
|
192
|
+
if (!fallback) return primary;
|
|
193
|
+
return {
|
|
194
|
+
...fallback,
|
|
195
|
+
...primary,
|
|
196
|
+
contextTiers: primary.contextTiers ?? fallback.contextTiers
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function findPrefixMatch(entries, canonical) {
|
|
200
|
+
for (const [key, costs] of entries) {
|
|
201
|
+
if (canonical.startsWith(key)) return costs;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
function applyContextTier(costs, inputTokens) {
|
|
206
|
+
if (!costs || !costs.contextTiers || costs.contextTiers.length === 0) return costs;
|
|
207
|
+
const tier = costs.contextTiers.filter((candidate) => inputTokens > candidate.minPromptTokens).sort((a, b) => b.minPromptTokens - a.minPromptTokens)[0];
|
|
208
|
+
if (!tier) return costs;
|
|
209
|
+
return {
|
|
210
|
+
...costs,
|
|
211
|
+
inputCostPerToken: tier.inputCostPerToken ?? costs.inputCostPerToken,
|
|
212
|
+
outputCostPerToken: tier.outputCostPerToken ?? costs.outputCostPerToken,
|
|
213
|
+
cacheWriteCostPerToken: tier.cacheWriteCostPerToken ?? costs.cacheWriteCostPerToken,
|
|
214
|
+
cacheReadCostPerToken: tier.cacheReadCostPerToken ?? costs.cacheReadCostPerToken
|
|
215
|
+
};
|
|
216
|
+
}
|
|
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;
|
|
172
218
|
var init_models = __esm({
|
|
173
219
|
"src/models.ts"() {
|
|
174
220
|
"use strict";
|
|
@@ -176,6 +222,9 @@ var init_models = __esm({
|
|
|
176
222
|
LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
177
223
|
CACHE_TTL_MS2 = 24 * 60 * 60 * 1e3;
|
|
178
224
|
WEB_SEARCH_COST = 0.01;
|
|
225
|
+
LONG_CONTEXT_1M_THRESHOLD_TOKENS = 272e3;
|
|
226
|
+
GEMINI_LONG_CONTEXT_THRESHOLD_TOKENS = 2e5;
|
|
227
|
+
DIRECT_PROVIDER_PREFIXES = ["openai/", "anthropic/", "google/", "vertex_ai/", "minimax/"];
|
|
179
228
|
FALLBACK_PRICING = {
|
|
180
229
|
"claude-opus-4-7": { inputCostPerToken: 5e-6, outputCostPerToken: 25e-6, cacheWriteCostPerToken: 625e-8, cacheReadCostPerToken: 5e-7, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 6 },
|
|
181
230
|
"claude-opus-4-6": { inputCostPerToken: 5e-6, outputCostPerToken: 25e-6, cacheWriteCostPerToken: 625e-8, cacheReadCostPerToken: 5e-7, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 6 },
|
|
@@ -191,14 +240,60 @@ var init_models = __esm({
|
|
|
191
240
|
"claude-3-5-haiku": { inputCostPerToken: 8e-7, outputCostPerToken: 4e-6, cacheWriteCostPerToken: 1e-6, cacheReadCostPerToken: 8e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
192
241
|
"gpt-4o": { inputCostPerToken: 25e-7, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 25e-7, cacheReadCostPerToken: 125e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
193
242
|
"gpt-4o-mini": { inputCostPerToken: 15e-8, outputCostPerToken: 6e-7, cacheWriteCostPerToken: 15e-8, cacheReadCostPerToken: 75e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
194
|
-
"gemini-2.5-pro": {
|
|
243
|
+
"gemini-2.5-pro": {
|
|
244
|
+
inputCostPerToken: 125e-8,
|
|
245
|
+
outputCostPerToken: 1e-5,
|
|
246
|
+
cacheWriteCostPerToken: 125e-8,
|
|
247
|
+
cacheReadCostPerToken: 125e-9,
|
|
248
|
+
webSearchCostPerRequest: WEB_SEARCH_COST,
|
|
249
|
+
fastMultiplier: 1,
|
|
250
|
+
contextTiers: [{
|
|
251
|
+
minPromptTokens: GEMINI_LONG_CONTEXT_THRESHOLD_TOKENS,
|
|
252
|
+
inputCostPerToken: 25e-7,
|
|
253
|
+
outputCostPerToken: 15e-6,
|
|
254
|
+
cacheReadCostPerToken: 25e-8
|
|
255
|
+
}]
|
|
256
|
+
},
|
|
195
257
|
"gpt-5.5": { inputCostPerToken: 5e-6, outputCostPerToken: 3e-5, cacheWriteCostPerToken: 5e-6, cacheReadCostPerToken: 5e-7, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
196
|
-
"gpt-5.
|
|
258
|
+
"gpt-5.5-pro": { inputCostPerToken: 3e-5, outputCostPerToken: 18e-5, cacheWriteCostPerToken: 0, cacheReadCostPerToken: 0, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
259
|
+
"gpt-5.4": {
|
|
260
|
+
inputCostPerToken: 25e-7,
|
|
261
|
+
outputCostPerToken: 15e-6,
|
|
262
|
+
cacheWriteCostPerToken: 25e-7,
|
|
263
|
+
cacheReadCostPerToken: 25e-8,
|
|
264
|
+
webSearchCostPerRequest: WEB_SEARCH_COST,
|
|
265
|
+
fastMultiplier: 1,
|
|
266
|
+
contextTiers: [{
|
|
267
|
+
minPromptTokens: LONG_CONTEXT_1M_THRESHOLD_TOKENS,
|
|
268
|
+
inputCostPerToken: 5e-6,
|
|
269
|
+
outputCostPerToken: 225e-7,
|
|
270
|
+
cacheWriteCostPerToken: 5e-6,
|
|
271
|
+
cacheReadCostPerToken: 5e-7
|
|
272
|
+
}]
|
|
273
|
+
},
|
|
274
|
+
"gpt-5.4-pro": {
|
|
275
|
+
inputCostPerToken: 3e-5,
|
|
276
|
+
outputCostPerToken: 18e-5,
|
|
277
|
+
cacheWriteCostPerToken: 0,
|
|
278
|
+
cacheReadCostPerToken: 0,
|
|
279
|
+
webSearchCostPerRequest: WEB_SEARCH_COST,
|
|
280
|
+
fastMultiplier: 1,
|
|
281
|
+
contextTiers: [{
|
|
282
|
+
minPromptTokens: LONG_CONTEXT_1M_THRESHOLD_TOKENS,
|
|
283
|
+
inputCostPerToken: 6e-5,
|
|
284
|
+
outputCostPerToken: 27e-5
|
|
285
|
+
}]
|
|
286
|
+
},
|
|
197
287
|
"gpt-5.4-mini": { inputCostPerToken: 75e-8, outputCostPerToken: 45e-7, cacheWriteCostPerToken: 75e-8, cacheReadCostPerToken: 75e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
288
|
+
"gpt-5.4-nano": { inputCostPerToken: 2e-7, outputCostPerToken: 125e-8, cacheWriteCostPerToken: 2e-7, cacheReadCostPerToken: 2e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
198
289
|
"gpt-5.3-codex": { inputCostPerToken: 175e-8, outputCostPerToken: 14e-6, cacheWriteCostPerToken: 175e-8, cacheReadCostPerToken: 175e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
199
290
|
"gpt-5.3-codex-spark": { inputCostPerToken: 175e-8, outputCostPerToken: 14e-6, cacheWriteCostPerToken: 175e-8, cacheReadCostPerToken: 175e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
200
|
-
"gpt-5": { inputCostPerToken:
|
|
201
|
-
"gpt-5
|
|
291
|
+
"gpt-5.2": { inputCostPerToken: 175e-8, outputCostPerToken: 14e-6, cacheWriteCostPerToken: 175e-8, cacheReadCostPerToken: 175e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
292
|
+
"gpt-5.1": { inputCostPerToken: 125e-8, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 125e-8, cacheReadCostPerToken: 125e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
293
|
+
"gpt-5": { inputCostPerToken: 125e-8, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 125e-8, cacheReadCostPerToken: 125e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
294
|
+
"gpt-5-codex": { inputCostPerToken: 125e-8, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 125e-8, cacheReadCostPerToken: 125e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
295
|
+
"gpt-5-mini": { inputCostPerToken: 25e-8, outputCostPerToken: 2e-6, cacheWriteCostPerToken: 25e-8, cacheReadCostPerToken: 25e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
296
|
+
"gpt-5-nano": { inputCostPerToken: 5e-8, outputCostPerToken: 4e-7, cacheWriteCostPerToken: 5e-8, cacheReadCostPerToken: 5e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
202
297
|
"gpt-4.1": { inputCostPerToken: 2e-6, outputCostPerToken: 8e-6, cacheWriteCostPerToken: 2e-6, cacheReadCostPerToken: 5e-7, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
203
298
|
"gpt-4.1-mini": { inputCostPerToken: 4e-7, outputCostPerToken: 16e-7, cacheWriteCostPerToken: 4e-7, cacheReadCostPerToken: 1e-7, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
204
299
|
"gpt-4.1-nano": { inputCostPerToken: 1e-7, outputCostPerToken: 4e-7, cacheWriteCostPerToken: 1e-7, cacheReadCostPerToken: 25e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
@@ -206,9 +301,9 @@ var init_models = __esm({
|
|
|
206
301
|
"o4-mini": { inputCostPerToken: 11e-7, outputCostPerToken: 44e-7, cacheWriteCostPerToken: 11e-7, cacheReadCostPerToken: 275e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
207
302
|
"codex-mini-latest": { inputCostPerToken: 15e-7, outputCostPerToken: 6e-6, cacheWriteCostPerToken: 15e-7, cacheReadCostPerToken: 375e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
208
303
|
"codex-mini": { inputCostPerToken: 15e-7, outputCostPerToken: 6e-6, cacheWriteCostPerToken: 15e-7, cacheReadCostPerToken: 375e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
209
|
-
"gpt-5.1-codex": { inputCostPerToken: 125e-8, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 125e-8, cacheReadCostPerToken:
|
|
210
|
-
"gpt-5.1-codex-mini": { inputCostPerToken: 25e-8, outputCostPerToken: 2e-6, cacheWriteCostPerToken: 25e-8, cacheReadCostPerToken:
|
|
211
|
-
"gpt-5.2-codex": { inputCostPerToken: 175e-8, outputCostPerToken: 14e-6, cacheWriteCostPerToken: 175e-8, cacheReadCostPerToken:
|
|
304
|
+
"gpt-5.1-codex": { inputCostPerToken: 125e-8, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 125e-8, cacheReadCostPerToken: 125e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
305
|
+
"gpt-5.1-codex-mini": { inputCostPerToken: 25e-8, outputCostPerToken: 2e-6, cacheWriteCostPerToken: 25e-8, cacheReadCostPerToken: 25e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
306
|
+
"gpt-5.2-codex": { inputCostPerToken: 175e-8, outputCostPerToken: 14e-6, cacheWriteCostPerToken: 175e-8, cacheReadCostPerToken: 175e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
212
307
|
"MiniMax-M2.7-highspeed": { inputCostPerToken: 6e-7, outputCostPerToken: 24e-7, cacheWriteCostPerToken: 375e-9, cacheReadCostPerToken: 6e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
|
|
213
308
|
"MiniMax-M2.7": { inputCostPerToken: 3e-7, outputCostPerToken: 12e-7, cacheWriteCostPerToken: 375e-9, cacheReadCostPerToken: 6e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }
|
|
214
309
|
};
|
|
@@ -870,7 +965,7 @@ __export(cursor_agent_exports, {
|
|
|
870
965
|
createCursorAgentProvider: () => createCursorAgentProvider,
|
|
871
966
|
cursor_agent: () => cursor_agent
|
|
872
967
|
});
|
|
873
|
-
import { createHash } from "crypto";
|
|
968
|
+
import { createHash as createHash2 } from "crypto";
|
|
874
969
|
import { existsSync as existsSync2 } from "fs";
|
|
875
970
|
import { readdir as readdir7, readFile as readFile6, stat as stat9 } from "fs/promises";
|
|
876
971
|
import { join as join14, basename as basename6 } from "path";
|
|
@@ -934,7 +1029,7 @@ function resolveModel3(raw) {
|
|
|
934
1029
|
function toConversationId(transcriptPath) {
|
|
935
1030
|
const filename = basename6(transcriptPath, ".txt");
|
|
936
1031
|
if (filename.length === 36 && UUID_LIKE.test(filename)) return filename;
|
|
937
|
-
return
|
|
1032
|
+
return createHash2("sha1").update(transcriptPath).digest("hex").slice(0, 16);
|
|
938
1033
|
}
|
|
939
1034
|
function extractUserQuery(userBlock) {
|
|
940
1035
|
const chunks = [];
|
|
@@ -1548,6 +1643,9 @@ function formatCost(costUSD) {
|
|
|
1548
1643
|
}
|
|
1549
1644
|
|
|
1550
1645
|
// src/day-aggregator.ts
|
|
1646
|
+
function formatDate(date) {
|
|
1647
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
1648
|
+
}
|
|
1551
1649
|
function emptyEntry(date) {
|
|
1552
1650
|
return {
|
|
1553
1651
|
date,
|
|
@@ -1565,9 +1663,23 @@ function emptyEntry(date) {
|
|
|
1565
1663
|
providers: {}
|
|
1566
1664
|
};
|
|
1567
1665
|
}
|
|
1666
|
+
function emptyProviderBreakdown() {
|
|
1667
|
+
return {
|
|
1668
|
+
calls: 0,
|
|
1669
|
+
cost: 0,
|
|
1670
|
+
sessions: 0,
|
|
1671
|
+
inputTokens: 0,
|
|
1672
|
+
outputTokens: 0,
|
|
1673
|
+
cacheReadTokens: 0,
|
|
1674
|
+
cacheWriteTokens: 0,
|
|
1675
|
+
editTurns: 0,
|
|
1676
|
+
oneShotTurns: 0,
|
|
1677
|
+
models: {},
|
|
1678
|
+
categories: {}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1568
1681
|
function dateKey(iso) {
|
|
1569
|
-
|
|
1570
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1682
|
+
return formatDate(new Date(iso));
|
|
1571
1683
|
}
|
|
1572
1684
|
function aggregateProjectsIntoDays(projects) {
|
|
1573
1685
|
const byDate = /* @__PURE__ */ new Map();
|
|
@@ -1579,10 +1691,20 @@ function aggregateProjectsIntoDays(projects) {
|
|
|
1579
1691
|
}
|
|
1580
1692
|
return d;
|
|
1581
1693
|
};
|
|
1694
|
+
const ensureProvider = (date, provider) => {
|
|
1695
|
+
const day = ensure(date);
|
|
1696
|
+
let breakdown = day.providers[provider];
|
|
1697
|
+
if (!breakdown) {
|
|
1698
|
+
breakdown = emptyProviderBreakdown();
|
|
1699
|
+
day.providers[provider] = breakdown;
|
|
1700
|
+
}
|
|
1701
|
+
return breakdown;
|
|
1702
|
+
};
|
|
1582
1703
|
for (const project of projects) {
|
|
1583
1704
|
for (const session of project.sessions) {
|
|
1584
1705
|
const sessionDate = dateKey(session.firstTimestamp);
|
|
1585
1706
|
ensure(sessionDate).sessions += 1;
|
|
1707
|
+
const providersSeenInSession = /* @__PURE__ */ new Set();
|
|
1586
1708
|
for (const turn of session.turns) {
|
|
1587
1709
|
if (turn.assistantCalls.length === 0) continue;
|
|
1588
1710
|
const turnDate = dateKey(turn.assistantCalls[0].timestamp);
|
|
@@ -1598,7 +1720,11 @@ function aggregateProjectsIntoDays(projects) {
|
|
|
1598
1720
|
cat.editTurns += editTurns;
|
|
1599
1721
|
cat.oneShotTurns += oneShotTurns;
|
|
1600
1722
|
turnDay.categories[turn.category] = cat;
|
|
1723
|
+
const callsByProvider = /* @__PURE__ */ new Map();
|
|
1601
1724
|
for (const call of turn.assistantCalls) {
|
|
1725
|
+
const existingProviderCalls = callsByProvider.get(call.provider) ?? [];
|
|
1726
|
+
existingProviderCalls.push(call);
|
|
1727
|
+
callsByProvider.set(call.provider, existingProviderCalls);
|
|
1602
1728
|
const callDate = dateKey(call.timestamp);
|
|
1603
1729
|
const callDay = ensure(callDate);
|
|
1604
1730
|
callDay.cost += call.costUSD;
|
|
@@ -1622,39 +1748,90 @@ function aggregateProjectsIntoDays(projects) {
|
|
|
1622
1748
|
model.cacheReadTokens += call.usage.cacheReadInputTokens;
|
|
1623
1749
|
model.cacheWriteTokens += call.usage.cacheCreationInputTokens;
|
|
1624
1750
|
callDay.models[call.model] = model;
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
provider
|
|
1628
|
-
|
|
1751
|
+
}
|
|
1752
|
+
for (const [provider, calls] of callsByProvider) {
|
|
1753
|
+
providersSeenInSession.add(provider);
|
|
1754
|
+
const providerTurnDay = ensureProvider(turnDate, provider);
|
|
1755
|
+
const providerTurnCost = calls.reduce((sum, call) => sum + call.costUSD, 0);
|
|
1756
|
+
providerTurnDay.editTurns += editTurns;
|
|
1757
|
+
providerTurnDay.oneShotTurns += oneShotTurns;
|
|
1758
|
+
const providerCategory = providerTurnDay.categories[turn.category] ?? { turns: 0, cost: 0, editTurns: 0, oneShotTurns: 0 };
|
|
1759
|
+
providerCategory.turns += 1;
|
|
1760
|
+
providerCategory.cost += providerTurnCost;
|
|
1761
|
+
providerCategory.editTurns += editTurns;
|
|
1762
|
+
providerCategory.oneShotTurns += oneShotTurns;
|
|
1763
|
+
providerTurnDay.categories[turn.category] = providerCategory;
|
|
1764
|
+
for (const call of calls) {
|
|
1765
|
+
const callDate = dateKey(call.timestamp);
|
|
1766
|
+
const providerCallDay = ensureProvider(callDate, provider);
|
|
1767
|
+
providerCallDay.cost += call.costUSD;
|
|
1768
|
+
providerCallDay.calls += 1;
|
|
1769
|
+
providerCallDay.inputTokens += call.usage.inputTokens;
|
|
1770
|
+
providerCallDay.outputTokens += call.usage.outputTokens;
|
|
1771
|
+
providerCallDay.cacheReadTokens += call.usage.cacheReadInputTokens;
|
|
1772
|
+
providerCallDay.cacheWriteTokens += call.usage.cacheCreationInputTokens;
|
|
1773
|
+
const model = providerCallDay.models[call.model] ?? {
|
|
1774
|
+
calls: 0,
|
|
1775
|
+
cost: 0,
|
|
1776
|
+
inputTokens: 0,
|
|
1777
|
+
outputTokens: 0,
|
|
1778
|
+
cacheReadTokens: 0,
|
|
1779
|
+
cacheWriteTokens: 0
|
|
1780
|
+
};
|
|
1781
|
+
model.calls += 1;
|
|
1782
|
+
model.cost += call.costUSD;
|
|
1783
|
+
model.inputTokens += call.usage.inputTokens;
|
|
1784
|
+
model.outputTokens += call.usage.outputTokens;
|
|
1785
|
+
model.cacheReadTokens += call.usage.cacheReadInputTokens;
|
|
1786
|
+
model.cacheWriteTokens += call.usage.cacheCreationInputTokens;
|
|
1787
|
+
providerCallDay.models[call.model] = model;
|
|
1788
|
+
}
|
|
1629
1789
|
}
|
|
1630
1790
|
}
|
|
1791
|
+
for (const provider of providersSeenInSession) {
|
|
1792
|
+
ensureProvider(sessionDate, provider).sessions += 1;
|
|
1793
|
+
}
|
|
1631
1794
|
}
|
|
1632
1795
|
}
|
|
1633
1796
|
return [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
1634
1797
|
}
|
|
1635
1798
|
function filterDaysToProvider(days, provider) {
|
|
1636
1799
|
return days.map((d) => {
|
|
1637
|
-
const p = d.providers[provider];
|
|
1638
|
-
if (!p) return null;
|
|
1800
|
+
const p = d.providers[provider] ?? emptyProviderBreakdown();
|
|
1639
1801
|
return {
|
|
1640
1802
|
...d,
|
|
1641
1803
|
cost: p.cost,
|
|
1642
1804
|
calls: p.calls,
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
providers: { [provider]: p }
|
|
1805
|
+
sessions: p.sessions,
|
|
1806
|
+
inputTokens: p.inputTokens,
|
|
1807
|
+
outputTokens: p.outputTokens,
|
|
1808
|
+
cacheReadTokens: p.cacheReadTokens,
|
|
1809
|
+
cacheWriteTokens: p.cacheWriteTokens,
|
|
1810
|
+
editTurns: p.editTurns,
|
|
1811
|
+
oneShotTurns: p.oneShotTurns,
|
|
1812
|
+
models: { ...p.models },
|
|
1813
|
+
categories: { ...p.categories },
|
|
1814
|
+
providers: d.providers[provider] ? { [provider]: p } : {}
|
|
1654
1815
|
};
|
|
1655
|
-
})
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
function fillMissingDays(start, end, days) {
|
|
1819
|
+
const byDate = new Map(days.map((day) => [day.date, day]));
|
|
1820
|
+
const filled = [];
|
|
1821
|
+
const cursor2 = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
1822
|
+
const endDate = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
|
1823
|
+
while (cursor2.getTime() <= endDate.getTime()) {
|
|
1824
|
+
const key = formatDate(cursor2);
|
|
1825
|
+
filled.push(byDate.get(key) ?? emptyEntry(key));
|
|
1826
|
+
cursor2.setDate(cursor2.getDate() + 1);
|
|
1827
|
+
}
|
|
1828
|
+
return filled;
|
|
1656
1829
|
}
|
|
1657
1830
|
function buildPeriodDataFromDays(days, label) {
|
|
1831
|
+
if (days.length === 0) {
|
|
1832
|
+
process.stderr.write(`[exe-watcher] WARNING: buildPeriodDataFromDays called with 0 days for label '${label}'
|
|
1833
|
+
`);
|
|
1834
|
+
}
|
|
1658
1835
|
let cost = 0, calls = 0, sessions = 0;
|
|
1659
1836
|
let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0;
|
|
1660
1837
|
const catTotals = {};
|
|
@@ -2201,6 +2378,7 @@ var modelDisplayNames = {
|
|
|
2201
2378
|
"gpt-4o-mini": "GPT-4o Mini",
|
|
2202
2379
|
"gpt-4o": "GPT-4o"
|
|
2203
2380
|
};
|
|
2381
|
+
var modelDisplayEntries = Object.entries(modelDisplayNames).sort((a, b) => b[0].length - a[0].length);
|
|
2204
2382
|
var toolNameMap = {
|
|
2205
2383
|
exec_command: "Bash",
|
|
2206
2384
|
read_file: "Read",
|
|
@@ -2392,7 +2570,7 @@ function createCodexProvider(codexDir) {
|
|
|
2392
2570
|
name: "codex",
|
|
2393
2571
|
displayName: "Codex",
|
|
2394
2572
|
modelDisplayName(model) {
|
|
2395
|
-
for (const [key, name] of
|
|
2573
|
+
for (const [key, name] of modelDisplayEntries) {
|
|
2396
2574
|
if (model.startsWith(key)) return name;
|
|
2397
2575
|
}
|
|
2398
2576
|
return model;
|
|
@@ -2444,7 +2622,7 @@ var toolNameMap2 = {
|
|
|
2444
2622
|
fetch_webpage: "WebFetch",
|
|
2445
2623
|
github_repo: "GitHub"
|
|
2446
2624
|
};
|
|
2447
|
-
var
|
|
2625
|
+
var modelDisplayEntries2 = Object.entries(modelDisplayNames2).sort((a, b) => b[0].length - a[0].length);
|
|
2448
2626
|
function getCopilotSessionStateDir(override) {
|
|
2449
2627
|
return override ?? join9(homedir6(), ".copilot", "session-state");
|
|
2450
2628
|
}
|
|
@@ -2540,7 +2718,7 @@ function createCopilotProvider(sessionStateDir) {
|
|
|
2540
2718
|
name: "copilot",
|
|
2541
2719
|
displayName: "Copilot",
|
|
2542
2720
|
modelDisplayName(model) {
|
|
2543
|
-
for (const [key, name] of
|
|
2721
|
+
for (const [key, name] of modelDisplayEntries2) {
|
|
2544
2722
|
if (model === key || model.startsWith(key + "-")) return name;
|
|
2545
2723
|
}
|
|
2546
2724
|
return model;
|
|
@@ -2585,7 +2763,7 @@ var toolNameMap3 = {
|
|
|
2585
2763
|
todo: "TodoWrite",
|
|
2586
2764
|
patch: "Patch"
|
|
2587
2765
|
};
|
|
2588
|
-
var
|
|
2766
|
+
var modelDisplayEntries3 = Object.entries(modelDisplayNames3).sort((a, b) => b[0].length - a[0].length);
|
|
2589
2767
|
function getPiSessionsDir(override) {
|
|
2590
2768
|
return override ?? join10(homedir7(), ".pi", "agent", "sessions");
|
|
2591
2769
|
}
|
|
@@ -2707,7 +2885,7 @@ function createPiProvider(sessionsDir) {
|
|
|
2707
2885
|
name: "pi",
|
|
2708
2886
|
displayName: "Pi",
|
|
2709
2887
|
modelDisplayName(model) {
|
|
2710
|
-
for (const [key, name] of
|
|
2888
|
+
for (const [key, name] of modelDisplayEntries3) {
|
|
2711
2889
|
if (model.startsWith(key)) return name;
|
|
2712
2890
|
}
|
|
2713
2891
|
return model;
|
|
@@ -2730,7 +2908,7 @@ function createOmpProvider(sessionsDir) {
|
|
|
2730
2908
|
name: "omp",
|
|
2731
2909
|
displayName: "OMP",
|
|
2732
2910
|
modelDisplayName(model) {
|
|
2733
|
-
for (const [key, name] of
|
|
2911
|
+
for (const [key, name] of modelDisplayEntries3) {
|
|
2734
2912
|
if (model.startsWith(key)) return name;
|
|
2735
2913
|
}
|
|
2736
2914
|
return model;
|
|
@@ -3315,9 +3493,68 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
|
|
|
3315
3493
|
var CACHE_TTL_MS3 = 6e4;
|
|
3316
3494
|
var MAX_CACHE_ENTRIES = 10;
|
|
3317
3495
|
var sessionCache = /* @__PURE__ */ new Map();
|
|
3318
|
-
|
|
3496
|
+
var inFlightSourceContexts = /* @__PURE__ */ new Map();
|
|
3497
|
+
function sourceContextCacheKey(providerFilter) {
|
|
3498
|
+
return JSON.stringify({
|
|
3499
|
+
provider: providerFilter ?? "all",
|
|
3500
|
+
claudeConfigDir: process.env["CLAUDE_CONFIG_DIR"] ?? "",
|
|
3501
|
+
codexHome: process.env["CODEX_HOME"] ?? "",
|
|
3502
|
+
xdgDataHome: process.env["XDG_DATA_HOME"] ?? "",
|
|
3503
|
+
home: process.env["HOME"] ?? ""
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
function cacheKey(dateRange, providerFilter, sourceFingerprint = "static") {
|
|
3319
3507
|
const s = dateRange ? `${dateRange.start.getTime()}:${dateRange.end.getTime()}` : "none";
|
|
3320
|
-
return `${s}:${providerFilter ?? "all"}`;
|
|
3508
|
+
return `${s}:${providerFilter ?? "all"}:${sourceFingerprint}`;
|
|
3509
|
+
}
|
|
3510
|
+
async function buildSourceFingerprint(path) {
|
|
3511
|
+
try {
|
|
3512
|
+
const fp = await stat10(path);
|
|
3513
|
+
if (!fp.isDirectory()) return `f:${Math.round(fp.mtimeMs)}:${fp.size}`;
|
|
3514
|
+
const entries = await readdir8(path);
|
|
3515
|
+
const children = await Promise.all(entries.map(async (entry) => stat10(join15(path, entry)).catch(() => null)));
|
|
3516
|
+
let childCount = 0;
|
|
3517
|
+
let childSizeBytes = 0;
|
|
3518
|
+
let newestChildMtimeMs = 0;
|
|
3519
|
+
for (const child of children) {
|
|
3520
|
+
if (!child) continue;
|
|
3521
|
+
childCount += 1;
|
|
3522
|
+
childSizeBytes += child.size;
|
|
3523
|
+
newestChildMtimeMs = Math.max(newestChildMtimeMs, child.mtimeMs);
|
|
3524
|
+
}
|
|
3525
|
+
return `d:${Math.round(fp.mtimeMs)}:${fp.size}:${childCount}:${Math.round(newestChildMtimeMs)}:${childSizeBytes}`;
|
|
3526
|
+
} catch {
|
|
3527
|
+
return "missing";
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
async function buildCacheFingerprint(sources) {
|
|
3531
|
+
const parts = await Promise.all(sources.map(async (source) => {
|
|
3532
|
+
const fingerprint = await buildSourceFingerprint(source.path);
|
|
3533
|
+
return `${source.provider}:${source.project}:${source.path}:${fingerprint}`;
|
|
3534
|
+
}));
|
|
3535
|
+
return parts.sort().join("|");
|
|
3536
|
+
}
|
|
3537
|
+
async function getSourceContext(providerFilter) {
|
|
3538
|
+
const key = sourceContextCacheKey(providerFilter);
|
|
3539
|
+
const cached = inFlightSourceContexts.get(key);
|
|
3540
|
+
if (cached) {
|
|
3541
|
+
return cached;
|
|
3542
|
+
}
|
|
3543
|
+
const promise = (async () => {
|
|
3544
|
+
const sources = await discoverAllSessions(providerFilter);
|
|
3545
|
+
const fingerprint = await buildCacheFingerprint(sources);
|
|
3546
|
+
return { sources, fingerprint };
|
|
3547
|
+
})();
|
|
3548
|
+
inFlightSourceContexts.set(key, promise);
|
|
3549
|
+
try {
|
|
3550
|
+
return await promise;
|
|
3551
|
+
} catch (err) {
|
|
3552
|
+
throw err;
|
|
3553
|
+
} finally {
|
|
3554
|
+
if (inFlightSourceContexts.get(key) === promise) {
|
|
3555
|
+
inFlightSourceContexts.delete(key);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3321
3558
|
}
|
|
3322
3559
|
function cachePut(key, data) {
|
|
3323
3560
|
const now = Date.now();
|
|
@@ -3351,12 +3588,13 @@ function filterProjectsByName(projects, include, exclude) {
|
|
|
3351
3588
|
return result;
|
|
3352
3589
|
}
|
|
3353
3590
|
async function parseAllSessions(dateRange, providerFilter) {
|
|
3354
|
-
const
|
|
3591
|
+
const sourceContext = await getSourceContext(providerFilter);
|
|
3592
|
+
const allSources = sourceContext.sources;
|
|
3593
|
+
const key = cacheKey(dateRange, providerFilter, sourceContext.fingerprint);
|
|
3355
3594
|
const cached = sessionCache.get(key);
|
|
3356
3595
|
if (cached && Date.now() - cached.ts < CACHE_TTL_MS3) return cached.data;
|
|
3357
3596
|
const seenMsgIds = /* @__PURE__ */ new Set();
|
|
3358
3597
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
3359
|
-
const allSources = await discoverAllSessions(providerFilter);
|
|
3360
3598
|
const claudeSources = allSources.filter((s) => s.provider === "claude");
|
|
3361
3599
|
const nonClaudeSources = allSources.filter((s) => s.provider !== "claude");
|
|
3362
3600
|
const claudeDirs = claudeSources.map((s) => ({ path: s.path, name: s.project }));
|
|
@@ -3543,7 +3781,7 @@ function buildHistory(daily) {
|
|
|
3543
3781
|
const trimmed = sorted.slice(-HISTORY_DAYS_LIMIT);
|
|
3544
3782
|
return { daily: trimmed };
|
|
3545
3783
|
}
|
|
3546
|
-
function buildMenubarPayload(current, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge) {
|
|
3784
|
+
function buildMenubarPayload(current, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge, diagnostics) {
|
|
3547
3785
|
return {
|
|
3548
3786
|
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3549
3787
|
current: {
|
|
@@ -3561,6 +3799,7 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
|
|
|
3561
3799
|
},
|
|
3562
3800
|
optimize: buildOptimize(optimize),
|
|
3563
3801
|
history: buildHistory(dailyHistory),
|
|
3802
|
+
diagnostics: diagnostics ?? { daysCount: 0, parseTimeMs: 0, warnings: [] },
|
|
3564
3803
|
agentStats: agentStats ?? null,
|
|
3565
3804
|
exeOsDetected: exeOsDetected ?? false,
|
|
3566
3805
|
statsFileAge: statsFileAge ?? null,
|
|
@@ -3570,43 +3809,62 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
|
|
|
3570
3809
|
|
|
3571
3810
|
// src/daily-cache.ts
|
|
3572
3811
|
init_cache_dir();
|
|
3573
|
-
import { randomBytes as randomBytes5 } from "crypto";
|
|
3812
|
+
import { createHash as createHash3, randomBytes as randomBytes5 } from "crypto";
|
|
3574
3813
|
import { existsSync as existsSync3 } from "fs";
|
|
3575
3814
|
import { mkdir as mkdir7, open as open5, readFile as readFile7, rename as rename6, unlink as unlink3 } from "fs/promises";
|
|
3576
3815
|
import { join as join16 } from "path";
|
|
3577
|
-
var DAILY_CACHE_VERSION =
|
|
3816
|
+
var DAILY_CACHE_VERSION = 4;
|
|
3817
|
+
var DEFAULT_DAILY_CACHE_SCOPE = "global";
|
|
3578
3818
|
var DAILY_CACHE_FILENAME = "daily-cache.json";
|
|
3579
|
-
function getCachePath3() {
|
|
3580
|
-
|
|
3819
|
+
function getCachePath3(scopeKey) {
|
|
3820
|
+
if (scopeKey === DEFAULT_DAILY_CACHE_SCOPE) {
|
|
3821
|
+
return join16(getCacheDir(), DAILY_CACHE_FILENAME);
|
|
3822
|
+
}
|
|
3823
|
+
return join16(getCacheDir(), `daily-cache-${scopeKey}.json`);
|
|
3581
3824
|
}
|
|
3582
|
-
function emptyCache() {
|
|
3583
|
-
return { version: DAILY_CACHE_VERSION, lastComputedDate: null, days: [] };
|
|
3825
|
+
function emptyCache(scopeKey) {
|
|
3826
|
+
return { version: DAILY_CACHE_VERSION, scopeKey, lastComputedDate: null, days: [] };
|
|
3827
|
+
}
|
|
3828
|
+
function normalizeScopeTerms(values) {
|
|
3829
|
+
return [...new Set((values ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean))].sort();
|
|
3830
|
+
}
|
|
3831
|
+
function buildDailyCacheScopeKey(include, exclude) {
|
|
3832
|
+
const normalizedInclude = normalizeScopeTerms(include);
|
|
3833
|
+
const normalizedExclude = normalizeScopeTerms(exclude);
|
|
3834
|
+
if (normalizedInclude.length === 0 && normalizedExclude.length === 0) {
|
|
3835
|
+
return DEFAULT_DAILY_CACHE_SCOPE;
|
|
3836
|
+
}
|
|
3837
|
+
const raw = JSON.stringify({ include: normalizedInclude, exclude: normalizedExclude });
|
|
3838
|
+
return `scope-${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3584
3839
|
}
|
|
3585
3840
|
function isValidCache(parsed) {
|
|
3586
3841
|
if (!parsed || typeof parsed !== "object") return false;
|
|
3587
3842
|
const c = parsed;
|
|
3588
3843
|
if (c.version !== DAILY_CACHE_VERSION) return false;
|
|
3844
|
+
if (typeof c.scopeKey !== "string" || c.scopeKey.length === 0) return false;
|
|
3589
3845
|
if (!Array.isArray(c.days)) return false;
|
|
3590
3846
|
return true;
|
|
3591
3847
|
}
|
|
3592
|
-
async function loadDailyCache() {
|
|
3593
|
-
const path = getCachePath3();
|
|
3594
|
-
if (!existsSync3(path)) return emptyCache();
|
|
3848
|
+
async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
|
|
3849
|
+
const path = getCachePath3(scopeKey);
|
|
3850
|
+
if (!existsSync3(path)) return emptyCache(scopeKey);
|
|
3595
3851
|
try {
|
|
3596
3852
|
const raw = await readFile7(path, "utf-8");
|
|
3597
3853
|
const parsed = JSON.parse(raw);
|
|
3598
|
-
if (!isValidCache(parsed)) return emptyCache();
|
|
3854
|
+
if (!isValidCache(parsed)) return emptyCache(scopeKey);
|
|
3855
|
+
if (parsed.scopeKey !== scopeKey) return emptyCache(scopeKey);
|
|
3599
3856
|
return parsed;
|
|
3600
3857
|
} catch {
|
|
3601
|
-
return emptyCache();
|
|
3858
|
+
return emptyCache(scopeKey);
|
|
3602
3859
|
}
|
|
3603
3860
|
}
|
|
3604
3861
|
async function saveDailyCache(cache) {
|
|
3605
3862
|
const dir = getCacheDir();
|
|
3606
3863
|
if (!existsSync3(dir)) await mkdir7(dir, { recursive: true });
|
|
3607
|
-
const
|
|
3864
|
+
const scopeKey = cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE;
|
|
3865
|
+
const finalPath = getCachePath3(scopeKey);
|
|
3608
3866
|
const tempPath = `${finalPath}.${randomBytes5(8).toString("hex")}.tmp`;
|
|
3609
|
-
const payload = JSON.stringify(cache);
|
|
3867
|
+
const payload = JSON.stringify({ ...cache, scopeKey });
|
|
3610
3868
|
const handle = await open5(tempPath, "w", 384);
|
|
3611
3869
|
try {
|
|
3612
3870
|
await handle.writeFile(payload, { encoding: "utf-8" });
|
|
@@ -3624,14 +3882,20 @@ async function saveDailyCache(cache) {
|
|
|
3624
3882
|
throw err;
|
|
3625
3883
|
}
|
|
3626
3884
|
}
|
|
3627
|
-
function addNewDays(cache, incoming, newestDate) {
|
|
3885
|
+
function addNewDays(cache, incoming, newestDate, options = {}) {
|
|
3628
3886
|
const byDate = new Map(cache.days.map((d) => [d.date, d]));
|
|
3629
3887
|
for (const day of incoming) {
|
|
3630
3888
|
byDate.set(day.date, day);
|
|
3631
3889
|
}
|
|
3632
3890
|
const merged = Array.from(byDate.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
3633
|
-
const
|
|
3634
|
-
|
|
3891
|
+
const candidateLast = options.coveredThrough ?? newestDate;
|
|
3892
|
+
const nextLast = cache.lastComputedDate && cache.lastComputedDate > candidateLast ? cache.lastComputedDate : candidateLast;
|
|
3893
|
+
return {
|
|
3894
|
+
version: DAILY_CACHE_VERSION,
|
|
3895
|
+
scopeKey: cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE,
|
|
3896
|
+
lastComputedDate: nextLast,
|
|
3897
|
+
days: merged
|
|
3898
|
+
};
|
|
3635
3899
|
}
|
|
3636
3900
|
function getDaysInRange(cache, start, end) {
|
|
3637
3901
|
return cache.days.filter((d) => d.date >= start && d.date <= end);
|
|
@@ -3858,6 +4122,9 @@ async function scanSessions(dateRange) {
|
|
|
3858
4122
|
});
|
|
3859
4123
|
return { toolCalls: allCalls, projectCwds: allCwds, apiCalls: allApiCalls, userMessages: allUserMessages };
|
|
3860
4124
|
}
|
|
4125
|
+
function prewarmOptimizeScan(dateRange) {
|
|
4126
|
+
return scanSessions(dateRange);
|
|
4127
|
+
}
|
|
3861
4128
|
function readJsonFile(path) {
|
|
3862
4129
|
const raw = readSessionFileSync(path);
|
|
3863
4130
|
if (raw === null) return null;
|
|
@@ -4368,7 +4635,7 @@ function cacheKey2(projects, dateRange) {
|
|
|
4368
4635
|
const fingerprint = projects.length + ":" + projects.reduce((s, p) => s + p.totalApiCalls, 0);
|
|
4369
4636
|
return `${dr}:${fingerprint}`;
|
|
4370
4637
|
}
|
|
4371
|
-
async function scanAndDetect(projects, dateRange) {
|
|
4638
|
+
async function scanAndDetect(projects, dateRange, preScanned) {
|
|
4372
4639
|
if (projects.length === 0) {
|
|
4373
4640
|
return { findings: [], costRate: 0, healthScore: 100, healthGrade: "A" };
|
|
4374
4641
|
}
|
|
@@ -4376,7 +4643,7 @@ async function scanAndDetect(projects, dateRange) {
|
|
|
4376
4643
|
const cached = resultCache.get(key);
|
|
4377
4644
|
if (cached && Date.now() - cached.ts < RESULT_CACHE_TTL_MS) return cached.data;
|
|
4378
4645
|
const costRate = computeInputCostRate(projects);
|
|
4379
|
-
const { toolCalls, projectCwds, apiCalls, userMessages } = await scanSessions(dateRange);
|
|
4646
|
+
const { toolCalls, projectCwds, apiCalls, userMessages } = preScanned ? await preScanned : await scanSessions(dateRange);
|
|
4380
4647
|
const findings = [];
|
|
4381
4648
|
const syncDetectors = [
|
|
4382
4649
|
() => detectCacheBloat(apiCalls, projects, dateRange),
|
|
@@ -4487,13 +4754,13 @@ function renderOptimize(findings, costRate, periodLabel, periodCost, sessionCoun
|
|
|
4487
4754
|
lines.push("");
|
|
4488
4755
|
return lines.join("\n");
|
|
4489
4756
|
}
|
|
4490
|
-
async function runOptimize(projects, periodLabel, dateRange) {
|
|
4757
|
+
async function runOptimize(projects, periodLabel, dateRange, preScanned) {
|
|
4491
4758
|
if (projects.length === 0) {
|
|
4492
4759
|
console.log(chalk2.dim("\n No usage data found for this period.\n"));
|
|
4493
4760
|
return;
|
|
4494
4761
|
}
|
|
4495
4762
|
process.stderr.write(chalk2.dim(" Analyzing your sessions...\n"));
|
|
4496
|
-
const { findings, costRate, healthScore, healthGrade } = await scanAndDetect(projects, dateRange);
|
|
4763
|
+
const { findings, costRate, healthScore, healthGrade } = await scanAndDetect(projects, dateRange, preScanned);
|
|
4497
4764
|
const sessions = projects.flatMap((p) => p.sessions);
|
|
4498
4765
|
const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0);
|
|
4499
4766
|
const callCount = projects.reduce((s, p) => s + p.totalApiCalls, 0);
|
|
@@ -6504,7 +6771,7 @@ function getDateRange2(period) {
|
|
|
6504
6771
|
return { range: { start, end: yesterdayEnd }, label: `Yesterday (${toDateString(start)})` };
|
|
6505
6772
|
}
|
|
6506
6773
|
case "week": {
|
|
6507
|
-
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() -
|
|
6774
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
|
|
6508
6775
|
return { range: { start, end }, label: "Last 7 Days" };
|
|
6509
6776
|
}
|
|
6510
6777
|
case "month": {
|
|
@@ -6512,7 +6779,7 @@ function getDateRange2(period) {
|
|
|
6512
6779
|
return { range: { start, end }, label: `${now.toLocaleString("default", { month: "long" })} ${now.getFullYear()}` };
|
|
6513
6780
|
}
|
|
6514
6781
|
case "30days": {
|
|
6515
|
-
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() -
|
|
6782
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 29);
|
|
6516
6783
|
return { range: { start, end }, label: "Last 30 Days" };
|
|
6517
6784
|
}
|
|
6518
6785
|
case "all": {
|
|
@@ -6520,7 +6787,7 @@ function getDateRange2(period) {
|
|
|
6520
6787
|
return { range: { start, end }, label: "Last 6 months" };
|
|
6521
6788
|
}
|
|
6522
6789
|
default: {
|
|
6523
|
-
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() -
|
|
6790
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
|
|
6524
6791
|
return { range: { start, end }, label: "Last 7 Days" };
|
|
6525
6792
|
}
|
|
6526
6793
|
}
|
|
@@ -6770,8 +7037,9 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
6770
7037
|
const yesterdayEnd = new Date(todayStart.getTime() - 1);
|
|
6771
7038
|
const yesterdayStr = toDateString(new Date(todayStart.getTime() - MS_PER_DAY3));
|
|
6772
7039
|
const isAllProviders = pf === "all";
|
|
7040
|
+
const dailyCacheScope = buildDailyCacheScopeKey(opts.project, opts.exclude);
|
|
6773
7041
|
const cache = await withDailyCacheLock(async () => {
|
|
6774
|
-
let c = await loadDailyCache();
|
|
7042
|
+
let c = await loadDailyCache(dailyCacheScope);
|
|
6775
7043
|
const hadYesterday = c.days.some((d) => d.date >= yesterdayStr);
|
|
6776
7044
|
if (hadYesterday) {
|
|
6777
7045
|
const freshDays = c.days.filter((d) => d.date < yesterdayStr);
|
|
@@ -6786,32 +7054,53 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
6786
7054
|
if (gapStart.getTime() <= yesterdayEnd.getTime()) {
|
|
6787
7055
|
const gapRange = { start: gapStart, end: yesterdayEnd };
|
|
6788
7056
|
const gapProjects = filterProjectsByName(await parseAllSessions(gapRange, "all"), opts.project, opts.exclude);
|
|
6789
|
-
const gapDays = aggregateProjectsIntoDays(gapProjects);
|
|
6790
|
-
c = addNewDays(c, gapDays, yesterdayStr);
|
|
7057
|
+
const gapDays = fillMissingDays(gapRange.start, gapRange.end, aggregateProjectsIntoDays(gapProjects));
|
|
7058
|
+
c = addNewDays(c, gapDays, yesterdayStr, { coveredThrough: yesterdayStr });
|
|
6791
7059
|
await saveDailyCache(c);
|
|
6792
7060
|
}
|
|
6793
7061
|
return c;
|
|
6794
7062
|
});
|
|
6795
7063
|
let currentData;
|
|
6796
|
-
|
|
6797
|
-
let scanRange;
|
|
7064
|
+
const scanRange = periodInfo.range;
|
|
6798
7065
|
const todayRange = { start: todayStart, end: periodInfo.range.end };
|
|
6799
|
-
const
|
|
7066
|
+
const warnings = [];
|
|
7067
|
+
const parseStart = Date.now();
|
|
7068
|
+
let todayProjects;
|
|
7069
|
+
let allTodayProjects;
|
|
7070
|
+
try {
|
|
7071
|
+
allTodayProjects = await parseAllSessions(todayRange, "all");
|
|
7072
|
+
todayProjects = fp(allTodayProjects);
|
|
7073
|
+
} catch (err) {
|
|
7074
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7075
|
+
const warn2 = `parseAllSessions failed: ${msg}`;
|
|
7076
|
+
warnings.push(warn2);
|
|
7077
|
+
process.stderr.write(`[exe-watcher] WARNING: ${warn2}
|
|
7078
|
+
`);
|
|
7079
|
+
allTodayProjects = [];
|
|
7080
|
+
todayProjects = [];
|
|
7081
|
+
}
|
|
7082
|
+
if (todayProjects.length === 0) {
|
|
7083
|
+
const warn2 = `parseAllSessions returned 0 projects for range ${toDateString(todayRange.start)}..${toDateString(todayRange.end)}`;
|
|
7084
|
+
warnings.push(warn2);
|
|
7085
|
+
process.stderr.write(`[exe-watcher] WARNING: ${warn2}
|
|
7086
|
+
`);
|
|
7087
|
+
}
|
|
7088
|
+
const parseTimeMs = Date.now() - parseStart;
|
|
6800
7089
|
const todayDays = aggregateProjectsIntoDays(todayProjects);
|
|
7090
|
+
let daysCount = 0;
|
|
6801
7091
|
{
|
|
6802
7092
|
const rangeStartStr = toDateString(periodInfo.range.start);
|
|
6803
7093
|
const rangeEndStr = toDateString(periodInfo.range.end);
|
|
6804
7094
|
const historicalDays = getDaysInRange(cache, rangeStartStr, yesterdayStr);
|
|
6805
7095
|
const todayOnly = todayDays.filter((d) => d.date > yesterdayStr && d.date <= rangeEndStr);
|
|
6806
7096
|
const allDays = [...historicalDays, ...todayOnly].sort((a, b) => a.date.localeCompare(b.date));
|
|
7097
|
+
daysCount = allDays.length;
|
|
6807
7098
|
if (isAllProviders) {
|
|
6808
7099
|
currentData = buildPeriodDataFromDays(allDays, periodInfo.label);
|
|
6809
7100
|
} else {
|
|
6810
7101
|
const providerDays = filterDaysToProvider(allDays, pf);
|
|
6811
7102
|
currentData = buildPeriodDataFromDays(providerDays, periodInfo.label);
|
|
6812
7103
|
}
|
|
6813
|
-
scanProjects = todayProjects;
|
|
6814
|
-
scanRange = periodInfo.range;
|
|
6815
7104
|
}
|
|
6816
7105
|
const allProviders = await getAllProviders();
|
|
6817
7106
|
const displayNameByName = new Map(allProviders.map((p) => [p.name, p.displayName]));
|
|
@@ -6822,6 +7111,12 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
6822
7111
|
...getDaysInRange(cache, rangeStartStr, yesterdayStr),
|
|
6823
7112
|
...todayDays.filter((d) => d.date > yesterdayStr)
|
|
6824
7113
|
];
|
|
7114
|
+
if (allDaysForProviders.length === 0) {
|
|
7115
|
+
const warn2 = "allDaysForProviders is empty \u2014 all providers will show $0";
|
|
7116
|
+
warnings.push(warn2);
|
|
7117
|
+
process.stderr.write(`[exe-watcher] WARNING: ${warn2}
|
|
7118
|
+
`);
|
|
7119
|
+
}
|
|
6825
7120
|
const providerTotals = {};
|
|
6826
7121
|
for (const d of allDaysForProviders) {
|
|
6827
7122
|
for (const [name, p] of Object.entries(d.providers)) {
|
|
@@ -6845,7 +7140,7 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
6845
7140
|
const fullHistory = [...allCacheDays, ...todayDays.filter((d) => d.date > yesterdayStr)];
|
|
6846
7141
|
const dailyHistory = fullHistory.map((d) => {
|
|
6847
7142
|
if (isAllProviders) {
|
|
6848
|
-
const
|
|
7143
|
+
const topModels2 = Object.entries(d.models).filter(([name]) => name !== "<synthetic>").sort(([, a], [, b]) => b.cost - a.cost).slice(0, 5).map(([name, m]) => ({
|
|
6849
7144
|
name,
|
|
6850
7145
|
cost: m.cost,
|
|
6851
7146
|
calls: m.calls,
|
|
@@ -6860,22 +7155,42 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
6860
7155
|
outputTokens: d.outputTokens,
|
|
6861
7156
|
cacheReadTokens: d.cacheReadTokens,
|
|
6862
7157
|
cacheWriteTokens: d.cacheWriteTokens,
|
|
6863
|
-
topModels
|
|
7158
|
+
topModels: topModels2
|
|
6864
7159
|
};
|
|
6865
7160
|
}
|
|
6866
|
-
const prov = d.providers[pf]
|
|
7161
|
+
const prov = d.providers[pf];
|
|
7162
|
+
const topModels = Object.entries(prov?.models ?? {}).filter(([name]) => name !== "<synthetic>").sort(([, a], [, b]) => b.cost - a.cost).slice(0, 5).map(([name, m]) => ({
|
|
7163
|
+
name,
|
|
7164
|
+
cost: m.cost,
|
|
7165
|
+
calls: m.calls,
|
|
7166
|
+
inputTokens: m.inputTokens,
|
|
7167
|
+
outputTokens: m.outputTokens
|
|
7168
|
+
}));
|
|
6867
7169
|
return {
|
|
6868
7170
|
date: d.date,
|
|
6869
|
-
cost: prov
|
|
6870
|
-
calls: prov
|
|
6871
|
-
inputTokens: 0,
|
|
6872
|
-
outputTokens: 0,
|
|
6873
|
-
cacheReadTokens: 0,
|
|
6874
|
-
cacheWriteTokens: 0,
|
|
6875
|
-
topModels
|
|
7171
|
+
cost: prov?.cost ?? 0,
|
|
7172
|
+
calls: prov?.calls ?? 0,
|
|
7173
|
+
inputTokens: prov?.inputTokens ?? 0,
|
|
7174
|
+
outputTokens: prov?.outputTokens ?? 0,
|
|
7175
|
+
cacheReadTokens: prov?.cacheReadTokens ?? 0,
|
|
7176
|
+
cacheWriteTokens: prov?.cacheWriteTokens ?? 0,
|
|
7177
|
+
topModels
|
|
6876
7178
|
};
|
|
6877
7179
|
});
|
|
6878
|
-
|
|
7180
|
+
let optimize = null;
|
|
7181
|
+
if (opts.optimize !== false) {
|
|
7182
|
+
try {
|
|
7183
|
+
const optimizeProjects = isAllProviders && opts.period === "today" ? todayProjects : fp(await parseAllSessions(scanRange, isAllProviders ? "all" : pf));
|
|
7184
|
+
optimize = await scanAndDetect(optimizeProjects, scanRange);
|
|
7185
|
+
} catch (err) {
|
|
7186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7187
|
+
const warn2 = `optimize parse failed: ${msg}`;
|
|
7188
|
+
warnings.push(warn2);
|
|
7189
|
+
process.stderr.write(`[exe-watcher] WARNING: ${warn2}
|
|
7190
|
+
`);
|
|
7191
|
+
optimize = await scanAndDetect(todayProjects, scanRange);
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
6879
7194
|
const exeOsDir = join21(homedir14(), ".exe-os");
|
|
6880
7195
|
const exeOsDetected = existsSync6(exeOsDir);
|
|
6881
7196
|
let agentStats = null;
|
|
@@ -6899,13 +7214,13 @@ program.command("status").description("Compact status output (today + week + mon
|
|
|
6899
7214
|
if (agentStats) {
|
|
6900
7215
|
agentStats = estimateAgentCosts(agentStats);
|
|
6901
7216
|
}
|
|
6902
|
-
const [
|
|
6903
|
-
parseAllSessions(getDateRange2("today").range, "all"),
|
|
7217
|
+
const [proj7d, proj30d] = await Promise.all([
|
|
6904
7218
|
parseAllSessions(getDateRange2("week").range, "all"),
|
|
6905
7219
|
parseAllSessions(getDateRange2("30days").range, "all")
|
|
6906
7220
|
]);
|
|
6907
|
-
const projectSpend = buildProjectSpend(
|
|
6908
|
-
|
|
7221
|
+
const projectSpend = buildProjectSpend(allTodayProjects, proj7d, proj30d);
|
|
7222
|
+
const diagnostics = { daysCount, parseTimeMs, warnings };
|
|
7223
|
+
console.log(JSON.stringify(buildMenubarPayload(currentData, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge, diagnostics)));
|
|
6909
7224
|
return;
|
|
6910
7225
|
}
|
|
6911
7226
|
if (opts.format === "json") {
|
|
@@ -7203,8 +7518,9 @@ program.command("plan [action] [id]").description("Show or configure a subscript
|
|
|
7203
7518
|
program.command("optimize").description("Find token waste and get exact fixes").option("-p, --period <period>", "Analysis period: today, week, 30days, month, all", "30days").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").action(async (opts) => {
|
|
7204
7519
|
await loadPricing();
|
|
7205
7520
|
const { range, label } = getDateRange2(opts.period);
|
|
7521
|
+
const scanPromise = prewarmOptimizeScan(range);
|
|
7206
7522
|
const projects = await parseAllSessions(range, opts.provider);
|
|
7207
|
-
await runOptimize(projects, label, range);
|
|
7523
|
+
await runOptimize(projects, label, range, scanPromise);
|
|
7208
7524
|
});
|
|
7209
7525
|
program.command("compare").description("Compare two AI models side-by-side").option("-p, --period <period>", "Analysis period: today, week, 30days, month, all", "all").option("--provider <provider>", "Filter by provider: all, claude, codex, cursor", "all").action(async (opts) => {
|
|
7210
7526
|
await loadPricing();
|