burnwatch 0.10.1 → 0.12.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/CHANGELOG.md +63 -0
- package/dist/cli.js +119 -27
- package/dist/cli.js.map +1 -1
- package/dist/cost-impact.js +54 -0
- package/dist/cost-impact.js.map +1 -1
- package/dist/hooks/on-file-change.js +54 -0
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-prompt.js +24 -0
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +82 -42
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +68 -6
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +55 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +65 -10
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/registry.json +286 -2
- package/skills/burnwatch-interview/SKILL.md +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,65 @@ 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.12.0] - 2026-03-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **10 new services in registry** (14 → 24): Firebase, Cloudflare, Neon Postgres, MongoDB Atlas, Twilio, SendGrid, Sentry, Clerk, DeepSeek, Replicate. All with plans, pricing, gotchas, detection patterns, and cost-impact SDK call patterns.
|
|
13
|
+
- **EST tier activated**: BLIND services with accumulated cost-impact data from file change analysis are now upgraded to EST tier in the spend brief. Uses the midpoint of projected cost range. The EST tier was defined since v0.1.0 but never produced — now it is.
|
|
14
|
+
- **Connector and probe tests**: 20 new tests with mocked HTTP covering all 6 billing connectors and 9 service probes. Total test count: 40 → 60.
|
|
15
|
+
- **Registry cache invalidation**: The registry cache now tracks file mtime and auto-invalidates when `registry.json` is modified. Prevents stale data in long-running processes (MCP server).
|
|
16
|
+
- **Community contribution guide**: Rewrote `CONTRIBUTING.md` with tiered contribution path — registry-only (5 min, JSON), billing connector (30 min, TypeScript), and probe (plan auto-detection). Documented `.burnwatch/registry.json` project-local override for custom services.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Registry `apiTier` reconciled with actual connectors**: `browserbase` changed from `"est"` to `"live"` (has a working connector). `stripe` changed from `"live"` to `"calc"` (no billing connector — only has a probe for balance checking, not Stripe's fees to you).
|
|
21
|
+
- **MCP server version**: Was hardcoded at `"0.1.2"` since initial release. Now reads dynamically from `package.json`.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Cost-impact SDK call patterns added for Firebase, Twilio, SendGrid, MongoDB, Clerk, and Replicate.
|
|
26
|
+
|
|
27
|
+
## [0.11.0] - 2026-03-25
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **CALC services showed ~$0.00 in session brief**: Session-start hook was passing raw `planCost` as spend instead of projecting based on day of month. Now correctly calculates `(planCost / daysInMonth) * dayOfMonth`.
|
|
32
|
+
- **envKeysFound always empty**: `.env` file scanning only ran conditionally. Now always scans `.env`, `.env.local`, `.env.development` on disk and populates `envKeysFound` regardless of global key status.
|
|
33
|
+
- **Box-drawing characters render poorly in agent/IDE contexts**: Added `formatBriefMarkdown()` using markdown tables. Session-start hook now uses markdown format.
|
|
34
|
+
- **Excluded services showing as BLIND in brief**: Added `filter(s => s.tier !== "excluded")` to brief display.
|
|
35
|
+
- **`configure --key` silent when LIVE not possible**: Now returns `tierNote` explaining why a key didn't enable LIVE tracking (e.g., no billing connector for that service).
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`burnwatch reset` command**: Removes `.burnwatch/`, skills from `.claude/skills/`, and hooks from `.claude/settings.json`. Preserves global API keys in `~/.config/burnwatch/`.
|
|
40
|
+
- **Interview JSON enrichment**: `hasConnector`, `canGoLive`, `envKeysFound[]`, `suggestedAction`, and `instructions{}` fields guide agent behavior during the interview.
|
|
41
|
+
|
|
42
|
+
## [0.10.0] - 2026-03-25
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **Supabase billing connector**: Uses Management API via Personal Access Token (PAT). Detects plan tier (free/pro/team), maps to monthly cost, and checks usage endpoint for overages.
|
|
47
|
+
- **Browserbase billing connector**: Uses projects/usage API to track session count and browser minutes. Estimates spend from minutes × rate.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **Interview skill rewritten**: Complete rewrite of `/burnwatch-interview` SKILL.md. Now leads with API key discovery from `.env` files BEFORE asking questions. Enforces one-service-at-a-time pacing. Includes smart inference (e.g., `gpt-4o-mini` → low budget suggestion). Budget philosophy: LIVE → track actual + alert threshold; CALC → budget = plan cost; BLIND → safety net budget.
|
|
52
|
+
- **BLIND services no longer show $0.00**: Shows "—" instead of "$0.00" to avoid false confidence. Status label says "needs API key" instead of "no budget".
|
|
53
|
+
- **Brief footer says "No billing data: N"** instead of "Untracked: N" — because services ARE configured with budgets, they just lack billing API access.
|
|
54
|
+
- **`pollService` returns explicit error context** when LIVE fails instead of silently falling through to CALC/BLIND.
|
|
55
|
+
|
|
56
|
+
## [0.9.0] - 2026-03-25
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **Skill auto-installation during init**: `registerHooks()` now copies `/burnwatch-interview`, `/setup-burnwatch`, and `/spend` skills to `.claude/skills/` so agents can discover them.
|
|
61
|
+
- **Non-TTY init suggests `/burnwatch-interview`**: When running in an agent context, init output recommends the conversational interview skill instead of manual CLI commands.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **Terminology update**: All references to "vibe coding" changed to "AI-assisted development" across package description, README, llms.txt, and source files.
|
|
66
|
+
|
|
8
67
|
## [0.8.0] - 2026-03-25
|
|
9
68
|
|
|
10
69
|
### Added
|
|
@@ -133,6 +192,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
133
192
|
- Snapshot system for delta computation across sessions
|
|
134
193
|
- Claude Code skills: `/spend` (on-demand brief), `/setup-burnwatch` (guided onboarding)
|
|
135
194
|
|
|
195
|
+
[0.12.0]: https://github.com/RaleighSF/burnwatch/compare/v0.11.0...v0.12.0
|
|
196
|
+
[0.11.0]: https://github.com/RaleighSF/burnwatch/compare/v0.10.0...v0.11.0
|
|
197
|
+
[0.10.0]: https://github.com/RaleighSF/burnwatch/compare/v0.9.0...v0.10.0
|
|
198
|
+
[0.9.0]: https://github.com/RaleighSF/burnwatch/compare/v0.8.0...v0.9.0
|
|
136
199
|
[0.8.0]: https://github.com/RaleighSF/burnwatch/compare/v0.7.0...v0.8.0
|
|
137
200
|
[0.7.0]: https://github.com/RaleighSF/burnwatch/compare/v0.6.0...v0.7.0
|
|
138
201
|
[0.6.0]: https://github.com/RaleighSF/burnwatch/compare/v0.5.2...v0.6.0
|
package/dist/cli.js
CHANGED
|
@@ -363,9 +363,22 @@ import * as path2 from "path";
|
|
|
363
363
|
import * as url from "url";
|
|
364
364
|
var __dirname = path2.dirname(url.fileURLToPath(import.meta.url));
|
|
365
365
|
var cachedRegistry = null;
|
|
366
|
+
var cachedRegistryPath = null;
|
|
367
|
+
var cachedRegistryMtime = null;
|
|
366
368
|
function loadRegistry(projectRoot) {
|
|
369
|
+
if (cachedRegistry && cachedRegistryPath && cachedRegistryMtime !== null) {
|
|
370
|
+
try {
|
|
371
|
+
const stat = fs2.statSync(cachedRegistryPath);
|
|
372
|
+
if (stat.mtimeMs !== cachedRegistryMtime) {
|
|
373
|
+
cachedRegistry = null;
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
cachedRegistry = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
367
379
|
if (cachedRegistry) return cachedRegistry;
|
|
368
380
|
const registry = /* @__PURE__ */ new Map();
|
|
381
|
+
let resolvedPath = null;
|
|
369
382
|
const candidates = [
|
|
370
383
|
path2.resolve(__dirname, "../../registry.json"),
|
|
371
384
|
// from src/core/
|
|
@@ -375,6 +388,7 @@ function loadRegistry(projectRoot) {
|
|
|
375
388
|
for (const candidate of candidates) {
|
|
376
389
|
if (fs2.existsSync(candidate)) {
|
|
377
390
|
loadRegistryFile(candidate, registry);
|
|
391
|
+
resolvedPath = candidate;
|
|
378
392
|
break;
|
|
379
393
|
}
|
|
380
394
|
}
|
|
@@ -382,9 +396,19 @@ function loadRegistry(projectRoot) {
|
|
|
382
396
|
const localPath = path2.join(projectRoot, ".burnwatch", "registry.json");
|
|
383
397
|
if (fs2.existsSync(localPath)) {
|
|
384
398
|
loadRegistryFile(localPath, registry);
|
|
399
|
+
resolvedPath = localPath;
|
|
385
400
|
}
|
|
386
401
|
}
|
|
387
402
|
cachedRegistry = registry;
|
|
403
|
+
if (resolvedPath) {
|
|
404
|
+
try {
|
|
405
|
+
cachedRegistryPath = resolvedPath;
|
|
406
|
+
cachedRegistryMtime = fs2.statSync(resolvedPath).mtimeMs;
|
|
407
|
+
} catch {
|
|
408
|
+
cachedRegistryPath = null;
|
|
409
|
+
cachedRegistryMtime = null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
388
412
|
return registry;
|
|
389
413
|
}
|
|
390
414
|
function loadRegistryFile(filePath, registry) {
|
|
@@ -925,9 +949,37 @@ async function pollService(tracked) {
|
|
|
925
949
|
serviceConfig
|
|
926
950
|
);
|
|
927
951
|
if (!result.error) return result;
|
|
928
|
-
|
|
952
|
+
return {
|
|
953
|
+
serviceId: tracked.serviceId,
|
|
954
|
+
spend: tracked.planCost ?? 0,
|
|
955
|
+
isEstimate: true,
|
|
956
|
+
tier: tracked.planCost !== void 0 ? "calc" : "blind",
|
|
957
|
+
error: `LIVE failed (${result.error}) \u2014 showing ${tracked.planCost !== void 0 ? "CALC" : "BLIND"} fallback`
|
|
958
|
+
};
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return {
|
|
961
|
+
serviceId: tracked.serviceId,
|
|
962
|
+
spend: tracked.planCost ?? 0,
|
|
963
|
+
isEstimate: true,
|
|
964
|
+
tier: tracked.planCost !== void 0 ? "calc" : "blind",
|
|
965
|
+
error: `LIVE failed (${err instanceof Error ? err.message : "unknown"}) \u2014 showing ${tracked.planCost !== void 0 ? "CALC" : "BLIND"} fallback`
|
|
966
|
+
};
|
|
929
967
|
}
|
|
930
968
|
}
|
|
969
|
+
if (connector && tracked.hasApiKey && !serviceConfig?.apiKey) {
|
|
970
|
+
const projectedSpend = tracked.planCost !== void 0 ? (() => {
|
|
971
|
+
const now = /* @__PURE__ */ new Date();
|
|
972
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
973
|
+
return tracked.planCost / daysInMonth * now.getDate();
|
|
974
|
+
})() : 0;
|
|
975
|
+
return {
|
|
976
|
+
serviceId: tracked.serviceId,
|
|
977
|
+
spend: projectedSpend,
|
|
978
|
+
isEstimate: true,
|
|
979
|
+
tier: tracked.planCost !== void 0 ? "calc" : "blind",
|
|
980
|
+
error: "API key marked as configured but not found in ~/.config/burnwatch/ \u2014 re-run configure with --key"
|
|
981
|
+
};
|
|
982
|
+
}
|
|
931
983
|
if (tracked.planCost !== void 0) {
|
|
932
984
|
const now = /* @__PURE__ */ new Date();
|
|
933
985
|
const daysInMonth = new Date(
|
|
@@ -999,7 +1051,7 @@ function formatBrief(brief) {
|
|
|
999
1051
|
formatRow("Service", "Spend", "Conf", "Budget", "Left", width)
|
|
1000
1052
|
);
|
|
1001
1053
|
lines.push(`\u2551 ${hrSingle} \u2551`);
|
|
1002
|
-
for (const svc of brief.services) {
|
|
1054
|
+
for (const svc of brief.services.filter((s) => s.tier !== "excluded")) {
|
|
1003
1055
|
const spendStr = svc.tier === "blind" && svc.spend === 0 ? "\u2014" : svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
|
|
1004
1056
|
const badge = CONFIDENCE_BADGES[svc.tier];
|
|
1005
1057
|
const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
|
|
@@ -1568,6 +1620,9 @@ async function main() {
|
|
|
1568
1620
|
case "configure":
|
|
1569
1621
|
await cmdConfigure();
|
|
1570
1622
|
break;
|
|
1623
|
+
case "reset":
|
|
1624
|
+
cmdReset();
|
|
1625
|
+
break;
|
|
1571
1626
|
case "help":
|
|
1572
1627
|
case "--help":
|
|
1573
1628
|
case "-h":
|
|
@@ -1693,37 +1748,32 @@ async function cmdInterview() {
|
|
|
1693
1748
|
let keySource = null;
|
|
1694
1749
|
const envKeysFound = [];
|
|
1695
1750
|
const globalKey = globalConfig.services[serviceId]?.apiKey;
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
if (process.env[pattern]) {
|
|
1701
|
-
keySource = `env:${pattern}`;
|
|
1702
|
-
envKeysFound.push(pattern);
|
|
1703
|
-
break;
|
|
1704
|
-
}
|
|
1751
|
+
for (const pattern of definition.envPatterns) {
|
|
1752
|
+
if (process.env[pattern]) {
|
|
1753
|
+
envKeysFound.push(pattern);
|
|
1754
|
+
if (!keySource && !globalKey) keySource = `env:${pattern}`;
|
|
1705
1755
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
}
|
|
1756
|
+
}
|
|
1757
|
+
const envFiles = [".env", ".env.local", ".env.development"];
|
|
1758
|
+
for (const envFile of envFiles) {
|
|
1759
|
+
try {
|
|
1760
|
+
const envPath = path5.join(projectRoot, envFile);
|
|
1761
|
+
const envContent = fs5.readFileSync(envPath, "utf-8");
|
|
1762
|
+
for (const pattern of definition.envPatterns) {
|
|
1763
|
+
const regex = new RegExp(`^${pattern}=(.+)$`, "m");
|
|
1764
|
+
const match = envContent.match(regex);
|
|
1765
|
+
if (match?.[1]) {
|
|
1766
|
+
const label = `${pattern} (in ${envFile})`;
|
|
1767
|
+
if (!envKeysFound.some((k) => k.startsWith(pattern))) {
|
|
1768
|
+
envKeysFound.push(label);
|
|
1720
1769
|
}
|
|
1721
|
-
if (keySource)
|
|
1722
|
-
} catch {
|
|
1770
|
+
if (!keySource && !globalKey) keySource = `file:${envFile}:${pattern}`;
|
|
1723
1771
|
}
|
|
1724
1772
|
}
|
|
1773
|
+
} catch {
|
|
1725
1774
|
}
|
|
1726
1775
|
}
|
|
1776
|
+
if (globalKey) keySource = "global_config";
|
|
1727
1777
|
let apiKey = globalKey;
|
|
1728
1778
|
if (!apiKey && keySource?.startsWith("env:")) {
|
|
1729
1779
|
apiKey = process.env[keySource.slice(4)];
|
|
@@ -2132,6 +2182,47 @@ async function cmdReconcile() {
|
|
|
2132
2182
|
}
|
|
2133
2183
|
console.log("");
|
|
2134
2184
|
}
|
|
2185
|
+
function cmdReset() {
|
|
2186
|
+
const projectRoot = process.cwd();
|
|
2187
|
+
const burnwatchDir = path5.join(projectRoot, ".burnwatch");
|
|
2188
|
+
const claudeSkillsDir = path5.join(projectRoot, ".claude", "skills");
|
|
2189
|
+
if (!fs5.existsSync(burnwatchDir)) {
|
|
2190
|
+
console.log("burnwatch is not initialized in this project.");
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
fs5.rmSync(burnwatchDir, { recursive: true, force: true });
|
|
2194
|
+
console.log(`\u{1F5D1}\uFE0F Removed ${burnwatchDir}`);
|
|
2195
|
+
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2196
|
+
for (const skill of skillNames) {
|
|
2197
|
+
const skillDir = path5.join(claudeSkillsDir, skill);
|
|
2198
|
+
if (fs5.existsSync(skillDir)) {
|
|
2199
|
+
fs5.rmSync(skillDir, { recursive: true, force: true });
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
console.log("\u{1F5D1}\uFE0F Removed burnwatch skills from .claude/skills/");
|
|
2203
|
+
const settingsPath = path5.join(projectRoot, ".claude", "settings.json");
|
|
2204
|
+
if (fs5.existsSync(settingsPath)) {
|
|
2205
|
+
try {
|
|
2206
|
+
const settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
2207
|
+
const hooks = settings["hooks"];
|
|
2208
|
+
if (hooks) {
|
|
2209
|
+
for (const [event, hookList] of Object.entries(hooks)) {
|
|
2210
|
+
hooks[event] = hookList.filter(
|
|
2211
|
+
(h) => !h.hooks?.some((inner) => inner.command?.includes("burnwatch"))
|
|
2212
|
+
);
|
|
2213
|
+
if (hooks[event].length === 0) delete hooks[event];
|
|
2214
|
+
}
|
|
2215
|
+
if (Object.keys(hooks).length === 0) delete settings["hooks"];
|
|
2216
|
+
fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2217
|
+
console.log("\u{1F5D1}\uFE0F Removed burnwatch hooks from .claude/settings.json");
|
|
2218
|
+
}
|
|
2219
|
+
} catch {
|
|
2220
|
+
console.log("\u26A0\uFE0F Could not clean .claude/settings.json \u2014 remove burnwatch hooks manually");
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
console.log("\n\u2705 burnwatch fully reset. Global API keys in ~/.config/burnwatch/ were preserved.");
|
|
2224
|
+
console.log(" Run 'burnwatch init' to set up again.\n");
|
|
2225
|
+
}
|
|
2135
2226
|
function cmdHelp() {
|
|
2136
2227
|
console.log(`
|
|
2137
2228
|
burnwatch \u2014 Passive cost memory for AI-assisted development
|
|
@@ -2146,6 +2237,7 @@ Usage:
|
|
|
2146
2237
|
burnwatch reconcile Scan for untracked services
|
|
2147
2238
|
burnwatch interview --json Export state for agent-driven interview
|
|
2148
2239
|
burnwatch configure --service <id> [opts] Agent writes back interview answers
|
|
2240
|
+
burnwatch reset Remove all burnwatch config from this project
|
|
2149
2241
|
|
|
2150
2242
|
Options for 'configure':
|
|
2151
2243
|
--service <ID> Service to configure (required)
|