burnwatch 0.7.0 → 0.8.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 +13 -0
- package/dist/cli.js +555 -266
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/burnwatch-interview/SKILL.md +153 -0
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,290 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/services/base.ts
|
|
13
|
+
async function fetchJson(url2, options = {}) {
|
|
14
|
+
try {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeoutId = setTimeout(
|
|
17
|
+
() => controller.abort(),
|
|
18
|
+
options.timeout ?? 1e4
|
|
19
|
+
);
|
|
20
|
+
const response = await fetch(url2, {
|
|
21
|
+
method: options.method ?? "GET",
|
|
22
|
+
headers: options.headers,
|
|
23
|
+
body: options.body,
|
|
24
|
+
signal: controller.signal
|
|
25
|
+
});
|
|
26
|
+
clearTimeout(timeoutId);
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
status: response.status,
|
|
31
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
return { ok: true, status: response.status, data };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 0,
|
|
40
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
var init_base = __esm({
|
|
45
|
+
"src/services/base.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/probes.ts
|
|
51
|
+
var probes_exports = {};
|
|
52
|
+
__export(probes_exports, {
|
|
53
|
+
hasProbe: () => hasProbe,
|
|
54
|
+
probeService: () => probeService
|
|
55
|
+
});
|
|
56
|
+
function matchPlanByPrefix(detected, plans) {
|
|
57
|
+
const lower = detected.toLowerCase();
|
|
58
|
+
return plans.find((p) => {
|
|
59
|
+
if (p.type === "exclude") return false;
|
|
60
|
+
const firstWord = p.name.split(/[\s(]/)[0].toLowerCase();
|
|
61
|
+
return lower.includes(firstWord) || firstWord.includes(lower);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function probeService(serviceId, apiKey, plans) {
|
|
65
|
+
const probe = PROBES.get(serviceId);
|
|
66
|
+
if (!probe) return null;
|
|
67
|
+
try {
|
|
68
|
+
return await probe(apiKey, plans);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function hasProbe(serviceId) {
|
|
74
|
+
return PROBES.has(serviceId);
|
|
75
|
+
}
|
|
76
|
+
function formatK(n) {
|
|
77
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
78
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
|
|
79
|
+
return String(n);
|
|
80
|
+
}
|
|
81
|
+
var probeScrapfly, probeAnthropic, probeOpenAI, probeVercel, probeSupabase, probeStripe, probeBrowserbase, probeUpstash, probePostHog, PROBES;
|
|
82
|
+
var init_probes = __esm({
|
|
83
|
+
"src/probes.ts"() {
|
|
84
|
+
"use strict";
|
|
85
|
+
init_base();
|
|
86
|
+
probeScrapfly = async (apiKey, plans) => {
|
|
87
|
+
const result = await fetchJson(`https://api.scrapfly.io/account?key=${apiKey}`);
|
|
88
|
+
if (!result.ok || !result.data) return null;
|
|
89
|
+
const planName = result.data.subscription?.plan?.name;
|
|
90
|
+
let unitsUsed = 0;
|
|
91
|
+
let unitsTotal = 0;
|
|
92
|
+
if (result.data.subscription?.usage?.scrape) {
|
|
93
|
+
unitsUsed = result.data.subscription.usage.scrape.used ?? 0;
|
|
94
|
+
unitsTotal = result.data.subscription.usage.scrape.allowed ?? 0;
|
|
95
|
+
} else if (result.data.account) {
|
|
96
|
+
unitsUsed = result.data.account.credits_used ?? 0;
|
|
97
|
+
unitsTotal = result.data.account.credits_total ?? 0;
|
|
98
|
+
}
|
|
99
|
+
const matched = planName ? matchPlanByPrefix(planName, plans) : void 0;
|
|
100
|
+
return {
|
|
101
|
+
planName: planName ?? void 0,
|
|
102
|
+
matchedPlan: matched,
|
|
103
|
+
usage: {
|
|
104
|
+
unitsUsed,
|
|
105
|
+
unitsTotal,
|
|
106
|
+
unitName: "credits"
|
|
107
|
+
},
|
|
108
|
+
summary: matched ? `${matched.name} \u2014 ${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used` : `${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used`,
|
|
109
|
+
confidence: matched ? "high" : "medium"
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
probeAnthropic = async (apiKey, _plans) => {
|
|
113
|
+
const now = /* @__PURE__ */ new Date();
|
|
114
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
115
|
+
const params = new URLSearchParams({
|
|
116
|
+
start_date: startOfMonth.toISOString().split("T")[0],
|
|
117
|
+
end_date: now.toISOString().split("T")[0]
|
|
118
|
+
});
|
|
119
|
+
const result = await fetchJson(`https://api.anthropic.com/v1/organizations/cost_report?${params}`, {
|
|
120
|
+
headers: {
|
|
121
|
+
"x-api-key": apiKey,
|
|
122
|
+
"anthropic-version": "2023-06-01"
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (!result.ok || !result.data?.data) return null;
|
|
126
|
+
let totalCents = 0;
|
|
127
|
+
for (const entry of result.data.data) {
|
|
128
|
+
totalCents += parseFloat(entry.amount ?? "0");
|
|
129
|
+
}
|
|
130
|
+
const spend = totalCents / 100;
|
|
131
|
+
return {
|
|
132
|
+
usage: { spend, currency: "USD" },
|
|
133
|
+
summary: `$${spend.toFixed(2)} spent this billing period`,
|
|
134
|
+
confidence: "medium"
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
probeOpenAI = async (apiKey, _plans) => {
|
|
138
|
+
const now = /* @__PURE__ */ new Date();
|
|
139
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
140
|
+
const params = new URLSearchParams({
|
|
141
|
+
start_time: String(Math.floor(startOfMonth.getTime() / 1e3)),
|
|
142
|
+
end_time: String(Math.floor(now.getTime() / 1e3))
|
|
143
|
+
});
|
|
144
|
+
const result = await fetchJson(`https://api.openai.com/v1/organization/usage/completions?${params}`, {
|
|
145
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
146
|
+
});
|
|
147
|
+
if (!result.ok || !result.data?.data) return null;
|
|
148
|
+
let totalTokens = 0;
|
|
149
|
+
for (const bucket of result.data.data) {
|
|
150
|
+
for (const r of bucket.results ?? []) {
|
|
151
|
+
totalTokens += r.amount?.value ?? 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
usage: { unitsUsed: totalTokens, unitName: "tokens" },
|
|
156
|
+
summary: `${formatK(totalTokens)} tokens used this period`,
|
|
157
|
+
confidence: "medium"
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
probeVercel = async (apiKey, plans) => {
|
|
161
|
+
const teamsResult = await fetchJson("https://api.vercel.com/v2/teams", {
|
|
162
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
163
|
+
});
|
|
164
|
+
if (teamsResult.ok && teamsResult.data?.teams?.[0]) {
|
|
165
|
+
const team = teamsResult.data.teams[0];
|
|
166
|
+
const planName = team.billing?.plan;
|
|
167
|
+
if (planName) {
|
|
168
|
+
const matched = matchPlanByPrefix(planName, plans);
|
|
169
|
+
return {
|
|
170
|
+
planName,
|
|
171
|
+
matchedPlan: matched,
|
|
172
|
+
summary: `Team "${team.name}" on ${planName} plan`,
|
|
173
|
+
confidence: matched ? "high" : "medium"
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const userResult = await fetchJson("https://api.vercel.com/v2/user", {
|
|
178
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
179
|
+
});
|
|
180
|
+
if (userResult.ok && userResult.data?.user) {
|
|
181
|
+
const plan = userResult.data.user.billing?.plan ?? "hobby";
|
|
182
|
+
const matched = matchPlanByPrefix(plan, plans);
|
|
183
|
+
return {
|
|
184
|
+
planName: plan,
|
|
185
|
+
matchedPlan: matched,
|
|
186
|
+
summary: `Personal account on ${plan} plan`,
|
|
187
|
+
confidence: matched ? "high" : "low"
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
probeSupabase = async (apiKey, plans) => {
|
|
193
|
+
const orgsResult = await fetchJson("https://api.supabase.com/v1/organizations", {
|
|
194
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
195
|
+
});
|
|
196
|
+
if (!orgsResult.ok || !orgsResult.data || !Array.isArray(orgsResult.data)) return null;
|
|
197
|
+
const org = orgsResult.data[0];
|
|
198
|
+
if (!org) return null;
|
|
199
|
+
const planName = org.billing?.plan;
|
|
200
|
+
if (planName) {
|
|
201
|
+
const matched = matchPlanByPrefix(planName, plans);
|
|
202
|
+
return {
|
|
203
|
+
planName,
|
|
204
|
+
matchedPlan: matched,
|
|
205
|
+
summary: `Org "${org.name}" on ${planName} plan`,
|
|
206
|
+
confidence: matched ? "high" : "medium"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
summary: `Org "${org.name}" found (plan not detected)`,
|
|
211
|
+
confidence: "low"
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
probeStripe = async (apiKey, _plans) => {
|
|
215
|
+
const result = await fetchJson("https://api.stripe.com/v1/balance", {
|
|
216
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
217
|
+
});
|
|
218
|
+
if (!result.ok || !result.data) return null;
|
|
219
|
+
const available = result.data.available?.[0];
|
|
220
|
+
const pending = result.data.pending?.[0];
|
|
221
|
+
const totalCents = (available?.amount ?? 0) + (pending?.amount ?? 0);
|
|
222
|
+
const currency = (available?.currency ?? "usd").toUpperCase();
|
|
223
|
+
return {
|
|
224
|
+
usage: { spend: totalCents / 100, currency },
|
|
225
|
+
summary: `Balance: ${currency} ${(totalCents / 100).toFixed(2)} (${((available?.amount ?? 0) / 100).toFixed(2)} available)`,
|
|
226
|
+
confidence: "medium"
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
probeBrowserbase = async (apiKey, _plans) => {
|
|
230
|
+
const projResult = await fetchJson("https://api.browserbase.com/v1/projects", {
|
|
231
|
+
headers: { "X-BB-API-Key": apiKey }
|
|
232
|
+
});
|
|
233
|
+
if (!projResult.ok || !projResult.data?.[0]?.id) return null;
|
|
234
|
+
const projectId = projResult.data[0].id;
|
|
235
|
+
const usageResult = await fetchJson(`https://api.browserbase.com/v1/projects/${projectId}/usage`, {
|
|
236
|
+
headers: { "X-BB-API-Key": apiKey }
|
|
237
|
+
});
|
|
238
|
+
if (!usageResult.ok || !usageResult.data) {
|
|
239
|
+
return {
|
|
240
|
+
summary: `Project "${projResult.data[0].name}" found`,
|
|
241
|
+
confidence: "low"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const sessions = usageResult.data.sessions_count ?? 0;
|
|
245
|
+
const hours = usageResult.data.browser_hours ?? 0;
|
|
246
|
+
return {
|
|
247
|
+
usage: { unitsUsed: sessions, unitName: "sessions" },
|
|
248
|
+
summary: `${sessions} sessions, ${hours.toFixed(1)} browser hours this period`,
|
|
249
|
+
confidence: "medium"
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
probeUpstash = async (apiKey, _plans) => {
|
|
253
|
+
const result = await fetchJson("https://api.upstash.com/v2/redis/databases", {
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
if (!result.ok) return null;
|
|
259
|
+
const dbCount = Array.isArray(result.data) ? result.data.length : 0;
|
|
260
|
+
return {
|
|
261
|
+
summary: `${dbCount} Redis database${dbCount !== 1 ? "s" : ""} found`,
|
|
262
|
+
confidence: "low"
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
probePostHog = async (apiKey, _plans) => {
|
|
266
|
+
const result = await fetchJson("https://us.posthog.com/api/organizations/@current", {
|
|
267
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
268
|
+
});
|
|
269
|
+
if (!result.ok || !result.data) return null;
|
|
270
|
+
return {
|
|
271
|
+
summary: "Organization found",
|
|
272
|
+
confidence: "low"
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
PROBES = /* @__PURE__ */ new Map([
|
|
276
|
+
["scrapfly", probeScrapfly],
|
|
277
|
+
["anthropic", probeAnthropic],
|
|
278
|
+
["openai", probeOpenAI],
|
|
279
|
+
["vercel", probeVercel],
|
|
280
|
+
["supabase", probeSupabase],
|
|
281
|
+
["stripe", probeStripe],
|
|
282
|
+
["browserbase", probeBrowserbase],
|
|
283
|
+
["upstash", probeUpstash],
|
|
284
|
+
["posthog", probePostHog]
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
2
288
|
|
|
3
289
|
// src/cli.ts
|
|
4
290
|
import * as fs5 from "fs";
|
|
@@ -307,40 +593,8 @@ function walkDir(dir, pattern, maxDepth = 5) {
|
|
|
307
593
|
return results;
|
|
308
594
|
}
|
|
309
595
|
|
|
310
|
-
// src/services/base.ts
|
|
311
|
-
async function fetchJson(url2, options = {}) {
|
|
312
|
-
try {
|
|
313
|
-
const controller = new AbortController();
|
|
314
|
-
const timeoutId = setTimeout(
|
|
315
|
-
() => controller.abort(),
|
|
316
|
-
options.timeout ?? 1e4
|
|
317
|
-
);
|
|
318
|
-
const response = await fetch(url2, {
|
|
319
|
-
method: options.method ?? "GET",
|
|
320
|
-
headers: options.headers,
|
|
321
|
-
body: options.body,
|
|
322
|
-
signal: controller.signal
|
|
323
|
-
});
|
|
324
|
-
clearTimeout(timeoutId);
|
|
325
|
-
if (!response.ok) {
|
|
326
|
-
return {
|
|
327
|
-
ok: false,
|
|
328
|
-
status: response.status,
|
|
329
|
-
error: `HTTP ${response.status}: ${response.statusText}`
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
const data = await response.json();
|
|
333
|
-
return { ok: true, status: response.status, data };
|
|
334
|
-
} catch (err) {
|
|
335
|
-
return {
|
|
336
|
-
ok: false,
|
|
337
|
-
status: 0,
|
|
338
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
596
|
// src/services/anthropic.ts
|
|
597
|
+
init_base();
|
|
344
598
|
var anthropicConnector = {
|
|
345
599
|
serviceId: "anthropic",
|
|
346
600
|
async fetchSpend(apiKey) {
|
|
@@ -384,6 +638,7 @@ var anthropicConnector = {
|
|
|
384
638
|
};
|
|
385
639
|
|
|
386
640
|
// src/services/openai.ts
|
|
641
|
+
init_base();
|
|
387
642
|
var openaiConnector = {
|
|
388
643
|
serviceId: "openai",
|
|
389
644
|
async fetchSpend(apiKey) {
|
|
@@ -427,6 +682,7 @@ var openaiConnector = {
|
|
|
427
682
|
};
|
|
428
683
|
|
|
429
684
|
// src/services/vercel.ts
|
|
685
|
+
init_base();
|
|
430
686
|
var vercelConnector = {
|
|
431
687
|
serviceId: "vercel",
|
|
432
688
|
async fetchSpend(token, options) {
|
|
@@ -467,6 +723,7 @@ var vercelConnector = {
|
|
|
467
723
|
};
|
|
468
724
|
|
|
469
725
|
// src/services/scrapfly.ts
|
|
726
|
+
init_base();
|
|
470
727
|
var scrapflyConnector = {
|
|
471
728
|
serviceId: "scrapfly",
|
|
472
729
|
async fetchSpend(apiKey) {
|
|
@@ -511,6 +768,7 @@ var scrapflyConnector = {
|
|
|
511
768
|
};
|
|
512
769
|
|
|
513
770
|
// src/services/index.ts
|
|
771
|
+
init_base();
|
|
514
772
|
var connectors = /* @__PURE__ */ new Map([
|
|
515
773
|
["anthropic", anthropicConnector],
|
|
516
774
|
["openai", openaiConnector],
|
|
@@ -819,235 +1077,7 @@ function saveSnapshot(brief, projectRoot) {
|
|
|
819
1077
|
|
|
820
1078
|
// src/interactive-init.ts
|
|
821
1079
|
import * as readline from "readline";
|
|
822
|
-
|
|
823
|
-
// src/probes.ts
|
|
824
|
-
function matchPlanByPrefix(detected, plans) {
|
|
825
|
-
const lower = detected.toLowerCase();
|
|
826
|
-
return plans.find((p) => {
|
|
827
|
-
if (p.type === "exclude") return false;
|
|
828
|
-
const firstWord = p.name.split(/[\s(]/)[0].toLowerCase();
|
|
829
|
-
return lower.includes(firstWord) || firstWord.includes(lower);
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
var probeScrapfly = async (apiKey, plans) => {
|
|
833
|
-
const result = await fetchJson(`https://api.scrapfly.io/account?key=${apiKey}`);
|
|
834
|
-
if (!result.ok || !result.data) return null;
|
|
835
|
-
const planName = result.data.subscription?.plan?.name;
|
|
836
|
-
let unitsUsed = 0;
|
|
837
|
-
let unitsTotal = 0;
|
|
838
|
-
if (result.data.subscription?.usage?.scrape) {
|
|
839
|
-
unitsUsed = result.data.subscription.usage.scrape.used ?? 0;
|
|
840
|
-
unitsTotal = result.data.subscription.usage.scrape.allowed ?? 0;
|
|
841
|
-
} else if (result.data.account) {
|
|
842
|
-
unitsUsed = result.data.account.credits_used ?? 0;
|
|
843
|
-
unitsTotal = result.data.account.credits_total ?? 0;
|
|
844
|
-
}
|
|
845
|
-
const matched = planName ? matchPlanByPrefix(planName, plans) : void 0;
|
|
846
|
-
return {
|
|
847
|
-
planName: planName ?? void 0,
|
|
848
|
-
matchedPlan: matched,
|
|
849
|
-
usage: {
|
|
850
|
-
unitsUsed,
|
|
851
|
-
unitsTotal,
|
|
852
|
-
unitName: "credits"
|
|
853
|
-
},
|
|
854
|
-
summary: matched ? `${matched.name} \u2014 ${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used` : `${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used`,
|
|
855
|
-
confidence: matched ? "high" : "medium"
|
|
856
|
-
};
|
|
857
|
-
};
|
|
858
|
-
var probeAnthropic = async (apiKey, _plans) => {
|
|
859
|
-
const now = /* @__PURE__ */ new Date();
|
|
860
|
-
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
861
|
-
const params = new URLSearchParams({
|
|
862
|
-
start_date: startOfMonth.toISOString().split("T")[0],
|
|
863
|
-
end_date: now.toISOString().split("T")[0]
|
|
864
|
-
});
|
|
865
|
-
const result = await fetchJson(`https://api.anthropic.com/v1/organizations/cost_report?${params}`, {
|
|
866
|
-
headers: {
|
|
867
|
-
"x-api-key": apiKey,
|
|
868
|
-
"anthropic-version": "2023-06-01"
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
if (!result.ok || !result.data?.data) return null;
|
|
872
|
-
let totalCents = 0;
|
|
873
|
-
for (const entry of result.data.data) {
|
|
874
|
-
totalCents += parseFloat(entry.amount ?? "0");
|
|
875
|
-
}
|
|
876
|
-
const spend = totalCents / 100;
|
|
877
|
-
return {
|
|
878
|
-
usage: { spend, currency: "USD" },
|
|
879
|
-
summary: `$${spend.toFixed(2)} spent this billing period`,
|
|
880
|
-
confidence: "medium"
|
|
881
|
-
};
|
|
882
|
-
};
|
|
883
|
-
var probeOpenAI = async (apiKey, _plans) => {
|
|
884
|
-
const now = /* @__PURE__ */ new Date();
|
|
885
|
-
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
886
|
-
const params = new URLSearchParams({
|
|
887
|
-
start_time: String(Math.floor(startOfMonth.getTime() / 1e3)),
|
|
888
|
-
end_time: String(Math.floor(now.getTime() / 1e3))
|
|
889
|
-
});
|
|
890
|
-
const result = await fetchJson(`https://api.openai.com/v1/organization/usage/completions?${params}`, {
|
|
891
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
892
|
-
});
|
|
893
|
-
if (!result.ok || !result.data?.data) return null;
|
|
894
|
-
let totalTokens = 0;
|
|
895
|
-
for (const bucket of result.data.data) {
|
|
896
|
-
for (const r of bucket.results ?? []) {
|
|
897
|
-
totalTokens += r.amount?.value ?? 0;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
return {
|
|
901
|
-
usage: { unitsUsed: totalTokens, unitName: "tokens" },
|
|
902
|
-
summary: `${formatK(totalTokens)} tokens used this period`,
|
|
903
|
-
confidence: "medium"
|
|
904
|
-
};
|
|
905
|
-
};
|
|
906
|
-
var probeVercel = async (apiKey, plans) => {
|
|
907
|
-
const teamsResult = await fetchJson("https://api.vercel.com/v2/teams", {
|
|
908
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
909
|
-
});
|
|
910
|
-
if (teamsResult.ok && teamsResult.data?.teams?.[0]) {
|
|
911
|
-
const team = teamsResult.data.teams[0];
|
|
912
|
-
const planName = team.billing?.plan;
|
|
913
|
-
if (planName) {
|
|
914
|
-
const matched = matchPlanByPrefix(planName, plans);
|
|
915
|
-
return {
|
|
916
|
-
planName,
|
|
917
|
-
matchedPlan: matched,
|
|
918
|
-
summary: `Team "${team.name}" on ${planName} plan`,
|
|
919
|
-
confidence: matched ? "high" : "medium"
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
const userResult = await fetchJson("https://api.vercel.com/v2/user", {
|
|
924
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
925
|
-
});
|
|
926
|
-
if (userResult.ok && userResult.data?.user) {
|
|
927
|
-
const plan = userResult.data.user.billing?.plan ?? "hobby";
|
|
928
|
-
const matched = matchPlanByPrefix(plan, plans);
|
|
929
|
-
return {
|
|
930
|
-
planName: plan,
|
|
931
|
-
matchedPlan: matched,
|
|
932
|
-
summary: `Personal account on ${plan} plan`,
|
|
933
|
-
confidence: matched ? "high" : "low"
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
return null;
|
|
937
|
-
};
|
|
938
|
-
var probeSupabase = async (apiKey, plans) => {
|
|
939
|
-
const orgsResult = await fetchJson("https://api.supabase.com/v1/organizations", {
|
|
940
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
941
|
-
});
|
|
942
|
-
if (!orgsResult.ok || !orgsResult.data || !Array.isArray(orgsResult.data)) return null;
|
|
943
|
-
const org = orgsResult.data[0];
|
|
944
|
-
if (!org) return null;
|
|
945
|
-
const planName = org.billing?.plan;
|
|
946
|
-
if (planName) {
|
|
947
|
-
const matched = matchPlanByPrefix(planName, plans);
|
|
948
|
-
return {
|
|
949
|
-
planName,
|
|
950
|
-
matchedPlan: matched,
|
|
951
|
-
summary: `Org "${org.name}" on ${planName} plan`,
|
|
952
|
-
confidence: matched ? "high" : "medium"
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
return {
|
|
956
|
-
summary: `Org "${org.name}" found (plan not detected)`,
|
|
957
|
-
confidence: "low"
|
|
958
|
-
};
|
|
959
|
-
};
|
|
960
|
-
var probeStripe = async (apiKey, _plans) => {
|
|
961
|
-
const result = await fetchJson("https://api.stripe.com/v1/balance", {
|
|
962
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
963
|
-
});
|
|
964
|
-
if (!result.ok || !result.data) return null;
|
|
965
|
-
const available = result.data.available?.[0];
|
|
966
|
-
const pending = result.data.pending?.[0];
|
|
967
|
-
const totalCents = (available?.amount ?? 0) + (pending?.amount ?? 0);
|
|
968
|
-
const currency = (available?.currency ?? "usd").toUpperCase();
|
|
969
|
-
return {
|
|
970
|
-
usage: { spend: totalCents / 100, currency },
|
|
971
|
-
summary: `Balance: ${currency} ${(totalCents / 100).toFixed(2)} (${((available?.amount ?? 0) / 100).toFixed(2)} available)`,
|
|
972
|
-
confidence: "medium"
|
|
973
|
-
};
|
|
974
|
-
};
|
|
975
|
-
var probeBrowserbase = async (apiKey, _plans) => {
|
|
976
|
-
const projResult = await fetchJson("https://api.browserbase.com/v1/projects", {
|
|
977
|
-
headers: { "X-BB-API-Key": apiKey }
|
|
978
|
-
});
|
|
979
|
-
if (!projResult.ok || !projResult.data?.[0]?.id) return null;
|
|
980
|
-
const projectId = projResult.data[0].id;
|
|
981
|
-
const usageResult = await fetchJson(`https://api.browserbase.com/v1/projects/${projectId}/usage`, {
|
|
982
|
-
headers: { "X-BB-API-Key": apiKey }
|
|
983
|
-
});
|
|
984
|
-
if (!usageResult.ok || !usageResult.data) {
|
|
985
|
-
return {
|
|
986
|
-
summary: `Project "${projResult.data[0].name}" found`,
|
|
987
|
-
confidence: "low"
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
const sessions = usageResult.data.sessions_count ?? 0;
|
|
991
|
-
const hours = usageResult.data.browser_hours ?? 0;
|
|
992
|
-
return {
|
|
993
|
-
usage: { unitsUsed: sessions, unitName: "sessions" },
|
|
994
|
-
summary: `${sessions} sessions, ${hours.toFixed(1)} browser hours this period`,
|
|
995
|
-
confidence: "medium"
|
|
996
|
-
};
|
|
997
|
-
};
|
|
998
|
-
var probeUpstash = async (apiKey, _plans) => {
|
|
999
|
-
const result = await fetchJson("https://api.upstash.com/v2/redis/databases", {
|
|
1000
|
-
headers: {
|
|
1001
|
-
Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
|
-
if (!result.ok) return null;
|
|
1005
|
-
const dbCount = Array.isArray(result.data) ? result.data.length : 0;
|
|
1006
|
-
return {
|
|
1007
|
-
summary: `${dbCount} Redis database${dbCount !== 1 ? "s" : ""} found`,
|
|
1008
|
-
confidence: "low"
|
|
1009
|
-
};
|
|
1010
|
-
};
|
|
1011
|
-
var probePostHog = async (apiKey, _plans) => {
|
|
1012
|
-
const result = await fetchJson("https://us.posthog.com/api/organizations/@current", {
|
|
1013
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1014
|
-
});
|
|
1015
|
-
if (!result.ok || !result.data) return null;
|
|
1016
|
-
return {
|
|
1017
|
-
summary: "Organization found",
|
|
1018
|
-
confidence: "low"
|
|
1019
|
-
};
|
|
1020
|
-
};
|
|
1021
|
-
var PROBES = /* @__PURE__ */ new Map([
|
|
1022
|
-
["scrapfly", probeScrapfly],
|
|
1023
|
-
["anthropic", probeAnthropic],
|
|
1024
|
-
["openai", probeOpenAI],
|
|
1025
|
-
["vercel", probeVercel],
|
|
1026
|
-
["supabase", probeSupabase],
|
|
1027
|
-
["stripe", probeStripe],
|
|
1028
|
-
["browserbase", probeBrowserbase],
|
|
1029
|
-
["upstash", probeUpstash],
|
|
1030
|
-
["posthog", probePostHog]
|
|
1031
|
-
]);
|
|
1032
|
-
async function probeService(serviceId, apiKey, plans) {
|
|
1033
|
-
const probe = PROBES.get(serviceId);
|
|
1034
|
-
if (!probe) return null;
|
|
1035
|
-
try {
|
|
1036
|
-
return await probe(apiKey, plans);
|
|
1037
|
-
} catch {
|
|
1038
|
-
return null;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
function hasProbe(serviceId) {
|
|
1042
|
-
return PROBES.has(serviceId);
|
|
1043
|
-
}
|
|
1044
|
-
function formatK(n) {
|
|
1045
|
-
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
1046
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
|
|
1047
|
-
return String(n);
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// src/interactive-init.ts
|
|
1080
|
+
init_probes();
|
|
1051
1081
|
function formatUnits(n) {
|
|
1052
1082
|
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
1053
1083
|
if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
|
|
@@ -1394,6 +1424,12 @@ async function main() {
|
|
|
1394
1424
|
case "reconcile":
|
|
1395
1425
|
await cmdReconcile();
|
|
1396
1426
|
break;
|
|
1427
|
+
case "interview":
|
|
1428
|
+
await cmdInterview();
|
|
1429
|
+
break;
|
|
1430
|
+
case "configure":
|
|
1431
|
+
await cmdConfigure();
|
|
1432
|
+
break;
|
|
1397
1433
|
case "help":
|
|
1398
1434
|
case "--help":
|
|
1399
1435
|
case "-h":
|
|
@@ -1479,6 +1515,250 @@ async function cmdInit() {
|
|
|
1479
1515
|
console.log(" burnwatch add <svc> Update a service's budget or API key");
|
|
1480
1516
|
console.log(" burnwatch init Re-run this setup anytime\n");
|
|
1481
1517
|
}
|
|
1518
|
+
async function cmdInterview() {
|
|
1519
|
+
const projectRoot = process.cwd();
|
|
1520
|
+
if (!isInitialized(projectRoot)) {
|
|
1521
|
+
ensureProjectDirs(projectRoot);
|
|
1522
|
+
const detected = detectServices(projectRoot);
|
|
1523
|
+
let projectName = path5.basename(projectRoot);
|
|
1524
|
+
try {
|
|
1525
|
+
const pkg = JSON.parse(
|
|
1526
|
+
fs5.readFileSync(path5.join(projectRoot, "package.json"), "utf-8")
|
|
1527
|
+
);
|
|
1528
|
+
if (pkg.name) projectName = pkg.name;
|
|
1529
|
+
} catch {
|
|
1530
|
+
}
|
|
1531
|
+
const config2 = {
|
|
1532
|
+
projectName,
|
|
1533
|
+
services: {},
|
|
1534
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1535
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1536
|
+
};
|
|
1537
|
+
const result = await autoConfigureServices(detected);
|
|
1538
|
+
config2.services = result.services;
|
|
1539
|
+
writeProjectConfig(config2, projectRoot);
|
|
1540
|
+
}
|
|
1541
|
+
const config = readProjectConfig(projectRoot);
|
|
1542
|
+
const globalConfig = readGlobalConfig();
|
|
1543
|
+
const allRegistryServices = getAllServices(projectRoot);
|
|
1544
|
+
const serviceStates = [];
|
|
1545
|
+
for (const [serviceId, tracked] of Object.entries(config.services)) {
|
|
1546
|
+
const definition = allRegistryServices.find((s) => s.id === serviceId);
|
|
1547
|
+
if (!definition) continue;
|
|
1548
|
+
let keySource = null;
|
|
1549
|
+
const globalKey = globalConfig.services[serviceId]?.apiKey;
|
|
1550
|
+
if (globalKey) keySource = "global_config";
|
|
1551
|
+
else {
|
|
1552
|
+
for (const pattern of definition.envPatterns) {
|
|
1553
|
+
if (process.env[pattern]) {
|
|
1554
|
+
keySource = `env:${pattern}`;
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
let probeResult = null;
|
|
1560
|
+
const { probeService: probe, hasProbe: checkProbe } = await Promise.resolve().then(() => (init_probes(), probes_exports));
|
|
1561
|
+
const apiKey = globalKey ?? (keySource?.startsWith("env:") ? process.env[keySource.slice(4)] : void 0);
|
|
1562
|
+
if (apiKey && checkProbe(serviceId)) {
|
|
1563
|
+
probeResult = await probe(serviceId, apiKey, definition.plans ?? []);
|
|
1564
|
+
}
|
|
1565
|
+
let tier = "blind";
|
|
1566
|
+
if (tracked.excluded) tier = "excluded";
|
|
1567
|
+
else if (tracked.hasApiKey) tier = "live";
|
|
1568
|
+
else if (tracked.planCost !== void 0) tier = "calc";
|
|
1569
|
+
let riskCategory = "flat";
|
|
1570
|
+
if (definition.billingModel === "token_usage") riskCategory = "llm";
|
|
1571
|
+
else if (["credit_pool", "percentage", "per_unit"].includes(definition.billingModel)) riskCategory = "usage";
|
|
1572
|
+
else if (definition.billingModel === "compute") riskCategory = "infra";
|
|
1573
|
+
const keyHints = {
|
|
1574
|
+
anthropic: "Admin key from console.anthropic.com \u2192 Settings \u2192 Admin API Keys (sk-ant-admin-*)",
|
|
1575
|
+
openai: "Admin key from platform.openai.com \u2192 Settings \u2192 API Keys (sk-admin-*)",
|
|
1576
|
+
vercel: "Token from vercel.com/account/tokens",
|
|
1577
|
+
supabase: "PAT from supabase.com/dashboard \u2192 Account \u2192 Access Tokens (not service_role key)",
|
|
1578
|
+
stripe: "Secret key from dashboard.stripe.com \u2192 Developers \u2192 API Keys (sk_live_*)",
|
|
1579
|
+
scrapfly: "API key from scrapfly.io/dashboard",
|
|
1580
|
+
browserbase: "API key from browserbase.com \u2192 Settings \u2192 API Keys",
|
|
1581
|
+
upstash: "email:api_key from console.upstash.com \u2192 Account \u2192 Management API",
|
|
1582
|
+
posthog: "Personal API key from posthog.com \u2192 Settings \u2192 Personal API Keys"
|
|
1583
|
+
};
|
|
1584
|
+
serviceStates.push({
|
|
1585
|
+
serviceId,
|
|
1586
|
+
serviceName: definition.name,
|
|
1587
|
+
currentPlan: tracked.planName ?? null,
|
|
1588
|
+
currentBudget: tracked.budget ?? null,
|
|
1589
|
+
hasApiKey: tracked.hasApiKey,
|
|
1590
|
+
keySource,
|
|
1591
|
+
tier,
|
|
1592
|
+
excluded: tracked.excluded ?? false,
|
|
1593
|
+
hasProbe: checkProbe(serviceId),
|
|
1594
|
+
probeResult,
|
|
1595
|
+
availablePlans: (definition.plans ?? []).map((p, i) => ({
|
|
1596
|
+
index: i + 1,
|
|
1597
|
+
name: p.name,
|
|
1598
|
+
type: p.type,
|
|
1599
|
+
monthlyCost: p.monthlyBase ?? null,
|
|
1600
|
+
includedUnits: p.includedUnits ?? null,
|
|
1601
|
+
unitName: p.unitName ?? null,
|
|
1602
|
+
suggestedBudget: p.suggestedBudget ?? null,
|
|
1603
|
+
isDefault: p.default ?? false
|
|
1604
|
+
})),
|
|
1605
|
+
riskCategory,
|
|
1606
|
+
billingModel: definition.billingModel,
|
|
1607
|
+
apiKeyHint: keyHints[serviceId] ?? null,
|
|
1608
|
+
allowance: tracked.allowance ?? null
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
const riskOrder = ["llm", "usage", "infra", "flat"];
|
|
1612
|
+
serviceStates.sort(
|
|
1613
|
+
(a, b) => riskOrder.indexOf(a.riskCategory) - riskOrder.indexOf(b.riskCategory)
|
|
1614
|
+
);
|
|
1615
|
+
const output = {
|
|
1616
|
+
projectName: config.projectName,
|
|
1617
|
+
serviceCount: serviceStates.length,
|
|
1618
|
+
totalBudget: serviceStates.reduce((sum, s) => sum + (s.currentBudget ?? 0), 0),
|
|
1619
|
+
liveCount: serviceStates.filter((s) => s.tier === "live").length,
|
|
1620
|
+
blindCount: serviceStates.filter((s) => s.tier === "blind").length,
|
|
1621
|
+
services: serviceStates,
|
|
1622
|
+
configureCommand: "burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]"
|
|
1623
|
+
};
|
|
1624
|
+
if (flags.has("--json")) {
|
|
1625
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1626
|
+
} else {
|
|
1627
|
+
console.log(`
|
|
1628
|
+
\u{1F4CB} Interview state for ${config.projectName}
|
|
1629
|
+
`);
|
|
1630
|
+
console.log(` ${serviceStates.length} services detected`);
|
|
1631
|
+
console.log(` ${output.liveCount} with API keys (LIVE)`);
|
|
1632
|
+
console.log(` ${output.blindCount} without tracking (BLIND)`);
|
|
1633
|
+
console.log(` Total budget: $${output.totalBudget}/mo
|
|
1634
|
+
`);
|
|
1635
|
+
console.log(` Use --json for machine-readable output.`);
|
|
1636
|
+
console.log(` Use 'burnwatch configure' to update services.
|
|
1637
|
+
`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async function cmdConfigure() {
|
|
1641
|
+
const projectRoot = process.cwd();
|
|
1642
|
+
if (!isInitialized(projectRoot)) {
|
|
1643
|
+
console.error('\u274C burnwatch not initialized. Run "burnwatch init" first.');
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
}
|
|
1646
|
+
const options = {};
|
|
1647
|
+
for (let i = 1; i < args.length; i++) {
|
|
1648
|
+
const arg = args[i];
|
|
1649
|
+
if (arg.startsWith("--") && i + 1 < args.length) {
|
|
1650
|
+
options[arg.slice(2)] = args[i + 1];
|
|
1651
|
+
i++;
|
|
1652
|
+
} else if (arg === "--exclude") {
|
|
1653
|
+
options["exclude"] = "true";
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const serviceId = options["service"];
|
|
1657
|
+
if (!serviceId) {
|
|
1658
|
+
console.error("Usage: burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]");
|
|
1659
|
+
process.exit(1);
|
|
1660
|
+
}
|
|
1661
|
+
const config = readProjectConfig(projectRoot);
|
|
1662
|
+
const definition = getService(serviceId, projectRoot);
|
|
1663
|
+
const globalConfig = readGlobalConfig();
|
|
1664
|
+
let tracked = config.services[serviceId];
|
|
1665
|
+
if (!tracked) {
|
|
1666
|
+
tracked = {
|
|
1667
|
+
serviceId,
|
|
1668
|
+
detectedVia: ["manual"],
|
|
1669
|
+
hasApiKey: false,
|
|
1670
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1671
|
+
budget: 0
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
if (options["exclude"] === "true") {
|
|
1675
|
+
tracked.excluded = true;
|
|
1676
|
+
tracked.planName = "Don't track for this project";
|
|
1677
|
+
delete tracked.budget;
|
|
1678
|
+
delete tracked.planCost;
|
|
1679
|
+
delete tracked.allowance;
|
|
1680
|
+
config.services[serviceId] = tracked;
|
|
1681
|
+
writeProjectConfig(config, projectRoot);
|
|
1682
|
+
console.log(JSON.stringify({ success: true, serviceId, action: "excluded" }));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (options["plan"]) {
|
|
1686
|
+
const planSearch = options["plan"].toLowerCase();
|
|
1687
|
+
const plans = definition?.plans ?? [];
|
|
1688
|
+
const matched = plans.find(
|
|
1689
|
+
(p) => p.name.toLowerCase().includes(planSearch) || p.name.toLowerCase().split(/[\s(]/)[0] === planSearch
|
|
1690
|
+
);
|
|
1691
|
+
if (matched) {
|
|
1692
|
+
tracked.planName = matched.name;
|
|
1693
|
+
tracked.excluded = false;
|
|
1694
|
+
if (matched.type === "flat" && matched.monthlyBase !== void 0) {
|
|
1695
|
+
tracked.planCost = matched.monthlyBase;
|
|
1696
|
+
if (options["budget"] === void 0 && (tracked.budget === void 0 || tracked.budget === 0)) {
|
|
1697
|
+
tracked.budget = matched.monthlyBase;
|
|
1698
|
+
}
|
|
1699
|
+
} else if (matched.suggestedBudget !== void 0 && options["budget"] === void 0) {
|
|
1700
|
+
if (tracked.budget === void 0 || tracked.budget === 0) {
|
|
1701
|
+
tracked.budget = matched.suggestedBudget;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (matched.includedUnits !== void 0 && matched.unitName) {
|
|
1705
|
+
tracked.allowance = {
|
|
1706
|
+
included: matched.includedUnits,
|
|
1707
|
+
unitName: matched.unitName
|
|
1708
|
+
};
|
|
1709
|
+
} else {
|
|
1710
|
+
delete tracked.allowance;
|
|
1711
|
+
}
|
|
1712
|
+
} else {
|
|
1713
|
+
tracked.planName = options["plan"];
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (options["budget"] !== void 0) {
|
|
1717
|
+
const parsed = parseFloat(options["budget"]);
|
|
1718
|
+
if (!isNaN(parsed)) {
|
|
1719
|
+
tracked.budget = parsed;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (options["key"]) {
|
|
1723
|
+
tracked.hasApiKey = true;
|
|
1724
|
+
if (!globalConfig.services[serviceId]) {
|
|
1725
|
+
globalConfig.services[serviceId] = {};
|
|
1726
|
+
}
|
|
1727
|
+
globalConfig.services[serviceId].apiKey = options["key"];
|
|
1728
|
+
writeGlobalConfig(globalConfig);
|
|
1729
|
+
const { probeService: probe, hasProbe: checkProbe } = await Promise.resolve().then(() => (init_probes(), probes_exports));
|
|
1730
|
+
if (checkProbe(serviceId) && definition?.plans) {
|
|
1731
|
+
const probeResult = await probe(serviceId, options["key"], definition.plans);
|
|
1732
|
+
if (probeResult?.matchedPlan && probeResult.confidence === "high" && !options["plan"]) {
|
|
1733
|
+
const mp = probeResult.matchedPlan;
|
|
1734
|
+
tracked.planName = mp.name;
|
|
1735
|
+
if (mp.type === "flat" && mp.monthlyBase !== void 0) {
|
|
1736
|
+
tracked.planCost = mp.monthlyBase;
|
|
1737
|
+
if (options["budget"] === void 0) tracked.budget = mp.monthlyBase;
|
|
1738
|
+
}
|
|
1739
|
+
if (mp.includedUnits !== void 0 && mp.unitName) {
|
|
1740
|
+
tracked.allowance = { included: mp.includedUnits, unitName: mp.unitName };
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
config.services[serviceId] = tracked;
|
|
1746
|
+
writeProjectConfig(config, projectRoot);
|
|
1747
|
+
let tier = "blind";
|
|
1748
|
+
if (tracked.excluded) tier = "excluded";
|
|
1749
|
+
else if (tracked.hasApiKey) tier = "live";
|
|
1750
|
+
else if (tracked.planCost !== void 0) tier = "calc";
|
|
1751
|
+
const result = {
|
|
1752
|
+
success: true,
|
|
1753
|
+
serviceId,
|
|
1754
|
+
plan: tracked.planName ?? null,
|
|
1755
|
+
budget: tracked.budget ?? null,
|
|
1756
|
+
tier,
|
|
1757
|
+
hasApiKey: tracked.hasApiKey,
|
|
1758
|
+
allowance: tracked.allowance ?? null
|
|
1759
|
+
};
|
|
1760
|
+
console.log(JSON.stringify(result));
|
|
1761
|
+
}
|
|
1482
1762
|
async function cmdAdd() {
|
|
1483
1763
|
const projectRoot = process.cwd();
|
|
1484
1764
|
if (!isInitialized(projectRoot)) {
|
|
@@ -1643,6 +1923,15 @@ Usage:
|
|
|
1643
1923
|
burnwatch status Show current spend brief
|
|
1644
1924
|
burnwatch services List all services in registry
|
|
1645
1925
|
burnwatch reconcile Scan for untracked services
|
|
1926
|
+
burnwatch interview --json Export state for agent-driven interview
|
|
1927
|
+
burnwatch configure --service <id> [opts] Agent writes back interview answers
|
|
1928
|
+
|
|
1929
|
+
Options for 'configure':
|
|
1930
|
+
--service <ID> Service to configure (required)
|
|
1931
|
+
--plan <NAME> Plan name (fuzzy matches against registry)
|
|
1932
|
+
--budget <AMOUNT> Monthly budget in USD
|
|
1933
|
+
--key <API_KEY> API key for LIVE tracking
|
|
1934
|
+
--exclude Exclude this service from tracking
|
|
1646
1935
|
|
|
1647
1936
|
Options for 'add':
|
|
1648
1937
|
--key <API_KEY> API key for LIVE tracking (saved to ~/.config/burnwatch/)
|
|
@@ -1652,10 +1941,10 @@ Options for 'add':
|
|
|
1652
1941
|
|
|
1653
1942
|
Examples:
|
|
1654
1943
|
burnwatch init
|
|
1655
|
-
burnwatch
|
|
1656
|
-
burnwatch
|
|
1657
|
-
burnwatch
|
|
1658
|
-
burnwatch
|
|
1944
|
+
burnwatch interview --json
|
|
1945
|
+
burnwatch configure --service anthropic --plan "API Usage" --budget 100
|
|
1946
|
+
burnwatch configure --service supabase --plan pro --budget 25 --key sbp_xxx
|
|
1947
|
+
burnwatch configure --service posthog --plan free --budget 0
|
|
1659
1948
|
burnwatch status
|
|
1660
1949
|
`);
|
|
1661
1950
|
}
|