burnwatch 0.8.2 → 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/README.md +67 -10
- package/dist/cli.js +190 -15
- package/dist/cli.js.map +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +141 -5
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +139 -3
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +141 -5
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/skills/burnwatch-interview/SKILL.md +76 -51
package/README.md
CHANGED
|
@@ -88,20 +88,28 @@ npx burnwatch init
|
|
|
88
88
|
✅ burnwatch initialized!
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
### 2.
|
|
91
|
+
### 2. Configure your services
|
|
92
|
+
|
|
93
|
+
**In Claude Code (recommended):** Just ask the agent:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
You: "Let's set up burnwatch for this project"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The agent runs `/burnwatch-interview` — a conversational flow that walks through each detected service one at a time, confirms plans, sets budgets, and configures API keys. No terminal commands needed.
|
|
100
|
+
|
|
101
|
+
**From the terminal:**
|
|
92
102
|
|
|
93
103
|
```bash
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
burnwatch add scrapfly --key $SCRAPFLY_KEY --budget 50
|
|
97
|
-
burnwatch add vercel --token $VERCEL_TOKEN --budget 50
|
|
104
|
+
burnwatch init # interactive interview in TTY mode
|
|
105
|
+
```
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
burnwatch add posthog --plan-cost 0 --budget 0
|
|
101
|
-
burnwatch add inngest --plan-cost 25 --budget 25
|
|
107
|
+
**Manual CLI (if you prefer):**
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
burnwatch add
|
|
109
|
+
```bash
|
|
110
|
+
burnwatch add anthropic --key $ANTHROPIC_ADMIN_KEY --budget 100
|
|
111
|
+
burnwatch add scrapfly --key $SCRAPFLY_KEY --budget 50
|
|
112
|
+
burnwatch configure --service posthog --plan "Free" --budget 0
|
|
105
113
|
```
|
|
106
114
|
|
|
107
115
|
API keys are stored in `~/.config/burnwatch/` (global, `chmod 600`). They **never** touch your project directory. They never end up in git.
|
|
@@ -229,12 +237,51 @@ This feedback loop doesn't exist anywhere else today. The agent has cost memory.
|
|
|
229
237
|
|
|
230
238
|
<br>
|
|
231
239
|
|
|
240
|
+
## Claude Code Setup
|
|
241
|
+
|
|
242
|
+
burnwatch is designed to work natively with Claude Code. When you run `npx burnwatch init`, it:
|
|
243
|
+
|
|
244
|
+
1. **Detects** all paid services in your project
|
|
245
|
+
2. **Copies hooks** to `.burnwatch/hooks/` (session start, prompt submit, file change, stop)
|
|
246
|
+
3. **Installs skills** to `.claude/skills/` — three agent skills become available:
|
|
247
|
+
- `/setup-burnwatch` — Guided onboarding that walks you through configuring all services
|
|
248
|
+
- `/burnwatch-interview` — Conversational interview that confirms plans, budgets, and API keys one service at a time
|
|
249
|
+
- `/spend` — Quick spend check (or `/spend scrapfly` for a specific service)
|
|
250
|
+
4. **Registers hooks** in `.claude/settings.json`
|
|
251
|
+
|
|
252
|
+
### Agent-driven configuration
|
|
253
|
+
|
|
254
|
+
Instead of running CLI commands manually, let the agent interview you:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
You: "Let's configure burnwatch"
|
|
258
|
+
|
|
259
|
+
Agent: I found 11 services in your project. Let me walk through each one.
|
|
260
|
+
|
|
261
|
+
First — Anthropic. Defaulted to "API Usage" at $100/mo.
|
|
262
|
+
I checked your API — you've spent $47.23 this month.
|
|
263
|
+
Does $100/mo feel right, or want to adjust?
|
|
264
|
+
|
|
265
|
+
You: That's good, keep it.
|
|
266
|
+
|
|
267
|
+
Agent: Got it — Anthropic set to $100/mo. ✅
|
|
268
|
+
|
|
269
|
+
Next — Scrapfly. Probe detected Pro plan, 250K/1M credits used.
|
|
270
|
+
Keep the $100/mo budget?
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The agent works through services by risk category (LLMs first, then usage-based, then infrastructure), batches free-tier services together, and writes each answer back immediately via `burnwatch configure`.
|
|
274
|
+
|
|
275
|
+
<br>
|
|
276
|
+
|
|
232
277
|
## CLI Reference
|
|
233
278
|
|
|
234
279
|
```
|
|
235
280
|
burnwatch init Initialize in current project
|
|
236
281
|
burnwatch setup Init + auto-configure all detected services
|
|
237
282
|
burnwatch add <service> [options] Register a service for tracking
|
|
283
|
+
burnwatch configure [options] Update service config (plan, budget, key)
|
|
284
|
+
burnwatch interview --json Export current state as JSON (for agent use)
|
|
238
285
|
burnwatch status Show current spend brief
|
|
239
286
|
burnwatch services List all services in registry
|
|
240
287
|
burnwatch reconcile Scan for untracked services
|
|
@@ -251,6 +298,16 @@ burnwatch version Show version
|
|
|
251
298
|
| `--budget <N>` | Monthly budget in USD |
|
|
252
299
|
| `--plan-cost <N>` | Monthly plan cost for CALC tracking |
|
|
253
300
|
|
|
301
|
+
### `burnwatch configure` options
|
|
302
|
+
|
|
303
|
+
| Flag | Description |
|
|
304
|
+
|------|-------------|
|
|
305
|
+
| `--service <ID>` | Service to configure (e.g., `anthropic`, `scrapfly`) |
|
|
306
|
+
| `--plan <NAME>` | Set the plan tier |
|
|
307
|
+
| `--budget <N>` | Set monthly budget in USD |
|
|
308
|
+
| `--key <KEY>` | Set API key for LIVE tracking |
|
|
309
|
+
| `--exclude` | Exclude service from tracking |
|
|
310
|
+
|
|
254
311
|
<br>
|
|
255
312
|
|
|
256
313
|
## Config Model
|
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 ? `
|
|
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" : ""}
|
|
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
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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" : ""}
|
|
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
|
|
2021
|
+
Run 'burnwatch configure --service <id> --key <KEY>' to enable live billing.
|
|
1867
2022
|
`);
|
|
1868
2023
|
}
|
|
1869
2024
|
}
|
|
@@ -1987,6 +2142,26 @@ function registerHooks(projectRoot) {
|
|
|
1987
2142
|
}
|
|
1988
2143
|
}
|
|
1989
2144
|
console.log(` Hook scripts copied to ${localHooksDir}`);
|
|
2145
|
+
const sourceSkillsDir = path5.resolve(
|
|
2146
|
+
path5.dirname(new URL(import.meta.url).pathname),
|
|
2147
|
+
"../skills"
|
|
2148
|
+
);
|
|
2149
|
+
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2150
|
+
const claudeSkillsDir = path5.join(projectRoot, ".claude", "skills");
|
|
2151
|
+
for (const skillName of skillNames) {
|
|
2152
|
+
const srcSkill = path5.join(sourceSkillsDir, skillName, "SKILL.md");
|
|
2153
|
+
const destDir = path5.join(claudeSkillsDir, skillName);
|
|
2154
|
+
const destSkill = path5.join(destDir, "SKILL.md");
|
|
2155
|
+
try {
|
|
2156
|
+
if (fs5.existsSync(srcSkill)) {
|
|
2157
|
+
fs5.mkdirSync(destDir, { recursive: true });
|
|
2158
|
+
fs5.copyFileSync(srcSkill, destSkill);
|
|
2159
|
+
}
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
console.error(` Warning: Could not copy skill ${skillName}: ${err instanceof Error ? err.message : err}`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
console.log(` Skills installed to ${claudeSkillsDir}`);
|
|
1990
2165
|
const claudeDir = path5.join(projectRoot, ".claude");
|
|
1991
2166
|
const settingsPath = path5.join(claudeDir, "settings.json");
|
|
1992
2167
|
fs5.mkdirSync(claudeDir, { recursive: true });
|