burnwatch 0.9.0 → 0.10.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.
package/dist/cli.js CHANGED
@@ -767,13 +767,151 @@ var scrapflyConnector = {
767
767
  }
768
768
  };
769
769
 
770
+ // src/services/supabase.ts
771
+ init_base();
772
+ var supabaseConnector = {
773
+ serviceId: "supabase",
774
+ async fetchSpend(token) {
775
+ const orgsResult = await fetchJson("https://api.supabase.com/v1/organizations", {
776
+ headers: {
777
+ Authorization: `Bearer ${token}`
778
+ }
779
+ });
780
+ if (!orgsResult.ok || !orgsResult.data) {
781
+ return {
782
+ serviceId: "supabase",
783
+ spend: 0,
784
+ isEstimate: true,
785
+ tier: "est",
786
+ error: orgsResult.error ?? "Failed to fetch Supabase orgs \u2014 is this a PAT (not service_role key)?"
787
+ };
788
+ }
789
+ const org = orgsResult.data[0];
790
+ if (!org?.id) {
791
+ return {
792
+ serviceId: "supabase",
793
+ spend: 0,
794
+ isEstimate: true,
795
+ tier: "est",
796
+ error: "No Supabase organization found"
797
+ };
798
+ }
799
+ const planName = org.billing?.plan ?? "unknown";
800
+ const planCosts = {
801
+ free: 0,
802
+ pro: 25,
803
+ team: 599,
804
+ enterprise: 0
805
+ // custom pricing
806
+ };
807
+ const baseCost = planCosts[planName.toLowerCase()] ?? 0;
808
+ let totalSpend = baseCost;
809
+ const usageResult = await fetchJson(`https://api.supabase.com/v1/organizations/${org.id}/usage`, {
810
+ headers: {
811
+ Authorization: `Bearer ${token}`
812
+ }
813
+ });
814
+ if (usageResult.ok && usageResult.data) {
815
+ if (usageResult.data.usage) {
816
+ const overageCost = usageResult.data.usage.reduce(
817
+ (sum, item) => sum + (item.cost ?? 0),
818
+ 0
819
+ );
820
+ if (overageCost > 0) totalSpend = baseCost + overageCost;
821
+ } else if (usageResult.data.total_usage !== void 0) {
822
+ totalSpend = usageResult.data.total_usage;
823
+ }
824
+ }
825
+ return {
826
+ serviceId: "supabase",
827
+ spend: totalSpend,
828
+ isEstimate: false,
829
+ tier: "live",
830
+ raw: {
831
+ plan: planName,
832
+ base_cost: baseCost,
833
+ org_id: org.id,
834
+ org_name: org.name,
835
+ ...usageResult.data ?? {}
836
+ }
837
+ };
838
+ }
839
+ };
840
+
841
+ // src/services/browserbase.ts
842
+ init_base();
843
+ var browserbaseConnector = {
844
+ serviceId: "browserbase",
845
+ async fetchSpend(apiKey) {
846
+ const projectsResult = await fetchJson("https://api.browserbase.com/v1/projects", {
847
+ headers: {
848
+ "X-BB-API-Key": apiKey
849
+ }
850
+ });
851
+ if (!projectsResult.ok || !projectsResult.data) {
852
+ return {
853
+ serviceId: "browserbase",
854
+ spend: 0,
855
+ isEstimate: true,
856
+ tier: "est",
857
+ error: projectsResult.error ?? "Failed to fetch Browserbase projects"
858
+ };
859
+ }
860
+ const project = projectsResult.data[0];
861
+ if (!project?.id) {
862
+ return {
863
+ serviceId: "browserbase",
864
+ spend: 0,
865
+ isEstimate: true,
866
+ tier: "est",
867
+ error: "No Browserbase project found"
868
+ };
869
+ }
870
+ const usageResult = await fetchJson(`https://api.browserbase.com/v1/projects/${project.id}/usage`, {
871
+ headers: {
872
+ "X-BB-API-Key": apiKey
873
+ }
874
+ });
875
+ if (!usageResult.ok || !usageResult.data) {
876
+ return {
877
+ serviceId: "browserbase",
878
+ spend: 0,
879
+ isEstimate: true,
880
+ tier: "est",
881
+ error: "Projects found but usage endpoint failed"
882
+ };
883
+ }
884
+ const minutes = usageResult.data.total_minutes ?? usageResult.data.usage?.minutes ?? (usageResult.data.total_hours ?? usageResult.data.usage?.hours ?? 0) * 60;
885
+ const sessionCount = usageResult.data.total_sessions ?? usageResult.data.usage?.sessions ?? 0;
886
+ const minuteRate = 0.1;
887
+ const spend = minutes * minuteRate;
888
+ return {
889
+ serviceId: "browserbase",
890
+ spend,
891
+ isEstimate: true,
892
+ // rate may vary by plan
893
+ tier: "est",
894
+ unitsUsed: sessionCount,
895
+ unitName: "sessions",
896
+ raw: {
897
+ minutes,
898
+ sessions: sessionCount,
899
+ minute_rate: minuteRate,
900
+ ...usageResult.data
901
+ }
902
+ };
903
+ }
904
+ };
905
+
770
906
  // src/services/index.ts
771
907
  init_base();
772
908
  var connectors = /* @__PURE__ */ new Map([
773
909
  ["anthropic", anthropicConnector],
774
910
  ["openai", openaiConnector],
775
911
  ["vercel", vercelConnector],
776
- ["scrapfly", scrapflyConnector]
912
+ ["scrapfly", scrapflyConnector],
913
+ ["supabase", supabaseConnector],
914
+ ["browserbase", browserbaseConnector]
777
915
  ]);
778
916
  async function pollService(tracked) {
779
917
  const globalConfig = readGlobalConfig();
@@ -862,7 +1000,7 @@ function formatBrief(brief) {
862
1000
  );
863
1001
  lines.push(`\u2551 ${hrSingle} \u2551`);
864
1002
  for (const svc of brief.services) {
865
- const spendStr = svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
1003
+ const spendStr = svc.tier === "blind" && svc.spend === 0 ? "\u2014" : svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
866
1004
  const badge = CONFIDENCE_BADGES[svc.tier];
867
1005
  const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
868
1006
  const leftStr = formatLeft(svc);
@@ -880,7 +1018,7 @@ function formatBrief(brief) {
880
1018
  lines.push(`\u2560${hrDouble}\u2563`);
881
1019
  const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
882
1020
  const marginStr = brief.estimateMargin > 0 ? ` Est margin: \xB1$${brief.estimateMargin.toFixed(0)}` : "";
883
- const untrackedStr = brief.untrackedCount > 0 ? `Untracked: ${brief.untrackedCount} \u26A0\uFE0F` : `Untracked: 0 \u2705`;
1021
+ const untrackedStr = brief.untrackedCount > 0 ? `No billing data: ${brief.untrackedCount} \u26A0\uFE0F` : `All tracked \u2705`;
884
1022
  lines.push(
885
1023
  `\u2551 TOTAL: ${totalStr} ${untrackedStr}${marginStr}`.padEnd(
886
1024
  width + 1
@@ -931,7 +1069,7 @@ function buildBrief(projectName, snapshots, blindCount) {
931
1069
  alerts.push({
932
1070
  serviceId: "_blind",
933
1071
  type: "blind_service",
934
- message: `${blindCount} service${blindCount > 1 ? "s" : ""} detected but untracked - run 'burnwatch init' to configure`,
1072
+ message: `${blindCount} service${blindCount > 1 ? "s" : ""} have no billing data \u2014 add API keys for live tracking`,
935
1073
  severity: "warning"
936
1074
  });
937
1075
  }
@@ -964,7 +1102,7 @@ function buildSnapshot(serviceId, tier, spend, budget, allowanceData) {
964
1102
  const isEstimate = tier === "est" || tier === "calc";
965
1103
  const budgetPercent = budget ? spend / budget * 100 : void 0;
966
1104
  let status = "unknown";
967
- let statusLabel = "no budget";
1105
+ let statusLabel = tier === "blind" ? "needs API key" : "no budget";
968
1106
  if (budget) {
969
1107
  if (budgetPercent > 100) {
970
1108
  status = "over";
@@ -1510,10 +1648,16 @@ async function cmdInit() {
1510
1648
  console.log("\n\u{1F517} Registering Claude Code hooks...\n");
1511
1649
  registerHooks(projectRoot);
1512
1650
  console.log("\nburnwatch initialized.\n");
1513
- console.log("Next steps:");
1514
- console.log(" burnwatch status Show current spend");
1515
- console.log(" burnwatch add <svc> Update a service's budget or API key");
1516
- console.log(" burnwatch init Re-run this setup anytime\n");
1651
+ if (process.stdin.isTTY) {
1652
+ console.log("Next steps:");
1653
+ console.log(" burnwatch status Show current spend");
1654
+ console.log(" burnwatch add <svc> Update a service's budget or API key");
1655
+ console.log(" burnwatch init Re-run this setup anytime\n");
1656
+ } else {
1657
+ console.log("Next steps:");
1658
+ console.log(" Ask your agent to run /burnwatch-interview for guided setup");
1659
+ console.log(" Or run 'burnwatch status' to see current spend\n");
1660
+ }
1517
1661
  }
1518
1662
  async function cmdInterview() {
1519
1663
  const projectRoot = process.cwd();
@@ -1744,16 +1888,27 @@ async function cmdConfigure() {
1744
1888
  }
1745
1889
  config.services[serviceId] = tracked;
1746
1890
  writeProjectConfig(config, projectRoot);
1891
+ const connectorServices = ["anthropic", "openai", "vercel", "scrapfly", "supabase", "browserbase"];
1892
+ const hasConnector = connectorServices.includes(serviceId);
1747
1893
  let tier = "blind";
1748
- if (tracked.excluded) tier = "excluded";
1749
- else if (tracked.hasApiKey) tier = "live";
1750
- else if (tracked.planCost !== void 0) tier = "calc";
1894
+ let tierNote = null;
1895
+ if (tracked.excluded) {
1896
+ tier = "excluded";
1897
+ } else if (tracked.hasApiKey && hasConnector) {
1898
+ tier = "live";
1899
+ } else if (tracked.hasApiKey && !hasConnector) {
1900
+ tier = "calc";
1901
+ tierNote = `Key saved but ${serviceId} has no billing connector yet \u2014 tracking as CALC. The key will be used for probing during interviews.`;
1902
+ } else if (tracked.planCost !== void 0) {
1903
+ tier = "calc";
1904
+ }
1751
1905
  const result = {
1752
1906
  success: true,
1753
1907
  serviceId,
1754
1908
  plan: tracked.planName ?? null,
1755
1909
  budget: tracked.budget ?? null,
1756
1910
  tier,
1911
+ tierNote,
1757
1912
  hasApiKey: tracked.hasApiKey,
1758
1913
  allowance: tracked.allowance ?? null
1759
1914
  };
@@ -1858,12 +2013,12 @@ async function cmdStatus() {
1858
2013
  console.log(formatBrief(brief));
1859
2014
  console.log("");
1860
2015
  if (blindCount > 0) {
1861
- console.log(`\u26A0\uFE0F ${blindCount} service${blindCount > 1 ? "s" : ""} untracked:`);
2016
+ console.log(`\u26A0\uFE0F ${blindCount} service${blindCount > 1 ? "s" : ""} with no billing data:`);
1862
2017
  for (const snap of snapshots.filter((s) => s.tier === "blind")) {
1863
- console.log(` \u2022 ${snap.serviceId}`);
2018
+ console.log(` \u2022 ${snap.serviceId} \u2014 add an API key for live tracking`);
1864
2019
  }
1865
2020
  console.log(`
1866
- Run 'burnwatch init' to configure budgets and API keys.
2021
+ Run 'burnwatch configure --service <id> --key <KEY>' to enable live billing.
1867
2022
  `);
1868
2023
  }
1869
2024
  }