burnwatch 0.11.0 → 0.12.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 +76 -0
- package/dist/cli.js +118 -64
- package/dist/cli.js.map +1 -1
- package/dist/cost-impact.js +54 -0
- package/dist/cost-impact.js.map +1 -1
- package/dist/{detector-CSgHJEdg.d.ts → detector-myYS2eVC.d.ts} +10 -1
- package/dist/hooks/on-file-change.js +69 -2
- 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 +38 -2
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +39 -5
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +72 -4
- package/dist/index.js.map +1 -1
- package/dist/interactive-init.d.ts +1 -1
- package/dist/interactive-init.js +64 -14
- package/dist/interactive-init.js.map +1 -1
- package/dist/mcp-server.js +49 -10
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/registry.json +286 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,77 @@ 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.1] - 2026-03-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`keysFoundInEnv` always 0 in interview JSON**: The env scanner in `burnwatch interview --json` used a fragile regex (`^KEY=(.+)$`) and hardcoded 3 file names. It failed on `export KEY=value` format, spaces around `=`, and missed `.env` files in subdirectories. Now uses the same recursive `findEnvFiles()` + `parseEnvKeys()` as the detector — proven to work since v0.1.0.
|
|
13
|
+
- **`autoConfigureServices` missed keys in `.env` files**: The `findEnvKey()` helper only checked `process.env`, which is empty in agent contexts. Now also scans `.env*` files on disk, finding keys the same way the detector does.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`parseEnvKeys()` utility**: Shared env file parser handles `export` prefix, quoted values, `\r\n` line endings, inline comments, and spaces around `=`. Used by both the detector and interview scanner.
|
|
18
|
+
- **10 new tests** (60 → 70): `parseEnvKeys` edge cases (7 tests), `findEnvFiles` behavior (2 tests), `detectInFileChange` with `export` prefix (1 test).
|
|
19
|
+
|
|
20
|
+
## [0.12.0] - 2026-03-25
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- **Connector and probe tests**: 20 new tests with mocked HTTP covering all 6 billing connectors and 9 service probes. Total test count: 40 → 60.
|
|
27
|
+
- **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).
|
|
28
|
+
- **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.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **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).
|
|
33
|
+
- **MCP server version**: Was hardcoded at `"0.1.2"` since initial release. Now reads dynamically from `package.json`.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Cost-impact SDK call patterns added for Firebase, Twilio, SendGrid, MongoDB, Clerk, and Replicate.
|
|
38
|
+
|
|
39
|
+
## [0.11.0] - 2026-03-25
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **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`.
|
|
44
|
+
- **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.
|
|
45
|
+
- **Box-drawing characters render poorly in agent/IDE contexts**: Added `formatBriefMarkdown()` using markdown tables. Session-start hook now uses markdown format.
|
|
46
|
+
- **Excluded services showing as BLIND in brief**: Added `filter(s => s.tier !== "excluded")` to brief display.
|
|
47
|
+
- **`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).
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`burnwatch reset` command**: Removes `.burnwatch/`, skills from `.claude/skills/`, and hooks from `.claude/settings.json`. Preserves global API keys in `~/.config/burnwatch/`.
|
|
52
|
+
- **Interview JSON enrichment**: `hasConnector`, `canGoLive`, `envKeysFound[]`, `suggestedAction`, and `instructions{}` fields guide agent behavior during the interview.
|
|
53
|
+
|
|
54
|
+
## [0.10.0] - 2026-03-25
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **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.
|
|
59
|
+
- **Browserbase billing connector**: Uses projects/usage API to track session count and browser minutes. Estimates spend from minutes × rate.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **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.
|
|
64
|
+
- **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".
|
|
65
|
+
- **Brief footer says "No billing data: N"** instead of "Untracked: N" — because services ARE configured with budgets, they just lack billing API access.
|
|
66
|
+
- **`pollService` returns explicit error context** when LIVE fails instead of silently falling through to CALC/BLIND.
|
|
67
|
+
|
|
68
|
+
## [0.9.0] - 2026-03-25
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **Skill auto-installation during init**: `registerHooks()` now copies `/burnwatch-interview`, `/setup-burnwatch`, and `/spend` skills to `.claude/skills/` so agents can discover them.
|
|
73
|
+
- **Non-TTY init suggests `/burnwatch-interview`**: When running in an agent context, init output recommends the conversational interview skill instead of manual CLI commands.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **Terminology update**: All references to "vibe coding" changed to "AI-assisted development" across package description, README, llms.txt, and source files.
|
|
78
|
+
|
|
8
79
|
## [0.8.0] - 2026-03-25
|
|
9
80
|
|
|
10
81
|
### Added
|
|
@@ -133,6 +204,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
133
204
|
- Snapshot system for delta computation across sessions
|
|
134
205
|
- Claude Code skills: `/spend` (on-demand brief), `/setup-burnwatch` (guided onboarding)
|
|
135
206
|
|
|
207
|
+
[0.12.1]: https://github.com/RaleighSF/burnwatch/compare/v0.12.0...v0.12.1
|
|
208
|
+
[0.12.0]: https://github.com/RaleighSF/burnwatch/compare/v0.11.0...v0.12.0
|
|
209
|
+
[0.11.0]: https://github.com/RaleighSF/burnwatch/compare/v0.10.0...v0.11.0
|
|
210
|
+
[0.10.0]: https://github.com/RaleighSF/burnwatch/compare/v0.9.0...v0.10.0
|
|
211
|
+
[0.9.0]: https://github.com/RaleighSF/burnwatch/compare/v0.8.0...v0.9.0
|
|
136
212
|
[0.8.0]: https://github.com/RaleighSF/burnwatch/compare/v0.7.0...v0.8.0
|
|
137
213
|
[0.7.0]: https://github.com/RaleighSF/burnwatch/compare/v0.6.0...v0.7.0
|
|
138
214
|
[0.6.0]: https://github.com/RaleighSF/burnwatch/compare/v0.5.2...v0.6.0
|
package/dist/cli.js
CHANGED
|
@@ -287,8 +287,8 @@ var init_probes = __esm({
|
|
|
287
287
|
});
|
|
288
288
|
|
|
289
289
|
// src/cli.ts
|
|
290
|
-
import * as
|
|
291
|
-
import * as
|
|
290
|
+
import * as fs6 from "fs";
|
|
291
|
+
import * as path6 from "path";
|
|
292
292
|
|
|
293
293
|
// src/core/config.ts
|
|
294
294
|
import * as fs from "fs";
|
|
@@ -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) {
|
|
@@ -478,8 +502,7 @@ function collectEnvVars(projectRoot) {
|
|
|
478
502
|
for (const envFile of envFiles) {
|
|
479
503
|
try {
|
|
480
504
|
const content = fs3.readFileSync(envFile, "utf-8");
|
|
481
|
-
|
|
482
|
-
for (const key of keys) {
|
|
505
|
+
for (const key of parseEnvKeys(content)) {
|
|
483
506
|
envVars.add(key);
|
|
484
507
|
}
|
|
485
508
|
} catch {
|
|
@@ -487,6 +510,19 @@ function collectEnvVars(projectRoot) {
|
|
|
487
510
|
}
|
|
488
511
|
return envVars;
|
|
489
512
|
}
|
|
513
|
+
function parseEnvKeys(content) {
|
|
514
|
+
const keys = /* @__PURE__ */ new Set();
|
|
515
|
+
for (const line of content.split("\n")) {
|
|
516
|
+
const trimmed = line.trim();
|
|
517
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
518
|
+
const stripped = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
519
|
+
const eqIdx = stripped.indexOf("=");
|
|
520
|
+
if (eqIdx > 0) {
|
|
521
|
+
keys.add(stripped.slice(0, eqIdx).trim());
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return keys;
|
|
525
|
+
}
|
|
490
526
|
function findEnvFiles(dir, maxDepth) {
|
|
491
527
|
const results = [];
|
|
492
528
|
if (maxDepth <= 0) return results;
|
|
@@ -1243,6 +1279,8 @@ function saveSnapshot(brief, projectRoot) {
|
|
|
1243
1279
|
|
|
1244
1280
|
// src/interactive-init.ts
|
|
1245
1281
|
import * as readline from "readline";
|
|
1282
|
+
import * as fs5 from "fs";
|
|
1283
|
+
import "path";
|
|
1246
1284
|
init_probes();
|
|
1247
1285
|
function formatUnits(n) {
|
|
1248
1286
|
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
@@ -1289,11 +1327,27 @@ function ask(rl, question) {
|
|
|
1289
1327
|
});
|
|
1290
1328
|
});
|
|
1291
1329
|
}
|
|
1292
|
-
function findEnvKey(service) {
|
|
1330
|
+
function findEnvKey(service, projectRoot = process.cwd()) {
|
|
1293
1331
|
for (const pattern of service.envPatterns) {
|
|
1294
1332
|
const val = process.env[pattern];
|
|
1295
1333
|
if (val && val.length > 0) return val;
|
|
1296
1334
|
}
|
|
1335
|
+
if (projectRoot) {
|
|
1336
|
+
const envFiles = findEnvFiles(projectRoot, 3);
|
|
1337
|
+
for (const filePath of envFiles) {
|
|
1338
|
+
try {
|
|
1339
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
1340
|
+
for (const pattern of service.envPatterns) {
|
|
1341
|
+
const regex = new RegExp(`^(?:export\\s+)?${pattern}\\s*=\\s*(.+)$`, "m");
|
|
1342
|
+
const match = content.match(regex);
|
|
1343
|
+
if (match?.[1]) {
|
|
1344
|
+
return match[1].trim().replace(/^["']|["']$/g, "");
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1297
1351
|
return void 0;
|
|
1298
1352
|
}
|
|
1299
1353
|
async function autoConfigureServices(detected) {
|
|
@@ -1622,10 +1676,10 @@ async function cmdInit() {
|
|
|
1622
1676
|
const projectRoot = process.cwd();
|
|
1623
1677
|
const nonInteractive = flags.has("--non-interactive") || flags.has("--ni");
|
|
1624
1678
|
const alreadyInitialized = isInitialized(projectRoot);
|
|
1625
|
-
let projectName =
|
|
1679
|
+
let projectName = path6.basename(projectRoot);
|
|
1626
1680
|
try {
|
|
1627
|
-
const pkgPath =
|
|
1628
|
-
const pkg = JSON.parse(
|
|
1681
|
+
const pkgPath = path6.join(projectRoot, "package.json");
|
|
1682
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
1629
1683
|
if (pkg.name) projectName = pkg.name;
|
|
1630
1684
|
} catch {
|
|
1631
1685
|
}
|
|
@@ -1664,8 +1718,8 @@ async function cmdInit() {
|
|
|
1664
1718
|
config.services = result.services;
|
|
1665
1719
|
}
|
|
1666
1720
|
writeProjectConfig(config, projectRoot);
|
|
1667
|
-
const gitignorePath =
|
|
1668
|
-
|
|
1721
|
+
const gitignorePath = path6.join(projectConfigDir(projectRoot), ".gitignore");
|
|
1722
|
+
fs6.writeFileSync(
|
|
1669
1723
|
gitignorePath,
|
|
1670
1724
|
[
|
|
1671
1725
|
"# Burnwatch \u2014 ignore cache and snapshots, keep ledger and config",
|
|
@@ -1695,10 +1749,10 @@ async function cmdInterview() {
|
|
|
1695
1749
|
if (!isInitialized(projectRoot)) {
|
|
1696
1750
|
ensureProjectDirs(projectRoot);
|
|
1697
1751
|
const detected = detectServices(projectRoot);
|
|
1698
|
-
let projectName =
|
|
1752
|
+
let projectName = path6.basename(projectRoot);
|
|
1699
1753
|
try {
|
|
1700
1754
|
const pkg = JSON.parse(
|
|
1701
|
-
|
|
1755
|
+
fs6.readFileSync(path6.join(projectRoot, "package.json"), "utf-8")
|
|
1702
1756
|
);
|
|
1703
1757
|
if (pkg.name) projectName = pkg.name;
|
|
1704
1758
|
} catch {
|
|
@@ -1730,20 +1784,19 @@ async function cmdInterview() {
|
|
|
1730
1784
|
if (!keySource && !globalKey) keySource = `env:${pattern}`;
|
|
1731
1785
|
}
|
|
1732
1786
|
}
|
|
1733
|
-
const envFiles =
|
|
1734
|
-
for (const
|
|
1787
|
+
const envFiles = findEnvFiles(projectRoot, 3);
|
|
1788
|
+
for (const envFilePath of envFiles) {
|
|
1735
1789
|
try {
|
|
1736
|
-
const
|
|
1737
|
-
const
|
|
1790
|
+
const envContent = fs6.readFileSync(envFilePath, "utf-8");
|
|
1791
|
+
const envKeys = parseEnvKeys(envContent);
|
|
1792
|
+
const envFileName = path6.relative(projectRoot, envFilePath);
|
|
1738
1793
|
for (const pattern of definition.envPatterns) {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
if (match?.[1]) {
|
|
1742
|
-
const label = `${pattern} (in ${envFile})`;
|
|
1794
|
+
if (envKeys.has(pattern)) {
|
|
1795
|
+
const label = `${pattern} (in ${envFileName})`;
|
|
1743
1796
|
if (!envKeysFound.some((k) => k.startsWith(pattern))) {
|
|
1744
1797
|
envKeysFound.push(label);
|
|
1745
1798
|
}
|
|
1746
|
-
if (!keySource && !globalKey) keySource = `file:${
|
|
1799
|
+
if (!keySource && !globalKey) keySource = `file:${envFileName}:${pattern}`;
|
|
1747
1800
|
}
|
|
1748
1801
|
}
|
|
1749
1802
|
} catch {
|
|
@@ -1759,8 +1812,9 @@ async function cmdInterview() {
|
|
|
1759
1812
|
const envFile = parts[1];
|
|
1760
1813
|
const envVar = parts[2];
|
|
1761
1814
|
try {
|
|
1762
|
-
const envContent =
|
|
1763
|
-
const
|
|
1815
|
+
const envContent = fs6.readFileSync(path6.join(projectRoot, envFile), "utf-8");
|
|
1816
|
+
const regex = new RegExp(`^(?:export\\s+)?${envVar}\\s*=\\s*(.+)$`, "m");
|
|
1817
|
+
const match = envContent.match(regex);
|
|
1764
1818
|
if (match?.[1]) apiKey = match[1].trim().replace(/^["']|["']$/g, "");
|
|
1765
1819
|
} catch {
|
|
1766
1820
|
}
|
|
@@ -2160,26 +2214,26 @@ async function cmdReconcile() {
|
|
|
2160
2214
|
}
|
|
2161
2215
|
function cmdReset() {
|
|
2162
2216
|
const projectRoot = process.cwd();
|
|
2163
|
-
const burnwatchDir =
|
|
2164
|
-
const claudeSkillsDir =
|
|
2165
|
-
if (!
|
|
2217
|
+
const burnwatchDir = path6.join(projectRoot, ".burnwatch");
|
|
2218
|
+
const claudeSkillsDir = path6.join(projectRoot, ".claude", "skills");
|
|
2219
|
+
if (!fs6.existsSync(burnwatchDir)) {
|
|
2166
2220
|
console.log("burnwatch is not initialized in this project.");
|
|
2167
2221
|
return;
|
|
2168
2222
|
}
|
|
2169
|
-
|
|
2223
|
+
fs6.rmSync(burnwatchDir, { recursive: true, force: true });
|
|
2170
2224
|
console.log(`\u{1F5D1}\uFE0F Removed ${burnwatchDir}`);
|
|
2171
2225
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2172
2226
|
for (const skill of skillNames) {
|
|
2173
|
-
const skillDir =
|
|
2174
|
-
if (
|
|
2175
|
-
|
|
2227
|
+
const skillDir = path6.join(claudeSkillsDir, skill);
|
|
2228
|
+
if (fs6.existsSync(skillDir)) {
|
|
2229
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
2176
2230
|
}
|
|
2177
2231
|
}
|
|
2178
2232
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch skills from .claude/skills/");
|
|
2179
|
-
const settingsPath =
|
|
2180
|
-
if (
|
|
2233
|
+
const settingsPath = path6.join(projectRoot, ".claude", "settings.json");
|
|
2234
|
+
if (fs6.existsSync(settingsPath)) {
|
|
2181
2235
|
try {
|
|
2182
|
-
const settings = JSON.parse(
|
|
2236
|
+
const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
2183
2237
|
const hooks = settings["hooks"];
|
|
2184
2238
|
if (hooks) {
|
|
2185
2239
|
for (const [event, hookList] of Object.entries(hooks)) {
|
|
@@ -2189,7 +2243,7 @@ function cmdReset() {
|
|
|
2189
2243
|
if (hooks[event].length === 0) delete hooks[event];
|
|
2190
2244
|
}
|
|
2191
2245
|
if (Object.keys(hooks).length === 0) delete settings["hooks"];
|
|
2192
|
-
|
|
2246
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2193
2247
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch hooks from .claude/settings.json");
|
|
2194
2248
|
}
|
|
2195
2249
|
} catch {
|
|
@@ -2239,23 +2293,23 @@ Examples:
|
|
|
2239
2293
|
}
|
|
2240
2294
|
function cmdVersion() {
|
|
2241
2295
|
try {
|
|
2242
|
-
const pkgPath =
|
|
2243
|
-
|
|
2296
|
+
const pkgPath = path6.resolve(
|
|
2297
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
2244
2298
|
"../package.json"
|
|
2245
2299
|
);
|
|
2246
|
-
const pkg = JSON.parse(
|
|
2300
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
2247
2301
|
console.log(`burnwatch v${pkg.version}`);
|
|
2248
2302
|
} catch {
|
|
2249
2303
|
console.log("burnwatch v0.1.0");
|
|
2250
2304
|
}
|
|
2251
2305
|
}
|
|
2252
2306
|
function registerHooks(projectRoot) {
|
|
2253
|
-
const sourceHooksDir =
|
|
2254
|
-
|
|
2307
|
+
const sourceHooksDir = path6.resolve(
|
|
2308
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
2255
2309
|
"hooks"
|
|
2256
2310
|
);
|
|
2257
|
-
const localHooksDir =
|
|
2258
|
-
|
|
2311
|
+
const localHooksDir = path6.join(projectRoot, ".burnwatch", "hooks");
|
|
2312
|
+
fs6.mkdirSync(localHooksDir, { recursive: true });
|
|
2259
2313
|
const hookFiles = [
|
|
2260
2314
|
"on-session-start.js",
|
|
2261
2315
|
"on-prompt.js",
|
|
@@ -2263,45 +2317,45 @@ function registerHooks(projectRoot) {
|
|
|
2263
2317
|
"on-stop.js"
|
|
2264
2318
|
];
|
|
2265
2319
|
for (const file of hookFiles) {
|
|
2266
|
-
const src =
|
|
2267
|
-
const dest =
|
|
2320
|
+
const src = path6.join(sourceHooksDir, file);
|
|
2321
|
+
const dest = path6.join(localHooksDir, file);
|
|
2268
2322
|
try {
|
|
2269
|
-
|
|
2323
|
+
fs6.copyFileSync(src, dest);
|
|
2270
2324
|
const mapSrc = src + ".map";
|
|
2271
|
-
if (
|
|
2272
|
-
|
|
2325
|
+
if (fs6.existsSync(mapSrc)) {
|
|
2326
|
+
fs6.copyFileSync(mapSrc, dest + ".map");
|
|
2273
2327
|
}
|
|
2274
2328
|
} catch (err) {
|
|
2275
2329
|
console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);
|
|
2276
2330
|
}
|
|
2277
2331
|
}
|
|
2278
2332
|
console.log(` Hook scripts copied to ${localHooksDir}`);
|
|
2279
|
-
const sourceSkillsDir =
|
|
2280
|
-
|
|
2333
|
+
const sourceSkillsDir = path6.resolve(
|
|
2334
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
2281
2335
|
"../skills"
|
|
2282
2336
|
);
|
|
2283
2337
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2284
|
-
const claudeSkillsDir =
|
|
2338
|
+
const claudeSkillsDir = path6.join(projectRoot, ".claude", "skills");
|
|
2285
2339
|
for (const skillName of skillNames) {
|
|
2286
|
-
const srcSkill =
|
|
2287
|
-
const destDir =
|
|
2288
|
-
const destSkill =
|
|
2340
|
+
const srcSkill = path6.join(sourceSkillsDir, skillName, "SKILL.md");
|
|
2341
|
+
const destDir = path6.join(claudeSkillsDir, skillName);
|
|
2342
|
+
const destSkill = path6.join(destDir, "SKILL.md");
|
|
2289
2343
|
try {
|
|
2290
|
-
if (
|
|
2291
|
-
|
|
2292
|
-
|
|
2344
|
+
if (fs6.existsSync(srcSkill)) {
|
|
2345
|
+
fs6.mkdirSync(destDir, { recursive: true });
|
|
2346
|
+
fs6.copyFileSync(srcSkill, destSkill);
|
|
2293
2347
|
}
|
|
2294
2348
|
} catch (err) {
|
|
2295
2349
|
console.error(` Warning: Could not copy skill ${skillName}: ${err instanceof Error ? err.message : err}`);
|
|
2296
2350
|
}
|
|
2297
2351
|
}
|
|
2298
2352
|
console.log(` Skills installed to ${claudeSkillsDir}`);
|
|
2299
|
-
const claudeDir =
|
|
2300
|
-
const settingsPath =
|
|
2301
|
-
|
|
2353
|
+
const claudeDir = path6.join(projectRoot, ".claude");
|
|
2354
|
+
const settingsPath = path6.join(claudeDir, "settings.json");
|
|
2355
|
+
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
2302
2356
|
let settings = {};
|
|
2303
2357
|
try {
|
|
2304
|
-
const existing =
|
|
2358
|
+
const existing = fs6.readFileSync(settingsPath, "utf-8");
|
|
2305
2359
|
settings = JSON.parse(existing);
|
|
2306
2360
|
console.log(` Merging into existing ${settingsPath}`);
|
|
2307
2361
|
} catch {
|
|
@@ -2317,7 +2371,7 @@ function registerHooks(projectRoot) {
|
|
|
2317
2371
|
hooks: [
|
|
2318
2372
|
{
|
|
2319
2373
|
type: "command",
|
|
2320
|
-
command: `node "${
|
|
2374
|
+
command: `node "${path6.join(hooksDir, "on-session-start.js")}"`,
|
|
2321
2375
|
timeout: 15
|
|
2322
2376
|
}
|
|
2323
2377
|
]
|
|
@@ -2330,7 +2384,7 @@ function registerHooks(projectRoot) {
|
|
|
2330
2384
|
hooks: [
|
|
2331
2385
|
{
|
|
2332
2386
|
type: "command",
|
|
2333
|
-
command: `node "${
|
|
2387
|
+
command: `node "${path6.join(hooksDir, "on-prompt.js")}"`,
|
|
2334
2388
|
timeout: 5
|
|
2335
2389
|
}
|
|
2336
2390
|
]
|
|
@@ -2342,7 +2396,7 @@ function registerHooks(projectRoot) {
|
|
|
2342
2396
|
hooks: [
|
|
2343
2397
|
{
|
|
2344
2398
|
type: "command",
|
|
2345
|
-
command: `node "${
|
|
2399
|
+
command: `node "${path6.join(hooksDir, "on-file-change.js")}"`,
|
|
2346
2400
|
timeout: 5
|
|
2347
2401
|
}
|
|
2348
2402
|
]
|
|
@@ -2352,14 +2406,14 @@ function registerHooks(projectRoot) {
|
|
|
2352
2406
|
hooks: [
|
|
2353
2407
|
{
|
|
2354
2408
|
type: "command",
|
|
2355
|
-
command: `node "${
|
|
2409
|
+
command: `node "${path6.join(hooksDir, "on-stop.js")}"`,
|
|
2356
2410
|
timeout: 15,
|
|
2357
2411
|
async: true
|
|
2358
2412
|
}
|
|
2359
2413
|
]
|
|
2360
2414
|
});
|
|
2361
2415
|
settings["hooks"] = hooks;
|
|
2362
|
-
|
|
2416
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2363
2417
|
console.log(` Hooks registered in ${settingsPath}`);
|
|
2364
2418
|
}
|
|
2365
2419
|
function addHookIfMissing(hookArray, _eventName, hookConfig) {
|