burnwatch 0.14.0 → 0.14.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/dist/cli.js CHANGED
@@ -639,9 +639,9 @@ var anthropicConnector = {
639
639
  async fetchSpend(apiKey) {
640
640
  const now = /* @__PURE__ */ new Date();
641
641
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
642
- const startDate = startOfMonth.toISOString().split("T")[0];
643
- const endDate = now.toISOString().split("T")[0];
644
- const url3 = `https://api.anthropic.com/v1/organizations/usage?start_date=${startDate}&end_date=${endDate}`;
642
+ const startingAt = startOfMonth.toISOString();
643
+ const endingAt = now.toISOString();
644
+ const url3 = `https://api.anthropic.com/v1/organizations/cost_report?starting_at=${encodeURIComponent(startingAt)}&ending_at=${encodeURIComponent(endingAt)}&bucket_width=1m`;
645
645
  const result = await fetchJson(url3, {
646
646
  headers: {
647
647
  "x-api-key": apiKey,
@@ -654,17 +654,18 @@ var anthropicConnector = {
654
654
  spend: 0,
655
655
  isEstimate: true,
656
656
  tier: "est",
657
- error: result.error ?? "Failed to fetch Anthropic usage"
657
+ error: result.error ?? "Failed to fetch Anthropic cost report"
658
658
  };
659
659
  }
660
660
  let totalSpend = 0;
661
- if (result.data.total_cost_usd !== void 0) {
662
- totalSpend = result.data.total_cost_usd;
663
- } else if (result.data.data) {
664
- totalSpend = result.data.data.reduce(
665
- (sum, entry) => sum + (entry.total_cost_usd ?? entry.spend ?? 0),
666
- 0
667
- );
661
+ if (result.data.data) {
662
+ for (const bucket of result.data.data) {
663
+ if (bucket.results) {
664
+ for (const entry of bucket.results) {
665
+ totalSpend += entry.cost_usd ?? 0;
666
+ }
667
+ }
668
+ }
668
669
  }
669
670
  return {
670
671
  serviceId: "anthropic",
@@ -1094,24 +1095,33 @@ function formatBrief(brief) {
1094
1095
  lines.push(formatRow("Service", "Spend", "Conf", "Budget", "Left"));
1095
1096
  lines.push(`\u2551 ${hrThin}`);
1096
1097
  for (const svc of brief.services.filter((s) => s.tier !== "excluded")) {
1097
- const spendStr = svc.tier === "blind" && svc.spend === 0 ? "\u2014" : svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
1098
+ const spendStr = formatSpendValue(svc);
1098
1099
  const badge = CONFIDENCE_BADGES[svc.tier];
1099
1100
  const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
1100
1101
  const leftStr = formatLeft(svc);
1101
1102
  lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr));
1102
1103
  if (svc.allowance) {
1103
1104
  const usedStr = formatCompact(svc.allowance.used);
1104
- const totalStr2 = formatCompact(svc.allowance.included);
1105
+ const totalStr = formatCompact(svc.allowance.included);
1105
1106
  const pctStr = svc.allowance.percent.toFixed(0);
1106
1107
  const warn = svc.allowance.percent >= 75 ? " \u26A0\uFE0F" : "";
1107
- lines.push(`\u2551 \u21B3 ${usedStr}/${totalStr2} ${svc.allowance.unitName} (${pctStr}%)${warn}`);
1108
+ lines.push(`\u2551 \u21B3 ${usedStr}/${totalStr} ${svc.allowance.unitName} (${pctStr}%)${warn}`);
1108
1109
  }
1109
1110
  }
1110
1111
  lines.push(`\u2560${hr}`);
1111
- const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
1112
+ const parts = [];
1113
+ if (brief.liveSpend > 0) {
1114
+ parts.push(`Spend: $${brief.liveSpend.toFixed(2)}`);
1115
+ }
1116
+ if (brief.planCostTotal > 0) {
1117
+ parts.push(`Plans: $${brief.planCostTotal.toFixed(0)}/mo`);
1118
+ }
1119
+ if (parts.length === 0) {
1120
+ parts.push(`$${brief.totalSpend.toFixed(2)}`);
1121
+ }
1112
1122
  const marginStr = brief.estimateMargin > 0 ? ` Est margin: \xB1$${brief.estimateMargin.toFixed(0)}` : "";
1113
1123
  const untrackedStr = brief.untrackedCount > 0 ? `No billing data: ${brief.untrackedCount} \u26A0\uFE0F` : `All tracked \u2705`;
1114
- lines.push(`\u2551 TOTAL: ${totalStr} ${untrackedStr}${marginStr}`);
1124
+ lines.push(`\u2551 ${parts.join(" | ")} ${untrackedStr}${marginStr}`);
1115
1125
  for (const alert of brief.alerts) {
1116
1126
  const icon = alert.severity === "critical" ? "\u{1F6A8}" : "\u26A0\uFE0F";
1117
1127
  lines.push(`\u2551 ${icon} ${alert.message}`);
@@ -1126,11 +1136,18 @@ function buildBrief(projectName, snapshots, blindCount) {
1126
1136
  year: "numeric"
1127
1137
  });
1128
1138
  let totalSpend = 0;
1139
+ let liveSpend = 0;
1140
+ let planCostTotal = 0;
1129
1141
  let hasEstimates = false;
1130
1142
  let estimateMargin = 0;
1131
1143
  const alerts = [];
1132
1144
  for (const snap of snapshots) {
1133
1145
  totalSpend += snap.spend;
1146
+ if (snap.isPlanCost) {
1147
+ planCostTotal += snap.spend;
1148
+ } else if (snap.tier === "live") {
1149
+ liveSpend += snap.spend;
1150
+ }
1134
1151
  if (snap.isEstimate) {
1135
1152
  hasEstimates = true;
1136
1153
  estimateMargin += snap.spend * 0.15;
@@ -1167,6 +1184,8 @@ function buildBrief(projectName, snapshots, blindCount) {
1167
1184
  period,
1168
1185
  services: snapshots,
1169
1186
  totalSpend,
1187
+ liveSpend,
1188
+ planCostTotal,
1170
1189
  totalIsEstimate: hasEstimates,
1171
1190
  estimateMargin,
1172
1191
  untrackedCount: blindCount,
@@ -1176,6 +1195,12 @@ function buildBrief(projectName, snapshots, blindCount) {
1176
1195
  function formatRow(service, spend, conf, budget, left) {
1177
1196
  return `\u2551 ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(9)} ${budget.padEnd(7)} ${left}`;
1178
1197
  }
1198
+ function formatSpendValue(svc) {
1199
+ if (svc.tier === "blind" && svc.spend === 0) return "\u2014";
1200
+ if (svc.isPlanCost) return `$${svc.spend.toFixed(0)}/mo`;
1201
+ if (svc.isEstimate) return `~$${svc.spend.toFixed(2)}`;
1202
+ return `$${svc.spend.toFixed(2)}`;
1203
+ }
1179
1204
  function formatLeft(snap) {
1180
1205
  if (!snap.budget) return "\u2014";
1181
1206
  if (snap.status === "over") return "\u26A0\uFE0F OVER";
@@ -1189,6 +1214,7 @@ function buildSnapshot(serviceId, tier, spend, budget, allowanceData, isEstimate
1189
1214
  if (isNaN(spend) || !isFinite(spend)) spend = 0;
1190
1215
  if (budget !== void 0 && (isNaN(budget) || !isFinite(budget))) budget = void 0;
1191
1216
  const isEstimate = isEstimateOverride ?? (tier === "est" || tier === "calc");
1217
+ const isPlanCost = tier === "calc" && isFlatPlan === true;
1192
1218
  const budgetPercent = budget && budget > 0 ? spend / budget * 100 : void 0;
1193
1219
  let status = "unknown";
1194
1220
  let statusLabel = tier === "blind" ? "needs API key" : "no budget";
@@ -1229,6 +1255,7 @@ function buildSnapshot(serviceId, tier, spend, budget, allowanceData, isEstimate
1229
1255
  serviceId,
1230
1256
  spend,
1231
1257
  isEstimate,
1258
+ isPlanCost,
1232
1259
  tier,
1233
1260
  budget,
1234
1261
  budgetPercent,
@@ -1626,6 +1653,31 @@ async function runInteractiveInit(detected) {
1626
1653
  } else {
1627
1654
  tracked2.budget = defaultBudget;
1628
1655
  }
1656
+ if (tracked2.budget > 0 && tracked2.budget !== (chosen.monthlyBase ?? 0)) {
1657
+ const betterPlan = plans.find(
1658
+ (p) => p !== chosen && p.type !== "exclude" && p.monthlyBase === tracked2.budget
1659
+ );
1660
+ if (betterPlan) {
1661
+ const switchAnswer = await ask(
1662
+ rl,
1663
+ ` Budget matches "${betterPlan.name}" \u2014 switch to that plan? [Y/n]: `
1664
+ );
1665
+ if (!switchAnswer || switchAnswer.toLowerCase() !== "n") {
1666
+ chosen = betterPlan;
1667
+ tracked2.planName = betterPlan.name;
1668
+ if (betterPlan.type === "flat" && betterPlan.monthlyBase !== void 0) {
1669
+ tracked2.planCost = betterPlan.monthlyBase;
1670
+ }
1671
+ if (betterPlan.includedUnits !== void 0 && betterPlan.unitName) {
1672
+ tracked2.allowance = {
1673
+ included: betterPlan.includedUnits,
1674
+ unitName: betterPlan.unitName
1675
+ };
1676
+ }
1677
+ console.log(` -> Switched to ${betterPlan.name}`);
1678
+ }
1679
+ }
1680
+ }
1629
1681
  services[service.id] = tracked2;
1630
1682
  const tierLabel = tracked2.hasApiKey ? "LIVE" : tracked2.planCost !== void 0 ? "CALC" : "BLIND";
1631
1683
  const allowanceStr = tracked2.allowance ? ` | ${formatUnits(tracked2.allowance.included)} ${tracked2.allowance.unitName}` : "";