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
package/dist/cli.js CHANGED
@@ -10,14 +10,14 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/services/base.ts
13
- async function fetchJson(url2, options = {}) {
13
+ async function fetchJson(url3, options = {}) {
14
14
  try {
15
15
  const controller = new AbortController();
16
16
  const timeoutId = setTimeout(
17
17
  () => controller.abort(),
18
18
  options.timeout ?? 1e4
19
19
  );
20
- const response = await fetch(url2, {
20
+ const response = await fetch(url3, {
21
21
  method: options.method ?? "GET",
22
22
  headers: options.headers,
23
23
  body: options.body,
@@ -55,11 +55,19 @@ __export(probes_exports, {
55
55
  });
56
56
  function matchPlanByPrefix(detected, plans) {
57
57
  const lower = detected.toLowerCase();
58
- return plans.find((p) => {
58
+ const candidates = plans.filter((p) => {
59
59
  if (p.type === "exclude") return false;
60
60
  const firstWord = p.name.split(/[\s(]/)[0].toLowerCase();
61
61
  return lower.includes(firstWord) || firstWord.includes(lower);
62
62
  });
63
+ if (candidates.length <= 1) return candidates[0];
64
+ const dollarMatch = detected.match(/\$(\d+)/);
65
+ if (dollarMatch) {
66
+ const amount = parseInt(dollarMatch[1], 10);
67
+ const byAmount = candidates.find((p) => p.monthlyBase === amount);
68
+ if (byAmount) return byAmount;
69
+ }
70
+ return candidates[0];
63
71
  }
64
72
  async function probeService(serviceId, apiKey, plans) {
65
73
  const probe = PROBES.get(serviceId);
@@ -78,7 +86,7 @@ function formatK(n) {
78
86
  if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
79
87
  return String(n);
80
88
  }
81
- var probeScrapfly, probeAnthropic, probeOpenAI, probeVercel, probeSupabase, probeStripe, probeBrowserbase, probeUpstash, probePostHog, PROBES;
89
+ var probeScrapfly, probeAnthropic, probeOpenAI, probeVercel, probeSupabase, probeBrowserbase, probeUpstash, probePostHog, PROBES;
82
90
  var init_probes = __esm({
83
91
  "src/probes.ts"() {
84
92
  "use strict";
@@ -190,10 +198,21 @@ var init_probes = __esm({
190
198
  return null;
191
199
  };
192
200
  probeSupabase = async (apiKey, plans) => {
201
+ if (apiKey.startsWith("eyJ")) {
202
+ return {
203
+ summary: "Key is a service_role JWT \u2014 Supabase Management API requires a Personal Access Token (sbp_*) from supabase.com/dashboard \u2192 Account \u2192 Access Tokens",
204
+ confidence: "low"
205
+ };
206
+ }
193
207
  const orgsResult = await fetchJson("https://api.supabase.com/v1/organizations", {
194
208
  headers: { Authorization: `Bearer ${apiKey}` }
195
209
  });
196
- if (!orgsResult.ok || !orgsResult.data || !Array.isArray(orgsResult.data)) return null;
210
+ if (!orgsResult.ok || !orgsResult.data || !Array.isArray(orgsResult.data)) {
211
+ return {
212
+ summary: "Supabase API call failed \u2014 ensure key is a Personal Access Token (sbp_*), not a service_role key",
213
+ confidence: "low"
214
+ };
215
+ }
197
216
  const org = orgsResult.data[0];
198
217
  if (!org) return null;
199
218
  const planName = org.billing?.plan;
@@ -211,21 +230,6 @@ var init_probes = __esm({
211
230
  confidence: "low"
212
231
  };
213
232
  };
214
- probeStripe = async (apiKey, _plans) => {
215
- const result = await fetchJson("https://api.stripe.com/v1/balance", {
216
- headers: { Authorization: `Bearer ${apiKey}` }
217
- });
218
- if (!result.ok || !result.data) return null;
219
- const available = result.data.available?.[0];
220
- const pending = result.data.pending?.[0];
221
- const totalCents = (available?.amount ?? 0) + (pending?.amount ?? 0);
222
- const currency = (available?.currency ?? "usd").toUpperCase();
223
- return {
224
- usage: { spend: totalCents / 100, currency },
225
- summary: `Balance: ${currency} ${(totalCents / 100).toFixed(2)} (${((available?.amount ?? 0) / 100).toFixed(2)} available)`,
226
- confidence: "medium"
227
- };
228
- };
229
233
  probeBrowserbase = async (apiKey, _plans) => {
230
234
  const projResult = await fetchJson("https://api.browserbase.com/v1/projects", {
231
235
  headers: { "X-BB-API-Key": apiKey }
@@ -278,7 +282,6 @@ var init_probes = __esm({
278
282
  ["openai", probeOpenAI],
279
283
  ["vercel", probeVercel],
280
284
  ["supabase", probeSupabase],
281
- ["stripe", probeStripe],
282
285
  ["browserbase", probeBrowserbase],
283
286
  ["upstash", probeUpstash],
284
287
  ["posthog", probePostHog]
@@ -287,8 +290,8 @@ var init_probes = __esm({
287
290
  });
288
291
 
289
292
  // src/cli.ts
290
- import * as fs7 from "fs";
291
- import * as path7 from "path";
293
+ import * as fs8 from "fs";
294
+ import * as path8 from "path";
292
295
 
293
296
  // src/core/config.ts
294
297
  import * as fs from "fs";
@@ -638,8 +641,8 @@ var anthropicConnector = {
638
641
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
639
642
  const startDate = startOfMonth.toISOString().split("T")[0];
640
643
  const endDate = now.toISOString().split("T")[0];
641
- const url2 = `https://api.anthropic.com/v1/organizations/usage?start_date=${startDate}&end_date=${endDate}`;
642
- const result = await fetchJson(url2, {
644
+ const url3 = `https://api.anthropic.com/v1/organizations/usage?start_date=${startDate}&end_date=${endDate}`;
645
+ const result = await fetchJson(url3, {
643
646
  headers: {
644
647
  "x-api-key": apiKey,
645
648
  "anthropic-version": "2023-06-01"
@@ -681,8 +684,8 @@ var openaiConnector = {
681
684
  const now = /* @__PURE__ */ new Date();
682
685
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
683
686
  const startTime = Math.floor(startOfMonth.getTime() / 1e3);
684
- const url2 = `https://api.openai.com/v1/organization/costs?start_time=${startTime}`;
685
- const result = await fetchJson(url2, {
687
+ const url3 = `https://api.openai.com/v1/organization/costs?start_time=${startTime}`;
688
+ const result = await fetchJson(url3, {
686
689
  headers: {
687
690
  Authorization: `Bearer ${apiKey}`
688
691
  }
@@ -728,8 +731,8 @@ var vercelConnector = {
728
731
  async fetchSpend(token, options) {
729
732
  const teamId = options?.["teamId"] ?? "";
730
733
  const teamParam = teamId ? `?teamId=${teamId}` : "";
731
- const url2 = `https://api.vercel.com/v2/usage${teamParam}`;
732
- const result = await fetchJson(url2, {
734
+ const url3 = `https://api.vercel.com/v2/usage${teamParam}`;
735
+ const result = await fetchJson(url3, {
733
736
  headers: {
734
737
  Authorization: `Bearer ${token}`
735
738
  }
@@ -767,8 +770,8 @@ init_base();
767
770
  var scrapflyConnector = {
768
771
  serviceId: "scrapfly",
769
772
  async fetchSpend(apiKey) {
770
- const url2 = `https://api.scrapfly.io/account?key=${apiKey}`;
771
- const result = await fetchJson(url2);
773
+ const url3 = `https://api.scrapfly.io/account?key=${apiKey}`;
774
+ const result = await fetchJson(url3);
772
775
  if (!result.ok || !result.data) {
773
776
  return {
774
777
  serviceId: "scrapfly",
@@ -812,6 +815,15 @@ init_base();
812
815
  var supabaseConnector = {
813
816
  serviceId: "supabase",
814
817
  async fetchSpend(token) {
818
+ if (token.startsWith("eyJ")) {
819
+ return {
820
+ serviceId: "supabase",
821
+ spend: 0,
822
+ isEstimate: true,
823
+ tier: "est",
824
+ error: "Key is a service_role JWT \u2014 needs a Personal Access Token (sbp_*) from supabase.com/dashboard \u2192 Account \u2192 Access Tokens"
825
+ };
826
+ }
815
827
  const orgsResult = await fetchJson("https://api.supabase.com/v1/organizations", {
816
828
  headers: {
817
829
  Authorization: `Bearer ${token}`
@@ -928,9 +940,8 @@ var browserbaseConnector = {
928
940
  return {
929
941
  serviceId: "browserbase",
930
942
  spend,
931
- isEstimate: true,
932
- // rate may vary by plan
933
- tier: "est",
943
+ isEstimate: false,
944
+ tier: "live",
934
945
  unitsUsed: sessionCount,
935
946
  unitName: "sessions",
936
947
  raw: {
@@ -959,76 +970,79 @@ async function pollService(tracked) {
959
970
  const connector = connectors.get(tracked.serviceId);
960
971
  const definition = getService(tracked.serviceId);
961
972
  let effectivePlanCost = tracked.planCost;
962
- if (effectivePlanCost === void 0 && tracked.planName && definition?.plans) {
973
+ let isFlatPlan = false;
974
+ if (tracked.planName && definition?.plans) {
963
975
  const matchedPlan = definition.plans.find(
964
976
  (p) => p.name === tracked.planName || p.name.toLowerCase().includes((tracked.planName ?? "").toLowerCase())
965
977
  );
966
- if (matchedPlan?.monthlyBase !== void 0) {
967
- effectivePlanCost = matchedPlan.monthlyBase;
978
+ if (matchedPlan) {
979
+ isFlatPlan = matchedPlan.type === "flat";
980
+ if (effectivePlanCost === void 0 && matchedPlan.monthlyBase !== void 0) {
981
+ effectivePlanCost = matchedPlan.monthlyBase;
982
+ }
968
983
  }
969
984
  }
985
+ const calcSpend = (cost) => {
986
+ if (isFlatPlan) return cost;
987
+ const now = /* @__PURE__ */ new Date();
988
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
989
+ return cost / daysInMonth * now.getDate();
990
+ };
970
991
  if (connector && serviceConfig?.apiKey) {
971
992
  try {
972
993
  const result = await connector.fetchSpend(
973
994
  serviceConfig.apiKey,
974
995
  serviceConfig
975
996
  );
976
- if (!result.error) return result;
977
- const fallbackSpend = effectivePlanCost !== void 0 ? (() => {
978
- const now = /* @__PURE__ */ new Date();
979
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
980
- return effectivePlanCost / daysInMonth * now.getDate();
981
- })() : 0;
997
+ if (!result.error) {
998
+ if (isFlatPlan && effectivePlanCost !== void 0) {
999
+ if (result.spend < effectivePlanCost) {
1000
+ result.spend = effectivePlanCost;
1001
+ }
1002
+ result.isEstimate = false;
1003
+ }
1004
+ result.isFlatPlan = isFlatPlan;
1005
+ return result;
1006
+ }
1007
+ const fallbackSpend = effectivePlanCost !== void 0 ? calcSpend(effectivePlanCost) : 0;
982
1008
  return {
983
1009
  serviceId: tracked.serviceId,
984
1010
  spend: fallbackSpend,
985
- isEstimate: true,
1011
+ isEstimate: isFlatPlan ? false : true,
986
1012
  tier: effectivePlanCost !== void 0 ? "calc" : "blind",
1013
+ isFlatPlan,
987
1014
  error: `LIVE failed (${result.error}) \u2014 showing ${effectivePlanCost !== void 0 ? "CALC" : "BLIND"} fallback`
988
1015
  };
989
1016
  } catch (err) {
990
- const fallbackSpend = effectivePlanCost !== void 0 ? (() => {
991
- const now = /* @__PURE__ */ new Date();
992
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
993
- return effectivePlanCost / daysInMonth * now.getDate();
994
- })() : 0;
1017
+ const fallbackSpend = effectivePlanCost !== void 0 ? calcSpend(effectivePlanCost) : 0;
995
1018
  return {
996
1019
  serviceId: tracked.serviceId,
997
1020
  spend: fallbackSpend,
998
- isEstimate: true,
1021
+ isEstimate: isFlatPlan ? false : true,
999
1022
  tier: effectivePlanCost !== void 0 ? "calc" : "blind",
1023
+ isFlatPlan,
1000
1024
  error: `LIVE failed (${err instanceof Error ? err.message : "unknown"}) \u2014 showing ${effectivePlanCost !== void 0 ? "CALC" : "BLIND"} fallback`
1001
1025
  };
1002
1026
  }
1003
1027
  }
1004
1028
  if (connector && tracked.hasApiKey && !serviceConfig?.apiKey) {
1005
- const projectedSpend = effectivePlanCost !== void 0 ? (() => {
1006
- const now = /* @__PURE__ */ new Date();
1007
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1008
- return effectivePlanCost / daysInMonth * now.getDate();
1009
- })() : 0;
1029
+ const projectedSpend = effectivePlanCost !== void 0 ? calcSpend(effectivePlanCost) : 0;
1010
1030
  return {
1011
1031
  serviceId: tracked.serviceId,
1012
1032
  spend: projectedSpend,
1013
- isEstimate: true,
1033
+ isEstimate: isFlatPlan ? false : true,
1014
1034
  tier: effectivePlanCost !== void 0 ? "calc" : "blind",
1035
+ isFlatPlan,
1015
1036
  error: "API key marked as configured but not found in ~/.config/burnwatch/ \u2014 re-run configure with --key"
1016
1037
  };
1017
1038
  }
1018
1039
  if (effectivePlanCost !== void 0) {
1019
- const now = /* @__PURE__ */ new Date();
1020
- const daysInMonth = new Date(
1021
- now.getFullYear(),
1022
- now.getMonth() + 1,
1023
- 0
1024
- ).getDate();
1025
- const dayOfMonth = now.getDate();
1026
- const projectedSpend = effectivePlanCost / daysInMonth * dayOfMonth;
1027
1040
  return {
1028
1041
  serviceId: tracked.serviceId,
1029
- spend: projectedSpend,
1030
- isEstimate: true,
1031
- tier: "calc"
1042
+ spend: calcSpend(effectivePlanCost),
1043
+ isEstimate: !isFlatPlan,
1044
+ tier: "calc",
1045
+ isFlatPlan
1032
1046
  };
1033
1047
  }
1034
1048
  if (definition) {
@@ -1072,52 +1086,37 @@ var CONFIDENCE_BADGES = {
1072
1086
  // src/core/brief.ts
1073
1087
  function formatBrief(brief) {
1074
1088
  const lines = [];
1075
- const width = 62;
1076
- const hrDouble = "\u2550".repeat(width);
1077
- const hrSingle = "\u2500".repeat(width - 4);
1078
- lines.push(`\u2554${hrDouble}\u2557`);
1079
- lines.push(
1080
- `\u2551 BURNWATCH \u2014 ${brief.projectName} \u2014 ${brief.period}`.padEnd(
1081
- width + 1
1082
- ) + "\u2551"
1083
- );
1084
- lines.push(`\u2560${hrDouble}\u2563`);
1085
- lines.push(
1086
- formatRow("Service", "Spend", "Conf", "Budget", "Left", width)
1087
- );
1088
- lines.push(`\u2551 ${hrSingle} \u2551`);
1089
+ const hr = "\u2550".repeat(62);
1090
+ const hrThin = "\u2500".repeat(58);
1091
+ lines.push(`\u2554${hr}`);
1092
+ lines.push(`\u2551 BURNWATCH \u2014 ${brief.projectName} \u2014 ${brief.period}`);
1093
+ lines.push(`\u2560${hr}`);
1094
+ lines.push(formatRow("Service", "Spend", "Conf", "Budget", "Left"));
1095
+ lines.push(`\u2551 ${hrThin}`);
1089
1096
  for (const svc of brief.services.filter((s) => s.tier !== "excluded")) {
1090
1097
  const spendStr = svc.tier === "blind" && svc.spend === 0 ? "\u2014" : svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
1091
1098
  const badge = CONFIDENCE_BADGES[svc.tier];
1092
1099
  const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
1093
1100
  const leftStr = formatLeft(svc);
1094
- lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));
1101
+ lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr));
1095
1102
  if (svc.allowance) {
1096
1103
  const usedStr = formatCompact(svc.allowance.used);
1097
1104
  const totalStr2 = formatCompact(svc.allowance.included);
1098
1105
  const pctStr = svc.allowance.percent.toFixed(0);
1099
1106
  const warn = svc.allowance.percent >= 75 ? " \u26A0\uFE0F" : "";
1100
- lines.push(
1101
- `\u2551 \u21B3 ${usedStr}/${totalStr2} ${svc.allowance.unitName} (${pctStr}%)${warn}`.padEnd(width + 1) + "\u2551"
1102
- );
1107
+ lines.push(`\u2551 \u21B3 ${usedStr}/${totalStr2} ${svc.allowance.unitName} (${pctStr}%)${warn}`);
1103
1108
  }
1104
1109
  }
1105
- lines.push(`\u2560${hrDouble}\u2563`);
1110
+ lines.push(`\u2560${hr}`);
1106
1111
  const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
1107
1112
  const marginStr = brief.estimateMargin > 0 ? ` Est margin: \xB1$${brief.estimateMargin.toFixed(0)}` : "";
1108
1113
  const untrackedStr = brief.untrackedCount > 0 ? `No billing data: ${brief.untrackedCount} \u26A0\uFE0F` : `All tracked \u2705`;
1109
- lines.push(
1110
- `\u2551 TOTAL: ${totalStr} ${untrackedStr}${marginStr}`.padEnd(
1111
- width + 1
1112
- ) + "\u2551"
1113
- );
1114
+ lines.push(`\u2551 TOTAL: ${totalStr} ${untrackedStr}${marginStr}`);
1114
1115
  for (const alert of brief.alerts) {
1115
1116
  const icon = alert.severity === "critical" ? "\u{1F6A8}" : "\u26A0\uFE0F";
1116
- lines.push(
1117
- `\u2551 ${icon} ${alert.message}`.padEnd(width + 1) + "\u2551"
1118
- );
1117
+ lines.push(`\u2551 ${icon} ${alert.message}`);
1119
1118
  }
1120
- lines.push(`\u255A${hrDouble}\u255D`);
1119
+ lines.push(`\u255A${hr}`);
1121
1120
  return lines.join("\n");
1122
1121
  }
1123
1122
  function buildBrief(projectName, snapshots, blindCount) {
@@ -1144,12 +1143,14 @@ function buildBrief(projectName, snapshots, blindCount) {
1144
1143
  severity: "critical"
1145
1144
  });
1146
1145
  } else if (snap.status === "caution" && snap.budgetPercent && snap.budgetPercent >= 80) {
1147
- alerts.push({
1148
- serviceId: snap.serviceId,
1149
- type: "near_budget",
1150
- message: `${snap.serviceId} at ${snap.budgetPercent.toFixed(0)}% of budget`,
1151
- severity: "warning"
1152
- });
1146
+ if (!(snap.isFlatPlan && snap.budgetPercent >= 99.5 && snap.budgetPercent <= 100.5)) {
1147
+ alerts.push({
1148
+ serviceId: snap.serviceId,
1149
+ type: "near_budget",
1150
+ message: `${snap.serviceId} at ${snap.budgetPercent.toFixed(0)}% of budget`,
1151
+ severity: "warning"
1152
+ });
1153
+ }
1153
1154
  }
1154
1155
  }
1155
1156
  if (blindCount > 0) {
@@ -1172,30 +1173,32 @@ function buildBrief(projectName, snapshots, blindCount) {
1172
1173
  alerts
1173
1174
  };
1174
1175
  }
1175
- function formatRow(service, spend, conf, budget, left, width) {
1176
- const row = ` ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(7)} ${budget.padEnd(7)} ${left}`;
1177
- return `\u2551${row}`.padEnd(width + 1) + "\u2551";
1176
+ function formatRow(service, spend, conf, budget, left) {
1177
+ return `\u2551 ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(9)} ${budget.padEnd(7)} ${left}`;
1178
1178
  }
1179
1179
  function formatLeft(snap) {
1180
1180
  if (!snap.budget) return "\u2014";
1181
- if (snap.status === "over") return "\u26A0\uFE0F OVR";
1181
+ if (snap.status === "over") return "\u26A0\uFE0F OVER";
1182
1182
  if (snap.budgetPercent !== void 0) {
1183
1183
  const remaining = 100 - snap.budgetPercent;
1184
1184
  return `${remaining.toFixed(0)}%`;
1185
1185
  }
1186
1186
  return "\u2014";
1187
1187
  }
1188
- function buildSnapshot(serviceId, tier, spend, budget, allowanceData) {
1188
+ function buildSnapshot(serviceId, tier, spend, budget, allowanceData, isEstimateOverride, isFlatPlan) {
1189
1189
  if (isNaN(spend) || !isFinite(spend)) spend = 0;
1190
1190
  if (budget !== void 0 && (isNaN(budget) || !isFinite(budget))) budget = void 0;
1191
- const isEstimate = tier === "est" || tier === "calc";
1191
+ const isEstimate = isEstimateOverride ?? (tier === "est" || tier === "calc");
1192
1192
  const budgetPercent = budget && budget > 0 ? spend / budget * 100 : void 0;
1193
1193
  let status = "unknown";
1194
1194
  let statusLabel = tier === "blind" ? "needs API key" : "no budget";
1195
1195
  if (budget) {
1196
1196
  if (budgetPercent > 100) {
1197
1197
  status = "over";
1198
- statusLabel = `\u26A0\uFE0F ${budgetPercent.toFixed(0)}% over`;
1198
+ statusLabel = `${budgetPercent.toFixed(0)}% over`;
1199
+ } else if (isFlatPlan && budgetPercent >= 99.5) {
1200
+ status = "healthy";
1201
+ statusLabel = "flat \u2014 on plan";
1199
1202
  } else if (budgetPercent >= 75) {
1200
1203
  status = "caution";
1201
1204
  statusLabel = `${(100 - budgetPercent).toFixed(0)}% \u2014 caution`;
@@ -1231,6 +1234,7 @@ function buildSnapshot(serviceId, tier, spend, budget, allowanceData) {
1231
1234
  budgetPercent,
1232
1235
  status,
1233
1236
  statusLabel,
1237
+ isFlatPlan,
1234
1238
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1235
1239
  allowance
1236
1240
  };
@@ -1324,7 +1328,6 @@ var API_KEY_HINTS = {
1324
1328
  openai: "Org key: platform.openai.com -> Settings -> API Keys",
1325
1329
  vercel: "Token: vercel.com/account/tokens",
1326
1330
  supabase: "Service role key: supabase.com/dashboard -> Settings -> API",
1327
- stripe: "Secret key: dashboard.stripe.com -> Developers -> API Keys",
1328
1331
  scrapfly: "API key: scrapfly.io/dashboard"
1329
1332
  };
1330
1333
  function classifyRisk(service) {
@@ -1346,9 +1349,9 @@ function groupByRisk(detected) {
1346
1349
  return groups;
1347
1350
  }
1348
1351
  function ask(rl, question) {
1349
- return new Promise((resolve3) => {
1352
+ return new Promise((resolve4) => {
1350
1353
  rl.question(question, (answer) => {
1351
- resolve3(answer.trim());
1354
+ resolve4(answer.trim());
1352
1355
  });
1353
1356
  });
1354
1357
  }
@@ -1648,10 +1651,41 @@ async function runInteractiveInit(detected) {
1648
1651
  }
1649
1652
 
1650
1653
  // src/utilization.ts
1651
- import * as fs6 from "fs";
1652
- import * as path6 from "path";
1654
+ import * as fs7 from "fs";
1655
+ import * as path7 from "path";
1653
1656
 
1654
1657
  // src/cost-impact.ts
1658
+ import * as fs6 from "fs";
1659
+ import * as path6 from "path";
1660
+ import * as url2 from "url";
1661
+ var __dirname2 = path6.dirname(url2.fileURLToPath(import.meta.url));
1662
+ var manifestCache = null;
1663
+ function loadBillingManifests() {
1664
+ if (manifestCache) return manifestCache;
1665
+ manifestCache = /* @__PURE__ */ new Map();
1666
+ const candidates = [
1667
+ path6.resolve(__dirname2, "../billing"),
1668
+ // from src/ during dev
1669
+ path6.resolve(__dirname2, "../../billing")
1670
+ // from dist/
1671
+ ];
1672
+ for (const dir of candidates) {
1673
+ if (!fs6.existsSync(dir)) continue;
1674
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "billing.schema.json");
1675
+ for (const file of files) {
1676
+ try {
1677
+ const raw = fs6.readFileSync(path6.join(dir, file), "utf-8");
1678
+ const manifest = JSON.parse(raw);
1679
+ if (manifest.serviceId) {
1680
+ manifestCache.set(manifest.serviceId, manifest);
1681
+ }
1682
+ } catch {
1683
+ }
1684
+ }
1685
+ break;
1686
+ }
1687
+ return manifestCache;
1688
+ }
1655
1689
  var SERVICE_CALL_PATTERNS = {
1656
1690
  anthropic: [
1657
1691
  /\.messages\.create\s*\(/g,
@@ -1697,11 +1731,6 @@ var SERVICE_CALL_PATTERNS = {
1697
1731
  /resend\.emails\.send\s*\(/g,
1698
1732
  /\.emails\.send\s*\(/g
1699
1733
  ],
1700
- stripe: [
1701
- /stripe\.charges\.create\s*\(/g,
1702
- /stripe\.paymentIntents\.create\s*\(/g,
1703
- /stripe\.checkout\.sessions\.create\s*\(/g
1704
- ],
1705
1734
  supabase: [
1706
1735
  /supabase\.from\s*\(/g,
1707
1736
  /\.rpc\s*\(/g,
@@ -1715,11 +1744,6 @@ var SERVICE_CALL_PATTERNS = {
1715
1744
  /posthog\.capture\s*\(/g,
1716
1745
  /\.capture\s*\(/g
1717
1746
  ],
1718
- aws: [
1719
- /\.send\s*\(new\s+\w+Command/g,
1720
- /s3Client\.send\s*\(/g,
1721
- /lambdaClient\.send\s*\(/g
1722
- ],
1723
1747
  firebase: [
1724
1748
  /firestore\.\w+\(\s*["']/g,
1725
1749
  /\.collection\s*\(/g,
@@ -1749,40 +1773,134 @@ var SERVICE_CALL_PATTERNS = {
1749
1773
  replicate: [
1750
1774
  /replicate\.run\s*\(/g,
1751
1775
  /replicate\.predictions\.create\s*\(/g
1776
+ ],
1777
+ vercel: [
1778
+ /\.functions\./g,
1779
+ /edge\s+function/gi
1752
1780
  ]
1753
1781
  };
1782
+ function extractNumericContext(content) {
1783
+ const ctx = /* @__PURE__ */ new Map();
1784
+ const assignRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(\d+)\s*[;,\n]/g;
1785
+ let m;
1786
+ while ((m = assignRegex.exec(content)) !== null) {
1787
+ ctx.set(m[1], parseInt(m[2], 10));
1788
+ }
1789
+ const arrayCtorRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:new\s+)?Array\s*\(\s*(\d+)\s*\)/g;
1790
+ while ((m = arrayCtorRegex.exec(content)) !== null) {
1791
+ ctx.set(m[1], parseInt(m[2], 10));
1792
+ }
1793
+ const arrayLitRegex = /(?:const|let|var)\s+(\w+)\s*=\s*\[([^\]]*)\]/g;
1794
+ while ((m = arrayLitRegex.exec(content)) !== null) {
1795
+ const elements = m[2].split(",").filter((e) => e.trim().length > 0);
1796
+ if (elements.length > 1) {
1797
+ ctx.set(m[1], elements.length);
1798
+ }
1799
+ }
1800
+ return ctx;
1801
+ }
1802
+ function resolveLoopBound(varName, content, ctx) {
1803
+ const num = parseInt(varName, 10);
1804
+ if (!isNaN(num) && num > 0) return num;
1805
+ if (ctx.has(varName)) return ctx.get(varName);
1806
+ const lengthMatch = varName.match(/^(\w+)\.length$/);
1807
+ if (lengthMatch && ctx.has(lengthMatch[1])) {
1808
+ return ctx.get(lengthMatch[1]);
1809
+ }
1810
+ return null;
1811
+ }
1812
+ function resolveIterableSize(content, ctx) {
1813
+ const forOfMatch = content.match(/for\s*\(\s*(?:const|let|var)\s+\w+\s+of\s+(\w+)/);
1814
+ if (forOfMatch) {
1815
+ const resolved = ctx.get(forOfMatch[1]);
1816
+ if (resolved) return resolved;
1817
+ }
1818
+ const iterMatch = content.match(/(\w+)\s*\.\s*(?:map|forEach)\s*\(/);
1819
+ if (iterMatch) {
1820
+ const resolved = ctx.get(iterMatch[1]);
1821
+ if (resolved) return resolved;
1822
+ }
1823
+ return null;
1824
+ }
1754
1825
  function detectMultipliers(content) {
1755
1826
  const multipliers = [];
1756
- if (/for\s*\(.*;\s*\w+\s*<\s*(\w+)/g.test(content)) {
1757
- const loopMatch = content.match(/for\s*\(.*;\s*\w+\s*<\s*(\d+)/);
1758
- if (loopMatch) {
1759
- const bound = parseInt(loopMatch[1]);
1760
- if (bound > 1) {
1761
- multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });
1762
- }
1827
+ const ctx = extractNumericContext(content);
1828
+ const forLoopRegex = /for\s*\(.*;\s*\w+\s*(?:<|<=)\s*([\w.]+)/g;
1829
+ const forLoopMatch = forLoopRegex.exec(content);
1830
+ if (forLoopMatch) {
1831
+ const boundExpr = forLoopMatch[1];
1832
+ const resolved = resolveLoopBound(boundExpr, content, ctx);
1833
+ if (resolved && resolved > 1) {
1834
+ multipliers.push({ label: `for loop (${resolved} iterations)`, factor: resolved });
1763
1835
  } else {
1764
- multipliers.push({ label: "for loop (variable bound)", factor: 10 });
1836
+ const hintMatch = content.match(new RegExp(`(?:const|let|var)\\s+${boundExpr}\\s*=\\s*(\\w+)\\.length`));
1837
+ if (hintMatch) {
1838
+ const arrayName = hintMatch[1];
1839
+ const arraySize = ctx.get(arrayName);
1840
+ if (arraySize && arraySize > 1) {
1841
+ multipliers.push({ label: `for loop (${arraySize} iterations via ${arrayName}.length)`, factor: arraySize });
1842
+ } else {
1843
+ multipliers.push({ label: `for loop (${arrayName}.length \u2014 variable bound)`, factor: 10 });
1844
+ }
1845
+ } else {
1846
+ multipliers.push({ label: "for loop (variable bound)", factor: 10 });
1847
+ }
1765
1848
  }
1766
1849
  }
1767
1850
  if (/\.\s*map\s*\(\s*(async\s*)?\(/g.test(content)) {
1768
- multipliers.push({ label: ".map() iteration", factor: 10 });
1769
- }
1770
- if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
1771
- multipliers.push({ label: ".forEach() iteration", factor: 10 });
1851
+ const size = resolveIterableSize(content, ctx);
1852
+ if (size && size > 1) {
1853
+ multipliers.push({ label: `.map() over ${size} items`, factor: size });
1854
+ } else {
1855
+ multipliers.push({ label: ".map() iteration", factor: 10 });
1856
+ }
1857
+ } else if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
1858
+ const size = resolveIterableSize(content, ctx);
1859
+ if (size && size > 1) {
1860
+ multipliers.push({ label: `.forEach() over ${size} items`, factor: size });
1861
+ } else {
1862
+ multipliers.push({ label: ".forEach() iteration", factor: 10 });
1863
+ }
1772
1864
  }
1773
1865
  if (/for\s*\(\s*(const|let|var)\s+\w+\s+(of|in)\s+/g.test(content)) {
1774
- multipliers.push({ label: "for...of/in loop", factor: 10 });
1866
+ const size = resolveIterableSize(content, ctx);
1867
+ if (size && size > 1) {
1868
+ multipliers.push({ label: `for...of over ${size} items`, factor: size });
1869
+ } else {
1870
+ multipliers.push({ label: "for...of/in loop", factor: 10 });
1871
+ }
1775
1872
  }
1776
1873
  if (/Promise\.all\s*\(/g.test(content)) {
1777
- multipliers.push({ label: "Promise.all (parallel batch)", factor: 10 });
1874
+ const hasMap = multipliers.some((m) => m.label.includes(".map()"));
1875
+ if (!hasMap) {
1876
+ multipliers.push({ label: "Promise.all (parallel batch)", factor: 10 });
1877
+ }
1778
1878
  }
1779
- if (/cron|schedule|interval|setInterval|every\s+\d+\s*(min|hour|day|sec)/gi.test(content)) {
1879
+ if (/cron|schedule|interval|setInterval|every\s+(?:\d+|other)\s*(min|hour|day|sec|week)/gi.test(content)) {
1780
1880
  if (/every\s+5\s*min/gi.test(content) || /\*\/5\s+\*\s+\*/g.test(content)) {
1781
1881
  multipliers.push({ label: "cron: every 5 minutes", factor: 8640 });
1882
+ } else if (/every\s+15\s*min/gi.test(content) || /\*\/15\s+\*\s+\*/g.test(content)) {
1883
+ multipliers.push({ label: "cron: every 15 minutes", factor: 2880 });
1884
+ } else if (/every\s+30\s*min/gi.test(content) || /\*\/30\s+\*\s+\*/g.test(content)) {
1885
+ multipliers.push({ label: "cron: every 30 minutes", factor: 1440 });
1782
1886
  } else if (/every\s+1?\s*hour/gi.test(content) || /0\s+\*\s+\*\s+\*/g.test(content)) {
1783
1887
  multipliers.push({ label: "cron: hourly", factor: 720 });
1888
+ } else if (/every\s+(\d+)\s*hours?/gi.test(content)) {
1889
+ const hoursMatch = content.match(/every\s+(\d+)\s*hours?/i);
1890
+ const hours = parseInt(hoursMatch[1], 10);
1891
+ const factor = Math.round(720 / hours);
1892
+ multipliers.push({ label: `cron: every ${hours} hours`, factor });
1893
+ } else if (/every\s+other\s+day|every\s+2\s*days?/gi.test(content) || /\*\/2\s+/g.test(content)) {
1894
+ multipliers.push({ label: "cron: every other day", factor: 15 });
1895
+ } else if (/every\s+(\d+)\s*days?/gi.test(content)) {
1896
+ const daysMatch = content.match(/every\s+(\d+)\s*days?/i);
1897
+ const days = parseInt(daysMatch[1], 10);
1898
+ const factor = Math.round(30 / days);
1899
+ multipliers.push({ label: `cron: every ${days} days`, factor });
1784
1900
  } else if (/every\s+1?\s*day/gi.test(content) || /0\s+0\s+\*\s+\*/g.test(content)) {
1785
1901
  multipliers.push({ label: "cron: daily", factor: 30 });
1902
+ } else if (/every\s+1?\s*week/gi.test(content) || /0\s+0\s+\*\s+\*\s+0/g.test(content)) {
1903
+ multipliers.push({ label: "cron: weekly", factor: 4 });
1786
1904
  } else {
1787
1905
  multipliers.push({ label: "scheduled execution", factor: 30 });
1788
1906
  }
@@ -1794,40 +1912,92 @@ function detectMultipliers(content) {
1794
1912
  multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });
1795
1913
  }
1796
1914
  }
1915
+ for (const [name, value] of ctx) {
1916
+ if (value >= 100 && /^(total|count|num|max|limit|size|pages|items|urls|batch)/i.test(name)) {
1917
+ const alreadyCovered = multipliers.some((m) => m.factor === value);
1918
+ if (!alreadyCovered) {
1919
+ multipliers.push({ label: `${name} = ${value}`, factor: value });
1920
+ }
1921
+ }
1922
+ }
1797
1923
  return multipliers;
1798
1924
  }
1799
- var GOTCHA_MULTIPLIERS = {
1800
- scrapfly: {
1801
- low: 1,
1802
- high: 25,
1803
- explanation: "anti-bot bypass consumes 5-25x base credits"
1804
- },
1805
- browserbase: {
1806
- low: 1,
1807
- high: 5,
1808
- explanation: "session duration affects cost \u2014 long sessions burn more"
1809
- },
1810
- anthropic: {
1811
- low: 1,
1812
- high: 60,
1813
- explanation: "Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)"
1814
- },
1815
- openai: {
1816
- low: 1,
1817
- high: 30,
1818
- explanation: "GPT-4 mini vs GPT-5 (30x cost range)"
1819
- },
1820
- stripe: {
1925
+ function detectVariant(dimension, content) {
1926
+ if (!dimension.variants || dimension.variants.length === 0) return void 0;
1927
+ for (const variant of dimension.variants) {
1928
+ if (!variant.codePatterns) continue;
1929
+ for (const pattern of variant.codePatterns) {
1930
+ try {
1931
+ if (new RegExp(pattern, "i").test(content)) {
1932
+ return variant;
1933
+ }
1934
+ } catch {
1935
+ }
1936
+ }
1937
+ }
1938
+ return dimension.variants.find((v) => v.isDefault);
1939
+ }
1940
+ function detectManifestMultipliers(manifest, content) {
1941
+ if (!manifest.costMultipliers || manifest.costMultipliers.length === 0) {
1942
+ return void 0;
1943
+ }
1944
+ let maxFactor = 1;
1945
+ const activeNames = [];
1946
+ for (const cm of manifest.costMultipliers) {
1947
+ if (!cm.codePatterns) continue;
1948
+ for (const pattern of cm.codePatterns) {
1949
+ try {
1950
+ if (new RegExp(pattern, "i").test(content)) {
1951
+ maxFactor = Math.max(maxFactor, cm.factor);
1952
+ activeNames.push(cm.name);
1953
+ break;
1954
+ }
1955
+ } catch {
1956
+ }
1957
+ }
1958
+ }
1959
+ if (activeNames.length === 0) return void 0;
1960
+ return {
1821
1961
  low: 1,
1822
- high: 1.5,
1823
- explanation: "international cards add 1-1.5% extra"
1962
+ high: maxFactor,
1963
+ explanation: activeNames.join(", ")
1964
+ };
1965
+ }
1966
+ function computeManifestPerCallCost(manifest, content) {
1967
+ let totalPerCall = 0;
1968
+ const parts = [];
1969
+ const unitsPerCall = manifest.typicalDevUsage?.unitsPerCall ?? 1;
1970
+ for (const dim of manifest.billingDimensions) {
1971
+ const variant = detectVariant(dim, content);
1972
+ const ratePerUnit = variant?.ratePerUnit ?? dim.ratePerUnit;
1973
+ const ratePer = variant?.ratePer ?? dim.ratePer ?? 1;
1974
+ const dimCostPerCall = unitsPerCall / ratePer * ratePerUnit;
1975
+ if (dimCostPerCall > 0) {
1976
+ totalPerCall += dimCostPerCall;
1977
+ const variantLabel = variant?.name ?? dim.name;
1978
+ parts.push(variantLabel);
1979
+ }
1824
1980
  }
1981
+ return {
1982
+ perCall: totalPerCall,
1983
+ explanation: parts.length > 0 ? parts.join(" + ") : manifest.name
1984
+ };
1985
+ }
1986
+ var LEGACY_GOTCHA_MULTIPLIERS = {};
1987
+ var LEGACY_CALL_COSTS = {
1988
+ firebase: 1e-4,
1989
+ twilio: 0.01,
1990
+ sendgrid: 1e-3,
1991
+ "mongodb-atlas": 1e-4,
1992
+ clerk: 0,
1993
+ replicate: 0.01
1825
1994
  };
1826
1995
  function analyzeCostImpact(filePath, content, projectRoot) {
1827
- if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {
1996
+ if (!/\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(filePath)) {
1828
1997
  return [];
1829
1998
  }
1830
1999
  const registry = loadRegistry(projectRoot);
2000
+ const manifests = loadBillingManifests();
1831
2001
  const impacts = [];
1832
2002
  const multipliers = detectMultipliers(content);
1833
2003
  for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {
@@ -1843,32 +2013,48 @@ function analyzeCostImpact(filePath, content, projectRoot) {
1843
2013
  const service = registry.get(serviceId);
1844
2014
  if (!service) continue;
1845
2015
  const multiplierFactor = multipliers.length > 0 ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1) : 1;
1846
- const baseMonthlyRuns = multipliers.some((m) => m.label.startsWith("cron")) ? 1 : 50;
2016
+ const manifest = manifests.get(serviceId);
2017
+ let baseMonthlyRuns;
2018
+ const isCron = multipliers.some((m) => m.label.startsWith("cron"));
2019
+ if (isCron) {
2020
+ baseMonthlyRuns = 1;
2021
+ } else if (manifest?.typicalDevUsage?.callsPerDevHour && manifest.typicalDevUsage.callsPerDevHour > 0) {
2022
+ baseMonthlyRuns = 132;
2023
+ } else {
2024
+ baseMonthlyRuns = 50;
2025
+ }
1847
2026
  const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;
1848
- const gotcha = GOTCHA_MULTIPLIERS[serviceId];
1849
- const unitRate = service.pricing?.unitRate ?? 0;
1850
2027
  let costLow;
1851
2028
  let costHigh;
1852
- if (unitRate > 0) {
1853
- costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
1854
- costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
1855
- } else if (service.pricing?.monthlyBase !== void 0) {
1856
- costLow = 0;
1857
- costHigh = 0;
1858
- } else {
1859
- const typicalCallCosts = {
1860
- anthropic: 3e-3,
1861
- // ~$3/MTok * ~1K tokens average
1862
- openai: 2e-3,
1863
- "google-gemini": 1e-3,
1864
- scrapfly: 15e-5,
1865
- browserbase: 0.01,
1866
- resend: 1e-3,
1867
- stripe: 0.3
1868
- };
1869
- const perCall = typicalCallCosts[serviceId] ?? 1e-3;
2029
+ let rangeExplanation;
2030
+ if (manifest) {
2031
+ const { perCall } = computeManifestPerCallCost(manifest, content);
2032
+ const gotcha = detectManifestMultipliers(manifest, content);
1870
2033
  costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
1871
2034
  costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
2035
+ rangeExplanation = gotcha?.explanation;
2036
+ if (!gotcha && manifest.costMultipliers && manifest.costMultipliers.length > 0) {
2037
+ const maxFactor = Math.max(...manifest.costMultipliers.map((m) => m.factor));
2038
+ if (maxFactor > 1) {
2039
+ costHigh = monthlyInvocations * perCall * maxFactor;
2040
+ rangeExplanation = manifest.costMultipliers.map((m) => m.description).join("; ");
2041
+ }
2042
+ }
2043
+ } else {
2044
+ const gotcha = LEGACY_GOTCHA_MULTIPLIERS[serviceId];
2045
+ const unitRate = service.pricing?.unitRate ?? 0;
2046
+ if (unitRate > 0) {
2047
+ costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
2048
+ costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
2049
+ } else if (service.pricing?.monthlyBase !== void 0 && service.pricing.monthlyBase > 0) {
2050
+ costLow = 0;
2051
+ costHigh = 0;
2052
+ } else {
2053
+ const perCall = LEGACY_CALL_COSTS[serviceId] ?? 1e-3;
2054
+ costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
2055
+ costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
2056
+ }
2057
+ rangeExplanation = gotcha?.explanation;
1872
2058
  }
1873
2059
  if (costLow === 0 && costHigh === 0) continue;
1874
2060
  impacts.push({
@@ -1881,7 +2067,7 @@ function analyzeCostImpact(filePath, content, projectRoot) {
1881
2067
  monthlyInvocations,
1882
2068
  costLow,
1883
2069
  costHigh,
1884
- rangeExplanation: gotcha?.explanation
2070
+ rangeExplanation
1885
2071
  });
1886
2072
  }
1887
2073
  return impacts;
@@ -1889,11 +2075,11 @@ function analyzeCostImpact(filePath, content, projectRoot) {
1889
2075
 
1890
2076
  // src/utilization.ts
1891
2077
  function utilizationModelPath(projectRoot) {
1892
- return path6.join(projectDataDir(projectRoot), "utilization.json");
2078
+ return path7.join(projectDataDir(projectRoot), "utilization.json");
1893
2079
  }
1894
2080
  function readUtilizationModel(projectRoot) {
1895
2081
  try {
1896
- const raw = fs6.readFileSync(utilizationModelPath(projectRoot), "utf-8");
2082
+ const raw = fs7.readFileSync(utilizationModelPath(projectRoot), "utf-8");
1897
2083
  return JSON.parse(raw);
1898
2084
  } catch {
1899
2085
  return {
@@ -1906,9 +2092,9 @@ function readUtilizationModel(projectRoot) {
1906
2092
  }
1907
2093
  function writeUtilizationModel(model, projectRoot) {
1908
2094
  const dir = projectDataDir(projectRoot);
1909
- fs6.mkdirSync(dir, { recursive: true });
2095
+ fs7.mkdirSync(dir, { recursive: true });
1910
2096
  model.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1911
- fs6.writeFileSync(
2097
+ fs7.writeFileSync(
1912
2098
  utilizationModelPath(projectRoot),
1913
2099
  JSON.stringify(model, null, 2) + "\n",
1914
2100
  "utf-8"
@@ -1989,10 +2175,10 @@ function buildUtilizationModel(projectRoot) {
1989
2175
  const files = findSourceFiles(projectRoot);
1990
2176
  for (const file of files) {
1991
2177
  try {
1992
- const content = fs6.readFileSync(file, "utf-8");
2178
+ const content = fs7.readFileSync(file, "utf-8");
1993
2179
  const callSites = analyzeFileUtilization(file, content, projectRoot);
1994
2180
  if (callSites.length > 0) {
1995
- const relPath = path6.relative(projectRoot, file);
2181
+ const relPath = path7.relative(projectRoot, file);
1996
2182
  const relativeSites = callSites.map((cs) => ({
1997
2183
  ...cs,
1998
2184
  filePath: relPath
@@ -2008,21 +2194,21 @@ function findSourceFiles(projectRoot) {
2008
2194
  const files = [];
2009
2195
  const dirsToScan = [];
2010
2196
  for (const dir of CODE_DIRS) {
2011
- const fullPath = path6.join(projectRoot, dir);
2012
- if (fs6.existsSync(fullPath)) {
2197
+ const fullPath = path7.join(projectRoot, dir);
2198
+ if (fs7.existsSync(fullPath)) {
2013
2199
  dirsToScan.push(fullPath);
2014
2200
  }
2015
2201
  }
2016
2202
  try {
2017
- const entries = fs6.readdirSync(projectRoot, { withFileTypes: true });
2203
+ const entries = fs7.readdirSync(projectRoot, { withFileTypes: true });
2018
2204
  for (const entry of entries) {
2019
2205
  if (!entry.isDirectory()) continue;
2020
2206
  if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name.startsWith(".")) continue;
2021
- const subPkgPath = path6.join(projectRoot, entry.name, "package.json");
2022
- if (fs6.existsSync(subPkgPath)) {
2207
+ const subPkgPath = path7.join(projectRoot, entry.name, "package.json");
2208
+ if (fs7.existsSync(subPkgPath)) {
2023
2209
  for (const dir of CODE_DIRS) {
2024
- const fullPath = path6.join(projectRoot, entry.name, dir);
2025
- if (fs6.existsSync(fullPath)) {
2210
+ const fullPath = path7.join(projectRoot, entry.name, dir);
2211
+ if (fs7.existsSync(fullPath)) {
2026
2212
  dirsToScan.push(fullPath);
2027
2213
  }
2028
2214
  }
@@ -2038,10 +2224,10 @@ function findSourceFiles(projectRoot) {
2038
2224
  function walkDir2(dir, pattern, results, maxDepth = 5) {
2039
2225
  if (maxDepth <= 0) return;
2040
2226
  try {
2041
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
2227
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
2042
2228
  for (const entry of entries) {
2043
2229
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2044
- const fullPath = path6.join(dir, entry.name);
2230
+ const fullPath = path7.join(dir, entry.name);
2045
2231
  if (entry.isDirectory()) {
2046
2232
  walkDir2(fullPath, pattern, results, maxDepth - 1);
2047
2233
  } else if (pattern.test(entry.name)) {
@@ -2137,10 +2323,10 @@ async function cmdInit() {
2137
2323
  const projectRoot = process.cwd();
2138
2324
  const nonInteractive = flags.has("--non-interactive") || flags.has("--ni");
2139
2325
  const alreadyInitialized = isInitialized(projectRoot);
2140
- let projectName = path7.basename(projectRoot);
2326
+ let projectName = path8.basename(projectRoot);
2141
2327
  try {
2142
- const pkgPath = path7.join(projectRoot, "package.json");
2143
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
2328
+ const pkgPath = path8.join(projectRoot, "package.json");
2329
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
2144
2330
  if (pkg.name) projectName = pkg.name;
2145
2331
  } catch {
2146
2332
  }
@@ -2179,8 +2365,8 @@ async function cmdInit() {
2179
2365
  config.services = result.services;
2180
2366
  }
2181
2367
  writeProjectConfig(config, projectRoot);
2182
- const gitignorePath = path7.join(projectConfigDir(projectRoot), ".gitignore");
2183
- fs7.writeFileSync(
2368
+ const gitignorePath = path8.join(projectConfigDir(projectRoot), ".gitignore");
2369
+ fs8.writeFileSync(
2184
2370
  gitignorePath,
2185
2371
  [
2186
2372
  "# Burnwatch \u2014 ignore cache and snapshots, keep ledger and config",
@@ -2224,10 +2410,10 @@ async function cmdInterview() {
2224
2410
  if (!isInitialized(projectRoot)) {
2225
2411
  ensureProjectDirs(projectRoot);
2226
2412
  const detected = detectServices(projectRoot);
2227
- let projectName = path7.basename(projectRoot);
2413
+ let projectName = path8.basename(projectRoot);
2228
2414
  try {
2229
2415
  const pkg = JSON.parse(
2230
- fs7.readFileSync(path7.join(projectRoot, "package.json"), "utf-8")
2416
+ fs8.readFileSync(path8.join(projectRoot, "package.json"), "utf-8")
2231
2417
  );
2232
2418
  if (pkg.name) projectName = pkg.name;
2233
2419
  } catch {
@@ -2262,9 +2448,9 @@ async function cmdInterview() {
2262
2448
  const envFiles = findEnvFiles(projectRoot, 3);
2263
2449
  for (const envFilePath of envFiles) {
2264
2450
  try {
2265
- const envContent = fs7.readFileSync(envFilePath, "utf-8");
2451
+ const envContent = fs8.readFileSync(envFilePath, "utf-8");
2266
2452
  const envKeys = parseEnvKeys(envContent);
2267
- const envFileName = path7.relative(projectRoot, envFilePath);
2453
+ const envFileName = path8.relative(projectRoot, envFilePath);
2268
2454
  for (const pattern of definition.envPatterns) {
2269
2455
  if (envKeys.has(pattern)) {
2270
2456
  const label = `${pattern} (in ${envFileName})`;
@@ -2287,7 +2473,7 @@ async function cmdInterview() {
2287
2473
  const envFile = parts[1];
2288
2474
  const envVar = parts[2];
2289
2475
  try {
2290
- const envContent = fs7.readFileSync(path7.join(projectRoot, envFile), "utf-8");
2476
+ const envContent = fs8.readFileSync(path8.join(projectRoot, envFile), "utf-8");
2291
2477
  const regex = new RegExp(`^(?:export\\s+)?${envVar}\\s*=\\s*(.+)$`, "m");
2292
2478
  const match = envContent.match(regex);
2293
2479
  if (match?.[1]) apiKey = match[1].trim().replace(/^["']|["']$/g, "");
@@ -2312,7 +2498,6 @@ async function cmdInterview() {
2312
2498
  openai: "Admin key from platform.openai.com \u2192 Settings \u2192 API Keys (sk-admin-*)",
2313
2499
  vercel: "Token from vercel.com/account/tokens",
2314
2500
  supabase: "PAT from supabase.com/dashboard \u2192 Account \u2192 Access Tokens (not service_role key)",
2315
- stripe: "Secret key from dashboard.stripe.com \u2192 Developers \u2192 API Keys (sk_live_*)",
2316
2501
  scrapfly: "API key from scrapfly.io/dashboard",
2317
2502
  browserbase: "API key from browserbase.com \u2192 Settings \u2192 API Keys",
2318
2503
  upstash: "email:api_key from console.upstash.com \u2192 Account \u2192 Management API",
@@ -2465,9 +2650,45 @@ async function cmdConfigure() {
2465
2650
  if (options["plan"]) {
2466
2651
  const planSearch = options["plan"].toLowerCase();
2467
2652
  const plans = definition?.plans ?? [];
2468
- const matched = plans.find(
2653
+ const budgetHint = options["budget"] !== void 0 ? parseFloat(options["budget"]) : void 0;
2654
+ let candidates = plans.filter(
2469
2655
  (p) => p.name.toLowerCase().includes(planSearch) || p.name.toLowerCase().split(/[\s(]/)[0] === planSearch
2470
2656
  );
2657
+ if (candidates.length === 0) {
2658
+ const stripped = planSearch.replace(/\(\s*\//, "(").replace(/\$\d+/g, "");
2659
+ candidates = plans.filter((p) => {
2660
+ const pStripped = p.name.toLowerCase().replace(/\$\d+/g, "").replace(/\(\s*\//, "(");
2661
+ return pStripped.includes(stripped) || stripped.includes(pStripped.split(/[\s(]/)[0]);
2662
+ });
2663
+ }
2664
+ let matched = candidates[0];
2665
+ if (candidates.length > 1) {
2666
+ let disambiguated = false;
2667
+ const dollarMatch = options["plan"].match(/\$(\d+)/);
2668
+ if (dollarMatch) {
2669
+ const amount = parseInt(dollarMatch[1], 10);
2670
+ const byAmount = candidates.find((p) => p.monthlyBase === amount);
2671
+ if (byAmount) {
2672
+ matched = byAmount;
2673
+ disambiguated = true;
2674
+ }
2675
+ }
2676
+ if (budgetHint !== void 0 && !isNaN(budgetHint)) {
2677
+ const byBudget = candidates.find((p) => p.monthlyBase === budgetHint);
2678
+ if (byBudget) {
2679
+ matched = byBudget;
2680
+ disambiguated = true;
2681
+ }
2682
+ }
2683
+ if (!disambiguated) {
2684
+ const numMatch = options["plan"].match(/(\d{2,})/);
2685
+ if (numMatch) {
2686
+ const amount = parseInt(numMatch[1], 10);
2687
+ const byAmount = candidates.find((p) => p.monthlyBase === amount);
2688
+ if (byAmount) matched = byAmount;
2689
+ }
2690
+ }
2691
+ }
2471
2692
  if (matched) {
2472
2693
  tracked.planName = matched.name;
2473
2694
  tracked.excluded = false;
@@ -2482,42 +2703,12 @@ async function cmdConfigure() {
2482
2703
  }
2483
2704
  }
2484
2705
  if (matched.includedUnits !== void 0 && matched.unitName) {
2485
- tracked.allowance = {
2486
- included: matched.includedUnits,
2487
- unitName: matched.unitName
2488
- };
2706
+ tracked.allowance = { included: matched.includedUnits, unitName: matched.unitName };
2489
2707
  } else {
2490
2708
  delete tracked.allowance;
2491
2709
  }
2492
2710
  } else {
2493
- const stripped = planSearch.replace(/\(\s*\//, "(").replace(/\$\d+/g, "");
2494
- const secondTry = plans.find(
2495
- (p) => {
2496
- const pStripped = p.name.toLowerCase().replace(/\$\d+/g, "").replace(/\(\s*\//, "(");
2497
- return pStripped.includes(stripped) || stripped.includes(pStripped.split(/[\s(]/)[0]);
2498
- }
2499
- );
2500
- if (secondTry) {
2501
- tracked.planName = secondTry.name;
2502
- tracked.excluded = false;
2503
- if (secondTry.type === "flat" && secondTry.monthlyBase !== void 0) {
2504
- tracked.planCost = secondTry.monthlyBase;
2505
- if (options["budget"] === void 0 && (tracked.budget === void 0 || tracked.budget === 0)) {
2506
- tracked.budget = secondTry.monthlyBase;
2507
- }
2508
- } else if (secondTry.suggestedBudget !== void 0 && options["budget"] === void 0) {
2509
- if (tracked.budget === void 0 || tracked.budget === 0) {
2510
- tracked.budget = secondTry.suggestedBudget;
2511
- }
2512
- }
2513
- if (secondTry.includedUnits !== void 0 && secondTry.unitName) {
2514
- tracked.allowance = { included: secondTry.includedUnits, unitName: secondTry.unitName };
2515
- } else {
2516
- delete tracked.allowance;
2517
- }
2518
- } else {
2519
- tracked.planName = options["plan"];
2520
- }
2711
+ tracked.planName = options["plan"];
2521
2712
  }
2522
2713
  }
2523
2714
  if (options["budget"] !== void 0) {
@@ -2669,7 +2860,7 @@ async function cmdStatus() {
2669
2860
  const snapshots = results.map((r) => {
2670
2861
  const tracked = config.services[r.serviceId];
2671
2862
  const allowanceData = r.unitsUsed !== void 0 && r.unitsTotal !== void 0 && r.unitName ? { used: r.unitsUsed, included: r.unitsTotal, unitName: r.unitName } : tracked?.allowance ? { used: 0, included: tracked.allowance.included, unitName: tracked.allowance.unitName } : void 0;
2672
- return buildSnapshot(r.serviceId, r.tier, r.spend, tracked?.budget, allowanceData);
2863
+ return buildSnapshot(r.serviceId, r.tier, r.spend, tracked?.budget, allowanceData, r.isEstimate, r.isFlatPlan);
2673
2864
  });
2674
2865
  const blindCount = snapshots.filter((s) => s.tier === "blind").length;
2675
2866
  const brief = buildBrief(config.projectName, snapshots, blindCount);
@@ -2779,26 +2970,26 @@ function cmdScan() {
2779
2970
  }
2780
2971
  function cmdReset() {
2781
2972
  const projectRoot = process.cwd();
2782
- const burnwatchDir = path7.join(projectRoot, ".burnwatch");
2783
- const claudeSkillsDir = path7.join(projectRoot, ".claude", "skills");
2784
- if (!fs7.existsSync(burnwatchDir)) {
2973
+ const burnwatchDir = path8.join(projectRoot, ".burnwatch");
2974
+ const claudeSkillsDir = path8.join(projectRoot, ".claude", "skills");
2975
+ if (!fs8.existsSync(burnwatchDir)) {
2785
2976
  console.log("burnwatch is not initialized in this project.");
2786
2977
  return;
2787
2978
  }
2788
- fs7.rmSync(burnwatchDir, { recursive: true, force: true });
2979
+ fs8.rmSync(burnwatchDir, { recursive: true, force: true });
2789
2980
  console.log(`\u{1F5D1}\uFE0F Removed ${burnwatchDir}`);
2790
2981
  const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
2791
2982
  for (const skill of skillNames) {
2792
- const skillDir = path7.join(claudeSkillsDir, skill);
2793
- if (fs7.existsSync(skillDir)) {
2794
- fs7.rmSync(skillDir, { recursive: true, force: true });
2983
+ const skillDir = path8.join(claudeSkillsDir, skill);
2984
+ if (fs8.existsSync(skillDir)) {
2985
+ fs8.rmSync(skillDir, { recursive: true, force: true });
2795
2986
  }
2796
2987
  }
2797
2988
  console.log("\u{1F5D1}\uFE0F Removed burnwatch skills from .claude/skills/");
2798
- const settingsPath = path7.join(projectRoot, ".claude", "settings.json");
2799
- if (fs7.existsSync(settingsPath)) {
2989
+ const settingsPath = path8.join(projectRoot, ".claude", "settings.json");
2990
+ if (fs8.existsSync(settingsPath)) {
2800
2991
  try {
2801
- const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
2992
+ const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
2802
2993
  const hooks = settings["hooks"];
2803
2994
  if (hooks) {
2804
2995
  for (const [event, hookList] of Object.entries(hooks)) {
@@ -2808,7 +2999,7 @@ function cmdReset() {
2808
2999
  if (hooks[event].length === 0) delete hooks[event];
2809
3000
  }
2810
3001
  if (Object.keys(hooks).length === 0) delete settings["hooks"];
2811
- fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
3002
+ fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2812
3003
  console.log("\u{1F5D1}\uFE0F Removed burnwatch hooks from .claude/settings.json");
2813
3004
  }
2814
3005
  } catch {
@@ -2859,23 +3050,23 @@ Examples:
2859
3050
  }
2860
3051
  function cmdVersion() {
2861
3052
  try {
2862
- const pkgPath = path7.resolve(
2863
- path7.dirname(new URL(import.meta.url).pathname),
3053
+ const pkgPath = path8.resolve(
3054
+ path8.dirname(new URL(import.meta.url).pathname),
2864
3055
  "../package.json"
2865
3056
  );
2866
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
3057
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
2867
3058
  console.log(`burnwatch v${pkg.version}`);
2868
3059
  } catch {
2869
3060
  console.log("burnwatch v0.1.0");
2870
3061
  }
2871
3062
  }
2872
3063
  function registerHooks(projectRoot) {
2873
- const sourceHooksDir = path7.resolve(
2874
- path7.dirname(new URL(import.meta.url).pathname),
3064
+ const sourceHooksDir = path8.resolve(
3065
+ path8.dirname(new URL(import.meta.url).pathname),
2875
3066
  "hooks"
2876
3067
  );
2877
- const localHooksDir = path7.join(projectRoot, ".burnwatch", "hooks");
2878
- fs7.mkdirSync(localHooksDir, { recursive: true });
3068
+ const localHooksDir = path8.join(projectRoot, ".burnwatch", "hooks");
3069
+ fs8.mkdirSync(localHooksDir, { recursive: true });
2879
3070
  const hookFiles = [
2880
3071
  "on-session-start.js",
2881
3072
  "on-prompt.js",
@@ -2883,45 +3074,45 @@ function registerHooks(projectRoot) {
2883
3074
  "on-stop.js"
2884
3075
  ];
2885
3076
  for (const file of hookFiles) {
2886
- const src = path7.join(sourceHooksDir, file);
2887
- const dest = path7.join(localHooksDir, file);
3077
+ const src = path8.join(sourceHooksDir, file);
3078
+ const dest = path8.join(localHooksDir, file);
2888
3079
  try {
2889
- fs7.copyFileSync(src, dest);
3080
+ fs8.copyFileSync(src, dest);
2890
3081
  const mapSrc = src + ".map";
2891
- if (fs7.existsSync(mapSrc)) {
2892
- fs7.copyFileSync(mapSrc, dest + ".map");
3082
+ if (fs8.existsSync(mapSrc)) {
3083
+ fs8.copyFileSync(mapSrc, dest + ".map");
2893
3084
  }
2894
3085
  } catch (err) {
2895
3086
  console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);
2896
3087
  }
2897
3088
  }
2898
3089
  console.log(` Hook scripts copied to ${localHooksDir}`);
2899
- const sourceSkillsDir = path7.resolve(
2900
- path7.dirname(new URL(import.meta.url).pathname),
3090
+ const sourceSkillsDir = path8.resolve(
3091
+ path8.dirname(new URL(import.meta.url).pathname),
2901
3092
  "../skills"
2902
3093
  );
2903
3094
  const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
2904
- const claudeSkillsDir = path7.join(projectRoot, ".claude", "skills");
3095
+ const claudeSkillsDir = path8.join(projectRoot, ".claude", "skills");
2905
3096
  for (const skillName of skillNames) {
2906
- const srcSkill = path7.join(sourceSkillsDir, skillName, "SKILL.md");
2907
- const destDir = path7.join(claudeSkillsDir, skillName);
2908
- const destSkill = path7.join(destDir, "SKILL.md");
3097
+ const srcSkill = path8.join(sourceSkillsDir, skillName, "SKILL.md");
3098
+ const destDir = path8.join(claudeSkillsDir, skillName);
3099
+ const destSkill = path8.join(destDir, "SKILL.md");
2909
3100
  try {
2910
- if (fs7.existsSync(srcSkill)) {
2911
- fs7.mkdirSync(destDir, { recursive: true });
2912
- fs7.copyFileSync(srcSkill, destSkill);
3101
+ if (fs8.existsSync(srcSkill)) {
3102
+ fs8.mkdirSync(destDir, { recursive: true });
3103
+ fs8.copyFileSync(srcSkill, destSkill);
2913
3104
  }
2914
3105
  } catch (err) {
2915
3106
  console.error(` Warning: Could not copy skill ${skillName}: ${err instanceof Error ? err.message : err}`);
2916
3107
  }
2917
3108
  }
2918
3109
  console.log(` Skills installed to ${claudeSkillsDir}`);
2919
- const claudeDir = path7.join(projectRoot, ".claude");
2920
- const settingsPath = path7.join(claudeDir, "settings.json");
2921
- fs7.mkdirSync(claudeDir, { recursive: true });
3110
+ const claudeDir = path8.join(projectRoot, ".claude");
3111
+ const settingsPath = path8.join(claudeDir, "settings.json");
3112
+ fs8.mkdirSync(claudeDir, { recursive: true });
2922
3113
  let settings = {};
2923
3114
  try {
2924
- const existing = fs7.readFileSync(settingsPath, "utf-8");
3115
+ const existing = fs8.readFileSync(settingsPath, "utf-8");
2925
3116
  settings = JSON.parse(existing);
2926
3117
  console.log(` Merging into existing ${settingsPath}`);
2927
3118
  } catch {
@@ -2937,7 +3128,7 @@ function registerHooks(projectRoot) {
2937
3128
  hooks: [
2938
3129
  {
2939
3130
  type: "command",
2940
- command: `node "${path7.join(hooksDir, "on-session-start.js")}"`,
3131
+ command: `node "${path8.join(hooksDir, "on-session-start.js")}"`,
2941
3132
  timeout: 15
2942
3133
  }
2943
3134
  ]
@@ -2950,7 +3141,7 @@ function registerHooks(projectRoot) {
2950
3141
  hooks: [
2951
3142
  {
2952
3143
  type: "command",
2953
- command: `node "${path7.join(hooksDir, "on-prompt.js")}"`,
3144
+ command: `node "${path8.join(hooksDir, "on-prompt.js")}"`,
2954
3145
  timeout: 5
2955
3146
  }
2956
3147
  ]
@@ -2962,7 +3153,7 @@ function registerHooks(projectRoot) {
2962
3153
  hooks: [
2963
3154
  {
2964
3155
  type: "command",
2965
- command: `node "${path7.join(hooksDir, "on-file-change.js")}"`,
3156
+ command: `node "${path8.join(hooksDir, "on-file-change.js")}"`,
2966
3157
  timeout: 5
2967
3158
  }
2968
3159
  ]
@@ -2972,14 +3163,14 @@ function registerHooks(projectRoot) {
2972
3163
  hooks: [
2973
3164
  {
2974
3165
  type: "command",
2975
- command: `node "${path7.join(hooksDir, "on-stop.js")}"`,
3166
+ command: `node "${path8.join(hooksDir, "on-stop.js")}"`,
2976
3167
  timeout: 15,
2977
3168
  async: true
2978
3169
  }
2979
3170
  ]
2980
3171
  });
2981
3172
  settings["hooks"] = hooks;
2982
- fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
3173
+ fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2983
3174
  console.log(` Hooks registered in ${settingsPath}`);
2984
3175
  }
2985
3176
  function addHookIfMissing(hookArray, _eventName, hookConfig) {