burnwatch 0.13.1 → 0.14.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +276 -214
  3. package/billing/anthropic.json +49 -0
  4. package/billing/billing.schema.json +213 -0
  5. package/billing/browserbase.json +68 -0
  6. package/billing/google-gemini.json +66 -0
  7. package/billing/inngest.json +45 -0
  8. package/billing/openai.json +70 -0
  9. package/billing/posthog.json +61 -0
  10. package/billing/resend.json +49 -0
  11. package/billing/scrapfly.json +38 -0
  12. package/billing/supabase.json +32 -0
  13. package/billing/upstash.json +65 -0
  14. package/billing/vercel.json +85 -0
  15. package/billing/voyage-ai.json +42 -0
  16. package/dist/cli.js +477 -286
  17. package/dist/cli.js.map +1 -1
  18. package/dist/cost-impact.d.ts +8 -4
  19. package/dist/cost-impact.js +261 -72
  20. package/dist/cost-impact.js.map +1 -1
  21. package/dist/{detector-myYS2eVC.d.ts → detector-DiBj3WjE.d.ts} +1 -1
  22. package/dist/hooks/on-file-change.js +271 -88
  23. package/dist/hooks/on-file-change.js.map +1 -1
  24. package/dist/hooks/on-prompt.js.map +1 -1
  25. package/dist/hooks/on-session-start.js +104 -79
  26. package/dist/hooks/on-session-start.js.map +1 -1
  27. package/dist/hooks/on-stop.js +65 -46
  28. package/dist/hooks/on-stop.js.map +1 -1
  29. package/dist/index.d.ts +6 -4
  30. package/dist/index.js +286 -113
  31. package/dist/index.js.map +1 -1
  32. package/dist/interactive-init.d.ts +2 -2
  33. package/dist/interactive-init.js +21 -19
  34. package/dist/interactive-init.js.map +1 -1
  35. package/dist/mcp-server.js +719 -88
  36. package/dist/mcp-server.js.map +1 -1
  37. package/dist/{types-BwIeWOYc.d.ts → types-CUAiYzmE.d.ts} +2 -0
  38. package/llms.txt +1 -1
  39. package/package.json +2 -1
  40. package/registry.json +12 -68
  41. package/skills/burnwatch-interview/SKILL.md +2 -4
  42. package/skills/setup-burnwatch/SKILL.md +7 -9
@@ -1,15 +1,19 @@
1
- import { C as CostImpact } from './types-BwIeWOYc.js';
1
+ import { C as CostImpact } from './types-CUAiYzmE.js';
2
2
 
3
3
  /**
4
4
  * Predictive cost impact analysis.
5
5
  *
6
6
  * Scans file content for SDK call sites, detects multipliers (loops, .map(), etc.),
7
- * and projects monthly cost using registry pricing data.
7
+ * and projects monthly cost using billing manifest data when available,
8
+ * falling back to registry pricing data.
8
9
  */
9
10
 
11
+ /** Clear manifest cache (for testing). */
12
+ declare function clearManifestCache(): void;
10
13
  /**
11
14
  * Analyze a file's content for cost-impacting SDK calls.
12
- * Returns cost impact estimates for each detected service.
15
+ * Uses billing manifests for precise per-model/per-variant pricing.
16
+ * Falls back to registry pricing for services without manifests.
13
17
  */
14
18
  declare function analyzeCostImpact(filePath: string, content: string, projectRoot?: string): CostImpact[];
15
19
  /**
@@ -20,4 +24,4 @@ declare function formatCostImpactCard(impacts: CostImpact[], currentBudgets: Rec
20
24
  budget?: number;
21
25
  }>): string;
22
26
 
23
- export { analyzeCostImpact, formatCostImpactCard };
27
+ export { analyzeCostImpact, clearManifestCache, formatCostImpactCard };
@@ -1,3 +1,8 @@
1
+ // src/cost-impact.ts
2
+ import * as fs2 from "fs";
3
+ import * as path2 from "path";
4
+ import * as url2 from "url";
5
+
1
6
  // src/core/registry.ts
2
7
  import * as fs from "fs";
3
8
  import * as path from "path";
@@ -64,6 +69,37 @@ function loadRegistryFile(filePath, registry) {
64
69
  }
65
70
 
66
71
  // src/cost-impact.ts
72
+ var __dirname2 = path2.dirname(url2.fileURLToPath(import.meta.url));
73
+ var manifestCache = null;
74
+ function loadBillingManifests() {
75
+ if (manifestCache) return manifestCache;
76
+ manifestCache = /* @__PURE__ */ new Map();
77
+ const candidates = [
78
+ path2.resolve(__dirname2, "../billing"),
79
+ // from src/ during dev
80
+ path2.resolve(__dirname2, "../../billing")
81
+ // from dist/
82
+ ];
83
+ for (const dir of candidates) {
84
+ if (!fs2.existsSync(dir)) continue;
85
+ const files = fs2.readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "billing.schema.json");
86
+ for (const file of files) {
87
+ try {
88
+ const raw = fs2.readFileSync(path2.join(dir, file), "utf-8");
89
+ const manifest = JSON.parse(raw);
90
+ if (manifest.serviceId) {
91
+ manifestCache.set(manifest.serviceId, manifest);
92
+ }
93
+ } catch {
94
+ }
95
+ }
96
+ break;
97
+ }
98
+ return manifestCache;
99
+ }
100
+ function clearManifestCache() {
101
+ manifestCache = null;
102
+ }
67
103
  var SERVICE_CALL_PATTERNS = {
68
104
  anthropic: [
69
105
  /\.messages\.create\s*\(/g,
@@ -109,11 +145,6 @@ var SERVICE_CALL_PATTERNS = {
109
145
  /resend\.emails\.send\s*\(/g,
110
146
  /\.emails\.send\s*\(/g
111
147
  ],
112
- stripe: [
113
- /stripe\.charges\.create\s*\(/g,
114
- /stripe\.paymentIntents\.create\s*\(/g,
115
- /stripe\.checkout\.sessions\.create\s*\(/g
116
- ],
117
148
  supabase: [
118
149
  /supabase\.from\s*\(/g,
119
150
  /\.rpc\s*\(/g,
@@ -127,11 +158,6 @@ var SERVICE_CALL_PATTERNS = {
127
158
  /posthog\.capture\s*\(/g,
128
159
  /\.capture\s*\(/g
129
160
  ],
130
- aws: [
131
- /\.send\s*\(new\s+\w+Command/g,
132
- /s3Client\.send\s*\(/g,
133
- /lambdaClient\.send\s*\(/g
134
- ],
135
161
  firebase: [
136
162
  /firestore\.\w+\(\s*["']/g,
137
163
  /\.collection\s*\(/g,
@@ -161,40 +187,134 @@ var SERVICE_CALL_PATTERNS = {
161
187
  replicate: [
162
188
  /replicate\.run\s*\(/g,
163
189
  /replicate\.predictions\.create\s*\(/g
190
+ ],
191
+ vercel: [
192
+ /\.functions\./g,
193
+ /edge\s+function/gi
164
194
  ]
165
195
  };
196
+ function extractNumericContext(content) {
197
+ const ctx = /* @__PURE__ */ new Map();
198
+ const assignRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(\d+)\s*[;,\n]/g;
199
+ let m;
200
+ while ((m = assignRegex.exec(content)) !== null) {
201
+ ctx.set(m[1], parseInt(m[2], 10));
202
+ }
203
+ const arrayCtorRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:new\s+)?Array\s*\(\s*(\d+)\s*\)/g;
204
+ while ((m = arrayCtorRegex.exec(content)) !== null) {
205
+ ctx.set(m[1], parseInt(m[2], 10));
206
+ }
207
+ const arrayLitRegex = /(?:const|let|var)\s+(\w+)\s*=\s*\[([^\]]*)\]/g;
208
+ while ((m = arrayLitRegex.exec(content)) !== null) {
209
+ const elements = m[2].split(",").filter((e) => e.trim().length > 0);
210
+ if (elements.length > 1) {
211
+ ctx.set(m[1], elements.length);
212
+ }
213
+ }
214
+ return ctx;
215
+ }
216
+ function resolveLoopBound(varName, content, ctx) {
217
+ const num = parseInt(varName, 10);
218
+ if (!isNaN(num) && num > 0) return num;
219
+ if (ctx.has(varName)) return ctx.get(varName);
220
+ const lengthMatch = varName.match(/^(\w+)\.length$/);
221
+ if (lengthMatch && ctx.has(lengthMatch[1])) {
222
+ return ctx.get(lengthMatch[1]);
223
+ }
224
+ return null;
225
+ }
226
+ function resolveIterableSize(content, ctx) {
227
+ const forOfMatch = content.match(/for\s*\(\s*(?:const|let|var)\s+\w+\s+of\s+(\w+)/);
228
+ if (forOfMatch) {
229
+ const resolved = ctx.get(forOfMatch[1]);
230
+ if (resolved) return resolved;
231
+ }
232
+ const iterMatch = content.match(/(\w+)\s*\.\s*(?:map|forEach)\s*\(/);
233
+ if (iterMatch) {
234
+ const resolved = ctx.get(iterMatch[1]);
235
+ if (resolved) return resolved;
236
+ }
237
+ return null;
238
+ }
166
239
  function detectMultipliers(content) {
167
240
  const multipliers = [];
168
- if (/for\s*\(.*;\s*\w+\s*<\s*(\w+)/g.test(content)) {
169
- const loopMatch = content.match(/for\s*\(.*;\s*\w+\s*<\s*(\d+)/);
170
- if (loopMatch) {
171
- const bound = parseInt(loopMatch[1]);
172
- if (bound > 1) {
173
- multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });
174
- }
241
+ const ctx = extractNumericContext(content);
242
+ const forLoopRegex = /for\s*\(.*;\s*\w+\s*(?:<|<=)\s*([\w.]+)/g;
243
+ const forLoopMatch = forLoopRegex.exec(content);
244
+ if (forLoopMatch) {
245
+ const boundExpr = forLoopMatch[1];
246
+ const resolved = resolveLoopBound(boundExpr, content, ctx);
247
+ if (resolved && resolved > 1) {
248
+ multipliers.push({ label: `for loop (${resolved} iterations)`, factor: resolved });
175
249
  } else {
176
- multipliers.push({ label: "for loop (variable bound)", factor: 10 });
250
+ const hintMatch = content.match(new RegExp(`(?:const|let|var)\\s+${boundExpr}\\s*=\\s*(\\w+)\\.length`));
251
+ if (hintMatch) {
252
+ const arrayName = hintMatch[1];
253
+ const arraySize = ctx.get(arrayName);
254
+ if (arraySize && arraySize > 1) {
255
+ multipliers.push({ label: `for loop (${arraySize} iterations via ${arrayName}.length)`, factor: arraySize });
256
+ } else {
257
+ multipliers.push({ label: `for loop (${arrayName}.length \u2014 variable bound)`, factor: 10 });
258
+ }
259
+ } else {
260
+ multipliers.push({ label: "for loop (variable bound)", factor: 10 });
261
+ }
177
262
  }
178
263
  }
179
264
  if (/\.\s*map\s*\(\s*(async\s*)?\(/g.test(content)) {
180
- multipliers.push({ label: ".map() iteration", factor: 10 });
181
- }
182
- if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
183
- multipliers.push({ label: ".forEach() iteration", factor: 10 });
265
+ const size = resolveIterableSize(content, ctx);
266
+ if (size && size > 1) {
267
+ multipliers.push({ label: `.map() over ${size} items`, factor: size });
268
+ } else {
269
+ multipliers.push({ label: ".map() iteration", factor: 10 });
270
+ }
271
+ } else if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
272
+ const size = resolveIterableSize(content, ctx);
273
+ if (size && size > 1) {
274
+ multipliers.push({ label: `.forEach() over ${size} items`, factor: size });
275
+ } else {
276
+ multipliers.push({ label: ".forEach() iteration", factor: 10 });
277
+ }
184
278
  }
185
279
  if (/for\s*\(\s*(const|let|var)\s+\w+\s+(of|in)\s+/g.test(content)) {
186
- multipliers.push({ label: "for...of/in loop", factor: 10 });
280
+ const size = resolveIterableSize(content, ctx);
281
+ if (size && size > 1) {
282
+ multipliers.push({ label: `for...of over ${size} items`, factor: size });
283
+ } else {
284
+ multipliers.push({ label: "for...of/in loop", factor: 10 });
285
+ }
187
286
  }
188
287
  if (/Promise\.all\s*\(/g.test(content)) {
189
- multipliers.push({ label: "Promise.all (parallel batch)", factor: 10 });
288
+ const hasMap = multipliers.some((m) => m.label.includes(".map()"));
289
+ if (!hasMap) {
290
+ multipliers.push({ label: "Promise.all (parallel batch)", factor: 10 });
291
+ }
190
292
  }
191
- if (/cron|schedule|interval|setInterval|every\s+\d+\s*(min|hour|day|sec)/gi.test(content)) {
293
+ if (/cron|schedule|interval|setInterval|every\s+(?:\d+|other)\s*(min|hour|day|sec|week)/gi.test(content)) {
192
294
  if (/every\s+5\s*min/gi.test(content) || /\*\/5\s+\*\s+\*/g.test(content)) {
193
295
  multipliers.push({ label: "cron: every 5 minutes", factor: 8640 });
296
+ } else if (/every\s+15\s*min/gi.test(content) || /\*\/15\s+\*\s+\*/g.test(content)) {
297
+ multipliers.push({ label: "cron: every 15 minutes", factor: 2880 });
298
+ } else if (/every\s+30\s*min/gi.test(content) || /\*\/30\s+\*\s+\*/g.test(content)) {
299
+ multipliers.push({ label: "cron: every 30 minutes", factor: 1440 });
194
300
  } else if (/every\s+1?\s*hour/gi.test(content) || /0\s+\*\s+\*\s+\*/g.test(content)) {
195
301
  multipliers.push({ label: "cron: hourly", factor: 720 });
302
+ } else if (/every\s+(\d+)\s*hours?/gi.test(content)) {
303
+ const hoursMatch = content.match(/every\s+(\d+)\s*hours?/i);
304
+ const hours = parseInt(hoursMatch[1], 10);
305
+ const factor = Math.round(720 / hours);
306
+ multipliers.push({ label: `cron: every ${hours} hours`, factor });
307
+ } else if (/every\s+other\s+day|every\s+2\s*days?/gi.test(content) || /\*\/2\s+/g.test(content)) {
308
+ multipliers.push({ label: "cron: every other day", factor: 15 });
309
+ } else if (/every\s+(\d+)\s*days?/gi.test(content)) {
310
+ const daysMatch = content.match(/every\s+(\d+)\s*days?/i);
311
+ const days = parseInt(daysMatch[1], 10);
312
+ const factor = Math.round(30 / days);
313
+ multipliers.push({ label: `cron: every ${days} days`, factor });
196
314
  } else if (/every\s+1?\s*day/gi.test(content) || /0\s+0\s+\*\s+\*/g.test(content)) {
197
315
  multipliers.push({ label: "cron: daily", factor: 30 });
316
+ } else if (/every\s+1?\s*week/gi.test(content) || /0\s+0\s+\*\s+\*\s+0/g.test(content)) {
317
+ multipliers.push({ label: "cron: weekly", factor: 4 });
198
318
  } else {
199
319
  multipliers.push({ label: "scheduled execution", factor: 30 });
200
320
  }
@@ -206,40 +326,92 @@ function detectMultipliers(content) {
206
326
  multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });
207
327
  }
208
328
  }
329
+ for (const [name, value] of ctx) {
330
+ if (value >= 100 && /^(total|count|num|max|limit|size|pages|items|urls|batch)/i.test(name)) {
331
+ const alreadyCovered = multipliers.some((m) => m.factor === value);
332
+ if (!alreadyCovered) {
333
+ multipliers.push({ label: `${name} = ${value}`, factor: value });
334
+ }
335
+ }
336
+ }
209
337
  return multipliers;
210
338
  }
211
- var GOTCHA_MULTIPLIERS = {
212
- scrapfly: {
213
- low: 1,
214
- high: 25,
215
- explanation: "anti-bot bypass consumes 5-25x base credits"
216
- },
217
- browserbase: {
218
- low: 1,
219
- high: 5,
220
- explanation: "session duration affects cost \u2014 long sessions burn more"
221
- },
222
- anthropic: {
223
- low: 1,
224
- high: 60,
225
- explanation: "Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)"
226
- },
227
- openai: {
228
- low: 1,
229
- high: 30,
230
- explanation: "GPT-4 mini vs GPT-5 (30x cost range)"
231
- },
232
- stripe: {
339
+ function detectVariant(dimension, content) {
340
+ if (!dimension.variants || dimension.variants.length === 0) return void 0;
341
+ for (const variant of dimension.variants) {
342
+ if (!variant.codePatterns) continue;
343
+ for (const pattern of variant.codePatterns) {
344
+ try {
345
+ if (new RegExp(pattern, "i").test(content)) {
346
+ return variant;
347
+ }
348
+ } catch {
349
+ }
350
+ }
351
+ }
352
+ return dimension.variants.find((v) => v.isDefault);
353
+ }
354
+ function detectManifestMultipliers(manifest, content) {
355
+ if (!manifest.costMultipliers || manifest.costMultipliers.length === 0) {
356
+ return void 0;
357
+ }
358
+ let maxFactor = 1;
359
+ const activeNames = [];
360
+ for (const cm of manifest.costMultipliers) {
361
+ if (!cm.codePatterns) continue;
362
+ for (const pattern of cm.codePatterns) {
363
+ try {
364
+ if (new RegExp(pattern, "i").test(content)) {
365
+ maxFactor = Math.max(maxFactor, cm.factor);
366
+ activeNames.push(cm.name);
367
+ break;
368
+ }
369
+ } catch {
370
+ }
371
+ }
372
+ }
373
+ if (activeNames.length === 0) return void 0;
374
+ return {
233
375
  low: 1,
234
- high: 1.5,
235
- explanation: "international cards add 1-1.5% extra"
376
+ high: maxFactor,
377
+ explanation: activeNames.join(", ")
378
+ };
379
+ }
380
+ function computeManifestPerCallCost(manifest, content) {
381
+ let totalPerCall = 0;
382
+ const parts = [];
383
+ const unitsPerCall = manifest.typicalDevUsage?.unitsPerCall ?? 1;
384
+ for (const dim of manifest.billingDimensions) {
385
+ const variant = detectVariant(dim, content);
386
+ const ratePerUnit = variant?.ratePerUnit ?? dim.ratePerUnit;
387
+ const ratePer = variant?.ratePer ?? dim.ratePer ?? 1;
388
+ const dimCostPerCall = unitsPerCall / ratePer * ratePerUnit;
389
+ if (dimCostPerCall > 0) {
390
+ totalPerCall += dimCostPerCall;
391
+ const variantLabel = variant?.name ?? dim.name;
392
+ parts.push(variantLabel);
393
+ }
236
394
  }
395
+ return {
396
+ perCall: totalPerCall,
397
+ explanation: parts.length > 0 ? parts.join(" + ") : manifest.name
398
+ };
399
+ }
400
+ var LEGACY_GOTCHA_MULTIPLIERS = {};
401
+ var LEGACY_CALL_COSTS = {
402
+ firebase: 1e-4,
403
+ twilio: 0.01,
404
+ sendgrid: 1e-3,
405
+ "mongodb-atlas": 1e-4,
406
+ clerk: 0,
407
+ replicate: 0.01
237
408
  };
238
409
  function analyzeCostImpact(filePath, content, projectRoot) {
239
- if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {
410
+ if (!/\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(filePath)) {
240
411
  return [];
241
412
  }
242
413
  const registry = loadRegistry(projectRoot);
414
+ const manifests = loadBillingManifests();
243
415
  const impacts = [];
244
416
  const multipliers = detectMultipliers(content);
245
417
  for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {
@@ -255,32 +427,48 @@ function analyzeCostImpact(filePath, content, projectRoot) {
255
427
  const service = registry.get(serviceId);
256
428
  if (!service) continue;
257
429
  const multiplierFactor = multipliers.length > 0 ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1) : 1;
258
- const baseMonthlyRuns = multipliers.some((m) => m.label.startsWith("cron")) ? 1 : 50;
430
+ const manifest = manifests.get(serviceId);
431
+ let baseMonthlyRuns;
432
+ const isCron = multipliers.some((m) => m.label.startsWith("cron"));
433
+ if (isCron) {
434
+ baseMonthlyRuns = 1;
435
+ } else if (manifest?.typicalDevUsage?.callsPerDevHour && manifest.typicalDevUsage.callsPerDevHour > 0) {
436
+ baseMonthlyRuns = 132;
437
+ } else {
438
+ baseMonthlyRuns = 50;
439
+ }
259
440
  const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;
260
- const gotcha = GOTCHA_MULTIPLIERS[serviceId];
261
- const unitRate = service.pricing?.unitRate ?? 0;
262
441
  let costLow;
263
442
  let costHigh;
264
- if (unitRate > 0) {
265
- costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
266
- costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
267
- } else if (service.pricing?.monthlyBase !== void 0) {
268
- costLow = 0;
269
- costHigh = 0;
270
- } else {
271
- const typicalCallCosts = {
272
- anthropic: 3e-3,
273
- // ~$3/MTok * ~1K tokens average
274
- openai: 2e-3,
275
- "google-gemini": 1e-3,
276
- scrapfly: 15e-5,
277
- browserbase: 0.01,
278
- resend: 1e-3,
279
- stripe: 0.3
280
- };
281
- const perCall = typicalCallCosts[serviceId] ?? 1e-3;
443
+ let rangeExplanation;
444
+ if (manifest) {
445
+ const { perCall } = computeManifestPerCallCost(manifest, content);
446
+ const gotcha = detectManifestMultipliers(manifest, content);
282
447
  costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
283
448
  costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
449
+ rangeExplanation = gotcha?.explanation;
450
+ if (!gotcha && manifest.costMultipliers && manifest.costMultipliers.length > 0) {
451
+ const maxFactor = Math.max(...manifest.costMultipliers.map((m) => m.factor));
452
+ if (maxFactor > 1) {
453
+ costHigh = monthlyInvocations * perCall * maxFactor;
454
+ rangeExplanation = manifest.costMultipliers.map((m) => m.description).join("; ");
455
+ }
456
+ }
457
+ } else {
458
+ const gotcha = LEGACY_GOTCHA_MULTIPLIERS[serviceId];
459
+ const unitRate = service.pricing?.unitRate ?? 0;
460
+ if (unitRate > 0) {
461
+ costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
462
+ costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
463
+ } else if (service.pricing?.monthlyBase !== void 0 && service.pricing.monthlyBase > 0) {
464
+ costLow = 0;
465
+ costHigh = 0;
466
+ } else {
467
+ const perCall = LEGACY_CALL_COSTS[serviceId] ?? 1e-3;
468
+ costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
469
+ costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
470
+ }
471
+ rangeExplanation = gotcha?.explanation;
284
472
  }
285
473
  if (costLow === 0 && costHigh === 0) continue;
286
474
  impacts.push({
@@ -293,7 +481,7 @@ function analyzeCostImpact(filePath, content, projectRoot) {
293
481
  monthlyInvocations,
294
482
  costLow,
295
483
  costHigh,
296
- rangeExplanation: gotcha?.explanation
484
+ rangeExplanation
297
485
  });
298
486
  }
299
487
  return impacts;
@@ -330,6 +518,7 @@ function formatCostImpactCard(impacts, currentBudgets) {
330
518
  }
331
519
  export {
332
520
  analyzeCostImpact,
521
+ clearManifestCache,
333
522
  formatCostImpactCard
334
523
  };
335
524
  //# sourceMappingURL=cost-impact.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/registry.ts","../src/cost-impact.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type { ServiceDefinition } from \"./types.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\ninterface RegistryFile {\n version: string;\n lastUpdated: string;\n services: Record<string, ServiceDefinition>;\n}\n\nlet cachedRegistry: Map<string, ServiceDefinition> | null = null;\nlet cachedRegistryPath: string | null = null;\nlet cachedRegistryMtime: number | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n * Invalidates cache when the registry file has been modified (for long-running processes like MCP server).\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n // Check cache validity — invalidate if the file has been modified\n if (cachedRegistry && cachedRegistryPath && cachedRegistryMtime !== null) {\n try {\n const stat = fs.statSync(cachedRegistryPath);\n if (stat.mtimeMs !== cachedRegistryMtime) {\n cachedRegistry = null;\n }\n } catch {\n // File gone — invalidate\n cachedRegistry = null;\n }\n }\n\n if (cachedRegistry) return cachedRegistry;\n\n const registry = new Map<string, ServiceDefinition>();\n\n // Load bundled registry (shipped with package)\n // Try multiple possible locations — depends on whether running from src/ or dist/\n let resolvedPath: string | null = null;\n const candidates = [\n path.resolve(__dirname, \"../../registry.json\"), // from src/core/\n path.resolve(__dirname, \"../registry.json\"), // from dist/\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n loadRegistryFile(candidate, registry);\n resolvedPath = candidate;\n break;\n }\n }\n\n // Load project-local override (if exists) — takes precedence for mtime tracking\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n resolvedPath = localPath;\n }\n }\n\n cachedRegistry = registry;\n\n // Track mtime for cache invalidation (important for long-running MCP server)\n if (resolvedPath) {\n try {\n cachedRegistryPath = resolvedPath;\n cachedRegistryMtime = fs.statSync(resolvedPath).mtimeMs;\n } catch {\n cachedRegistryPath = null;\n cachedRegistryMtime = null;\n }\n }\n\n return registry;\n}\n\nfunction loadRegistryFile(\n filePath: string,\n registry: Map<string, ServiceDefinition>,\n): void {\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(raw) as RegistryFile;\n for (const [id, service] of Object.entries(data.services)) {\n registry.set(id, { ...service, id });\n }\n } catch {\n // Silently skip missing or malformed registry files\n }\n}\n\n/** Clear the cached registry (for testing). */\nexport function clearRegistryCache(): void {\n cachedRegistry = null;\n cachedRegistryPath = null;\n cachedRegistryMtime = null;\n}\n\n/** Get a single service definition by ID. */\nexport function getService(\n id: string,\n projectRoot?: string,\n): ServiceDefinition | undefined {\n return loadRegistry(projectRoot).get(id);\n}\n\n/** Get all service definitions. */\nexport function getAllServices(\n projectRoot?: string,\n): ServiceDefinition[] {\n return Array.from(loadRegistry(projectRoot).values());\n}\n","/**\n * Predictive cost impact analysis.\n *\n * Scans file content for SDK call sites, detects multipliers (loops, .map(), etc.),\n * and projects monthly cost using registry pricing data.\n */\n\nimport type {\n CostImpact,\n ServiceDefinition,\n} from \"./core/types.js\";\nimport { loadRegistry } from \"./core/registry.js\";\n\n/** SDK call patterns per service — maps serviceId to regex patterns for call sites */\nconst SERVICE_CALL_PATTERNS: Record<string, RegExp[]> = {\n anthropic: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /anthropic\\.\\w+\\.create\\s*\\(/g,\n ],\n openai: [\n /\\.chat\\.completions\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /\\.images\\.generate\\s*\\(/g,\n /\\.embeddings\\.create\\s*\\(/g,\n /openai\\.\\w+\\.create\\s*\\(/g,\n ],\n \"google-gemini\": [\n /\\.generateContent\\s*\\(/g,\n /\\.generateContentStream\\s*\\(/g,\n /model\\.generate\\w*\\s*\\(/g,\n ],\n \"voyage-ai\": [\n /\\.embed\\s*\\(/g,\n /voyageai\\.embed\\s*\\(/g,\n ],\n scrapfly: [\n /\\.scrape\\s*\\(/g,\n /scrapfly\\.scrape\\s*\\(/g,\n /\\.async_scrape\\s*\\(/g,\n /ScrapeConfig\\s*\\(/g,\n ],\n browserbase: [\n /\\.createSession\\s*\\(/g,\n /\\.sessions\\.create\\s*\\(/g,\n /stagehand\\.act\\s*\\(/g,\n /stagehand\\.extract\\s*\\(/g,\n ],\n upstash: [\n /redis\\.\\w+\\s*\\(/g,\n /\\.set\\s*\\(/g,\n /\\.get\\s*\\(/g,\n /\\.incr\\s*\\(/g,\n /\\.hset\\s*\\(/g,\n ],\n resend: [\n /resend\\.emails\\.send\\s*\\(/g,\n /\\.emails\\.send\\s*\\(/g,\n ],\n stripe: [\n /stripe\\.charges\\.create\\s*\\(/g,\n /stripe\\.paymentIntents\\.create\\s*\\(/g,\n /stripe\\.checkout\\.sessions\\.create\\s*\\(/g,\n ],\n supabase: [\n /supabase\\.from\\s*\\(/g,\n /\\.rpc\\s*\\(/g,\n /supabase\\.storage/g,\n ],\n inngest: [\n /inngest\\.send\\s*\\(/g,\n /\\.createFunction\\s*\\(/g,\n ],\n posthog: [\n /posthog\\.capture\\s*\\(/g,\n /\\.capture\\s*\\(/g,\n ],\n aws: [\n /\\.send\\s*\\(new\\s+\\w+Command/g,\n /s3Client\\.send\\s*\\(/g,\n /lambdaClient\\.send\\s*\\(/g,\n ],\n firebase: [\n /firestore\\.\\w+\\(\\s*[\"']/g,\n /\\.collection\\s*\\(/g,\n /\\.doc\\s*\\(/g,\n /admin\\.firestore\\(\\)/g,\n ],\n twilio: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.calls\\.create\\s*\\(/g,\n /twilio\\.messages/g,\n ],\n sendgrid: [\n /sgMail\\.send\\s*\\(/g,\n /\\.send\\s*\\(\\s*msg/g,\n ],\n \"mongodb-atlas\": [\n /\\.find\\s*\\(/g,\n /\\.insertOne\\s*\\(/g,\n /\\.insertMany\\s*\\(/g,\n /\\.updateOne\\s*\\(/g,\n /\\.aggregate\\s*\\(/g,\n ],\n clerk: [\n /clerkClient\\.\\w+/g,\n /auth\\(\\)/g,\n ],\n replicate: [\n /replicate\\.run\\s*\\(/g,\n /replicate\\.predictions\\.create\\s*\\(/g,\n ],\n};\n\n/** Multiplier patterns — things that make calls happen more than once */\ninterface MultiplierMatch {\n label: string;\n factor: number;\n}\n\nfunction detectMultipliers(content: string): MultiplierMatch[] {\n const multipliers: MultiplierMatch[] = [];\n\n // for loops — assume 10x as conservative estimate\n if (/for\\s*\\(.*;\\s*\\w+\\s*<\\s*(\\w+)/g.test(content)) {\n // Try to extract the loop bound\n const loopMatch = content.match(/for\\s*\\(.*;\\s*\\w+\\s*<\\s*(\\d+)/);\n if (loopMatch) {\n const bound = parseInt(loopMatch[1]!);\n if (bound > 1) {\n multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });\n }\n } else {\n multipliers.push({ label: \"for loop (variable bound)\", factor: 10 });\n }\n }\n\n // .map() calls\n if (/\\.\\s*map\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n multipliers.push({ label: \".map() iteration\", factor: 10 });\n }\n\n // .forEach() calls\n if (/\\.\\s*forEach\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n multipliers.push({ label: \".forEach() iteration\", factor: 10 });\n }\n\n // for...of / for...in\n if (/for\\s*\\(\\s*(const|let|var)\\s+\\w+\\s+(of|in)\\s+/g.test(content)) {\n multipliers.push({ label: \"for...of/in loop\", factor: 10 });\n }\n\n // Promise.all with array\n if (/Promise\\.all\\s*\\(/g.test(content)) {\n multipliers.push({ label: \"Promise.all (parallel batch)\", factor: 10 });\n }\n\n // Cron patterns in comments or configuration\n if (/cron|schedule|interval|setInterval|every\\s+\\d+\\s*(min|hour|day|sec)/gi.test(content)) {\n // Estimate: if hourly = 720/mo, daily = 30/mo, every 5 min = 8640/mo\n if (/every\\s+5\\s*min/gi.test(content) || /\\*\\/5\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 5 minutes\", factor: 8640 });\n } else if (/every\\s+1?\\s*hour/gi.test(content) || /0\\s+\\*\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: hourly\", factor: 720 });\n } else if (/every\\s+1?\\s*day/gi.test(content) || /0\\s+0\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: daily\", factor: 30 });\n } else {\n multipliers.push({ label: \"scheduled execution\", factor: 30 });\n }\n }\n\n // Batch size hints\n const batchMatch = content.match(/batch[_\\s]?size\\s*[=:]\\s*(\\d+)/i);\n if (batchMatch) {\n const batchSize = parseInt(batchMatch[1]!);\n if (batchSize > 1) {\n multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });\n }\n }\n\n return multipliers;\n}\n\n/** Gotcha-based cost multipliers per service */\nconst GOTCHA_MULTIPLIERS: Record<string, { low: number; high: number; explanation: string }> = {\n scrapfly: {\n low: 1,\n high: 25,\n explanation: \"anti-bot bypass consumes 5-25x base credits\",\n },\n browserbase: {\n low: 1,\n high: 5,\n explanation: \"session duration affects cost — long sessions burn more\",\n },\n anthropic: {\n low: 1,\n high: 60,\n explanation: \"Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)\",\n },\n openai: {\n low: 1,\n high: 30,\n explanation: \"GPT-4 mini vs GPT-5 (30x cost range)\",\n },\n stripe: {\n low: 1,\n high: 1.5,\n explanation: \"international cards add 1-1.5% extra\",\n },\n};\n\n/**\n * Analyze a file's content for cost-impacting SDK calls.\n * Returns cost impact estimates for each detected service.\n */\nexport function analyzeCostImpact(\n filePath: string,\n content: string,\n projectRoot?: string,\n): CostImpact[] {\n // Only analyze source files\n if (!/\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {\n return [];\n }\n\n const registry = loadRegistry(projectRoot);\n const impacts: CostImpact[] = [];\n const multipliers = detectMultipliers(content);\n\n for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {\n let totalCalls = 0;\n\n for (const pattern of patterns) {\n // Reset lastIndex for global regexes\n pattern.lastIndex = 0;\n const matches = content.match(pattern);\n if (matches) {\n totalCalls += matches.length;\n }\n }\n\n if (totalCalls === 0) continue;\n\n const service = registry.get(serviceId);\n if (!service) continue;\n\n // Calculate effective multiplier\n const multiplierFactor = multipliers.length > 0\n ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1)\n : 1;\n\n // Assume ~30 working days, ~8 dev hours/day as baseline for monthly projections\n // If no cron detected, assume the code runs during dev sessions: ~50 times/month\n const baseMonthlyRuns = multipliers.some((m) => m.label.startsWith(\"cron\"))\n ? 1 // cron multiplier already encodes frequency\n : 50; // ~2 dev runs per working day\n\n const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;\n\n // Get cost estimates\n const gotcha = GOTCHA_MULTIPLIERS[serviceId];\n const unitRate = service.pricing?.unitRate ?? 0;\n\n let costLow: number;\n let costHigh: number;\n\n if (unitRate > 0) {\n costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);\n } else if (service.pricing?.monthlyBase !== undefined) {\n // Flat-rate services — cost is the plan, not per-invocation\n costLow = 0;\n costHigh = 0;\n } else {\n // Estimate based on typical per-call costs\n const typicalCallCosts: Record<string, number> = {\n anthropic: 0.003, // ~$3/MTok * ~1K tokens average\n openai: 0.002,\n \"google-gemini\": 0.001,\n scrapfly: 0.00015,\n browserbase: 0.01,\n resend: 0.001,\n stripe: 0.30,\n };\n const perCall = typicalCallCosts[serviceId] ?? 0.001;\n costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);\n }\n\n // Skip if no meaningful cost\n if (costLow === 0 && costHigh === 0) continue;\n\n impacts.push({\n serviceId,\n serviceName: service.name,\n filePath,\n callCount: totalCalls,\n multipliers: multipliers.map((m) => m.label),\n multiplierFactor,\n monthlyInvocations,\n costLow,\n costHigh,\n rangeExplanation: gotcha?.explanation,\n });\n }\n\n return impacts;\n}\n\n/**\n * Format a cost impact card for injection into Claude's context.\n */\nexport function formatCostImpactCard(\n impacts: CostImpact[],\n currentBudgets: Record<string, { spend: number; budget?: number }>,\n): string {\n const fileName = impacts[0]?.filePath.split(\"/\").pop() ?? \"unknown\";\n const lines: string[] = [];\n\n lines.push(`[BURNWATCH] ⚠️ Cost impact estimate for ${fileName}`);\n\n for (const impact of impacts) {\n const lowStr = impact.costLow < 1\n ? `$${impact.costLow.toFixed(2)}`\n : `$${impact.costLow.toFixed(0)}`;\n const highStr = impact.costHigh < 1\n ? `$${impact.costHigh.toFixed(2)}`\n : `$${impact.costHigh.toFixed(0)}`;\n\n const rangeStr = impact.costLow === impact.costHigh\n ? lowStr\n : `${lowStr}-${highStr}`;\n\n lines.push(\n ` ${impact.serviceName}: ~${impact.monthlyInvocations.toLocaleString()} calls/mo → ${rangeStr}/mo` +\n (impact.rangeExplanation ? ` (${impact.rangeExplanation})` : \"\"),\n );\n\n // Show current budget status if available\n const current = currentBudgets[impact.serviceId];\n if (current) {\n const budgetStr = current.budget\n ? `$${current.spend.toFixed(0)}/$${current.budget} budget`\n : `$${current.spend.toFixed(0)} (no budget set)`;\n const pctStr = current.budget && current.budget > 0\n ? ` (${((current.spend / current.budget) * 100).toFixed(0)}%)`\n : \"\";\n lines.push(` Current: ${budgetStr}${pctStr}`);\n }\n\n // Suggest alternatives from registry\n const registry = loadRegistry();\n const service = registry.get(impact.serviceId);\n if (service?.alternatives && service.alternatives.length > 0 && impact.costHigh > 10) {\n const freeAlts = service.alternatives.filter(\n (a) => a.includes(\"free\") || a.includes(\"cheerio\") || a.includes(\"playwright\") || a.includes(\"self-hosted\"),\n );\n if (freeAlts.length > 0) {\n lines.push(` Consider: ${freeAlts.join(\", \")} for lower-cost alternative`);\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cost-impact.ts","../src/core/registry.ts"],"sourcesContent":["/**\n * Predictive cost impact analysis.\n *\n * Scans file content for SDK call sites, detects multipliers (loops, .map(), etc.),\n * and projects monthly cost using billing manifest data when available,\n * falling back to registry pricing data.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type {\n CostImpact,\n ServiceDefinition,\n} from \"./core/types.js\";\nimport { loadRegistry } from \"./core/registry.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\n// ── Billing manifest types ──────────────────────────────────────────\n\ninterface DimensionVariant {\n id: string;\n name: string;\n ratePerUnit: number;\n ratePer?: number;\n codePatterns?: string[];\n isDefault?: boolean;\n}\n\ninterface BillingDimension {\n id: string;\n name: string;\n unit: string;\n ratePerUnit: number;\n ratePer?: number;\n variants?: DimensionVariant[];\n}\n\ninterface CostMultiplier {\n id: string;\n name: string;\n factor: number;\n description: string;\n codePatterns?: string[];\n}\n\ninterface BillingManifest {\n serviceId: string;\n name: string;\n billingDimensions: BillingDimension[];\n plans?: Array<{\n id: string;\n name: string;\n monthlyBase: number;\n included?: Record<string, number>;\n overageRates?: Record<string, number>;\n hardCap?: boolean;\n isDefault?: boolean;\n }>;\n costMultipliers?: CostMultiplier[];\n typicalDevUsage?: {\n callsPerDevHour: number;\n unitsPerCall: number;\n unitName: string;\n };\n notes?: string;\n}\n\n// ── Manifest loader (cached) ────────────────────────────────────────\n\nlet manifestCache: Map<string, BillingManifest> | null = null;\n\nfunction loadBillingManifests(): Map<string, BillingManifest> {\n if (manifestCache) return manifestCache;\n\n manifestCache = new Map();\n\n // Try multiple possible locations for the billing/ directory\n const candidates = [\n path.resolve(__dirname, \"../billing\"), // from src/ during dev\n path.resolve(__dirname, \"../../billing\"), // from dist/\n ];\n\n for (const dir of candidates) {\n if (!fs.existsSync(dir)) continue;\n const files = fs.readdirSync(dir).filter((f) => f.endsWith(\".json\") && f !== \"billing.schema.json\");\n for (const file of files) {\n try {\n const raw = fs.readFileSync(path.join(dir, file), \"utf-8\");\n const manifest = JSON.parse(raw) as BillingManifest;\n if (manifest.serviceId) {\n manifestCache.set(manifest.serviceId, manifest);\n }\n } catch {\n // Skip malformed manifests\n }\n }\n break; // Use the first directory that exists\n }\n\n return manifestCache;\n}\n\n/** Clear manifest cache (for testing). */\nexport function clearManifestCache(): void {\n manifestCache = null;\n}\n\n// ── SDK call patterns per service ───────────────────────────────────\n\nconst SERVICE_CALL_PATTERNS: Record<string, RegExp[]> = {\n anthropic: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /anthropic\\.\\w+\\.create\\s*\\(/g,\n ],\n openai: [\n /\\.chat\\.completions\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /\\.images\\.generate\\s*\\(/g,\n /\\.embeddings\\.create\\s*\\(/g,\n /openai\\.\\w+\\.create\\s*\\(/g,\n ],\n \"google-gemini\": [\n /\\.generateContent\\s*\\(/g,\n /\\.generateContentStream\\s*\\(/g,\n /model\\.generate\\w*\\s*\\(/g,\n ],\n \"voyage-ai\": [\n /\\.embed\\s*\\(/g,\n /voyageai\\.embed\\s*\\(/g,\n ],\n scrapfly: [\n /\\.scrape\\s*\\(/g,\n /scrapfly\\.scrape\\s*\\(/g,\n /\\.async_scrape\\s*\\(/g,\n /ScrapeConfig\\s*\\(/g,\n ],\n browserbase: [\n /\\.createSession\\s*\\(/g,\n /\\.sessions\\.create\\s*\\(/g,\n /stagehand\\.act\\s*\\(/g,\n /stagehand\\.extract\\s*\\(/g,\n ],\n upstash: [\n /redis\\.\\w+\\s*\\(/g,\n /\\.set\\s*\\(/g,\n /\\.get\\s*\\(/g,\n /\\.incr\\s*\\(/g,\n /\\.hset\\s*\\(/g,\n ],\n resend: [\n /resend\\.emails\\.send\\s*\\(/g,\n /\\.emails\\.send\\s*\\(/g,\n ],\n supabase: [\n /supabase\\.from\\s*\\(/g,\n /\\.rpc\\s*\\(/g,\n /supabase\\.storage/g,\n ],\n inngest: [\n /inngest\\.send\\s*\\(/g,\n /\\.createFunction\\s*\\(/g,\n ],\n posthog: [\n /posthog\\.capture\\s*\\(/g,\n /\\.capture\\s*\\(/g,\n ],\n firebase: [\n /firestore\\.\\w+\\(\\s*[\"']/g,\n /\\.collection\\s*\\(/g,\n /\\.doc\\s*\\(/g,\n /admin\\.firestore\\(\\)/g,\n ],\n twilio: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.calls\\.create\\s*\\(/g,\n /twilio\\.messages/g,\n ],\n sendgrid: [\n /sgMail\\.send\\s*\\(/g,\n /\\.send\\s*\\(\\s*msg/g,\n ],\n \"mongodb-atlas\": [\n /\\.find\\s*\\(/g,\n /\\.insertOne\\s*\\(/g,\n /\\.insertMany\\s*\\(/g,\n /\\.updateOne\\s*\\(/g,\n /\\.aggregate\\s*\\(/g,\n ],\n clerk: [\n /clerkClient\\.\\w+/g,\n /auth\\(\\)/g,\n ],\n replicate: [\n /replicate\\.run\\s*\\(/g,\n /replicate\\.predictions\\.create\\s*\\(/g,\n ],\n vercel: [\n /\\.functions\\./g,\n /edge\\s+function/gi,\n ],\n};\n\n// ── Multiplier detection ────────────────────────────────────────────\n\ninterface MultiplierMatch {\n label: string;\n factor: number;\n}\n\n/**\n * Extract numeric constants and variable assignments from code.\n * Builds a map of variable name → numeric value for resolving loop bounds.\n *\n * Handles:\n * const COUNT = 1000;\n * let total = 500;\n * const urls = Array(200)\n * const items = [...]; // count commas\n * const pages = new Array(50);\n */\nfunction extractNumericContext(content: string): Map<string, number> {\n const ctx = new Map<string, number>();\n\n // const/let/var NAME = NUMBER\n const assignRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*(\\d+)\\s*[;,\\n]/g;\n let m: RegExpExecArray | null;\n while ((m = assignRegex.exec(content)) !== null) {\n ctx.set(m[1]!, parseInt(m[2]!, 10));\n }\n\n // Array(N) or new Array(N)\n const arrayCtorRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:new\\s+)?Array\\s*\\(\\s*(\\d+)\\s*\\)/g;\n while ((m = arrayCtorRegex.exec(content)) !== null) {\n ctx.set(m[1]!, parseInt(m[2]!, 10));\n }\n\n // Array literals — count elements: const x = [a, b, c]\n const arrayLitRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\[([^\\]]*)\\]/g;\n while ((m = arrayLitRegex.exec(content)) !== null) {\n const elements = m[2]!.split(\",\").filter((e) => e.trim().length > 0);\n if (elements.length > 1) {\n ctx.set(m[1]!, elements.length);\n }\n }\n\n return ctx;\n}\n\n/**\n * Try to resolve a variable name to a numeric value.\n * Checks: direct numeric literal, variable from context, .length property.\n */\nfunction resolveLoopBound(\n varName: string,\n content: string,\n ctx: Map<string, number>,\n): number | null {\n // Direct numeric literal\n const num = parseInt(varName, 10);\n if (!isNaN(num) && num > 0) return num;\n\n // Variable from extracted context\n if (ctx.has(varName)) return ctx.get(varName)!;\n\n // foo.length — try to resolve foo\n const lengthMatch = varName.match(/^(\\w+)\\.length$/);\n if (lengthMatch && ctx.has(lengthMatch[1]!)) {\n return ctx.get(lengthMatch[1]!)!;\n }\n\n return null;\n}\n\n/**\n * Detect iteration size from the iterable in for...of or .map()/.forEach().\n * Looks for the array variable and resolves its size from context.\n */\nfunction resolveIterableSize(\n content: string,\n ctx: Map<string, number>,\n): number | null {\n // for (const x of ARRAY) — extract ARRAY name\n const forOfMatch = content.match(/for\\s*\\(\\s*(?:const|let|var)\\s+\\w+\\s+of\\s+(\\w+)/);\n if (forOfMatch) {\n const resolved = ctx.get(forOfMatch[1]!);\n if (resolved) return resolved;\n }\n\n // ARRAY.map(...) or ARRAY.forEach(...)\n const iterMatch = content.match(/(\\w+)\\s*\\.\\s*(?:map|forEach)\\s*\\(/);\n if (iterMatch) {\n const resolved = ctx.get(iterMatch[1]!);\n if (resolved) return resolved;\n }\n\n return null;\n}\n\nfunction detectMultipliers(content: string): MultiplierMatch[] {\n const multipliers: MultiplierMatch[] = [];\n const ctx = extractNumericContext(content);\n\n // ── for (i = 0; i < BOUND; i++) loops ──\n const forLoopRegex = /for\\s*\\(.*;\\s*\\w+\\s*(?:<|<=)\\s*([\\w.]+)/g;\n const forLoopMatch = forLoopRegex.exec(content);\n if (forLoopMatch) {\n const boundExpr = forLoopMatch[1]!;\n const resolved = resolveLoopBound(boundExpr, content, ctx);\n if (resolved && resolved > 1) {\n multipliers.push({ label: `for loop (${resolved} iterations)`, factor: resolved });\n } else {\n // Couldn't resolve — check if the bound var name hints at size\n const hintMatch = content.match(new RegExp(`(?:const|let|var)\\\\s+${boundExpr}\\\\s*=\\\\s*(\\\\w+)\\\\.length`));\n if (hintMatch) {\n const arrayName = hintMatch[1]!;\n const arraySize = ctx.get(arrayName);\n if (arraySize && arraySize > 1) {\n multipliers.push({ label: `for loop (${arraySize} iterations via ${arrayName}.length)`, factor: arraySize });\n } else {\n multipliers.push({ label: `for loop (${arrayName}.length — variable bound)`, factor: 10 });\n }\n } else {\n multipliers.push({ label: \"for loop (variable bound)\", factor: 10 });\n }\n }\n }\n\n // ── .map() / .forEach() ──\n if (/\\.\\s*map\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n const size = resolveIterableSize(content, ctx);\n if (size && size > 1) {\n multipliers.push({ label: `.map() over ${size} items`, factor: size });\n } else {\n multipliers.push({ label: \".map() iteration\", factor: 10 });\n }\n } else if (/\\.\\s*forEach\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n const size = resolveIterableSize(content, ctx);\n if (size && size > 1) {\n multipliers.push({ label: `.forEach() over ${size} items`, factor: size });\n } else {\n multipliers.push({ label: \".forEach() iteration\", factor: 10 });\n }\n }\n\n // ── for...of / for...in ──\n if (/for\\s*\\(\\s*(const|let|var)\\s+\\w+\\s+(of|in)\\s+/g.test(content)) {\n const size = resolveIterableSize(content, ctx);\n if (size && size > 1) {\n multipliers.push({ label: `for...of over ${size} items`, factor: size });\n } else {\n multipliers.push({ label: \"for...of/in loop\", factor: 10 });\n }\n }\n\n // ── Promise.all ──\n if (/Promise\\.all\\s*\\(/g.test(content)) {\n // Promise.all usually wraps a .map() — don't double-count if .map() was already detected\n const hasMap = multipliers.some((m) => m.label.includes(\".map()\"));\n if (!hasMap) {\n multipliers.push({ label: \"Promise.all (parallel batch)\", factor: 10 });\n }\n }\n\n // ── Cron patterns ──\n if (/cron|schedule|interval|setInterval|every\\s+(?:\\d+|other)\\s*(min|hour|day|sec|week)/gi.test(content)) {\n if (/every\\s+5\\s*min/gi.test(content) || /\\*\\/5\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 5 minutes\", factor: 8640 });\n } else if (/every\\s+15\\s*min/gi.test(content) || /\\*\\/15\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 15 minutes\", factor: 2880 });\n } else if (/every\\s+30\\s*min/gi.test(content) || /\\*\\/30\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 30 minutes\", factor: 1440 });\n } else if (/every\\s+1?\\s*hour/gi.test(content) || /0\\s+\\*\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: hourly\", factor: 720 });\n } else if (/every\\s+(\\d+)\\s*hours?/gi.test(content)) {\n const hoursMatch = content.match(/every\\s+(\\d+)\\s*hours?/i);\n const hours = parseInt(hoursMatch![1]!, 10);\n const factor = Math.round(720 / hours);\n multipliers.push({ label: `cron: every ${hours} hours`, factor });\n } else if (/every\\s+other\\s+day|every\\s+2\\s*days?/gi.test(content) || /\\*\\/2\\s+/g.test(content)) {\n multipliers.push({ label: \"cron: every other day\", factor: 15 });\n } else if (/every\\s+(\\d+)\\s*days?/gi.test(content)) {\n const daysMatch = content.match(/every\\s+(\\d+)\\s*days?/i);\n const days = parseInt(daysMatch![1]!, 10);\n const factor = Math.round(30 / days);\n multipliers.push({ label: `cron: every ${days} days`, factor });\n } else if (/every\\s+1?\\s*day/gi.test(content) || /0\\s+0\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: daily\", factor: 30 });\n } else if (/every\\s+1?\\s*week/gi.test(content) || /0\\s+0\\s+\\*\\s+\\*\\s+0/g.test(content)) {\n multipliers.push({ label: \"cron: weekly\", factor: 4 });\n } else {\n multipliers.push({ label: \"scheduled execution\", factor: 30 });\n }\n }\n\n // ── Batch size hints ──\n const batchMatch = content.match(/batch[_\\s]?size\\s*[=:]\\s*(\\d+)/i);\n if (batchMatch) {\n const batchSize = parseInt(batchMatch[1]!);\n if (batchSize > 1) {\n multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });\n }\n }\n\n // ── Explicit count/total/limit constants ──\n // Catch patterns like: const TOTAL_PAGES = 1000, const NUM_REQUESTS = 500\n for (const [name, value] of ctx) {\n if (value >= 100 && /^(total|count|num|max|limit|size|pages|items|urls|batch)/i.test(name)) {\n // Only add if we didn't already detect this from a loop/map\n const alreadyCovered = multipliers.some((m) => m.factor === value);\n if (!alreadyCovered) {\n multipliers.push({ label: `${name} = ${value}`, factor: value });\n }\n }\n }\n\n return multipliers;\n}\n\n// ── Manifest-based cost computation ─────────────────────────────────\n\n/**\n * Detect which variant of a billing dimension is being used in the code.\n * Returns the matching variant, or the default variant, or undefined.\n */\nfunction detectVariant(\n dimension: BillingDimension,\n content: string,\n): DimensionVariant | undefined {\n if (!dimension.variants || dimension.variants.length === 0) return undefined;\n\n // Try to match code patterns for each variant\n for (const variant of dimension.variants) {\n if (!variant.codePatterns) continue;\n for (const pattern of variant.codePatterns) {\n try {\n if (new RegExp(pattern, \"i\").test(content)) {\n return variant;\n }\n } catch {\n // Skip invalid regex\n }\n }\n }\n\n // Fall back to default variant\n return dimension.variants.find((v) => v.isDefault);\n}\n\n/**\n * Detect active cost multipliers from the manifest based on code patterns.\n * Returns { low, high, explanation } similar to the old GOTCHA_MULTIPLIERS.\n */\nfunction detectManifestMultipliers(\n manifest: BillingManifest,\n content: string,\n): { low: number; high: number; explanation: string } | undefined {\n if (!manifest.costMultipliers || manifest.costMultipliers.length === 0) {\n return undefined;\n }\n\n let maxFactor = 1;\n const activeNames: string[] = [];\n\n for (const cm of manifest.costMultipliers) {\n if (!cm.codePatterns) continue;\n for (const pattern of cm.codePatterns) {\n try {\n if (new RegExp(pattern, \"i\").test(content)) {\n maxFactor = Math.max(maxFactor, cm.factor);\n activeNames.push(cm.name);\n break; // One match per multiplier is enough\n }\n } catch {\n // Skip invalid regex\n }\n }\n }\n\n if (activeNames.length === 0) return undefined;\n\n return {\n low: 1,\n high: maxFactor,\n explanation: activeNames.join(\", \"),\n };\n}\n\n/**\n * Compute the per-call cost using billing manifest data.\n * Uses variant detection to pick the right rate from the manifest.\n */\nfunction computeManifestPerCallCost(\n manifest: BillingManifest,\n content: string,\n): { perCall: number; explanation: string } {\n // Sum across all billing dimensions (e.g., input_tokens + output_tokens)\n let totalPerCall = 0;\n const parts: string[] = [];\n\n const unitsPerCall = manifest.typicalDevUsage?.unitsPerCall ?? 1;\n\n for (const dim of manifest.billingDimensions) {\n const variant = detectVariant(dim, content);\n const ratePerUnit = variant?.ratePerUnit ?? dim.ratePerUnit;\n const ratePer = variant?.ratePer ?? dim.ratePer ?? 1;\n\n // cost = (units / ratePer) * ratePerUnit\n const dimCostPerCall = (unitsPerCall / ratePer) * ratePerUnit;\n\n if (dimCostPerCall > 0) {\n totalPerCall += dimCostPerCall;\n const variantLabel = variant?.name ?? dim.name;\n parts.push(variantLabel);\n }\n }\n\n return {\n perCall: totalPerCall,\n explanation: parts.length > 0 ? parts.join(\" + \") : manifest.name,\n };\n}\n\n// ── Legacy fallback (services without manifests) ────────────────────\n\nconst LEGACY_GOTCHA_MULTIPLIERS: Record<string, { low: number; high: number; explanation: string }> = {};\n\nconst LEGACY_CALL_COSTS: Record<string, number> = {\n firebase: 0.0001,\n twilio: 0.01,\n sendgrid: 0.001,\n \"mongodb-atlas\": 0.0001,\n clerk: 0,\n replicate: 0.01,\n};\n\n// ── Main analysis function ──────────────────────────────────────────\n\n/**\n * Analyze a file's content for cost-impacting SDK calls.\n * Uses billing manifests for precise per-model/per-variant pricing.\n * Falls back to registry pricing for services without manifests.\n */\nexport function analyzeCostImpact(\n filePath: string,\n content: string,\n projectRoot?: string,\n): CostImpact[] {\n // Only analyze source files\n if (!/\\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(filePath)) {\n return [];\n }\n\n const registry = loadRegistry(projectRoot);\n const manifests = loadBillingManifests();\n const impacts: CostImpact[] = [];\n const multipliers = detectMultipliers(content);\n\n for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {\n let totalCalls = 0;\n\n for (const pattern of patterns) {\n pattern.lastIndex = 0;\n const matches = content.match(pattern);\n if (matches) {\n totalCalls += matches.length;\n }\n }\n\n if (totalCalls === 0) continue;\n\n const service = registry.get(serviceId);\n if (!service) continue;\n\n // Calculate effective multiplier from code structure (loops, map, cron, etc.)\n const multiplierFactor = multipliers.length > 0\n ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1)\n : 1;\n\n const manifest = manifests.get(serviceId);\n\n // Smarter monthly run estimation:\n // - Cron: multiplier already encodes frequency, just 1 base run\n // - Manifest typicalDevUsage: use callsPerDevHour × 8h × 22 days\n // - Fallback: 50 runs/month (conservative)\n let baseMonthlyRuns: number;\n const isCron = multipliers.some((m) => m.label.startsWith(\"cron\"));\n\n if (isCron) {\n baseMonthlyRuns = 1; // cron multiplier factor already encodes frequency\n } else if (manifest?.typicalDevUsage?.callsPerDevHour && manifest.typicalDevUsage.callsPerDevHour > 0) {\n // ~22 working days, ~6 active dev hours/day = 132 dev hours/month\n // But we're counting call sites, not dev hours, so normalize:\n // If the dev makes N calls/hour and we see K call sites, estimate K invocations per dev run\n baseMonthlyRuns = 132; // dev-hours per month\n } else {\n baseMonthlyRuns = 50; // fallback: ~2 runs per working day\n }\n\n const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;\n\n let costLow: number;\n let costHigh: number;\n let rangeExplanation: string | undefined;\n\n if (manifest) {\n // ── Manifest-based pricing ──\n const { perCall } = computeManifestPerCallCost(manifest, content);\n const gotcha = detectManifestMultipliers(manifest, content);\n\n costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);\n rangeExplanation = gotcha?.explanation;\n\n // If no cost multipliers detected but manifest has them, show the range\n if (!gotcha && manifest.costMultipliers && manifest.costMultipliers.length > 0) {\n const maxFactor = Math.max(...manifest.costMultipliers.map((m) => m.factor));\n if (maxFactor > 1) {\n costHigh = monthlyInvocations * perCall * maxFactor;\n rangeExplanation = manifest.costMultipliers.map((m) => m.description).join(\"; \");\n }\n }\n } else {\n // ── Legacy fallback (no manifest) ──\n const gotcha = LEGACY_GOTCHA_MULTIPLIERS[serviceId];\n const unitRate = service.pricing?.unitRate ?? 0;\n\n if (unitRate > 0) {\n costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);\n } else if (service.pricing?.monthlyBase !== undefined && service.pricing.monthlyBase > 0) {\n // Flat-rate services — cost is the plan, not per-invocation\n costLow = 0;\n costHigh = 0;\n } else {\n const perCall = LEGACY_CALL_COSTS[serviceId] ?? 0.001;\n costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);\n }\n\n rangeExplanation = gotcha?.explanation;\n }\n\n // Skip if no meaningful cost\n if (costLow === 0 && costHigh === 0) continue;\n\n impacts.push({\n serviceId,\n serviceName: service.name,\n filePath,\n callCount: totalCalls,\n multipliers: multipliers.map((m) => m.label),\n multiplierFactor,\n monthlyInvocations,\n costLow,\n costHigh,\n rangeExplanation,\n });\n }\n\n return impacts;\n}\n\n/**\n * Format a cost impact card for injection into Claude's context.\n */\nexport function formatCostImpactCard(\n impacts: CostImpact[],\n currentBudgets: Record<string, { spend: number; budget?: number }>,\n): string {\n const fileName = impacts[0]?.filePath.split(\"/\").pop() ?? \"unknown\";\n const lines: string[] = [];\n\n lines.push(`[BURNWATCH] ⚠️ Cost impact estimate for ${fileName}`);\n\n for (const impact of impacts) {\n const lowStr = impact.costLow < 1\n ? `$${impact.costLow.toFixed(2)}`\n : `$${impact.costLow.toFixed(0)}`;\n const highStr = impact.costHigh < 1\n ? `$${impact.costHigh.toFixed(2)}`\n : `$${impact.costHigh.toFixed(0)}`;\n\n const rangeStr = impact.costLow === impact.costHigh\n ? lowStr\n : `${lowStr}-${highStr}`;\n\n lines.push(\n ` ${impact.serviceName}: ~${impact.monthlyInvocations.toLocaleString()} calls/mo → ${rangeStr}/mo` +\n (impact.rangeExplanation ? ` (${impact.rangeExplanation})` : \"\"),\n );\n\n // Show current budget status if available\n const current = currentBudgets[impact.serviceId];\n if (current) {\n const budgetStr = current.budget\n ? `$${current.spend.toFixed(0)}/$${current.budget} budget`\n : `$${current.spend.toFixed(0)} (no budget set)`;\n const pctStr = current.budget && current.budget > 0\n ? ` (${((current.spend / current.budget) * 100).toFixed(0)}%)`\n : \"\";\n lines.push(` Current: ${budgetStr}${pctStr}`);\n }\n\n // Suggest alternatives from registry\n const registry = loadRegistry();\n const service = registry.get(impact.serviceId);\n if (service?.alternatives && service.alternatives.length > 0 && impact.costHigh > 10) {\n const freeAlts = service.alternatives.filter(\n (a) => a.includes(\"free\") || a.includes(\"cheerio\") || a.includes(\"playwright\") || a.includes(\"self-hosted\"),\n );\n if (freeAlts.length > 0) {\n lines.push(` Consider: ${freeAlts.join(\", \")} for lower-cost alternative`);\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type { ServiceDefinition } from \"./types.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\ninterface RegistryFile {\n version: string;\n lastUpdated: string;\n services: Record<string, ServiceDefinition>;\n}\n\nlet cachedRegistry: Map<string, ServiceDefinition> | null = null;\nlet cachedRegistryPath: string | null = null;\nlet cachedRegistryMtime: number | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n * Invalidates cache when the registry file has been modified (for long-running processes like MCP server).\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n // Check cache validity — invalidate if the file has been modified\n if (cachedRegistry && cachedRegistryPath && cachedRegistryMtime !== null) {\n try {\n const stat = fs.statSync(cachedRegistryPath);\n if (stat.mtimeMs !== cachedRegistryMtime) {\n cachedRegistry = null;\n }\n } catch {\n // File gone — invalidate\n cachedRegistry = null;\n }\n }\n\n if (cachedRegistry) return cachedRegistry;\n\n const registry = new Map<string, ServiceDefinition>();\n\n // Load bundled registry (shipped with package)\n // Try multiple possible locations — depends on whether running from src/ or dist/\n let resolvedPath: string | null = null;\n const candidates = [\n path.resolve(__dirname, \"../../registry.json\"), // from src/core/\n path.resolve(__dirname, \"../registry.json\"), // from dist/\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n loadRegistryFile(candidate, registry);\n resolvedPath = candidate;\n break;\n }\n }\n\n // Load project-local override (if exists) — takes precedence for mtime tracking\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n resolvedPath = localPath;\n }\n }\n\n cachedRegistry = registry;\n\n // Track mtime for cache invalidation (important for long-running MCP server)\n if (resolvedPath) {\n try {\n cachedRegistryPath = resolvedPath;\n cachedRegistryMtime = fs.statSync(resolvedPath).mtimeMs;\n } catch {\n cachedRegistryPath = null;\n cachedRegistryMtime = null;\n }\n }\n\n return registry;\n}\n\nfunction loadRegistryFile(\n filePath: string,\n registry: Map<string, ServiceDefinition>,\n): void {\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(raw) as RegistryFile;\n for (const [id, service] of Object.entries(data.services)) {\n registry.set(id, { ...service, id });\n }\n } catch {\n // Silently skip missing or malformed registry files\n }\n}\n\n/** Clear the cached registry (for testing). */\nexport function clearRegistryCache(): void {\n cachedRegistry = null;\n cachedRegistryPath = null;\n cachedRegistryMtime = null;\n}\n\n/** Get a single service definition by ID. */\nexport function getService(\n id: string,\n projectRoot?: string,\n): ServiceDefinition | undefined {\n return loadRegistry(projectRoot).get(id);\n}\n\n/** Get all service definitions. */\nexport function getAllServices(\n projectRoot?: string,\n): ServiceDefinition[] {\n return Array.from(loadRegistry(projectRoot).values());\n}\n"],"mappings":"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","names":["fs","path","url","__dirname"]}
@@ -1,4 +1,4 @@
1
- import { S as ServiceDefinition, D as DetectionSource } from './types-BwIeWOYc.js';
1
+ import { S as ServiceDefinition, D as DetectionSource } from './types-CUAiYzmE.js';
2
2
 
3
3
  interface DetectionResult {
4
4
  service: ServiceDefinition;