burnwatch 0.1.2 → 0.3.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/dist/cli.js +139 -103
- package/dist/cli.js.map +1 -1
- 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 +104 -26
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +104 -26
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +934 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -122,7 +122,7 @@ function getAllServices(projectRoot) {
|
|
|
122
122
|
function detectServices(projectRoot) {
|
|
123
123
|
const registry = loadRegistry(projectRoot);
|
|
124
124
|
const results = /* @__PURE__ */ new Map();
|
|
125
|
-
const pkgDeps =
|
|
125
|
+
const pkgDeps = scanAllPackageJsons(projectRoot);
|
|
126
126
|
for (const [serviceId, service] of registry) {
|
|
127
127
|
const matchedPkgs = service.packageNames.filter(
|
|
128
128
|
(pkg) => pkgDeps.has(pkg)
|
|
@@ -134,7 +134,7 @@ function detectServices(projectRoot) {
|
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
-
const envVars =
|
|
137
|
+
const envVars = collectEnvVars(projectRoot);
|
|
138
138
|
for (const [serviceId, service] of registry) {
|
|
139
139
|
const matchedEnvs = service.envPatterns.filter(
|
|
140
140
|
(pattern) => envVars.has(pattern)
|
|
@@ -172,40 +172,118 @@ function getOrCreate(map, serviceId, service) {
|
|
|
172
172
|
}
|
|
173
173
|
return result;
|
|
174
174
|
}
|
|
175
|
-
function
|
|
175
|
+
function scanAllPackageJsons(projectRoot) {
|
|
176
176
|
const deps = /* @__PURE__ */ new Set();
|
|
177
|
-
const
|
|
177
|
+
const pkgFiles = findFiles(projectRoot, "package.json", 4);
|
|
178
|
+
for (const pkgPath of pkgFiles) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = fs3.readFileSync(pkgPath, "utf-8");
|
|
181
|
+
const pkg = JSON.parse(raw);
|
|
182
|
+
for (const name of Object.keys(pkg.dependencies ?? {})) deps.add(name);
|
|
183
|
+
for (const name of Object.keys(pkg.devDependencies ?? {})) deps.add(name);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return deps;
|
|
188
|
+
}
|
|
189
|
+
function collectEnvVars(projectRoot) {
|
|
190
|
+
const envVars = new Set(Object.keys(process.env));
|
|
191
|
+
const envFiles = findEnvFiles(projectRoot, 3);
|
|
192
|
+
for (const envFile of envFiles) {
|
|
193
|
+
try {
|
|
194
|
+
const content = fs3.readFileSync(envFile, "utf-8");
|
|
195
|
+
const keys = content.split("\n").filter((line) => line.includes("=") && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter(Boolean);
|
|
196
|
+
for (const key of keys) {
|
|
197
|
+
envVars.add(key);
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return envVars;
|
|
203
|
+
}
|
|
204
|
+
function findEnvFiles(dir, maxDepth) {
|
|
205
|
+
const results = [];
|
|
206
|
+
if (maxDepth <= 0) return results;
|
|
178
207
|
try {
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
208
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
|
|
211
|
+
const fullPath = path3.join(dir, entry.name);
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
results.push(...findEnvFiles(fullPath, maxDepth - 1));
|
|
214
|
+
} else if (entry.name.startsWith(".env")) {
|
|
215
|
+
results.push(fullPath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
183
218
|
} catch {
|
|
184
219
|
}
|
|
185
|
-
return
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
function findFiles(dir, fileName, maxDepth) {
|
|
223
|
+
const results = [];
|
|
224
|
+
if (maxDepth <= 0) return results;
|
|
225
|
+
try {
|
|
226
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
|
|
229
|
+
const fullPath = path3.join(dir, entry.name);
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
results.push(...findFiles(fullPath, fileName, maxDepth - 1));
|
|
232
|
+
} else if (entry.name === fileName) {
|
|
233
|
+
results.push(fullPath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
return results;
|
|
186
239
|
}
|
|
187
240
|
function scanImports(projectRoot) {
|
|
188
241
|
const imports = /* @__PURE__ */ new Set();
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
242
|
+
const codeDirs = ["src", "app", "lib", "pages", "components", "utils", "services", "hooks"];
|
|
243
|
+
const dirsToScan = [];
|
|
244
|
+
for (const dir of codeDirs) {
|
|
245
|
+
const fullPath = path3.join(projectRoot, dir);
|
|
246
|
+
if (fs3.existsSync(fullPath)) {
|
|
247
|
+
dirsToScan.push(fullPath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const entries = fs3.readdirSync(projectRoot, { withFileTypes: true });
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (!entry.isDirectory()) continue;
|
|
254
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name.startsWith(".")) continue;
|
|
255
|
+
const subPkgPath = path3.join(projectRoot, entry.name, "package.json");
|
|
256
|
+
if (fs3.existsSync(subPkgPath)) {
|
|
257
|
+
for (const dir of codeDirs) {
|
|
258
|
+
const fullPath = path3.join(projectRoot, entry.name, dir);
|
|
259
|
+
if (fs3.existsSync(fullPath)) {
|
|
260
|
+
dirsToScan.push(fullPath);
|
|
205
261
|
}
|
|
206
262
|
}
|
|
207
263
|
}
|
|
208
|
-
}
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
for (const dir of dirsToScan) {
|
|
268
|
+
const files = walkDir(dir, /\.(ts|tsx|js|jsx|mjs|cjs)$/);
|
|
269
|
+
for (const file of files) {
|
|
270
|
+
try {
|
|
271
|
+
const content = fs3.readFileSync(file, "utf-8");
|
|
272
|
+
const importRegex = /(?:from\s+["']|require\s*\(\s*["'])([^./][^"']*?)(?:["'])/g;
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
275
|
+
const pkg = match[1];
|
|
276
|
+
if (pkg) {
|
|
277
|
+
const parts = pkg.split("/");
|
|
278
|
+
if (parts[0]?.startsWith("@") && parts.length >= 2) {
|
|
279
|
+
imports.add(`${parts[0]}/${parts[1]}`);
|
|
280
|
+
} else if (parts[0]) {
|
|
281
|
+
imports.add(parts[0]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
209
287
|
}
|
|
210
288
|
}
|
|
211
289
|
return imports;
|
|
@@ -704,6 +782,7 @@ var command = args[0];
|
|
|
704
782
|
async function main() {
|
|
705
783
|
switch (command) {
|
|
706
784
|
case "init":
|
|
785
|
+
case "setup":
|
|
707
786
|
await cmdInit();
|
|
708
787
|
break;
|
|
709
788
|
case "add":
|
|
@@ -718,9 +797,6 @@ async function main() {
|
|
|
718
797
|
case "reconcile":
|
|
719
798
|
await cmdReconcile();
|
|
720
799
|
break;
|
|
721
|
-
case "setup":
|
|
722
|
-
await cmdSetup();
|
|
723
|
-
break;
|
|
724
800
|
case "help":
|
|
725
801
|
case "--help":
|
|
726
802
|
case "-h":
|
|
@@ -920,69 +996,6 @@ async function cmdStatus() {
|
|
|
920
996
|
console.log("");
|
|
921
997
|
}
|
|
922
998
|
}
|
|
923
|
-
async function cmdSetup() {
|
|
924
|
-
const projectRoot = process.cwd();
|
|
925
|
-
if (!isInitialized(projectRoot)) {
|
|
926
|
-
await cmdInit();
|
|
927
|
-
}
|
|
928
|
-
const config = readProjectConfig(projectRoot);
|
|
929
|
-
const detected = Object.values(config.services);
|
|
930
|
-
if (detected.length === 0) {
|
|
931
|
-
console.log("No paid services detected. You're all set!");
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
console.log("\u{1F4CB} Auto-configuring detected services...\n");
|
|
935
|
-
const globalConfig = readGlobalConfig();
|
|
936
|
-
const liveServices = [];
|
|
937
|
-
const calcServices = [];
|
|
938
|
-
const estServices = [];
|
|
939
|
-
const blindServices = [];
|
|
940
|
-
for (const tracked of detected) {
|
|
941
|
-
const definition = getService(tracked.serviceId, projectRoot);
|
|
942
|
-
if (!definition) continue;
|
|
943
|
-
const hasKey = !!globalConfig.services[tracked.serviceId]?.apiKey;
|
|
944
|
-
if (hasKey && definition.apiTier === "live") {
|
|
945
|
-
tracked.hasApiKey = true;
|
|
946
|
-
liveServices.push(`${definition.name}`);
|
|
947
|
-
} else if (definition.apiTier === "calc") {
|
|
948
|
-
calcServices.push(`${definition.name}`);
|
|
949
|
-
} else if (definition.apiTier === "est") {
|
|
950
|
-
estServices.push(`${definition.name}`);
|
|
951
|
-
} else {
|
|
952
|
-
blindServices.push(`${definition.name}`);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
writeProjectConfig(config, projectRoot);
|
|
956
|
-
if (liveServices.length > 0) {
|
|
957
|
-
console.log(` \u2705 LIVE (real billing data): ${liveServices.join(", ")}`);
|
|
958
|
-
}
|
|
959
|
-
if (calcServices.length > 0) {
|
|
960
|
-
console.log(` \u{1F7E1} CALC (flat-rate tracking): ${calcServices.join(", ")}`);
|
|
961
|
-
}
|
|
962
|
-
if (estServices.length > 0) {
|
|
963
|
-
console.log(` \u{1F7E0} EST (estimated from usage): ${estServices.join(", ")}`);
|
|
964
|
-
}
|
|
965
|
-
if (blindServices.length > 0) {
|
|
966
|
-
console.log(` \u{1F534} BLIND (detected, need API key): ${blindServices.join(", ")}`);
|
|
967
|
-
}
|
|
968
|
-
console.log("");
|
|
969
|
-
if (blindServices.length > 0) {
|
|
970
|
-
console.log("To upgrade BLIND services to LIVE, add API keys:");
|
|
971
|
-
for (const tracked of detected) {
|
|
972
|
-
const definition = getService(tracked.serviceId, projectRoot);
|
|
973
|
-
if (definition?.apiTier === "live" && !tracked.hasApiKey) {
|
|
974
|
-
const envHint = definition.envPatterns[0] ?? "YOUR_KEY";
|
|
975
|
-
console.log(` burnwatch add ${tracked.serviceId} --key $${envHint} --budget <N>`);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
console.log("");
|
|
979
|
-
}
|
|
980
|
-
console.log("To set budgets for any service:");
|
|
981
|
-
console.log(" burnwatch add <service> --budget <monthly_amount>");
|
|
982
|
-
console.log("");
|
|
983
|
-
console.log("Or use /setup-burnwatch in Claude Code for guided setup with budget suggestions.\n");
|
|
984
|
-
await cmdStatus();
|
|
985
|
-
}
|
|
986
999
|
function cmdServices() {
|
|
987
1000
|
const services = getAllServices();
|
|
988
1001
|
console.log(`
|
|
@@ -1066,24 +1079,47 @@ function cmdVersion() {
|
|
|
1066
1079
|
}
|
|
1067
1080
|
}
|
|
1068
1081
|
function registerHooks(projectRoot) {
|
|
1082
|
+
const sourceHooksDir = path5.resolve(
|
|
1083
|
+
path5.dirname(new URL(import.meta.url).pathname),
|
|
1084
|
+
"hooks"
|
|
1085
|
+
);
|
|
1086
|
+
const localHooksDir = path5.join(projectRoot, ".burnwatch", "hooks");
|
|
1087
|
+
fs5.mkdirSync(localHooksDir, { recursive: true });
|
|
1088
|
+
const hookFiles = [
|
|
1089
|
+
"on-session-start.js",
|
|
1090
|
+
"on-prompt.js",
|
|
1091
|
+
"on-file-change.js",
|
|
1092
|
+
"on-stop.js"
|
|
1093
|
+
];
|
|
1094
|
+
for (const file of hookFiles) {
|
|
1095
|
+
const src = path5.join(sourceHooksDir, file);
|
|
1096
|
+
const dest = path5.join(localHooksDir, file);
|
|
1097
|
+
try {
|
|
1098
|
+
fs5.copyFileSync(src, dest);
|
|
1099
|
+
const mapSrc = src + ".map";
|
|
1100
|
+
if (fs5.existsSync(mapSrc)) {
|
|
1101
|
+
fs5.copyFileSync(mapSrc, dest + ".map");
|
|
1102
|
+
}
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
console.log(` Hook scripts copied to ${localHooksDir}`);
|
|
1069
1108
|
const claudeDir = path5.join(projectRoot, ".claude");
|
|
1070
1109
|
const settingsPath = path5.join(claudeDir, "settings.json");
|
|
1071
1110
|
fs5.mkdirSync(claudeDir, { recursive: true });
|
|
1072
1111
|
let settings = {};
|
|
1073
1112
|
try {
|
|
1074
|
-
|
|
1113
|
+
const existing = fs5.readFileSync(settingsPath, "utf-8");
|
|
1114
|
+
settings = JSON.parse(existing);
|
|
1115
|
+
console.log(` Merging into existing ${settingsPath}`);
|
|
1075
1116
|
} catch {
|
|
1076
1117
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
const
|
|
1082
|
-
const hooksDir = path5.resolve(
|
|
1083
|
-
path5.dirname(new URL(import.meta.url).pathname),
|
|
1084
|
-
"hooks"
|
|
1085
|
-
);
|
|
1086
|
-
const hooks = settings["hooks"] ?? {};
|
|
1118
|
+
if (!settings["hooks"] || typeof settings["hooks"] !== "object") {
|
|
1119
|
+
settings["hooks"] = {};
|
|
1120
|
+
}
|
|
1121
|
+
const hooks = settings["hooks"];
|
|
1122
|
+
const hooksDir = localHooksDir;
|
|
1087
1123
|
if (!hooks["SessionStart"]) hooks["SessionStart"] = [];
|
|
1088
1124
|
addHookIfMissing(hooks["SessionStart"], "SessionStart", {
|
|
1089
1125
|
matcher: "startup|resume",
|