exe-watcher 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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 !== name && !pricing.has(stripped)) pricing.set(stripped, costs);
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
- if (Object.hasOwn(FALLBACK_PRICING, canonical)) return FALLBACK_PRICING[canonical];
118
- if (pricingCache?.has(canonical)) return pricingCache.get(canonical);
119
- for (const [key, costs] of pricingCache ?? /* @__PURE__ */ new Map()) {
120
- if (canonical.startsWith(key)) return costs;
121
- }
122
- for (const [key, costs] of Object.entries(FALLBACK_PRICING)) {
123
- if (canonical.startsWith(key)) return costs;
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 costs = getModelCosts(model);
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
- for (const [key, name] of Object.entries(shortNames2)) {
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
- var LITELLM_URL, CACHE_TTL_MS2, WEB_SEARCH_COST, FALLBACK_PRICING, pricingCache, BUILTIN_ALIASES, userAliases;
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": { inputCostPerToken: 125e-8, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 125e-8, cacheReadCostPerToken: 315e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
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.4": { inputCostPerToken: 25e-7, outputCostPerToken: 15e-6, cacheWriteCostPerToken: 25e-7, cacheReadCostPerToken: 25e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
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: 25e-7, outputCostPerToken: 1e-5, cacheWriteCostPerToken: 25e-7, cacheReadCostPerToken: 125e-8, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
201
- "gpt-5-mini": { inputCostPerToken: 4e-7, outputCostPerToken: 16e-7, cacheWriteCostPerToken: 4e-7, cacheReadCostPerToken: 2e-7, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
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: 625e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
210
- "gpt-5.1-codex-mini": { inputCostPerToken: 25e-8, outputCostPerToken: 2e-6, cacheWriteCostPerToken: 25e-8, cacheReadCostPerToken: 125e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
211
- "gpt-5.2-codex": { inputCostPerToken: 175e-8, outputCostPerToken: 14e-6, cacheWriteCostPerToken: 175e-8, cacheReadCostPerToken: 875e-9, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 },
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 createHash("sha1").update(transcriptPath).digest("hex").slice(0, 16);
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
- const d = new Date(iso);
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,11 +1748,48 @@ 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
- const provider = callDay.providers[call.provider] ?? { calls: 0, cost: 0 };
1626
- provider.calls += 1;
1627
- provider.cost += call.costUSD;
1628
- callDay.providers[call.provider] = provider;
1629
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
+ }
1789
+ }
1790
+ }
1791
+ for (const provider of providersSeenInSession) {
1792
+ ensureProvider(sessionDate, provider).sessions += 1;
1630
1793
  }
1631
1794
  }
1632
1795
  }
@@ -1634,27 +1797,41 @@ function aggregateProjectsIntoDays(projects) {
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
- // sessions count is not per-provider in cache — approximate at 0
1644
- sessions: 0,
1645
- inputTokens: 0,
1646
- outputTokens: 0,
1647
- cacheReadTokens: 0,
1648
- cacheWriteTokens: 0,
1649
- editTurns: 0,
1650
- oneShotTurns: 0,
1651
- models: {},
1652
- categories: {},
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
- }).filter((d) => d !== null);
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",
@@ -2301,6 +2479,10 @@ function createParser(source, seenKeys) {
2301
2479
  sessionModel = entry.payload?.model;
2302
2480
  continue;
2303
2481
  }
2482
+ if (entry.type === "turn_context" && entry.payload?.model) {
2483
+ sessionModel = entry.payload.model;
2484
+ continue;
2485
+ }
2304
2486
  if (entry.type === "response_item" && entry.payload?.type === "function_call") {
2305
2487
  const rawName = entry.payload.name ?? "";
2306
2488
  pendingTools.push(toolNameMap[rawName] ?? rawName);
@@ -2392,7 +2574,7 @@ function createCodexProvider(codexDir) {
2392
2574
  name: "codex",
2393
2575
  displayName: "Codex",
2394
2576
  modelDisplayName(model) {
2395
- for (const [key, name] of Object.entries(modelDisplayNames)) {
2577
+ for (const [key, name] of modelDisplayEntries) {
2396
2578
  if (model.startsWith(key)) return name;
2397
2579
  }
2398
2580
  return model;
@@ -2444,7 +2626,7 @@ var toolNameMap2 = {
2444
2626
  fetch_webpage: "WebFetch",
2445
2627
  github_repo: "GitHub"
2446
2628
  };
2447
- var modelDisplayEntries = Object.entries(modelDisplayNames2).sort((a, b) => b[0].length - a[0].length);
2629
+ var modelDisplayEntries2 = Object.entries(modelDisplayNames2).sort((a, b) => b[0].length - a[0].length);
2448
2630
  function getCopilotSessionStateDir(override) {
2449
2631
  return override ?? join9(homedir6(), ".copilot", "session-state");
2450
2632
  }
@@ -2540,7 +2722,7 @@ function createCopilotProvider(sessionStateDir) {
2540
2722
  name: "copilot",
2541
2723
  displayName: "Copilot",
2542
2724
  modelDisplayName(model) {
2543
- for (const [key, name] of modelDisplayEntries) {
2725
+ for (const [key, name] of modelDisplayEntries2) {
2544
2726
  if (model === key || model.startsWith(key + "-")) return name;
2545
2727
  }
2546
2728
  return model;
@@ -2585,7 +2767,7 @@ var toolNameMap3 = {
2585
2767
  todo: "TodoWrite",
2586
2768
  patch: "Patch"
2587
2769
  };
2588
- var modelDisplayEntries2 = Object.entries(modelDisplayNames3).sort((a, b) => b[0].length - a[0].length);
2770
+ var modelDisplayEntries3 = Object.entries(modelDisplayNames3).sort((a, b) => b[0].length - a[0].length);
2589
2771
  function getPiSessionsDir(override) {
2590
2772
  return override ?? join10(homedir7(), ".pi", "agent", "sessions");
2591
2773
  }
@@ -2707,7 +2889,7 @@ function createPiProvider(sessionsDir) {
2707
2889
  name: "pi",
2708
2890
  displayName: "Pi",
2709
2891
  modelDisplayName(model) {
2710
- for (const [key, name] of modelDisplayEntries2) {
2892
+ for (const [key, name] of modelDisplayEntries3) {
2711
2893
  if (model.startsWith(key)) return name;
2712
2894
  }
2713
2895
  return model;
@@ -2730,7 +2912,7 @@ function createOmpProvider(sessionsDir) {
2730
2912
  name: "omp",
2731
2913
  displayName: "OMP",
2732
2914
  modelDisplayName(model) {
2733
- for (const [key, name] of modelDisplayEntries2) {
2915
+ for (const [key, name] of modelDisplayEntries3) {
2734
2916
  if (model.startsWith(key)) return name;
2735
2917
  }
2736
2918
  return model;
@@ -3315,9 +3497,68 @@ async function parseProviderSources(providerName, sources, seenKeys, dateRange)
3315
3497
  var CACHE_TTL_MS3 = 6e4;
3316
3498
  var MAX_CACHE_ENTRIES = 10;
3317
3499
  var sessionCache = /* @__PURE__ */ new Map();
3318
- function cacheKey(dateRange, providerFilter) {
3500
+ var inFlightSourceContexts = /* @__PURE__ */ new Map();
3501
+ function sourceContextCacheKey(providerFilter) {
3502
+ return JSON.stringify({
3503
+ provider: providerFilter ?? "all",
3504
+ claudeConfigDir: process.env["CLAUDE_CONFIG_DIR"] ?? "",
3505
+ codexHome: process.env["CODEX_HOME"] ?? "",
3506
+ xdgDataHome: process.env["XDG_DATA_HOME"] ?? "",
3507
+ home: process.env["HOME"] ?? ""
3508
+ });
3509
+ }
3510
+ function cacheKey(dateRange, providerFilter, sourceFingerprint = "static") {
3319
3511
  const s = dateRange ? `${dateRange.start.getTime()}:${dateRange.end.getTime()}` : "none";
3320
- return `${s}:${providerFilter ?? "all"}`;
3512
+ return `${s}:${providerFilter ?? "all"}:${sourceFingerprint}`;
3513
+ }
3514
+ async function buildSourceFingerprint(path) {
3515
+ try {
3516
+ const fp = await stat10(path);
3517
+ if (!fp.isDirectory()) return `f:${Math.round(fp.mtimeMs)}:${fp.size}`;
3518
+ const entries = await readdir8(path);
3519
+ const children = await Promise.all(entries.map(async (entry) => stat10(join15(path, entry)).catch(() => null)));
3520
+ let childCount = 0;
3521
+ let childSizeBytes = 0;
3522
+ let newestChildMtimeMs = 0;
3523
+ for (const child of children) {
3524
+ if (!child) continue;
3525
+ childCount += 1;
3526
+ childSizeBytes += child.size;
3527
+ newestChildMtimeMs = Math.max(newestChildMtimeMs, child.mtimeMs);
3528
+ }
3529
+ return `d:${Math.round(fp.mtimeMs)}:${fp.size}:${childCount}:${Math.round(newestChildMtimeMs)}:${childSizeBytes}`;
3530
+ } catch {
3531
+ return "missing";
3532
+ }
3533
+ }
3534
+ async function buildCacheFingerprint(sources) {
3535
+ const parts = await Promise.all(sources.map(async (source) => {
3536
+ const fingerprint = await buildSourceFingerprint(source.path);
3537
+ return `${source.provider}:${source.project}:${source.path}:${fingerprint}`;
3538
+ }));
3539
+ return parts.sort().join("|");
3540
+ }
3541
+ async function getSourceContext(providerFilter) {
3542
+ const key = sourceContextCacheKey(providerFilter);
3543
+ const cached = inFlightSourceContexts.get(key);
3544
+ if (cached) {
3545
+ return cached;
3546
+ }
3547
+ const promise = (async () => {
3548
+ const sources = await discoverAllSessions(providerFilter);
3549
+ const fingerprint = await buildCacheFingerprint(sources);
3550
+ return { sources, fingerprint };
3551
+ })();
3552
+ inFlightSourceContexts.set(key, promise);
3553
+ try {
3554
+ return await promise;
3555
+ } catch (err) {
3556
+ throw err;
3557
+ } finally {
3558
+ if (inFlightSourceContexts.get(key) === promise) {
3559
+ inFlightSourceContexts.delete(key);
3560
+ }
3561
+ }
3321
3562
  }
3322
3563
  function cachePut(key, data) {
3323
3564
  const now = Date.now();
@@ -3351,12 +3592,13 @@ function filterProjectsByName(projects, include, exclude) {
3351
3592
  return result;
3352
3593
  }
3353
3594
  async function parseAllSessions(dateRange, providerFilter) {
3354
- const key = cacheKey(dateRange, providerFilter);
3595
+ const sourceContext = await getSourceContext(providerFilter);
3596
+ const allSources = sourceContext.sources;
3597
+ const key = cacheKey(dateRange, providerFilter, sourceContext.fingerprint);
3355
3598
  const cached = sessionCache.get(key);
3356
3599
  if (cached && Date.now() - cached.ts < CACHE_TTL_MS3) return cached.data;
3357
3600
  const seenMsgIds = /* @__PURE__ */ new Set();
3358
3601
  const seenKeys = /* @__PURE__ */ new Set();
3359
- const allSources = await discoverAllSessions(providerFilter);
3360
3602
  const claudeSources = allSources.filter((s) => s.provider === "claude");
3361
3603
  const nonClaudeSources = allSources.filter((s) => s.provider !== "claude");
3362
3604
  const claudeDirs = claudeSources.map((s) => ({ path: s.path, name: s.project }));
@@ -3543,7 +3785,7 @@ function buildHistory(daily) {
3543
3785
  const trimmed = sorted.slice(-HISTORY_DAYS_LIMIT);
3544
3786
  return { daily: trimmed };
3545
3787
  }
3546
- function buildMenubarPayload(current, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge) {
3788
+ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge, diagnostics) {
3547
3789
  return {
3548
3790
  generated: (/* @__PURE__ */ new Date()).toISOString(),
3549
3791
  current: {
@@ -3561,6 +3803,7 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
3561
3803
  },
3562
3804
  optimize: buildOptimize(optimize),
3563
3805
  history: buildHistory(dailyHistory),
3806
+ diagnostics: diagnostics ?? { daysCount: 0, parseTimeMs: 0, warnings: [] },
3564
3807
  agentStats: agentStats ?? null,
3565
3808
  exeOsDetected: exeOsDetected ?? false,
3566
3809
  statsFileAge: statsFileAge ?? null,
@@ -3570,43 +3813,62 @@ function buildMenubarPayload(current, providers, optimize, dailyHistory, agentSt
3570
3813
 
3571
3814
  // src/daily-cache.ts
3572
3815
  init_cache_dir();
3573
- import { randomBytes as randomBytes5 } from "crypto";
3816
+ import { createHash as createHash3, randomBytes as randomBytes5 } from "crypto";
3574
3817
  import { existsSync as existsSync3 } from "fs";
3575
3818
  import { mkdir as mkdir7, open as open5, readFile as readFile7, rename as rename6, unlink as unlink3 } from "fs/promises";
3576
3819
  import { join as join16 } from "path";
3577
- var DAILY_CACHE_VERSION = 3;
3820
+ var DAILY_CACHE_VERSION = 4;
3821
+ var DEFAULT_DAILY_CACHE_SCOPE = "global";
3578
3822
  var DAILY_CACHE_FILENAME = "daily-cache.json";
3579
- function getCachePath3() {
3580
- return join16(getCacheDir(), DAILY_CACHE_FILENAME);
3823
+ function getCachePath3(scopeKey) {
3824
+ if (scopeKey === DEFAULT_DAILY_CACHE_SCOPE) {
3825
+ return join16(getCacheDir(), DAILY_CACHE_FILENAME);
3826
+ }
3827
+ return join16(getCacheDir(), `daily-cache-${scopeKey}.json`);
3828
+ }
3829
+ function emptyCache(scopeKey) {
3830
+ return { version: DAILY_CACHE_VERSION, scopeKey, lastComputedDate: null, days: [] };
3831
+ }
3832
+ function normalizeScopeTerms(values) {
3833
+ return [...new Set((values ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean))].sort();
3581
3834
  }
3582
- function emptyCache() {
3583
- return { version: DAILY_CACHE_VERSION, lastComputedDate: null, days: [] };
3835
+ function buildDailyCacheScopeKey(include, exclude) {
3836
+ const normalizedInclude = normalizeScopeTerms(include);
3837
+ const normalizedExclude = normalizeScopeTerms(exclude);
3838
+ if (normalizedInclude.length === 0 && normalizedExclude.length === 0) {
3839
+ return DEFAULT_DAILY_CACHE_SCOPE;
3840
+ }
3841
+ const raw = JSON.stringify({ include: normalizedInclude, exclude: normalizedExclude });
3842
+ return `scope-${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
3584
3843
  }
3585
3844
  function isValidCache(parsed) {
3586
3845
  if (!parsed || typeof parsed !== "object") return false;
3587
3846
  const c = parsed;
3588
3847
  if (c.version !== DAILY_CACHE_VERSION) return false;
3848
+ if (typeof c.scopeKey !== "string" || c.scopeKey.length === 0) return false;
3589
3849
  if (!Array.isArray(c.days)) return false;
3590
3850
  return true;
3591
3851
  }
3592
- async function loadDailyCache() {
3593
- const path = getCachePath3();
3594
- if (!existsSync3(path)) return emptyCache();
3852
+ async function loadDailyCache(scopeKey = DEFAULT_DAILY_CACHE_SCOPE) {
3853
+ const path = getCachePath3(scopeKey);
3854
+ if (!existsSync3(path)) return emptyCache(scopeKey);
3595
3855
  try {
3596
3856
  const raw = await readFile7(path, "utf-8");
3597
3857
  const parsed = JSON.parse(raw);
3598
- if (!isValidCache(parsed)) return emptyCache();
3858
+ if (!isValidCache(parsed)) return emptyCache(scopeKey);
3859
+ if (parsed.scopeKey !== scopeKey) return emptyCache(scopeKey);
3599
3860
  return parsed;
3600
3861
  } catch {
3601
- return emptyCache();
3862
+ return emptyCache(scopeKey);
3602
3863
  }
3603
3864
  }
3604
3865
  async function saveDailyCache(cache) {
3605
3866
  const dir = getCacheDir();
3606
3867
  if (!existsSync3(dir)) await mkdir7(dir, { recursive: true });
3607
- const finalPath = getCachePath3();
3868
+ const scopeKey = cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE;
3869
+ const finalPath = getCachePath3(scopeKey);
3608
3870
  const tempPath = `${finalPath}.${randomBytes5(8).toString("hex")}.tmp`;
3609
- const payload = JSON.stringify(cache);
3871
+ const payload = JSON.stringify({ ...cache, scopeKey });
3610
3872
  const handle = await open5(tempPath, "w", 384);
3611
3873
  try {
3612
3874
  await handle.writeFile(payload, { encoding: "utf-8" });
@@ -3624,14 +3886,20 @@ async function saveDailyCache(cache) {
3624
3886
  throw err;
3625
3887
  }
3626
3888
  }
3627
- function addNewDays(cache, incoming, newestDate) {
3889
+ function addNewDays(cache, incoming, newestDate, options = {}) {
3628
3890
  const byDate = new Map(cache.days.map((d) => [d.date, d]));
3629
3891
  for (const day of incoming) {
3630
3892
  byDate.set(day.date, day);
3631
3893
  }
3632
3894
  const merged = Array.from(byDate.values()).sort((a, b) => a.date.localeCompare(b.date));
3633
- const nextLast = cache.lastComputedDate && cache.lastComputedDate > newestDate ? cache.lastComputedDate : newestDate;
3634
- return { version: DAILY_CACHE_VERSION, lastComputedDate: nextLast, days: merged };
3895
+ const candidateLast = options.coveredThrough ?? newestDate;
3896
+ const nextLast = cache.lastComputedDate && cache.lastComputedDate > candidateLast ? cache.lastComputedDate : candidateLast;
3897
+ return {
3898
+ version: DAILY_CACHE_VERSION,
3899
+ scopeKey: cache.scopeKey || DEFAULT_DAILY_CACHE_SCOPE,
3900
+ lastComputedDate: nextLast,
3901
+ days: merged
3902
+ };
3635
3903
  }
3636
3904
  function getDaysInRange(cache, start, end) {
3637
3905
  return cache.days.filter((d) => d.date >= start && d.date <= end);
@@ -3858,6 +4126,9 @@ async function scanSessions(dateRange) {
3858
4126
  });
3859
4127
  return { toolCalls: allCalls, projectCwds: allCwds, apiCalls: allApiCalls, userMessages: allUserMessages };
3860
4128
  }
4129
+ function prewarmOptimizeScan(dateRange) {
4130
+ return scanSessions(dateRange);
4131
+ }
3861
4132
  function readJsonFile(path) {
3862
4133
  const raw = readSessionFileSync(path);
3863
4134
  if (raw === null) return null;
@@ -4368,7 +4639,7 @@ function cacheKey2(projects, dateRange) {
4368
4639
  const fingerprint = projects.length + ":" + projects.reduce((s, p) => s + p.totalApiCalls, 0);
4369
4640
  return `${dr}:${fingerprint}`;
4370
4641
  }
4371
- async function scanAndDetect(projects, dateRange) {
4642
+ async function scanAndDetect(projects, dateRange, preScanned) {
4372
4643
  if (projects.length === 0) {
4373
4644
  return { findings: [], costRate: 0, healthScore: 100, healthGrade: "A" };
4374
4645
  }
@@ -4376,7 +4647,7 @@ async function scanAndDetect(projects, dateRange) {
4376
4647
  const cached = resultCache.get(key);
4377
4648
  if (cached && Date.now() - cached.ts < RESULT_CACHE_TTL_MS) return cached.data;
4378
4649
  const costRate = computeInputCostRate(projects);
4379
- const { toolCalls, projectCwds, apiCalls, userMessages } = await scanSessions(dateRange);
4650
+ const { toolCalls, projectCwds, apiCalls, userMessages } = preScanned ? await preScanned : await scanSessions(dateRange);
4380
4651
  const findings = [];
4381
4652
  const syncDetectors = [
4382
4653
  () => detectCacheBloat(apiCalls, projects, dateRange),
@@ -4487,13 +4758,13 @@ function renderOptimize(findings, costRate, periodLabel, periodCost, sessionCoun
4487
4758
  lines.push("");
4488
4759
  return lines.join("\n");
4489
4760
  }
4490
- async function runOptimize(projects, periodLabel, dateRange) {
4761
+ async function runOptimize(projects, periodLabel, dateRange, preScanned) {
4491
4762
  if (projects.length === 0) {
4492
4763
  console.log(chalk2.dim("\n No usage data found for this period.\n"));
4493
4764
  return;
4494
4765
  }
4495
4766
  process.stderr.write(chalk2.dim(" Analyzing your sessions...\n"));
4496
- const { findings, costRate, healthScore, healthGrade } = await scanAndDetect(projects, dateRange);
4767
+ const { findings, costRate, healthScore, healthGrade } = await scanAndDetect(projects, dateRange, preScanned);
4497
4768
  const sessions = projects.flatMap((p) => p.sessions);
4498
4769
  const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0);
4499
4770
  const callCount = projects.reduce((s, p) => s + p.totalApiCalls, 0);
@@ -6504,7 +6775,7 @@ function getDateRange2(period) {
6504
6775
  return { range: { start, end: yesterdayEnd }, label: `Yesterday (${toDateString(start)})` };
6505
6776
  }
6506
6777
  case "week": {
6507
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);
6778
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
6508
6779
  return { range: { start, end }, label: "Last 7 Days" };
6509
6780
  }
6510
6781
  case "month": {
@@ -6512,7 +6783,7 @@ function getDateRange2(period) {
6512
6783
  return { range: { start, end }, label: `${now.toLocaleString("default", { month: "long" })} ${now.getFullYear()}` };
6513
6784
  }
6514
6785
  case "30days": {
6515
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30);
6786
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 29);
6516
6787
  return { range: { start, end }, label: "Last 30 Days" };
6517
6788
  }
6518
6789
  case "all": {
@@ -6520,7 +6791,7 @@ function getDateRange2(period) {
6520
6791
  return { range: { start, end }, label: "Last 6 months" };
6521
6792
  }
6522
6793
  default: {
6523
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);
6794
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
6524
6795
  return { range: { start, end }, label: "Last 7 Days" };
6525
6796
  }
6526
6797
  }
@@ -6770,8 +7041,9 @@ program.command("status").description("Compact status output (today + week + mon
6770
7041
  const yesterdayEnd = new Date(todayStart.getTime() - 1);
6771
7042
  const yesterdayStr = toDateString(new Date(todayStart.getTime() - MS_PER_DAY3));
6772
7043
  const isAllProviders = pf === "all";
7044
+ const dailyCacheScope = buildDailyCacheScopeKey(opts.project, opts.exclude);
6773
7045
  const cache = await withDailyCacheLock(async () => {
6774
- let c = await loadDailyCache();
7046
+ let c = await loadDailyCache(dailyCacheScope);
6775
7047
  const hadYesterday = c.days.some((d) => d.date >= yesterdayStr);
6776
7048
  if (hadYesterday) {
6777
7049
  const freshDays = c.days.filter((d) => d.date < yesterdayStr);
@@ -6786,32 +7058,53 @@ program.command("status").description("Compact status output (today + week + mon
6786
7058
  if (gapStart.getTime() <= yesterdayEnd.getTime()) {
6787
7059
  const gapRange = { start: gapStart, end: yesterdayEnd };
6788
7060
  const gapProjects = filterProjectsByName(await parseAllSessions(gapRange, "all"), opts.project, opts.exclude);
6789
- const gapDays = aggregateProjectsIntoDays(gapProjects);
6790
- c = addNewDays(c, gapDays, yesterdayStr);
7061
+ const gapDays = fillMissingDays(gapRange.start, gapRange.end, aggregateProjectsIntoDays(gapProjects));
7062
+ c = addNewDays(c, gapDays, yesterdayStr, { coveredThrough: yesterdayStr });
6791
7063
  await saveDailyCache(c);
6792
7064
  }
6793
7065
  return c;
6794
7066
  });
6795
7067
  let currentData;
6796
- let scanProjects;
6797
- let scanRange;
7068
+ const scanRange = periodInfo.range;
6798
7069
  const todayRange = { start: todayStart, end: periodInfo.range.end };
6799
- const todayProjects = fp(await parseAllSessions(todayRange, "all"));
7070
+ const warnings = [];
7071
+ const parseStart = Date.now();
7072
+ let todayProjects;
7073
+ let allTodayProjects;
7074
+ try {
7075
+ allTodayProjects = await parseAllSessions(todayRange, "all");
7076
+ todayProjects = fp(allTodayProjects);
7077
+ } catch (err) {
7078
+ const msg = err instanceof Error ? err.message : String(err);
7079
+ const warn2 = `parseAllSessions failed: ${msg}`;
7080
+ warnings.push(warn2);
7081
+ process.stderr.write(`[exe-watcher] WARNING: ${warn2}
7082
+ `);
7083
+ allTodayProjects = [];
7084
+ todayProjects = [];
7085
+ }
7086
+ if (todayProjects.length === 0) {
7087
+ const warn2 = `parseAllSessions returned 0 projects for range ${toDateString(todayRange.start)}..${toDateString(todayRange.end)}`;
7088
+ warnings.push(warn2);
7089
+ process.stderr.write(`[exe-watcher] WARNING: ${warn2}
7090
+ `);
7091
+ }
7092
+ const parseTimeMs = Date.now() - parseStart;
6800
7093
  const todayDays = aggregateProjectsIntoDays(todayProjects);
7094
+ let daysCount = 0;
6801
7095
  {
6802
7096
  const rangeStartStr = toDateString(periodInfo.range.start);
6803
7097
  const rangeEndStr = toDateString(periodInfo.range.end);
6804
7098
  const historicalDays = getDaysInRange(cache, rangeStartStr, yesterdayStr);
6805
7099
  const todayOnly = todayDays.filter((d) => d.date > yesterdayStr && d.date <= rangeEndStr);
6806
7100
  const allDays = [...historicalDays, ...todayOnly].sort((a, b) => a.date.localeCompare(b.date));
7101
+ daysCount = allDays.length;
6807
7102
  if (isAllProviders) {
6808
7103
  currentData = buildPeriodDataFromDays(allDays, periodInfo.label);
6809
7104
  } else {
6810
7105
  const providerDays = filterDaysToProvider(allDays, pf);
6811
7106
  currentData = buildPeriodDataFromDays(providerDays, periodInfo.label);
6812
7107
  }
6813
- scanProjects = todayProjects;
6814
- scanRange = periodInfo.range;
6815
7108
  }
6816
7109
  const allProviders = await getAllProviders();
6817
7110
  const displayNameByName = new Map(allProviders.map((p) => [p.name, p.displayName]));
@@ -6822,6 +7115,12 @@ program.command("status").description("Compact status output (today + week + mon
6822
7115
  ...getDaysInRange(cache, rangeStartStr, yesterdayStr),
6823
7116
  ...todayDays.filter((d) => d.date > yesterdayStr)
6824
7117
  ];
7118
+ if (allDaysForProviders.length === 0) {
7119
+ const warn2 = "allDaysForProviders is empty \u2014 all providers will show $0";
7120
+ warnings.push(warn2);
7121
+ process.stderr.write(`[exe-watcher] WARNING: ${warn2}
7122
+ `);
7123
+ }
6825
7124
  const providerTotals = {};
6826
7125
  for (const d of allDaysForProviders) {
6827
7126
  for (const [name, p] of Object.entries(d.providers)) {
@@ -6845,7 +7144,7 @@ program.command("status").description("Compact status output (today + week + mon
6845
7144
  const fullHistory = [...allCacheDays, ...todayDays.filter((d) => d.date > yesterdayStr)];
6846
7145
  const dailyHistory = fullHistory.map((d) => {
6847
7146
  if (isAllProviders) {
6848
- const topModels = Object.entries(d.models).filter(([name]) => name !== "<synthetic>").sort(([, a], [, b]) => b.cost - a.cost).slice(0, 5).map(([name, m]) => ({
7147
+ const topModels2 = Object.entries(d.models).filter(([name]) => name !== "<synthetic>").sort(([, a], [, b]) => b.cost - a.cost).slice(0, 5).map(([name, m]) => ({
6849
7148
  name,
6850
7149
  cost: m.cost,
6851
7150
  calls: m.calls,
@@ -6860,22 +7159,42 @@ program.command("status").description("Compact status output (today + week + mon
6860
7159
  outputTokens: d.outputTokens,
6861
7160
  cacheReadTokens: d.cacheReadTokens,
6862
7161
  cacheWriteTokens: d.cacheWriteTokens,
6863
- topModels
7162
+ topModels: topModels2
6864
7163
  };
6865
7164
  }
6866
- const prov = d.providers[pf] ?? { calls: 0, cost: 0 };
7165
+ const prov = d.providers[pf];
7166
+ const topModels = Object.entries(prov?.models ?? {}).filter(([name]) => name !== "<synthetic>").sort(([, a], [, b]) => b.cost - a.cost).slice(0, 5).map(([name, m]) => ({
7167
+ name,
7168
+ cost: m.cost,
7169
+ calls: m.calls,
7170
+ inputTokens: m.inputTokens,
7171
+ outputTokens: m.outputTokens
7172
+ }));
6867
7173
  return {
6868
7174
  date: d.date,
6869
- cost: prov.cost,
6870
- calls: prov.calls,
6871
- inputTokens: 0,
6872
- outputTokens: 0,
6873
- cacheReadTokens: 0,
6874
- cacheWriteTokens: 0,
6875
- topModels: []
7175
+ cost: prov?.cost ?? 0,
7176
+ calls: prov?.calls ?? 0,
7177
+ inputTokens: prov?.inputTokens ?? 0,
7178
+ outputTokens: prov?.outputTokens ?? 0,
7179
+ cacheReadTokens: prov?.cacheReadTokens ?? 0,
7180
+ cacheWriteTokens: prov?.cacheWriteTokens ?? 0,
7181
+ topModels
6876
7182
  };
6877
7183
  });
6878
- const optimize = opts.optimize === false ? null : await scanAndDetect(scanProjects, scanRange);
7184
+ let optimize = null;
7185
+ if (opts.optimize !== false) {
7186
+ try {
7187
+ const optimizeProjects = isAllProviders && opts.period === "today" ? todayProjects : fp(await parseAllSessions(scanRange, isAllProviders ? "all" : pf));
7188
+ optimize = await scanAndDetect(optimizeProjects, scanRange);
7189
+ } catch (err) {
7190
+ const msg = err instanceof Error ? err.message : String(err);
7191
+ const warn2 = `optimize parse failed: ${msg}`;
7192
+ warnings.push(warn2);
7193
+ process.stderr.write(`[exe-watcher] WARNING: ${warn2}
7194
+ `);
7195
+ optimize = await scanAndDetect(todayProjects, scanRange);
7196
+ }
7197
+ }
6879
7198
  const exeOsDir = join21(homedir14(), ".exe-os");
6880
7199
  const exeOsDetected = existsSync6(exeOsDir);
6881
7200
  let agentStats = null;
@@ -6899,13 +7218,13 @@ program.command("status").description("Compact status output (today + week + mon
6899
7218
  if (agentStats) {
6900
7219
  agentStats = estimateAgentCosts(agentStats);
6901
7220
  }
6902
- const [proj24h, proj7d, proj30d] = await Promise.all([
6903
- parseAllSessions(getDateRange2("today").range, "all"),
7221
+ const [proj7d, proj30d] = await Promise.all([
6904
7222
  parseAllSessions(getDateRange2("week").range, "all"),
6905
7223
  parseAllSessions(getDateRange2("30days").range, "all")
6906
7224
  ]);
6907
- const projectSpend = buildProjectSpend(proj24h, proj7d, proj30d);
6908
- console.log(JSON.stringify(buildMenubarPayload(currentData, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge)));
7225
+ const projectSpend = buildProjectSpend(allTodayProjects, proj7d, proj30d);
7226
+ const diagnostics = { daysCount, parseTimeMs, warnings };
7227
+ console.log(JSON.stringify(buildMenubarPayload(currentData, providers, optimize, dailyHistory, agentStats, projectSpend, exeOsDetected, statsFileAge, diagnostics)));
6909
7228
  return;
6910
7229
  }
6911
7230
  if (opts.format === "json") {
@@ -7203,8 +7522,9 @@ program.command("plan [action] [id]").description("Show or configure a subscript
7203
7522
  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
7523
  await loadPricing();
7205
7524
  const { range, label } = getDateRange2(opts.period);
7525
+ const scanPromise = prewarmOptimizeScan(range);
7206
7526
  const projects = await parseAllSessions(range, opts.provider);
7207
- await runOptimize(projects, label, range);
7527
+ await runOptimize(projects, label, range, scanPromise);
7208
7528
  });
7209
7529
  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
7530
  await loadPricing();