burnwatch 0.4.3 ā 0.5.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 +19 -4
- package/dist/cli.js +185 -124
- package/dist/cli.js.map +1 -1
- package/dist/interactive-init.d.ts +18 -4
- package/dist/interactive-init.js +162 -49
- package/dist/interactive-init.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,17 +4,31 @@ import { D as DetectionResult } from './detector-C4LnLT-O.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Interactive init flow for burnwatch.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Conducts a per-service interview: detects what it can automatically
|
|
8
|
+
* (existing API keys, env vars), asks for plan selection, collects
|
|
9
|
+
* API keys for LIVE tracking, and ensures every service exits with
|
|
10
|
+
* a budget. No skipping.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
interface InteractiveInitResult {
|
|
12
14
|
services: Record<string, TrackedService>;
|
|
13
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Auto-configure all services without prompts.
|
|
18
|
+
*
|
|
19
|
+
* Applies the same logic as the interactive interview but picks
|
|
20
|
+
* defaults automatically: default plan, env var keys, budget = plan cost.
|
|
21
|
+
* Used when stdin is not a TTY (e.g., Claude Code, piped input).
|
|
22
|
+
*/
|
|
23
|
+
declare function autoConfigureServices(detected: DetectionResult[]): InteractiveInitResult;
|
|
14
24
|
/**
|
|
15
25
|
* Run the interactive init flow.
|
|
16
|
-
*
|
|
26
|
+
*
|
|
27
|
+
* For each detected service:
|
|
28
|
+
* 1. Ask which plan they're on
|
|
29
|
+
* 2. If LIVE-capable, check for existing key or ask for one
|
|
30
|
+
* 3. Set budget (defaults to plan cost, $0 for free - never skipped)
|
|
17
31
|
*/
|
|
18
32
|
declare function runInteractiveInit(detected: DetectionResult[]): Promise<InteractiveInitResult>;
|
|
19
33
|
|
|
20
|
-
export { type InteractiveInitResult, runInteractiveInit };
|
|
34
|
+
export { type InteractiveInitResult, autoConfigureServices, runInteractiveInit };
|
package/dist/interactive-init.js
CHANGED
|
@@ -63,10 +63,18 @@ async function fetchJson(url, options = {}) {
|
|
|
63
63
|
// src/interactive-init.ts
|
|
64
64
|
var RISK_ORDER = ["llm", "usage", "infra", "flat"];
|
|
65
65
|
var RISK_LABELS = {
|
|
66
|
-
llm: "
|
|
67
|
-
usage: "
|
|
68
|
-
infra: "
|
|
69
|
-
flat: "
|
|
66
|
+
llm: "LLM / AI Services (highest variable cost)",
|
|
67
|
+
usage: "Usage-Based Services",
|
|
68
|
+
infra: "Infrastructure & Compute",
|
|
69
|
+
flat: "Flat-Rate / Free Tier Services"
|
|
70
|
+
};
|
|
71
|
+
var API_KEY_HINTS = {
|
|
72
|
+
anthropic: "Admin key: console.anthropic.com -> Settings -> Admin API Keys",
|
|
73
|
+
openai: "Org key: platform.openai.com -> Settings -> API Keys",
|
|
74
|
+
vercel: "Token: vercel.com/account/tokens",
|
|
75
|
+
supabase: "Service role key: supabase.com/dashboard -> Settings -> API",
|
|
76
|
+
stripe: "Secret key: dashboard.stripe.com -> Developers -> API Keys",
|
|
77
|
+
scrapfly: "API key: scrapfly.io/dashboard"
|
|
70
78
|
};
|
|
71
79
|
function classifyRisk(service) {
|
|
72
80
|
if (service.billingModel === "token_usage") return "llm";
|
|
@@ -103,6 +111,88 @@ async function autoDetectScrapflyPlan(apiKey) {
|
|
|
103
111
|
}
|
|
104
112
|
return null;
|
|
105
113
|
}
|
|
114
|
+
function findEnvKey(service) {
|
|
115
|
+
for (const pattern of service.envPatterns) {
|
|
116
|
+
const val = process.env[pattern];
|
|
117
|
+
if (val && val.length > 0) return val;
|
|
118
|
+
}
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
function autoConfigureServices(detected) {
|
|
122
|
+
const services = {};
|
|
123
|
+
const groups = groupByRisk(detected);
|
|
124
|
+
const globalConfig = readGlobalConfig();
|
|
125
|
+
console.log(
|
|
126
|
+
`
|
|
127
|
+
Found ${detected.length} paid service${detected.length !== 1 ? "s" : ""}. Auto-configuring with defaults.
|
|
128
|
+
`
|
|
129
|
+
);
|
|
130
|
+
console.log(" Run 'burnwatch init' from your terminal for interactive setup.\n");
|
|
131
|
+
for (const category of RISK_ORDER) {
|
|
132
|
+
const group = groups.get(category);
|
|
133
|
+
if (group.length === 0) continue;
|
|
134
|
+
console.log(` ${RISK_LABELS[category]}`);
|
|
135
|
+
for (const det of group) {
|
|
136
|
+
const service = det.service;
|
|
137
|
+
const plans = service.plans ?? [];
|
|
138
|
+
const defaultPlan = plans.find((p) => p.default) ?? plans[0];
|
|
139
|
+
const tracked = {
|
|
140
|
+
serviceId: service.id,
|
|
141
|
+
detectedVia: det.sources,
|
|
142
|
+
hasApiKey: false,
|
|
143
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
144
|
+
budget: 0
|
|
145
|
+
};
|
|
146
|
+
if (defaultPlan && defaultPlan.type !== "exclude") {
|
|
147
|
+
tracked.planName = defaultPlan.name;
|
|
148
|
+
if (defaultPlan.type === "flat" && defaultPlan.monthlyBase !== void 0) {
|
|
149
|
+
tracked.planCost = defaultPlan.monthlyBase;
|
|
150
|
+
tracked.budget = defaultPlan.monthlyBase;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
154
|
+
const envKey = findEnvKey(service);
|
|
155
|
+
let keySource = "";
|
|
156
|
+
if (existingKey) {
|
|
157
|
+
tracked.hasApiKey = true;
|
|
158
|
+
keySource = " (key: global config)";
|
|
159
|
+
} else if (envKey) {
|
|
160
|
+
tracked.hasApiKey = true;
|
|
161
|
+
if (!globalConfig.services[service.id]) {
|
|
162
|
+
globalConfig.services[service.id] = {};
|
|
163
|
+
}
|
|
164
|
+
globalConfig.services[service.id].apiKey = envKey;
|
|
165
|
+
keySource = ` (key: ${service.envPatterns[0]})`;
|
|
166
|
+
}
|
|
167
|
+
const tierLabel = tracked.hasApiKey ? "LIVE" : tracked.planCost !== void 0 ? "CALC" : "BLIND";
|
|
168
|
+
const planStr = tracked.planName ? ` ${tracked.planName}` : "";
|
|
169
|
+
console.log(
|
|
170
|
+
` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`
|
|
171
|
+
);
|
|
172
|
+
services[service.id] = tracked;
|
|
173
|
+
}
|
|
174
|
+
console.log("");
|
|
175
|
+
}
|
|
176
|
+
const trackedList = Object.values(services);
|
|
177
|
+
const liveCount = trackedList.filter((s) => s.hasApiKey).length;
|
|
178
|
+
const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);
|
|
179
|
+
console.log(" " + "-".repeat(48));
|
|
180
|
+
console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);
|
|
181
|
+
if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);
|
|
182
|
+
const needBudget = trackedList.filter(
|
|
183
|
+
(s) => s.budget === 0 && s.planCost === void 0
|
|
184
|
+
);
|
|
185
|
+
if (needBudget.length > 0) {
|
|
186
|
+
console.log(`
|
|
187
|
+
${needBudget.length} usage-based service${needBudget.length > 1 ? "s" : ""} need budgets:`);
|
|
188
|
+
for (const svc of needBudget) {
|
|
189
|
+
console.log(` burnwatch add ${svc.serviceId} --budget <AMOUNT>`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
writeGlobalConfig(globalConfig);
|
|
194
|
+
return { services };
|
|
195
|
+
}
|
|
106
196
|
async function runInteractiveInit(detected) {
|
|
107
197
|
const rl = readline.createInterface({
|
|
108
198
|
input: process.stdin,
|
|
@@ -112,14 +202,16 @@ async function runInteractiveInit(detected) {
|
|
|
112
202
|
const groups = groupByRisk(detected);
|
|
113
203
|
const globalConfig = readGlobalConfig();
|
|
114
204
|
console.log(
|
|
115
|
-
|
|
205
|
+
`
|
|
206
|
+
Found ${detected.length} paid service${detected.length !== 1 ? "s" : ""}. Let's configure each one.
|
|
207
|
+
`
|
|
116
208
|
);
|
|
117
209
|
for (const category of RISK_ORDER) {
|
|
118
210
|
const group = groups.get(category);
|
|
119
211
|
if (group.length === 0) continue;
|
|
120
212
|
console.log(`
|
|
121
|
-
${RISK_LABELS[category]}`);
|
|
122
|
-
console.log("
|
|
213
|
+
${RISK_LABELS[category]}`);
|
|
214
|
+
console.log(" " + "-".repeat(48));
|
|
123
215
|
for (const det of group) {
|
|
124
216
|
const service = det.service;
|
|
125
217
|
const plans = service.plans;
|
|
@@ -131,23 +223,24 @@ ${RISK_LABELS[category]}`);
|
|
|
131
223
|
serviceId: service.id,
|
|
132
224
|
detectedVia: det.sources,
|
|
133
225
|
hasApiKey: false,
|
|
134
|
-
firstDetected: (/* @__PURE__ */ new Date()).toISOString()
|
|
226
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
227
|
+
budget: 0
|
|
135
228
|
};
|
|
136
|
-
console.log("
|
|
229
|
+
console.log(" -> Configured (no plan tiers in registry, budget: $0)");
|
|
137
230
|
continue;
|
|
138
231
|
}
|
|
139
232
|
const defaultIndex = plans.findIndex((p) => p.default);
|
|
140
233
|
console.log("");
|
|
141
234
|
for (let i = 0; i < plans.length; i++) {
|
|
142
235
|
const plan = plans[i];
|
|
143
|
-
const marker = i === defaultIndex ? "
|
|
144
|
-
const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? `
|
|
236
|
+
const marker = i === defaultIndex ? " *" : "";
|
|
237
|
+
const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? ` - $${plan.monthlyBase}/mo` : " - variable";
|
|
145
238
|
console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);
|
|
146
239
|
}
|
|
147
240
|
const defaultChoice = defaultIndex >= 0 ? String(defaultIndex + 1) : "1";
|
|
148
241
|
const answer = await ask(
|
|
149
242
|
rl,
|
|
150
|
-
`
|
|
243
|
+
` Which plan? [${defaultChoice}]: `
|
|
151
244
|
);
|
|
152
245
|
const choiceIndex = (answer === "" ? parseInt(defaultChoice) : parseInt(answer)) - 1;
|
|
153
246
|
const chosen = plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0];
|
|
@@ -160,10 +253,10 @@ ${RISK_LABELS[category]}`);
|
|
|
160
253
|
excluded: true,
|
|
161
254
|
planName: chosen.name
|
|
162
255
|
};
|
|
163
|
-
console.log(`
|
|
256
|
+
console.log(` -> ${service.name}: excluded`);
|
|
164
257
|
continue;
|
|
165
258
|
}
|
|
166
|
-
const
|
|
259
|
+
const tracked2 = {
|
|
167
260
|
serviceId: service.id,
|
|
168
261
|
detectedVia: det.sources,
|
|
169
262
|
hasApiKey: false,
|
|
@@ -171,72 +264,92 @@ ${RISK_LABELS[category]}`);
|
|
|
171
264
|
planName: chosen.name
|
|
172
265
|
};
|
|
173
266
|
if (chosen.type === "flat" && chosen.monthlyBase !== void 0) {
|
|
174
|
-
|
|
175
|
-
if (chosen.monthlyBase > 0) {
|
|
176
|
-
tracked.budget = chosen.monthlyBase;
|
|
177
|
-
}
|
|
267
|
+
tracked2.planCost = chosen.monthlyBase;
|
|
178
268
|
}
|
|
179
|
-
if (service.apiTier === "live"
|
|
269
|
+
if (service.apiTier === "live") {
|
|
180
270
|
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
271
|
+
const envKey = findEnvKey(service);
|
|
181
272
|
if (existingKey) {
|
|
182
|
-
console.log(`
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
tracked.planName = planName;
|
|
190
|
-
}
|
|
273
|
+
console.log(` API key: found in global config`);
|
|
274
|
+
tracked2.hasApiKey = true;
|
|
275
|
+
} else if (envKey) {
|
|
276
|
+
console.log(` API key: found in environment (${service.envPatterns[0]})`);
|
|
277
|
+
tracked2.hasApiKey = true;
|
|
278
|
+
if (!globalConfig.services[service.id]) {
|
|
279
|
+
globalConfig.services[service.id] = {};
|
|
191
280
|
}
|
|
192
|
-
|
|
281
|
+
globalConfig.services[service.id].apiKey = envKey;
|
|
282
|
+
} else {
|
|
283
|
+
const hint = API_KEY_HINTS[service.id];
|
|
284
|
+
if (hint) console.log(` ${hint}`);
|
|
193
285
|
const keyAnswer = await ask(
|
|
194
286
|
rl,
|
|
195
|
-
`
|
|
287
|
+
` API key for real-time tracking (Enter to skip): `
|
|
196
288
|
);
|
|
197
289
|
if (keyAnswer) {
|
|
198
|
-
|
|
290
|
+
tracked2.hasApiKey = true;
|
|
199
291
|
if (!globalConfig.services[service.id]) {
|
|
200
292
|
globalConfig.services[service.id] = {};
|
|
201
293
|
}
|
|
202
294
|
globalConfig.services[service.id].apiKey = keyAnswer;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (service.autoDetectPlan && service.id === "scrapfly" && tracked2.hasApiKey) {
|
|
298
|
+
const key = globalConfig.services[service.id]?.apiKey;
|
|
299
|
+
if (key) {
|
|
300
|
+
console.log(" Detecting plan from API...");
|
|
301
|
+
const planName = await autoDetectScrapflyPlan(key);
|
|
302
|
+
if (planName) {
|
|
303
|
+
console.log(` -> Detected plan: ${planName}`);
|
|
304
|
+
tracked2.planName = planName;
|
|
210
305
|
}
|
|
211
306
|
}
|
|
212
307
|
}
|
|
213
308
|
}
|
|
214
|
-
|
|
215
|
-
|
|
309
|
+
const planCost = chosen.monthlyBase ?? 0;
|
|
310
|
+
if (chosen.type === "usage" && planCost === 0) {
|
|
311
|
+
const budgetAnswer = await ask(
|
|
312
|
+
rl,
|
|
313
|
+
` Monthly budget: $`
|
|
314
|
+
);
|
|
315
|
+
const parsed = parseFloat(budgetAnswer);
|
|
316
|
+
tracked2.budget = !isNaN(parsed) ? parsed : 0;
|
|
317
|
+
} else {
|
|
216
318
|
const budgetAnswer = await ask(
|
|
217
319
|
rl,
|
|
218
|
-
` Monthly budget
|
|
320
|
+
` Monthly budget [$${planCost}]: $`
|
|
219
321
|
);
|
|
220
322
|
if (budgetAnswer) {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
323
|
+
const parsed = parseFloat(budgetAnswer);
|
|
324
|
+
tracked2.budget = !isNaN(parsed) ? parsed : planCost;
|
|
325
|
+
} else {
|
|
326
|
+
tracked2.budget = planCost;
|
|
225
327
|
}
|
|
226
328
|
}
|
|
227
|
-
services[service.id] =
|
|
228
|
-
const tierLabel =
|
|
229
|
-
const budgetStr = tracked.budget !== void 0 ? ` | Budget: $${tracked.budget}/mo` : "";
|
|
329
|
+
services[service.id] = tracked2;
|
|
330
|
+
const tierLabel = tracked2.hasApiKey ? "LIVE" : tracked2.planCost !== void 0 ? "CALC" : "BLIND";
|
|
230
331
|
console.log(
|
|
231
|
-
`
|
|
332
|
+
` -> ${service.name}: ${tracked2.planName} | ${tierLabel} | $${tracked2.budget}/mo`
|
|
232
333
|
);
|
|
233
334
|
}
|
|
234
335
|
}
|
|
336
|
+
const tracked = Object.values(services).filter((s) => !s.excluded);
|
|
337
|
+
const excluded = Object.values(services).filter((s) => s.excluded);
|
|
338
|
+
const liveCount = tracked.filter((s) => s.hasApiKey).length;
|
|
339
|
+
const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);
|
|
340
|
+
console.log("\n " + "=".repeat(48));
|
|
341
|
+
console.log(` ${tracked.length} services configured`);
|
|
342
|
+
if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);
|
|
343
|
+
if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);
|
|
344
|
+
if (excluded.length > 0) console.log(` ${excluded.length} excluded`);
|
|
345
|
+
console.log(` Total monthly budget: $${totalBudget}`);
|
|
346
|
+
console.log(" " + "=".repeat(48));
|
|
235
347
|
writeGlobalConfig(globalConfig);
|
|
236
348
|
rl.close();
|
|
237
349
|
return { services };
|
|
238
350
|
}
|
|
239
351
|
export {
|
|
352
|
+
autoConfigureServices,
|
|
240
353
|
runInteractiveInit
|
|
241
354
|
};
|
|
242
355
|
//# sourceMappingURL=interactive-init.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/interactive-init.ts","../src/core/config.ts","../src/services/base.ts"],"sourcesContent":["/**\n * Interactive init flow for burnwatch.\n *\n * Groups detected services by risk category, presents plan tiers,\n * and collects user choices via Node readline.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Risk categories in display order: LLMs first, then usage-based, infra, flat-rate */\nconst RISK_ORDER: ServiceRiskCategory[] = [\"llm\", \"usage\", \"infra\", \"flat\"];\n\nconst RISK_LABELS: Record<ServiceRiskCategory, string> = {\n llm: \"š¤ LLM / AI Services (highest variable cost)\",\n usage: \"š Usage-Based Services\",\n infra: \"šļø Infrastructure & Compute\",\n flat: \"š¦ Flat-Rate / Free Tier Services\",\n};\n\n/** Map service IDs to risk categories */\nfunction classifyRisk(service: ServiceDefinition): ServiceRiskCategory {\n if (service.billingModel === \"token_usage\") return \"llm\";\n if (\n service.billingModel === \"credit_pool\" ||\n service.billingModel === \"percentage\" ||\n service.billingModel === \"per_unit\"\n )\n return \"usage\";\n if (service.billingModel === \"compute\") return \"infra\";\n return \"flat\";\n}\n\n/** Group detection results by risk category */\nfunction groupByRisk(\n detected: DetectionResult[],\n): Map<ServiceRiskCategory, DetectionResult[]> {\n const groups = new Map<ServiceRiskCategory, DetectionResult[]>();\n for (const cat of RISK_ORDER) {\n groups.set(cat, []);\n }\n\n for (const det of detected) {\n const cat = classifyRisk(det.service);\n groups.get(cat)!.push(det);\n }\n\n return groups;\n}\n\n/** Prompt the user with a question and return their answer */\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/** Try to auto-detect plan from Scrapfly API */\nasync function autoDetectScrapflyPlan(\n apiKey: string,\n): Promise<string | null> {\n try {\n const result = await fetchJson<{\n subscription?: { plan?: { name?: string } };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (result.ok && result.data?.subscription?.plan?.name) {\n return result.data.subscription.plan.name;\n }\n } catch {\n // Ignore errors\n }\n return null;\n}\n\nexport interface InteractiveInitResult {\n services: Record<string, TrackedService>;\n}\n\n/**\n * Run the interactive init flow.\n * Shows detected services grouped by risk, lets user pick plans.\n */\nexport async function runInteractiveInit(\n detected: DetectionResult[],\n): Promise<InteractiveInitResult> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n \"\\nš Let's configure each detected service. Services are grouped by cost risk.\\n\",\n );\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(`\\n${RISK_LABELS[category]}`);\n console.log(\"ā\".repeat(50));\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans;\n\n console.log(`\\n ${service.name}`);\n console.log(` Detected via: ${det.details.join(\", \")}`);\n\n if (!plans || plans.length === 0) {\n // No plans defined ā fall back to basic tracking\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n };\n console.log(\" ā Auto-configured (no plan tiers available)\");\n continue;\n }\n\n // Show plan options\n const defaultIndex = plans.findIndex((p) => p.default);\n console.log(\"\");\n for (let i = 0; i < plans.length; i++) {\n const plan = plans[i]!;\n const marker = i === defaultIndex ? \" (recommended)\" : \"\";\n const costStr =\n plan.type === \"exclude\"\n ? \"\"\n : plan.monthlyBase !== undefined\n ? ` ā $${plan.monthlyBase}/mo`\n : \" ā variable\";\n console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);\n }\n\n const defaultChoice =\n defaultIndex >= 0 ? String(defaultIndex + 1) : \"1\";\n const answer = await ask(\n rl,\n ` Choose [${defaultChoice}]: `,\n );\n\n const choiceIndex = (answer === \"\" ? parseInt(defaultChoice) : parseInt(answer)) - 1;\n const chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n\n if (chosen.type === \"exclude\") {\n // Explicitly excluded\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n excluded: true,\n planName: chosen.name,\n };\n console.log(` ā ${service.name}: excluded from tracking`);\n continue;\n }\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n planName: chosen.name,\n };\n\n if (chosen.type === \"flat\" && chosen.monthlyBase !== undefined) {\n tracked.planCost = chosen.monthlyBase;\n // Auto-set budget to plan cost for paid flat plans\n if (chosen.monthlyBase > 0) {\n tracked.budget = chosen.monthlyBase;\n }\n }\n\n // If the service has a billing API, offer to provide a key\n if (service.apiTier === \"live\" || chosen.requiresKey) {\n // Check if we already have a key in global config\n const existingKey = globalConfig.services[service.id]?.apiKey;\n if (existingKey) {\n console.log(` š Using existing API key from global config`);\n tracked.hasApiKey = true;\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\") {\n console.log(\" š Auto-detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(existingKey);\n if (planName) {\n console.log(` ā Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n } else if (chosen.requiresKey) {\n const keyAnswer = await ask(\n rl,\n ` Enter API key (or press Enter to skip): `,\n );\n if (keyAnswer) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\") {\n console.log(\" š Auto-detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(keyAnswer);\n if (planName) {\n console.log(` ā Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n }\n }\n }\n\n // Always ask for budget if not already set to a meaningful value\n if (tracked.budget === undefined || tracked.budget === 0) {\n const suggestion = chosen.monthlyBase && chosen.monthlyBase > 0\n ? ` [${chosen.monthlyBase}]`\n : \"\";\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget in USD${suggestion} (or press Enter to skip): $`,\n );\n if (budgetAnswer) {\n const budget = parseFloat(budgetAnswer);\n if (!isNaN(budget)) {\n tracked.budget = budget;\n }\n }\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"ā
LIVE\"\n : tracked.planCost !== undefined\n ? \"š” CALC\"\n : \"š“ BLIND\";\n const budgetStr = tracked.budget !== undefined ? ` | Budget: $${tracked.budget}/mo` : \"\";\n console.log(\n ` ā ${service.name}: ${chosen.name} (${tierLabel}${budgetStr})`,\n );\n }\n }\n\n // Save any collected API keys\n writeGlobalConfig(globalConfig);\n\n rl.close();\n\n return { services };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory ā stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory ā stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory ā stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions ā this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/interactive-init.ts","../src/core/config.ts","../src/services/base.ts"],"sourcesContent":["/**\n * Interactive init flow for burnwatch.\n *\n * Conducts a per-service interview: detects what it can automatically\n * (existing API keys, env vars), asks for plan selection, collects\n * API keys for LIVE tracking, and ensures every service exits with\n * a budget. No skipping.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Risk categories in display order: LLMs first, then usage-based, infra, flat-rate */\nconst RISK_ORDER: ServiceRiskCategory[] = [\"llm\", \"usage\", \"infra\", \"flat\"];\n\nconst RISK_LABELS: Record<ServiceRiskCategory, string> = {\n llm: \"LLM / AI Services (highest variable cost)\",\n usage: \"Usage-Based Services\",\n infra: \"Infrastructure & Compute\",\n flat: \"Flat-Rate / Free Tier Services\",\n};\n\n/** Where to find API keys for LIVE-capable services */\nconst API_KEY_HINTS: Record<string, string> = {\n anthropic: \"Admin key: console.anthropic.com -> Settings -> Admin API Keys\",\n openai: \"Org key: platform.openai.com -> Settings -> API Keys\",\n vercel: \"Token: vercel.com/account/tokens\",\n supabase: \"Service role key: supabase.com/dashboard -> Settings -> API\",\n stripe: \"Secret key: dashboard.stripe.com -> Developers -> API Keys\",\n scrapfly: \"API key: scrapfly.io/dashboard\",\n};\n\n/** Map service IDs to risk categories */\nfunction classifyRisk(service: ServiceDefinition): ServiceRiskCategory {\n if (service.billingModel === \"token_usage\") return \"llm\";\n if (\n service.billingModel === \"credit_pool\" ||\n service.billingModel === \"percentage\" ||\n service.billingModel === \"per_unit\"\n )\n return \"usage\";\n if (service.billingModel === \"compute\") return \"infra\";\n return \"flat\";\n}\n\n/** Group detection results by risk category */\nfunction groupByRisk(\n detected: DetectionResult[],\n): Map<ServiceRiskCategory, DetectionResult[]> {\n const groups = new Map<ServiceRiskCategory, DetectionResult[]>();\n for (const cat of RISK_ORDER) {\n groups.set(cat, []);\n }\n\n for (const det of detected) {\n const cat = classifyRisk(det.service);\n groups.get(cat)!.push(det);\n }\n\n return groups;\n}\n\n/** Prompt the user with a question and return their answer */\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/** Try to auto-detect plan from Scrapfly API */\nasync function autoDetectScrapflyPlan(\n apiKey: string,\n): Promise<string | null> {\n try {\n const result = await fetchJson<{\n subscription?: { plan?: { name?: string } };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (result.ok && result.data?.subscription?.plan?.name) {\n return result.data.subscription.plan.name;\n }\n } catch {\n // Ignore errors\n }\n return null;\n}\n\n/** Scan environment for API keys matching service env patterns */\nfunction findEnvKey(service: ServiceDefinition): string | undefined {\n for (const pattern of service.envPatterns) {\n const val = process.env[pattern];\n if (val && val.length > 0) return val;\n }\n return undefined;\n}\n\nexport interface InteractiveInitResult {\n services: Record<string, TrackedService>;\n}\n\n/**\n * Auto-configure all services without prompts.\n *\n * Applies the same logic as the interactive interview but picks\n * defaults automatically: default plan, env var keys, budget = plan cost.\n * Used when stdin is not a TTY (e.g., Claude Code, piped input).\n */\nexport function autoConfigureServices(\n detected: DetectionResult[],\n): InteractiveInitResult {\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Auto-configuring with defaults.\\n`,\n );\n console.log(\" Run 'burnwatch init' from your terminal for interactive setup.\\n\");\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(` ${RISK_LABELS[category]}`);\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans ?? [];\n const defaultPlan = plans.find((p) => p.default) ?? plans[0];\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n\n if (defaultPlan && defaultPlan.type !== \"exclude\") {\n tracked.planName = defaultPlan.name;\n\n if (defaultPlan.type === \"flat\" && defaultPlan.monthlyBase !== undefined) {\n tracked.planCost = defaultPlan.monthlyBase;\n tracked.budget = defaultPlan.monthlyBase;\n }\n }\n\n // Check for existing API key in global config or environment\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n let keySource = \"\";\n\n if (existingKey) {\n tracked.hasApiKey = true;\n keySource = \" (key: global config)\";\n } else if (envKey) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n keySource = ` (key: ${service.envPatterns[0]})`;\n }\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n const planStr = tracked.planName ? ` ${tracked.planName}` : \"\";\n console.log(\n ` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`,\n );\n\n services[service.id] = tracked;\n }\n console.log(\"\");\n }\n\n // Summary\n const trackedList = Object.values(services);\n const liveCount = trackedList.filter((s) => s.hasApiKey).length;\n const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\" \" + \"-\".repeat(48));\n console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n\n const needBudget = trackedList.filter(\n (s) => s.budget === 0 && s.planCost === undefined,\n );\n if (needBudget.length > 0) {\n console.log(`\\n ${needBudget.length} usage-based service${needBudget.length > 1 ? \"s\" : \"\"} need budgets:`);\n for (const svc of needBudget) {\n console.log(` burnwatch add ${svc.serviceId} --budget <AMOUNT>`);\n }\n }\n console.log(\"\");\n\n // Save discovered keys\n writeGlobalConfig(globalConfig);\n\n return { services };\n}\n\n/**\n * Run the interactive init flow.\n *\n * For each detected service:\n * 1. Ask which plan they're on\n * 2. If LIVE-capable, check for existing key or ask for one\n * 3. Set budget (defaults to plan cost, $0 for free - never skipped)\n */\nexport async function runInteractiveInit(\n detected: DetectionResult[],\n): Promise<InteractiveInitResult> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Let's configure each one.\\n`,\n );\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(`\\n ${RISK_LABELS[category]}`);\n console.log(\" \" + \"-\".repeat(48));\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans;\n\n console.log(`\\n ${service.name}`);\n console.log(` Detected via: ${det.details.join(\", \")}`);\n\n if (!plans || plans.length === 0) {\n // No plans defined - basic tracking with $0 budget\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n console.log(\" -> Configured (no plan tiers in registry, budget: $0)\");\n continue;\n }\n\n // --- Plan selection ---\n const defaultIndex = plans.findIndex((p) => p.default);\n console.log(\"\");\n for (let i = 0; i < plans.length; i++) {\n const plan = plans[i]!;\n const marker = i === defaultIndex ? \" *\" : \"\";\n const costStr =\n plan.type === \"exclude\"\n ? \"\"\n : plan.monthlyBase !== undefined\n ? ` - $${plan.monthlyBase}/mo`\n : \" - variable\";\n console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);\n }\n\n const defaultChoice =\n defaultIndex >= 0 ? String(defaultIndex + 1) : \"1\";\n const answer = await ask(\n rl,\n ` Which plan? [${defaultChoice}]: `,\n );\n\n const choiceIndex = (answer === \"\" ? parseInt(defaultChoice) : parseInt(answer)) - 1;\n const chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n\n if (chosen.type === \"exclude\") {\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n excluded: true,\n planName: chosen.name,\n };\n console.log(` -> ${service.name}: excluded`);\n continue;\n }\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n planName: chosen.name,\n };\n\n if (chosen.type === \"flat\" && chosen.monthlyBase !== undefined) {\n tracked.planCost = chosen.monthlyBase;\n }\n\n // --- API key (LIVE-capable services) ---\n if (service.apiTier === \"live\") {\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n console.log(` API key: found in global config`);\n tracked.hasApiKey = true;\n } else if (envKey) {\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n } else {\n const hint = API_KEY_HINTS[service.id];\n if (hint) console.log(` ${hint}`);\n const keyAnswer = await ask(\n rl,\n ` API key for real-time tracking (Enter to skip): `,\n );\n if (keyAnswer) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n }\n }\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\" && tracked.hasApiKey) {\n const key = globalConfig.services[service.id]?.apiKey;\n if (key) {\n console.log(\" Detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(key);\n if (planName) {\n console.log(` -> Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n }\n }\n\n // --- Budget (always set, never skip) ---\n const planCost = chosen.monthlyBase ?? 0;\n\n if (chosen.type === \"usage\" && planCost === 0) {\n // Usage-based with no fixed cost - need a real number\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget: $`,\n );\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : 0;\n } else {\n // Flat plan or usage with known base - default to plan cost\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget [$${planCost}]: $`,\n );\n if (budgetAnswer) {\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : planCost;\n } else {\n tracked.budget = planCost;\n }\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo`,\n );\n }\n }\n\n // --- Summary ---\n const tracked = Object.values(services).filter((s) => !s.excluded);\n const excluded = Object.values(services).filter((s) => s.excluded);\n const liveCount = tracked.filter((s) => s.hasApiKey).length;\n const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\"\\n \" + \"=\".repeat(48));\n console.log(` ${tracked.length} services configured`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);\n if (excluded.length > 0) console.log(` ${excluded.length} excluded`);\n console.log(` Total monthly budget: $${totalBudget}`);\n console.log(\" \" + \"=\".repeat(48));\n\n // Save any collected API keys\n writeGlobalConfig(globalConfig);\n\n rl.close();\n\n return { services };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory ā stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory ā stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory ā stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions ā this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n"],"mappings":";AASA,YAAY,cAAc;;;ACT1B,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,YAAY,QAAQ;AAab,SAAS,kBAA0B;AACxC,QAAM,YAAY,QAAQ,IAAI,iBAAiB;AAC/C,MAAI,UAAW,QAAY,UAAK,WAAW,WAAW;AACtD,SAAY,UAAQ,WAAQ,GAAG,WAAW,WAAW;AACvD;AA0BO,SAAS,mBAAiC;AAC/C,QAAM,aAAkB,UAAK,gBAAgB,GAAG,aAAa;AAC7D,MAAI;AACF,UAAM,MAAS,gBAAa,YAAY,OAAO;AAC/C,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO,EAAE,UAAU,CAAC,EAAE;AAAA,EACxB;AACF;AAEO,SAAS,kBAAkB,QAA4B;AAC5D,QAAM,MAAM,gBAAgB;AAC5B,EAAG,aAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,QAAM,aAAkB,UAAK,KAAK,aAAa;AAC/C,EAAG,iBAAc,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,MAAM,OAAO;AAE5E,EAAG,aAAU,YAAY,GAAK;AAChC;;;ACpCA,eAAsB,UACpB,KACA,UAKI,CAAC,GAC+D;AACpE,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY;AAAA,MAChB,MAAM,WAAW,MAAM;AAAA,MACvB,QAAQ,WAAW;AAAA,IACrB;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ,QAAQ,UAAU;AAAA,MAC1B,SAAS,QAAQ;AAAA,MACjB,MAAM,QAAQ;AAAA,MACd,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,SAAS;AAEtB,QAAI,CAAC,SAAS,IAAI;AAChB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,SAAS;AAAA,QACjB,OAAO,QAAQ,SAAS,MAAM,KAAK,SAAS,UAAU;AAAA,MACxD;AAAA,IACF;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,EAAE,IAAI,MAAM,QAAQ,SAAS,QAAQ,KAAK;AAAA,EACnD,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,OAAO,eAAe,QAAQ,IAAI,UAAU;AAAA,IAC9C;AAAA,EACF;AACF;;;AF/CA,IAAM,aAAoC,CAAC,OAAO,SAAS,SAAS,MAAM;AAE1E,IAAM,cAAmD;AAAA,EACvD,KAAK;AAAA,EACL,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AACR;AAGA,IAAM,gBAAwC;AAAA,EAC5C,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,UAAU;AACZ;AAGA,SAAS,aAAa,SAAiD;AACrE,MAAI,QAAQ,iBAAiB,cAAe,QAAO;AACnD,MACE,QAAQ,iBAAiB,iBACzB,QAAQ,iBAAiB,gBACzB,QAAQ,iBAAiB;AAEzB,WAAO;AACT,MAAI,QAAQ,iBAAiB,UAAW,QAAO;AAC/C,SAAO;AACT;AAGA,SAAS,YACP,UAC6C;AAC7C,QAAM,SAAS,oBAAI,IAA4C;AAC/D,aAAW,OAAO,YAAY;AAC5B,WAAO,IAAI,KAAK,CAAC,CAAC;AAAA,EACpB;AAEA,aAAW,OAAO,UAAU;AAC1B,UAAM,MAAM,aAAa,IAAI,OAAO;AACpC,WAAO,IAAI,GAAG,EAAG,KAAK,GAAG;AAAA,EAC3B;AAEA,SAAO;AACT;AAGA,SAAS,IAAI,IAAwB,UAAmC;AACtE,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,OAAG,SAAS,UAAU,CAAC,WAAW;AAChC,cAAQ,OAAO,KAAK,CAAC;AAAA,IACvB,CAAC;AAAA,EACH,CAAC;AACH;AAGA,eAAe,uBACb,QACwB;AACxB,MAAI;AACF,UAAM,SAAS,MAAM,UAElB,uCAAuC,MAAM,EAAE;AAElD,QAAI,OAAO,MAAM,OAAO,MAAM,cAAc,MAAM,MAAM;AACtD,aAAO,OAAO,KAAK,aAAa,KAAK;AAAA,IACvC;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGA,SAAS,WAAW,SAAgD;AAClE,aAAW,WAAW,QAAQ,aAAa;AACzC,UAAM,MAAM,QAAQ,IAAI,OAAO;AAC/B,QAAI,OAAO,IAAI,SAAS,EAAG,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAaO,SAAS,sBACd,UACuB;AACvB,QAAM,WAA2C,CAAC;AAClD,QAAM,SAAS,YAAY,QAAQ;AACnC,QAAM,eAAe,iBAAiB;AAEtC,UAAQ;AAAA,IACN;AAAA,UAAa,SAAS,MAAM,gBAAgB,SAAS,WAAW,IAAI,MAAM,EAAE;AAAA;AAAA,EAC9E;AACA,UAAQ,IAAI,oEAAoE;AAEhF,aAAW,YAAY,YAAY;AACjC,UAAM,QAAQ,OAAO,IAAI,QAAQ;AACjC,QAAI,MAAM,WAAW,EAAG;AAExB,YAAQ,IAAI,KAAK,YAAY,QAAQ,CAAC,EAAE;AAExC,eAAW,OAAO,OAAO;AACvB,YAAM,UAAU,IAAI;AACpB,YAAM,QAAQ,QAAQ,SAAS,CAAC;AAChC,YAAM,cAAc,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC;AAE3D,YAAM,UAA0B;AAAA,QAC9B,WAAW,QAAQ;AAAA,QACnB,aAAa,IAAI;AAAA,QACjB,WAAW;AAAA,QACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,QACtC,QAAQ;AAAA,MACV;AAEA,UAAI,eAAe,YAAY,SAAS,WAAW;AACjD,gBAAQ,WAAW,YAAY;AAE/B,YAAI,YAAY,SAAS,UAAU,YAAY,gBAAgB,QAAW;AACxE,kBAAQ,WAAW,YAAY;AAC/B,kBAAQ,SAAS,YAAY;AAAA,QAC/B;AAAA,MACF;AAGA,YAAM,cAAc,aAAa,SAAS,QAAQ,EAAE,GAAG;AACvD,YAAM,SAAS,WAAW,OAAO;AACjC,UAAI,YAAY;AAEhB,UAAI,aAAa;AACf,gBAAQ,YAAY;AACpB,oBAAY;AAAA,MACd,WAAW,QAAQ;AACjB,gBAAQ,YAAY;AACpB,YAAI,CAAC,aAAa,SAAS,QAAQ,EAAE,GAAG;AACtC,uBAAa,SAAS,QAAQ,EAAE,IAAI,CAAC;AAAA,QACvC;AACA,qBAAa,SAAS,QAAQ,EAAE,EAAG,SAAS;AAC5C,oBAAY,UAAU,QAAQ,YAAY,CAAC,CAAC;AAAA,MAC9C;AAEA,YAAM,YAAY,QAAQ,YACtB,SACA,QAAQ,aAAa,SACnB,SACA;AACN,YAAM,UAAU,QAAQ,WAAW,IAAI,QAAQ,QAAQ,KAAK;AAC5D,cAAQ;AAAA,QACN,OAAO,QAAQ,IAAI,IAAI,OAAO,MAAM,SAAS,OAAO,QAAQ,MAAM,MAAM,SAAS;AAAA,MACnF;AAEA,eAAS,QAAQ,EAAE,IAAI;AAAA,IACzB;AACA,YAAQ,IAAI,EAAE;AAAA,EAChB;AAGA,QAAM,cAAc,OAAO,OAAO,QAAQ;AAC1C,QAAM,YAAY,YAAY,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE;AACzD,QAAM,cAAc,YAAY,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,UAAU,IAAI,CAAC;AAE3E,UAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;AACjC,UAAQ,IAAI,KAAK,YAAY,MAAM,yCAAyC,WAAW,KAAK;AAC5F,MAAI,YAAY,EAAG,SAAQ,IAAI,KAAK,SAAS,gCAAgC;AAE7E,QAAM,aAAa,YAAY;AAAA,IAC7B,CAAC,MAAM,EAAE,WAAW,KAAK,EAAE,aAAa;AAAA,EAC1C;AACA,MAAI,WAAW,SAAS,GAAG;AACzB,YAAQ,IAAI;AAAA,IAAO,WAAW,MAAM,uBAAuB,WAAW,SAAS,IAAI,MAAM,EAAE,gBAAgB;AAC3G,eAAW,OAAO,YAAY;AAC5B,cAAQ,IAAI,qBAAqB,IAAI,SAAS,oBAAoB;AAAA,IACpE;AAAA,EACF;AACA,UAAQ,IAAI,EAAE;AAGd,oBAAkB,YAAY;AAE9B,SAAO,EAAE,SAAS;AACpB;AAUA,eAAsB,mBACpB,UACgC;AAChC,QAAM,KAAc,yBAAgB;AAAA,IAClC,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,EAClB,CAAC;AAED,QAAM,WAA2C,CAAC;AAClD,QAAM,SAAS,YAAY,QAAQ;AACnC,QAAM,eAAe,iBAAiB;AAEtC,UAAQ;AAAA,IACN;AAAA,UAAa,SAAS,MAAM,gBAAgB,SAAS,WAAW,IAAI,MAAM,EAAE;AAAA;AAAA,EAC9E;AAEA,aAAW,YAAY,YAAY;AACjC,UAAM,QAAQ,OAAO,IAAI,QAAQ;AACjC,QAAI,MAAM,WAAW,EAAG;AAExB,YAAQ,IAAI;AAAA,IAAO,YAAY,QAAQ,CAAC,EAAE;AAC1C,YAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;AAEjC,eAAW,OAAO,OAAO;AACvB,YAAM,UAAU,IAAI;AACpB,YAAM,QAAQ,QAAQ;AAEtB,cAAQ,IAAI;AAAA,IAAO,QAAQ,IAAI,EAAE;AACjC,cAAQ,IAAI,mBAAmB,IAAI,QAAQ,KAAK,IAAI,CAAC,EAAE;AAEvD,UAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAEhC,iBAAS,QAAQ,EAAE,IAAI;AAAA,UACrB,WAAW,QAAQ;AAAA,UACnB,aAAa,IAAI;AAAA,UACjB,WAAW;AAAA,UACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,UACtC,QAAQ;AAAA,QACV;AACA,gBAAQ,IAAI,yDAAyD;AACrE;AAAA,MACF;AAGA,YAAM,eAAe,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO;AACrD,cAAQ,IAAI,EAAE;AACd,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAM,OAAO,MAAM,CAAC;AACpB,cAAM,SAAS,MAAM,eAAe,OAAO;AAC3C,cAAM,UACJ,KAAK,SAAS,YACV,KACA,KAAK,gBAAgB,SACnB,OAAO,KAAK,WAAW,QACvB;AACR,gBAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,IAAI,GAAG,OAAO,GAAG,MAAM,EAAE;AAAA,MAC7D;AAEA,YAAM,gBACJ,gBAAgB,IAAI,OAAO,eAAe,CAAC,IAAI;AACjD,YAAM,SAAS,MAAM;AAAA,QACnB;AAAA,QACA,kBAAkB,aAAa;AAAA,MACjC;AAEA,YAAM,eAAe,WAAW,KAAK,SAAS,aAAa,IAAI,SAAS,MAAM,KAAK;AACnF,YAAM,SACJ,MAAM,WAAW,KAAK,MAAM,gBAAgB,IAAI,eAAe,CAAC;AAElE,UAAI,OAAO,SAAS,WAAW;AAC7B,iBAAS,QAAQ,EAAE,IAAI;AAAA,UACrB,WAAW,QAAQ;AAAA,UACnB,aAAa,IAAI;AAAA,UACjB,WAAW;AAAA,UACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,UACtC,UAAU;AAAA,UACV,UAAU,OAAO;AAAA,QACnB;AACA,gBAAQ,IAAI,QAAQ,QAAQ,IAAI,YAAY;AAC5C;AAAA,MACF;AAEA,YAAMA,WAA0B;AAAA,QAC9B,WAAW,QAAQ;AAAA,QACnB,aAAa,IAAI;AAAA,QACjB,WAAW;AAAA,QACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,QACtC,UAAU,OAAO;AAAA,MACnB;AAEA,UAAI,OAAO,SAAS,UAAU,OAAO,gBAAgB,QAAW;AAC9D,QAAAA,SAAQ,WAAW,OAAO;AAAA,MAC5B;AAGA,UAAI,QAAQ,YAAY,QAAQ;AAC9B,cAAM,cAAc,aAAa,SAAS,QAAQ,EAAE,GAAG;AACvD,cAAM,SAAS,WAAW,OAAO;AAEjC,YAAI,aAAa;AACf,kBAAQ,IAAI,mCAAmC;AAC/C,UAAAA,SAAQ,YAAY;AAAA,QACtB,WAAW,QAAQ;AACjB,kBAAQ,IAAI,oCAAoC,QAAQ,YAAY,CAAC,CAAC,GAAG;AACzE,UAAAA,SAAQ,YAAY;AACpB,cAAI,CAAC,aAAa,SAAS,QAAQ,EAAE,GAAG;AACtC,yBAAa,SAAS,QAAQ,EAAE,IAAI,CAAC;AAAA,UACvC;AACA,uBAAa,SAAS,QAAQ,EAAE,EAAG,SAAS;AAAA,QAC9C,OAAO;AACL,gBAAM,OAAO,cAAc,QAAQ,EAAE;AACrC,cAAI,KAAM,SAAQ,IAAI,KAAK,IAAI,EAAE;AACjC,gBAAM,YAAY,MAAM;AAAA,YACtB;AAAA,YACA;AAAA,UACF;AACA,cAAI,WAAW;AACb,YAAAA,SAAQ,YAAY;AACpB,gBAAI,CAAC,aAAa,SAAS,QAAQ,EAAE,GAAG;AACtC,2BAAa,SAAS,QAAQ,EAAE,IAAI,CAAC;AAAA,YACvC;AACA,yBAAa,SAAS,QAAQ,EAAE,EAAG,SAAS;AAAA,UAC9C;AAAA,QACF;AAGA,YAAI,QAAQ,kBAAkB,QAAQ,OAAO,cAAcA,SAAQ,WAAW;AAC5E,gBAAM,MAAM,aAAa,SAAS,QAAQ,EAAE,GAAG;AAC/C,cAAI,KAAK;AACP,oBAAQ,IAAI,8BAA8B;AAC1C,kBAAM,WAAW,MAAM,uBAAuB,GAAG;AACjD,gBAAI,UAAU;AACZ,sBAAQ,IAAI,uBAAuB,QAAQ,EAAE;AAC7C,cAAAA,SAAQ,WAAW;AAAA,YACrB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAGA,YAAM,WAAW,OAAO,eAAe;AAEvC,UAAI,OAAO,SAAS,WAAW,aAAa,GAAG;AAE7C,cAAM,eAAe,MAAM;AAAA,UACzB;AAAA,UACA;AAAA,QACF;AACA,cAAM,SAAS,WAAW,YAAY;AACtC,QAAAA,SAAQ,SAAS,CAAC,MAAM,MAAM,IAAI,SAAS;AAAA,MAC7C,OAAO;AAEL,cAAM,eAAe,MAAM;AAAA,UACzB;AAAA,UACA,sBAAsB,QAAQ;AAAA,QAChC;AACA,YAAI,cAAc;AAChB,gBAAM,SAAS,WAAW,YAAY;AACtC,UAAAA,SAAQ,SAAS,CAAC,MAAM,MAAM,IAAI,SAAS;AAAA,QAC7C,OAAO;AACL,UAAAA,SAAQ,SAAS;AAAA,QACnB;AAAA,MACF;AAEA,eAAS,QAAQ,EAAE,IAAIA;AAEvB,YAAM,YAAYA,SAAQ,YACtB,SACAA,SAAQ,aAAa,SACnB,SACA;AACN,cAAQ;AAAA,QACN,QAAQ,QAAQ,IAAI,KAAKA,SAAQ,QAAQ,MAAM,SAAS,OAAOA,SAAQ,MAAM;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAAU,OAAO,OAAO,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ;AACjE,QAAM,WAAW,OAAO,OAAO,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ;AACjE,QAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE;AACrD,QAAM,cAAc,QAAQ,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,UAAU,IAAI,CAAC;AAEvE,UAAQ,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;AACnC,UAAQ,IAAI,KAAK,QAAQ,MAAM,sBAAsB;AACrD,MAAI,YAAY,EAAG,SAAQ,IAAI,OAAO,SAAS,gCAAgC;AAC/E,MAAI,QAAQ,SAAS,YAAY,EAAG,SAAQ,IAAI,OAAO,QAAQ,SAAS,SAAS,uBAAuB;AACxG,MAAI,SAAS,SAAS,EAAG,SAAQ,IAAI,OAAO,SAAS,MAAM,WAAW;AACtE,UAAQ,IAAI,4BAA4B,WAAW,EAAE;AACrD,UAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;AAGjC,oBAAkB,YAAY;AAE9B,KAAG,MAAM;AAET,SAAO,EAAE,SAAS;AACpB;","names":["tracked"]}
|
package/package.json
CHANGED