burnwatch 0.3.0 → 0.4.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 +24 -0
- package/README.md +30 -30
- package/dist/cli.js +240 -31
- package/dist/cli.js.map +1 -1
- package/dist/cost-impact.d.ts +23 -0
- package/dist/cost-impact.js +281 -0
- package/dist/cost-impact.js.map +1 -0
- package/dist/detector-C4LnLT-O.d.ts +28 -0
- package/dist/hooks/on-file-change.js +324 -6
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-prompt.js +2 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +10 -1
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +47 -3
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.d.ts +5 -159
- package/dist/index.js +248 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive-init.d.ts +20 -0
- package/dist/interactive-init.js +242 -0
- package/dist/interactive-init.js.map +1 -0
- package/dist/mcp-server.js +2 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/types-fDMu4rOd.d.ts +178 -0
- package/package.json +1 -1
- package/registry.json +89 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.4.0] - 2026-03-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Interactive init with plan tiers**: `burnwatch init` now walks through each detected service interactively, grouped by cost risk (LLMs first, then usage-based, infra, flat-rate). Users pick from known plan tiers per service (e.g., Anthropic API Usage, Max $100/mo, Pro $20/mo, or "Don't track").
|
|
13
|
+
- **Plan tiers for all 14 services**: Registry now includes plan options for Anthropic, OpenAI, Google Gemini, Voyage AI, Vercel, Supabase, Stripe, Scrapfly, Browserbase, Upstash, Resend, Inngest, PostHog, and AWS.
|
|
14
|
+
- **Smart defaults**: Each service has a recommended default plan. Flat plans auto-set the budget to the plan cost. API Usage plans prompt for keys and budgets.
|
|
15
|
+
- **Exclude option**: "Don't track for this project" explicitly excludes a service (shows as "excluded", not BLIND).
|
|
16
|
+
- **Auto-detect plan**: Scrapfly plan can be auto-detected from API key via the /account endpoint.
|
|
17
|
+
- **Non-interactive fallback**: `burnwatch init --non-interactive` preserves the original auto-detect behavior for CI/scripted use.
|
|
18
|
+
- **Predictive cost impact analysis**: PostToolUse hook now analyzes file writes for SDK call sites, detects multipliers (loops, .map(), Promise.all, cron schedules, batch sizes), and projects monthly cost ranges using registry pricing data and gotcha-based multipliers.
|
|
19
|
+
- **Cost impact cards**: When a file write contains tracked service SDK calls, a cost impact card is injected into Claude's context with estimated monthly cost, current budget status, and cheaper alternatives.
|
|
20
|
+
- **Cumulative session cost tracking**: Session cost impacts are accumulated across file changes and reported in the Stop hook.
|
|
21
|
+
- **Projected impact in ledger**: The spend ledger now includes a "projected impact" row showing session cost estimates.
|
|
22
|
+
- **New `excluded` confidence tier**: Services explicitly excluded by the user show ⬚ SKIP instead of 🔴 BLIND.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Registry version bumped to 0.2.0 with plan tier data.
|
|
27
|
+
- CLI now parses `--non-interactive` and `--ni` flags.
|
|
28
|
+
- PostToolUse hook expanded from detection-only to detection + cost impact analysis.
|
|
29
|
+
- Stop hook now reads and reports cumulative session cost impacts.
|
|
30
|
+
|
|
8
31
|
## [0.1.0] - 2026-03-24
|
|
9
32
|
|
|
10
33
|
### Added
|
|
@@ -20,4 +43,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
43
|
- Snapshot system for delta computation across sessions
|
|
21
44
|
- Claude Code skills: `/spend` (on-demand brief), `/setup-burnwatch` (guided onboarding)
|
|
22
45
|
|
|
46
|
+
[0.4.0]: https://github.com/RaleighSF/burnwatch/compare/v0.1.0...v0.4.0
|
|
23
47
|
[0.1.0]: https://github.com/RaleighSF/burnwatch/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -12,24 +12,24 @@
|
|
|
12
12
|
|
|
13
13
|
<br>
|
|
14
14
|
|
|
15
|
-
burnwatch detects every paid service in your project, tracks what you're spending, and injects budget context directly into your AI coding sessions
|
|
15
|
+
burnwatch detects every paid service in your project, tracks what you're spending, and injects budget context directly into your AI coding sessions - so the agent knows what things cost before it recommends burning more money.
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
|
|
19
|
-
║ BURNWATCH
|
|
20
|
-
|
|
21
|
-
║ Service Spend Conf
|
|
22
|
-
║ ──────────────────────────────────────────────────────────
|
|
23
|
-
║ Anthropic $47.20 ✅ LIVE
|
|
24
|
-
║ Vercel $23.00 ✅ LIVE
|
|
25
|
-
║ Scrapfly $127.00 ✅ LIVE
|
|
26
|
-
║ Browserbase ~$63.00 🟠 EST
|
|
27
|
-
║ Supabase $25.00 ✅ LIVE
|
|
28
|
-
║ PostHog ~$49.00 🟡 CALC
|
|
29
|
-
|
|
30
|
-
║ TOTAL: ~$334.20 Untracked: 0 ✅ Est margin: ±$20
|
|
31
|
-
║ 🚨
|
|
32
|
-
|
|
18
|
+
╔══════════════════════════════════════════════════════════════
|
|
19
|
+
║ BURNWATCH - your-app - March 2026
|
|
20
|
+
╠══════════════════════════════════════════════════════════════
|
|
21
|
+
║ Service Spend Conf Budget Left
|
|
22
|
+
║ ──────────────────────────────────────────────────────────
|
|
23
|
+
║ Anthropic $47.20 ✅ LIVE $100 53%
|
|
24
|
+
║ Vercel $23.00 ✅ LIVE $50 54%
|
|
25
|
+
║ Scrapfly $127.00 ✅ LIVE $50 ⚠️ OVR
|
|
26
|
+
║ Browserbase ~$63.00 🟠 EST $75 16%
|
|
27
|
+
║ Supabase $25.00 ✅ LIVE $100 75%
|
|
28
|
+
║ PostHog ~$49.00 🟡 CALC $49 0%
|
|
29
|
+
╠══════════════════════════════════════════════════════════════
|
|
30
|
+
║ TOTAL: ~$334.20 Untracked: 0 ✅ Est margin: ±$20
|
|
31
|
+
║ 🚨 SCRAPFLY 254% OVER BUDGET - review before use
|
|
32
|
+
╚══════════════════════════════════════════════════════════════
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
This brief appears automatically at the start of every [Claude Code](https://claude.ai/code) session. You don't open a dashboard. You don't remember to check anything. You just see what you're spending.
|
|
@@ -38,7 +38,7 @@ This brief appears automatically at the start of every [Claude Code](https://cla
|
|
|
38
38
|
|
|
39
39
|
## Why
|
|
40
40
|
|
|
41
|
-
Agentic development lets you ship 10x faster. It also lets you burn through $400 in Scrapfly credits, rack up unexpected Browserbase bills, and discover PostHog overages three weeks after the code that caused them was written
|
|
41
|
+
Agentic development lets you ship 10x faster. It also lets you burn through $400 in Scrapfly credits, rack up unexpected Browserbase bills, and discover PostHog overages three weeks after the code that caused them was written - by an agent, in a session you barely remember.
|
|
42
42
|
|
|
43
43
|
**78% of IT leaders experienced unexpected charges** tied to consumption-based or AI pricing in the past 12 months ([Zylo 2026 SaaS Management Index](https://zylo.com/research/saas-management-index/)).
|
|
44
44
|
|
|
@@ -56,7 +56,7 @@ npx burnwatch init
|
|
|
56
56
|
|
|
57
57
|
That's it. burnwatch scans your project, detects paid services, creates a `.burnwatch/` directory, and registers Claude Code hooks. Next time you start a session, you see your spend.
|
|
58
58
|
|
|
59
|
-
> **Requirements:** Node.js 18+
|
|
59
|
+
> **Requirements:** Node.js 18+ · Zero dependencies · Works with or without Claude Code
|
|
60
60
|
|
|
61
61
|
<br>
|
|
62
62
|
|
|
@@ -91,12 +91,12 @@ npx burnwatch init
|
|
|
91
91
|
### 2. Add API keys and budgets
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
# LIVE tracking
|
|
94
|
+
# LIVE tracking - real billing API data
|
|
95
95
|
burnwatch add anthropic --key $ANTHROPIC_ADMIN_KEY --budget 100
|
|
96
96
|
burnwatch add scrapfly --key $SCRAPFLY_KEY --budget 50
|
|
97
97
|
burnwatch add vercel --token $VERCEL_TOKEN --budget 50
|
|
98
98
|
|
|
99
|
-
# CALC tracking
|
|
99
|
+
# CALC tracking - flat-rate services
|
|
100
100
|
burnwatch add posthog --plan-cost 0 --budget 0
|
|
101
101
|
burnwatch add inngest --plan-cost 25 --budget 25
|
|
102
102
|
|
|
@@ -119,13 +119,13 @@ Start a Claude Code session. The spend brief appears automatically. When you men
|
|
|
119
119
|
```
|
|
120
120
|
You: "Use Scrapfly to scrape the competitor pricing pages"
|
|
121
121
|
|
|
122
|
-
[BURNWATCH] scrapfly
|
|
122
|
+
[BURNWATCH] scrapfly - current period
|
|
123
123
|
Spend: $127.00 | Budget: $50 | ⚠️ 254% over
|
|
124
124
|
Confidence: ✅ LIVE
|
|
125
125
|
⚠️ 254% of budget consumed
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
Claude factors this into its response
|
|
128
|
+
Claude factors this into its response - it might suggest Cheerio instead, or warn you before proceeding.
|
|
129
129
|
|
|
130
130
|
When a new paid service enters your project (new dependency, new env var, new import), burnwatch alerts immediately:
|
|
131
131
|
|
|
@@ -138,7 +138,7 @@ When a new paid service enters your project (new dependency, new env var, new im
|
|
|
138
138
|
|
|
139
139
|
## How It Works
|
|
140
140
|
|
|
141
|
-
burnwatch runs as [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks)
|
|
141
|
+
burnwatch runs as [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) - background scripts that fire on session lifecycle events. It never proxies your traffic. It never intercepts API calls. It watches the exhaust of your sessions silently, completely, and without interrupting the work.
|
|
142
142
|
|
|
143
143
|
### Four Detection Surfaces
|
|
144
144
|
|
|
@@ -172,18 +172,18 @@ If burnwatch can't track a service accurately, it says so. The ledger always sho
|
|
|
172
172
|
|
|
173
173
|
### The Ledger
|
|
174
174
|
|
|
175
|
-
burnwatch writes `.burnwatch/spend-ledger.md` at the end of every session
|
|
175
|
+
burnwatch writes `.burnwatch/spend-ledger.md` at the end of every session - human-readable, git-committable, designed to be read in 10 seconds:
|
|
176
176
|
|
|
177
177
|
```markdown
|
|
178
|
-
# Burnwatch Ledger
|
|
178
|
+
# Burnwatch Ledger - your-app
|
|
179
179
|
Last updated: 2026-03-24T14:32:11Z
|
|
180
180
|
|
|
181
181
|
## This Month (March 2026)
|
|
182
182
|
| Service | Spend | Conf | Budget | Status |
|
|
183
183
|
|---------|-------|------|--------|--------|
|
|
184
|
-
| Anthropic | $47.20 | ✅ LIVE | $100 | 53%
|
|
184
|
+
| Anthropic | $47.20 | ✅ LIVE | $100 | 53% - healthy |
|
|
185
185
|
| Scrapfly | $127.00 | ✅ LIVE | $50 | ⚠️ 254% over |
|
|
186
|
-
| Vercel | $23.00 | ✅ LIVE | $50 | 54%
|
|
186
|
+
| Vercel | $23.00 | ✅ LIVE | $50 | 54% - healthy |
|
|
187
187
|
|
|
188
188
|
## TOTAL: ~$209.70 (±$2 estimated margin)
|
|
189
189
|
## Untracked services: 0
|
|
@@ -216,7 +216,7 @@ Last updated: 2026-03-24T14:32:11Z
|
|
|
216
216
|
|
|
217
217
|
## How the Agent Changes Behavior
|
|
218
218
|
|
|
219
|
-
The real power isn't showing _you_ what you spent
|
|
219
|
+
The real power isn't showing _you_ what you spent - it's telling _the agent_ what everything costs, in context, so cost becomes a factor in every recommendation.
|
|
220
220
|
|
|
221
221
|
When Claude sees `Scrapfly: $127 / $50 budget, 254% over` in its context, it:
|
|
222
222
|
|
|
@@ -281,7 +281,7 @@ burnwatch doesn't need to run in every session. It takes snapshots when present
|
|
|
281
281
|
burnwatch reconcile
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
Re-scans your project for services introduced in sessions where burnwatch wasn't active. For billing APIs that expose cumulative usage (like Scrapfly's credit counter), it computes the delta between snapshots
|
|
284
|
+
Re-scans your project for services introduced in sessions where burnwatch wasn't active. For billing APIs that expose cumulative usage (like Scrapfly's credit counter), it computes the delta between snapshots - attributing spend across the gap.
|
|
285
285
|
|
|
286
286
|
<br>
|
|
287
287
|
|
|
@@ -304,7 +304,7 @@ Re-scans your project for services introduced in sessions where burnwatch wasn't
|
|
|
304
304
|
}
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
-
The `gotchas` and `alternatives` fields aren't just metadata
|
|
307
|
+
The `gotchas` and `alternatives` fields aren't just metadata - the agent reads them and uses them to make better recommendations. Every PR that adds a service makes burnwatch smarter for every user.
|
|
308
308
|
|
|
309
309
|
<br>
|
|
310
310
|
|
package/dist/cli.js
CHANGED
|
@@ -579,7 +579,8 @@ var CONFIDENCE_BADGES = {
|
|
|
579
579
|
live: "\u2705 LIVE",
|
|
580
580
|
calc: "\u{1F7E1} CALC",
|
|
581
581
|
est: "\u{1F7E0} EST",
|
|
582
|
-
blind: "\u{1F534} BLIND"
|
|
582
|
+
blind: "\u{1F534} BLIND",
|
|
583
|
+
excluded: "\u2B1A SKIP"
|
|
583
584
|
};
|
|
584
585
|
|
|
585
586
|
// src/core/brief.ts
|
|
@@ -744,6 +745,14 @@ function writeLedger(brief, projectRoot) {
|
|
|
744
745
|
`| ${svc.serviceId} | ${spendStr} | ${badge} | ${budgetStr} | ${svc.statusLabel} |`
|
|
745
746
|
);
|
|
746
747
|
}
|
|
748
|
+
const impactAlert = brief.alerts.find(
|
|
749
|
+
(a) => a.serviceId === "_session_impact"
|
|
750
|
+
);
|
|
751
|
+
if (impactAlert) {
|
|
752
|
+
lines.push(
|
|
753
|
+
`| _projected impact_ | \u2014 | \u{1F4C8} EST | \u2014 | ${impactAlert.message} |`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
747
756
|
lines.push("");
|
|
748
757
|
const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
|
|
749
758
|
const marginStr = brief.estimateMargin > 0 ? ` (\xB1$${brief.estimateMargin.toFixed(0)} estimated margin)` : "";
|
|
@@ -776,9 +785,188 @@ function saveSnapshot(brief, projectRoot) {
|
|
|
776
785
|
);
|
|
777
786
|
}
|
|
778
787
|
|
|
788
|
+
// src/interactive-init.ts
|
|
789
|
+
import * as readline from "readline";
|
|
790
|
+
var RISK_ORDER = ["llm", "usage", "infra", "flat"];
|
|
791
|
+
var RISK_LABELS = {
|
|
792
|
+
llm: "\u{1F916} LLM / AI Services (highest variable cost)",
|
|
793
|
+
usage: "\u{1F4CA} Usage-Based Services",
|
|
794
|
+
infra: "\u{1F3D7}\uFE0F Infrastructure & Compute",
|
|
795
|
+
flat: "\u{1F4E6} Flat-Rate / Free Tier Services"
|
|
796
|
+
};
|
|
797
|
+
function classifyRisk(service) {
|
|
798
|
+
if (service.billingModel === "token_usage") return "llm";
|
|
799
|
+
if (service.billingModel === "credit_pool" || service.billingModel === "percentage" || service.billingModel === "per_unit")
|
|
800
|
+
return "usage";
|
|
801
|
+
if (service.billingModel === "compute") return "infra";
|
|
802
|
+
return "flat";
|
|
803
|
+
}
|
|
804
|
+
function groupByRisk(detected) {
|
|
805
|
+
const groups = /* @__PURE__ */ new Map();
|
|
806
|
+
for (const cat of RISK_ORDER) {
|
|
807
|
+
groups.set(cat, []);
|
|
808
|
+
}
|
|
809
|
+
for (const det of detected) {
|
|
810
|
+
const cat = classifyRisk(det.service);
|
|
811
|
+
groups.get(cat).push(det);
|
|
812
|
+
}
|
|
813
|
+
return groups;
|
|
814
|
+
}
|
|
815
|
+
function ask(rl, question) {
|
|
816
|
+
return new Promise((resolve3) => {
|
|
817
|
+
rl.question(question, (answer) => {
|
|
818
|
+
resolve3(answer.trim());
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
async function autoDetectScrapflyPlan(apiKey) {
|
|
823
|
+
try {
|
|
824
|
+
const result = await fetchJson(`https://api.scrapfly.io/account?key=${apiKey}`);
|
|
825
|
+
if (result.ok && result.data?.subscription?.plan?.name) {
|
|
826
|
+
return result.data.subscription.plan.name;
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
async function runInteractiveInit(detected) {
|
|
833
|
+
const rl = readline.createInterface({
|
|
834
|
+
input: process.stdin,
|
|
835
|
+
output: process.stdout
|
|
836
|
+
});
|
|
837
|
+
const services = {};
|
|
838
|
+
const groups = groupByRisk(detected);
|
|
839
|
+
const globalConfig = readGlobalConfig();
|
|
840
|
+
console.log(
|
|
841
|
+
"\n\u{1F4CB} Let's configure each detected service. Services are grouped by cost risk.\n"
|
|
842
|
+
);
|
|
843
|
+
for (const category of RISK_ORDER) {
|
|
844
|
+
const group = groups.get(category);
|
|
845
|
+
if (group.length === 0) continue;
|
|
846
|
+
console.log(`
|
|
847
|
+
${RISK_LABELS[category]}`);
|
|
848
|
+
console.log("\u2500".repeat(50));
|
|
849
|
+
for (const det of group) {
|
|
850
|
+
const service = det.service;
|
|
851
|
+
const plans = service.plans;
|
|
852
|
+
console.log(`
|
|
853
|
+
${service.name}`);
|
|
854
|
+
console.log(` Detected via: ${det.details.join(", ")}`);
|
|
855
|
+
if (!plans || plans.length === 0) {
|
|
856
|
+
services[service.id] = {
|
|
857
|
+
serviceId: service.id,
|
|
858
|
+
detectedVia: det.sources,
|
|
859
|
+
hasApiKey: false,
|
|
860
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString()
|
|
861
|
+
};
|
|
862
|
+
console.log(" \u2192 Auto-configured (no plan tiers available)");
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const defaultIndex = plans.findIndex((p) => p.default);
|
|
866
|
+
console.log("");
|
|
867
|
+
for (let i = 0; i < plans.length; i++) {
|
|
868
|
+
const plan = plans[i];
|
|
869
|
+
const marker = i === defaultIndex ? " (recommended)" : "";
|
|
870
|
+
const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? ` \u2014 $${plan.monthlyBase}/mo` : " \u2014 variable";
|
|
871
|
+
console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);
|
|
872
|
+
}
|
|
873
|
+
const defaultChoice = defaultIndex >= 0 ? String(defaultIndex + 1) : "1";
|
|
874
|
+
const answer = await ask(
|
|
875
|
+
rl,
|
|
876
|
+
` Choose [${defaultChoice}]: `
|
|
877
|
+
);
|
|
878
|
+
const choiceIndex = (answer === "" ? parseInt(defaultChoice) : parseInt(answer)) - 1;
|
|
879
|
+
const chosen = plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0];
|
|
880
|
+
if (chosen.type === "exclude") {
|
|
881
|
+
services[service.id] = {
|
|
882
|
+
serviceId: service.id,
|
|
883
|
+
detectedVia: det.sources,
|
|
884
|
+
hasApiKey: false,
|
|
885
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
886
|
+
excluded: true,
|
|
887
|
+
planName: chosen.name
|
|
888
|
+
};
|
|
889
|
+
console.log(` \u2192 ${service.name}: excluded from tracking`);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const tracked = {
|
|
893
|
+
serviceId: service.id,
|
|
894
|
+
detectedVia: det.sources,
|
|
895
|
+
hasApiKey: false,
|
|
896
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
897
|
+
planName: chosen.name
|
|
898
|
+
};
|
|
899
|
+
if (chosen.type === "flat" && chosen.monthlyBase !== void 0) {
|
|
900
|
+
tracked.planCost = chosen.monthlyBase;
|
|
901
|
+
if (chosen.monthlyBase > 0) {
|
|
902
|
+
tracked.budget = chosen.monthlyBase;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (service.apiTier === "live" || chosen.requiresKey) {
|
|
906
|
+
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
907
|
+
if (existingKey) {
|
|
908
|
+
console.log(` \u{1F510} Using existing API key from global config`);
|
|
909
|
+
tracked.hasApiKey = true;
|
|
910
|
+
if (service.autoDetectPlan && service.id === "scrapfly") {
|
|
911
|
+
console.log(" \u{1F50D} Auto-detecting plan from API...");
|
|
912
|
+
const planName = await autoDetectScrapflyPlan(existingKey);
|
|
913
|
+
if (planName) {
|
|
914
|
+
console.log(` \u2192 Detected plan: ${planName}`);
|
|
915
|
+
tracked.planName = planName;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
} else if (chosen.requiresKey) {
|
|
919
|
+
const keyAnswer = await ask(
|
|
920
|
+
rl,
|
|
921
|
+
` Enter API key (or press Enter to skip): `
|
|
922
|
+
);
|
|
923
|
+
if (keyAnswer) {
|
|
924
|
+
tracked.hasApiKey = true;
|
|
925
|
+
if (!globalConfig.services[service.id]) {
|
|
926
|
+
globalConfig.services[service.id] = {};
|
|
927
|
+
}
|
|
928
|
+
globalConfig.services[service.id].apiKey = keyAnswer;
|
|
929
|
+
if (service.autoDetectPlan && service.id === "scrapfly") {
|
|
930
|
+
console.log(" \u{1F50D} Auto-detecting plan from API...");
|
|
931
|
+
const planName = await autoDetectScrapflyPlan(keyAnswer);
|
|
932
|
+
if (planName) {
|
|
933
|
+
console.log(` \u2192 Detected plan: ${planName}`);
|
|
934
|
+
tracked.planName = planName;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (tracked.budget === void 0 || tracked.budget === 0) {
|
|
941
|
+
const suggestion = chosen.monthlyBase && chosen.monthlyBase > 0 ? ` [${chosen.monthlyBase}]` : "";
|
|
942
|
+
const budgetAnswer = await ask(
|
|
943
|
+
rl,
|
|
944
|
+
` Monthly budget in USD${suggestion} (or press Enter to skip): $`
|
|
945
|
+
);
|
|
946
|
+
if (budgetAnswer) {
|
|
947
|
+
const budget = parseFloat(budgetAnswer);
|
|
948
|
+
if (!isNaN(budget)) {
|
|
949
|
+
tracked.budget = budget;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
services[service.id] = tracked;
|
|
954
|
+
const tierLabel = tracked.hasApiKey ? "\u2705 LIVE" : tracked.planCost !== void 0 ? "\u{1F7E1} CALC" : "\u{1F534} BLIND";
|
|
955
|
+
const budgetStr = tracked.budget !== void 0 ? ` | Budget: $${tracked.budget}/mo` : "";
|
|
956
|
+
console.log(
|
|
957
|
+
` \u2192 ${service.name}: ${chosen.name} (${tierLabel}${budgetStr})`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
writeGlobalConfig(globalConfig);
|
|
962
|
+
rl.close();
|
|
963
|
+
return { services };
|
|
964
|
+
}
|
|
965
|
+
|
|
779
966
|
// src/cli.ts
|
|
780
967
|
var args = process.argv.slice(2);
|
|
781
968
|
var command = args[0];
|
|
969
|
+
var flags = new Set(args.slice(1));
|
|
782
970
|
async function main() {
|
|
783
971
|
switch (command) {
|
|
784
972
|
case "init":
|
|
@@ -818,6 +1006,7 @@ async function main() {
|
|
|
818
1006
|
}
|
|
819
1007
|
async function cmdInit() {
|
|
820
1008
|
const projectRoot = process.cwd();
|
|
1009
|
+
const nonInteractive = flags.has("--non-interactive") || flags.has("--ni");
|
|
821
1010
|
if (isInitialized(projectRoot)) {
|
|
822
1011
|
console.log("\u2705 burnwatch is already initialized in this project.");
|
|
823
1012
|
console.log(` Config: ${projectConfigDir(projectRoot)}/config.json`);
|
|
@@ -839,14 +1028,32 @@ async function cmdInit() {
|
|
|
839
1028
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
840
1029
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
841
1030
|
};
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1031
|
+
if (!nonInteractive && detected.length > 0 && process.stdin.isTTY) {
|
|
1032
|
+
const result = await runInteractiveInit(detected);
|
|
1033
|
+
config.services = result.services;
|
|
1034
|
+
} else {
|
|
1035
|
+
for (const det of detected) {
|
|
1036
|
+
const tracked2 = {
|
|
1037
|
+
serviceId: det.service.id,
|
|
1038
|
+
detectedVia: det.sources,
|
|
1039
|
+
hasApiKey: false,
|
|
1040
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString()
|
|
1041
|
+
};
|
|
1042
|
+
config.services[det.service.id] = tracked2;
|
|
1043
|
+
}
|
|
1044
|
+
if (detected.length === 0) {
|
|
1045
|
+
console.log(" No paid services detected yet.");
|
|
1046
|
+
console.log(" Services will be detected as they enter your project.\n");
|
|
1047
|
+
} else {
|
|
1048
|
+
console.log(` Found ${detected.length} paid service${detected.length > 1 ? "s" : ""}:
|
|
1049
|
+
`);
|
|
1050
|
+
for (const det of detected) {
|
|
1051
|
+
const tierBadge = det.service.apiTier === "live" ? "\u2705 LIVE API available" : det.service.apiTier === "calc" ? "\u{1F7E1} Flat-rate tracking" : det.service.apiTier === "est" ? "\u{1F7E0} Estimate tracking" : "\u{1F534} Detection only";
|
|
1052
|
+
console.log(` \u2022 ${det.service.name} (${tierBadge})`);
|
|
1053
|
+
console.log(` Detected via: ${det.details.join(", ")}`);
|
|
1054
|
+
}
|
|
1055
|
+
console.log("");
|
|
1056
|
+
}
|
|
850
1057
|
}
|
|
851
1058
|
writeProjectConfig(config, projectRoot);
|
|
852
1059
|
const gitignorePath = path5.join(projectConfigDir(projectRoot), ".gitignore");
|
|
@@ -861,29 +1068,29 @@ async function cmdInit() {
|
|
|
861
1068
|
].join("\n"),
|
|
862
1069
|
"utf-8"
|
|
863
1070
|
);
|
|
864
|
-
|
|
865
|
-
console.log(" No paid services detected yet.");
|
|
866
|
-
console.log(" Services will be detected as they enter your project.\n");
|
|
867
|
-
} else {
|
|
868
|
-
console.log(` Found ${detected.length} paid service${detected.length > 1 ? "s" : ""}:
|
|
869
|
-
`);
|
|
870
|
-
for (const det of detected) {
|
|
871
|
-
const tierBadge = det.service.apiTier === "live" ? "\u2705 LIVE API available" : det.service.apiTier === "calc" ? "\u{1F7E1} Flat-rate tracking" : det.service.apiTier === "est" ? "\u{1F7E0} Estimate tracking" : "\u{1F534} Detection only";
|
|
872
|
-
console.log(` \u2022 ${det.service.name} (${tierBadge})`);
|
|
873
|
-
console.log(` Detected via: ${det.details.join(", ")}`);
|
|
874
|
-
}
|
|
875
|
-
console.log("");
|
|
876
|
-
}
|
|
877
|
-
console.log("\u{1F517} Registering Claude Code hooks...\n");
|
|
1071
|
+
console.log("\n\u{1F517} Registering Claude Code hooks...\n");
|
|
878
1072
|
registerHooks(projectRoot);
|
|
1073
|
+
const excluded = Object.values(config.services).filter((s) => s.excluded);
|
|
1074
|
+
const tracked = Object.values(config.services).filter((s) => !s.excluded);
|
|
879
1075
|
console.log("\u2705 burnwatch initialized!\n");
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1076
|
+
if (tracked.length > 0) {
|
|
1077
|
+
console.log(` Tracking ${tracked.length} service${tracked.length > 1 ? "s" : ""}`);
|
|
1078
|
+
for (const svc of tracked) {
|
|
1079
|
+
const planStr = svc.planName ? ` (${svc.planName})` : "";
|
|
1080
|
+
const budgetStr = svc.budget !== void 0 ? ` \u2014 $${svc.budget}/mo budget` : "";
|
|
1081
|
+
console.log(` \u2022 ${svc.serviceId}${planStr}${budgetStr}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (excluded.length > 0) {
|
|
1085
|
+
console.log(`
|
|
1086
|
+
Excluded ${excluded.length} service${excluded.length > 1 ? "s" : ""}:`);
|
|
1087
|
+
for (const svc of excluded) {
|
|
1088
|
+
console.log(` \u2022 ${svc.serviceId}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
console.log("\nNext steps:");
|
|
1092
|
+
console.log(" burnwatch status \u2014 Check your spend");
|
|
1093
|
+
console.log(" burnwatch add <svc> \u2014 Configure additional services\n");
|
|
887
1094
|
}
|
|
888
1095
|
async function cmdAdd() {
|
|
889
1096
|
const projectRoot = process.cwd();
|
|
@@ -1045,7 +1252,8 @@ function cmdHelp() {
|
|
|
1045
1252
|
burnwatch \u2014 Passive cost memory for vibe coding
|
|
1046
1253
|
|
|
1047
1254
|
Usage:
|
|
1048
|
-
burnwatch init
|
|
1255
|
+
burnwatch init Interactive setup \u2014 pick plans per service
|
|
1256
|
+
burnwatch init --non-interactive Auto-detect services, no prompts
|
|
1049
1257
|
burnwatch setup Init + auto-configure all detected services
|
|
1050
1258
|
burnwatch add <service> [options] Register a service for tracking
|
|
1051
1259
|
burnwatch status Show current spend brief
|
|
@@ -1060,6 +1268,7 @@ Options for 'add':
|
|
|
1060
1268
|
|
|
1061
1269
|
Examples:
|
|
1062
1270
|
burnwatch init
|
|
1271
|
+
burnwatch init --non-interactive
|
|
1063
1272
|
burnwatch add anthropic --key sk-ant-admin-xxx --budget 100
|
|
1064
1273
|
burnwatch add scrapfly --key scp-xxx --budget 50
|
|
1065
1274
|
burnwatch add posthog --plan-cost 0 --budget 0
|