burnwatch 0.5.2 → 0.7.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.
@@ -60,7 +60,239 @@ async function fetchJson(url, options = {}) {
60
60
  }
61
61
  }
62
62
 
63
+ // src/probes.ts
64
+ function matchPlanByPrefix(detected, plans) {
65
+ const lower = detected.toLowerCase();
66
+ return plans.find((p) => {
67
+ if (p.type === "exclude") return false;
68
+ const firstWord = p.name.split(/[\s(]/)[0].toLowerCase();
69
+ return lower.includes(firstWord) || firstWord.includes(lower);
70
+ });
71
+ }
72
+ var probeScrapfly = async (apiKey, plans) => {
73
+ const result = await fetchJson(`https://api.scrapfly.io/account?key=${apiKey}`);
74
+ if (!result.ok || !result.data) return null;
75
+ const planName = result.data.subscription?.plan?.name;
76
+ let unitsUsed = 0;
77
+ let unitsTotal = 0;
78
+ if (result.data.subscription?.usage?.scrape) {
79
+ unitsUsed = result.data.subscription.usage.scrape.used ?? 0;
80
+ unitsTotal = result.data.subscription.usage.scrape.allowed ?? 0;
81
+ } else if (result.data.account) {
82
+ unitsUsed = result.data.account.credits_used ?? 0;
83
+ unitsTotal = result.data.account.credits_total ?? 0;
84
+ }
85
+ const matched = planName ? matchPlanByPrefix(planName, plans) : void 0;
86
+ return {
87
+ planName: planName ?? void 0,
88
+ matchedPlan: matched,
89
+ usage: {
90
+ unitsUsed,
91
+ unitsTotal,
92
+ unitName: "credits"
93
+ },
94
+ summary: matched ? `${matched.name} \u2014 ${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used` : `${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used`,
95
+ confidence: matched ? "high" : "medium"
96
+ };
97
+ };
98
+ var probeAnthropic = async (apiKey, _plans) => {
99
+ const now = /* @__PURE__ */ new Date();
100
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
101
+ const params = new URLSearchParams({
102
+ start_date: startOfMonth.toISOString().split("T")[0],
103
+ end_date: now.toISOString().split("T")[0]
104
+ });
105
+ const result = await fetchJson(`https://api.anthropic.com/v1/organizations/cost_report?${params}`, {
106
+ headers: {
107
+ "x-api-key": apiKey,
108
+ "anthropic-version": "2023-06-01"
109
+ }
110
+ });
111
+ if (!result.ok || !result.data?.data) return null;
112
+ let totalCents = 0;
113
+ for (const entry of result.data.data) {
114
+ totalCents += parseFloat(entry.amount ?? "0");
115
+ }
116
+ const spend = totalCents / 100;
117
+ return {
118
+ usage: { spend, currency: "USD" },
119
+ summary: `$${spend.toFixed(2)} spent this billing period`,
120
+ confidence: "medium"
121
+ };
122
+ };
123
+ var probeOpenAI = async (apiKey, _plans) => {
124
+ const now = /* @__PURE__ */ new Date();
125
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
126
+ const params = new URLSearchParams({
127
+ start_time: String(Math.floor(startOfMonth.getTime() / 1e3)),
128
+ end_time: String(Math.floor(now.getTime() / 1e3))
129
+ });
130
+ const result = await fetchJson(`https://api.openai.com/v1/organization/usage/completions?${params}`, {
131
+ headers: { Authorization: `Bearer ${apiKey}` }
132
+ });
133
+ if (!result.ok || !result.data?.data) return null;
134
+ let totalTokens = 0;
135
+ for (const bucket of result.data.data) {
136
+ for (const r of bucket.results ?? []) {
137
+ totalTokens += r.amount?.value ?? 0;
138
+ }
139
+ }
140
+ return {
141
+ usage: { unitsUsed: totalTokens, unitName: "tokens" },
142
+ summary: `${formatK(totalTokens)} tokens used this period`,
143
+ confidence: "medium"
144
+ };
145
+ };
146
+ var probeVercel = async (apiKey, plans) => {
147
+ const teamsResult = await fetchJson("https://api.vercel.com/v2/teams", {
148
+ headers: { Authorization: `Bearer ${apiKey}` }
149
+ });
150
+ if (teamsResult.ok && teamsResult.data?.teams?.[0]) {
151
+ const team = teamsResult.data.teams[0];
152
+ const planName = team.billing?.plan;
153
+ if (planName) {
154
+ const matched = matchPlanByPrefix(planName, plans);
155
+ return {
156
+ planName,
157
+ matchedPlan: matched,
158
+ summary: `Team "${team.name}" on ${planName} plan`,
159
+ confidence: matched ? "high" : "medium"
160
+ };
161
+ }
162
+ }
163
+ const userResult = await fetchJson("https://api.vercel.com/v2/user", {
164
+ headers: { Authorization: `Bearer ${apiKey}` }
165
+ });
166
+ if (userResult.ok && userResult.data?.user) {
167
+ const plan = userResult.data.user.billing?.plan ?? "hobby";
168
+ const matched = matchPlanByPrefix(plan, plans);
169
+ return {
170
+ planName: plan,
171
+ matchedPlan: matched,
172
+ summary: `Personal account on ${plan} plan`,
173
+ confidence: matched ? "high" : "low"
174
+ };
175
+ }
176
+ return null;
177
+ };
178
+ var probeSupabase = async (apiKey, plans) => {
179
+ const orgsResult = await fetchJson("https://api.supabase.com/v1/organizations", {
180
+ headers: { Authorization: `Bearer ${apiKey}` }
181
+ });
182
+ if (!orgsResult.ok || !orgsResult.data || !Array.isArray(orgsResult.data)) return null;
183
+ const org = orgsResult.data[0];
184
+ if (!org) return null;
185
+ const planName = org.billing?.plan;
186
+ if (planName) {
187
+ const matched = matchPlanByPrefix(planName, plans);
188
+ return {
189
+ planName,
190
+ matchedPlan: matched,
191
+ summary: `Org "${org.name}" on ${planName} plan`,
192
+ confidence: matched ? "high" : "medium"
193
+ };
194
+ }
195
+ return {
196
+ summary: `Org "${org.name}" found (plan not detected)`,
197
+ confidence: "low"
198
+ };
199
+ };
200
+ var probeStripe = async (apiKey, _plans) => {
201
+ const result = await fetchJson("https://api.stripe.com/v1/balance", {
202
+ headers: { Authorization: `Bearer ${apiKey}` }
203
+ });
204
+ if (!result.ok || !result.data) return null;
205
+ const available = result.data.available?.[0];
206
+ const pending = result.data.pending?.[0];
207
+ const totalCents = (available?.amount ?? 0) + (pending?.amount ?? 0);
208
+ const currency = (available?.currency ?? "usd").toUpperCase();
209
+ return {
210
+ usage: { spend: totalCents / 100, currency },
211
+ summary: `Balance: ${currency} ${(totalCents / 100).toFixed(2)} (${((available?.amount ?? 0) / 100).toFixed(2)} available)`,
212
+ confidence: "medium"
213
+ };
214
+ };
215
+ var probeBrowserbase = async (apiKey, _plans) => {
216
+ const projResult = await fetchJson("https://api.browserbase.com/v1/projects", {
217
+ headers: { "X-BB-API-Key": apiKey }
218
+ });
219
+ if (!projResult.ok || !projResult.data?.[0]?.id) return null;
220
+ const projectId = projResult.data[0].id;
221
+ const usageResult = await fetchJson(`https://api.browserbase.com/v1/projects/${projectId}/usage`, {
222
+ headers: { "X-BB-API-Key": apiKey }
223
+ });
224
+ if (!usageResult.ok || !usageResult.data) {
225
+ return {
226
+ summary: `Project "${projResult.data[0].name}" found`,
227
+ confidence: "low"
228
+ };
229
+ }
230
+ const sessions = usageResult.data.sessions_count ?? 0;
231
+ const hours = usageResult.data.browser_hours ?? 0;
232
+ return {
233
+ usage: { unitsUsed: sessions, unitName: "sessions" },
234
+ summary: `${sessions} sessions, ${hours.toFixed(1)} browser hours this period`,
235
+ confidence: "medium"
236
+ };
237
+ };
238
+ var probeUpstash = async (apiKey, _plans) => {
239
+ const result = await fetchJson("https://api.upstash.com/v2/redis/databases", {
240
+ headers: {
241
+ Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`
242
+ }
243
+ });
244
+ if (!result.ok) return null;
245
+ const dbCount = Array.isArray(result.data) ? result.data.length : 0;
246
+ return {
247
+ summary: `${dbCount} Redis database${dbCount !== 1 ? "s" : ""} found`,
248
+ confidence: "low"
249
+ };
250
+ };
251
+ var probePostHog = async (apiKey, _plans) => {
252
+ const result = await fetchJson("https://us.posthog.com/api/organizations/@current", {
253
+ headers: { Authorization: `Bearer ${apiKey}` }
254
+ });
255
+ if (!result.ok || !result.data) return null;
256
+ return {
257
+ summary: "Organization found",
258
+ confidence: "low"
259
+ };
260
+ };
261
+ var PROBES = /* @__PURE__ */ new Map([
262
+ ["scrapfly", probeScrapfly],
263
+ ["anthropic", probeAnthropic],
264
+ ["openai", probeOpenAI],
265
+ ["vercel", probeVercel],
266
+ ["supabase", probeSupabase],
267
+ ["stripe", probeStripe],
268
+ ["browserbase", probeBrowserbase],
269
+ ["upstash", probeUpstash],
270
+ ["posthog", probePostHog]
271
+ ]);
272
+ async function probeService(serviceId, apiKey, plans) {
273
+ const probe = PROBES.get(serviceId);
274
+ if (!probe) return null;
275
+ try {
276
+ return await probe(apiKey, plans);
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+ function hasProbe(serviceId) {
282
+ return PROBES.has(serviceId);
283
+ }
284
+ function formatK(n) {
285
+ if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
286
+ if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
287
+ return String(n);
288
+ }
289
+
63
290
  // src/interactive-init.ts
291
+ function formatUnits(n) {
292
+ if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
293
+ if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
294
+ return String(n);
295
+ }
64
296
  var RISK_ORDER = ["llm", "usage", "infra", "flat"];
65
297
  var RISK_LABELS = {
66
298
  llm: "LLM / AI Services (highest variable cost)",
@@ -101,16 +333,6 @@ function ask(rl, question) {
101
333
  });
102
334
  });
103
335
  }
104
- async function autoDetectScrapflyPlan(apiKey) {
105
- try {
106
- const result = await fetchJson(`https://api.scrapfly.io/account?key=${apiKey}`);
107
- if (result.ok && result.data?.subscription?.plan?.name) {
108
- return result.data.subscription.plan.name;
109
- }
110
- } catch {
111
- }
112
- return null;
113
- }
114
336
  function findEnvKey(service) {
115
337
  for (const pattern of service.envPatterns) {
116
338
  const val = process.env[pattern];
@@ -118,7 +340,7 @@ function findEnvKey(service) {
118
340
  }
119
341
  return void 0;
120
342
  }
121
- function autoConfigureServices(detected) {
343
+ async function autoConfigureServices(detected) {
122
344
  const services = {};
123
345
  const groups = groupByRisk(detected);
124
346
  const globalConfig = readGlobalConfig();
@@ -151,25 +373,54 @@ function autoConfigureServices(detected) {
151
373
  } else if (defaultPlan.suggestedBudget !== void 0) {
152
374
  tracked.budget = defaultPlan.suggestedBudget;
153
375
  }
376
+ if (defaultPlan.includedUnits !== void 0 && defaultPlan.unitName) {
377
+ tracked.allowance = {
378
+ included: defaultPlan.includedUnits,
379
+ unitName: defaultPlan.unitName
380
+ };
381
+ }
154
382
  }
155
383
  const existingKey = globalConfig.services[service.id]?.apiKey;
156
384
  const envKey = findEnvKey(service);
157
385
  let keySource = "";
386
+ let apiKey;
158
387
  if (existingKey) {
159
388
  tracked.hasApiKey = true;
389
+ apiKey = existingKey;
160
390
  keySource = " (key: global config)";
161
391
  } else if (envKey) {
162
392
  tracked.hasApiKey = true;
393
+ apiKey = envKey;
163
394
  if (!globalConfig.services[service.id]) {
164
395
  globalConfig.services[service.id] = {};
165
396
  }
166
397
  globalConfig.services[service.id].apiKey = envKey;
167
398
  keySource = ` (key: ${service.envPatterns[0]})`;
168
399
  }
400
+ if (apiKey && hasProbe(service.id)) {
401
+ try {
402
+ const probe = await probeService(service.id, apiKey, plans);
403
+ if (probe?.matchedPlan && probe.confidence === "high") {
404
+ const mp = probe.matchedPlan;
405
+ tracked.planName = mp.name;
406
+ if (mp.type === "flat" && mp.monthlyBase !== void 0) {
407
+ tracked.planCost = mp.monthlyBase;
408
+ tracked.budget = mp.monthlyBase;
409
+ } else if (mp.suggestedBudget !== void 0) {
410
+ tracked.budget = mp.suggestedBudget;
411
+ }
412
+ if (mp.includedUnits !== void 0 && mp.unitName) {
413
+ tracked.allowance = { included: mp.includedUnits, unitName: mp.unitName };
414
+ }
415
+ }
416
+ } catch {
417
+ }
418
+ }
169
419
  const tierLabel = tracked.hasApiKey ? "LIVE" : tracked.planCost !== void 0 ? "CALC" : "BLIND";
170
420
  const planStr = tracked.planName ? ` ${tracked.planName}` : "";
421
+ const trackingStr = tracked.allowance ? `$${tracked.budget}/mo | ${formatUnits(tracked.allowance.included)} ${tracked.allowance.unitName}` : `$${tracked.budget}/mo`;
171
422
  console.log(
172
- ` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`
423
+ ` ${service.name}:${planStr} | ${tierLabel} | ${trackingStr}${keySource}`
173
424
  );
174
425
  services[service.id] = tracked;
175
426
  }
@@ -221,21 +472,61 @@ async function runInteractiveInit(detected) {
221
472
  console.log(" -> Configured (no plan tiers in registry, budget: $0)");
222
473
  continue;
223
474
  }
224
- const defaultIndex = plans.findIndex((p) => p.default);
225
- console.log("");
226
- for (let i = 0; i < plans.length; i++) {
227
- const plan = plans[i];
228
- const marker = i === defaultIndex ? " *" : "";
229
- const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? ` - $${plan.monthlyBase}/mo` : " - variable";
230
- console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);
475
+ let apiKey;
476
+ const existingKey = globalConfig.services[service.id]?.apiKey;
477
+ const envKey = findEnvKey(service);
478
+ if (existingKey) {
479
+ apiKey = existingKey;
480
+ console.log(` API key: found in global config`);
481
+ } else if (envKey) {
482
+ apiKey = envKey;
483
+ console.log(` API key: found in environment (${service.envPatterns[0]})`);
484
+ if (!globalConfig.services[service.id]) {
485
+ globalConfig.services[service.id] = {};
486
+ }
487
+ globalConfig.services[service.id].apiKey = envKey;
488
+ }
489
+ let chosen;
490
+ if (apiKey && hasProbe(service.id)) {
491
+ console.log(" Probing API...");
492
+ const probe = await probeService(service.id, apiKey, plans);
493
+ if (probe) {
494
+ console.log(` ${probe.summary}`);
495
+ if (probe.confidence === "high" && probe.matchedPlan) {
496
+ const plan = probe.matchedPlan;
497
+ const costStr = plan.monthlyBase !== void 0 ? `$${plan.monthlyBase}/mo` : "variable";
498
+ const unitsStr = plan.includedUnits && plan.unitName ? `, ${formatUnits(plan.includedUnits)} ${plan.unitName}` : "";
499
+ const confirm = await ask(
500
+ rl,
501
+ ` Detected: ${plan.name} (${costStr}${unitsStr}). Correct? [Y/n]: `
502
+ );
503
+ if (confirm === "" || confirm.toLowerCase().startsWith("y")) {
504
+ chosen = plan;
505
+ }
506
+ } else if (probe.confidence === "medium") {
507
+ if (probe.usage?.spend !== void 0) {
508
+ console.log(` Current spend: $${probe.usage.spend.toFixed(2)}`);
509
+ }
510
+ }
511
+ }
512
+ }
513
+ if (!chosen) {
514
+ const defaultIndex = plans.findIndex((p) => p.default);
515
+ console.log("");
516
+ for (let i = 0; i < plans.length; i++) {
517
+ const plan = plans[i];
518
+ const marker = i === defaultIndex ? " *" : "";
519
+ const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? ` - $${plan.monthlyBase}/mo` : " - variable";
520
+ console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);
521
+ }
522
+ const defaultChoice = defaultIndex >= 0 ? String(defaultIndex + 1) : "1";
523
+ const answer = await ask(
524
+ rl,
525
+ ` Which plan? [${defaultChoice}]: `
526
+ );
527
+ const choiceIndex = (answer === "" ? parseInt(defaultChoice) : parseInt(answer)) - 1;
528
+ chosen = plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0];
231
529
  }
232
- const defaultChoice = defaultIndex >= 0 ? String(defaultIndex + 1) : "1";
233
- const answer = await ask(
234
- rl,
235
- ` Which plan? [${defaultChoice}]: `
236
- );
237
- const choiceIndex = (answer === "" ? parseInt(defaultChoice) : parseInt(answer)) - 1;
238
- const chosen = plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0];
239
530
  if (chosen.type === "exclude") {
240
531
  services[service.id] = {
241
532
  serviceId: service.id,
@@ -251,49 +542,37 @@ async function runInteractiveInit(detected) {
251
542
  const tracked2 = {
252
543
  serviceId: service.id,
253
544
  detectedVia: det.sources,
254
- hasApiKey: false,
545
+ hasApiKey: !!apiKey,
255
546
  firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
256
547
  planName: chosen.name
257
548
  };
258
549
  if (chosen.type === "flat" && chosen.monthlyBase !== void 0) {
259
550
  tracked2.planCost = chosen.monthlyBase;
260
551
  }
261
- if (service.apiTier === "live") {
262
- const existingKey = globalConfig.services[service.id]?.apiKey;
263
- const envKey = findEnvKey(service);
264
- if (existingKey) {
265
- console.log(` API key: found in global config`);
266
- tracked2.hasApiKey = true;
267
- } else if (envKey) {
268
- console.log(` API key: found in environment (${service.envPatterns[0]})`);
552
+ if (chosen.includedUnits !== void 0 && chosen.unitName) {
553
+ tracked2.allowance = {
554
+ included: chosen.includedUnits,
555
+ unitName: chosen.unitName
556
+ };
557
+ }
558
+ if (!apiKey && hasProbe(service.id)) {
559
+ const hint = API_KEY_HINTS[service.id];
560
+ if (hint) console.log(` ${hint}`);
561
+ const keyAnswer = await ask(
562
+ rl,
563
+ ` API key for real-time tracking (Enter to skip): `
564
+ );
565
+ if (keyAnswer) {
269
566
  tracked2.hasApiKey = true;
567
+ apiKey = keyAnswer;
270
568
  if (!globalConfig.services[service.id]) {
271
569
  globalConfig.services[service.id] = {};
272
570
  }
273
- globalConfig.services[service.id].apiKey = envKey;
274
- } else {
275
- const hint = API_KEY_HINTS[service.id];
276
- if (hint) console.log(` ${hint}`);
277
- const keyAnswer = await ask(
278
- rl,
279
- ` API key for real-time tracking (Enter to skip): `
280
- );
281
- if (keyAnswer) {
282
- tracked2.hasApiKey = true;
283
- if (!globalConfig.services[service.id]) {
284
- globalConfig.services[service.id] = {};
285
- }
286
- globalConfig.services[service.id].apiKey = keyAnswer;
287
- }
288
- }
289
- if (service.autoDetectPlan && service.id === "scrapfly" && tracked2.hasApiKey) {
290
- const key = globalConfig.services[service.id]?.apiKey;
291
- if (key) {
292
- console.log(" Detecting plan from API...");
293
- const planName = await autoDetectScrapflyPlan(key);
294
- if (planName) {
295
- console.log(` -> Detected plan: ${planName}`);
296
- tracked2.planName = planName;
571
+ globalConfig.services[service.id].apiKey = keyAnswer;
572
+ if (hasProbe(service.id)) {
573
+ const probe = await probeService(service.id, keyAnswer, plans);
574
+ if (probe?.usage) {
575
+ console.log(` ${probe.summary}`);
297
576
  }
298
577
  }
299
578
  }
@@ -311,8 +590,9 @@ async function runInteractiveInit(detected) {
311
590
  }
312
591
  services[service.id] = tracked2;
313
592
  const tierLabel = tracked2.hasApiKey ? "LIVE" : tracked2.planCost !== void 0 ? "CALC" : "BLIND";
593
+ const allowanceStr = tracked2.allowance ? ` | ${formatUnits(tracked2.allowance.included)} ${tracked2.allowance.unitName}` : "";
314
594
  console.log(
315
- ` -> ${service.name}: ${tracked2.planName} | ${tierLabel} | $${tracked2.budget}/mo`
595
+ ` -> ${service.name}: ${tracked2.planName} | ${tierLabel} | $${tracked2.budget}/mo${allowanceStr}`
316
596
  );
317
597
  }
318
598
  }