burnwatch 0.12.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 +13 -0
- package/dist/cli.js +94 -64
- package/dist/cli.js.map +1 -1
- package/dist/{detector-CSgHJEdg.d.ts → detector-myYS2eVC.d.ts} +10 -1
- package/dist/hooks/on-file-change.js +15 -2
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +14 -2
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -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 +14 -2
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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
|
+
|
|
8
20
|
## [0.12.0] - 2026-03-25
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -192,6 +204,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
192
204
|
- Snapshot system for delta computation across sessions
|
|
193
205
|
- Claude Code skills: `/spend` (on-demand brief), `/setup-burnwatch` (guided onboarding)
|
|
194
206
|
|
|
207
|
+
[0.12.1]: https://github.com/RaleighSF/burnwatch/compare/v0.12.0...v0.12.1
|
|
195
208
|
[0.12.0]: https://github.com/RaleighSF/burnwatch/compare/v0.11.0...v0.12.0
|
|
196
209
|
[0.11.0]: https://github.com/RaleighSF/burnwatch/compare/v0.10.0...v0.11.0
|
|
197
210
|
[0.10.0]: https://github.com/RaleighSF/burnwatch/compare/v0.9.0...v0.10.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";
|
|
@@ -502,8 +502,7 @@ function collectEnvVars(projectRoot) {
|
|
|
502
502
|
for (const envFile of envFiles) {
|
|
503
503
|
try {
|
|
504
504
|
const content = fs3.readFileSync(envFile, "utf-8");
|
|
505
|
-
|
|
506
|
-
for (const key of keys) {
|
|
505
|
+
for (const key of parseEnvKeys(content)) {
|
|
507
506
|
envVars.add(key);
|
|
508
507
|
}
|
|
509
508
|
} catch {
|
|
@@ -511,6 +510,19 @@ function collectEnvVars(projectRoot) {
|
|
|
511
510
|
}
|
|
512
511
|
return envVars;
|
|
513
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
|
+
}
|
|
514
526
|
function findEnvFiles(dir, maxDepth) {
|
|
515
527
|
const results = [];
|
|
516
528
|
if (maxDepth <= 0) return results;
|
|
@@ -1267,6 +1279,8 @@ function saveSnapshot(brief, projectRoot) {
|
|
|
1267
1279
|
|
|
1268
1280
|
// src/interactive-init.ts
|
|
1269
1281
|
import * as readline from "readline";
|
|
1282
|
+
import * as fs5 from "fs";
|
|
1283
|
+
import "path";
|
|
1270
1284
|
init_probes();
|
|
1271
1285
|
function formatUnits(n) {
|
|
1272
1286
|
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
@@ -1313,11 +1327,27 @@ function ask(rl, question) {
|
|
|
1313
1327
|
});
|
|
1314
1328
|
});
|
|
1315
1329
|
}
|
|
1316
|
-
function findEnvKey(service) {
|
|
1330
|
+
function findEnvKey(service, projectRoot = process.cwd()) {
|
|
1317
1331
|
for (const pattern of service.envPatterns) {
|
|
1318
1332
|
const val = process.env[pattern];
|
|
1319
1333
|
if (val && val.length > 0) return val;
|
|
1320
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
|
+
}
|
|
1321
1351
|
return void 0;
|
|
1322
1352
|
}
|
|
1323
1353
|
async function autoConfigureServices(detected) {
|
|
@@ -1646,10 +1676,10 @@ async function cmdInit() {
|
|
|
1646
1676
|
const projectRoot = process.cwd();
|
|
1647
1677
|
const nonInteractive = flags.has("--non-interactive") || flags.has("--ni");
|
|
1648
1678
|
const alreadyInitialized = isInitialized(projectRoot);
|
|
1649
|
-
let projectName =
|
|
1679
|
+
let projectName = path6.basename(projectRoot);
|
|
1650
1680
|
try {
|
|
1651
|
-
const pkgPath =
|
|
1652
|
-
const pkg = JSON.parse(
|
|
1681
|
+
const pkgPath = path6.join(projectRoot, "package.json");
|
|
1682
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
1653
1683
|
if (pkg.name) projectName = pkg.name;
|
|
1654
1684
|
} catch {
|
|
1655
1685
|
}
|
|
@@ -1688,8 +1718,8 @@ async function cmdInit() {
|
|
|
1688
1718
|
config.services = result.services;
|
|
1689
1719
|
}
|
|
1690
1720
|
writeProjectConfig(config, projectRoot);
|
|
1691
|
-
const gitignorePath =
|
|
1692
|
-
|
|
1721
|
+
const gitignorePath = path6.join(projectConfigDir(projectRoot), ".gitignore");
|
|
1722
|
+
fs6.writeFileSync(
|
|
1693
1723
|
gitignorePath,
|
|
1694
1724
|
[
|
|
1695
1725
|
"# Burnwatch \u2014 ignore cache and snapshots, keep ledger and config",
|
|
@@ -1719,10 +1749,10 @@ async function cmdInterview() {
|
|
|
1719
1749
|
if (!isInitialized(projectRoot)) {
|
|
1720
1750
|
ensureProjectDirs(projectRoot);
|
|
1721
1751
|
const detected = detectServices(projectRoot);
|
|
1722
|
-
let projectName =
|
|
1752
|
+
let projectName = path6.basename(projectRoot);
|
|
1723
1753
|
try {
|
|
1724
1754
|
const pkg = JSON.parse(
|
|
1725
|
-
|
|
1755
|
+
fs6.readFileSync(path6.join(projectRoot, "package.json"), "utf-8")
|
|
1726
1756
|
);
|
|
1727
1757
|
if (pkg.name) projectName = pkg.name;
|
|
1728
1758
|
} catch {
|
|
@@ -1754,20 +1784,19 @@ async function cmdInterview() {
|
|
|
1754
1784
|
if (!keySource && !globalKey) keySource = `env:${pattern}`;
|
|
1755
1785
|
}
|
|
1756
1786
|
}
|
|
1757
|
-
const envFiles =
|
|
1758
|
-
for (const
|
|
1787
|
+
const envFiles = findEnvFiles(projectRoot, 3);
|
|
1788
|
+
for (const envFilePath of envFiles) {
|
|
1759
1789
|
try {
|
|
1760
|
-
const
|
|
1761
|
-
const
|
|
1790
|
+
const envContent = fs6.readFileSync(envFilePath, "utf-8");
|
|
1791
|
+
const envKeys = parseEnvKeys(envContent);
|
|
1792
|
+
const envFileName = path6.relative(projectRoot, envFilePath);
|
|
1762
1793
|
for (const pattern of definition.envPatterns) {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
if (match?.[1]) {
|
|
1766
|
-
const label = `${pattern} (in ${envFile})`;
|
|
1794
|
+
if (envKeys.has(pattern)) {
|
|
1795
|
+
const label = `${pattern} (in ${envFileName})`;
|
|
1767
1796
|
if (!envKeysFound.some((k) => k.startsWith(pattern))) {
|
|
1768
1797
|
envKeysFound.push(label);
|
|
1769
1798
|
}
|
|
1770
|
-
if (!keySource && !globalKey) keySource = `file:${
|
|
1799
|
+
if (!keySource && !globalKey) keySource = `file:${envFileName}:${pattern}`;
|
|
1771
1800
|
}
|
|
1772
1801
|
}
|
|
1773
1802
|
} catch {
|
|
@@ -1783,8 +1812,9 @@ async function cmdInterview() {
|
|
|
1783
1812
|
const envFile = parts[1];
|
|
1784
1813
|
const envVar = parts[2];
|
|
1785
1814
|
try {
|
|
1786
|
-
const envContent =
|
|
1787
|
-
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);
|
|
1788
1818
|
if (match?.[1]) apiKey = match[1].trim().replace(/^["']|["']$/g, "");
|
|
1789
1819
|
} catch {
|
|
1790
1820
|
}
|
|
@@ -2184,26 +2214,26 @@ async function cmdReconcile() {
|
|
|
2184
2214
|
}
|
|
2185
2215
|
function cmdReset() {
|
|
2186
2216
|
const projectRoot = process.cwd();
|
|
2187
|
-
const burnwatchDir =
|
|
2188
|
-
const claudeSkillsDir =
|
|
2189
|
-
if (!
|
|
2217
|
+
const burnwatchDir = path6.join(projectRoot, ".burnwatch");
|
|
2218
|
+
const claudeSkillsDir = path6.join(projectRoot, ".claude", "skills");
|
|
2219
|
+
if (!fs6.existsSync(burnwatchDir)) {
|
|
2190
2220
|
console.log("burnwatch is not initialized in this project.");
|
|
2191
2221
|
return;
|
|
2192
2222
|
}
|
|
2193
|
-
|
|
2223
|
+
fs6.rmSync(burnwatchDir, { recursive: true, force: true });
|
|
2194
2224
|
console.log(`\u{1F5D1}\uFE0F Removed ${burnwatchDir}`);
|
|
2195
2225
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2196
2226
|
for (const skill of skillNames) {
|
|
2197
|
-
const skillDir =
|
|
2198
|
-
if (
|
|
2199
|
-
|
|
2227
|
+
const skillDir = path6.join(claudeSkillsDir, skill);
|
|
2228
|
+
if (fs6.existsSync(skillDir)) {
|
|
2229
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
2200
2230
|
}
|
|
2201
2231
|
}
|
|
2202
2232
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch skills from .claude/skills/");
|
|
2203
|
-
const settingsPath =
|
|
2204
|
-
if (
|
|
2233
|
+
const settingsPath = path6.join(projectRoot, ".claude", "settings.json");
|
|
2234
|
+
if (fs6.existsSync(settingsPath)) {
|
|
2205
2235
|
try {
|
|
2206
|
-
const settings = JSON.parse(
|
|
2236
|
+
const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
2207
2237
|
const hooks = settings["hooks"];
|
|
2208
2238
|
if (hooks) {
|
|
2209
2239
|
for (const [event, hookList] of Object.entries(hooks)) {
|
|
@@ -2213,7 +2243,7 @@ function cmdReset() {
|
|
|
2213
2243
|
if (hooks[event].length === 0) delete hooks[event];
|
|
2214
2244
|
}
|
|
2215
2245
|
if (Object.keys(hooks).length === 0) delete settings["hooks"];
|
|
2216
|
-
|
|
2246
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2217
2247
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch hooks from .claude/settings.json");
|
|
2218
2248
|
}
|
|
2219
2249
|
} catch {
|
|
@@ -2263,23 +2293,23 @@ Examples:
|
|
|
2263
2293
|
}
|
|
2264
2294
|
function cmdVersion() {
|
|
2265
2295
|
try {
|
|
2266
|
-
const pkgPath =
|
|
2267
|
-
|
|
2296
|
+
const pkgPath = path6.resolve(
|
|
2297
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
2268
2298
|
"../package.json"
|
|
2269
2299
|
);
|
|
2270
|
-
const pkg = JSON.parse(
|
|
2300
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
2271
2301
|
console.log(`burnwatch v${pkg.version}`);
|
|
2272
2302
|
} catch {
|
|
2273
2303
|
console.log("burnwatch v0.1.0");
|
|
2274
2304
|
}
|
|
2275
2305
|
}
|
|
2276
2306
|
function registerHooks(projectRoot) {
|
|
2277
|
-
const sourceHooksDir =
|
|
2278
|
-
|
|
2307
|
+
const sourceHooksDir = path6.resolve(
|
|
2308
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
2279
2309
|
"hooks"
|
|
2280
2310
|
);
|
|
2281
|
-
const localHooksDir =
|
|
2282
|
-
|
|
2311
|
+
const localHooksDir = path6.join(projectRoot, ".burnwatch", "hooks");
|
|
2312
|
+
fs6.mkdirSync(localHooksDir, { recursive: true });
|
|
2283
2313
|
const hookFiles = [
|
|
2284
2314
|
"on-session-start.js",
|
|
2285
2315
|
"on-prompt.js",
|
|
@@ -2287,45 +2317,45 @@ function registerHooks(projectRoot) {
|
|
|
2287
2317
|
"on-stop.js"
|
|
2288
2318
|
];
|
|
2289
2319
|
for (const file of hookFiles) {
|
|
2290
|
-
const src =
|
|
2291
|
-
const dest =
|
|
2320
|
+
const src = path6.join(sourceHooksDir, file);
|
|
2321
|
+
const dest = path6.join(localHooksDir, file);
|
|
2292
2322
|
try {
|
|
2293
|
-
|
|
2323
|
+
fs6.copyFileSync(src, dest);
|
|
2294
2324
|
const mapSrc = src + ".map";
|
|
2295
|
-
if (
|
|
2296
|
-
|
|
2325
|
+
if (fs6.existsSync(mapSrc)) {
|
|
2326
|
+
fs6.copyFileSync(mapSrc, dest + ".map");
|
|
2297
2327
|
}
|
|
2298
2328
|
} catch (err) {
|
|
2299
2329
|
console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);
|
|
2300
2330
|
}
|
|
2301
2331
|
}
|
|
2302
2332
|
console.log(` Hook scripts copied to ${localHooksDir}`);
|
|
2303
|
-
const sourceSkillsDir =
|
|
2304
|
-
|
|
2333
|
+
const sourceSkillsDir = path6.resolve(
|
|
2334
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
2305
2335
|
"../skills"
|
|
2306
2336
|
);
|
|
2307
2337
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2308
|
-
const claudeSkillsDir =
|
|
2338
|
+
const claudeSkillsDir = path6.join(projectRoot, ".claude", "skills");
|
|
2309
2339
|
for (const skillName of skillNames) {
|
|
2310
|
-
const srcSkill =
|
|
2311
|
-
const destDir =
|
|
2312
|
-
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");
|
|
2313
2343
|
try {
|
|
2314
|
-
if (
|
|
2315
|
-
|
|
2316
|
-
|
|
2344
|
+
if (fs6.existsSync(srcSkill)) {
|
|
2345
|
+
fs6.mkdirSync(destDir, { recursive: true });
|
|
2346
|
+
fs6.copyFileSync(srcSkill, destSkill);
|
|
2317
2347
|
}
|
|
2318
2348
|
} catch (err) {
|
|
2319
2349
|
console.error(` Warning: Could not copy skill ${skillName}: ${err instanceof Error ? err.message : err}`);
|
|
2320
2350
|
}
|
|
2321
2351
|
}
|
|
2322
2352
|
console.log(` Skills installed to ${claudeSkillsDir}`);
|
|
2323
|
-
const claudeDir =
|
|
2324
|
-
const settingsPath =
|
|
2325
|
-
|
|
2353
|
+
const claudeDir = path6.join(projectRoot, ".claude");
|
|
2354
|
+
const settingsPath = path6.join(claudeDir, "settings.json");
|
|
2355
|
+
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
2326
2356
|
let settings = {};
|
|
2327
2357
|
try {
|
|
2328
|
-
const existing =
|
|
2358
|
+
const existing = fs6.readFileSync(settingsPath, "utf-8");
|
|
2329
2359
|
settings = JSON.parse(existing);
|
|
2330
2360
|
console.log(` Merging into existing ${settingsPath}`);
|
|
2331
2361
|
} catch {
|
|
@@ -2341,7 +2371,7 @@ function registerHooks(projectRoot) {
|
|
|
2341
2371
|
hooks: [
|
|
2342
2372
|
{
|
|
2343
2373
|
type: "command",
|
|
2344
|
-
command: `node "${
|
|
2374
|
+
command: `node "${path6.join(hooksDir, "on-session-start.js")}"`,
|
|
2345
2375
|
timeout: 15
|
|
2346
2376
|
}
|
|
2347
2377
|
]
|
|
@@ -2354,7 +2384,7 @@ function registerHooks(projectRoot) {
|
|
|
2354
2384
|
hooks: [
|
|
2355
2385
|
{
|
|
2356
2386
|
type: "command",
|
|
2357
|
-
command: `node "${
|
|
2387
|
+
command: `node "${path6.join(hooksDir, "on-prompt.js")}"`,
|
|
2358
2388
|
timeout: 5
|
|
2359
2389
|
}
|
|
2360
2390
|
]
|
|
@@ -2366,7 +2396,7 @@ function registerHooks(projectRoot) {
|
|
|
2366
2396
|
hooks: [
|
|
2367
2397
|
{
|
|
2368
2398
|
type: "command",
|
|
2369
|
-
command: `node "${
|
|
2399
|
+
command: `node "${path6.join(hooksDir, "on-file-change.js")}"`,
|
|
2370
2400
|
timeout: 5
|
|
2371
2401
|
}
|
|
2372
2402
|
]
|
|
@@ -2376,14 +2406,14 @@ function registerHooks(projectRoot) {
|
|
|
2376
2406
|
hooks: [
|
|
2377
2407
|
{
|
|
2378
2408
|
type: "command",
|
|
2379
|
-
command: `node "${
|
|
2409
|
+
command: `node "${path6.join(hooksDir, "on-stop.js")}"`,
|
|
2380
2410
|
timeout: 15,
|
|
2381
2411
|
async: true
|
|
2382
2412
|
}
|
|
2383
2413
|
]
|
|
2384
2414
|
});
|
|
2385
2415
|
settings["hooks"] = hooks;
|
|
2386
|
-
|
|
2416
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2387
2417
|
console.log(` Hooks registered in ${settingsPath}`);
|
|
2388
2418
|
}
|
|
2389
2419
|
function addHookIfMissing(hookArray, _eventName, hookConfig) {
|