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 +12 -0
- package/dist/cli.js +69 -15
- package/dist/cli.js.map +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +38 -13
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +38 -13
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +38 -13
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
984
|
+
spend: fallbackSpend,
|
|
967
985
|
isEstimate: true,
|
|
968
|
-
tier:
|
|
969
|
-
error: `LIVE failed (${result.error}) \u2014 showing ${
|
|
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:
|
|
997
|
+
spend: fallbackSpend,
|
|
975
998
|
isEstimate: true,
|
|
976
|
-
tier:
|
|
977
|
-
error: `LIVE failed (${err instanceof Error ? err.message : "unknown"}) \u2014 showing ${
|
|
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 =
|
|
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
|
|
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:
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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.');
|