burnwatch 0.5.0 → 0.5.1
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 +7 -0
- package/dist/cli.js +94 -66
- package/dist/cli.js.map +1 -1
- package/dist/interactive-init.d.ts +9 -1
- package/dist/interactive-init.js +76 -0
- package/dist/interactive-init.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.5.1] - 2026-03-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Init actually works now**: `process.stdin.isTTY` was `undefined` in Claude Code and many terminal environments, so the interactive interview never ran. Init now has three modes: (1) TTY detected = full interactive interview, (2) no TTY = smart auto-configure with defaults, (3) `--non-interactive` = minimal CI mode. The auto-configure path picks each service's default plan, detects API keys from env vars and global config, sets budget = plan cost for flat plans and $0 for usage-based, and only shows `burnwatch add` commands for the handful of usage-based services that genuinely need a human-set budget.
|
|
13
|
+
|
|
8
14
|
## [0.5.0] - 2026-03-24
|
|
9
15
|
|
|
10
16
|
### Changed
|
|
@@ -66,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
66
72
|
- Snapshot system for delta computation across sessions
|
|
67
73
|
- Claude Code skills: `/spend` (on-demand brief), `/setup-burnwatch` (guided onboarding)
|
|
68
74
|
|
|
75
|
+
[0.5.1]: https://github.com/RaleighSF/burnwatch/compare/v0.5.0...v0.5.1
|
|
69
76
|
[0.5.0]: https://github.com/RaleighSF/burnwatch/compare/v0.4.3...v0.5.0
|
|
70
77
|
[0.4.3]: https://github.com/RaleighSF/burnwatch/compare/v0.4.2...v0.4.3
|
|
71
78
|
[0.4.2]: https://github.com/RaleighSF/burnwatch/compare/v0.4.0...v0.4.2
|
package/dist/cli.js
CHANGED
|
@@ -844,6 +844,81 @@ function findEnvKey(service) {
|
|
|
844
844
|
}
|
|
845
845
|
return void 0;
|
|
846
846
|
}
|
|
847
|
+
function autoConfigureServices(detected) {
|
|
848
|
+
const services = {};
|
|
849
|
+
const groups = groupByRisk(detected);
|
|
850
|
+
const globalConfig = readGlobalConfig();
|
|
851
|
+
console.log(
|
|
852
|
+
`
|
|
853
|
+
Found ${detected.length} paid service${detected.length !== 1 ? "s" : ""}. Auto-configuring with defaults.
|
|
854
|
+
`
|
|
855
|
+
);
|
|
856
|
+
console.log(" Run 'burnwatch init' from your terminal for interactive setup.\n");
|
|
857
|
+
for (const category of RISK_ORDER) {
|
|
858
|
+
const group = groups.get(category);
|
|
859
|
+
if (group.length === 0) continue;
|
|
860
|
+
console.log(` ${RISK_LABELS[category]}`);
|
|
861
|
+
for (const det of group) {
|
|
862
|
+
const service = det.service;
|
|
863
|
+
const plans = service.plans ?? [];
|
|
864
|
+
const defaultPlan = plans.find((p) => p.default) ?? plans[0];
|
|
865
|
+
const tracked = {
|
|
866
|
+
serviceId: service.id,
|
|
867
|
+
detectedVia: det.sources,
|
|
868
|
+
hasApiKey: false,
|
|
869
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
870
|
+
budget: 0
|
|
871
|
+
};
|
|
872
|
+
if (defaultPlan && defaultPlan.type !== "exclude") {
|
|
873
|
+
tracked.planName = defaultPlan.name;
|
|
874
|
+
if (defaultPlan.type === "flat" && defaultPlan.monthlyBase !== void 0) {
|
|
875
|
+
tracked.planCost = defaultPlan.monthlyBase;
|
|
876
|
+
tracked.budget = defaultPlan.monthlyBase;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
880
|
+
const envKey = findEnvKey(service);
|
|
881
|
+
let keySource = "";
|
|
882
|
+
if (existingKey) {
|
|
883
|
+
tracked.hasApiKey = true;
|
|
884
|
+
keySource = " (key: global config)";
|
|
885
|
+
} else if (envKey) {
|
|
886
|
+
tracked.hasApiKey = true;
|
|
887
|
+
if (!globalConfig.services[service.id]) {
|
|
888
|
+
globalConfig.services[service.id] = {};
|
|
889
|
+
}
|
|
890
|
+
globalConfig.services[service.id].apiKey = envKey;
|
|
891
|
+
keySource = ` (key: ${service.envPatterns[0]})`;
|
|
892
|
+
}
|
|
893
|
+
const tierLabel = tracked.hasApiKey ? "LIVE" : tracked.planCost !== void 0 ? "CALC" : "BLIND";
|
|
894
|
+
const planStr = tracked.planName ? ` ${tracked.planName}` : "";
|
|
895
|
+
console.log(
|
|
896
|
+
` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`
|
|
897
|
+
);
|
|
898
|
+
services[service.id] = tracked;
|
|
899
|
+
}
|
|
900
|
+
console.log("");
|
|
901
|
+
}
|
|
902
|
+
const trackedList = Object.values(services);
|
|
903
|
+
const liveCount = trackedList.filter((s) => s.hasApiKey).length;
|
|
904
|
+
const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);
|
|
905
|
+
console.log(" " + "-".repeat(48));
|
|
906
|
+
console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);
|
|
907
|
+
if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);
|
|
908
|
+
const needBudget = trackedList.filter(
|
|
909
|
+
(s) => s.budget === 0 && s.planCost === void 0
|
|
910
|
+
);
|
|
911
|
+
if (needBudget.length > 0) {
|
|
912
|
+
console.log(`
|
|
913
|
+
${needBudget.length} usage-based service${needBudget.length > 1 ? "s" : ""} need budgets:`);
|
|
914
|
+
for (const svc of needBudget) {
|
|
915
|
+
console.log(` burnwatch add ${svc.serviceId} --budget <AMOUNT>`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
console.log("");
|
|
919
|
+
writeGlobalConfig(globalConfig);
|
|
920
|
+
return { services };
|
|
921
|
+
}
|
|
847
922
|
async function runInteractiveInit(detected) {
|
|
848
923
|
const rl = readline.createInterface({
|
|
849
924
|
input: process.stdin,
|
|
@@ -1062,76 +1137,29 @@ async function cmdInit() {
|
|
|
1062
1137
|
createdAt: existingConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1063
1138
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1064
1139
|
};
|
|
1065
|
-
if (
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
} else {
|
|
1069
|
-
const globalConfig = readGlobalConfig();
|
|
1140
|
+
if (detected.length === 0) {
|
|
1141
|
+
console.log(" No paid services detected yet.");
|
|
1142
|
+
console.log(" Services will be detected as they enter your project.\n");
|
|
1143
|
+
} else if (nonInteractive) {
|
|
1070
1144
|
for (const det of detected) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
hasApiKey: existing?.hasApiKey ?? false,
|
|
1080
|
-
firstDetected: existing?.firstDetected ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1081
|
-
budget: 0
|
|
1082
|
-
// Always set — $0 is intentional, not missing
|
|
1083
|
-
};
|
|
1084
|
-
if (defaultPlan && defaultPlan.type !== "exclude") {
|
|
1085
|
-
tracked.planName = defaultPlan.name;
|
|
1086
|
-
if (defaultPlan.type === "flat" && defaultPlan.monthlyBase !== void 0) {
|
|
1087
|
-
tracked.planCost = defaultPlan.monthlyBase;
|
|
1088
|
-
tracked.budget = defaultPlan.monthlyBase;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
1092
|
-
if (existingKey) {
|
|
1093
|
-
tracked.hasApiKey = true;
|
|
1094
|
-
} else {
|
|
1095
|
-
for (const pattern of service.envPatterns) {
|
|
1096
|
-
if (process.env[pattern]) {
|
|
1097
|
-
tracked.hasApiKey = true;
|
|
1098
|
-
if (!globalConfig.services[service.id]) {
|
|
1099
|
-
globalConfig.services[service.id] = {};
|
|
1100
|
-
}
|
|
1101
|
-
globalConfig.services[service.id].apiKey = process.env[pattern];
|
|
1102
|
-
break;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1145
|
+
if (!config.services[det.service.id]) {
|
|
1146
|
+
config.services[det.service.id] = {
|
|
1147
|
+
serviceId: det.service.id,
|
|
1148
|
+
detectedVia: det.sources,
|
|
1149
|
+
hasApiKey: false,
|
|
1150
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1151
|
+
budget: 0
|
|
1152
|
+
};
|
|
1105
1153
|
}
|
|
1106
|
-
config.services[service.id] = tracked;
|
|
1107
1154
|
}
|
|
1108
|
-
|
|
1109
|
-
if (detected.length === 0) {
|
|
1110
|
-
console.log(" No paid services detected yet.");
|
|
1111
|
-
console.log(" Services will be detected as they enter your project.\n");
|
|
1112
|
-
} else {
|
|
1113
|
-
console.log(` Found ${detected.length} paid service${detected.length > 1 ? "s" : ""}:
|
|
1155
|
+
console.log(` Registered ${detected.length} services (non-interactive).
|
|
1114
1156
|
`);
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
console.log(` ${det.service.name}${planStr} [${tierBadge}]${budgetStr}`);
|
|
1122
|
-
if (svc?.budget === 0 && det.service.apiTier !== "calc") {
|
|
1123
|
-
needBudget.push(det.service.id);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
console.log("");
|
|
1127
|
-
if (needBudget.length > 0) {
|
|
1128
|
-
console.log(` ${needBudget.length} service${needBudget.length > 1 ? "s" : ""} need budgets. Set them with:`);
|
|
1129
|
-
for (const id of needBudget) {
|
|
1130
|
-
console.log(` burnwatch add ${id} --budget <AMOUNT>`);
|
|
1131
|
-
}
|
|
1132
|
-
console.log("");
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1157
|
+
} else if (process.stdin.isTTY) {
|
|
1158
|
+
const result = await runInteractiveInit(detected);
|
|
1159
|
+
config.services = result.services;
|
|
1160
|
+
} else {
|
|
1161
|
+
const result = autoConfigureServices(detected);
|
|
1162
|
+
config.services = result.services;
|
|
1135
1163
|
}
|
|
1136
1164
|
writeProjectConfig(config, projectRoot);
|
|
1137
1165
|
const gitignorePath = path5.join(projectConfigDir(projectRoot), ".gitignore");
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/core/config.ts","../src/detection/detector.ts","../src/core/registry.ts","../src/services/base.ts","../src/services/anthropic.ts","../src/services/openai.ts","../src/services/vercel.ts","../src/services/scrapfly.ts","../src/services/index.ts","../src/core/types.ts","../src/core/brief.ts","../src/core/ledger.ts","../src/interactive-init.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * burnwatch CLI\n *\n * Usage:\n * burnwatch init — Initialize in current project\n * burnwatch add <service> [options] — Register a service\n * burnwatch status — Show current spend brief\n * burnwatch reconcile — Scan for untracked sessions\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n ensureProjectDirs,\n readProjectConfig,\n writeProjectConfig,\n readGlobalConfig,\n writeGlobalConfig,\n projectConfigDir,\n isInitialized,\n} from \"./core/config.js\";\nimport type { ProjectConfig } from \"./core/config.js\";\nimport type { TrackedService } from \"./core/types.js\";\nimport { detectServices } from \"./detection/detector.js\";\nimport { pollAllServices } from \"./services/index.js\";\nimport { buildSnapshot, buildBrief, formatBrief } from \"./core/brief.js\";\nimport { writeLedger, saveSnapshot } from \"./core/ledger.js\";\nimport { getService, getAllServices } from \"./core/registry.js\";\nimport { runInteractiveInit } from \"./interactive-init.js\";\n\nconst args = process.argv.slice(2);\nconst command = args[0];\nconst flags = new Set(args.slice(1));\n\nasync function main(): Promise<void> {\n switch (command) {\n case \"init\":\n case \"setup\":\n await cmdInit();\n break;\n case \"add\":\n await cmdAdd();\n break;\n case \"status\":\n await cmdStatus();\n break;\n case \"services\":\n cmdServices();\n break;\n case \"reconcile\":\n await cmdReconcile();\n break;\n case \"help\":\n case \"--help\":\n case \"-h\":\n cmdHelp();\n break;\n case \"version\":\n case \"--version\":\n case \"-v\":\n cmdVersion();\n break;\n default:\n if (command) {\n console.error(`Unknown command: ${command}`);\n console.error('Run \"burnwatch help\" for usage.');\n process.exit(1);\n }\n cmdHelp();\n }\n}\n\n// --- Commands ---\n\nasync function cmdInit(): Promise<void> {\n const projectRoot = process.cwd();\n const nonInteractive = flags.has(\"--non-interactive\") || flags.has(\"--ni\");\n const alreadyInitialized = isInitialized(projectRoot);\n\n // Detect project name from package.json\n let projectName = path.basename(projectRoot);\n try {\n const pkgPath = path.join(projectRoot, \"package.json\");\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n name?: string;\n };\n if (pkg.name) projectName = pkg.name;\n } catch {\n // Use directory name\n }\n\n // Create directories (idempotent)\n ensureProjectDirs(projectRoot);\n\n // Run detection\n console.log(\"🔍 Scanning project for paid services...\\n\");\n const detected = detectServices(projectRoot);\n\n // Load existing config or create new one\n const existingConfig = alreadyInitialized ? readProjectConfig(projectRoot) : null;\n const config: ProjectConfig = {\n projectName: existingConfig?.projectName ?? projectName,\n services: existingConfig?.services ?? {},\n createdAt: existingConfig?.createdAt ?? new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n if (!nonInteractive && detected.length > 0 && process.stdin.isTTY) {\n // Interactive mode — walk through each service with plan tiers\n const result = await runInteractiveInit(detected);\n config.services = result.services;\n } else {\n // Non-interactive mode — auto-register with default plans, always set budget\n const globalConfig = readGlobalConfig();\n\n for (const det of detected) {\n const existing = config.services[det.service.id];\n const service = det.service;\n const plans = service.plans ?? [];\n const defaultPlan = plans.find((p) => p.default) ?? plans[0];\n\n // Skip services that already have budgets configured\n if (existing?.budget !== undefined && existing.budget > 0) continue;\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: existing?.detectedVia ?? det.sources,\n hasApiKey: existing?.hasApiKey ?? false,\n firstDetected: existing?.firstDetected ?? new Date().toISOString(),\n budget: 0, // Always set — $0 is intentional, not missing\n };\n\n if (defaultPlan && defaultPlan.type !== \"exclude\") {\n tracked.planName = defaultPlan.name;\n\n if (defaultPlan.type === \"flat\" && defaultPlan.monthlyBase !== undefined) {\n tracked.planCost = defaultPlan.monthlyBase;\n tracked.budget = defaultPlan.monthlyBase; // $0 for free, plan cost for paid\n }\n }\n\n // Check for existing API key in global config or environment\n const existingKey = globalConfig.services[service.id]?.apiKey;\n if (existingKey) {\n tracked.hasApiKey = true;\n } else {\n // Check env vars\n for (const pattern of service.envPatterns) {\n if (process.env[pattern]) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = process.env[pattern]!;\n break;\n }\n }\n }\n\n config.services[service.id] = tracked;\n }\n\n // Save any discovered keys\n writeGlobalConfig(globalConfig);\n\n // Report findings\n if (detected.length === 0) {\n console.log(\" No paid services detected yet.\");\n console.log(' Services will be detected as they enter your project.\\n');\n } else {\n console.log(` Found ${detected.length} paid service${detected.length > 1 ? \"s\" : \"\"}:\\n`);\n\n const needBudget: string[] = [];\n for (const det of detected) {\n const svc = config.services[det.service.id];\n const tierBadge =\n det.service.apiTier === \"live\"\n ? svc?.hasApiKey ? \"LIVE\" : \"BLIND\"\n : det.service.apiTier === \"calc\"\n ? \"CALC\"\n : det.service.apiTier === \"est\"\n ? \"EST\"\n : \"BLIND\";\n\n const planStr = svc?.planName ? ` (${svc.planName})` : \"\";\n const budgetStr = svc?.budget !== undefined ? ` $${svc.budget}/mo` : \"\";\n console.log(` ${det.service.name}${planStr} [${tierBadge}]${budgetStr}`);\n\n if (svc?.budget === 0 && det.service.apiTier !== \"calc\") {\n needBudget.push(det.service.id);\n }\n }\n console.log(\"\");\n\n if (needBudget.length > 0) {\n console.log(` ${needBudget.length} service${needBudget.length > 1 ? \"s\" : \"\"} need budgets. Set them with:`);\n for (const id of needBudget) {\n console.log(` burnwatch add ${id} --budget <AMOUNT>`);\n }\n console.log(\"\");\n }\n }\n }\n\n writeProjectConfig(config, projectRoot);\n\n // Write initial .gitignore for the .burnwatch directory\n const gitignorePath = path.join(projectConfigDir(projectRoot), \".gitignore\");\n fs.writeFileSync(\n gitignorePath,\n [\n \"# Burnwatch — ignore cache and snapshots, keep ledger and config\",\n \"data/cache/\",\n \"data/snapshots/\",\n \"data/events.jsonl\",\n \"\",\n ].join(\"\\n\"),\n \"utf-8\",\n );\n\n // Register Claude Code hooks\n console.log(\"\\n🔗 Registering Claude Code hooks...\\n\");\n registerHooks(projectRoot);\n\n console.log(\"\\nburnwatch initialized.\\n\");\n console.log(\"Next steps:\");\n console.log(\" burnwatch status Show current spend\");\n console.log(\" burnwatch add <svc> Update a service's budget or API key\");\n console.log(\" burnwatch init Re-run this setup anytime\\n\");\n}\n\nasync function cmdAdd(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n console.error('❌ burnwatch not initialized. Run \"burnwatch init\" first.');\n process.exit(1);\n }\n\n const serviceId = args[1];\n if (!serviceId) {\n console.error(\"Usage: burnwatch add <service> [--key KEY] [--budget N]\");\n process.exit(1);\n }\n\n // Parse options\n const options: Record<string, string> = {};\n for (let i = 2; i < args.length; i++) {\n const arg = args[i]!;\n if (arg.startsWith(\"--\") && i + 1 < args.length) {\n options[arg.slice(2)] = args[i + 1]!;\n i++;\n }\n }\n\n const apiKey = options[\"key\"] ?? options[\"token\"];\n const budget = options[\"budget\"] ? parseFloat(options[\"budget\"]) : undefined;\n const planCost = options[\"plan-cost\"]\n ? parseFloat(options[\"plan-cost\"])\n : undefined;\n\n // Check if service is in registry\n const definition = getService(serviceId, projectRoot);\n if (!definition) {\n console.error(\n `⚠️ \"${serviceId}\" not found in registry. Adding as custom service.`,\n );\n }\n\n // Update project config\n const config = readProjectConfig(projectRoot)!;\n const existing = config.services[serviceId];\n\n const tracked: TrackedService = {\n serviceId,\n detectedVia: existing?.detectedVia ?? [\"manual\"],\n budget: budget ?? existing?.budget,\n hasApiKey: !!apiKey || (existing?.hasApiKey ?? false),\n planCost: planCost ?? existing?.planCost,\n firstDetected: existing?.firstDetected ?? new Date().toISOString(),\n };\n\n config.services[serviceId] = tracked;\n writeProjectConfig(config, projectRoot);\n\n // Save API key to global config (never in project dir)\n if (apiKey) {\n const globalConfig = readGlobalConfig();\n if (!globalConfig.services[serviceId]) {\n globalConfig.services[serviceId] = {};\n }\n globalConfig.services[serviceId]!.apiKey = apiKey;\n writeGlobalConfig(globalConfig);\n console.log(`🔐 API key saved to global config (never stored in project)`);\n }\n\n let tierLabel: string;\n if (!definition) {\n tierLabel = \"🔴 BLIND\";\n } else if (apiKey) {\n tierLabel = \"✅ LIVE\";\n } else if (planCost !== undefined) {\n tierLabel = \"🟡 CALC\";\n } else if (definition.apiTier === \"est\") {\n tierLabel = \"🟠 EST\";\n } else if (definition.apiTier === \"calc\") {\n tierLabel = \"🟡 CALC\";\n } else if (definition.apiTier === \"live\" && !apiKey) {\n tierLabel = `🔴 BLIND (add --key for ✅ LIVE)`;\n } else {\n tierLabel = \"🔴 BLIND\";\n }\n\n console.log(`\\n✅ ${serviceId} configured:`);\n console.log(` Tier: ${tierLabel}`);\n if (budget) console.log(` Budget: $${budget}/mo`);\n if (planCost) console.log(` Plan cost: $${planCost}/mo`);\n console.log(\"\");\n}\n\nasync function cmdStatus(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n console.error('❌ burnwatch not initialized. Run \"burnwatch init\" first.');\n process.exit(1);\n }\n\n const config = readProjectConfig(projectRoot)!;\n const trackedServices = Object.values(config.services);\n\n if (trackedServices.length === 0) {\n console.log(\"No services tracked yet.\");\n console.log('Run \"burnwatch add <service>\" to start tracking.');\n return;\n }\n\n console.log(\"📊 Polling services...\\n\");\n\n const results = await pollAllServices(trackedServices);\n const snapshots = results.map((r) =>\n buildSnapshot(\n r.serviceId,\n r.tier,\n r.spend,\n config.services[r.serviceId]?.budget,\n ),\n );\n\n const blindCount = snapshots.filter((s) => s.tier === \"blind\").length;\n const brief = buildBrief(config.projectName, snapshots, blindCount);\n\n // Save snapshot and update ledger\n saveSnapshot(brief, projectRoot);\n writeLedger(brief, projectRoot);\n\n // Display the brief\n console.log(formatBrief(brief));\n console.log(\"\");\n\n if (blindCount > 0) {\n console.log(`⚠️ ${blindCount} service${blindCount > 1 ? \"s\" : \"\"} untracked:`);\n for (const snap of snapshots.filter((s) => s.tier === \"blind\")) {\n console.log(` • ${snap.serviceId}`);\n }\n console.log(`\\n Run 'burnwatch init' to configure budgets and API keys.\\n`);\n }\n}\n\nasync function cmdSetup(): Promise<void> {\n const projectRoot = process.cwd();\n\n // Step 1: Init if needed\n if (!isInitialized(projectRoot)) {\n await cmdInit();\n }\n\n const config = readProjectConfig(projectRoot)!;\n const detected = Object.values(config.services);\n\n if (detected.length === 0) {\n console.log(\"No paid services detected. You're all set!\");\n return;\n }\n\n console.log(\"📋 Auto-configuring detected services...\\n\");\n\n // Step 2: Check global config for existing API keys\n const globalConfig = readGlobalConfig();\n\n // Step 3: Auto-configure each service based on registry tier + available keys\n const liveServices: string[] = [];\n const calcServices: string[] = [];\n const estServices: string[] = [];\n const blindServices: string[] = [];\n\n for (const tracked of detected) {\n const definition = getService(tracked.serviceId, projectRoot);\n if (!definition) continue;\n\n const hasKey = !!globalConfig.services[tracked.serviceId]?.apiKey;\n\n if (hasKey && definition.apiTier === \"live\") {\n tracked.hasApiKey = true;\n liveServices.push(`${definition.name}`);\n } else if (definition.apiTier === \"calc\") {\n calcServices.push(`${definition.name}`);\n } else if (definition.apiTier === \"est\") {\n estServices.push(`${definition.name}`);\n } else {\n blindServices.push(`${definition.name}`);\n }\n }\n\n writeProjectConfig(config, projectRoot);\n\n // Report\n if (liveServices.length > 0) {\n console.log(` ✅ LIVE (real billing data): ${liveServices.join(\", \")}`);\n }\n if (calcServices.length > 0) {\n console.log(` 🟡 CALC (flat-rate tracking): ${calcServices.join(\", \")}`);\n }\n if (estServices.length > 0) {\n console.log(` 🟠 EST (estimated from usage): ${estServices.join(\", \")}`);\n }\n if (blindServices.length > 0) {\n console.log(` 🔴 BLIND (detected, need API key): ${blindServices.join(\", \")}`);\n }\n\n console.log(\"\");\n\n if (blindServices.length > 0) {\n console.log(\"To upgrade BLIND services to LIVE, add API keys:\");\n for (const tracked of detected) {\n const definition = getService(tracked.serviceId, projectRoot);\n if (definition?.apiTier === \"live\" && !tracked.hasApiKey) {\n const envHint = definition.envPatterns[0] ?? \"YOUR_KEY\";\n console.log(` burnwatch add ${tracked.serviceId} --key $${envHint} --budget <N>`);\n }\n }\n console.log(\"\");\n }\n\n console.log(\"To set budgets for any service:\");\n console.log(\" burnwatch add <service> --budget <monthly_amount>\");\n console.log(\"\");\n console.log(\"Or use /setup-burnwatch in Claude Code for guided setup with budget suggestions.\\n\");\n\n // Show brief\n await cmdStatus();\n}\n\nfunction cmdServices(): void {\n const services = getAllServices();\n console.log(`\\n📋 Registry: ${services.length} services available\\n`);\n\n for (const svc of services) {\n const tierBadge =\n svc.apiTier === \"live\"\n ? \"✅ LIVE\"\n : svc.apiTier === \"calc\"\n ? \"🟡 CALC\"\n : svc.apiTier === \"est\"\n ? \"🟠 EST\"\n : \"🔴 BLIND\";\n\n console.log(` ${svc.name.padEnd(24)} ${tierBadge.padEnd(10)} ${svc.billingModel}`);\n }\n\n console.log(\"\");\n}\n\nasync function cmdReconcile(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n console.error('❌ burnwatch not initialized. Run \"burnwatch init\" first.');\n process.exit(1);\n }\n\n console.log(\"🔍 Scanning for untracked services and missed sessions...\\n\");\n\n // Re-run detection against current project state\n const detected = detectServices(projectRoot);\n const config = readProjectConfig(projectRoot)!;\n let newCount = 0;\n\n for (const det of detected) {\n if (!config.services[det.service.id]) {\n config.services[det.service.id] = {\n serviceId: det.service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n };\n newCount++;\n console.log(` 🆕 ${det.service.name} — detected via ${det.details.join(\", \")}`);\n }\n }\n\n if (newCount > 0) {\n writeProjectConfig(config, projectRoot);\n console.log(\n `\\n✅ Found ${newCount} new service${newCount > 1 ? \"s\" : \"\"}. Run 'burnwatch status' to see updated brief.`,\n );\n } else {\n console.log(\" ✅ No new services found. All services already tracked.\");\n }\n\n console.log(\"\");\n}\n\nfunction cmdHelp(): void {\n console.log(`\nburnwatch — Passive cost memory for vibe coding\n\nUsage:\n burnwatch init Interactive setup — pick plans per service\n burnwatch init --non-interactive Auto-detect services, no prompts\n burnwatch setup Init + auto-configure all detected services\n burnwatch add <service> [options] Register a service for tracking\n burnwatch status Show current spend brief\n burnwatch services List all services in registry\n burnwatch reconcile Scan for untracked services\n\nOptions for 'add':\n --key <API_KEY> API key for LIVE tracking (saved to ~/.config/burnwatch/)\n --token <TOKEN> Same as --key (alias)\n --budget <AMOUNT> Monthly budget in USD\n --plan-cost <AMOUNT> Monthly plan cost for CALC tracking\n\nExamples:\n burnwatch init\n burnwatch init --non-interactive\n burnwatch add anthropic --key sk-ant-admin-xxx --budget 100\n burnwatch add scrapfly --key scp-xxx --budget 50\n burnwatch add posthog --plan-cost 0 --budget 0\n burnwatch status\n`);\n}\n\nfunction cmdVersion(): void {\n try {\n const pkgPath = path.resolve(\n path.dirname(new URL(import.meta.url).pathname),\n \"../package.json\",\n );\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n version: string;\n };\n console.log(`burnwatch v${pkg.version}`);\n } catch {\n console.log(\"burnwatch v0.1.0\");\n }\n}\n\n// --- Hook Registration ---\n\nfunction registerHooks(projectRoot: string): void {\n // Step 1: Copy hook scripts into .burnwatch/hooks/ for durability.\n // This avoids relying on ephemeral npx cache paths.\n const sourceHooksDir = path.resolve(\n path.dirname(new URL(import.meta.url).pathname),\n \"hooks\",\n );\n const localHooksDir = path.join(projectRoot, \".burnwatch\", \"hooks\");\n fs.mkdirSync(localHooksDir, { recursive: true });\n\n const hookFiles = [\n \"on-session-start.js\",\n \"on-prompt.js\",\n \"on-file-change.js\",\n \"on-stop.js\",\n ];\n\n for (const file of hookFiles) {\n const src = path.join(sourceHooksDir, file);\n const dest = path.join(localHooksDir, file);\n try {\n fs.copyFileSync(src, dest);\n // Also copy sourcemaps if they exist\n const mapSrc = src + \".map\";\n if (fs.existsSync(mapSrc)) {\n fs.copyFileSync(mapSrc, dest + \".map\");\n }\n } catch (err) {\n console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);\n }\n }\n\n console.log(` Hook scripts copied to ${localHooksDir}`);\n\n // Step 2: Find or create .claude/settings.json — MERGE, never overwrite\n const claudeDir = path.join(projectRoot, \".claude\");\n const settingsPath = path.join(claudeDir, \"settings.json\");\n\n fs.mkdirSync(claudeDir, { recursive: true });\n\n // Read existing settings (preserve everything)\n let settings: Record<string, unknown> = {};\n try {\n const existing = fs.readFileSync(settingsPath, \"utf-8\");\n settings = JSON.parse(existing) as Record<string, unknown>;\n console.log(` Merging into existing ${settingsPath}`);\n } catch {\n // No existing settings — start fresh\n }\n\n // Ensure hooks object exists, preserve all existing hooks\n if (!settings[\"hooks\"] || typeof settings[\"hooks\"] !== \"object\") {\n settings[\"hooks\"] = {};\n }\n const hooks = settings[\"hooks\"] as Record<string, unknown[]>;\n\n // Use the local .burnwatch/hooks/ paths (durable, not ephemeral)\n const hooksDir = localHooksDir;\n\n // SessionStart hook\n if (!hooks[\"SessionStart\"]) hooks[\"SessionStart\"] = [];\n addHookIfMissing(hooks[\"SessionStart\"] as unknown[], \"SessionStart\", {\n matcher: \"startup|resume\",\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-session-start.js\")}\"`,\n timeout: 15,\n },\n ],\n });\n\n // UserPromptSubmit hook\n if (!hooks[\"UserPromptSubmit\"]) hooks[\"UserPromptSubmit\"] = [];\n addHookIfMissing(\n hooks[\"UserPromptSubmit\"] as unknown[],\n \"UserPromptSubmit\",\n {\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-prompt.js\")}\"`,\n timeout: 5,\n },\n ],\n },\n );\n\n // PostToolUse hook (Edit|Write only)\n if (!hooks[\"PostToolUse\"]) hooks[\"PostToolUse\"] = [];\n addHookIfMissing(hooks[\"PostToolUse\"] as unknown[], \"PostToolUse\", {\n matcher: \"Edit|Write\",\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-file-change.js\")}\"`,\n timeout: 5,\n },\n ],\n });\n\n // Stop hook (async — don't block session end)\n if (!hooks[\"Stop\"]) hooks[\"Stop\"] = [];\n addHookIfMissing(hooks[\"Stop\"] as unknown[], \"Stop\", {\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-stop.js\")}\"`,\n timeout: 15,\n async: true,\n },\n ],\n });\n\n settings[\"hooks\"] = hooks;\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\", \"utf-8\");\n console.log(` Hooks registered in ${settingsPath}`);\n}\n\nfunction addHookIfMissing(\n hookArray: unknown[],\n _eventName: string,\n hookConfig: unknown,\n): void {\n // Check if burnwatch hook is already registered\n const existing = hookArray.some((h) => {\n const hook = h as { hooks?: Array<{ command?: string }> };\n return hook.hooks?.some((inner) => inner.command?.includes(\"burnwatch\"));\n });\n\n if (!existing) {\n hookArray.push(hookConfig);\n }\n}\n\n// --- Entry ---\n\nmain().catch((err) => {\n console.error(\"Error:\", err instanceof Error ? err.message : err);\n process.exit(1);\n});\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { loadRegistry } from \"../core/registry.js\";\nimport type { ServiceDefinition, DetectionSource } from \"../core/types.js\";\n\nexport interface DetectionResult {\n service: ServiceDefinition;\n sources: DetectionSource[];\n details: string[];\n}\n\n/**\n * Run all detection surfaces against the current project.\n * Returns services detected via any combination of:\n * - package.json dependencies (recursive — finds monorepo subdirectories)\n * - environment variable patterns (process.env + .env* files recursive)\n * - import statement scanning (recursive from project root)\n * - (prompt mention scanning is handled separately in hooks)\n */\nexport function detectServices(projectRoot: string): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results = new Map<string, DetectionResult>();\n\n // Surface 1: Package manifest scanning (recursive — finds all package.json files)\n const pkgDeps = scanAllPackageJsons(projectRoot);\n for (const [serviceId, service] of registry) {\n const matchedPkgs = service.packageNames.filter((pkg) =>\n pkgDeps.has(pkg),\n );\n if (matchedPkgs.length > 0) {\n getOrCreate(results, serviceId, service).sources.push(\"package_json\");\n getOrCreate(results, serviceId, service).details.push(\n `package.json: ${matchedPkgs.join(\", \")}`,\n );\n }\n }\n\n // Surface 2: Environment variable pattern matching\n // Check both process.env AND .env* files in the project tree\n const envVars = collectEnvVars(projectRoot);\n for (const [serviceId, service] of registry) {\n const matchedEnvs = service.envPatterns.filter((pattern) =>\n envVars.has(pattern),\n );\n if (matchedEnvs.length > 0) {\n getOrCreate(results, serviceId, service).sources.push(\"env_var\");\n getOrCreate(results, serviceId, service).details.push(\n `env vars: ${matchedEnvs.join(\", \")}`,\n );\n }\n }\n\n // Surface 3: Import statement analysis (recursive from project root)\n const importHits = scanImports(projectRoot);\n for (const [serviceId, service] of registry) {\n const matchedImports = service.importPatterns.filter((pattern) =>\n importHits.has(pattern),\n );\n if (matchedImports.length > 0) {\n if (\n !getOrCreate(results, serviceId, service).sources.includes(\n \"import_scan\",\n )\n ) {\n getOrCreate(results, serviceId, service).sources.push(\"import_scan\");\n getOrCreate(results, serviceId, service).details.push(\n `imports: ${matchedImports.join(\", \")}`,\n );\n }\n }\n }\n\n return Array.from(results.values());\n}\n\n/**\n * Detect services mentioned in a prompt string.\n * Used by the UserPromptSubmit hook.\n */\nexport function detectMentions(\n prompt: string,\n projectRoot?: string,\n): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results: DetectionResult[] = [];\n const promptLower = prompt.toLowerCase();\n\n for (const [, service] of registry) {\n const matched = service.mentionKeywords.some((keyword) =>\n promptLower.includes(keyword.toLowerCase()),\n );\n if (matched) {\n results.push({\n service,\n sources: [\"prompt_mention\"],\n details: [`mentioned in prompt`],\n });\n }\n }\n\n return results;\n}\n\n/**\n * Detect new services introduced in a file change.\n * Used by the PostToolUse hook for Write/Edit events.\n */\nexport function detectInFileChange(\n filePath: string,\n content: string,\n projectRoot?: string,\n): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results: DetectionResult[] = [];\n const fileName = path.basename(filePath);\n\n // Check if it's a package.json change\n if (fileName === \"package.json\") {\n try {\n const pkg = JSON.parse(content) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const allDeps = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ]);\n\n for (const [, service] of registry) {\n const matched = service.packageNames.filter((p) => allDeps.has(p));\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"package_json\"],\n details: [`new dependency: ${matched.join(\", \")}`],\n });\n }\n }\n } catch {\n // Not valid JSON, skip\n }\n return results;\n }\n\n // Check if it's an env file change\n if (fileName.startsWith(\".env\")) {\n const envKeys = content\n .split(\"\\n\")\n .filter((line) => line.includes(\"=\") && !line.startsWith(\"#\"))\n .map((line) => line.split(\"=\")[0]!.trim());\n\n for (const [, service] of registry) {\n const matched = service.envPatterns.filter((p) => envKeys.includes(p));\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"env_var\"],\n details: [`new env var: ${matched.join(\", \")}`],\n });\n }\n }\n return results;\n }\n\n // Check for import statements in source files\n if (/\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {\n for (const [, service] of registry) {\n const matched = service.importPatterns.filter(\n (pattern) =>\n content.includes(`from \"${pattern}`) ||\n content.includes(`from '${pattern}`) ||\n content.includes(`require(\"${pattern}`) ||\n content.includes(`require('${pattern}`),\n );\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"import_scan\"],\n details: [`import added: ${matched.join(\", \")}`],\n });\n }\n }\n }\n\n return results;\n}\n\n// --- Helpers ---\n\nfunction getOrCreate(\n map: Map<string, DetectionResult>,\n serviceId: string,\n service: ServiceDefinition,\n): DetectionResult {\n let result = map.get(serviceId);\n if (!result) {\n result = { service, sources: [], details: [] };\n map.set(serviceId, result);\n }\n return result;\n}\n\n/**\n * Recursively find and scan ALL package.json files in the project tree.\n * Handles monorepos where dependencies live in subdirectories.\n */\nfunction scanAllPackageJsons(projectRoot: string): Set<string> {\n const deps = new Set<string>();\n const pkgFiles = findFiles(projectRoot, \"package.json\", 4);\n\n for (const pkgPath of pkgFiles) {\n try {\n const raw = fs.readFileSync(pkgPath, \"utf-8\");\n const pkg = JSON.parse(raw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n for (const name of Object.keys(pkg.dependencies ?? {})) deps.add(name);\n for (const name of Object.keys(pkg.devDependencies ?? {})) deps.add(name);\n } catch {\n // Skip malformed package.json\n }\n }\n\n return deps;\n}\n\n/**\n * Collect environment variable names from both process.env\n * and all .env* files found recursively in the project tree.\n */\nfunction collectEnvVars(projectRoot: string): Set<string> {\n const envVars = new Set(Object.keys(process.env));\n\n // Find all .env* files in the project tree\n const envFiles = findEnvFiles(projectRoot, 3);\n\n for (const envFile of envFiles) {\n try {\n const content = fs.readFileSync(envFile, \"utf-8\");\n const keys = content\n .split(\"\\n\")\n .filter((line) => line.includes(\"=\") && !line.startsWith(\"#\"))\n .map((line) => line.split(\"=\")[0]!.trim())\n .filter(Boolean);\n\n for (const key of keys) {\n envVars.add(key);\n }\n } catch {\n // Skip unreadable files\n }\n }\n\n return envVars;\n}\n\n/**\n * Find all .env* files recursively (but not in node_modules, .git, dist, etc.)\n */\nfunction findEnvFiles(dir: string, maxDepth: number): string[] {\n const results: string[] = [];\n if (maxDepth <= 0) return results;\n\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name === \"node_modules\" || entry.name === \".git\" || entry.name === \"dist\") continue;\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...findEnvFiles(fullPath, maxDepth - 1));\n } else if (entry.name.startsWith(\".env\")) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n\n return results;\n}\n\n/**\n * Find files with a specific name recursively.\n * Used to find package.json files across monorepo subdirectories.\n */\nfunction findFiles(dir: string, fileName: string, maxDepth: number): string[] {\n const results: string[] = [];\n if (maxDepth <= 0) return results;\n\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name === \"node_modules\" || entry.name === \".git\" || entry.name === \"dist\") continue;\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...findFiles(fullPath, fileName, maxDepth - 1));\n } else if (entry.name === fileName) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n\n return results;\n}\n\n/**\n * Lightweight import scanning.\n * Recursively scans the project for import/require statements.\n * Looks in src/, app/, lib/, pages/, and any other code directories.\n * Does NOT do a full AST parse — just string matching.\n */\nfunction scanImports(projectRoot: string): Set<string> {\n const imports = new Set<string>();\n\n // Scan common code directories + the root itself for source files\n const codeDirs = [\"src\", \"app\", \"lib\", \"pages\", \"components\", \"utils\", \"services\", \"hooks\"];\n const dirsToScan: string[] = [];\n\n for (const dir of codeDirs) {\n const fullPath = path.join(projectRoot, dir);\n if (fs.existsSync(fullPath)) {\n dirsToScan.push(fullPath);\n }\n }\n\n // Also check subdirectories (monorepo support)\n try {\n const entries = fs.readdirSync(projectRoot, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name === \"node_modules\" || entry.name === \".git\" || entry.name === \"dist\" || entry.name.startsWith(\".\")) continue;\n\n // Check if this subdirectory has its own package.json (monorepo package)\n const subPkgPath = path.join(projectRoot, entry.name, \"package.json\");\n if (fs.existsSync(subPkgPath)) {\n // Scan this subpackage's code directories\n for (const dir of codeDirs) {\n const fullPath = path.join(projectRoot, entry.name, dir);\n if (fs.existsSync(fullPath)) {\n dirsToScan.push(fullPath);\n }\n }\n }\n }\n } catch {\n // Skip if root is unreadable\n }\n\n for (const dir of dirsToScan) {\n const files = walkDir(dir, /\\.(ts|tsx|js|jsx|mjs|cjs)$/);\n for (const file of files) {\n try {\n const content = fs.readFileSync(file, \"utf-8\");\n // Match: import ... from \"package\" or require(\"package\")\n const importRegex =\n /(?:from\\s+[\"']|require\\s*\\(\\s*[\"'])([^./][^\"']*?)(?:[\"'])/g;\n let match: RegExpExecArray | null;\n while ((match = importRegex.exec(content)) !== null) {\n const pkg = match[1];\n if (pkg) {\n // Normalize scoped packages: @scope/pkg/subpath -> @scope/pkg\n const parts = pkg.split(\"/\");\n if (parts[0]?.startsWith(\"@\") && parts.length >= 2) {\n imports.add(`${parts[0]}/${parts[1]}`);\n } else if (parts[0]) {\n imports.add(parts[0]);\n }\n }\n }\n } catch {\n // Skip unreadable files\n }\n }\n }\n\n return imports;\n}\n\n/** Recursively walk a directory, returning files matching the pattern. */\nfunction walkDir(dir: string, pattern: RegExp, maxDepth = 5): string[] {\n const results: string[] = [];\n if (maxDepth <= 0) return results;\n\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\") continue;\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...walkDir(fullPath, pattern, maxDepth - 1));\n } else if (pattern.test(entry.name)) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n\n return results;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type { ServiceDefinition } from \"./types.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\ninterface RegistryFile {\n version: string;\n lastUpdated: string;\n services: Record<string, ServiceDefinition>;\n}\n\nlet cachedRegistry: Map<string, ServiceDefinition> | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n if (cachedRegistry) return cachedRegistry;\n\n const registry = new Map<string, ServiceDefinition>();\n\n // Load bundled registry (shipped with package)\n // Try multiple possible locations — depends on whether running from src/ or dist/\n const candidates = [\n path.resolve(__dirname, \"../../registry.json\"), // from src/core/\n path.resolve(__dirname, \"../registry.json\"), // from dist/\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n loadRegistryFile(candidate, registry);\n break;\n }\n }\n\n // Load project-local override (if exists)\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n }\n }\n\n cachedRegistry = registry;\n return registry;\n}\n\nfunction loadRegistryFile(\n filePath: string,\n registry: Map<string, ServiceDefinition>,\n): void {\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(raw) as RegistryFile;\n for (const [id, service] of Object.entries(data.services)) {\n registry.set(id, { ...service, id });\n }\n } catch {\n // Silently skip missing or malformed registry files\n }\n}\n\n/** Clear the cached registry (for testing). */\nexport function clearRegistryCache(): void {\n cachedRegistry = null;\n}\n\n/** Get a single service definition by ID. */\nexport function getService(\n id: string,\n projectRoot?: string,\n): ServiceDefinition | undefined {\n return loadRegistry(projectRoot).get(id);\n}\n\n/** Get all service definitions. */\nexport function getAllServices(\n projectRoot?: string,\n): ServiceDefinition[] {\n return Array.from(loadRegistry(projectRoot).values());\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Anthropic billing connector.\n * Uses the /v1/organizations/usage endpoint.\n * Requires an admin API key.\n */\nexport const anthropicConnector: BillingConnector = {\n serviceId: \"anthropic\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n // Get current month date range\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const startDate = startOfMonth.toISOString().split(\"T\")[0]!;\n const endDate = now.toISOString().split(\"T\")[0]!;\n\n const url = `https://api.anthropic.com/v1/organizations/usage?start_date=${startDate}&end_date=${endDate}`;\n\n const result = await fetchJson<{\n data?: Array<{ total_cost_usd?: number; spend?: number }>;\n total_cost_usd?: number;\n }>(url, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"anthropic\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Anthropic usage\",\n };\n }\n\n // Sum up usage across the period\n let totalSpend = 0;\n if (result.data.total_cost_usd !== undefined) {\n totalSpend = result.data.total_cost_usd;\n } else if (result.data.data) {\n totalSpend = result.data.data.reduce(\n (sum, entry) => sum + (entry.total_cost_usd ?? entry.spend ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"anthropic\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * OpenAI billing connector.\n * Uses the /v1/organization/costs endpoint.\n * Requires an organization-level API key.\n */\nexport const openaiConnector: BillingConnector = {\n serviceId: \"openai\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n // OpenAI uses Unix timestamps\n const startTime = Math.floor(startOfMonth.getTime() / 1000);\n\n const url = `https://api.openai.com/v1/organization/costs?start_time=${startTime}`;\n\n const result = await fetchJson<{\n data?: Array<{\n results?: Array<{\n amount?: { value?: number };\n }>;\n }>;\n object?: string;\n }>(url, {\n headers: {\n Authorization: `Bearer ${apiKey}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"openai\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch OpenAI usage\",\n };\n }\n\n // Sum all cost buckets\n let totalSpend = 0;\n if (result.data.data) {\n for (const bucket of result.data.data) {\n if (bucket.results) {\n for (const r of bucket.results) {\n totalSpend += r.amount?.value ?? 0;\n }\n }\n }\n }\n\n // OpenAI returns costs in cents, convert to dollars\n totalSpend = totalSpend / 100;\n\n return {\n serviceId: \"openai\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Vercel billing connector.\n * Uses the Vercel billing API.\n * Requires a Vercel token (personal or team-scoped).\n */\nexport const vercelConnector: BillingConnector = {\n serviceId: \"vercel\",\n\n async fetchSpend(\n token: string,\n options?: Record<string, string>,\n ): Promise<BillingResult> {\n const teamId = options?.[\"teamId\"] ?? \"\";\n const teamParam = teamId ? `?teamId=${teamId}` : \"\";\n\n // Fetch current billing period usage\n const url = `https://api.vercel.com/v2/usage${teamParam}`;\n\n const result = await fetchJson<{\n usage?: {\n total?: number;\n bandwidth?: { total?: number };\n serverlessFunctionExecution?: { total?: number };\n edgeFunctionExecution?: { total?: number };\n imageOptimization?: { total?: number };\n };\n billing?: {\n plan?: string;\n period?: { start?: string; end?: string };\n invoiceItems?: Array<{ amount?: number }>;\n };\n }>(url, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"vercel\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Vercel usage\",\n };\n }\n\n // Sum up usage costs\n let totalSpend = 0;\n if (result.data.usage?.total !== undefined) {\n totalSpend = result.data.usage.total;\n } else if (result.data.billing?.invoiceItems) {\n totalSpend = result.data.billing.invoiceItems.reduce(\n (sum, item) => sum + (item.amount ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"vercel\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Scrapfly billing connector.\n * Uses the /account endpoint which returns credits used/remaining.\n * Works with the standard API key — no special billing key needed.\n */\nexport const scrapflyConnector: BillingConnector = {\n serviceId: \"scrapfly\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const url = `https://api.scrapfly.io/account?key=${apiKey}`;\n\n const result = await fetchJson<{\n subscription?: {\n usage?: {\n scrape?: { used?: number; allowed?: number };\n };\n };\n account?: {\n credits_used?: number;\n credits_total?: number;\n };\n }>(url);\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"scrapfly\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Scrapfly account\",\n };\n }\n\n // Extract credits used from the response\n let creditsUsed = 0;\n let creditsTotal = 0;\n\n if (result.data.subscription?.usage?.scrape) {\n creditsUsed = result.data.subscription.usage.scrape.used ?? 0;\n creditsTotal = result.data.subscription.usage.scrape.allowed ?? 0;\n } else if (result.data.account) {\n creditsUsed = result.data.account.credits_used ?? 0;\n creditsTotal = result.data.account.credits_total ?? 0;\n }\n\n // Convert credits to USD at registry rate\n const creditRate = 0.00015; // $0.00015 per credit\n const spend = creditsUsed * creditRate;\n\n return {\n serviceId: \"scrapfly\",\n spend,\n isEstimate: false,\n tier: \"live\",\n raw: {\n credits_used: creditsUsed,\n credits_total: creditsTotal,\n credit_rate: creditRate,\n ...(result.data as unknown as Record<string, unknown>),\n },\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { anthropicConnector } from \"./anthropic.js\";\nimport { openaiConnector } from \"./openai.js\";\nimport { vercelConnector } from \"./vercel.js\";\nimport { scrapflyConnector } from \"./scrapfly.js\";\nimport type { TrackedService, ConfidenceTier } from \"../core/types.js\";\nimport { readGlobalConfig } from \"../core/config.js\";\nimport { getService } from \"../core/registry.js\";\n\n/** All available billing connectors, keyed by service ID. */\nconst connectors: Map<string, BillingConnector> = new Map([\n [\"anthropic\", anthropicConnector],\n [\"openai\", openaiConnector],\n [\"vercel\", vercelConnector],\n [\"scrapfly\", scrapflyConnector],\n]);\n\n/**\n * Poll spend for a single tracked service.\n * Returns the best available data based on connector availability and API keys.\n */\nexport async function pollService(\n tracked: TrackedService,\n): Promise<BillingResult> {\n const globalConfig = readGlobalConfig();\n const serviceConfig = globalConfig.services[tracked.serviceId];\n const connector = connectors.get(tracked.serviceId);\n const definition = getService(tracked.serviceId);\n\n // If we have a connector and an API key, try LIVE\n if (connector && serviceConfig?.apiKey) {\n try {\n const result = await connector.fetchSpend(\n serviceConfig.apiKey,\n serviceConfig as unknown as Record<string, string>,\n );\n if (!result.error) return result;\n // Fall through to lower tiers on error\n } catch {\n // Fall through\n }\n }\n\n // If user provided a plan cost, use CALC\n if (tracked.planCost !== undefined) {\n const now = new Date();\n const daysInMonth = new Date(\n now.getFullYear(),\n now.getMonth() + 1,\n 0,\n ).getDate();\n const dayOfMonth = now.getDate();\n const projectedSpend = (tracked.planCost / daysInMonth) * dayOfMonth;\n\n return {\n serviceId: tracked.serviceId,\n spend: projectedSpend,\n isEstimate: true,\n tier: \"calc\",\n };\n }\n\n // If service is in registry but we have no key and no plan cost\n if (definition) {\n let tier: ConfidenceTier;\n if (tracked.tierOverride) {\n tier = tracked.tierOverride;\n } else if (definition.apiTier === \"live\") {\n // Has a LIVE API but we don't have the key — mark as BLIND\n tier = \"blind\";\n } else {\n // EST, CALC, or BLIND — use the registry's declared tier\n tier = definition.apiTier;\n }\n\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: tier !== \"live\",\n tier,\n error: tier === \"blind\" ? \"No API key configured\" : undefined,\n };\n }\n\n // Completely unknown service\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: true,\n tier: \"blind\",\n error: \"Unknown service — not in registry\",\n };\n}\n\n/**\n * Poll all tracked services concurrently.\n * Returns results in the same order as input.\n */\nexport async function pollAllServices(\n services: TrackedService[],\n): Promise<BillingResult[]> {\n return Promise.all(services.map(pollService));\n}\n\nexport { type BillingConnector, type BillingResult } from \"./base.js\";\n","/**\n * Confidence tiers for spend tracking.\n *\n * LIVE — Real billing API data\n * CALC — Fixed monthly cost, user-entered\n * EST — Estimated from usage signals + pricing formula\n * BLIND — Detected in project, no tracking configured\n */\nexport type ConfidenceTier = \"live\" | \"calc\" | \"est\" | \"blind\" | \"excluded\";\n\nexport const CONFIDENCE_BADGES: Record<ConfidenceTier, string> = {\n live: \"✅ LIVE\",\n calc: \"🟡 CALC\",\n est: \"🟠 EST\",\n blind: \"🔴 BLIND\",\n excluded: \"⬚ SKIP\",\n};\n\n/** How a service charges — determines tracking strategy. */\nexport type BillingModel =\n | \"token_usage\" // Per-token (Anthropic, OpenAI, Gemini)\n | \"credit_pool\" // Fixed credit bucket (Scrapfly)\n | \"per_unit\" // Per-email, per-session, per-command (Resend, Browserbase, Upstash)\n | \"percentage\" // Percentage of transaction (Stripe)\n | \"flat_monthly\" // Fixed monthly subscription (PostHog, Inngest free tier)\n | \"tiered\" // Free up to X, then jumps (PostHog, Supabase)\n | \"compute\" // Compute-time based (Vercel, AWS)\n | \"unknown\";\n\n/** How cost scales — helps the agent reason about future spend. */\nexport type ScalingShape =\n | \"linear\" // Each unit costs the same\n | \"linear_burndown\" // Fixed pool, each use depletes it\n | \"tiered_jump\" // Free until threshold, then expensive\n | \"percentage\" // Proportional to revenue/volume\n | \"fixed\" // Flat monthly, no scaling\n | \"unknown\";\n\n/** A plan tier option for a service in the registry. */\nexport interface PlanTier {\n /** Human-readable plan name */\n name: string;\n /** Plan type: usage (pay-as-you-go), flat (fixed monthly), exclude (don't track) */\n type: \"usage\" | \"flat\" | \"exclude\";\n /** Monthly base cost for flat plans */\n monthlyBase?: number;\n /** Whether this plan requires an API key for tracking */\n requiresKey?: boolean;\n /** Whether this is the default/most common plan */\n default?: boolean;\n}\n\n/** Risk category for service grouping in interactive init. */\nexport type ServiceRiskCategory = \"llm\" | \"usage\" | \"infra\" | \"flat\";\n\n/** A service definition from the registry. */\nexport interface ServiceDefinition {\n /** Unique service identifier */\n id: string;\n /** Human-readable name */\n name: string;\n /** Package names in npm/pip that indicate this service */\n packageNames: string[];\n /** Env var patterns that indicate this service */\n envPatterns: string[];\n /** Import patterns to scan for (regex strings) */\n importPatterns: string[];\n /** Keywords that indicate mentions in prompts */\n mentionKeywords: string[];\n /** Billing model */\n billingModel: BillingModel;\n /** How cost scales */\n scalingShape: ScalingShape;\n /** What tier of tracking is available */\n apiTier: ConfidenceTier;\n /** Billing API endpoint, if available */\n apiEndpoint?: string;\n /** Pricing details */\n pricing?: {\n /** Human-readable formula */\n formula?: string;\n /** Rate per unit, if applicable */\n unitRate?: number;\n /** Unit name (token, credit, email, session, etc.) */\n unitName?: string;\n /** Monthly base cost, if flat */\n monthlyBase?: number;\n };\n /** Known gotchas that affect cost */\n gotchas?: string[];\n /** Alternative services (free or cheaper) */\n alternatives?: string[];\n /** Documentation URL */\n docsUrl?: string;\n /** Last time pricing was verified */\n lastVerified?: string;\n /** Notes about recent pricing changes */\n pricingNotes?: string;\n /** Available plan tiers for interactive init */\n plans?: PlanTier[];\n /** Whether the plan can be auto-detected from an API key */\n autoDetectPlan?: boolean;\n}\n\n/** A tracked service instance — a service definition + user config. */\nexport interface TrackedService {\n /** Service definition ID */\n serviceId: string;\n /** How this service was detected */\n detectedVia: DetectionSource[];\n /** User-configured monthly budget */\n budget?: number;\n /** Whether the user has provided an API/billing key */\n hasApiKey: boolean;\n /** Override confidence tier (e.g., user provided billing key upgrades to LIVE) */\n tierOverride?: ConfidenceTier;\n /** User-entered monthly plan cost (for CALC tier) */\n planCost?: number;\n /** When this service was first detected */\n firstDetected: string;\n /** Explicitly excluded from tracking by user */\n excluded?: boolean;\n /** Plan name selected during interactive init */\n planName?: string;\n}\n\nexport type DetectionSource =\n | \"package_json\"\n | \"env_var\"\n | \"import_scan\"\n | \"prompt_mention\"\n | \"git_diff\"\n | \"manual\";\n\n/** A spend snapshot for a single service at a point in time. */\nexport interface SpendSnapshot {\n serviceId: string;\n /** Current period spend (or estimate) */\n spend: number;\n /** Is the spend figure exact or estimated? */\n isEstimate: boolean;\n /** Confidence tier for this reading */\n tier: ConfidenceTier;\n /** Budget allocated */\n budget?: number;\n /** Percentage of budget consumed */\n budgetPercent?: number;\n /** Budget status */\n status: \"healthy\" | \"caution\" | \"over\" | \"unknown\";\n /** Human-readable status label */\n statusLabel: string;\n /** Raw data from billing API, if available */\n raw?: Record<string, unknown>;\n /** Timestamp of this snapshot */\n timestamp: string;\n}\n\n/** The full spend brief, injected at session start. */\nexport interface SpendBrief {\n projectName: string;\n generatedAt: string;\n period: string;\n services: SpendSnapshot[];\n totalSpend: number;\n totalIsEstimate: boolean;\n estimateMargin: number;\n untrackedCount: number;\n alerts: SpendAlert[];\n}\n\nexport interface SpendAlert {\n serviceId: string;\n type: \"over_budget\" | \"near_budget\" | \"new_service\" | \"stale_data\" | \"blind_service\";\n message: string;\n severity: \"warning\" | \"critical\" | \"info\";\n}\n\n/** Ledger entry — one row in spend-ledger.md */\nexport interface LedgerEntry {\n serviceId: string;\n serviceName: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n budget?: number;\n statusLabel: string;\n}\n\n/** Event logged to events.jsonl */\nexport interface SpendEvent {\n timestamp: string;\n sessionId: string;\n type:\n | \"session_start\"\n | \"session_end\"\n | \"service_detected\"\n | \"service_mentioned\"\n | \"spend_polled\"\n | \"budget_alert\"\n | \"ledger_written\"\n | \"cost_impact\";\n data: Record<string, unknown>;\n}\n\n/** A cost impact estimate for a file change. */\nexport interface CostImpact {\n serviceId: string;\n serviceName: string;\n filePath: string;\n /** Number of SDK call sites found */\n callCount: number;\n /** Detected multipliers (loops, .map(), etc.) */\n multipliers: string[];\n /** Effective multiplier applied to call count */\n multiplierFactor: number;\n /** Estimated monthly invocations */\n monthlyInvocations: number;\n /** Low estimate monthly cost */\n costLow: number;\n /** High estimate monthly cost */\n costHigh: number;\n /** Gotcha-based cost range explanation */\n rangeExplanation?: string;\n}\n\n/**\n * Hook input — the JSON received via stdin from Claude Code.\n * Subset of fields we care about.\n */\nexport interface HookInput {\n session_id: string;\n transcript_path?: string;\n cwd: string;\n hook_event_name: string;\n // SessionStart\n source?: string;\n // UserPromptSubmit\n prompt?: string;\n // PostToolUse\n tool_name?: string;\n tool_input?: {\n file_path?: string;\n command?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n };\n}\n\n/**\n * Hook output — the JSON we write to stdout for Claude Code.\n */\nexport interface HookOutput {\n hookSpecificOutput?: {\n hookEventName: string;\n additionalContext?: string;\n };\n}\n","import type {\n SpendBrief,\n SpendSnapshot,\n SpendAlert,\n ConfidenceTier,\n} from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\n\n/**\n * Format a spend brief as a text block for injection into Claude's context.\n */\nexport function formatBrief(brief: SpendBrief): string {\n const lines: string[] = [];\n const width = 62;\n const hrDouble = \"═\".repeat(width);\n const hrSingle = \"─\".repeat(width - 4);\n\n lines.push(`╔${hrDouble}╗`);\n lines.push(\n `║ BURNWATCH — ${brief.projectName} — ${brief.period}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n lines.push(`╠${hrDouble}╣`);\n\n // Header\n lines.push(\n formatRow(\"Service\", \"Spend\", \"Conf\", \"Budget\", \"Left\", width),\n );\n lines.push(`║ ${hrSingle} ║`);\n\n // Service rows\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n const leftStr = formatLeft(svc);\n\n lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));\n }\n\n // Footer\n lines.push(`╠${hrDouble}╣`);\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr = brief.estimateMargin > 0\n ? ` Est margin: ±$${brief.estimateMargin.toFixed(0)}`\n : \"\";\n const untrackedStr =\n brief.untrackedCount > 0\n ? `Untracked: ${brief.untrackedCount} ⚠️`\n : `Untracked: 0 ✅`;\n\n lines.push(\n `║ TOTAL: ${totalStr} ${untrackedStr}${marginStr}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n\n // Alerts\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(\n `║ ${icon} ${alert.message}`.padEnd(width + 1) + \"║\",\n );\n }\n\n lines.push(`╚${hrDouble}╝`);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Format a single-service spend card for injection on mention.\n */\nexport function formatSpendCard(snapshot: SpendSnapshot): string {\n const badge = CONFIDENCE_BADGES[snapshot.tier];\n const spendStr = snapshot.isEstimate\n ? `~$${snapshot.spend.toFixed(2)}`\n : `$${snapshot.spend.toFixed(2)}`;\n const budgetStr = snapshot.budget\n ? `Budget: $${snapshot.budget}`\n : \"No budget set\";\n const statusStr = snapshot.statusLabel;\n\n const lines = [\n `[BURNWATCH] ${snapshot.serviceId} — current period`,\n ` Spend: ${spendStr} | ${budgetStr} | ${statusStr}`,\n ` Confidence: ${badge}`,\n ];\n\n if (snapshot.status === \"over\" && snapshot.budgetPercent) {\n lines.push(\n ` ⚠️ ${snapshot.budgetPercent.toFixed(0)}% of budget consumed`,\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Build a SpendBrief from snapshots and project config.\n */\nexport function buildBrief(\n projectName: string,\n snapshots: SpendSnapshot[],\n blindCount: number,\n): SpendBrief {\n const now = new Date();\n const period = now.toLocaleDateString(\"en-US\", {\n month: \"long\",\n year: \"numeric\",\n });\n\n let totalSpend = 0;\n let hasEstimates = false;\n let estimateMargin = 0;\n const alerts: SpendAlert[] = [];\n\n for (const snap of snapshots) {\n totalSpend += snap.spend;\n if (snap.isEstimate) {\n hasEstimates = true;\n estimateMargin += snap.spend * 0.15; // ±15% margin on estimates\n }\n\n if (snap.status === \"over\") {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"over_budget\",\n message: `${snap.serviceId.toUpperCase()} ${snap.budgetPercent?.toFixed(0) ?? \"?\"}% OVER BUDGET — review before use`,\n severity: \"critical\",\n });\n } else if (snap.status === \"caution\" && snap.budgetPercent && snap.budgetPercent >= 80) {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"near_budget\",\n message: `${snap.serviceId} at ${snap.budgetPercent.toFixed(0)}% of budget`,\n severity: \"warning\",\n });\n }\n }\n\n if (blindCount > 0) {\n alerts.push({\n serviceId: \"_blind\",\n type: \"blind_service\",\n message: `${blindCount} service${blindCount > 1 ? \"s\" : \"\"} detected but untracked - run 'burnwatch init' to configure`,\n severity: \"warning\",\n });\n }\n\n return {\n projectName,\n generatedAt: now.toISOString(),\n period,\n services: snapshots,\n totalSpend,\n totalIsEstimate: hasEstimates,\n estimateMargin,\n untrackedCount: blindCount,\n alerts,\n };\n}\n\n// --- Helpers ---\n\nfunction formatRow(\n service: string,\n spend: string,\n conf: string,\n budget: string,\n left: string,\n width: number,\n): string {\n const row = ` ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(7)} ${budget.padEnd(7)} ${left}`;\n return `║${row}`.padEnd(width + 1) + \"║\";\n}\n\nfunction formatLeft(snap: SpendSnapshot): string {\n if (!snap.budget) return \"—\";\n if (snap.status === \"over\") return \"⚠️ OVR\";\n if (snap.budgetPercent !== undefined) {\n const remaining = 100 - snap.budgetPercent;\n return `${remaining.toFixed(0)}%`;\n }\n return \"—\";\n}\n\n/**\n * Build a SpendSnapshot from tracked service data.\n */\nexport function buildSnapshot(\n serviceId: string,\n tier: ConfidenceTier,\n spend: number,\n budget?: number,\n): SpendSnapshot {\n const isEstimate = tier === \"est\" || tier === \"calc\";\n const budgetPercent = budget ? (spend / budget) * 100 : undefined;\n\n let status: SpendSnapshot[\"status\"] = \"unknown\";\n let statusLabel = \"no budget\";\n\n if (budget) {\n if (budgetPercent! > 100) {\n status = \"over\";\n statusLabel = `⚠️ ${budgetPercent!.toFixed(0)}% over`;\n } else if (budgetPercent! >= 75) {\n status = \"caution\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — caution`;\n } else {\n status = \"healthy\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — healthy`;\n }\n }\n\n if (tier === \"calc\" && budget) {\n statusLabel = `flat — on plan`;\n status = \"healthy\";\n }\n\n return {\n serviceId,\n spend,\n isEstimate,\n tier,\n budget,\n budgetPercent,\n status,\n statusLabel,\n timestamp: new Date().toISOString(),\n };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { SpendBrief, SpendEvent } from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\nimport { projectConfigDir, projectDataDir } from \"./config.js\";\n\n/**\n * Write the spend ledger as a human-readable markdown file.\n * Designed to be git-committable and readable in 10 seconds.\n */\nexport function writeLedger(brief: SpendBrief, projectRoot?: string): void {\n const now = new Date();\n const lines: string[] = [];\n\n lines.push(`# Burnwatch Ledger — ${brief.projectName}`);\n lines.push(`Last updated: ${now.toISOString()}`);\n lines.push(\"\");\n lines.push(`## This Month (${brief.period})`);\n lines.push(\"\");\n lines.push(\"| Service | Spend | Conf | Budget | Status |\");\n lines.push(\"|---------|-------|------|--------|--------|\");\n\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n\n lines.push(\n `| ${svc.serviceId} | ${spendStr} | ${badge} | ${budgetStr} | ${svc.statusLabel} |`,\n );\n }\n\n // Add projected impact row if session impacts exist in alerts\n const impactAlert = brief.alerts.find(\n (a) => a.serviceId === \"_session_impact\",\n );\n if (impactAlert) {\n lines.push(\n `| _projected impact_ | — | 📈 EST | — | ${impactAlert.message} |`,\n );\n }\n\n lines.push(\"\");\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr =\n brief.estimateMargin > 0\n ? ` (±$${brief.estimateMargin.toFixed(0)} estimated margin)`\n : \"\";\n lines.push(`## TOTAL: ${totalStr}${marginStr}`);\n lines.push(`## Untracked services: ${brief.untrackedCount}`);\n lines.push(\"\");\n\n if (brief.alerts.length > 0) {\n lines.push(\"## Alerts\");\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(`- ${icon} ${alert.message}`);\n }\n lines.push(\"\");\n }\n\n const ledgerPath = path.join(\n projectConfigDir(projectRoot),\n \"spend-ledger.md\",\n );\n fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });\n fs.writeFileSync(ledgerPath, lines.join(\"\\n\") + \"\\n\", \"utf-8\");\n}\n\n/**\n * Append an event to the append-only event log.\n */\nexport function logEvent(event: SpendEvent, projectRoot?: string): void {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n fs.mkdirSync(path.dirname(logPath), { recursive: true });\n fs.appendFileSync(logPath, JSON.stringify(event) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Read recent events from the event log.\n */\nexport function readRecentEvents(\n count: number,\n projectRoot?: string,\n): SpendEvent[] {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n try {\n const raw = fs.readFileSync(logPath, \"utf-8\");\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n return lines\n .slice(-count)\n .map((line) => JSON.parse(line) as SpendEvent);\n } catch {\n return [];\n }\n}\n\n/**\n * Save a spend snapshot to the snapshots directory.\n * Used for delta computation across sessions.\n */\nexport function saveSnapshot(brief: SpendBrief, projectRoot?: string): void {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n fs.mkdirSync(snapshotDir, { recursive: true });\n const filename = `snapshot-${new Date().toISOString().replace(/[:.]/g, \"-\")}.json`;\n fs.writeFileSync(\n path.join(snapshotDir, filename),\n JSON.stringify(brief, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\n/**\n * Read the most recent snapshot, if any.\n */\nexport function readLatestSnapshot(\n projectRoot?: string,\n): SpendBrief | null {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n try {\n const files = fs\n .readdirSync(snapshotDir)\n .filter((f) => f.startsWith(\"snapshot-\") && f.endsWith(\".json\"))\n .sort()\n .reverse();\n\n if (files.length === 0) return null;\n\n const raw = fs.readFileSync(\n path.join(snapshotDir, files[0]!),\n \"utf-8\",\n );\n return JSON.parse(raw) as SpendBrief;\n } catch {\n return null;\n }\n}\n","/**\n * Interactive init flow for burnwatch.\n *\n * Conducts a per-service interview: detects what it can automatically\n * (existing API keys, env vars), asks for plan selection, collects\n * API keys for LIVE tracking, and ensures every service exits with\n * a budget. No skipping.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Risk categories in display order: LLMs first, then usage-based, infra, flat-rate */\nconst RISK_ORDER: ServiceRiskCategory[] = [\"llm\", \"usage\", \"infra\", \"flat\"];\n\nconst RISK_LABELS: Record<ServiceRiskCategory, string> = {\n llm: \"LLM / AI Services (highest variable cost)\",\n usage: \"Usage-Based Services\",\n infra: \"Infrastructure & Compute\",\n flat: \"Flat-Rate / Free Tier Services\",\n};\n\n/** Where to find API keys for LIVE-capable services */\nconst API_KEY_HINTS: Record<string, string> = {\n anthropic: \"Admin key: console.anthropic.com -> Settings -> Admin API Keys\",\n openai: \"Org key: platform.openai.com -> Settings -> API Keys\",\n vercel: \"Token: vercel.com/account/tokens\",\n supabase: \"Service role key: supabase.com/dashboard -> Settings -> API\",\n stripe: \"Secret key: dashboard.stripe.com -> Developers -> API Keys\",\n scrapfly: \"API key: scrapfly.io/dashboard\",\n};\n\n/** Map service IDs to risk categories */\nfunction classifyRisk(service: ServiceDefinition): ServiceRiskCategory {\n if (service.billingModel === \"token_usage\") return \"llm\";\n if (\n service.billingModel === \"credit_pool\" ||\n service.billingModel === \"percentage\" ||\n service.billingModel === \"per_unit\"\n )\n return \"usage\";\n if (service.billingModel === \"compute\") return \"infra\";\n return \"flat\";\n}\n\n/** Group detection results by risk category */\nfunction groupByRisk(\n detected: DetectionResult[],\n): Map<ServiceRiskCategory, DetectionResult[]> {\n const groups = new Map<ServiceRiskCategory, DetectionResult[]>();\n for (const cat of RISK_ORDER) {\n groups.set(cat, []);\n }\n\n for (const det of detected) {\n const cat = classifyRisk(det.service);\n groups.get(cat)!.push(det);\n }\n\n return groups;\n}\n\n/** Prompt the user with a question and return their answer */\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/** Try to auto-detect plan from Scrapfly API */\nasync function autoDetectScrapflyPlan(\n apiKey: string,\n): Promise<string | null> {\n try {\n const result = await fetchJson<{\n subscription?: { plan?: { name?: string } };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (result.ok && result.data?.subscription?.plan?.name) {\n return result.data.subscription.plan.name;\n }\n } catch {\n // Ignore errors\n }\n return null;\n}\n\n/** Scan environment for API keys matching service env patterns */\nfunction findEnvKey(service: ServiceDefinition): string | undefined {\n for (const pattern of service.envPatterns) {\n const val = process.env[pattern];\n if (val && val.length > 0) return val;\n }\n return undefined;\n}\n\nexport interface InteractiveInitResult {\n services: Record<string, TrackedService>;\n}\n\n/**\n * Run the interactive init flow.\n *\n * For each detected service:\n * 1. Ask which plan they're on\n * 2. If LIVE-capable, check for existing key or ask for one\n * 3. Set budget (defaults to plan cost, $0 for free - never skipped)\n */\nexport async function runInteractiveInit(\n detected: DetectionResult[],\n): Promise<InteractiveInitResult> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Let's configure each one.\\n`,\n );\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(`\\n ${RISK_LABELS[category]}`);\n console.log(\" \" + \"-\".repeat(48));\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans;\n\n console.log(`\\n ${service.name}`);\n console.log(` Detected via: ${det.details.join(\", \")}`);\n\n if (!plans || plans.length === 0) {\n // No plans defined - basic tracking with $0 budget\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n console.log(\" -> Configured (no plan tiers in registry, budget: $0)\");\n continue;\n }\n\n // --- Plan selection ---\n const defaultIndex = plans.findIndex((p) => p.default);\n console.log(\"\");\n for (let i = 0; i < plans.length; i++) {\n const plan = plans[i]!;\n const marker = i === defaultIndex ? \" *\" : \"\";\n const costStr =\n plan.type === \"exclude\"\n ? \"\"\n : plan.monthlyBase !== undefined\n ? ` - $${plan.monthlyBase}/mo`\n : \" - variable\";\n console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);\n }\n\n const defaultChoice =\n defaultIndex >= 0 ? String(defaultIndex + 1) : \"1\";\n const answer = await ask(\n rl,\n ` Which plan? [${defaultChoice}]: `,\n );\n\n const choiceIndex = (answer === \"\" ? parseInt(defaultChoice) : parseInt(answer)) - 1;\n const chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n\n if (chosen.type === \"exclude\") {\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n excluded: true,\n planName: chosen.name,\n };\n console.log(` -> ${service.name}: excluded`);\n continue;\n }\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n planName: chosen.name,\n };\n\n if (chosen.type === \"flat\" && chosen.monthlyBase !== undefined) {\n tracked.planCost = chosen.monthlyBase;\n }\n\n // --- API key (LIVE-capable services) ---\n if (service.apiTier === \"live\") {\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n console.log(` API key: found in global config`);\n tracked.hasApiKey = true;\n } else if (envKey) {\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n } else {\n const hint = API_KEY_HINTS[service.id];\n if (hint) console.log(` ${hint}`);\n const keyAnswer = await ask(\n rl,\n ` API key for real-time tracking (Enter to skip): `,\n );\n if (keyAnswer) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n }\n }\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\" && tracked.hasApiKey) {\n const key = globalConfig.services[service.id]?.apiKey;\n if (key) {\n console.log(\" Detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(key);\n if (planName) {\n console.log(` -> Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n }\n }\n\n // --- Budget (always set, never skip) ---\n const planCost = chosen.monthlyBase ?? 0;\n\n if (chosen.type === \"usage\" && planCost === 0) {\n // Usage-based with no fixed cost - need a real number\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget: $`,\n );\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : 0;\n } else {\n // Flat plan or usage with known base - default to plan cost\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget [$${planCost}]: $`,\n );\n if (budgetAnswer) {\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : planCost;\n } else {\n tracked.budget = planCost;\n }\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo`,\n );\n }\n }\n\n // --- Summary ---\n const tracked = Object.values(services).filter((s) => !s.excluded);\n const excluded = Object.values(services).filter((s) => s.excluded);\n const liveCount = tracked.filter((s) => s.hasApiKey).length;\n const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\"\\n \" + \"=\".repeat(48));\n console.log(` ${tracked.length} services configured`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);\n if (excluded.length > 0) console.log(` ${excluded.length} excluded`);\n console.log(` Total monthly budget: $${totalBudget}`);\n console.log(\" \" + \"=\".repeat(48));\n\n // Save any collected API keys\n writeGlobalConfig(globalConfig);\n\n rl.close();\n\n return { services };\n}\n"],"mappings":"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","names":["fs","path","fs","path","fs","path","url","url","url","url","url","fs","path","resolve","tracked"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/core/config.ts","../src/detection/detector.ts","../src/core/registry.ts","../src/services/base.ts","../src/services/anthropic.ts","../src/services/openai.ts","../src/services/vercel.ts","../src/services/scrapfly.ts","../src/services/index.ts","../src/core/types.ts","../src/core/brief.ts","../src/core/ledger.ts","../src/interactive-init.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * burnwatch CLI\n *\n * Usage:\n * burnwatch init — Initialize in current project\n * burnwatch add <service> [options] — Register a service\n * burnwatch status — Show current spend brief\n * burnwatch reconcile — Scan for untracked sessions\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n ensureProjectDirs,\n readProjectConfig,\n writeProjectConfig,\n readGlobalConfig,\n writeGlobalConfig,\n projectConfigDir,\n isInitialized,\n} from \"./core/config.js\";\nimport type { ProjectConfig } from \"./core/config.js\";\nimport type { TrackedService } from \"./core/types.js\";\nimport { detectServices } from \"./detection/detector.js\";\nimport { pollAllServices } from \"./services/index.js\";\nimport { buildSnapshot, buildBrief, formatBrief } from \"./core/brief.js\";\nimport { writeLedger, saveSnapshot } from \"./core/ledger.js\";\nimport { getService, getAllServices } from \"./core/registry.js\";\nimport { runInteractiveInit, autoConfigureServices } from \"./interactive-init.js\";\n\nconst args = process.argv.slice(2);\nconst command = args[0];\nconst flags = new Set(args.slice(1));\n\nasync function main(): Promise<void> {\n switch (command) {\n case \"init\":\n case \"setup\":\n await cmdInit();\n break;\n case \"add\":\n await cmdAdd();\n break;\n case \"status\":\n await cmdStatus();\n break;\n case \"services\":\n cmdServices();\n break;\n case \"reconcile\":\n await cmdReconcile();\n break;\n case \"help\":\n case \"--help\":\n case \"-h\":\n cmdHelp();\n break;\n case \"version\":\n case \"--version\":\n case \"-v\":\n cmdVersion();\n break;\n default:\n if (command) {\n console.error(`Unknown command: ${command}`);\n console.error('Run \"burnwatch help\" for usage.');\n process.exit(1);\n }\n cmdHelp();\n }\n}\n\n// --- Commands ---\n\nasync function cmdInit(): Promise<void> {\n const projectRoot = process.cwd();\n const nonInteractive = flags.has(\"--non-interactive\") || flags.has(\"--ni\");\n const alreadyInitialized = isInitialized(projectRoot);\n\n // Detect project name from package.json\n let projectName = path.basename(projectRoot);\n try {\n const pkgPath = path.join(projectRoot, \"package.json\");\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n name?: string;\n };\n if (pkg.name) projectName = pkg.name;\n } catch {\n // Use directory name\n }\n\n // Create directories (idempotent)\n ensureProjectDirs(projectRoot);\n\n // Run detection\n console.log(\"🔍 Scanning project for paid services...\\n\");\n const detected = detectServices(projectRoot);\n\n // Load existing config or create new one\n const existingConfig = alreadyInitialized ? readProjectConfig(projectRoot) : null;\n const config: ProjectConfig = {\n projectName: existingConfig?.projectName ?? projectName,\n services: existingConfig?.services ?? {},\n createdAt: existingConfig?.createdAt ?? new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n if (detected.length === 0) {\n console.log(\" No paid services detected yet.\");\n console.log(\" Services will be detected as they enter your project.\\n\");\n } else if (nonInteractive) {\n // Explicit --non-interactive: minimal auto-register (CI/scripts)\n for (const det of detected) {\n if (!config.services[det.service.id]) {\n config.services[det.service.id] = {\n serviceId: det.service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n }\n }\n console.log(` Registered ${detected.length} services (non-interactive).\\n`);\n } else if (process.stdin.isTTY) {\n // TTY: full interactive interview with readline prompts\n const result = await runInteractiveInit(detected);\n config.services = result.services;\n } else {\n // No TTY (Claude Code, piped): auto-configure with smart defaults\n const result = autoConfigureServices(detected);\n config.services = result.services;\n }\n\n writeProjectConfig(config, projectRoot);\n\n // Write initial .gitignore for the .burnwatch directory\n const gitignorePath = path.join(projectConfigDir(projectRoot), \".gitignore\");\n fs.writeFileSync(\n gitignorePath,\n [\n \"# Burnwatch — ignore cache and snapshots, keep ledger and config\",\n \"data/cache/\",\n \"data/snapshots/\",\n \"data/events.jsonl\",\n \"\",\n ].join(\"\\n\"),\n \"utf-8\",\n );\n\n // Register Claude Code hooks\n console.log(\"\\n🔗 Registering Claude Code hooks...\\n\");\n registerHooks(projectRoot);\n\n console.log(\"\\nburnwatch initialized.\\n\");\n console.log(\"Next steps:\");\n console.log(\" burnwatch status Show current spend\");\n console.log(\" burnwatch add <svc> Update a service's budget or API key\");\n console.log(\" burnwatch init Re-run this setup anytime\\n\");\n}\n\nasync function cmdAdd(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n console.error('❌ burnwatch not initialized. Run \"burnwatch init\" first.');\n process.exit(1);\n }\n\n const serviceId = args[1];\n if (!serviceId) {\n console.error(\"Usage: burnwatch add <service> [--key KEY] [--budget N]\");\n process.exit(1);\n }\n\n // Parse options\n const options: Record<string, string> = {};\n for (let i = 2; i < args.length; i++) {\n const arg = args[i]!;\n if (arg.startsWith(\"--\") && i + 1 < args.length) {\n options[arg.slice(2)] = args[i + 1]!;\n i++;\n }\n }\n\n const apiKey = options[\"key\"] ?? options[\"token\"];\n const budget = options[\"budget\"] ? parseFloat(options[\"budget\"]) : undefined;\n const planCost = options[\"plan-cost\"]\n ? parseFloat(options[\"plan-cost\"])\n : undefined;\n\n // Check if service is in registry\n const definition = getService(serviceId, projectRoot);\n if (!definition) {\n console.error(\n `⚠️ \"${serviceId}\" not found in registry. Adding as custom service.`,\n );\n }\n\n // Update project config\n const config = readProjectConfig(projectRoot)!;\n const existing = config.services[serviceId];\n\n const tracked: TrackedService = {\n serviceId,\n detectedVia: existing?.detectedVia ?? [\"manual\"],\n budget: budget ?? existing?.budget,\n hasApiKey: !!apiKey || (existing?.hasApiKey ?? false),\n planCost: planCost ?? existing?.planCost,\n firstDetected: existing?.firstDetected ?? new Date().toISOString(),\n };\n\n config.services[serviceId] = tracked;\n writeProjectConfig(config, projectRoot);\n\n // Save API key to global config (never in project dir)\n if (apiKey) {\n const globalConfig = readGlobalConfig();\n if (!globalConfig.services[serviceId]) {\n globalConfig.services[serviceId] = {};\n }\n globalConfig.services[serviceId]!.apiKey = apiKey;\n writeGlobalConfig(globalConfig);\n console.log(`🔐 API key saved to global config (never stored in project)`);\n }\n\n let tierLabel: string;\n if (!definition) {\n tierLabel = \"🔴 BLIND\";\n } else if (apiKey) {\n tierLabel = \"✅ LIVE\";\n } else if (planCost !== undefined) {\n tierLabel = \"🟡 CALC\";\n } else if (definition.apiTier === \"est\") {\n tierLabel = \"🟠 EST\";\n } else if (definition.apiTier === \"calc\") {\n tierLabel = \"🟡 CALC\";\n } else if (definition.apiTier === \"live\" && !apiKey) {\n tierLabel = `🔴 BLIND (add --key for ✅ LIVE)`;\n } else {\n tierLabel = \"🔴 BLIND\";\n }\n\n console.log(`\\n✅ ${serviceId} configured:`);\n console.log(` Tier: ${tierLabel}`);\n if (budget) console.log(` Budget: $${budget}/mo`);\n if (planCost) console.log(` Plan cost: $${planCost}/mo`);\n console.log(\"\");\n}\n\nasync function cmdStatus(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n console.error('❌ burnwatch not initialized. Run \"burnwatch init\" first.');\n process.exit(1);\n }\n\n const config = readProjectConfig(projectRoot)!;\n const trackedServices = Object.values(config.services);\n\n if (trackedServices.length === 0) {\n console.log(\"No services tracked yet.\");\n console.log('Run \"burnwatch add <service>\" to start tracking.');\n return;\n }\n\n console.log(\"📊 Polling services...\\n\");\n\n const results = await pollAllServices(trackedServices);\n const snapshots = results.map((r) =>\n buildSnapshot(\n r.serviceId,\n r.tier,\n r.spend,\n config.services[r.serviceId]?.budget,\n ),\n );\n\n const blindCount = snapshots.filter((s) => s.tier === \"blind\").length;\n const brief = buildBrief(config.projectName, snapshots, blindCount);\n\n // Save snapshot and update ledger\n saveSnapshot(brief, projectRoot);\n writeLedger(brief, projectRoot);\n\n // Display the brief\n console.log(formatBrief(brief));\n console.log(\"\");\n\n if (blindCount > 0) {\n console.log(`⚠️ ${blindCount} service${blindCount > 1 ? \"s\" : \"\"} untracked:`);\n for (const snap of snapshots.filter((s) => s.tier === \"blind\")) {\n console.log(` • ${snap.serviceId}`);\n }\n console.log(`\\n Run 'burnwatch init' to configure budgets and API keys.\\n`);\n }\n}\n\nasync function cmdSetup(): Promise<void> {\n const projectRoot = process.cwd();\n\n // Step 1: Init if needed\n if (!isInitialized(projectRoot)) {\n await cmdInit();\n }\n\n const config = readProjectConfig(projectRoot)!;\n const detected = Object.values(config.services);\n\n if (detected.length === 0) {\n console.log(\"No paid services detected. You're all set!\");\n return;\n }\n\n console.log(\"📋 Auto-configuring detected services...\\n\");\n\n // Step 2: Check global config for existing API keys\n const globalConfig = readGlobalConfig();\n\n // Step 3: Auto-configure each service based on registry tier + available keys\n const liveServices: string[] = [];\n const calcServices: string[] = [];\n const estServices: string[] = [];\n const blindServices: string[] = [];\n\n for (const tracked of detected) {\n const definition = getService(tracked.serviceId, projectRoot);\n if (!definition) continue;\n\n const hasKey = !!globalConfig.services[tracked.serviceId]?.apiKey;\n\n if (hasKey && definition.apiTier === \"live\") {\n tracked.hasApiKey = true;\n liveServices.push(`${definition.name}`);\n } else if (definition.apiTier === \"calc\") {\n calcServices.push(`${definition.name}`);\n } else if (definition.apiTier === \"est\") {\n estServices.push(`${definition.name}`);\n } else {\n blindServices.push(`${definition.name}`);\n }\n }\n\n writeProjectConfig(config, projectRoot);\n\n // Report\n if (liveServices.length > 0) {\n console.log(` ✅ LIVE (real billing data): ${liveServices.join(\", \")}`);\n }\n if (calcServices.length > 0) {\n console.log(` 🟡 CALC (flat-rate tracking): ${calcServices.join(\", \")}`);\n }\n if (estServices.length > 0) {\n console.log(` 🟠 EST (estimated from usage): ${estServices.join(\", \")}`);\n }\n if (blindServices.length > 0) {\n console.log(` 🔴 BLIND (detected, need API key): ${blindServices.join(\", \")}`);\n }\n\n console.log(\"\");\n\n if (blindServices.length > 0) {\n console.log(\"To upgrade BLIND services to LIVE, add API keys:\");\n for (const tracked of detected) {\n const definition = getService(tracked.serviceId, projectRoot);\n if (definition?.apiTier === \"live\" && !tracked.hasApiKey) {\n const envHint = definition.envPatterns[0] ?? \"YOUR_KEY\";\n console.log(` burnwatch add ${tracked.serviceId} --key $${envHint} --budget <N>`);\n }\n }\n console.log(\"\");\n }\n\n console.log(\"To set budgets for any service:\");\n console.log(\" burnwatch add <service> --budget <monthly_amount>\");\n console.log(\"\");\n console.log(\"Or use /setup-burnwatch in Claude Code for guided setup with budget suggestions.\\n\");\n\n // Show brief\n await cmdStatus();\n}\n\nfunction cmdServices(): void {\n const services = getAllServices();\n console.log(`\\n📋 Registry: ${services.length} services available\\n`);\n\n for (const svc of services) {\n const tierBadge =\n svc.apiTier === \"live\"\n ? \"✅ LIVE\"\n : svc.apiTier === \"calc\"\n ? \"🟡 CALC\"\n : svc.apiTier === \"est\"\n ? \"🟠 EST\"\n : \"🔴 BLIND\";\n\n console.log(` ${svc.name.padEnd(24)} ${tierBadge.padEnd(10)} ${svc.billingModel}`);\n }\n\n console.log(\"\");\n}\n\nasync function cmdReconcile(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n console.error('❌ burnwatch not initialized. Run \"burnwatch init\" first.');\n process.exit(1);\n }\n\n console.log(\"🔍 Scanning for untracked services and missed sessions...\\n\");\n\n // Re-run detection against current project state\n const detected = detectServices(projectRoot);\n const config = readProjectConfig(projectRoot)!;\n let newCount = 0;\n\n for (const det of detected) {\n if (!config.services[det.service.id]) {\n config.services[det.service.id] = {\n serviceId: det.service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n };\n newCount++;\n console.log(` 🆕 ${det.service.name} — detected via ${det.details.join(\", \")}`);\n }\n }\n\n if (newCount > 0) {\n writeProjectConfig(config, projectRoot);\n console.log(\n `\\n✅ Found ${newCount} new service${newCount > 1 ? \"s\" : \"\"}. Run 'burnwatch status' to see updated brief.`,\n );\n } else {\n console.log(\" ✅ No new services found. All services already tracked.\");\n }\n\n console.log(\"\");\n}\n\nfunction cmdHelp(): void {\n console.log(`\nburnwatch — Passive cost memory for vibe coding\n\nUsage:\n burnwatch init Interactive setup — pick plans per service\n burnwatch init --non-interactive Auto-detect services, no prompts\n burnwatch setup Init + auto-configure all detected services\n burnwatch add <service> [options] Register a service for tracking\n burnwatch status Show current spend brief\n burnwatch services List all services in registry\n burnwatch reconcile Scan for untracked services\n\nOptions for 'add':\n --key <API_KEY> API key for LIVE tracking (saved to ~/.config/burnwatch/)\n --token <TOKEN> Same as --key (alias)\n --budget <AMOUNT> Monthly budget in USD\n --plan-cost <AMOUNT> Monthly plan cost for CALC tracking\n\nExamples:\n burnwatch init\n burnwatch init --non-interactive\n burnwatch add anthropic --key sk-ant-admin-xxx --budget 100\n burnwatch add scrapfly --key scp-xxx --budget 50\n burnwatch add posthog --plan-cost 0 --budget 0\n burnwatch status\n`);\n}\n\nfunction cmdVersion(): void {\n try {\n const pkgPath = path.resolve(\n path.dirname(new URL(import.meta.url).pathname),\n \"../package.json\",\n );\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n version: string;\n };\n console.log(`burnwatch v${pkg.version}`);\n } catch {\n console.log(\"burnwatch v0.1.0\");\n }\n}\n\n// --- Hook Registration ---\n\nfunction registerHooks(projectRoot: string): void {\n // Step 1: Copy hook scripts into .burnwatch/hooks/ for durability.\n // This avoids relying on ephemeral npx cache paths.\n const sourceHooksDir = path.resolve(\n path.dirname(new URL(import.meta.url).pathname),\n \"hooks\",\n );\n const localHooksDir = path.join(projectRoot, \".burnwatch\", \"hooks\");\n fs.mkdirSync(localHooksDir, { recursive: true });\n\n const hookFiles = [\n \"on-session-start.js\",\n \"on-prompt.js\",\n \"on-file-change.js\",\n \"on-stop.js\",\n ];\n\n for (const file of hookFiles) {\n const src = path.join(sourceHooksDir, file);\n const dest = path.join(localHooksDir, file);\n try {\n fs.copyFileSync(src, dest);\n // Also copy sourcemaps if they exist\n const mapSrc = src + \".map\";\n if (fs.existsSync(mapSrc)) {\n fs.copyFileSync(mapSrc, dest + \".map\");\n }\n } catch (err) {\n console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);\n }\n }\n\n console.log(` Hook scripts copied to ${localHooksDir}`);\n\n // Step 2: Find or create .claude/settings.json — MERGE, never overwrite\n const claudeDir = path.join(projectRoot, \".claude\");\n const settingsPath = path.join(claudeDir, \"settings.json\");\n\n fs.mkdirSync(claudeDir, { recursive: true });\n\n // Read existing settings (preserve everything)\n let settings: Record<string, unknown> = {};\n try {\n const existing = fs.readFileSync(settingsPath, \"utf-8\");\n settings = JSON.parse(existing) as Record<string, unknown>;\n console.log(` Merging into existing ${settingsPath}`);\n } catch {\n // No existing settings — start fresh\n }\n\n // Ensure hooks object exists, preserve all existing hooks\n if (!settings[\"hooks\"] || typeof settings[\"hooks\"] !== \"object\") {\n settings[\"hooks\"] = {};\n }\n const hooks = settings[\"hooks\"] as Record<string, unknown[]>;\n\n // Use the local .burnwatch/hooks/ paths (durable, not ephemeral)\n const hooksDir = localHooksDir;\n\n // SessionStart hook\n if (!hooks[\"SessionStart\"]) hooks[\"SessionStart\"] = [];\n addHookIfMissing(hooks[\"SessionStart\"] as unknown[], \"SessionStart\", {\n matcher: \"startup|resume\",\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-session-start.js\")}\"`,\n timeout: 15,\n },\n ],\n });\n\n // UserPromptSubmit hook\n if (!hooks[\"UserPromptSubmit\"]) hooks[\"UserPromptSubmit\"] = [];\n addHookIfMissing(\n hooks[\"UserPromptSubmit\"] as unknown[],\n \"UserPromptSubmit\",\n {\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-prompt.js\")}\"`,\n timeout: 5,\n },\n ],\n },\n );\n\n // PostToolUse hook (Edit|Write only)\n if (!hooks[\"PostToolUse\"]) hooks[\"PostToolUse\"] = [];\n addHookIfMissing(hooks[\"PostToolUse\"] as unknown[], \"PostToolUse\", {\n matcher: \"Edit|Write\",\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-file-change.js\")}\"`,\n timeout: 5,\n },\n ],\n });\n\n // Stop hook (async — don't block session end)\n if (!hooks[\"Stop\"]) hooks[\"Stop\"] = [];\n addHookIfMissing(hooks[\"Stop\"] as unknown[], \"Stop\", {\n hooks: [\n {\n type: \"command\",\n command: `node \"${path.join(hooksDir, \"on-stop.js\")}\"`,\n timeout: 15,\n async: true,\n },\n ],\n });\n\n settings[\"hooks\"] = hooks;\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\", \"utf-8\");\n console.log(` Hooks registered in ${settingsPath}`);\n}\n\nfunction addHookIfMissing(\n hookArray: unknown[],\n _eventName: string,\n hookConfig: unknown,\n): void {\n // Check if burnwatch hook is already registered\n const existing = hookArray.some((h) => {\n const hook = h as { hooks?: Array<{ command?: string }> };\n return hook.hooks?.some((inner) => inner.command?.includes(\"burnwatch\"));\n });\n\n if (!existing) {\n hookArray.push(hookConfig);\n }\n}\n\n// --- Entry ---\n\nmain().catch((err) => {\n console.error(\"Error:\", err instanceof Error ? err.message : err);\n process.exit(1);\n});\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { loadRegistry } from \"../core/registry.js\";\nimport type { ServiceDefinition, DetectionSource } from \"../core/types.js\";\n\nexport interface DetectionResult {\n service: ServiceDefinition;\n sources: DetectionSource[];\n details: string[];\n}\n\n/**\n * Run all detection surfaces against the current project.\n * Returns services detected via any combination of:\n * - package.json dependencies (recursive — finds monorepo subdirectories)\n * - environment variable patterns (process.env + .env* files recursive)\n * - import statement scanning (recursive from project root)\n * - (prompt mention scanning is handled separately in hooks)\n */\nexport function detectServices(projectRoot: string): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results = new Map<string, DetectionResult>();\n\n // Surface 1: Package manifest scanning (recursive — finds all package.json files)\n const pkgDeps = scanAllPackageJsons(projectRoot);\n for (const [serviceId, service] of registry) {\n const matchedPkgs = service.packageNames.filter((pkg) =>\n pkgDeps.has(pkg),\n );\n if (matchedPkgs.length > 0) {\n getOrCreate(results, serviceId, service).sources.push(\"package_json\");\n getOrCreate(results, serviceId, service).details.push(\n `package.json: ${matchedPkgs.join(\", \")}`,\n );\n }\n }\n\n // Surface 2: Environment variable pattern matching\n // Check both process.env AND .env* files in the project tree\n const envVars = collectEnvVars(projectRoot);\n for (const [serviceId, service] of registry) {\n const matchedEnvs = service.envPatterns.filter((pattern) =>\n envVars.has(pattern),\n );\n if (matchedEnvs.length > 0) {\n getOrCreate(results, serviceId, service).sources.push(\"env_var\");\n getOrCreate(results, serviceId, service).details.push(\n `env vars: ${matchedEnvs.join(\", \")}`,\n );\n }\n }\n\n // Surface 3: Import statement analysis (recursive from project root)\n const importHits = scanImports(projectRoot);\n for (const [serviceId, service] of registry) {\n const matchedImports = service.importPatterns.filter((pattern) =>\n importHits.has(pattern),\n );\n if (matchedImports.length > 0) {\n if (\n !getOrCreate(results, serviceId, service).sources.includes(\n \"import_scan\",\n )\n ) {\n getOrCreate(results, serviceId, service).sources.push(\"import_scan\");\n getOrCreate(results, serviceId, service).details.push(\n `imports: ${matchedImports.join(\", \")}`,\n );\n }\n }\n }\n\n return Array.from(results.values());\n}\n\n/**\n * Detect services mentioned in a prompt string.\n * Used by the UserPromptSubmit hook.\n */\nexport function detectMentions(\n prompt: string,\n projectRoot?: string,\n): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results: DetectionResult[] = [];\n const promptLower = prompt.toLowerCase();\n\n for (const [, service] of registry) {\n const matched = service.mentionKeywords.some((keyword) =>\n promptLower.includes(keyword.toLowerCase()),\n );\n if (matched) {\n results.push({\n service,\n sources: [\"prompt_mention\"],\n details: [`mentioned in prompt`],\n });\n }\n }\n\n return results;\n}\n\n/**\n * Detect new services introduced in a file change.\n * Used by the PostToolUse hook for Write/Edit events.\n */\nexport function detectInFileChange(\n filePath: string,\n content: string,\n projectRoot?: string,\n): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results: DetectionResult[] = [];\n const fileName = path.basename(filePath);\n\n // Check if it's a package.json change\n if (fileName === \"package.json\") {\n try {\n const pkg = JSON.parse(content) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const allDeps = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ]);\n\n for (const [, service] of registry) {\n const matched = service.packageNames.filter((p) => allDeps.has(p));\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"package_json\"],\n details: [`new dependency: ${matched.join(\", \")}`],\n });\n }\n }\n } catch {\n // Not valid JSON, skip\n }\n return results;\n }\n\n // Check if it's an env file change\n if (fileName.startsWith(\".env\")) {\n const envKeys = content\n .split(\"\\n\")\n .filter((line) => line.includes(\"=\") && !line.startsWith(\"#\"))\n .map((line) => line.split(\"=\")[0]!.trim());\n\n for (const [, service] of registry) {\n const matched = service.envPatterns.filter((p) => envKeys.includes(p));\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"env_var\"],\n details: [`new env var: ${matched.join(\", \")}`],\n });\n }\n }\n return results;\n }\n\n // Check for import statements in source files\n if (/\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {\n for (const [, service] of registry) {\n const matched = service.importPatterns.filter(\n (pattern) =>\n content.includes(`from \"${pattern}`) ||\n content.includes(`from '${pattern}`) ||\n content.includes(`require(\"${pattern}`) ||\n content.includes(`require('${pattern}`),\n );\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"import_scan\"],\n details: [`import added: ${matched.join(\", \")}`],\n });\n }\n }\n }\n\n return results;\n}\n\n// --- Helpers ---\n\nfunction getOrCreate(\n map: Map<string, DetectionResult>,\n serviceId: string,\n service: ServiceDefinition,\n): DetectionResult {\n let result = map.get(serviceId);\n if (!result) {\n result = { service, sources: [], details: [] };\n map.set(serviceId, result);\n }\n return result;\n}\n\n/**\n * Recursively find and scan ALL package.json files in the project tree.\n * Handles monorepos where dependencies live in subdirectories.\n */\nfunction scanAllPackageJsons(projectRoot: string): Set<string> {\n const deps = new Set<string>();\n const pkgFiles = findFiles(projectRoot, \"package.json\", 4);\n\n for (const pkgPath of pkgFiles) {\n try {\n const raw = fs.readFileSync(pkgPath, \"utf-8\");\n const pkg = JSON.parse(raw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n for (const name of Object.keys(pkg.dependencies ?? {})) deps.add(name);\n for (const name of Object.keys(pkg.devDependencies ?? {})) deps.add(name);\n } catch {\n // Skip malformed package.json\n }\n }\n\n return deps;\n}\n\n/**\n * Collect environment variable names from both process.env\n * and all .env* files found recursively in the project tree.\n */\nfunction collectEnvVars(projectRoot: string): Set<string> {\n const envVars = new Set(Object.keys(process.env));\n\n // Find all .env* files in the project tree\n const envFiles = findEnvFiles(projectRoot, 3);\n\n for (const envFile of envFiles) {\n try {\n const content = fs.readFileSync(envFile, \"utf-8\");\n const keys = content\n .split(\"\\n\")\n .filter((line) => line.includes(\"=\") && !line.startsWith(\"#\"))\n .map((line) => line.split(\"=\")[0]!.trim())\n .filter(Boolean);\n\n for (const key of keys) {\n envVars.add(key);\n }\n } catch {\n // Skip unreadable files\n }\n }\n\n return envVars;\n}\n\n/**\n * Find all .env* files recursively (but not in node_modules, .git, dist, etc.)\n */\nfunction findEnvFiles(dir: string, maxDepth: number): string[] {\n const results: string[] = [];\n if (maxDepth <= 0) return results;\n\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name === \"node_modules\" || entry.name === \".git\" || entry.name === \"dist\") continue;\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...findEnvFiles(fullPath, maxDepth - 1));\n } else if (entry.name.startsWith(\".env\")) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n\n return results;\n}\n\n/**\n * Find files with a specific name recursively.\n * Used to find package.json files across monorepo subdirectories.\n */\nfunction findFiles(dir: string, fileName: string, maxDepth: number): string[] {\n const results: string[] = [];\n if (maxDepth <= 0) return results;\n\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name === \"node_modules\" || entry.name === \".git\" || entry.name === \"dist\") continue;\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...findFiles(fullPath, fileName, maxDepth - 1));\n } else if (entry.name === fileName) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n\n return results;\n}\n\n/**\n * Lightweight import scanning.\n * Recursively scans the project for import/require statements.\n * Looks in src/, app/, lib/, pages/, and any other code directories.\n * Does NOT do a full AST parse — just string matching.\n */\nfunction scanImports(projectRoot: string): Set<string> {\n const imports = new Set<string>();\n\n // Scan common code directories + the root itself for source files\n const codeDirs = [\"src\", \"app\", \"lib\", \"pages\", \"components\", \"utils\", \"services\", \"hooks\"];\n const dirsToScan: string[] = [];\n\n for (const dir of codeDirs) {\n const fullPath = path.join(projectRoot, dir);\n if (fs.existsSync(fullPath)) {\n dirsToScan.push(fullPath);\n }\n }\n\n // Also check subdirectories (monorepo support)\n try {\n const entries = fs.readdirSync(projectRoot, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name === \"node_modules\" || entry.name === \".git\" || entry.name === \"dist\" || entry.name.startsWith(\".\")) continue;\n\n // Check if this subdirectory has its own package.json (monorepo package)\n const subPkgPath = path.join(projectRoot, entry.name, \"package.json\");\n if (fs.existsSync(subPkgPath)) {\n // Scan this subpackage's code directories\n for (const dir of codeDirs) {\n const fullPath = path.join(projectRoot, entry.name, dir);\n if (fs.existsSync(fullPath)) {\n dirsToScan.push(fullPath);\n }\n }\n }\n }\n } catch {\n // Skip if root is unreadable\n }\n\n for (const dir of dirsToScan) {\n const files = walkDir(dir, /\\.(ts|tsx|js|jsx|mjs|cjs)$/);\n for (const file of files) {\n try {\n const content = fs.readFileSync(file, \"utf-8\");\n // Match: import ... from \"package\" or require(\"package\")\n const importRegex =\n /(?:from\\s+[\"']|require\\s*\\(\\s*[\"'])([^./][^\"']*?)(?:[\"'])/g;\n let match: RegExpExecArray | null;\n while ((match = importRegex.exec(content)) !== null) {\n const pkg = match[1];\n if (pkg) {\n // Normalize scoped packages: @scope/pkg/subpath -> @scope/pkg\n const parts = pkg.split(\"/\");\n if (parts[0]?.startsWith(\"@\") && parts.length >= 2) {\n imports.add(`${parts[0]}/${parts[1]}`);\n } else if (parts[0]) {\n imports.add(parts[0]);\n }\n }\n }\n } catch {\n // Skip unreadable files\n }\n }\n }\n\n return imports;\n}\n\n/** Recursively walk a directory, returning files matching the pattern. */\nfunction walkDir(dir: string, pattern: RegExp, maxDepth = 5): string[] {\n const results: string[] = [];\n if (maxDepth <= 0) return results;\n\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\") continue;\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...walkDir(fullPath, pattern, maxDepth - 1));\n } else if (pattern.test(entry.name)) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n\n return results;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type { ServiceDefinition } from \"./types.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\ninterface RegistryFile {\n version: string;\n lastUpdated: string;\n services: Record<string, ServiceDefinition>;\n}\n\nlet cachedRegistry: Map<string, ServiceDefinition> | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n if (cachedRegistry) return cachedRegistry;\n\n const registry = new Map<string, ServiceDefinition>();\n\n // Load bundled registry (shipped with package)\n // Try multiple possible locations — depends on whether running from src/ or dist/\n const candidates = [\n path.resolve(__dirname, \"../../registry.json\"), // from src/core/\n path.resolve(__dirname, \"../registry.json\"), // from dist/\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n loadRegistryFile(candidate, registry);\n break;\n }\n }\n\n // Load project-local override (if exists)\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n }\n }\n\n cachedRegistry = registry;\n return registry;\n}\n\nfunction loadRegistryFile(\n filePath: string,\n registry: Map<string, ServiceDefinition>,\n): void {\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(raw) as RegistryFile;\n for (const [id, service] of Object.entries(data.services)) {\n registry.set(id, { ...service, id });\n }\n } catch {\n // Silently skip missing or malformed registry files\n }\n}\n\n/** Clear the cached registry (for testing). */\nexport function clearRegistryCache(): void {\n cachedRegistry = null;\n}\n\n/** Get a single service definition by ID. */\nexport function getService(\n id: string,\n projectRoot?: string,\n): ServiceDefinition | undefined {\n return loadRegistry(projectRoot).get(id);\n}\n\n/** Get all service definitions. */\nexport function getAllServices(\n projectRoot?: string,\n): ServiceDefinition[] {\n return Array.from(loadRegistry(projectRoot).values());\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Anthropic billing connector.\n * Uses the /v1/organizations/usage endpoint.\n * Requires an admin API key.\n */\nexport const anthropicConnector: BillingConnector = {\n serviceId: \"anthropic\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n // Get current month date range\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const startDate = startOfMonth.toISOString().split(\"T\")[0]!;\n const endDate = now.toISOString().split(\"T\")[0]!;\n\n const url = `https://api.anthropic.com/v1/organizations/usage?start_date=${startDate}&end_date=${endDate}`;\n\n const result = await fetchJson<{\n data?: Array<{ total_cost_usd?: number; spend?: number }>;\n total_cost_usd?: number;\n }>(url, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"anthropic\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Anthropic usage\",\n };\n }\n\n // Sum up usage across the period\n let totalSpend = 0;\n if (result.data.total_cost_usd !== undefined) {\n totalSpend = result.data.total_cost_usd;\n } else if (result.data.data) {\n totalSpend = result.data.data.reduce(\n (sum, entry) => sum + (entry.total_cost_usd ?? entry.spend ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"anthropic\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * OpenAI billing connector.\n * Uses the /v1/organization/costs endpoint.\n * Requires an organization-level API key.\n */\nexport const openaiConnector: BillingConnector = {\n serviceId: \"openai\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n // OpenAI uses Unix timestamps\n const startTime = Math.floor(startOfMonth.getTime() / 1000);\n\n const url = `https://api.openai.com/v1/organization/costs?start_time=${startTime}`;\n\n const result = await fetchJson<{\n data?: Array<{\n results?: Array<{\n amount?: { value?: number };\n }>;\n }>;\n object?: string;\n }>(url, {\n headers: {\n Authorization: `Bearer ${apiKey}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"openai\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch OpenAI usage\",\n };\n }\n\n // Sum all cost buckets\n let totalSpend = 0;\n if (result.data.data) {\n for (const bucket of result.data.data) {\n if (bucket.results) {\n for (const r of bucket.results) {\n totalSpend += r.amount?.value ?? 0;\n }\n }\n }\n }\n\n // OpenAI returns costs in cents, convert to dollars\n totalSpend = totalSpend / 100;\n\n return {\n serviceId: \"openai\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Vercel billing connector.\n * Uses the Vercel billing API.\n * Requires a Vercel token (personal or team-scoped).\n */\nexport const vercelConnector: BillingConnector = {\n serviceId: \"vercel\",\n\n async fetchSpend(\n token: string,\n options?: Record<string, string>,\n ): Promise<BillingResult> {\n const teamId = options?.[\"teamId\"] ?? \"\";\n const teamParam = teamId ? `?teamId=${teamId}` : \"\";\n\n // Fetch current billing period usage\n const url = `https://api.vercel.com/v2/usage${teamParam}`;\n\n const result = await fetchJson<{\n usage?: {\n total?: number;\n bandwidth?: { total?: number };\n serverlessFunctionExecution?: { total?: number };\n edgeFunctionExecution?: { total?: number };\n imageOptimization?: { total?: number };\n };\n billing?: {\n plan?: string;\n period?: { start?: string; end?: string };\n invoiceItems?: Array<{ amount?: number }>;\n };\n }>(url, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"vercel\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Vercel usage\",\n };\n }\n\n // Sum up usage costs\n let totalSpend = 0;\n if (result.data.usage?.total !== undefined) {\n totalSpend = result.data.usage.total;\n } else if (result.data.billing?.invoiceItems) {\n totalSpend = result.data.billing.invoiceItems.reduce(\n (sum, item) => sum + (item.amount ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"vercel\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Scrapfly billing connector.\n * Uses the /account endpoint which returns credits used/remaining.\n * Works with the standard API key — no special billing key needed.\n */\nexport const scrapflyConnector: BillingConnector = {\n serviceId: \"scrapfly\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const url = `https://api.scrapfly.io/account?key=${apiKey}`;\n\n const result = await fetchJson<{\n subscription?: {\n usage?: {\n scrape?: { used?: number; allowed?: number };\n };\n };\n account?: {\n credits_used?: number;\n credits_total?: number;\n };\n }>(url);\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"scrapfly\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Scrapfly account\",\n };\n }\n\n // Extract credits used from the response\n let creditsUsed = 0;\n let creditsTotal = 0;\n\n if (result.data.subscription?.usage?.scrape) {\n creditsUsed = result.data.subscription.usage.scrape.used ?? 0;\n creditsTotal = result.data.subscription.usage.scrape.allowed ?? 0;\n } else if (result.data.account) {\n creditsUsed = result.data.account.credits_used ?? 0;\n creditsTotal = result.data.account.credits_total ?? 0;\n }\n\n // Convert credits to USD at registry rate\n const creditRate = 0.00015; // $0.00015 per credit\n const spend = creditsUsed * creditRate;\n\n return {\n serviceId: \"scrapfly\",\n spend,\n isEstimate: false,\n tier: \"live\",\n raw: {\n credits_used: creditsUsed,\n credits_total: creditsTotal,\n credit_rate: creditRate,\n ...(result.data as unknown as Record<string, unknown>),\n },\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { anthropicConnector } from \"./anthropic.js\";\nimport { openaiConnector } from \"./openai.js\";\nimport { vercelConnector } from \"./vercel.js\";\nimport { scrapflyConnector } from \"./scrapfly.js\";\nimport type { TrackedService, ConfidenceTier } from \"../core/types.js\";\nimport { readGlobalConfig } from \"../core/config.js\";\nimport { getService } from \"../core/registry.js\";\n\n/** All available billing connectors, keyed by service ID. */\nconst connectors: Map<string, BillingConnector> = new Map([\n [\"anthropic\", anthropicConnector],\n [\"openai\", openaiConnector],\n [\"vercel\", vercelConnector],\n [\"scrapfly\", scrapflyConnector],\n]);\n\n/**\n * Poll spend for a single tracked service.\n * Returns the best available data based on connector availability and API keys.\n */\nexport async function pollService(\n tracked: TrackedService,\n): Promise<BillingResult> {\n const globalConfig = readGlobalConfig();\n const serviceConfig = globalConfig.services[tracked.serviceId];\n const connector = connectors.get(tracked.serviceId);\n const definition = getService(tracked.serviceId);\n\n // If we have a connector and an API key, try LIVE\n if (connector && serviceConfig?.apiKey) {\n try {\n const result = await connector.fetchSpend(\n serviceConfig.apiKey,\n serviceConfig as unknown as Record<string, string>,\n );\n if (!result.error) return result;\n // Fall through to lower tiers on error\n } catch {\n // Fall through\n }\n }\n\n // If user provided a plan cost, use CALC\n if (tracked.planCost !== undefined) {\n const now = new Date();\n const daysInMonth = new Date(\n now.getFullYear(),\n now.getMonth() + 1,\n 0,\n ).getDate();\n const dayOfMonth = now.getDate();\n const projectedSpend = (tracked.planCost / daysInMonth) * dayOfMonth;\n\n return {\n serviceId: tracked.serviceId,\n spend: projectedSpend,\n isEstimate: true,\n tier: \"calc\",\n };\n }\n\n // If service is in registry but we have no key and no plan cost\n if (definition) {\n let tier: ConfidenceTier;\n if (tracked.tierOverride) {\n tier = tracked.tierOverride;\n } else if (definition.apiTier === \"live\") {\n // Has a LIVE API but we don't have the key — mark as BLIND\n tier = \"blind\";\n } else {\n // EST, CALC, or BLIND — use the registry's declared tier\n tier = definition.apiTier;\n }\n\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: tier !== \"live\",\n tier,\n error: tier === \"blind\" ? \"No API key configured\" : undefined,\n };\n }\n\n // Completely unknown service\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: true,\n tier: \"blind\",\n error: \"Unknown service — not in registry\",\n };\n}\n\n/**\n * Poll all tracked services concurrently.\n * Returns results in the same order as input.\n */\nexport async function pollAllServices(\n services: TrackedService[],\n): Promise<BillingResult[]> {\n return Promise.all(services.map(pollService));\n}\n\nexport { type BillingConnector, type BillingResult } from \"./base.js\";\n","/**\n * Confidence tiers for spend tracking.\n *\n * LIVE — Real billing API data\n * CALC — Fixed monthly cost, user-entered\n * EST — Estimated from usage signals + pricing formula\n * BLIND — Detected in project, no tracking configured\n */\nexport type ConfidenceTier = \"live\" | \"calc\" | \"est\" | \"blind\" | \"excluded\";\n\nexport const CONFIDENCE_BADGES: Record<ConfidenceTier, string> = {\n live: \"✅ LIVE\",\n calc: \"🟡 CALC\",\n est: \"🟠 EST\",\n blind: \"🔴 BLIND\",\n excluded: \"⬚ SKIP\",\n};\n\n/** How a service charges — determines tracking strategy. */\nexport type BillingModel =\n | \"token_usage\" // Per-token (Anthropic, OpenAI, Gemini)\n | \"credit_pool\" // Fixed credit bucket (Scrapfly)\n | \"per_unit\" // Per-email, per-session, per-command (Resend, Browserbase, Upstash)\n | \"percentage\" // Percentage of transaction (Stripe)\n | \"flat_monthly\" // Fixed monthly subscription (PostHog, Inngest free tier)\n | \"tiered\" // Free up to X, then jumps (PostHog, Supabase)\n | \"compute\" // Compute-time based (Vercel, AWS)\n | \"unknown\";\n\n/** How cost scales — helps the agent reason about future spend. */\nexport type ScalingShape =\n | \"linear\" // Each unit costs the same\n | \"linear_burndown\" // Fixed pool, each use depletes it\n | \"tiered_jump\" // Free until threshold, then expensive\n | \"percentage\" // Proportional to revenue/volume\n | \"fixed\" // Flat monthly, no scaling\n | \"unknown\";\n\n/** A plan tier option for a service in the registry. */\nexport interface PlanTier {\n /** Human-readable plan name */\n name: string;\n /** Plan type: usage (pay-as-you-go), flat (fixed monthly), exclude (don't track) */\n type: \"usage\" | \"flat\" | \"exclude\";\n /** Monthly base cost for flat plans */\n monthlyBase?: number;\n /** Whether this plan requires an API key for tracking */\n requiresKey?: boolean;\n /** Whether this is the default/most common plan */\n default?: boolean;\n}\n\n/** Risk category for service grouping in interactive init. */\nexport type ServiceRiskCategory = \"llm\" | \"usage\" | \"infra\" | \"flat\";\n\n/** A service definition from the registry. */\nexport interface ServiceDefinition {\n /** Unique service identifier */\n id: string;\n /** Human-readable name */\n name: string;\n /** Package names in npm/pip that indicate this service */\n packageNames: string[];\n /** Env var patterns that indicate this service */\n envPatterns: string[];\n /** Import patterns to scan for (regex strings) */\n importPatterns: string[];\n /** Keywords that indicate mentions in prompts */\n mentionKeywords: string[];\n /** Billing model */\n billingModel: BillingModel;\n /** How cost scales */\n scalingShape: ScalingShape;\n /** What tier of tracking is available */\n apiTier: ConfidenceTier;\n /** Billing API endpoint, if available */\n apiEndpoint?: string;\n /** Pricing details */\n pricing?: {\n /** Human-readable formula */\n formula?: string;\n /** Rate per unit, if applicable */\n unitRate?: number;\n /** Unit name (token, credit, email, session, etc.) */\n unitName?: string;\n /** Monthly base cost, if flat */\n monthlyBase?: number;\n };\n /** Known gotchas that affect cost */\n gotchas?: string[];\n /** Alternative services (free or cheaper) */\n alternatives?: string[];\n /** Documentation URL */\n docsUrl?: string;\n /** Last time pricing was verified */\n lastVerified?: string;\n /** Notes about recent pricing changes */\n pricingNotes?: string;\n /** Available plan tiers for interactive init */\n plans?: PlanTier[];\n /** Whether the plan can be auto-detected from an API key */\n autoDetectPlan?: boolean;\n}\n\n/** A tracked service instance — a service definition + user config. */\nexport interface TrackedService {\n /** Service definition ID */\n serviceId: string;\n /** How this service was detected */\n detectedVia: DetectionSource[];\n /** User-configured monthly budget */\n budget?: number;\n /** Whether the user has provided an API/billing key */\n hasApiKey: boolean;\n /** Override confidence tier (e.g., user provided billing key upgrades to LIVE) */\n tierOverride?: ConfidenceTier;\n /** User-entered monthly plan cost (for CALC tier) */\n planCost?: number;\n /** When this service was first detected */\n firstDetected: string;\n /** Explicitly excluded from tracking by user */\n excluded?: boolean;\n /** Plan name selected during interactive init */\n planName?: string;\n}\n\nexport type DetectionSource =\n | \"package_json\"\n | \"env_var\"\n | \"import_scan\"\n | \"prompt_mention\"\n | \"git_diff\"\n | \"manual\";\n\n/** A spend snapshot for a single service at a point in time. */\nexport interface SpendSnapshot {\n serviceId: string;\n /** Current period spend (or estimate) */\n spend: number;\n /** Is the spend figure exact or estimated? */\n isEstimate: boolean;\n /** Confidence tier for this reading */\n tier: ConfidenceTier;\n /** Budget allocated */\n budget?: number;\n /** Percentage of budget consumed */\n budgetPercent?: number;\n /** Budget status */\n status: \"healthy\" | \"caution\" | \"over\" | \"unknown\";\n /** Human-readable status label */\n statusLabel: string;\n /** Raw data from billing API, if available */\n raw?: Record<string, unknown>;\n /** Timestamp of this snapshot */\n timestamp: string;\n}\n\n/** The full spend brief, injected at session start. */\nexport interface SpendBrief {\n projectName: string;\n generatedAt: string;\n period: string;\n services: SpendSnapshot[];\n totalSpend: number;\n totalIsEstimate: boolean;\n estimateMargin: number;\n untrackedCount: number;\n alerts: SpendAlert[];\n}\n\nexport interface SpendAlert {\n serviceId: string;\n type: \"over_budget\" | \"near_budget\" | \"new_service\" | \"stale_data\" | \"blind_service\";\n message: string;\n severity: \"warning\" | \"critical\" | \"info\";\n}\n\n/** Ledger entry — one row in spend-ledger.md */\nexport interface LedgerEntry {\n serviceId: string;\n serviceName: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n budget?: number;\n statusLabel: string;\n}\n\n/** Event logged to events.jsonl */\nexport interface SpendEvent {\n timestamp: string;\n sessionId: string;\n type:\n | \"session_start\"\n | \"session_end\"\n | \"service_detected\"\n | \"service_mentioned\"\n | \"spend_polled\"\n | \"budget_alert\"\n | \"ledger_written\"\n | \"cost_impact\";\n data: Record<string, unknown>;\n}\n\n/** A cost impact estimate for a file change. */\nexport interface CostImpact {\n serviceId: string;\n serviceName: string;\n filePath: string;\n /** Number of SDK call sites found */\n callCount: number;\n /** Detected multipliers (loops, .map(), etc.) */\n multipliers: string[];\n /** Effective multiplier applied to call count */\n multiplierFactor: number;\n /** Estimated monthly invocations */\n monthlyInvocations: number;\n /** Low estimate monthly cost */\n costLow: number;\n /** High estimate monthly cost */\n costHigh: number;\n /** Gotcha-based cost range explanation */\n rangeExplanation?: string;\n}\n\n/**\n * Hook input — the JSON received via stdin from Claude Code.\n * Subset of fields we care about.\n */\nexport interface HookInput {\n session_id: string;\n transcript_path?: string;\n cwd: string;\n hook_event_name: string;\n // SessionStart\n source?: string;\n // UserPromptSubmit\n prompt?: string;\n // PostToolUse\n tool_name?: string;\n tool_input?: {\n file_path?: string;\n command?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n };\n}\n\n/**\n * Hook output — the JSON we write to stdout for Claude Code.\n */\nexport interface HookOutput {\n hookSpecificOutput?: {\n hookEventName: string;\n additionalContext?: string;\n };\n}\n","import type {\n SpendBrief,\n SpendSnapshot,\n SpendAlert,\n ConfidenceTier,\n} from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\n\n/**\n * Format a spend brief as a text block for injection into Claude's context.\n */\nexport function formatBrief(brief: SpendBrief): string {\n const lines: string[] = [];\n const width = 62;\n const hrDouble = \"═\".repeat(width);\n const hrSingle = \"─\".repeat(width - 4);\n\n lines.push(`╔${hrDouble}╗`);\n lines.push(\n `║ BURNWATCH — ${brief.projectName} — ${brief.period}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n lines.push(`╠${hrDouble}╣`);\n\n // Header\n lines.push(\n formatRow(\"Service\", \"Spend\", \"Conf\", \"Budget\", \"Left\", width),\n );\n lines.push(`║ ${hrSingle} ║`);\n\n // Service rows\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n const leftStr = formatLeft(svc);\n\n lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));\n }\n\n // Footer\n lines.push(`╠${hrDouble}╣`);\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr = brief.estimateMargin > 0\n ? ` Est margin: ±$${brief.estimateMargin.toFixed(0)}`\n : \"\";\n const untrackedStr =\n brief.untrackedCount > 0\n ? `Untracked: ${brief.untrackedCount} ⚠️`\n : `Untracked: 0 ✅`;\n\n lines.push(\n `║ TOTAL: ${totalStr} ${untrackedStr}${marginStr}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n\n // Alerts\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(\n `║ ${icon} ${alert.message}`.padEnd(width + 1) + \"║\",\n );\n }\n\n lines.push(`╚${hrDouble}╝`);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Format a single-service spend card for injection on mention.\n */\nexport function formatSpendCard(snapshot: SpendSnapshot): string {\n const badge = CONFIDENCE_BADGES[snapshot.tier];\n const spendStr = snapshot.isEstimate\n ? `~$${snapshot.spend.toFixed(2)}`\n : `$${snapshot.spend.toFixed(2)}`;\n const budgetStr = snapshot.budget\n ? `Budget: $${snapshot.budget}`\n : \"No budget set\";\n const statusStr = snapshot.statusLabel;\n\n const lines = [\n `[BURNWATCH] ${snapshot.serviceId} — current period`,\n ` Spend: ${spendStr} | ${budgetStr} | ${statusStr}`,\n ` Confidence: ${badge}`,\n ];\n\n if (snapshot.status === \"over\" && snapshot.budgetPercent) {\n lines.push(\n ` ⚠️ ${snapshot.budgetPercent.toFixed(0)}% of budget consumed`,\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Build a SpendBrief from snapshots and project config.\n */\nexport function buildBrief(\n projectName: string,\n snapshots: SpendSnapshot[],\n blindCount: number,\n): SpendBrief {\n const now = new Date();\n const period = now.toLocaleDateString(\"en-US\", {\n month: \"long\",\n year: \"numeric\",\n });\n\n let totalSpend = 0;\n let hasEstimates = false;\n let estimateMargin = 0;\n const alerts: SpendAlert[] = [];\n\n for (const snap of snapshots) {\n totalSpend += snap.spend;\n if (snap.isEstimate) {\n hasEstimates = true;\n estimateMargin += snap.spend * 0.15; // ±15% margin on estimates\n }\n\n if (snap.status === \"over\") {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"over_budget\",\n message: `${snap.serviceId.toUpperCase()} ${snap.budgetPercent?.toFixed(0) ?? \"?\"}% OVER BUDGET — review before use`,\n severity: \"critical\",\n });\n } else if (snap.status === \"caution\" && snap.budgetPercent && snap.budgetPercent >= 80) {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"near_budget\",\n message: `${snap.serviceId} at ${snap.budgetPercent.toFixed(0)}% of budget`,\n severity: \"warning\",\n });\n }\n }\n\n if (blindCount > 0) {\n alerts.push({\n serviceId: \"_blind\",\n type: \"blind_service\",\n message: `${blindCount} service${blindCount > 1 ? \"s\" : \"\"} detected but untracked - run 'burnwatch init' to configure`,\n severity: \"warning\",\n });\n }\n\n return {\n projectName,\n generatedAt: now.toISOString(),\n period,\n services: snapshots,\n totalSpend,\n totalIsEstimate: hasEstimates,\n estimateMargin,\n untrackedCount: blindCount,\n alerts,\n };\n}\n\n// --- Helpers ---\n\nfunction formatRow(\n service: string,\n spend: string,\n conf: string,\n budget: string,\n left: string,\n width: number,\n): string {\n const row = ` ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(7)} ${budget.padEnd(7)} ${left}`;\n return `║${row}`.padEnd(width + 1) + \"║\";\n}\n\nfunction formatLeft(snap: SpendSnapshot): string {\n if (!snap.budget) return \"—\";\n if (snap.status === \"over\") return \"⚠️ OVR\";\n if (snap.budgetPercent !== undefined) {\n const remaining = 100 - snap.budgetPercent;\n return `${remaining.toFixed(0)}%`;\n }\n return \"—\";\n}\n\n/**\n * Build a SpendSnapshot from tracked service data.\n */\nexport function buildSnapshot(\n serviceId: string,\n tier: ConfidenceTier,\n spend: number,\n budget?: number,\n): SpendSnapshot {\n const isEstimate = tier === \"est\" || tier === \"calc\";\n const budgetPercent = budget ? (spend / budget) * 100 : undefined;\n\n let status: SpendSnapshot[\"status\"] = \"unknown\";\n let statusLabel = \"no budget\";\n\n if (budget) {\n if (budgetPercent! > 100) {\n status = \"over\";\n statusLabel = `⚠️ ${budgetPercent!.toFixed(0)}% over`;\n } else if (budgetPercent! >= 75) {\n status = \"caution\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — caution`;\n } else {\n status = \"healthy\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — healthy`;\n }\n }\n\n if (tier === \"calc\" && budget) {\n statusLabel = `flat — on plan`;\n status = \"healthy\";\n }\n\n return {\n serviceId,\n spend,\n isEstimate,\n tier,\n budget,\n budgetPercent,\n status,\n statusLabel,\n timestamp: new Date().toISOString(),\n };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { SpendBrief, SpendEvent } from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\nimport { projectConfigDir, projectDataDir } from \"./config.js\";\n\n/**\n * Write the spend ledger as a human-readable markdown file.\n * Designed to be git-committable and readable in 10 seconds.\n */\nexport function writeLedger(brief: SpendBrief, projectRoot?: string): void {\n const now = new Date();\n const lines: string[] = [];\n\n lines.push(`# Burnwatch Ledger — ${brief.projectName}`);\n lines.push(`Last updated: ${now.toISOString()}`);\n lines.push(\"\");\n lines.push(`## This Month (${brief.period})`);\n lines.push(\"\");\n lines.push(\"| Service | Spend | Conf | Budget | Status |\");\n lines.push(\"|---------|-------|------|--------|--------|\");\n\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n\n lines.push(\n `| ${svc.serviceId} | ${spendStr} | ${badge} | ${budgetStr} | ${svc.statusLabel} |`,\n );\n }\n\n // Add projected impact row if session impacts exist in alerts\n const impactAlert = brief.alerts.find(\n (a) => a.serviceId === \"_session_impact\",\n );\n if (impactAlert) {\n lines.push(\n `| _projected impact_ | — | 📈 EST | — | ${impactAlert.message} |`,\n );\n }\n\n lines.push(\"\");\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr =\n brief.estimateMargin > 0\n ? ` (±$${brief.estimateMargin.toFixed(0)} estimated margin)`\n : \"\";\n lines.push(`## TOTAL: ${totalStr}${marginStr}`);\n lines.push(`## Untracked services: ${brief.untrackedCount}`);\n lines.push(\"\");\n\n if (brief.alerts.length > 0) {\n lines.push(\"## Alerts\");\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(`- ${icon} ${alert.message}`);\n }\n lines.push(\"\");\n }\n\n const ledgerPath = path.join(\n projectConfigDir(projectRoot),\n \"spend-ledger.md\",\n );\n fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });\n fs.writeFileSync(ledgerPath, lines.join(\"\\n\") + \"\\n\", \"utf-8\");\n}\n\n/**\n * Append an event to the append-only event log.\n */\nexport function logEvent(event: SpendEvent, projectRoot?: string): void {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n fs.mkdirSync(path.dirname(logPath), { recursive: true });\n fs.appendFileSync(logPath, JSON.stringify(event) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Read recent events from the event log.\n */\nexport function readRecentEvents(\n count: number,\n projectRoot?: string,\n): SpendEvent[] {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n try {\n const raw = fs.readFileSync(logPath, \"utf-8\");\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n return lines\n .slice(-count)\n .map((line) => JSON.parse(line) as SpendEvent);\n } catch {\n return [];\n }\n}\n\n/**\n * Save a spend snapshot to the snapshots directory.\n * Used for delta computation across sessions.\n */\nexport function saveSnapshot(brief: SpendBrief, projectRoot?: string): void {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n fs.mkdirSync(snapshotDir, { recursive: true });\n const filename = `snapshot-${new Date().toISOString().replace(/[:.]/g, \"-\")}.json`;\n fs.writeFileSync(\n path.join(snapshotDir, filename),\n JSON.stringify(brief, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\n/**\n * Read the most recent snapshot, if any.\n */\nexport function readLatestSnapshot(\n projectRoot?: string,\n): SpendBrief | null {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n try {\n const files = fs\n .readdirSync(snapshotDir)\n .filter((f) => f.startsWith(\"snapshot-\") && f.endsWith(\".json\"))\n .sort()\n .reverse();\n\n if (files.length === 0) return null;\n\n const raw = fs.readFileSync(\n path.join(snapshotDir, files[0]!),\n \"utf-8\",\n );\n return JSON.parse(raw) as SpendBrief;\n } catch {\n return null;\n }\n}\n","/**\n * Interactive init flow for burnwatch.\n *\n * Conducts a per-service interview: detects what it can automatically\n * (existing API keys, env vars), asks for plan selection, collects\n * API keys for LIVE tracking, and ensures every service exits with\n * a budget. No skipping.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Risk categories in display order: LLMs first, then usage-based, infra, flat-rate */\nconst RISK_ORDER: ServiceRiskCategory[] = [\"llm\", \"usage\", \"infra\", \"flat\"];\n\nconst RISK_LABELS: Record<ServiceRiskCategory, string> = {\n llm: \"LLM / AI Services (highest variable cost)\",\n usage: \"Usage-Based Services\",\n infra: \"Infrastructure & Compute\",\n flat: \"Flat-Rate / Free Tier Services\",\n};\n\n/** Where to find API keys for LIVE-capable services */\nconst API_KEY_HINTS: Record<string, string> = {\n anthropic: \"Admin key: console.anthropic.com -> Settings -> Admin API Keys\",\n openai: \"Org key: platform.openai.com -> Settings -> API Keys\",\n vercel: \"Token: vercel.com/account/tokens\",\n supabase: \"Service role key: supabase.com/dashboard -> Settings -> API\",\n stripe: \"Secret key: dashboard.stripe.com -> Developers -> API Keys\",\n scrapfly: \"API key: scrapfly.io/dashboard\",\n};\n\n/** Map service IDs to risk categories */\nfunction classifyRisk(service: ServiceDefinition): ServiceRiskCategory {\n if (service.billingModel === \"token_usage\") return \"llm\";\n if (\n service.billingModel === \"credit_pool\" ||\n service.billingModel === \"percentage\" ||\n service.billingModel === \"per_unit\"\n )\n return \"usage\";\n if (service.billingModel === \"compute\") return \"infra\";\n return \"flat\";\n}\n\n/** Group detection results by risk category */\nfunction groupByRisk(\n detected: DetectionResult[],\n): Map<ServiceRiskCategory, DetectionResult[]> {\n const groups = new Map<ServiceRiskCategory, DetectionResult[]>();\n for (const cat of RISK_ORDER) {\n groups.set(cat, []);\n }\n\n for (const det of detected) {\n const cat = classifyRisk(det.service);\n groups.get(cat)!.push(det);\n }\n\n return groups;\n}\n\n/** Prompt the user with a question and return their answer */\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/** Try to auto-detect plan from Scrapfly API */\nasync function autoDetectScrapflyPlan(\n apiKey: string,\n): Promise<string | null> {\n try {\n const result = await fetchJson<{\n subscription?: { plan?: { name?: string } };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (result.ok && result.data?.subscription?.plan?.name) {\n return result.data.subscription.plan.name;\n }\n } catch {\n // Ignore errors\n }\n return null;\n}\n\n/** Scan environment for API keys matching service env patterns */\nfunction findEnvKey(service: ServiceDefinition): string | undefined {\n for (const pattern of service.envPatterns) {\n const val = process.env[pattern];\n if (val && val.length > 0) return val;\n }\n return undefined;\n}\n\nexport interface InteractiveInitResult {\n services: Record<string, TrackedService>;\n}\n\n/**\n * Auto-configure all services without prompts.\n *\n * Applies the same logic as the interactive interview but picks\n * defaults automatically: default plan, env var keys, budget = plan cost.\n * Used when stdin is not a TTY (e.g., Claude Code, piped input).\n */\nexport function autoConfigureServices(\n detected: DetectionResult[],\n): InteractiveInitResult {\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Auto-configuring with defaults.\\n`,\n );\n console.log(\" Run 'burnwatch init' from your terminal for interactive setup.\\n\");\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(` ${RISK_LABELS[category]}`);\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans ?? [];\n const defaultPlan = plans.find((p) => p.default) ?? plans[0];\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n\n if (defaultPlan && defaultPlan.type !== \"exclude\") {\n tracked.planName = defaultPlan.name;\n\n if (defaultPlan.type === \"flat\" && defaultPlan.monthlyBase !== undefined) {\n tracked.planCost = defaultPlan.monthlyBase;\n tracked.budget = defaultPlan.monthlyBase;\n }\n }\n\n // Check for existing API key in global config or environment\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n let keySource = \"\";\n\n if (existingKey) {\n tracked.hasApiKey = true;\n keySource = \" (key: global config)\";\n } else if (envKey) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n keySource = ` (key: ${service.envPatterns[0]})`;\n }\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n const planStr = tracked.planName ? ` ${tracked.planName}` : \"\";\n console.log(\n ` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`,\n );\n\n services[service.id] = tracked;\n }\n console.log(\"\");\n }\n\n // Summary\n const trackedList = Object.values(services);\n const liveCount = trackedList.filter((s) => s.hasApiKey).length;\n const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\" \" + \"-\".repeat(48));\n console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n\n const needBudget = trackedList.filter(\n (s) => s.budget === 0 && s.planCost === undefined,\n );\n if (needBudget.length > 0) {\n console.log(`\\n ${needBudget.length} usage-based service${needBudget.length > 1 ? \"s\" : \"\"} need budgets:`);\n for (const svc of needBudget) {\n console.log(` burnwatch add ${svc.serviceId} --budget <AMOUNT>`);\n }\n }\n console.log(\"\");\n\n // Save discovered keys\n writeGlobalConfig(globalConfig);\n\n return { services };\n}\n\n/**\n * Run the interactive init flow.\n *\n * For each detected service:\n * 1. Ask which plan they're on\n * 2. If LIVE-capable, check for existing key or ask for one\n * 3. Set budget (defaults to plan cost, $0 for free - never skipped)\n */\nexport async function runInteractiveInit(\n detected: DetectionResult[],\n): Promise<InteractiveInitResult> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Let's configure each one.\\n`,\n );\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(`\\n ${RISK_LABELS[category]}`);\n console.log(\" \" + \"-\".repeat(48));\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans;\n\n console.log(`\\n ${service.name}`);\n console.log(` Detected via: ${det.details.join(\", \")}`);\n\n if (!plans || plans.length === 0) {\n // No plans defined - basic tracking with $0 budget\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n console.log(\" -> Configured (no plan tiers in registry, budget: $0)\");\n continue;\n }\n\n // --- Plan selection ---\n const defaultIndex = plans.findIndex((p) => p.default);\n console.log(\"\");\n for (let i = 0; i < plans.length; i++) {\n const plan = plans[i]!;\n const marker = i === defaultIndex ? \" *\" : \"\";\n const costStr =\n plan.type === \"exclude\"\n ? \"\"\n : plan.monthlyBase !== undefined\n ? ` - $${plan.monthlyBase}/mo`\n : \" - variable\";\n console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);\n }\n\n const defaultChoice =\n defaultIndex >= 0 ? String(defaultIndex + 1) : \"1\";\n const answer = await ask(\n rl,\n ` Which plan? [${defaultChoice}]: `,\n );\n\n const choiceIndex = (answer === \"\" ? parseInt(defaultChoice) : parseInt(answer)) - 1;\n const chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n\n if (chosen.type === \"exclude\") {\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n excluded: true,\n planName: chosen.name,\n };\n console.log(` -> ${service.name}: excluded`);\n continue;\n }\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n planName: chosen.name,\n };\n\n if (chosen.type === \"flat\" && chosen.monthlyBase !== undefined) {\n tracked.planCost = chosen.monthlyBase;\n }\n\n // --- API key (LIVE-capable services) ---\n if (service.apiTier === \"live\") {\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n console.log(` API key: found in global config`);\n tracked.hasApiKey = true;\n } else if (envKey) {\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n } else {\n const hint = API_KEY_HINTS[service.id];\n if (hint) console.log(` ${hint}`);\n const keyAnswer = await ask(\n rl,\n ` API key for real-time tracking (Enter to skip): `,\n );\n if (keyAnswer) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n }\n }\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\" && tracked.hasApiKey) {\n const key = globalConfig.services[service.id]?.apiKey;\n if (key) {\n console.log(\" Detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(key);\n if (planName) {\n console.log(` -> Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n }\n }\n\n // --- Budget (always set, never skip) ---\n const planCost = chosen.monthlyBase ?? 0;\n\n if (chosen.type === \"usage\" && planCost === 0) {\n // Usage-based with no fixed cost - need a real number\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget: $`,\n );\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : 0;\n } else {\n // Flat plan or usage with known base - default to plan cost\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget [$${planCost}]: $`,\n );\n if (budgetAnswer) {\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : planCost;\n } else {\n tracked.budget = planCost;\n }\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo`,\n );\n }\n }\n\n // --- Summary ---\n const tracked = Object.values(services).filter((s) => !s.excluded);\n const excluded = Object.values(services).filter((s) => s.excluded);\n const liveCount = tracked.filter((s) => s.hasApiKey).length;\n const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\"\\n \" + \"=\".repeat(48));\n console.log(` ${tracked.length} services configured`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);\n if (excluded.length > 0) console.log(` ${excluded.length} excluded`);\n console.log(` Total monthly budget: $${totalBudget}`);\n console.log(\" \" + \"=\".repeat(48));\n\n // Save any collected API keys\n writeGlobalConfig(globalConfig);\n\n rl.close();\n\n return { services };\n}\n"],"mappings":"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","names":["fs","path","fs","path","fs","path","url","url","url","url","url","fs","path","resolve","tracked"]}
|
|
@@ -13,6 +13,14 @@ import { D as DetectionResult } from './detector-C4LnLT-O.js';
|
|
|
13
13
|
interface InteractiveInitResult {
|
|
14
14
|
services: Record<string, TrackedService>;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Auto-configure all services without prompts.
|
|
18
|
+
*
|
|
19
|
+
* Applies the same logic as the interactive interview but picks
|
|
20
|
+
* defaults automatically: default plan, env var keys, budget = plan cost.
|
|
21
|
+
* Used when stdin is not a TTY (e.g., Claude Code, piped input).
|
|
22
|
+
*/
|
|
23
|
+
declare function autoConfigureServices(detected: DetectionResult[]): InteractiveInitResult;
|
|
16
24
|
/**
|
|
17
25
|
* Run the interactive init flow.
|
|
18
26
|
*
|
|
@@ -23,4 +31,4 @@ interface InteractiveInitResult {
|
|
|
23
31
|
*/
|
|
24
32
|
declare function runInteractiveInit(detected: DetectionResult[]): Promise<InteractiveInitResult>;
|
|
25
33
|
|
|
26
|
-
export { type InteractiveInitResult, runInteractiveInit };
|
|
34
|
+
export { type InteractiveInitResult, autoConfigureServices, runInteractiveInit };
|
package/dist/interactive-init.js
CHANGED
|
@@ -118,6 +118,81 @@ function findEnvKey(service) {
|
|
|
118
118
|
}
|
|
119
119
|
return void 0;
|
|
120
120
|
}
|
|
121
|
+
function autoConfigureServices(detected) {
|
|
122
|
+
const services = {};
|
|
123
|
+
const groups = groupByRisk(detected);
|
|
124
|
+
const globalConfig = readGlobalConfig();
|
|
125
|
+
console.log(
|
|
126
|
+
`
|
|
127
|
+
Found ${detected.length} paid service${detected.length !== 1 ? "s" : ""}. Auto-configuring with defaults.
|
|
128
|
+
`
|
|
129
|
+
);
|
|
130
|
+
console.log(" Run 'burnwatch init' from your terminal for interactive setup.\n");
|
|
131
|
+
for (const category of RISK_ORDER) {
|
|
132
|
+
const group = groups.get(category);
|
|
133
|
+
if (group.length === 0) continue;
|
|
134
|
+
console.log(` ${RISK_LABELS[category]}`);
|
|
135
|
+
for (const det of group) {
|
|
136
|
+
const service = det.service;
|
|
137
|
+
const plans = service.plans ?? [];
|
|
138
|
+
const defaultPlan = plans.find((p) => p.default) ?? plans[0];
|
|
139
|
+
const tracked = {
|
|
140
|
+
serviceId: service.id,
|
|
141
|
+
detectedVia: det.sources,
|
|
142
|
+
hasApiKey: false,
|
|
143
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
144
|
+
budget: 0
|
|
145
|
+
};
|
|
146
|
+
if (defaultPlan && defaultPlan.type !== "exclude") {
|
|
147
|
+
tracked.planName = defaultPlan.name;
|
|
148
|
+
if (defaultPlan.type === "flat" && defaultPlan.monthlyBase !== void 0) {
|
|
149
|
+
tracked.planCost = defaultPlan.monthlyBase;
|
|
150
|
+
tracked.budget = defaultPlan.monthlyBase;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
154
|
+
const envKey = findEnvKey(service);
|
|
155
|
+
let keySource = "";
|
|
156
|
+
if (existingKey) {
|
|
157
|
+
tracked.hasApiKey = true;
|
|
158
|
+
keySource = " (key: global config)";
|
|
159
|
+
} else if (envKey) {
|
|
160
|
+
tracked.hasApiKey = true;
|
|
161
|
+
if (!globalConfig.services[service.id]) {
|
|
162
|
+
globalConfig.services[service.id] = {};
|
|
163
|
+
}
|
|
164
|
+
globalConfig.services[service.id].apiKey = envKey;
|
|
165
|
+
keySource = ` (key: ${service.envPatterns[0]})`;
|
|
166
|
+
}
|
|
167
|
+
const tierLabel = tracked.hasApiKey ? "LIVE" : tracked.planCost !== void 0 ? "CALC" : "BLIND";
|
|
168
|
+
const planStr = tracked.planName ? ` ${tracked.planName}` : "";
|
|
169
|
+
console.log(
|
|
170
|
+
` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`
|
|
171
|
+
);
|
|
172
|
+
services[service.id] = tracked;
|
|
173
|
+
}
|
|
174
|
+
console.log("");
|
|
175
|
+
}
|
|
176
|
+
const trackedList = Object.values(services);
|
|
177
|
+
const liveCount = trackedList.filter((s) => s.hasApiKey).length;
|
|
178
|
+
const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);
|
|
179
|
+
console.log(" " + "-".repeat(48));
|
|
180
|
+
console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);
|
|
181
|
+
if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);
|
|
182
|
+
const needBudget = trackedList.filter(
|
|
183
|
+
(s) => s.budget === 0 && s.planCost === void 0
|
|
184
|
+
);
|
|
185
|
+
if (needBudget.length > 0) {
|
|
186
|
+
console.log(`
|
|
187
|
+
${needBudget.length} usage-based service${needBudget.length > 1 ? "s" : ""} need budgets:`);
|
|
188
|
+
for (const svc of needBudget) {
|
|
189
|
+
console.log(` burnwatch add ${svc.serviceId} --budget <AMOUNT>`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
writeGlobalConfig(globalConfig);
|
|
194
|
+
return { services };
|
|
195
|
+
}
|
|
121
196
|
async function runInteractiveInit(detected) {
|
|
122
197
|
const rl = readline.createInterface({
|
|
123
198
|
input: process.stdin,
|
|
@@ -274,6 +349,7 @@ async function runInteractiveInit(detected) {
|
|
|
274
349
|
return { services };
|
|
275
350
|
}
|
|
276
351
|
export {
|
|
352
|
+
autoConfigureServices,
|
|
277
353
|
runInteractiveInit
|
|
278
354
|
};
|
|
279
355
|
//# sourceMappingURL=interactive-init.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/interactive-init.ts","../src/core/config.ts","../src/services/base.ts"],"sourcesContent":["/**\n * Interactive init flow for burnwatch.\n *\n * Conducts a per-service interview: detects what it can automatically\n * (existing API keys, env vars), asks for plan selection, collects\n * API keys for LIVE tracking, and ensures every service exits with\n * a budget. No skipping.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Risk categories in display order: LLMs first, then usage-based, infra, flat-rate */\nconst RISK_ORDER: ServiceRiskCategory[] = [\"llm\", \"usage\", \"infra\", \"flat\"];\n\nconst RISK_LABELS: Record<ServiceRiskCategory, string> = {\n llm: \"LLM / AI Services (highest variable cost)\",\n usage: \"Usage-Based Services\",\n infra: \"Infrastructure & Compute\",\n flat: \"Flat-Rate / Free Tier Services\",\n};\n\n/** Where to find API keys for LIVE-capable services */\nconst API_KEY_HINTS: Record<string, string> = {\n anthropic: \"Admin key: console.anthropic.com -> Settings -> Admin API Keys\",\n openai: \"Org key: platform.openai.com -> Settings -> API Keys\",\n vercel: \"Token: vercel.com/account/tokens\",\n supabase: \"Service role key: supabase.com/dashboard -> Settings -> API\",\n stripe: \"Secret key: dashboard.stripe.com -> Developers -> API Keys\",\n scrapfly: \"API key: scrapfly.io/dashboard\",\n};\n\n/** Map service IDs to risk categories */\nfunction classifyRisk(service: ServiceDefinition): ServiceRiskCategory {\n if (service.billingModel === \"token_usage\") return \"llm\";\n if (\n service.billingModel === \"credit_pool\" ||\n service.billingModel === \"percentage\" ||\n service.billingModel === \"per_unit\"\n )\n return \"usage\";\n if (service.billingModel === \"compute\") return \"infra\";\n return \"flat\";\n}\n\n/** Group detection results by risk category */\nfunction groupByRisk(\n detected: DetectionResult[],\n): Map<ServiceRiskCategory, DetectionResult[]> {\n const groups = new Map<ServiceRiskCategory, DetectionResult[]>();\n for (const cat of RISK_ORDER) {\n groups.set(cat, []);\n }\n\n for (const det of detected) {\n const cat = classifyRisk(det.service);\n groups.get(cat)!.push(det);\n }\n\n return groups;\n}\n\n/** Prompt the user with a question and return their answer */\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/** Try to auto-detect plan from Scrapfly API */\nasync function autoDetectScrapflyPlan(\n apiKey: string,\n): Promise<string | null> {\n try {\n const result = await fetchJson<{\n subscription?: { plan?: { name?: string } };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (result.ok && result.data?.subscription?.plan?.name) {\n return result.data.subscription.plan.name;\n }\n } catch {\n // Ignore errors\n }\n return null;\n}\n\n/** Scan environment for API keys matching service env patterns */\nfunction findEnvKey(service: ServiceDefinition): string | undefined {\n for (const pattern of service.envPatterns) {\n const val = process.env[pattern];\n if (val && val.length > 0) return val;\n }\n return undefined;\n}\n\nexport interface InteractiveInitResult {\n services: Record<string, TrackedService>;\n}\n\n/**\n * Run the interactive init flow.\n *\n * For each detected service:\n * 1. Ask which plan they're on\n * 2. If LIVE-capable, check for existing key or ask for one\n * 3. Set budget (defaults to plan cost, $0 for free - never skipped)\n */\nexport async function runInteractiveInit(\n detected: DetectionResult[],\n): Promise<InteractiveInitResult> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Let's configure each one.\\n`,\n );\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(`\\n ${RISK_LABELS[category]}`);\n console.log(\" \" + \"-\".repeat(48));\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans;\n\n console.log(`\\n ${service.name}`);\n console.log(` Detected via: ${det.details.join(\", \")}`);\n\n if (!plans || plans.length === 0) {\n // No plans defined - basic tracking with $0 budget\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n console.log(\" -> Configured (no plan tiers in registry, budget: $0)\");\n continue;\n }\n\n // --- Plan selection ---\n const defaultIndex = plans.findIndex((p) => p.default);\n console.log(\"\");\n for (let i = 0; i < plans.length; i++) {\n const plan = plans[i]!;\n const marker = i === defaultIndex ? \" *\" : \"\";\n const costStr =\n plan.type === \"exclude\"\n ? \"\"\n : plan.monthlyBase !== undefined\n ? ` - $${plan.monthlyBase}/mo`\n : \" - variable\";\n console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);\n }\n\n const defaultChoice =\n defaultIndex >= 0 ? String(defaultIndex + 1) : \"1\";\n const answer = await ask(\n rl,\n ` Which plan? [${defaultChoice}]: `,\n );\n\n const choiceIndex = (answer === \"\" ? parseInt(defaultChoice) : parseInt(answer)) - 1;\n const chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n\n if (chosen.type === \"exclude\") {\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n excluded: true,\n planName: chosen.name,\n };\n console.log(` -> ${service.name}: excluded`);\n continue;\n }\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n planName: chosen.name,\n };\n\n if (chosen.type === \"flat\" && chosen.monthlyBase !== undefined) {\n tracked.planCost = chosen.monthlyBase;\n }\n\n // --- API key (LIVE-capable services) ---\n if (service.apiTier === \"live\") {\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n console.log(` API key: found in global config`);\n tracked.hasApiKey = true;\n } else if (envKey) {\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n } else {\n const hint = API_KEY_HINTS[service.id];\n if (hint) console.log(` ${hint}`);\n const keyAnswer = await ask(\n rl,\n ` API key for real-time tracking (Enter to skip): `,\n );\n if (keyAnswer) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n }\n }\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\" && tracked.hasApiKey) {\n const key = globalConfig.services[service.id]?.apiKey;\n if (key) {\n console.log(\" Detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(key);\n if (planName) {\n console.log(` -> Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n }\n }\n\n // --- Budget (always set, never skip) ---\n const planCost = chosen.monthlyBase ?? 0;\n\n if (chosen.type === \"usage\" && planCost === 0) {\n // Usage-based with no fixed cost - need a real number\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget: $`,\n );\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : 0;\n } else {\n // Flat plan or usage with known base - default to plan cost\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget [$${planCost}]: $`,\n );\n if (budgetAnswer) {\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : planCost;\n } else {\n tracked.budget = planCost;\n }\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo`,\n );\n }\n }\n\n // --- Summary ---\n const tracked = Object.values(services).filter((s) => !s.excluded);\n const excluded = Object.values(services).filter((s) => s.excluded);\n const liveCount = tracked.filter((s) => s.hasApiKey).length;\n const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\"\\n \" + \"=\".repeat(48));\n console.log(` ${tracked.length} services configured`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);\n if (excluded.length > 0) console.log(` ${excluded.length} excluded`);\n console.log(` Total monthly budget: $${totalBudget}`);\n console.log(\" \" + \"=\".repeat(48));\n\n // Save any collected API keys\n writeGlobalConfig(globalConfig);\n\n rl.close();\n\n return { services };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n"],"mappings":"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","names":["tracked"]}
|
|
1
|
+
{"version":3,"sources":["../src/interactive-init.ts","../src/core/config.ts","../src/services/base.ts"],"sourcesContent":["/**\n * Interactive init flow for burnwatch.\n *\n * Conducts a per-service interview: detects what it can automatically\n * (existing API keys, env vars), asks for plan selection, collects\n * API keys for LIVE tracking, and ensures every service exits with\n * a budget. No skipping.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Risk categories in display order: LLMs first, then usage-based, infra, flat-rate */\nconst RISK_ORDER: ServiceRiskCategory[] = [\"llm\", \"usage\", \"infra\", \"flat\"];\n\nconst RISK_LABELS: Record<ServiceRiskCategory, string> = {\n llm: \"LLM / AI Services (highest variable cost)\",\n usage: \"Usage-Based Services\",\n infra: \"Infrastructure & Compute\",\n flat: \"Flat-Rate / Free Tier Services\",\n};\n\n/** Where to find API keys for LIVE-capable services */\nconst API_KEY_HINTS: Record<string, string> = {\n anthropic: \"Admin key: console.anthropic.com -> Settings -> Admin API Keys\",\n openai: \"Org key: platform.openai.com -> Settings -> API Keys\",\n vercel: \"Token: vercel.com/account/tokens\",\n supabase: \"Service role key: supabase.com/dashboard -> Settings -> API\",\n stripe: \"Secret key: dashboard.stripe.com -> Developers -> API Keys\",\n scrapfly: \"API key: scrapfly.io/dashboard\",\n};\n\n/** Map service IDs to risk categories */\nfunction classifyRisk(service: ServiceDefinition): ServiceRiskCategory {\n if (service.billingModel === \"token_usage\") return \"llm\";\n if (\n service.billingModel === \"credit_pool\" ||\n service.billingModel === \"percentage\" ||\n service.billingModel === \"per_unit\"\n )\n return \"usage\";\n if (service.billingModel === \"compute\") return \"infra\";\n return \"flat\";\n}\n\n/** Group detection results by risk category */\nfunction groupByRisk(\n detected: DetectionResult[],\n): Map<ServiceRiskCategory, DetectionResult[]> {\n const groups = new Map<ServiceRiskCategory, DetectionResult[]>();\n for (const cat of RISK_ORDER) {\n groups.set(cat, []);\n }\n\n for (const det of detected) {\n const cat = classifyRisk(det.service);\n groups.get(cat)!.push(det);\n }\n\n return groups;\n}\n\n/** Prompt the user with a question and return their answer */\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/** Try to auto-detect plan from Scrapfly API */\nasync function autoDetectScrapflyPlan(\n apiKey: string,\n): Promise<string | null> {\n try {\n const result = await fetchJson<{\n subscription?: { plan?: { name?: string } };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (result.ok && result.data?.subscription?.plan?.name) {\n return result.data.subscription.plan.name;\n }\n } catch {\n // Ignore errors\n }\n return null;\n}\n\n/** Scan environment for API keys matching service env patterns */\nfunction findEnvKey(service: ServiceDefinition): string | undefined {\n for (const pattern of service.envPatterns) {\n const val = process.env[pattern];\n if (val && val.length > 0) return val;\n }\n return undefined;\n}\n\nexport interface InteractiveInitResult {\n services: Record<string, TrackedService>;\n}\n\n/**\n * Auto-configure all services without prompts.\n *\n * Applies the same logic as the interactive interview but picks\n * defaults automatically: default plan, env var keys, budget = plan cost.\n * Used when stdin is not a TTY (e.g., Claude Code, piped input).\n */\nexport function autoConfigureServices(\n detected: DetectionResult[],\n): InteractiveInitResult {\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Auto-configuring with defaults.\\n`,\n );\n console.log(\" Run 'burnwatch init' from your terminal for interactive setup.\\n\");\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(` ${RISK_LABELS[category]}`);\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans ?? [];\n const defaultPlan = plans.find((p) => p.default) ?? plans[0];\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n\n if (defaultPlan && defaultPlan.type !== \"exclude\") {\n tracked.planName = defaultPlan.name;\n\n if (defaultPlan.type === \"flat\" && defaultPlan.monthlyBase !== undefined) {\n tracked.planCost = defaultPlan.monthlyBase;\n tracked.budget = defaultPlan.monthlyBase;\n }\n }\n\n // Check for existing API key in global config or environment\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n let keySource = \"\";\n\n if (existingKey) {\n tracked.hasApiKey = true;\n keySource = \" (key: global config)\";\n } else if (envKey) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n keySource = ` (key: ${service.envPatterns[0]})`;\n }\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n const planStr = tracked.planName ? ` ${tracked.planName}` : \"\";\n console.log(\n ` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`,\n );\n\n services[service.id] = tracked;\n }\n console.log(\"\");\n }\n\n // Summary\n const trackedList = Object.values(services);\n const liveCount = trackedList.filter((s) => s.hasApiKey).length;\n const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\" \" + \"-\".repeat(48));\n console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n\n const needBudget = trackedList.filter(\n (s) => s.budget === 0 && s.planCost === undefined,\n );\n if (needBudget.length > 0) {\n console.log(`\\n ${needBudget.length} usage-based service${needBudget.length > 1 ? \"s\" : \"\"} need budgets:`);\n for (const svc of needBudget) {\n console.log(` burnwatch add ${svc.serviceId} --budget <AMOUNT>`);\n }\n }\n console.log(\"\");\n\n // Save discovered keys\n writeGlobalConfig(globalConfig);\n\n return { services };\n}\n\n/**\n * Run the interactive init flow.\n *\n * For each detected service:\n * 1. Ask which plan they're on\n * 2. If LIVE-capable, check for existing key or ask for one\n * 3. Set budget (defaults to plan cost, $0 for free - never skipped)\n */\nexport async function runInteractiveInit(\n detected: DetectionResult[],\n): Promise<InteractiveInitResult> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Let's configure each one.\\n`,\n );\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(`\\n ${RISK_LABELS[category]}`);\n console.log(\" \" + \"-\".repeat(48));\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans;\n\n console.log(`\\n ${service.name}`);\n console.log(` Detected via: ${det.details.join(\", \")}`);\n\n if (!plans || plans.length === 0) {\n // No plans defined - basic tracking with $0 budget\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n console.log(\" -> Configured (no plan tiers in registry, budget: $0)\");\n continue;\n }\n\n // --- Plan selection ---\n const defaultIndex = plans.findIndex((p) => p.default);\n console.log(\"\");\n for (let i = 0; i < plans.length; i++) {\n const plan = plans[i]!;\n const marker = i === defaultIndex ? \" *\" : \"\";\n const costStr =\n plan.type === \"exclude\"\n ? \"\"\n : plan.monthlyBase !== undefined\n ? ` - $${plan.monthlyBase}/mo`\n : \" - variable\";\n console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);\n }\n\n const defaultChoice =\n defaultIndex >= 0 ? String(defaultIndex + 1) : \"1\";\n const answer = await ask(\n rl,\n ` Which plan? [${defaultChoice}]: `,\n );\n\n const choiceIndex = (answer === \"\" ? parseInt(defaultChoice) : parseInt(answer)) - 1;\n const chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n\n if (chosen.type === \"exclude\") {\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n excluded: true,\n planName: chosen.name,\n };\n console.log(` -> ${service.name}: excluded`);\n continue;\n }\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n planName: chosen.name,\n };\n\n if (chosen.type === \"flat\" && chosen.monthlyBase !== undefined) {\n tracked.planCost = chosen.monthlyBase;\n }\n\n // --- API key (LIVE-capable services) ---\n if (service.apiTier === \"live\") {\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n console.log(` API key: found in global config`);\n tracked.hasApiKey = true;\n } else if (envKey) {\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n } else {\n const hint = API_KEY_HINTS[service.id];\n if (hint) console.log(` ${hint}`);\n const keyAnswer = await ask(\n rl,\n ` API key for real-time tracking (Enter to skip): `,\n );\n if (keyAnswer) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n }\n }\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\" && tracked.hasApiKey) {\n const key = globalConfig.services[service.id]?.apiKey;\n if (key) {\n console.log(\" Detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(key);\n if (planName) {\n console.log(` -> Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n }\n }\n\n // --- Budget (always set, never skip) ---\n const planCost = chosen.monthlyBase ?? 0;\n\n if (chosen.type === \"usage\" && planCost === 0) {\n // Usage-based with no fixed cost - need a real number\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget: $`,\n );\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : 0;\n } else {\n // Flat plan or usage with known base - default to plan cost\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget [$${planCost}]: $`,\n );\n if (budgetAnswer) {\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : planCost;\n } else {\n tracked.budget = planCost;\n }\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo`,\n );\n }\n }\n\n // --- Summary ---\n const tracked = Object.values(services).filter((s) => !s.excluded);\n const excluded = Object.values(services).filter((s) => s.excluded);\n const liveCount = tracked.filter((s) => s.hasApiKey).length;\n const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\"\\n \" + \"=\".repeat(48));\n console.log(` ${tracked.length} services configured`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);\n if (excluded.length > 0) console.log(` ${excluded.length} excluded`);\n console.log(` Total monthly budget: $${totalBudget}`);\n console.log(\" \" + \"=\".repeat(48));\n\n // Save any collected API keys\n writeGlobalConfig(globalConfig);\n\n rl.close();\n\n return { services };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n"],"mappings":";AASA,YAAY,cAAc;;;ACT1B,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,YAAY,QAAQ;AAab,SAAS,kBAA0B;AACxC,QAAM,YAAY,QAAQ,IAAI,iBAAiB;AAC/C,MAAI,UAAW,QAAY,UAAK,WAAW,WAAW;AACtD,SAAY,UAAQ,WAAQ,GAAG,WAAW,WAAW;AACvD;AA0BO,SAAS,mBAAiC;AAC/C,QAAM,aAAkB,UAAK,gBAAgB,GAAG,aAAa;AAC7D,MAAI;AACF,UAAM,MAAS,gBAAa,YAAY,OAAO;AAC/C,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO,EAAE,UAAU,CAAC,EAAE;AAAA,EACxB;AACF;AAEO,SAAS,kBAAkB,QAA4B;AAC5D,QAAM,MAAM,gBAAgB;AAC5B,EAAG,aAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,QAAM,aAAkB,UAAK,KAAK,aAAa;AAC/C,EAAG,iBAAc,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,MAAM,OAAO;AAE5E,EAAG,aAAU,YAAY,GAAK;AAChC;;;ACpCA,eAAsB,UACpB,KACA,UAKI,CAAC,GAC+D;AACpE,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY;AAAA,MAChB,MAAM,WAAW,MAAM;AAAA,MACvB,QAAQ,WAAW;AAAA,IACrB;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ,QAAQ,UAAU;AAAA,MAC1B,SAAS,QAAQ;AAAA,MACjB,MAAM,QAAQ;AAAA,MACd,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,SAAS;AAEtB,QAAI,CAAC,SAAS,IAAI;AAChB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,SAAS;AAAA,QACjB,OAAO,QAAQ,SAAS,MAAM,KAAK,SAAS,UAAU;AAAA,MACxD;AAAA,IACF;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,EAAE,IAAI,MAAM,QAAQ,SAAS,QAAQ,KAAK;AAAA,EACnD,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,OAAO,eAAe,QAAQ,IAAI,UAAU;AAAA,IAC9C;AAAA,EACF;AACF;;;AF/CA,IAAM,aAAoC,CAAC,OAAO,SAAS,SAAS,MAAM;AAE1E,IAAM,cAAmD;AAAA,EACvD,KAAK;AAAA,EACL,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AACR;AAGA,IAAM,gBAAwC;AAAA,EAC5C,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,UAAU;AACZ;AAGA,SAAS,aAAa,SAAiD;AACrE,MAAI,QAAQ,iBAAiB,cAAe,QAAO;AACnD,MACE,QAAQ,iBAAiB,iBACzB,QAAQ,iBAAiB,gBACzB,QAAQ,iBAAiB;AAEzB,WAAO;AACT,MAAI,QAAQ,iBAAiB,UAAW,QAAO;AAC/C,SAAO;AACT;AAGA,SAAS,YACP,UAC6C;AAC7C,QAAM,SAAS,oBAAI,IAA4C;AAC/D,aAAW,OAAO,YAAY;AAC5B,WAAO,IAAI,KAAK,CAAC,CAAC;AAAA,EACpB;AAEA,aAAW,OAAO,UAAU;AAC1B,UAAM,MAAM,aAAa,IAAI,OAAO;AACpC,WAAO,IAAI,GAAG,EAAG,KAAK,GAAG;AAAA,EAC3B;AAEA,SAAO;AACT;AAGA,SAAS,IAAI,IAAwB,UAAmC;AACtE,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,OAAG,SAAS,UAAU,CAAC,WAAW;AAChC,cAAQ,OAAO,KAAK,CAAC;AAAA,IACvB,CAAC;AAAA,EACH,CAAC;AACH;AAGA,eAAe,uBACb,QACwB;AACxB,MAAI;AACF,UAAM,SAAS,MAAM,UAElB,uCAAuC,MAAM,EAAE;AAElD,QAAI,OAAO,MAAM,OAAO,MAAM,cAAc,MAAM,MAAM;AACtD,aAAO,OAAO,KAAK,aAAa,KAAK;AAAA,IACvC;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGA,SAAS,WAAW,SAAgD;AAClE,aAAW,WAAW,QAAQ,aAAa;AACzC,UAAM,MAAM,QAAQ,IAAI,OAAO;AAC/B,QAAI,OAAO,IAAI,SAAS,EAAG,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAaO,SAAS,sBACd,UACuB;AACvB,QAAM,WAA2C,CAAC;AAClD,QAAM,SAAS,YAAY,QAAQ;AACnC,QAAM,eAAe,iBAAiB;AAEtC,UAAQ;AAAA,IACN;AAAA,UAAa,SAAS,MAAM,gBAAgB,SAAS,WAAW,IAAI,MAAM,EAAE;AAAA;AAAA,EAC9E;AACA,UAAQ,IAAI,oEAAoE;AAEhF,aAAW,YAAY,YAAY;AACjC,UAAM,QAAQ,OAAO,IAAI,QAAQ;AACjC,QAAI,MAAM,WAAW,EAAG;AAExB,YAAQ,IAAI,KAAK,YAAY,QAAQ,CAAC,EAAE;AAExC,eAAW,OAAO,OAAO;AACvB,YAAM,UAAU,IAAI;AACpB,YAAM,QAAQ,QAAQ,SAAS,CAAC;AAChC,YAAM,cAAc,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC;AAE3D,YAAM,UAA0B;AAAA,QAC9B,WAAW,QAAQ;AAAA,QACnB,aAAa,IAAI;AAAA,QACjB,WAAW;AAAA,QACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,QACtC,QAAQ;AAAA,MACV;AAEA,UAAI,eAAe,YAAY,SAAS,WAAW;AACjD,gBAAQ,WAAW,YAAY;AAE/B,YAAI,YAAY,SAAS,UAAU,YAAY,gBAAgB,QAAW;AACxE,kBAAQ,WAAW,YAAY;AAC/B,kBAAQ,SAAS,YAAY;AAAA,QAC/B;AAAA,MACF;AAGA,YAAM,cAAc,aAAa,SAAS,QAAQ,EAAE,GAAG;AACvD,YAAM,SAAS,WAAW,OAAO;AACjC,UAAI,YAAY;AAEhB,UAAI,aAAa;AACf,gBAAQ,YAAY;AACpB,oBAAY;AAAA,MACd,WAAW,QAAQ;AACjB,gBAAQ,YAAY;AACpB,YAAI,CAAC,aAAa,SAAS,QAAQ,EAAE,GAAG;AACtC,uBAAa,SAAS,QAAQ,EAAE,IAAI,CAAC;AAAA,QACvC;AACA,qBAAa,SAAS,QAAQ,EAAE,EAAG,SAAS;AAC5C,oBAAY,UAAU,QAAQ,YAAY,CAAC,CAAC;AAAA,MAC9C;AAEA,YAAM,YAAY,QAAQ,YACtB,SACA,QAAQ,aAAa,SACnB,SACA;AACN,YAAM,UAAU,QAAQ,WAAW,IAAI,QAAQ,QAAQ,KAAK;AAC5D,cAAQ;AAAA,QACN,OAAO,QAAQ,IAAI,IAAI,OAAO,MAAM,SAAS,OAAO,QAAQ,MAAM,MAAM,SAAS;AAAA,MACnF;AAEA,eAAS,QAAQ,EAAE,IAAI;AAAA,IACzB;AACA,YAAQ,IAAI,EAAE;AAAA,EAChB;AAGA,QAAM,cAAc,OAAO,OAAO,QAAQ;AAC1C,QAAM,YAAY,YAAY,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE;AACzD,QAAM,cAAc,YAAY,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,UAAU,IAAI,CAAC;AAE3E,UAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;AACjC,UAAQ,IAAI,KAAK,YAAY,MAAM,yCAAyC,WAAW,KAAK;AAC5F,MAAI,YAAY,EAAG,SAAQ,IAAI,KAAK,SAAS,gCAAgC;AAE7E,QAAM,aAAa,YAAY;AAAA,IAC7B,CAAC,MAAM,EAAE,WAAW,KAAK,EAAE,aAAa;AAAA,EAC1C;AACA,MAAI,WAAW,SAAS,GAAG;AACzB,YAAQ,IAAI;AAAA,IAAO,WAAW,MAAM,uBAAuB,WAAW,SAAS,IAAI,MAAM,EAAE,gBAAgB;AAC3G,eAAW,OAAO,YAAY;AAC5B,cAAQ,IAAI,qBAAqB,IAAI,SAAS,oBAAoB;AAAA,IACpE;AAAA,EACF;AACA,UAAQ,IAAI,EAAE;AAGd,oBAAkB,YAAY;AAE9B,SAAO,EAAE,SAAS;AACpB;AAUA,eAAsB,mBACpB,UACgC;AAChC,QAAM,KAAc,yBAAgB;AAAA,IAClC,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,EAClB,CAAC;AAED,QAAM,WAA2C,CAAC;AAClD,QAAM,SAAS,YAAY,QAAQ;AACnC,QAAM,eAAe,iBAAiB;AAEtC,UAAQ;AAAA,IACN;AAAA,UAAa,SAAS,MAAM,gBAAgB,SAAS,WAAW,IAAI,MAAM,EAAE;AAAA;AAAA,EAC9E;AAEA,aAAW,YAAY,YAAY;AACjC,UAAM,QAAQ,OAAO,IAAI,QAAQ;AACjC,QAAI,MAAM,WAAW,EAAG;AAExB,YAAQ,IAAI;AAAA,IAAO,YAAY,QAAQ,CAAC,EAAE;AAC1C,YAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;AAEjC,eAAW,OAAO,OAAO;AACvB,YAAM,UAAU,IAAI;AACpB,YAAM,QAAQ,QAAQ;AAEtB,cAAQ,IAAI;AAAA,IAAO,QAAQ,IAAI,EAAE;AACjC,cAAQ,IAAI,mBAAmB,IAAI,QAAQ,KAAK,IAAI,CAAC,EAAE;AAEvD,UAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAEhC,iBAAS,QAAQ,EAAE,IAAI;AAAA,UACrB,WAAW,QAAQ;AAAA,UACnB,aAAa,IAAI;AAAA,UACjB,WAAW;AAAA,UACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,UACtC,QAAQ;AAAA,QACV;AACA,gBAAQ,IAAI,yDAAyD;AACrE;AAAA,MACF;AAGA,YAAM,eAAe,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO;AACrD,cAAQ,IAAI,EAAE;AACd,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAM,OAAO,MAAM,CAAC;AACpB,cAAM,SAAS,MAAM,eAAe,OAAO;AAC3C,cAAM,UACJ,KAAK,SAAS,YACV,KACA,KAAK,gBAAgB,SACnB,OAAO,KAAK,WAAW,QACvB;AACR,gBAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,IAAI,GAAG,OAAO,GAAG,MAAM,EAAE;AAAA,MAC7D;AAEA,YAAM,gBACJ,gBAAgB,IAAI,OAAO,eAAe,CAAC,IAAI;AACjD,YAAM,SAAS,MAAM;AAAA,QACnB;AAAA,QACA,kBAAkB,aAAa;AAAA,MACjC;AAEA,YAAM,eAAe,WAAW,KAAK,SAAS,aAAa,IAAI,SAAS,MAAM,KAAK;AACnF,YAAM,SACJ,MAAM,WAAW,KAAK,MAAM,gBAAgB,IAAI,eAAe,CAAC;AAElE,UAAI,OAAO,SAAS,WAAW;AAC7B,iBAAS,QAAQ,EAAE,IAAI;AAAA,UACrB,WAAW,QAAQ;AAAA,UACnB,aAAa,IAAI;AAAA,UACjB,WAAW;AAAA,UACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,UACtC,UAAU;AAAA,UACV,UAAU,OAAO;AAAA,QACnB;AACA,gBAAQ,IAAI,QAAQ,QAAQ,IAAI,YAAY;AAC5C;AAAA,MACF;AAEA,YAAMA,WAA0B;AAAA,QAC9B,WAAW,QAAQ;AAAA,QACnB,aAAa,IAAI;AAAA,QACjB,WAAW;AAAA,QACX,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,QACtC,UAAU,OAAO;AAAA,MACnB;AAEA,UAAI,OAAO,SAAS,UAAU,OAAO,gBAAgB,QAAW;AAC9D,QAAAA,SAAQ,WAAW,OAAO;AAAA,MAC5B;AAGA,UAAI,QAAQ,YAAY,QAAQ;AAC9B,cAAM,cAAc,aAAa,SAAS,QAAQ,EAAE,GAAG;AACvD,cAAM,SAAS,WAAW,OAAO;AAEjC,YAAI,aAAa;AACf,kBAAQ,IAAI,mCAAmC;AAC/C,UAAAA,SAAQ,YAAY;AAAA,QACtB,WAAW,QAAQ;AACjB,kBAAQ,IAAI,oCAAoC,QAAQ,YAAY,CAAC,CAAC,GAAG;AACzE,UAAAA,SAAQ,YAAY;AACpB,cAAI,CAAC,aAAa,SAAS,QAAQ,EAAE,GAAG;AACtC,yBAAa,SAAS,QAAQ,EAAE,IAAI,CAAC;AAAA,UACvC;AACA,uBAAa,SAAS,QAAQ,EAAE,EAAG,SAAS;AAAA,QAC9C,OAAO;AACL,gBAAM,OAAO,cAAc,QAAQ,EAAE;AACrC,cAAI,KAAM,SAAQ,IAAI,KAAK,IAAI,EAAE;AACjC,gBAAM,YAAY,MAAM;AAAA,YACtB;AAAA,YACA;AAAA,UACF;AACA,cAAI,WAAW;AACb,YAAAA,SAAQ,YAAY;AACpB,gBAAI,CAAC,aAAa,SAAS,QAAQ,EAAE,GAAG;AACtC,2BAAa,SAAS,QAAQ,EAAE,IAAI,CAAC;AAAA,YACvC;AACA,yBAAa,SAAS,QAAQ,EAAE,EAAG,SAAS;AAAA,UAC9C;AAAA,QACF;AAGA,YAAI,QAAQ,kBAAkB,QAAQ,OAAO,cAAcA,SAAQ,WAAW;AAC5E,gBAAM,MAAM,aAAa,SAAS,QAAQ,EAAE,GAAG;AAC/C,cAAI,KAAK;AACP,oBAAQ,IAAI,8BAA8B;AAC1C,kBAAM,WAAW,MAAM,uBAAuB,GAAG;AACjD,gBAAI,UAAU;AACZ,sBAAQ,IAAI,uBAAuB,QAAQ,EAAE;AAC7C,cAAAA,SAAQ,WAAW;AAAA,YACrB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAGA,YAAM,WAAW,OAAO,eAAe;AAEvC,UAAI,OAAO,SAAS,WAAW,aAAa,GAAG;AAE7C,cAAM,eAAe,MAAM;AAAA,UACzB;AAAA,UACA;AAAA,QACF;AACA,cAAM,SAAS,WAAW,YAAY;AACtC,QAAAA,SAAQ,SAAS,CAAC,MAAM,MAAM,IAAI,SAAS;AAAA,MAC7C,OAAO;AAEL,cAAM,eAAe,MAAM;AAAA,UACzB;AAAA,UACA,sBAAsB,QAAQ;AAAA,QAChC;AACA,YAAI,cAAc;AAChB,gBAAM,SAAS,WAAW,YAAY;AACtC,UAAAA,SAAQ,SAAS,CAAC,MAAM,MAAM,IAAI,SAAS;AAAA,QAC7C,OAAO;AACL,UAAAA,SAAQ,SAAS;AAAA,QACnB;AAAA,MACF;AAEA,eAAS,QAAQ,EAAE,IAAIA;AAEvB,YAAM,YAAYA,SAAQ,YACtB,SACAA,SAAQ,aAAa,SACnB,SACA;AACN,cAAQ;AAAA,QACN,QAAQ,QAAQ,IAAI,KAAKA,SAAQ,QAAQ,MAAM,SAAS,OAAOA,SAAQ,MAAM;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAAU,OAAO,OAAO,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ;AACjE,QAAM,WAAW,OAAO,OAAO,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ;AACjE,QAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE;AACrD,QAAM,cAAc,QAAQ,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,UAAU,IAAI,CAAC;AAEvE,UAAQ,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;AACnC,UAAQ,IAAI,KAAK,QAAQ,MAAM,sBAAsB;AACrD,MAAI,YAAY,EAAG,SAAQ,IAAI,OAAO,SAAS,gCAAgC;AAC/E,MAAI,QAAQ,SAAS,YAAY,EAAG,SAAQ,IAAI,OAAO,QAAQ,SAAS,SAAS,uBAAuB;AACxG,MAAI,SAAS,SAAS,EAAG,SAAQ,IAAI,OAAO,SAAS,MAAM,WAAW;AACtE,UAAQ,IAAI,4BAA4B,WAAW,EAAE;AACrD,UAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;AAGjC,oBAAkB,YAAY;AAE9B,KAAG,MAAM;AAET,SAAO,EAAE,SAAS;AACpB;","names":["tracked"]}
|
package/package.json
CHANGED