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/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 init --non-interactive
1656
- burnwatch add anthropic --key sk-ant-admin-xxx --budget 100
1657
- burnwatch add scrapfly --key scp-xxx --budget 50
1658
- burnwatch add posthog --plan-cost 0 --budget 0
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
  }