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,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/hooks/on-file-change.ts
4
- import * as fs6 from "fs";
5
- import * as path6 from "path";
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
- if (/for\s*\(.*;\s*\w+\s*<\s*(\w+)/g.test(content)) {
303
- const loopMatch = content.match(/for\s*\(.*;\s*\w+\s*<\s*(\d+)/);
304
- if (loopMatch) {
305
- const bound = parseInt(loopMatch[1]);
306
- if (bound > 1) {
307
- multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });
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
- multipliers.push({ label: "for loop (variable bound)", factor: 10 });
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
- multipliers.push({ label: ".map() iteration", factor: 10 });
315
- }
316
- if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
317
- multipliers.push({ label: ".forEach() iteration", factor: 10 });
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
- multipliers.push({ label: "for...of/in loop", factor: 10 });
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.push({ label: "Promise.all (parallel batch)", factor: 10 });
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+\d+\s*(min|hour|day|sec)/gi.test(content)) {
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
- var GOTCHA_MULTIPLIERS = {
346
- scrapfly: {
347
- low: 1,
348
- high: 25,
349
- explanation: "anti-bot bypass consumes 5-25x base credits"
350
- },
351
- browserbase: {
352
- low: 1,
353
- high: 5,
354
- explanation: "session duration affects cost \u2014 long sessions burn more"
355
- },
356
- anthropic: {
357
- low: 1,
358
- high: 60,
359
- explanation: "Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)"
360
- },
361
- openai: {
362
- low: 1,
363
- high: 30,
364
- explanation: "GPT-4 mini vs GPT-5 (30x cost range)"
365
- },
366
- stripe: {
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: 1.5,
369
- explanation: "international cards add 1-1.5% extra"
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 baseMonthlyRuns = multipliers.some((m) => m.label.startsWith("cron")) ? 1 : 50;
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
- if (unitRate > 0) {
399
- costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
400
- costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
401
- } else if (service.pricing?.monthlyBase !== void 0) {
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: gotcha?.explanation
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 fs5 from "fs";
468
- import * as path5 from "path";
650
+ import * as fs6 from "fs";
651
+ import * as path6 from "path";
469
652
  function utilizationModelPath(projectRoot) {
470
- return path5.join(projectDataDir(projectRoot), "utilization.json");
653
+ return path6.join(projectDataDir(projectRoot), "utilization.json");
471
654
  }
472
655
  function readUtilizationModel(projectRoot) {
473
656
  try {
474
- const raw = fs5.readFileSync(utilizationModelPath(projectRoot), "utf-8");
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
- fs5.mkdirSync(dir, { recursive: true });
670
+ fs6.mkdirSync(dir, { recursive: true });
488
671
  model.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
489
- fs5.writeFileSync(
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 path6.join(projectDataDir(projectRoot), "cache", `session-impact-${sessionId}.json`);
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 = fs6.readFileSync(sessionImpactPath(projectRoot, sessionId), "utf-8");
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 = path6.join(projectDataDir(projectRoot), "cache");
574
- fs6.mkdirSync(dir, { recursive: true });
575
- fs6.writeFileSync(
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 = fs6.readFileSync(0, "utf-8");
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 = fs6.readFileSync(filePath, "utf-8");
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 = path6.relative(projectRoot, filePath);
878
+ const relPath = path7.relative(projectRoot, filePath);
696
879
  const callSites = analyzeFileUtilization(filePath, content, projectRoot).map(
697
880
  (cs) => ({ ...cs, filePath: relPath })
698
881
  );