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.
- package/CHANGELOG.md +44 -0
- package/dist/cli.js +383 -72
- package/dist/cli.js.map +1 -1
- package/dist/cost-impact.d.ts +1 -1
- package/dist/{detector-C6owsMy7.d.ts → detector-CSgHJEdg.d.ts} +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +43 -7
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +26 -3
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +32 -3
- package/dist/index.js.map +1 -1
- package/dist/interactive-init.d.ts +3 -3
- package/dist/interactive-init.js +340 -60
- package/dist/interactive-init.js.map +1 -1
- package/dist/mcp-server.js +35 -3
- package/dist/mcp-server.js.map +1 -1
- package/dist/{types-CO6v71UM.d.ts → types-BwIeWOYc.d.ts} +22 -0
- package/package.json +1 -1
- package/registry.json +4 -5
package/dist/interactive-init.js
CHANGED
|
@@ -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} |
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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:
|
|
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 (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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 =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
}
|