burnwatch 0.13.0 → 0.13.1

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 CHANGED
@@ -5,6 +5,18 @@ All notable changes to burnwatch will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.13.1] - 2026-03-25
9
+
10
+ ### Fixed
11
+
12
+ - **OpenAI NaN spend / NaN% budget**: The OpenAI connector didn't guard against non-numeric `amount.value` responses. One NaN from OpenAI poisoned the entire brief total. Added type checking in the connector and a global NaN guard in `buildSnapshot()` — no single bad connector can ever corrupt the display again.
13
+ - **Excluded services still showing in status**: Stripe, AWS, and other excluded services appeared in `burnwatch status` with ~$0.00 CALC. Now filtered out before polling — excluded services never appear in the brief.
14
+ - **Anthropic shows BLIND despite having admin key stored**: The LIVE API call fails (endpoint/auth issue), and the CALC fallback requires `planCost` which was never set due to shell `$` variable expansion eating plan names like `"Max ($200/mo)"` → `"Max (/mo)"`. Three-part fix:
15
+ 1. **Shell-safe plan matching**: Configure command now strips `$` amounts before fuzzy matching, so `"Max (/mo)"` correctly matches `"Max ($200/mo)"`.
16
+ 2. **Registry plan fallback**: `pollService()` now resolves `planCost` from the registry plan's `monthlyBase` when `planCost` is missing but `planName` is set — so LIVE failures gracefully degrade to CALC instead of BLIND.
17
+ 3. **Prorated CALC spend on failure**: When LIVE fails and falls back to CALC, spend is now prorated to day-of-month instead of showing $0.
18
+ - **CALC services all showing $0 spend**: Flat-fee services (Vercel Pro, Browserbase Startup, etc.) had `planCost: 0` because the configure command's plan match failed silently. The shell-safe matching fix above prevents this for future configurations.
19
+
8
20
  ## [0.13.0] - 2026-03-25
9
21
 
10
22
  ### Added
package/dist/cli.js CHANGED
@@ -701,12 +701,16 @@ var openaiConnector = {
701
701
  for (const bucket of result.data.data) {
702
702
  if (bucket.results) {
703
703
  for (const r of bucket.results) {
704
- totalSpend += r.amount?.value ?? 0;
704
+ const val = r.amount?.value;
705
+ if (typeof val === "number" && !isNaN(val)) {
706
+ totalSpend += val;
707
+ }
705
708
  }
706
709
  }
707
710
  }
708
711
  }
709
712
  totalSpend = totalSpend / 100;
713
+ if (isNaN(totalSpend)) totalSpend = 0;
710
714
  return {
711
715
  serviceId: "openai",
712
716
  spend: totalSpend,
@@ -954,6 +958,15 @@ async function pollService(tracked) {
954
958
  const serviceConfig = globalConfig.services[tracked.serviceId];
955
959
  const connector = connectors.get(tracked.serviceId);
956
960
  const definition = getService(tracked.serviceId);
961
+ let effectivePlanCost = tracked.planCost;
962
+ if (effectivePlanCost === void 0 && tracked.planName && definition?.plans) {
963
+ const matchedPlan = definition.plans.find(
964
+ (p) => p.name === tracked.planName || p.name.toLowerCase().includes((tracked.planName ?? "").toLowerCase())
965
+ );
966
+ if (matchedPlan?.monthlyBase !== void 0) {
967
+ effectivePlanCost = matchedPlan.monthlyBase;
968
+ }
969
+ }
957
970
  if (connector && serviceConfig?.apiKey) {
958
971
  try {
959
972
  const result = await connector.fetchSpend(
@@ -961,38 +974,48 @@ async function pollService(tracked) {
961
974
  serviceConfig
962
975
  );
963
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;
964
982
  return {
965
983
  serviceId: tracked.serviceId,
966
- spend: tracked.planCost ?? 0,
984
+ spend: fallbackSpend,
967
985
  isEstimate: true,
968
- tier: tracked.planCost !== void 0 ? "calc" : "blind",
969
- error: `LIVE failed (${result.error}) \u2014 showing ${tracked.planCost !== void 0 ? "CALC" : "BLIND"} fallback`
986
+ tier: effectivePlanCost !== void 0 ? "calc" : "blind",
987
+ error: `LIVE failed (${result.error}) \u2014 showing ${effectivePlanCost !== void 0 ? "CALC" : "BLIND"} fallback`
970
988
  };
971
989
  } 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;
972
995
  return {
973
996
  serviceId: tracked.serviceId,
974
- spend: tracked.planCost ?? 0,
997
+ spend: fallbackSpend,
975
998
  isEstimate: true,
976
- tier: tracked.planCost !== void 0 ? "calc" : "blind",
977
- error: `LIVE failed (${err instanceof Error ? err.message : "unknown"}) \u2014 showing ${tracked.planCost !== void 0 ? "CALC" : "BLIND"} fallback`
999
+ tier: effectivePlanCost !== void 0 ? "calc" : "blind",
1000
+ error: `LIVE failed (${err instanceof Error ? err.message : "unknown"}) \u2014 showing ${effectivePlanCost !== void 0 ? "CALC" : "BLIND"} fallback`
978
1001
  };
979
1002
  }
980
1003
  }
981
1004
  if (connector && tracked.hasApiKey && !serviceConfig?.apiKey) {
982
- const projectedSpend = tracked.planCost !== void 0 ? (() => {
1005
+ const projectedSpend = effectivePlanCost !== void 0 ? (() => {
983
1006
  const now = /* @__PURE__ */ new Date();
984
1007
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
985
- return tracked.planCost / daysInMonth * now.getDate();
1008
+ return effectivePlanCost / daysInMonth * now.getDate();
986
1009
  })() : 0;
987
1010
  return {
988
1011
  serviceId: tracked.serviceId,
989
1012
  spend: projectedSpend,
990
1013
  isEstimate: true,
991
- tier: tracked.planCost !== void 0 ? "calc" : "blind",
1014
+ tier: effectivePlanCost !== void 0 ? "calc" : "blind",
992
1015
  error: "API key marked as configured but not found in ~/.config/burnwatch/ \u2014 re-run configure with --key"
993
1016
  };
994
1017
  }
995
- if (tracked.planCost !== void 0) {
1018
+ if (effectivePlanCost !== void 0) {
996
1019
  const now = /* @__PURE__ */ new Date();
997
1020
  const daysInMonth = new Date(
998
1021
  now.getFullYear(),
@@ -1000,7 +1023,7 @@ async function pollService(tracked) {
1000
1023
  0
1001
1024
  ).getDate();
1002
1025
  const dayOfMonth = now.getDate();
1003
- const projectedSpend = tracked.planCost / daysInMonth * dayOfMonth;
1026
+ const projectedSpend = effectivePlanCost / daysInMonth * dayOfMonth;
1004
1027
  return {
1005
1028
  serviceId: tracked.serviceId,
1006
1029
  spend: projectedSpend,
@@ -1163,8 +1186,10 @@ function formatLeft(snap) {
1163
1186
  return "\u2014";
1164
1187
  }
1165
1188
  function buildSnapshot(serviceId, tier, spend, budget, allowanceData) {
1189
+ if (isNaN(spend) || !isFinite(spend)) spend = 0;
1190
+ if (budget !== void 0 && (isNaN(budget) || !isFinite(budget))) budget = void 0;
1166
1191
  const isEstimate = tier === "est" || tier === "calc";
1167
- const budgetPercent = budget ? spend / budget * 100 : void 0;
1192
+ const budgetPercent = budget && budget > 0 ? spend / budget * 100 : void 0;
1168
1193
  let status = "unknown";
1169
1194
  let statusLabel = tier === "blind" ? "needs API key" : "no budget";
1170
1195
  if (budget) {
@@ -2465,7 +2490,34 @@ async function cmdConfigure() {
2465
2490
  delete tracked.allowance;
2466
2491
  }
2467
2492
  } else {
2468
- tracked.planName = options["plan"];
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
+ }
2469
2521
  }
2470
2522
  }
2471
2523
  if (options["budget"] !== void 0) {
@@ -2604,7 +2656,9 @@ async function cmdStatus() {
2604
2656
  process.exit(1);
2605
2657
  }
2606
2658
  const config = readProjectConfig(projectRoot);
2607
- const trackedServices = Object.values(config.services);
2659
+ const trackedServices = Object.values(config.services).filter(
2660
+ (s) => !s.excluded
2661
+ );
2608
2662
  if (trackedServices.length === 0) {
2609
2663
  console.log("No services tracked yet.");
2610
2664
  console.log('Run "burnwatch add <service>" to start tracking.');