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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/hooks/on-file-change.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
4
|
+
import * as fs7 from "fs";
|
|
5
|
+
import * as path7 from "path";
|
|
6
6
|
|
|
7
7
|
// src/core/config.ts
|
|
8
8
|
import * as fs from "fs";
|
|
@@ -198,6 +198,37 @@ function readLatestSnapshot(projectRoot) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
// src/cost-impact.ts
|
|
201
|
+
import * as fs5 from "fs";
|
|
202
|
+
import * as path5 from "path";
|
|
203
|
+
import * as url2 from "url";
|
|
204
|
+
var __dirname2 = path5.dirname(url2.fileURLToPath(import.meta.url));
|
|
205
|
+
var manifestCache = null;
|
|
206
|
+
function loadBillingManifests() {
|
|
207
|
+
if (manifestCache) return manifestCache;
|
|
208
|
+
manifestCache = /* @__PURE__ */ new Map();
|
|
209
|
+
const candidates = [
|
|
210
|
+
path5.resolve(__dirname2, "../billing"),
|
|
211
|
+
// from src/ during dev
|
|
212
|
+
path5.resolve(__dirname2, "../../billing")
|
|
213
|
+
// from dist/
|
|
214
|
+
];
|
|
215
|
+
for (const dir of candidates) {
|
|
216
|
+
if (!fs5.existsSync(dir)) continue;
|
|
217
|
+
const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "billing.schema.json");
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
try {
|
|
220
|
+
const raw = fs5.readFileSync(path5.join(dir, file), "utf-8");
|
|
221
|
+
const manifest = JSON.parse(raw);
|
|
222
|
+
if (manifest.serviceId) {
|
|
223
|
+
manifestCache.set(manifest.serviceId, manifest);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
return manifestCache;
|
|
231
|
+
}
|
|
201
232
|
var SERVICE_CALL_PATTERNS = {
|
|
202
233
|
anthropic: [
|
|
203
234
|
/\.messages\.create\s*\(/g,
|
|
@@ -243,11 +274,6 @@ var SERVICE_CALL_PATTERNS = {
|
|
|
243
274
|
/resend\.emails\.send\s*\(/g,
|
|
244
275
|
/\.emails\.send\s*\(/g
|
|
245
276
|
],
|
|
246
|
-
stripe: [
|
|
247
|
-
/stripe\.charges\.create\s*\(/g,
|
|
248
|
-
/stripe\.paymentIntents\.create\s*\(/g,
|
|
249
|
-
/stripe\.checkout\.sessions\.create\s*\(/g
|
|
250
|
-
],
|
|
251
277
|
supabase: [
|
|
252
278
|
/supabase\.from\s*\(/g,
|
|
253
279
|
/\.rpc\s*\(/g,
|
|
@@ -261,11 +287,6 @@ var SERVICE_CALL_PATTERNS = {
|
|
|
261
287
|
/posthog\.capture\s*\(/g,
|
|
262
288
|
/\.capture\s*\(/g
|
|
263
289
|
],
|
|
264
|
-
aws: [
|
|
265
|
-
/\.send\s*\(new\s+\w+Command/g,
|
|
266
|
-
/s3Client\.send\s*\(/g,
|
|
267
|
-
/lambdaClient\.send\s*\(/g
|
|
268
|
-
],
|
|
269
290
|
firebase: [
|
|
270
291
|
/firestore\.\w+\(\s*["']/g,
|
|
271
292
|
/\.collection\s*\(/g,
|
|
@@ -295,40 +316,134 @@ var SERVICE_CALL_PATTERNS = {
|
|
|
295
316
|
replicate: [
|
|
296
317
|
/replicate\.run\s*\(/g,
|
|
297
318
|
/replicate\.predictions\.create\s*\(/g
|
|
319
|
+
],
|
|
320
|
+
vercel: [
|
|
321
|
+
/\.functions\./g,
|
|
322
|
+
/edge\s+function/gi
|
|
298
323
|
]
|
|
299
324
|
};
|
|
325
|
+
function extractNumericContext(content) {
|
|
326
|
+
const ctx = /* @__PURE__ */ new Map();
|
|
327
|
+
const assignRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(\d+)\s*[;,\n]/g;
|
|
328
|
+
let m;
|
|
329
|
+
while ((m = assignRegex.exec(content)) !== null) {
|
|
330
|
+
ctx.set(m[1], parseInt(m[2], 10));
|
|
331
|
+
}
|
|
332
|
+
const arrayCtorRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:new\s+)?Array\s*\(\s*(\d+)\s*\)/g;
|
|
333
|
+
while ((m = arrayCtorRegex.exec(content)) !== null) {
|
|
334
|
+
ctx.set(m[1], parseInt(m[2], 10));
|
|
335
|
+
}
|
|
336
|
+
const arrayLitRegex = /(?:const|let|var)\s+(\w+)\s*=\s*\[([^\]]*)\]/g;
|
|
337
|
+
while ((m = arrayLitRegex.exec(content)) !== null) {
|
|
338
|
+
const elements = m[2].split(",").filter((e) => e.trim().length > 0);
|
|
339
|
+
if (elements.length > 1) {
|
|
340
|
+
ctx.set(m[1], elements.length);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return ctx;
|
|
344
|
+
}
|
|
345
|
+
function resolveLoopBound(varName, content, ctx) {
|
|
346
|
+
const num = parseInt(varName, 10);
|
|
347
|
+
if (!isNaN(num) && num > 0) return num;
|
|
348
|
+
if (ctx.has(varName)) return ctx.get(varName);
|
|
349
|
+
const lengthMatch = varName.match(/^(\w+)\.length$/);
|
|
350
|
+
if (lengthMatch && ctx.has(lengthMatch[1])) {
|
|
351
|
+
return ctx.get(lengthMatch[1]);
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
function resolveIterableSize(content, ctx) {
|
|
356
|
+
const forOfMatch = content.match(/for\s*\(\s*(?:const|let|var)\s+\w+\s+of\s+(\w+)/);
|
|
357
|
+
if (forOfMatch) {
|
|
358
|
+
const resolved = ctx.get(forOfMatch[1]);
|
|
359
|
+
if (resolved) return resolved;
|
|
360
|
+
}
|
|
361
|
+
const iterMatch = content.match(/(\w+)\s*\.\s*(?:map|forEach)\s*\(/);
|
|
362
|
+
if (iterMatch) {
|
|
363
|
+
const resolved = ctx.get(iterMatch[1]);
|
|
364
|
+
if (resolved) return resolved;
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
300
368
|
function detectMultipliers(content) {
|
|
301
369
|
const multipliers = [];
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
370
|
+
const ctx = extractNumericContext(content);
|
|
371
|
+
const forLoopRegex = /for\s*\(.*;\s*\w+\s*(?:<|<=)\s*([\w.]+)/g;
|
|
372
|
+
const forLoopMatch = forLoopRegex.exec(content);
|
|
373
|
+
if (forLoopMatch) {
|
|
374
|
+
const boundExpr = forLoopMatch[1];
|
|
375
|
+
const resolved = resolveLoopBound(boundExpr, content, ctx);
|
|
376
|
+
if (resolved && resolved > 1) {
|
|
377
|
+
multipliers.push({ label: `for loop (${resolved} iterations)`, factor: resolved });
|
|
309
378
|
} else {
|
|
310
|
-
|
|
379
|
+
const hintMatch = content.match(new RegExp(`(?:const|let|var)\\s+${boundExpr}\\s*=\\s*(\\w+)\\.length`));
|
|
380
|
+
if (hintMatch) {
|
|
381
|
+
const arrayName = hintMatch[1];
|
|
382
|
+
const arraySize = ctx.get(arrayName);
|
|
383
|
+
if (arraySize && arraySize > 1) {
|
|
384
|
+
multipliers.push({ label: `for loop (${arraySize} iterations via ${arrayName}.length)`, factor: arraySize });
|
|
385
|
+
} else {
|
|
386
|
+
multipliers.push({ label: `for loop (${arrayName}.length \u2014 variable bound)`, factor: 10 });
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
multipliers.push({ label: "for loop (variable bound)", factor: 10 });
|
|
390
|
+
}
|
|
311
391
|
}
|
|
312
392
|
}
|
|
313
393
|
if (/\.\s*map\s*\(\s*(async\s*)?\(/g.test(content)) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
394
|
+
const size = resolveIterableSize(content, ctx);
|
|
395
|
+
if (size && size > 1) {
|
|
396
|
+
multipliers.push({ label: `.map() over ${size} items`, factor: size });
|
|
397
|
+
} else {
|
|
398
|
+
multipliers.push({ label: ".map() iteration", factor: 10 });
|
|
399
|
+
}
|
|
400
|
+
} else if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
|
|
401
|
+
const size = resolveIterableSize(content, ctx);
|
|
402
|
+
if (size && size > 1) {
|
|
403
|
+
multipliers.push({ label: `.forEach() over ${size} items`, factor: size });
|
|
404
|
+
} else {
|
|
405
|
+
multipliers.push({ label: ".forEach() iteration", factor: 10 });
|
|
406
|
+
}
|
|
318
407
|
}
|
|
319
408
|
if (/for\s*\(\s*(const|let|var)\s+\w+\s+(of|in)\s+/g.test(content)) {
|
|
320
|
-
|
|
409
|
+
const size = resolveIterableSize(content, ctx);
|
|
410
|
+
if (size && size > 1) {
|
|
411
|
+
multipliers.push({ label: `for...of over ${size} items`, factor: size });
|
|
412
|
+
} else {
|
|
413
|
+
multipliers.push({ label: "for...of/in loop", factor: 10 });
|
|
414
|
+
}
|
|
321
415
|
}
|
|
322
416
|
if (/Promise\.all\s*\(/g.test(content)) {
|
|
323
|
-
multipliers.
|
|
417
|
+
const hasMap = multipliers.some((m) => m.label.includes(".map()"));
|
|
418
|
+
if (!hasMap) {
|
|
419
|
+
multipliers.push({ label: "Promise.all (parallel batch)", factor: 10 });
|
|
420
|
+
}
|
|
324
421
|
}
|
|
325
|
-
if (/cron|schedule|interval|setInterval|every\s
|
|
422
|
+
if (/cron|schedule|interval|setInterval|every\s+(?:\d+|other)\s*(min|hour|day|sec|week)/gi.test(content)) {
|
|
326
423
|
if (/every\s+5\s*min/gi.test(content) || /\*\/5\s+\*\s+\*/g.test(content)) {
|
|
327
424
|
multipliers.push({ label: "cron: every 5 minutes", factor: 8640 });
|
|
425
|
+
} else if (/every\s+15\s*min/gi.test(content) || /\*\/15\s+\*\s+\*/g.test(content)) {
|
|
426
|
+
multipliers.push({ label: "cron: every 15 minutes", factor: 2880 });
|
|
427
|
+
} else if (/every\s+30\s*min/gi.test(content) || /\*\/30\s+\*\s+\*/g.test(content)) {
|
|
428
|
+
multipliers.push({ label: "cron: every 30 minutes", factor: 1440 });
|
|
328
429
|
} else if (/every\s+1?\s*hour/gi.test(content) || /0\s+\*\s+\*\s+\*/g.test(content)) {
|
|
329
430
|
multipliers.push({ label: "cron: hourly", factor: 720 });
|
|
431
|
+
} else if (/every\s+(\d+)\s*hours?/gi.test(content)) {
|
|
432
|
+
const hoursMatch = content.match(/every\s+(\d+)\s*hours?/i);
|
|
433
|
+
const hours = parseInt(hoursMatch[1], 10);
|
|
434
|
+
const factor = Math.round(720 / hours);
|
|
435
|
+
multipliers.push({ label: `cron: every ${hours} hours`, factor });
|
|
436
|
+
} else if (/every\s+other\s+day|every\s+2\s*days?/gi.test(content) || /\*\/2\s+/g.test(content)) {
|
|
437
|
+
multipliers.push({ label: "cron: every other day", factor: 15 });
|
|
438
|
+
} else if (/every\s+(\d+)\s*days?/gi.test(content)) {
|
|
439
|
+
const daysMatch = content.match(/every\s+(\d+)\s*days?/i);
|
|
440
|
+
const days = parseInt(daysMatch[1], 10);
|
|
441
|
+
const factor = Math.round(30 / days);
|
|
442
|
+
multipliers.push({ label: `cron: every ${days} days`, factor });
|
|
330
443
|
} else if (/every\s+1?\s*day/gi.test(content) || /0\s+0\s+\*\s+\*/g.test(content)) {
|
|
331
444
|
multipliers.push({ label: "cron: daily", factor: 30 });
|
|
445
|
+
} else if (/every\s+1?\s*week/gi.test(content) || /0\s+0\s+\*\s+\*\s+0/g.test(content)) {
|
|
446
|
+
multipliers.push({ label: "cron: weekly", factor: 4 });
|
|
332
447
|
} else {
|
|
333
448
|
multipliers.push({ label: "scheduled execution", factor: 30 });
|
|
334
449
|
}
|
|
@@ -340,40 +455,92 @@ function detectMultipliers(content) {
|
|
|
340
455
|
multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });
|
|
341
456
|
}
|
|
342
457
|
}
|
|
458
|
+
for (const [name, value] of ctx) {
|
|
459
|
+
if (value >= 100 && /^(total|count|num|max|limit|size|pages|items|urls|batch)/i.test(name)) {
|
|
460
|
+
const alreadyCovered = multipliers.some((m) => m.factor === value);
|
|
461
|
+
if (!alreadyCovered) {
|
|
462
|
+
multipliers.push({ label: `${name} = ${value}`, factor: value });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
343
466
|
return multipliers;
|
|
344
467
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
468
|
+
function detectVariant(dimension, content) {
|
|
469
|
+
if (!dimension.variants || dimension.variants.length === 0) return void 0;
|
|
470
|
+
for (const variant of dimension.variants) {
|
|
471
|
+
if (!variant.codePatterns) continue;
|
|
472
|
+
for (const pattern of variant.codePatterns) {
|
|
473
|
+
try {
|
|
474
|
+
if (new RegExp(pattern, "i").test(content)) {
|
|
475
|
+
return variant;
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return dimension.variants.find((v) => v.isDefault);
|
|
482
|
+
}
|
|
483
|
+
function detectManifestMultipliers(manifest, content) {
|
|
484
|
+
if (!manifest.costMultipliers || manifest.costMultipliers.length === 0) {
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
let maxFactor = 1;
|
|
488
|
+
const activeNames = [];
|
|
489
|
+
for (const cm of manifest.costMultipliers) {
|
|
490
|
+
if (!cm.codePatterns) continue;
|
|
491
|
+
for (const pattern of cm.codePatterns) {
|
|
492
|
+
try {
|
|
493
|
+
if (new RegExp(pattern, "i").test(content)) {
|
|
494
|
+
maxFactor = Math.max(maxFactor, cm.factor);
|
|
495
|
+
activeNames.push(cm.name);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
} catch {
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (activeNames.length === 0) return void 0;
|
|
503
|
+
return {
|
|
367
504
|
low: 1,
|
|
368
|
-
high:
|
|
369
|
-
explanation: "
|
|
505
|
+
high: maxFactor,
|
|
506
|
+
explanation: activeNames.join(", ")
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function computeManifestPerCallCost(manifest, content) {
|
|
510
|
+
let totalPerCall = 0;
|
|
511
|
+
const parts = [];
|
|
512
|
+
const unitsPerCall = manifest.typicalDevUsage?.unitsPerCall ?? 1;
|
|
513
|
+
for (const dim of manifest.billingDimensions) {
|
|
514
|
+
const variant = detectVariant(dim, content);
|
|
515
|
+
const ratePerUnit = variant?.ratePerUnit ?? dim.ratePerUnit;
|
|
516
|
+
const ratePer = variant?.ratePer ?? dim.ratePer ?? 1;
|
|
517
|
+
const dimCostPerCall = unitsPerCall / ratePer * ratePerUnit;
|
|
518
|
+
if (dimCostPerCall > 0) {
|
|
519
|
+
totalPerCall += dimCostPerCall;
|
|
520
|
+
const variantLabel = variant?.name ?? dim.name;
|
|
521
|
+
parts.push(variantLabel);
|
|
522
|
+
}
|
|
370
523
|
}
|
|
524
|
+
return {
|
|
525
|
+
perCall: totalPerCall,
|
|
526
|
+
explanation: parts.length > 0 ? parts.join(" + ") : manifest.name
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
var LEGACY_GOTCHA_MULTIPLIERS = {};
|
|
530
|
+
var LEGACY_CALL_COSTS = {
|
|
531
|
+
firebase: 1e-4,
|
|
532
|
+
twilio: 0.01,
|
|
533
|
+
sendgrid: 1e-3,
|
|
534
|
+
"mongodb-atlas": 1e-4,
|
|
535
|
+
clerk: 0,
|
|
536
|
+
replicate: 0.01
|
|
371
537
|
};
|
|
372
538
|
function analyzeCostImpact(filePath, content, projectRoot) {
|
|
373
|
-
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {
|
|
539
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(filePath)) {
|
|
374
540
|
return [];
|
|
375
541
|
}
|
|
376
542
|
const registry = loadRegistry(projectRoot);
|
|
543
|
+
const manifests = loadBillingManifests();
|
|
377
544
|
const impacts = [];
|
|
378
545
|
const multipliers = detectMultipliers(content);
|
|
379
546
|
for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {
|
|
@@ -389,32 +556,48 @@ function analyzeCostImpact(filePath, content, projectRoot) {
|
|
|
389
556
|
const service = registry.get(serviceId);
|
|
390
557
|
if (!service) continue;
|
|
391
558
|
const multiplierFactor = multipliers.length > 0 ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1) : 1;
|
|
392
|
-
const
|
|
559
|
+
const manifest = manifests.get(serviceId);
|
|
560
|
+
let baseMonthlyRuns;
|
|
561
|
+
const isCron = multipliers.some((m) => m.label.startsWith("cron"));
|
|
562
|
+
if (isCron) {
|
|
563
|
+
baseMonthlyRuns = 1;
|
|
564
|
+
} else if (manifest?.typicalDevUsage?.callsPerDevHour && manifest.typicalDevUsage.callsPerDevHour > 0) {
|
|
565
|
+
baseMonthlyRuns = 132;
|
|
566
|
+
} else {
|
|
567
|
+
baseMonthlyRuns = 50;
|
|
568
|
+
}
|
|
393
569
|
const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;
|
|
394
|
-
const gotcha = GOTCHA_MULTIPLIERS[serviceId];
|
|
395
|
-
const unitRate = service.pricing?.unitRate ?? 0;
|
|
396
570
|
let costLow;
|
|
397
571
|
let costHigh;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
costLow = 0;
|
|
403
|
-
costHigh = 0;
|
|
404
|
-
} else {
|
|
405
|
-
const typicalCallCosts = {
|
|
406
|
-
anthropic: 3e-3,
|
|
407
|
-
// ~$3/MTok * ~1K tokens average
|
|
408
|
-
openai: 2e-3,
|
|
409
|
-
"google-gemini": 1e-3,
|
|
410
|
-
scrapfly: 15e-5,
|
|
411
|
-
browserbase: 0.01,
|
|
412
|
-
resend: 1e-3,
|
|
413
|
-
stripe: 0.3
|
|
414
|
-
};
|
|
415
|
-
const perCall = typicalCallCosts[serviceId] ?? 1e-3;
|
|
572
|
+
let rangeExplanation;
|
|
573
|
+
if (manifest) {
|
|
574
|
+
const { perCall } = computeManifestPerCallCost(manifest, content);
|
|
575
|
+
const gotcha = detectManifestMultipliers(manifest, content);
|
|
416
576
|
costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
|
|
417
577
|
costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
|
|
578
|
+
rangeExplanation = gotcha?.explanation;
|
|
579
|
+
if (!gotcha && manifest.costMultipliers && manifest.costMultipliers.length > 0) {
|
|
580
|
+
const maxFactor = Math.max(...manifest.costMultipliers.map((m) => m.factor));
|
|
581
|
+
if (maxFactor > 1) {
|
|
582
|
+
costHigh = monthlyInvocations * perCall * maxFactor;
|
|
583
|
+
rangeExplanation = manifest.costMultipliers.map((m) => m.description).join("; ");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
const gotcha = LEGACY_GOTCHA_MULTIPLIERS[serviceId];
|
|
588
|
+
const unitRate = service.pricing?.unitRate ?? 0;
|
|
589
|
+
if (unitRate > 0) {
|
|
590
|
+
costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
|
|
591
|
+
costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
|
|
592
|
+
} else if (service.pricing?.monthlyBase !== void 0 && service.pricing.monthlyBase > 0) {
|
|
593
|
+
costLow = 0;
|
|
594
|
+
costHigh = 0;
|
|
595
|
+
} else {
|
|
596
|
+
const perCall = LEGACY_CALL_COSTS[serviceId] ?? 1e-3;
|
|
597
|
+
costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
|
|
598
|
+
costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
|
|
599
|
+
}
|
|
600
|
+
rangeExplanation = gotcha?.explanation;
|
|
418
601
|
}
|
|
419
602
|
if (costLow === 0 && costHigh === 0) continue;
|
|
420
603
|
impacts.push({
|
|
@@ -427,7 +610,7 @@ function analyzeCostImpact(filePath, content, projectRoot) {
|
|
|
427
610
|
monthlyInvocations,
|
|
428
611
|
costLow,
|
|
429
612
|
costHigh,
|
|
430
|
-
rangeExplanation
|
|
613
|
+
rangeExplanation
|
|
431
614
|
});
|
|
432
615
|
}
|
|
433
616
|
return impacts;
|
|
@@ -464,14 +647,14 @@ function formatCostImpactCard(impacts, currentBudgets) {
|
|
|
464
647
|
}
|
|
465
648
|
|
|
466
649
|
// src/utilization.ts
|
|
467
|
-
import * as
|
|
468
|
-
import * as
|
|
650
|
+
import * as fs6 from "fs";
|
|
651
|
+
import * as path6 from "path";
|
|
469
652
|
function utilizationModelPath(projectRoot) {
|
|
470
|
-
return
|
|
653
|
+
return path6.join(projectDataDir(projectRoot), "utilization.json");
|
|
471
654
|
}
|
|
472
655
|
function readUtilizationModel(projectRoot) {
|
|
473
656
|
try {
|
|
474
|
-
const raw =
|
|
657
|
+
const raw = fs6.readFileSync(utilizationModelPath(projectRoot), "utf-8");
|
|
475
658
|
return JSON.parse(raw);
|
|
476
659
|
} catch {
|
|
477
660
|
return {
|
|
@@ -484,9 +667,9 @@ function readUtilizationModel(projectRoot) {
|
|
|
484
667
|
}
|
|
485
668
|
function writeUtilizationModel(model, projectRoot) {
|
|
486
669
|
const dir = projectDataDir(projectRoot);
|
|
487
|
-
|
|
670
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
488
671
|
model.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
489
|
-
|
|
672
|
+
fs6.writeFileSync(
|
|
490
673
|
utilizationModelPath(projectRoot),
|
|
491
674
|
JSON.stringify(model, null, 2) + "\n",
|
|
492
675
|
"utf-8"
|
|
@@ -559,20 +742,20 @@ function recalculateServiceTotals(model, serviceId) {
|
|
|
559
742
|
|
|
560
743
|
// src/hooks/on-file-change.ts
|
|
561
744
|
function sessionImpactPath(projectRoot, sessionId) {
|
|
562
|
-
return
|
|
745
|
+
return path7.join(projectDataDir(projectRoot), "cache", `session-impact-${sessionId}.json`);
|
|
563
746
|
}
|
|
564
747
|
function readSessionImpacts(projectRoot, sessionId) {
|
|
565
748
|
try {
|
|
566
|
-
const raw =
|
|
749
|
+
const raw = fs7.readFileSync(sessionImpactPath(projectRoot, sessionId), "utf-8");
|
|
567
750
|
return JSON.parse(raw);
|
|
568
751
|
} catch {
|
|
569
752
|
return {};
|
|
570
753
|
}
|
|
571
754
|
}
|
|
572
755
|
function writeSessionImpacts(projectRoot, sessionId, impacts) {
|
|
573
|
-
const dir =
|
|
574
|
-
|
|
575
|
-
|
|
756
|
+
const dir = path7.join(projectDataDir(projectRoot), "cache");
|
|
757
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
758
|
+
fs7.writeFileSync(
|
|
576
759
|
sessionImpactPath(projectRoot, sessionId),
|
|
577
760
|
JSON.stringify(impacts, null, 2) + "\n",
|
|
578
761
|
"utf-8"
|
|
@@ -581,7 +764,7 @@ function writeSessionImpacts(projectRoot, sessionId, impacts) {
|
|
|
581
764
|
function main() {
|
|
582
765
|
let input;
|
|
583
766
|
try {
|
|
584
|
-
const stdin =
|
|
767
|
+
const stdin = fs7.readFileSync(0, "utf-8");
|
|
585
768
|
input = JSON.parse(stdin);
|
|
586
769
|
} catch {
|
|
587
770
|
process.exit(0);
|
|
@@ -599,7 +782,7 @@ function main() {
|
|
|
599
782
|
}
|
|
600
783
|
let content;
|
|
601
784
|
try {
|
|
602
|
-
content =
|
|
785
|
+
content = fs7.readFileSync(filePath, "utf-8");
|
|
603
786
|
} catch {
|
|
604
787
|
process.exit(0);
|
|
605
788
|
return;
|
|
@@ -692,7 +875,7 @@ function main() {
|
|
|
692
875
|
);
|
|
693
876
|
}
|
|
694
877
|
try {
|
|
695
|
-
const relPath =
|
|
878
|
+
const relPath = path7.relative(projectRoot, filePath);
|
|
696
879
|
const callSites = analyzeFileUtilization(filePath, content, projectRoot).map(
|
|
697
880
|
(cs) => ({ ...cs, filePath: relPath })
|
|
698
881
|
);
|