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.
- package/CHANGELOG.md +9 -0
- package/README.md +276 -214
- package/billing/anthropic.json +49 -0
- package/billing/billing.schema.json +213 -0
- package/billing/browserbase.json +68 -0
- package/billing/google-gemini.json +66 -0
- package/billing/inngest.json +45 -0
- package/billing/openai.json +70 -0
- package/billing/posthog.json +61 -0
- package/billing/resend.json +49 -0
- package/billing/scrapfly.json +38 -0
- package/billing/supabase.json +32 -0
- package/billing/upstash.json +65 -0
- package/billing/vercel.json +85 -0
- package/billing/voyage-ai.json +42 -0
- package/dist/cli.js +477 -286
- package/dist/cli.js.map +1 -1
- package/dist/cost-impact.d.ts +8 -4
- package/dist/cost-impact.js +261 -72
- package/dist/cost-impact.js.map +1 -1
- package/dist/{detector-myYS2eVC.d.ts → detector-DiBj3WjE.d.ts} +1 -1
- package/dist/hooks/on-file-change.js +271 -88
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +104 -79
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +65 -46
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +286 -113
- package/dist/index.js.map +1 -1
- package/dist/interactive-init.d.ts +2 -2
- package/dist/interactive-init.js +21 -19
- package/dist/interactive-init.js.map +1 -1
- package/dist/mcp-server.js +719 -88
- package/dist/mcp-server.js.map +1 -1
- package/dist/{types-BwIeWOYc.d.ts → types-CUAiYzmE.d.ts} +2 -0
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/registry.json +12 -68
- package/skills/burnwatch-interview/SKILL.md +2 -4
- package/skills/setup-burnwatch/SKILL.md +7 -9
package/dist/cost-impact.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { C as CostImpact } from './types-
|
|
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
|
|
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
|
-
*
|
|
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 };
|
package/dist/cost-impact.js
CHANGED
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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:
|
|
235
|
-
explanation: "
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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
|
package/dist/cost-impact.js.map
CHANGED
|
@@ -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"]}
|