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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ All notable changes to burnwatch will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.0] - 2026-03-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Service probing system**: New extensible `probes.ts` module that auto-detects plan tiers, usage, and billing data from service APIs. Adding a new service probe is a single function — the system supports N services. Probes for 9 of 14 services:
|
|
13
|
+
- **Scrapfly**: Plan name + credit usage (high confidence — skips plan selection)
|
|
14
|
+
- **Vercel**: Plan tier from team/user API (high confidence)
|
|
15
|
+
- **Supabase**: Plan tier from Management API (high confidence, requires PAT)
|
|
16
|
+
- **Anthropic**: Current month USD spend from Admin API cost report (medium — shows spend, still asks plan)
|
|
17
|
+
- **OpenAI**: Token usage from Admin API (medium)
|
|
18
|
+
- **Stripe**: Balance and processing volume (medium)
|
|
19
|
+
- **Browserbase**: Session count and browser hours (medium)
|
|
20
|
+
- **Upstash**: Database discovery (low — validates key)
|
|
21
|
+
- **PostHog**: Organization discovery (low — validates key)
|
|
22
|
+
- **Tiered discovery in interview**: Init now follows API → key → ask → hedge:
|
|
23
|
+
1. Find API key (env vars, global config)
|
|
24
|
+
2. If key found + probe exists → hit the API, show what we found
|
|
25
|
+
3. High confidence → "Detected: Pro ($100/mo, 1M credits). Correct? [Y/n]"
|
|
26
|
+
4. Medium confidence → show usage data, then ask plan
|
|
27
|
+
5. No key → show plan list, ask for key after
|
|
28
|
+
6. Budget always set
|
|
29
|
+
- **Auto-configure probing**: Non-TTY mode (Claude Code) also probes APIs when keys are available, auto-matching detected plans instead of always using defaults.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Interview no longer requires `autoDetectPlan` in registry**: Any service with a probe in `PROBES` map is automatically probed when a key is available. Adding a service-specific probe is the only requirement.
|
|
34
|
+
- **Key detection runs before plan selection for all services**: Previously only LIVE-tier services checked for keys. Now any service with a probe gets key detection first.
|
|
35
|
+
|
|
36
|
+
## [0.6.0] - 2026-03-24
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Allowance tracking for credit-pool services**: Services like Scrapfly that sell a fixed credit pool (e.g., Pro $100/mo = 1M credits) now track unit consumption against the plan allowance, not just dollar spend. The brief shows `↳ 850K/1M credits (85%) ⚠️` alongside the dollar line. This is the distinction between "budget" (what you pay) and "spend metric" (what you consume).
|
|
41
|
+
- **Allowance data in spend snapshots**: `SpendSnapshot.allowance` provides `used`, `included`, `unitName`, and `percent` for credit-pool services with LIVE connectors.
|
|
42
|
+
- **PlanTier `includedUnits` and `unitName`**: Registry plans can now declare how many units are included (e.g., `"includedUnits": 1000000, "unitName": "credits"`). Init automatically sets `TrackedService.allowance` from plan selection.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Scrapfly registry updated to real pricing**: Discovery $30/200K, Pro $100/1M (default), Startup $250/2.5M, Enterprise $500/5.5M. Previously had incorrect plan names and prices.
|
|
47
|
+
- **Scrapfly connector returns unit data**: `BillingResult` now includes `unitsUsed`, `unitsTotal`, and `unitName` so the brief can show credit consumption alongside dollar spend.
|
|
48
|
+
- **Allowance-aware status labels**: Credit-pool services show "750K credits left" or "⚠️ 125% of 1M credits used" instead of generic budget percentages.
|
|
49
|
+
|
|
8
50
|
## [0.5.2] - 2026-03-24
|
|
9
51
|
|
|
10
52
|
### Changed
|
|
@@ -79,6 +121,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
79
121
|
- Snapshot system for delta computation across sessions
|
|
80
122
|
- Claude Code skills: `/spend` (on-demand brief), `/setup-burnwatch` (guided onboarding)
|
|
81
123
|
|
|
124
|
+
[0.7.0]: https://github.com/RaleighSF/burnwatch/compare/v0.6.0...v0.7.0
|
|
125
|
+
[0.6.0]: https://github.com/RaleighSF/burnwatch/compare/v0.5.2...v0.6.0
|
|
82
126
|
[0.5.2]: https://github.com/RaleighSF/burnwatch/compare/v0.5.1...v0.5.2
|
|
83
127
|
[0.5.1]: https://github.com/RaleighSF/burnwatch/compare/v0.5.0...v0.5.1
|
|
84
128
|
[0.5.0]: https://github.com/RaleighSF/burnwatch/compare/v0.4.3...v0.5.0
|
package/dist/cli.js
CHANGED
|
@@ -497,6 +497,9 @@ var scrapflyConnector = {
|
|
|
497
497
|
spend,
|
|
498
498
|
isEstimate: false,
|
|
499
499
|
tier: "live",
|
|
500
|
+
unitsUsed: creditsUsed,
|
|
501
|
+
unitsTotal: creditsTotal,
|
|
502
|
+
unitName: "credits",
|
|
500
503
|
raw: {
|
|
501
504
|
credits_used: creditsUsed,
|
|
502
505
|
credits_total: creditsTotal,
|
|
@@ -606,6 +609,15 @@ function formatBrief(brief) {
|
|
|
606
609
|
const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
|
|
607
610
|
const leftStr = formatLeft(svc);
|
|
608
611
|
lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));
|
|
612
|
+
if (svc.allowance) {
|
|
613
|
+
const usedStr = formatCompact(svc.allowance.used);
|
|
614
|
+
const totalStr2 = formatCompact(svc.allowance.included);
|
|
615
|
+
const pctStr = svc.allowance.percent.toFixed(0);
|
|
616
|
+
const warn = svc.allowance.percent >= 75 ? " \u26A0\uFE0F" : "";
|
|
617
|
+
lines.push(
|
|
618
|
+
`\u2551 \u21B3 ${usedStr}/${totalStr2} ${svc.allowance.unitName} (${pctStr}%)${warn}`.padEnd(width + 1) + "\u2551"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
609
621
|
}
|
|
610
622
|
lines.push(`\u2560${hrDouble}\u2563`);
|
|
611
623
|
const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
|
|
@@ -690,7 +702,7 @@ function formatLeft(snap) {
|
|
|
690
702
|
}
|
|
691
703
|
return "\u2014";
|
|
692
704
|
}
|
|
693
|
-
function buildSnapshot(serviceId, tier, spend, budget) {
|
|
705
|
+
function buildSnapshot(serviceId, tier, spend, budget, allowanceData) {
|
|
694
706
|
const isEstimate = tier === "est" || tier === "calc";
|
|
695
707
|
const budgetPercent = budget ? spend / budget * 100 : void 0;
|
|
696
708
|
let status = "unknown";
|
|
@@ -707,7 +719,21 @@ function buildSnapshot(serviceId, tier, spend, budget) {
|
|
|
707
719
|
statusLabel = `${(100 - budgetPercent).toFixed(0)}% \u2014 healthy`;
|
|
708
720
|
}
|
|
709
721
|
}
|
|
710
|
-
|
|
722
|
+
let allowance;
|
|
723
|
+
if (allowanceData && allowanceData.included > 0) {
|
|
724
|
+
const percent = allowanceData.used / allowanceData.included * 100;
|
|
725
|
+
allowance = { ...allowanceData, percent };
|
|
726
|
+
if (percent > 100) {
|
|
727
|
+
status = "over";
|
|
728
|
+
statusLabel = `\u26A0\uFE0F ${percent.toFixed(0)}% of ${formatCompact(allowanceData.included)} ${allowanceData.unitName} used`;
|
|
729
|
+
} else if (percent >= 75) {
|
|
730
|
+
status = "caution";
|
|
731
|
+
statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left \u2014 caution`;
|
|
732
|
+
} else {
|
|
733
|
+
status = "healthy";
|
|
734
|
+
statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left`;
|
|
735
|
+
}
|
|
736
|
+
} else if (tier === "calc" && budget) {
|
|
711
737
|
statusLabel = `flat \u2014 on plan`;
|
|
712
738
|
status = "healthy";
|
|
713
739
|
}
|
|
@@ -720,9 +746,15 @@ function buildSnapshot(serviceId, tier, spend, budget) {
|
|
|
720
746
|
budgetPercent,
|
|
721
747
|
status,
|
|
722
748
|
statusLabel,
|
|
723
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
749
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
750
|
+
allowance
|
|
724
751
|
};
|
|
725
752
|
}
|
|
753
|
+
function formatCompact(n) {
|
|
754
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
755
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
|
|
756
|
+
return String(n);
|
|
757
|
+
}
|
|
726
758
|
|
|
727
759
|
// src/core/ledger.ts
|
|
728
760
|
import * as fs4 from "fs";
|
|
@@ -787,6 +819,240 @@ function saveSnapshot(brief, projectRoot) {
|
|
|
787
819
|
|
|
788
820
|
// src/interactive-init.ts
|
|
789
821
|
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
|
|
1051
|
+
function formatUnits(n) {
|
|
1052
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
1053
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
|
|
1054
|
+
return String(n);
|
|
1055
|
+
}
|
|
790
1056
|
var RISK_ORDER = ["llm", "usage", "infra", "flat"];
|
|
791
1057
|
var RISK_LABELS = {
|
|
792
1058
|
llm: "LLM / AI Services (highest variable cost)",
|
|
@@ -827,16 +1093,6 @@ function ask(rl, question) {
|
|
|
827
1093
|
});
|
|
828
1094
|
});
|
|
829
1095
|
}
|
|
830
|
-
async function autoDetectScrapflyPlan(apiKey) {
|
|
831
|
-
try {
|
|
832
|
-
const result = await fetchJson(`https://api.scrapfly.io/account?key=${apiKey}`);
|
|
833
|
-
if (result.ok && result.data?.subscription?.plan?.name) {
|
|
834
|
-
return result.data.subscription.plan.name;
|
|
835
|
-
}
|
|
836
|
-
} catch {
|
|
837
|
-
}
|
|
838
|
-
return null;
|
|
839
|
-
}
|
|
840
1096
|
function findEnvKey(service) {
|
|
841
1097
|
for (const pattern of service.envPatterns) {
|
|
842
1098
|
const val = process.env[pattern];
|
|
@@ -844,7 +1100,7 @@ function findEnvKey(service) {
|
|
|
844
1100
|
}
|
|
845
1101
|
return void 0;
|
|
846
1102
|
}
|
|
847
|
-
function autoConfigureServices(detected) {
|
|
1103
|
+
async function autoConfigureServices(detected) {
|
|
848
1104
|
const services = {};
|
|
849
1105
|
const groups = groupByRisk(detected);
|
|
850
1106
|
const globalConfig = readGlobalConfig();
|
|
@@ -877,25 +1133,54 @@ function autoConfigureServices(detected) {
|
|
|
877
1133
|
} else if (defaultPlan.suggestedBudget !== void 0) {
|
|
878
1134
|
tracked.budget = defaultPlan.suggestedBudget;
|
|
879
1135
|
}
|
|
1136
|
+
if (defaultPlan.includedUnits !== void 0 && defaultPlan.unitName) {
|
|
1137
|
+
tracked.allowance = {
|
|
1138
|
+
included: defaultPlan.includedUnits,
|
|
1139
|
+
unitName: defaultPlan.unitName
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
880
1142
|
}
|
|
881
1143
|
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
882
1144
|
const envKey = findEnvKey(service);
|
|
883
1145
|
let keySource = "";
|
|
1146
|
+
let apiKey;
|
|
884
1147
|
if (existingKey) {
|
|
885
1148
|
tracked.hasApiKey = true;
|
|
1149
|
+
apiKey = existingKey;
|
|
886
1150
|
keySource = " (key: global config)";
|
|
887
1151
|
} else if (envKey) {
|
|
888
1152
|
tracked.hasApiKey = true;
|
|
1153
|
+
apiKey = envKey;
|
|
889
1154
|
if (!globalConfig.services[service.id]) {
|
|
890
1155
|
globalConfig.services[service.id] = {};
|
|
891
1156
|
}
|
|
892
1157
|
globalConfig.services[service.id].apiKey = envKey;
|
|
893
1158
|
keySource = ` (key: ${service.envPatterns[0]})`;
|
|
894
1159
|
}
|
|
1160
|
+
if (apiKey && hasProbe(service.id)) {
|
|
1161
|
+
try {
|
|
1162
|
+
const probe = await probeService(service.id, apiKey, plans);
|
|
1163
|
+
if (probe?.matchedPlan && probe.confidence === "high") {
|
|
1164
|
+
const mp = probe.matchedPlan;
|
|
1165
|
+
tracked.planName = mp.name;
|
|
1166
|
+
if (mp.type === "flat" && mp.monthlyBase !== void 0) {
|
|
1167
|
+
tracked.planCost = mp.monthlyBase;
|
|
1168
|
+
tracked.budget = mp.monthlyBase;
|
|
1169
|
+
} else if (mp.suggestedBudget !== void 0) {
|
|
1170
|
+
tracked.budget = mp.suggestedBudget;
|
|
1171
|
+
}
|
|
1172
|
+
if (mp.includedUnits !== void 0 && mp.unitName) {
|
|
1173
|
+
tracked.allowance = { included: mp.includedUnits, unitName: mp.unitName };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
895
1179
|
const tierLabel = tracked.hasApiKey ? "LIVE" : tracked.planCost !== void 0 ? "CALC" : "BLIND";
|
|
896
1180
|
const planStr = tracked.planName ? ` ${tracked.planName}` : "";
|
|
1181
|
+
const trackingStr = tracked.allowance ? `$${tracked.budget}/mo | ${formatUnits(tracked.allowance.included)} ${tracked.allowance.unitName}` : `$${tracked.budget}/mo`;
|
|
897
1182
|
console.log(
|
|
898
|
-
` ${service.name}:${planStr} | ${tierLabel} |
|
|
1183
|
+
` ${service.name}:${planStr} | ${tierLabel} | ${trackingStr}${keySource}`
|
|
899
1184
|
);
|
|
900
1185
|
services[service.id] = tracked;
|
|
901
1186
|
}
|
|
@@ -947,21 +1232,61 @@ async function runInteractiveInit(detected) {
|
|
|
947
1232
|
console.log(" -> Configured (no plan tiers in registry, budget: $0)");
|
|
948
1233
|
continue;
|
|
949
1234
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1235
|
+
let apiKey;
|
|
1236
|
+
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
1237
|
+
const envKey = findEnvKey(service);
|
|
1238
|
+
if (existingKey) {
|
|
1239
|
+
apiKey = existingKey;
|
|
1240
|
+
console.log(` API key: found in global config`);
|
|
1241
|
+
} else if (envKey) {
|
|
1242
|
+
apiKey = envKey;
|
|
1243
|
+
console.log(` API key: found in environment (${service.envPatterns[0]})`);
|
|
1244
|
+
if (!globalConfig.services[service.id]) {
|
|
1245
|
+
globalConfig.services[service.id] = {};
|
|
1246
|
+
}
|
|
1247
|
+
globalConfig.services[service.id].apiKey = envKey;
|
|
1248
|
+
}
|
|
1249
|
+
let chosen;
|
|
1250
|
+
if (apiKey && hasProbe(service.id)) {
|
|
1251
|
+
console.log(" Probing API...");
|
|
1252
|
+
const probe = await probeService(service.id, apiKey, plans);
|
|
1253
|
+
if (probe) {
|
|
1254
|
+
console.log(` ${probe.summary}`);
|
|
1255
|
+
if (probe.confidence === "high" && probe.matchedPlan) {
|
|
1256
|
+
const plan = probe.matchedPlan;
|
|
1257
|
+
const costStr = plan.monthlyBase !== void 0 ? `$${plan.monthlyBase}/mo` : "variable";
|
|
1258
|
+
const unitsStr = plan.includedUnits && plan.unitName ? `, ${formatUnits(plan.includedUnits)} ${plan.unitName}` : "";
|
|
1259
|
+
const confirm = await ask(
|
|
1260
|
+
rl,
|
|
1261
|
+
` Detected: ${plan.name} (${costStr}${unitsStr}). Correct? [Y/n]: `
|
|
1262
|
+
);
|
|
1263
|
+
if (confirm === "" || confirm.toLowerCase().startsWith("y")) {
|
|
1264
|
+
chosen = plan;
|
|
1265
|
+
}
|
|
1266
|
+
} else if (probe.confidence === "medium") {
|
|
1267
|
+
if (probe.usage?.spend !== void 0) {
|
|
1268
|
+
console.log(` Current spend: $${probe.usage.spend.toFixed(2)}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (!chosen) {
|
|
1274
|
+
const defaultIndex = plans.findIndex((p) => p.default);
|
|
1275
|
+
console.log("");
|
|
1276
|
+
for (let i = 0; i < plans.length; i++) {
|
|
1277
|
+
const plan = plans[i];
|
|
1278
|
+
const marker = i === defaultIndex ? " *" : "";
|
|
1279
|
+
const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? ` - $${plan.monthlyBase}/mo` : " - variable";
|
|
1280
|
+
console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);
|
|
1281
|
+
}
|
|
1282
|
+
const defaultChoice = defaultIndex >= 0 ? String(defaultIndex + 1) : "1";
|
|
1283
|
+
const answer = await ask(
|
|
1284
|
+
rl,
|
|
1285
|
+
` Which plan? [${defaultChoice}]: `
|
|
1286
|
+
);
|
|
1287
|
+
const choiceIndex = (answer === "" ? parseInt(defaultChoice) : parseInt(answer)) - 1;
|
|
1288
|
+
chosen = plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0];
|
|
957
1289
|
}
|
|
958
|
-
const defaultChoice = defaultIndex >= 0 ? String(defaultIndex + 1) : "1";
|
|
959
|
-
const answer = await ask(
|
|
960
|
-
rl,
|
|
961
|
-
` Which plan? [${defaultChoice}]: `
|
|
962
|
-
);
|
|
963
|
-
const choiceIndex = (answer === "" ? parseInt(defaultChoice) : parseInt(answer)) - 1;
|
|
964
|
-
const chosen = plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0];
|
|
965
1290
|
if (chosen.type === "exclude") {
|
|
966
1291
|
services[service.id] = {
|
|
967
1292
|
serviceId: service.id,
|
|
@@ -977,49 +1302,37 @@ async function runInteractiveInit(detected) {
|
|
|
977
1302
|
const tracked2 = {
|
|
978
1303
|
serviceId: service.id,
|
|
979
1304
|
detectedVia: det.sources,
|
|
980
|
-
hasApiKey:
|
|
1305
|
+
hasApiKey: !!apiKey,
|
|
981
1306
|
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
982
1307
|
planName: chosen.name
|
|
983
1308
|
};
|
|
984
1309
|
if (chosen.type === "flat" && chosen.monthlyBase !== void 0) {
|
|
985
1310
|
tracked2.planCost = chosen.monthlyBase;
|
|
986
1311
|
}
|
|
987
|
-
if (
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1312
|
+
if (chosen.includedUnits !== void 0 && chosen.unitName) {
|
|
1313
|
+
tracked2.allowance = {
|
|
1314
|
+
included: chosen.includedUnits,
|
|
1315
|
+
unitName: chosen.unitName
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
if (!apiKey && hasProbe(service.id)) {
|
|
1319
|
+
const hint = API_KEY_HINTS[service.id];
|
|
1320
|
+
if (hint) console.log(` ${hint}`);
|
|
1321
|
+
const keyAnswer = await ask(
|
|
1322
|
+
rl,
|
|
1323
|
+
` API key for real-time tracking (Enter to skip): `
|
|
1324
|
+
);
|
|
1325
|
+
if (keyAnswer) {
|
|
995
1326
|
tracked2.hasApiKey = true;
|
|
1327
|
+
apiKey = keyAnswer;
|
|
996
1328
|
if (!globalConfig.services[service.id]) {
|
|
997
1329
|
globalConfig.services[service.id] = {};
|
|
998
1330
|
}
|
|
999
|
-
globalConfig.services[service.id].apiKey =
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
rl,
|
|
1005
|
-
` API key for real-time tracking (Enter to skip): `
|
|
1006
|
-
);
|
|
1007
|
-
if (keyAnswer) {
|
|
1008
|
-
tracked2.hasApiKey = true;
|
|
1009
|
-
if (!globalConfig.services[service.id]) {
|
|
1010
|
-
globalConfig.services[service.id] = {};
|
|
1011
|
-
}
|
|
1012
|
-
globalConfig.services[service.id].apiKey = keyAnswer;
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
if (service.autoDetectPlan && service.id === "scrapfly" && tracked2.hasApiKey) {
|
|
1016
|
-
const key = globalConfig.services[service.id]?.apiKey;
|
|
1017
|
-
if (key) {
|
|
1018
|
-
console.log(" Detecting plan from API...");
|
|
1019
|
-
const planName = await autoDetectScrapflyPlan(key);
|
|
1020
|
-
if (planName) {
|
|
1021
|
-
console.log(` -> Detected plan: ${planName}`);
|
|
1022
|
-
tracked2.planName = planName;
|
|
1331
|
+
globalConfig.services[service.id].apiKey = keyAnswer;
|
|
1332
|
+
if (hasProbe(service.id)) {
|
|
1333
|
+
const probe = await probeService(service.id, keyAnswer, plans);
|
|
1334
|
+
if (probe?.usage) {
|
|
1335
|
+
console.log(` ${probe.summary}`);
|
|
1023
1336
|
}
|
|
1024
1337
|
}
|
|
1025
1338
|
}
|
|
@@ -1037,8 +1350,9 @@ async function runInteractiveInit(detected) {
|
|
|
1037
1350
|
}
|
|
1038
1351
|
services[service.id] = tracked2;
|
|
1039
1352
|
const tierLabel = tracked2.hasApiKey ? "LIVE" : tracked2.planCost !== void 0 ? "CALC" : "BLIND";
|
|
1353
|
+
const allowanceStr = tracked2.allowance ? ` | ${formatUnits(tracked2.allowance.included)} ${tracked2.allowance.unitName}` : "";
|
|
1040
1354
|
console.log(
|
|
1041
|
-
` -> ${service.name}: ${tracked2.planName} | ${tierLabel} | $${tracked2.budget}/mo`
|
|
1355
|
+
` -> ${service.name}: ${tracked2.planName} | ${tierLabel} | $${tracked2.budget}/mo${allowanceStr}`
|
|
1042
1356
|
);
|
|
1043
1357
|
}
|
|
1044
1358
|
}
|
|
@@ -1141,7 +1455,7 @@ async function cmdInit() {
|
|
|
1141
1455
|
const result = await runInteractiveInit(detected);
|
|
1142
1456
|
config.services = result.services;
|
|
1143
1457
|
} else {
|
|
1144
|
-
const result = autoConfigureServices(detected);
|
|
1458
|
+
const result = await autoConfigureServices(detected);
|
|
1145
1459
|
config.services = result.services;
|
|
1146
1460
|
}
|
|
1147
1461
|
writeProjectConfig(config, projectRoot);
|
|
@@ -1252,14 +1566,11 @@ async function cmdStatus() {
|
|
|
1252
1566
|
}
|
|
1253
1567
|
console.log("\u{1F4CA} Polling services...\n");
|
|
1254
1568
|
const results = await pollAllServices(trackedServices);
|
|
1255
|
-
const snapshots = results.map(
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
config.services[r.serviceId]?.budget
|
|
1261
|
-
)
|
|
1262
|
-
);
|
|
1569
|
+
const snapshots = results.map((r) => {
|
|
1570
|
+
const tracked = config.services[r.serviceId];
|
|
1571
|
+
const allowanceData = r.unitsUsed !== void 0 && r.unitsTotal !== void 0 && r.unitName ? { used: r.unitsUsed, included: r.unitsTotal, unitName: r.unitName } : tracked?.allowance ? { used: 0, included: tracked.allowance.included, unitName: tracked.allowance.unitName } : void 0;
|
|
1572
|
+
return buildSnapshot(r.serviceId, r.tier, r.spend, tracked?.budget, allowanceData);
|
|
1573
|
+
});
|
|
1263
1574
|
const blindCount = snapshots.filter((s) => s.tier === "blind").length;
|
|
1264
1575
|
const brief = buildBrief(config.projectName, snapshots, blindCount);
|
|
1265
1576
|
saveSnapshot(brief, projectRoot);
|