burnwatch 0.8.0 → 0.8.2
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/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/burnwatch-interview/SKILL.md +75 -88
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1913,7 +1913,7 @@ async function cmdReconcile() {
|
|
|
1913
1913
|
}
|
|
1914
1914
|
function cmdHelp() {
|
|
1915
1915
|
console.log(`
|
|
1916
|
-
burnwatch \u2014 Passive cost memory for
|
|
1916
|
+
burnwatch \u2014 Passive cost memory for AI-assisted development
|
|
1917
1917
|
|
|
1918
1918
|
Usage:
|
|
1919
1919
|
burnwatch init Interactive setup \u2014 pick plans per service
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/services/base.ts","../src/probes.ts","../src/cli.ts","../src/core/config.ts","../src/detection/detector.ts","../src/core/registry.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":["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 /** For credit-pool services: units consumed this period */\n unitsUsed?: number;\n /** For credit-pool services: total units in plan allowance */\n unitsTotal?: number;\n /** For credit-pool services: unit name (e.g., \"credits\") */\n unitName?: 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","/**\n * Service probes — auto-detect plan, usage, and billing data from APIs.\n *\n * Each probe tries to discover as much as possible from a service's API:\n * 1. Plan tier (best case — \"You're on Pro\")\n * 2. Usage/spend data (good — \"You've used 850K credits\")\n * 3. Key validation (minimum — \"Your key works\")\n *\n * Probes are extensible: add a new entry to PROBES to support a new service.\n * The interview flow calls `probeService()` which looks up the right probe.\n */\n\nimport type { PlanTier } from \"./core/types.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Result from probing a service API */\nexport interface ProbeResult {\n /** Detected plan name (should match a registry PlanTier.name prefix) */\n planName?: string;\n /** Matched PlanTier from the registry */\n matchedPlan?: PlanTier;\n /** Usage/spend data discovered */\n usage?: {\n unitsUsed?: number;\n unitsTotal?: number;\n unitName?: string;\n spend?: number;\n currency?: string;\n };\n /** Human-readable summary of what was found */\n summary: string;\n /**\n * Discovery confidence:\n * high — plan tier identified (can skip plan selection)\n * medium — usage data found but plan unclear (show data, still ask plan)\n * low — key validates but no plan/usage info\n */\n confidence: \"high\" | \"medium\" | \"low\";\n}\n\n/** A probe function: given an API key and registry plans, discover what we can */\ntype ProbeFn = (\n apiKey: string,\n plans: PlanTier[],\n) => Promise<ProbeResult | null>;\n\n// ---------------------------------------------------------------------------\n// Service-specific probes\n// ---------------------------------------------------------------------------\n\n/** Match a detected plan name against registry plans */\nfunction matchPlan(\n detected: string,\n plans: PlanTier[],\n): PlanTier | undefined {\n const lower = detected.toLowerCase();\n return plans.find(\n (p) =>\n p.type !== \"exclude\" &&\n p.name.toLowerCase().includes(lower),\n );\n}\n\n/** Match by checking if the detected name appears as the first word of any plan */\nfunction matchPlanByPrefix(\n detected: string,\n plans: PlanTier[],\n): PlanTier | undefined {\n const lower = detected.toLowerCase();\n return plans.find((p) => {\n if (p.type === \"exclude\") return false;\n const firstWord = p.name.split(/[\\s(]/)[0]!.toLowerCase();\n return lower.includes(firstWord) || firstWord.includes(lower);\n });\n}\n\n// --- Scrapfly ---\nconst probeScrapfly: ProbeFn = async (apiKey, plans) => {\n const result = await fetchJson<{\n subscription?: {\n plan?: { name?: string };\n usage?: { scrape?: { used?: number; allowed?: number } };\n };\n account?: { credits_used?: number; credits_total?: number };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (!result.ok || !result.data) return null;\n\n const planName = result.data.subscription?.plan?.name;\n let unitsUsed = 0;\n let unitsTotal = 0;\n\n if (result.data.subscription?.usage?.scrape) {\n unitsUsed = result.data.subscription.usage.scrape.used ?? 0;\n unitsTotal = result.data.subscription.usage.scrape.allowed ?? 0;\n } else if (result.data.account) {\n unitsUsed = result.data.account.credits_used ?? 0;\n unitsTotal = result.data.account.credits_total ?? 0;\n }\n\n const matched = planName ? matchPlanByPrefix(planName, plans) : undefined;\n\n return {\n planName: planName ?? undefined,\n matchedPlan: matched,\n usage: {\n unitsUsed,\n unitsTotal,\n unitName: \"credits\",\n },\n summary: matched\n ? `${matched.name} — ${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used`\n : `${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used`,\n confidence: matched ? \"high\" : \"medium\",\n };\n};\n\n// --- Anthropic ---\nconst probeAnthropic: ProbeFn = async (apiKey, _plans) => {\n // Anthropic Admin API: GET /v1/organizations/cost_report\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const params = new URLSearchParams({\n start_date: startOfMonth.toISOString().split(\"T\")[0]!,\n end_date: now.toISOString().split(\"T\")[0]!,\n });\n\n const result = await fetchJson<{\n data?: Array<{ amount?: string; cost_type?: string }>;\n }>(`https://api.anthropic.com/v1/organizations/cost_report?${params}`, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n });\n\n if (!result.ok || !result.data?.data) return null;\n\n // Sum costs (returned in cents as strings)\n let totalCents = 0;\n for (const entry of result.data.data) {\n totalCents += parseFloat(entry.amount ?? \"0\");\n }\n const spend = totalCents / 100;\n\n return {\n usage: { spend, currency: \"USD\" },\n summary: `$${spend.toFixed(2)} spent this billing period`,\n confidence: \"medium\",\n };\n};\n\n// --- OpenAI ---\nconst probeOpenAI: ProbeFn = async (apiKey, _plans) => {\n // OpenAI Admin API: GET /v1/organization/usage/completions\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const params = new URLSearchParams({\n start_time: String(Math.floor(startOfMonth.getTime() / 1000)),\n end_time: String(Math.floor(now.getTime() / 1000)),\n });\n\n const result = await fetchJson<{\n data?: Array<{ results?: Array<{ amount?: { value?: number } }> }>;\n }>(`https://api.openai.com/v1/organization/usage/completions?${params}`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!result.ok || !result.data?.data) return null;\n\n // Sum usage values\n let totalTokens = 0;\n for (const bucket of result.data.data) {\n for (const r of bucket.results ?? []) {\n totalTokens += r.amount?.value ?? 0;\n }\n }\n\n return {\n usage: { unitsUsed: totalTokens, unitName: \"tokens\" },\n summary: `${formatK(totalTokens)} tokens used this period`,\n confidence: \"medium\",\n };\n};\n\n// --- Vercel ---\nconst probeVercel: ProbeFn = async (apiKey, plans) => {\n // Try to get team info first\n const teamsResult = await fetchJson<{\n teams?: Array<{ id?: string; name?: string; billing?: { plan?: string } }>;\n }>(\"https://api.vercel.com/v2/teams\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (teamsResult.ok && teamsResult.data?.teams?.[0]) {\n const team = teamsResult.data.teams[0];\n const planName = team.billing?.plan;\n if (planName) {\n const matched = matchPlanByPrefix(planName, plans);\n return {\n planName,\n matchedPlan: matched,\n summary: `Team \"${team.name}\" on ${planName} plan`,\n confidence: matched ? \"high\" : \"medium\",\n };\n }\n }\n\n // Fallback: try user endpoint for hobby plan detection\n const userResult = await fetchJson<{\n user?: { billing?: { plan?: string }; name?: string };\n }>(\"https://api.vercel.com/v2/user\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (userResult.ok && userResult.data?.user) {\n const plan = userResult.data.user.billing?.plan ?? \"hobby\";\n const matched = matchPlanByPrefix(plan, plans);\n return {\n planName: plan,\n matchedPlan: matched,\n summary: `Personal account on ${plan} plan`,\n confidence: matched ? \"high\" : \"low\",\n };\n }\n\n return null;\n};\n\n// --- Supabase ---\nconst probeSupabase: ProbeFn = async (apiKey, plans) => {\n // Supabase Management API requires a PAT, not service_role_key\n const orgsResult = await fetchJson<\n Array<{ id?: string; name?: string; billing?: { plan?: string }; subscription_id?: string }>\n >(\"https://api.supabase.com/v1/organizations\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!orgsResult.ok || !orgsResult.data || !Array.isArray(orgsResult.data)) return null;\n\n const org = orgsResult.data[0];\n if (!org) return null;\n\n const planName = org.billing?.plan;\n if (planName) {\n const matched = matchPlanByPrefix(planName, plans);\n return {\n planName,\n matchedPlan: matched,\n summary: `Org \"${org.name}\" on ${planName} plan`,\n confidence: matched ? \"high\" : \"medium\",\n };\n }\n\n return {\n summary: `Org \"${org.name}\" found (plan not detected)`,\n confidence: \"low\",\n };\n};\n\n// --- Stripe ---\nconst probeStripe: ProbeFn = async (apiKey, _plans) => {\n const result = await fetchJson<{\n available?: Array<{ amount?: number; currency?: string }>;\n pending?: Array<{ amount?: number; currency?: string }>;\n }>(\"https://api.stripe.com/v1/balance\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!result.ok || !result.data) return null;\n\n const available = result.data.available?.[0];\n const pending = result.data.pending?.[0];\n const totalCents = (available?.amount ?? 0) + (pending?.amount ?? 0);\n const currency = (available?.currency ?? \"usd\").toUpperCase();\n\n return {\n usage: { spend: totalCents / 100, currency },\n summary: `Balance: ${currency} ${(totalCents / 100).toFixed(2)} (${((available?.amount ?? 0) / 100).toFixed(2)} available)`,\n confidence: \"medium\",\n };\n};\n\n// --- Browserbase ---\nconst probeBrowserbase: ProbeFn = async (apiKey, _plans) => {\n // Browserbase: GET /v1/projects (to get project ID), then usage\n const projResult = await fetchJson<\n Array<{ id?: string; name?: string }>\n >(\"https://api.browserbase.com/v1/projects\", {\n headers: { \"X-BB-API-Key\": apiKey },\n });\n\n if (!projResult.ok || !projResult.data?.[0]?.id) return null;\n\n const projectId = projResult.data[0].id;\n const usageResult = await fetchJson<{\n sessions_count?: number;\n browser_hours?: number;\n }>(`https://api.browserbase.com/v1/projects/${projectId}/usage`, {\n headers: { \"X-BB-API-Key\": apiKey },\n });\n\n if (!usageResult.ok || !usageResult.data) {\n return {\n summary: `Project \"${projResult.data[0].name}\" found`,\n confidence: \"low\",\n };\n }\n\n const sessions = usageResult.data.sessions_count ?? 0;\n const hours = usageResult.data.browser_hours ?? 0;\n\n return {\n usage: { unitsUsed: sessions, unitName: \"sessions\" },\n summary: `${sessions} sessions, ${hours.toFixed(1)} browser hours this period`,\n confidence: \"medium\",\n };\n};\n\n// --- Upstash ---\nconst probeUpstash: ProbeFn = async (apiKey, _plans) => {\n // Upstash uses email:api_key basic auth for the management API\n // The key from env is typically the Redis REST token, not management key\n // Try the databases list endpoint\n const result = await fetchJson<\n Array<{ database_id?: string; database_name?: string; region?: string }>\n >(\"https://api.upstash.com/v2/redis/databases\", {\n headers: {\n Authorization: `Basic ${Buffer.from(apiKey).toString(\"base64\")}`,\n },\n });\n\n if (!result.ok) return null;\n\n const dbCount = Array.isArray(result.data) ? result.data.length : 0;\n return {\n summary: `${dbCount} Redis database${dbCount !== 1 ? \"s\" : \"\"} found`,\n confidence: \"low\",\n };\n};\n\n// --- PostHog ---\nconst probePostHog: ProbeFn = async (apiKey, _plans) => {\n const result = await fetchJson<{\n results?: Array<{ id?: string; name?: string }>;\n }>(\"https://us.posthog.com/api/organizations/@current\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!result.ok || !result.data) return null;\n\n return {\n summary: \"Organization found\",\n confidence: \"low\",\n };\n};\n\n// ---------------------------------------------------------------------------\n// Probe registry — maps service IDs to their probe function\n// ---------------------------------------------------------------------------\n\nconst PROBES: Map<string, ProbeFn> = new Map([\n [\"scrapfly\", probeScrapfly],\n [\"anthropic\", probeAnthropic],\n [\"openai\", probeOpenAI],\n [\"vercel\", probeVercel],\n [\"supabase\", probeSupabase],\n [\"stripe\", probeStripe],\n [\"browserbase\", probeBrowserbase],\n [\"upstash\", probeUpstash],\n [\"posthog\", probePostHog],\n]);\n\n/**\n * Probe a service using its API key.\n * Returns null if no probe exists for the service or the probe fails.\n */\nexport async function probeService(\n serviceId: string,\n apiKey: string,\n plans: PlanTier[],\n): Promise<ProbeResult | null> {\n const probe = PROBES.get(serviceId);\n if (!probe) return null;\n\n try {\n return await probe(apiKey, plans);\n } catch {\n return null;\n }\n}\n\n/** Check if a service has a probe available */\nexport function hasProbe(serviceId: string): boolean {\n return PROBES.has(serviceId);\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatK(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;\n return String(n);\n}\n","#!/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 \"interview\":\n await cmdInterview();\n break;\n case \"configure\":\n await cmdConfigure();\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 = await 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\n/**\n * burnwatch interview --json\n *\n * Exports the current project state as structured JSON for an AI agent\n * to conduct the interview conversationally. The agent reads this,\n * asks the user questions, then writes answers back via `burnwatch configure`.\n */\nasync function cmdInterview(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n // Auto-init first\n ensureProjectDirs(projectRoot);\n const detected = detectServices(projectRoot);\n let projectName = path.basename(projectRoot);\n try {\n const pkg = JSON.parse(\n fs.readFileSync(path.join(projectRoot, \"package.json\"), \"utf-8\"),\n ) as { name?: string };\n if (pkg.name) projectName = pkg.name;\n } catch {\n // Use directory name\n }\n\n const config: ProjectConfig = {\n projectName,\n services: {},\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n // Auto-configure with defaults first\n const result = await autoConfigureServices(detected);\n config.services = result.services;\n writeProjectConfig(config, projectRoot);\n }\n\n const config = readProjectConfig(projectRoot)!;\n const globalConfig = readGlobalConfig();\n const allRegistryServices = getAllServices(projectRoot);\n\n // Build structured export for each tracked service\n const serviceStates: Array<{\n serviceId: string;\n serviceName: string;\n currentPlan: string | null;\n currentBudget: number | null;\n hasApiKey: boolean;\n keySource: string | null;\n tier: string;\n excluded: boolean;\n hasProbe: boolean;\n probeResult: import(\"./probes.js\").ProbeResult | null;\n availablePlans: Array<{\n index: number;\n name: string;\n type: string;\n monthlyCost: number | null;\n includedUnits: number | null;\n unitName: string | null;\n suggestedBudget: number | null;\n isDefault: boolean;\n }>;\n riskCategory: string;\n billingModel: string;\n apiKeyHint: string | null;\n allowance: { included: number; unitName: string } | null;\n }> = [];\n\n for (const [serviceId, tracked] of Object.entries(config.services)) {\n const definition = allRegistryServices.find((s) => s.id === serviceId);\n if (!definition) continue;\n\n // Determine key source\n let keySource: string | null = null;\n const globalKey = globalConfig.services[serviceId]?.apiKey;\n if (globalKey) keySource = \"global_config\";\n else {\n for (const pattern of definition.envPatterns) {\n if (process.env[pattern]) {\n keySource = `env:${pattern}`;\n break;\n }\n }\n }\n\n // Try probing if we have a key\n let probeResult: import(\"./probes.js\").ProbeResult | null = null;\n const { probeService: probe, hasProbe: checkProbe } = await import(\"./probes.js\");\n const apiKey = globalKey ?? (keySource?.startsWith(\"env:\") ? process.env[keySource.slice(4)] : undefined);\n if (apiKey && checkProbe(serviceId)) {\n probeResult = await probe(serviceId, apiKey, definition.plans ?? []);\n }\n\n // Determine tier\n let tier = \"blind\";\n if (tracked.excluded) tier = \"excluded\";\n else if (tracked.hasApiKey) tier = \"live\";\n else if (tracked.planCost !== undefined) tier = \"calc\";\n\n // Risk category\n let riskCategory = \"flat\";\n if (definition.billingModel === \"token_usage\") riskCategory = \"llm\";\n else if ([\"credit_pool\", \"percentage\", \"per_unit\"].includes(definition.billingModel)) riskCategory = \"usage\";\n else if (definition.billingModel === \"compute\") riskCategory = \"infra\";\n\n const keyHints: Record<string, string> = {\n anthropic: \"Admin key from console.anthropic.com → Settings → Admin API Keys (sk-ant-admin-*)\",\n openai: \"Admin key from platform.openai.com → Settings → API Keys (sk-admin-*)\",\n vercel: \"Token from vercel.com/account/tokens\",\n supabase: \"PAT from supabase.com/dashboard → Account → Access Tokens (not service_role key)\",\n stripe: \"Secret key from dashboard.stripe.com → Developers → API Keys (sk_live_*)\",\n scrapfly: \"API key from scrapfly.io/dashboard\",\n browserbase: \"API key from browserbase.com → Settings → API Keys\",\n upstash: \"email:api_key from console.upstash.com → Account → Management API\",\n posthog: \"Personal API key from posthog.com → Settings → Personal API Keys\",\n };\n\n serviceStates.push({\n serviceId,\n serviceName: definition.name,\n currentPlan: tracked.planName ?? null,\n currentBudget: tracked.budget ?? null,\n hasApiKey: tracked.hasApiKey,\n keySource,\n tier,\n excluded: tracked.excluded ?? false,\n hasProbe: checkProbe(serviceId),\n probeResult,\n availablePlans: (definition.plans ?? []).map((p, i) => ({\n index: i + 1,\n name: p.name,\n type: p.type,\n monthlyCost: p.monthlyBase ?? null,\n includedUnits: p.includedUnits ?? null,\n unitName: p.unitName ?? null,\n suggestedBudget: p.suggestedBudget ?? null,\n isDefault: p.default ?? false,\n })),\n riskCategory,\n billingModel: definition.billingModel,\n apiKeyHint: keyHints[serviceId] ?? null,\n allowance: tracked.allowance ?? null,\n });\n }\n\n // Sort: llm first, then usage, infra, flat\n const riskOrder = [\"llm\", \"usage\", \"infra\", \"flat\"];\n serviceStates.sort(\n (a, b) => riskOrder.indexOf(a.riskCategory) - riskOrder.indexOf(b.riskCategory),\n );\n\n const output = {\n projectName: config.projectName,\n serviceCount: serviceStates.length,\n totalBudget: serviceStates.reduce((sum, s) => sum + (s.currentBudget ?? 0), 0),\n liveCount: serviceStates.filter((s) => s.tier === \"live\").length,\n blindCount: serviceStates.filter((s) => s.tier === \"blind\").length,\n services: serviceStates,\n configureCommand: \"burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]\",\n };\n\n if (flags.has(\"--json\")) {\n console.log(JSON.stringify(output, null, 2));\n } else {\n // Human-readable summary pointing to --json\n console.log(`\\n📋 Interview state for ${config.projectName}\\n`);\n console.log(` ${serviceStates.length} services detected`);\n console.log(` ${output.liveCount} with API keys (LIVE)`);\n console.log(` ${output.blindCount} without tracking (BLIND)`);\n console.log(` Total budget: $${output.totalBudget}/mo\\n`);\n console.log(` Use --json for machine-readable output.`);\n console.log(` Use 'burnwatch configure' to update services.\\n`);\n }\n}\n\n/**\n * burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]\n *\n * Agent-friendly command to write back interview answers for a single service.\n * Designed to be called by the AI agent after conversing with the user.\n */\nasync function cmdConfigure(): 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 // Parse named options\n const options: Record<string, string> = {};\n for (let i = 1; 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 } else if (arg === \"--exclude\") {\n options[\"exclude\"] = \"true\";\n }\n }\n\n const serviceId = options[\"service\"];\n if (!serviceId) {\n console.error(\"Usage: burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]\");\n process.exit(1);\n }\n\n const config = readProjectConfig(projectRoot)!;\n const definition = getService(serviceId, projectRoot);\n const globalConfig = readGlobalConfig();\n\n // Get or create tracked service entry\n let tracked = config.services[serviceId];\n if (!tracked) {\n tracked = {\n serviceId,\n detectedVia: [\"manual\"],\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n }\n\n // Handle --exclude\n if (options[\"exclude\"] === \"true\") {\n tracked.excluded = true;\n tracked.planName = \"Don't track for this project\";\n delete tracked.budget;\n delete tracked.planCost;\n delete tracked.allowance;\n config.services[serviceId] = tracked;\n writeProjectConfig(config, projectRoot);\n console.log(JSON.stringify({ success: true, serviceId, action: \"excluded\" }));\n return;\n }\n\n // Handle --plan\n if (options[\"plan\"]) {\n const planSearch = options[\"plan\"].toLowerCase();\n const plans = definition?.plans ?? [];\n const matched = plans.find(\n (p) =>\n p.name.toLowerCase().includes(planSearch) ||\n p.name.toLowerCase().split(/[\\s(]/)[0] === planSearch,\n );\n\n if (matched) {\n tracked.planName = matched.name;\n tracked.excluded = false;\n\n if (matched.type === \"flat\" && matched.monthlyBase !== undefined) {\n tracked.planCost = matched.monthlyBase;\n // Default budget to plan cost if not explicitly set\n if (options[\"budget\"] === undefined && (tracked.budget === undefined || tracked.budget === 0)) {\n tracked.budget = matched.monthlyBase;\n }\n } else if (matched.suggestedBudget !== undefined && options[\"budget\"] === undefined) {\n if (tracked.budget === undefined || tracked.budget === 0) {\n tracked.budget = matched.suggestedBudget;\n }\n }\n\n // Set allowance for credit-pool plans\n if (matched.includedUnits !== undefined && matched.unitName) {\n tracked.allowance = {\n included: matched.includedUnits,\n unitName: matched.unitName,\n };\n } else {\n delete tracked.allowance;\n }\n } else {\n // Use as literal plan name\n tracked.planName = options[\"plan\"];\n }\n }\n\n // Handle --budget\n if (options[\"budget\"] !== undefined) {\n const parsed = parseFloat(options[\"budget\"]);\n if (!isNaN(parsed)) {\n tracked.budget = parsed;\n }\n }\n\n // Handle --key\n if (options[\"key\"]) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[serviceId]) {\n globalConfig.services[serviceId] = {};\n }\n globalConfig.services[serviceId]!.apiKey = options[\"key\"];\n writeGlobalConfig(globalConfig);\n\n // Probe with the new key if possible\n const { probeService: probe, hasProbe: checkProbe } = await import(\"./probes.js\");\n if (checkProbe(serviceId) && definition?.plans) {\n const probeResult = await probe(serviceId, options[\"key\"], definition.plans);\n if (probeResult?.matchedPlan && probeResult.confidence === \"high\" && !options[\"plan\"]) {\n // Auto-apply detected plan\n const mp = probeResult.matchedPlan;\n tracked.planName = mp.name;\n if (mp.type === \"flat\" && mp.monthlyBase !== undefined) {\n tracked.planCost = mp.monthlyBase;\n if (options[\"budget\"] === undefined) tracked.budget = mp.monthlyBase;\n }\n if (mp.includedUnits !== undefined && mp.unitName) {\n tracked.allowance = { included: mp.includedUnits, unitName: mp.unitName };\n }\n }\n }\n }\n\n config.services[serviceId] = tracked;\n writeProjectConfig(config, projectRoot);\n\n // Determine final tier\n let tier = \"blind\";\n if (tracked.excluded) tier = \"excluded\";\n else if (tracked.hasApiKey) tier = \"live\";\n else if (tracked.planCost !== undefined) tier = \"calc\";\n\n const result = {\n success: true,\n serviceId,\n plan: tracked.planName ?? null,\n budget: tracked.budget ?? null,\n tier,\n hasApiKey: tracked.hasApiKey,\n allowance: tracked.allowance ?? null,\n };\n\n console.log(JSON.stringify(result));\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 const tracked = config.services[r.serviceId];\n const allowanceData = r.unitsUsed !== undefined && r.unitsTotal !== undefined && r.unitName\n ? { used: r.unitsUsed, included: r.unitsTotal, unitName: r.unitName }\n : tracked?.allowance\n ? { used: 0, included: tracked.allowance.included, unitName: tracked.allowance.unitName }\n : undefined;\n return buildSnapshot(r.serviceId, r.tier, r.spend, tracked?.budget, allowanceData);\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 burnwatch interview --json Export state for agent-driven interview\n burnwatch configure --service <id> [opts] Agent writes back interview answers\n\nOptions for 'configure':\n --service <ID> Service to configure (required)\n --plan <NAME> Plan name (fuzzy matches against registry)\n --budget <AMOUNT> Monthly budget in USD\n --key <API_KEY> API key for LIVE tracking\n --exclude Exclude this service from tracking\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 interview --json\n burnwatch configure --service anthropic --plan \"API Usage\" --budget 100\n burnwatch configure --service supabase --plan pro --budget 25 --key sbp_xxx\n burnwatch configure --service posthog --plan free --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 { 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 unitsUsed: creditsUsed,\n unitsTotal: creditsTotal,\n unitName: \"credits\",\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 /** Suggested starting budget for usage plans (reasonable dev-stage default) */\n suggestedBudget?: 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 /** Included units for credit-pool plans (e.g., 1M credits on Scrapfly Pro) */\n includedUnits?: number;\n /** Unit name for credit-pool plans (e.g., \"credits\", \"sessions\") */\n unitName?: string;\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 /** For credit-pool services: the unit allowance included in the plan */\n allowance?: {\n /** Total units included in the plan (e.g., 1000000 credits) */\n included: number;\n /** Unit name (e.g., \"credits\", \"sessions\", \"commands\") */\n unitName: string;\n };\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 /** For credit-pool services: unit consumption tracking */\n allowance?: {\n /** Units consumed this period */\n used: number;\n /** Total units included in plan */\n included: number;\n /** Unit name (e.g., \"credits\") */\n unitName: string;\n /** Percentage of allowance consumed */\n percent: number;\n };\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 // Show allowance consumption for credit-pool services\n if (svc.allowance) {\n const usedStr = formatCompact(svc.allowance.used);\n const totalStr = formatCompact(svc.allowance.included);\n const pctStr = svc.allowance.percent.toFixed(0);\n const warn = svc.allowance.percent >= 75 ? \" ⚠️\" : \"\";\n lines.push(\n `║ ↳ ${usedStr}/${totalStr} ${svc.allowance.unitName} (${pctStr}%)${warn}`.padEnd(width + 1) + \"║\",\n );\n }\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 allowanceData?: { used: number; included: number; unitName: string },\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 // For credit-pool services, allowance consumption drives the status\n let allowance: SpendSnapshot[\"allowance\"] | undefined;\n if (allowanceData && allowanceData.included > 0) {\n const percent = (allowanceData.used / allowanceData.included) * 100;\n allowance = { ...allowanceData, percent };\n\n // Override status based on allowance consumption\n if (percent > 100) {\n status = \"over\";\n statusLabel = `⚠️ ${percent.toFixed(0)}% of ${formatCompact(allowanceData.included)} ${allowanceData.unitName} used`;\n } else if (percent >= 75) {\n status = \"caution\";\n statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left — caution`;\n } else {\n status = \"healthy\";\n statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left`;\n }\n } else 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 allowance,\n };\n}\n\nfunction formatCompact(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;\n return String(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 { probeService, hasProbe } from \"./probes.js\";\n\n/** Format large numbers with K/M suffixes */\nfunction formatUnits(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;\n return String(n);\n}\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/** 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 async function autoConfigureServices(\n detected: DetectionResult[],\n): Promise<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 } else if (defaultPlan.suggestedBudget !== undefined) {\n tracked.budget = defaultPlan.suggestedBudget;\n }\n\n // Credit-pool services: track unit allowance, not just dollars\n if (defaultPlan.includedUnits !== undefined && defaultPlan.unitName) {\n tracked.allowance = {\n included: defaultPlan.includedUnits,\n unitName: defaultPlan.unitName,\n };\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 let apiKey: string | undefined;\n\n if (existingKey) {\n tracked.hasApiKey = true;\n apiKey = existingKey;\n keySource = \" (key: global config)\";\n } else if (envKey) {\n tracked.hasApiKey = true;\n apiKey = envKey;\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 // If we have a key and a probe, try to auto-detect the plan\n if (apiKey && hasProbe(service.id)) {\n try {\n const probe = await probeService(service.id, apiKey, plans);\n if (probe?.matchedPlan && probe.confidence === \"high\") {\n const mp = probe.matchedPlan;\n tracked.planName = mp.name;\n if (mp.type === \"flat\" && mp.monthlyBase !== undefined) {\n tracked.planCost = mp.monthlyBase;\n tracked.budget = mp.monthlyBase;\n } else if (mp.suggestedBudget !== undefined) {\n tracked.budget = mp.suggestedBudget;\n }\n if (mp.includedUnits !== undefined && mp.unitName) {\n tracked.allowance = { included: mp.includedUnits, unitName: mp.unitName };\n }\n }\n } catch {\n // Probe failed — use defaults\n }\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 const trackingStr = tracked.allowance\n ? `$${tracked.budget}/mo | ${formatUnits(tracked.allowance.included)} ${tracked.allowance.unitName}`\n : `$${tracked.budget}/mo`;\n console.log(\n ` ${service.name}:${planStr} | ${tierLabel} | ${trackingStr}${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 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 // --- Step 1: Find API key (env vars, global config, or ask) ---\n let apiKey: string | undefined;\n\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n apiKey = existingKey;\n console.log(` API key: found in global config`);\n } else if (envKey) {\n apiKey = envKey;\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n }\n\n // --- Step 2: If we have a key AND a probe exists, try auto-discovery ---\n let chosen: PlanTier | undefined;\n\n if (apiKey && hasProbe(service.id)) {\n console.log(\" Probing API...\");\n const probe = await probeService(service.id, apiKey, plans);\n\n if (probe) {\n console.log(` ${probe.summary}`);\n\n if (probe.confidence === \"high\" && probe.matchedPlan) {\n // Plan detected — confirm with user\n const plan = probe.matchedPlan;\n const costStr = plan.monthlyBase !== undefined ? `$${plan.monthlyBase}/mo` : \"variable\";\n const unitsStr = plan.includedUnits && plan.unitName\n ? `, ${formatUnits(plan.includedUnits)} ${plan.unitName}`\n : \"\";\n const confirm = await ask(\n rl,\n ` Detected: ${plan.name} (${costStr}${unitsStr}). Correct? [Y/n]: `,\n );\n if (confirm === \"\" || confirm.toLowerCase().startsWith(\"y\")) {\n chosen = plan;\n }\n } else if (probe.confidence === \"medium\") {\n // Usage data found but plan not certain — show it, still ask\n if (probe.usage?.spend !== undefined) {\n console.log(` Current spend: $${probe.usage.spend.toFixed(2)}`);\n }\n // Fall through to plan selection with context\n }\n // \"low\" confidence — key works but no useful data, fall through\n }\n }\n\n // --- Step 3: If no auto-detect or user said no, show plan list ---\n if (!chosen) {\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 chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n }\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: !!apiKey,\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 // Credit-pool services: track unit allowance\n if (chosen.includedUnits !== undefined && chosen.unitName) {\n tracked.allowance = {\n included: chosen.includedUnits,\n unitName: chosen.unitName,\n };\n }\n\n // --- Step 4: If we still don't have a key, offer to provide one ---\n if (!apiKey && hasProbe(service.id)) {\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 apiKey = keyAnswer;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n\n // Now that we have a key, probe to enrich tracking data\n if (hasProbe(service.id)) {\n const probe = await probeService(service.id, keyAnswer, plans);\n if (probe?.usage) {\n console.log(` ${probe.summary}`);\n }\n }\n }\n }\n\n // --- Step 5: Budget (always set, never skip) ---\n const defaultBudget = chosen.monthlyBase ?? chosen.suggestedBudget ?? 0;\n\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget [$${defaultBudget}]: $`,\n );\n if (budgetAnswer) {\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : defaultBudget;\n } else {\n tracked.budget = defaultBudget;\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n const allowanceStr = tracked.allowance\n ? ` | ${formatUnits(tracked.allowance.included)} ${tracked.allowance.unitName}`\n : \"\";\n console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo${allowanceStr}`,\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":["url","fs","path","fs","path","fs","path","url","url","url","url","totalStr","fs","path","resolve","tracked","config"]}
|
|
1
|
+
{"version":3,"sources":["../src/services/base.ts","../src/probes.ts","../src/cli.ts","../src/core/config.ts","../src/detection/detector.ts","../src/core/registry.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":["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 /** For credit-pool services: units consumed this period */\n unitsUsed?: number;\n /** For credit-pool services: total units in plan allowance */\n unitsTotal?: number;\n /** For credit-pool services: unit name (e.g., \"credits\") */\n unitName?: 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","/**\n * Service probes — auto-detect plan, usage, and billing data from APIs.\n *\n * Each probe tries to discover as much as possible from a service's API:\n * 1. Plan tier (best case — \"You're on Pro\")\n * 2. Usage/spend data (good — \"You've used 850K credits\")\n * 3. Key validation (minimum — \"Your key works\")\n *\n * Probes are extensible: add a new entry to PROBES to support a new service.\n * The interview flow calls `probeService()` which looks up the right probe.\n */\n\nimport type { PlanTier } from \"./core/types.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Result from probing a service API */\nexport interface ProbeResult {\n /** Detected plan name (should match a registry PlanTier.name prefix) */\n planName?: string;\n /** Matched PlanTier from the registry */\n matchedPlan?: PlanTier;\n /** Usage/spend data discovered */\n usage?: {\n unitsUsed?: number;\n unitsTotal?: number;\n unitName?: string;\n spend?: number;\n currency?: string;\n };\n /** Human-readable summary of what was found */\n summary: string;\n /**\n * Discovery confidence:\n * high — plan tier identified (can skip plan selection)\n * medium — usage data found but plan unclear (show data, still ask plan)\n * low — key validates but no plan/usage info\n */\n confidence: \"high\" | \"medium\" | \"low\";\n}\n\n/** A probe function: given an API key and registry plans, discover what we can */\ntype ProbeFn = (\n apiKey: string,\n plans: PlanTier[],\n) => Promise<ProbeResult | null>;\n\n// ---------------------------------------------------------------------------\n// Service-specific probes\n// ---------------------------------------------------------------------------\n\n/** Match a detected plan name against registry plans */\nfunction matchPlan(\n detected: string,\n plans: PlanTier[],\n): PlanTier | undefined {\n const lower = detected.toLowerCase();\n return plans.find(\n (p) =>\n p.type !== \"exclude\" &&\n p.name.toLowerCase().includes(lower),\n );\n}\n\n/** Match by checking if the detected name appears as the first word of any plan */\nfunction matchPlanByPrefix(\n detected: string,\n plans: PlanTier[],\n): PlanTier | undefined {\n const lower = detected.toLowerCase();\n return plans.find((p) => {\n if (p.type === \"exclude\") return false;\n const firstWord = p.name.split(/[\\s(]/)[0]!.toLowerCase();\n return lower.includes(firstWord) || firstWord.includes(lower);\n });\n}\n\n// --- Scrapfly ---\nconst probeScrapfly: ProbeFn = async (apiKey, plans) => {\n const result = await fetchJson<{\n subscription?: {\n plan?: { name?: string };\n usage?: { scrape?: { used?: number; allowed?: number } };\n };\n account?: { credits_used?: number; credits_total?: number };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (!result.ok || !result.data) return null;\n\n const planName = result.data.subscription?.plan?.name;\n let unitsUsed = 0;\n let unitsTotal = 0;\n\n if (result.data.subscription?.usage?.scrape) {\n unitsUsed = result.data.subscription.usage.scrape.used ?? 0;\n unitsTotal = result.data.subscription.usage.scrape.allowed ?? 0;\n } else if (result.data.account) {\n unitsUsed = result.data.account.credits_used ?? 0;\n unitsTotal = result.data.account.credits_total ?? 0;\n }\n\n const matched = planName ? matchPlanByPrefix(planName, plans) : undefined;\n\n return {\n planName: planName ?? undefined,\n matchedPlan: matched,\n usage: {\n unitsUsed,\n unitsTotal,\n unitName: \"credits\",\n },\n summary: matched\n ? `${matched.name} — ${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used`\n : `${formatK(unitsUsed)}/${formatK(unitsTotal)} credits used`,\n confidence: matched ? \"high\" : \"medium\",\n };\n};\n\n// --- Anthropic ---\nconst probeAnthropic: ProbeFn = async (apiKey, _plans) => {\n // Anthropic Admin API: GET /v1/organizations/cost_report\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const params = new URLSearchParams({\n start_date: startOfMonth.toISOString().split(\"T\")[0]!,\n end_date: now.toISOString().split(\"T\")[0]!,\n });\n\n const result = await fetchJson<{\n data?: Array<{ amount?: string; cost_type?: string }>;\n }>(`https://api.anthropic.com/v1/organizations/cost_report?${params}`, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n });\n\n if (!result.ok || !result.data?.data) return null;\n\n // Sum costs (returned in cents as strings)\n let totalCents = 0;\n for (const entry of result.data.data) {\n totalCents += parseFloat(entry.amount ?? \"0\");\n }\n const spend = totalCents / 100;\n\n return {\n usage: { spend, currency: \"USD\" },\n summary: `$${spend.toFixed(2)} spent this billing period`,\n confidence: \"medium\",\n };\n};\n\n// --- OpenAI ---\nconst probeOpenAI: ProbeFn = async (apiKey, _plans) => {\n // OpenAI Admin API: GET /v1/organization/usage/completions\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const params = new URLSearchParams({\n start_time: String(Math.floor(startOfMonth.getTime() / 1000)),\n end_time: String(Math.floor(now.getTime() / 1000)),\n });\n\n const result = await fetchJson<{\n data?: Array<{ results?: Array<{ amount?: { value?: number } }> }>;\n }>(`https://api.openai.com/v1/organization/usage/completions?${params}`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!result.ok || !result.data?.data) return null;\n\n // Sum usage values\n let totalTokens = 0;\n for (const bucket of result.data.data) {\n for (const r of bucket.results ?? []) {\n totalTokens += r.amount?.value ?? 0;\n }\n }\n\n return {\n usage: { unitsUsed: totalTokens, unitName: \"tokens\" },\n summary: `${formatK(totalTokens)} tokens used this period`,\n confidence: \"medium\",\n };\n};\n\n// --- Vercel ---\nconst probeVercel: ProbeFn = async (apiKey, plans) => {\n // Try to get team info first\n const teamsResult = await fetchJson<{\n teams?: Array<{ id?: string; name?: string; billing?: { plan?: string } }>;\n }>(\"https://api.vercel.com/v2/teams\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (teamsResult.ok && teamsResult.data?.teams?.[0]) {\n const team = teamsResult.data.teams[0];\n const planName = team.billing?.plan;\n if (planName) {\n const matched = matchPlanByPrefix(planName, plans);\n return {\n planName,\n matchedPlan: matched,\n summary: `Team \"${team.name}\" on ${planName} plan`,\n confidence: matched ? \"high\" : \"medium\",\n };\n }\n }\n\n // Fallback: try user endpoint for hobby plan detection\n const userResult = await fetchJson<{\n user?: { billing?: { plan?: string }; name?: string };\n }>(\"https://api.vercel.com/v2/user\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (userResult.ok && userResult.data?.user) {\n const plan = userResult.data.user.billing?.plan ?? \"hobby\";\n const matched = matchPlanByPrefix(plan, plans);\n return {\n planName: plan,\n matchedPlan: matched,\n summary: `Personal account on ${plan} plan`,\n confidence: matched ? \"high\" : \"low\",\n };\n }\n\n return null;\n};\n\n// --- Supabase ---\nconst probeSupabase: ProbeFn = async (apiKey, plans) => {\n // Supabase Management API requires a PAT, not service_role_key\n const orgsResult = await fetchJson<\n Array<{ id?: string; name?: string; billing?: { plan?: string }; subscription_id?: string }>\n >(\"https://api.supabase.com/v1/organizations\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!orgsResult.ok || !orgsResult.data || !Array.isArray(orgsResult.data)) return null;\n\n const org = orgsResult.data[0];\n if (!org) return null;\n\n const planName = org.billing?.plan;\n if (planName) {\n const matched = matchPlanByPrefix(planName, plans);\n return {\n planName,\n matchedPlan: matched,\n summary: `Org \"${org.name}\" on ${planName} plan`,\n confidence: matched ? \"high\" : \"medium\",\n };\n }\n\n return {\n summary: `Org \"${org.name}\" found (plan not detected)`,\n confidence: \"low\",\n };\n};\n\n// --- Stripe ---\nconst probeStripe: ProbeFn = async (apiKey, _plans) => {\n const result = await fetchJson<{\n available?: Array<{ amount?: number; currency?: string }>;\n pending?: Array<{ amount?: number; currency?: string }>;\n }>(\"https://api.stripe.com/v1/balance\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!result.ok || !result.data) return null;\n\n const available = result.data.available?.[0];\n const pending = result.data.pending?.[0];\n const totalCents = (available?.amount ?? 0) + (pending?.amount ?? 0);\n const currency = (available?.currency ?? \"usd\").toUpperCase();\n\n return {\n usage: { spend: totalCents / 100, currency },\n summary: `Balance: ${currency} ${(totalCents / 100).toFixed(2)} (${((available?.amount ?? 0) / 100).toFixed(2)} available)`,\n confidence: \"medium\",\n };\n};\n\n// --- Browserbase ---\nconst probeBrowserbase: ProbeFn = async (apiKey, _plans) => {\n // Browserbase: GET /v1/projects (to get project ID), then usage\n const projResult = await fetchJson<\n Array<{ id?: string; name?: string }>\n >(\"https://api.browserbase.com/v1/projects\", {\n headers: { \"X-BB-API-Key\": apiKey },\n });\n\n if (!projResult.ok || !projResult.data?.[0]?.id) return null;\n\n const projectId = projResult.data[0].id;\n const usageResult = await fetchJson<{\n sessions_count?: number;\n browser_hours?: number;\n }>(`https://api.browserbase.com/v1/projects/${projectId}/usage`, {\n headers: { \"X-BB-API-Key\": apiKey },\n });\n\n if (!usageResult.ok || !usageResult.data) {\n return {\n summary: `Project \"${projResult.data[0].name}\" found`,\n confidence: \"low\",\n };\n }\n\n const sessions = usageResult.data.sessions_count ?? 0;\n const hours = usageResult.data.browser_hours ?? 0;\n\n return {\n usage: { unitsUsed: sessions, unitName: \"sessions\" },\n summary: `${sessions} sessions, ${hours.toFixed(1)} browser hours this period`,\n confidence: \"medium\",\n };\n};\n\n// --- Upstash ---\nconst probeUpstash: ProbeFn = async (apiKey, _plans) => {\n // Upstash uses email:api_key basic auth for the management API\n // The key from env is typically the Redis REST token, not management key\n // Try the databases list endpoint\n const result = await fetchJson<\n Array<{ database_id?: string; database_name?: string; region?: string }>\n >(\"https://api.upstash.com/v2/redis/databases\", {\n headers: {\n Authorization: `Basic ${Buffer.from(apiKey).toString(\"base64\")}`,\n },\n });\n\n if (!result.ok) return null;\n\n const dbCount = Array.isArray(result.data) ? result.data.length : 0;\n return {\n summary: `${dbCount} Redis database${dbCount !== 1 ? \"s\" : \"\"} found`,\n confidence: \"low\",\n };\n};\n\n// --- PostHog ---\nconst probePostHog: ProbeFn = async (apiKey, _plans) => {\n const result = await fetchJson<{\n results?: Array<{ id?: string; name?: string }>;\n }>(\"https://us.posthog.com/api/organizations/@current\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n\n if (!result.ok || !result.data) return null;\n\n return {\n summary: \"Organization found\",\n confidence: \"low\",\n };\n};\n\n// ---------------------------------------------------------------------------\n// Probe registry — maps service IDs to their probe function\n// ---------------------------------------------------------------------------\n\nconst PROBES: Map<string, ProbeFn> = new Map([\n [\"scrapfly\", probeScrapfly],\n [\"anthropic\", probeAnthropic],\n [\"openai\", probeOpenAI],\n [\"vercel\", probeVercel],\n [\"supabase\", probeSupabase],\n [\"stripe\", probeStripe],\n [\"browserbase\", probeBrowserbase],\n [\"upstash\", probeUpstash],\n [\"posthog\", probePostHog],\n]);\n\n/**\n * Probe a service using its API key.\n * Returns null if no probe exists for the service or the probe fails.\n */\nexport async function probeService(\n serviceId: string,\n apiKey: string,\n plans: PlanTier[],\n): Promise<ProbeResult | null> {\n const probe = PROBES.get(serviceId);\n if (!probe) return null;\n\n try {\n return await probe(apiKey, plans);\n } catch {\n return null;\n }\n}\n\n/** Check if a service has a probe available */\nexport function hasProbe(serviceId: string): boolean {\n return PROBES.has(serviceId);\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatK(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;\n return String(n);\n}\n","#!/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 \"interview\":\n await cmdInterview();\n break;\n case \"configure\":\n await cmdConfigure();\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 = await 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\n/**\n * burnwatch interview --json\n *\n * Exports the current project state as structured JSON for an AI agent\n * to conduct the interview conversationally. The agent reads this,\n * asks the user questions, then writes answers back via `burnwatch configure`.\n */\nasync function cmdInterview(): Promise<void> {\n const projectRoot = process.cwd();\n\n if (!isInitialized(projectRoot)) {\n // Auto-init first\n ensureProjectDirs(projectRoot);\n const detected = detectServices(projectRoot);\n let projectName = path.basename(projectRoot);\n try {\n const pkg = JSON.parse(\n fs.readFileSync(path.join(projectRoot, \"package.json\"), \"utf-8\"),\n ) as { name?: string };\n if (pkg.name) projectName = pkg.name;\n } catch {\n // Use directory name\n }\n\n const config: ProjectConfig = {\n projectName,\n services: {},\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n // Auto-configure with defaults first\n const result = await autoConfigureServices(detected);\n config.services = result.services;\n writeProjectConfig(config, projectRoot);\n }\n\n const config = readProjectConfig(projectRoot)!;\n const globalConfig = readGlobalConfig();\n const allRegistryServices = getAllServices(projectRoot);\n\n // Build structured export for each tracked service\n const serviceStates: Array<{\n serviceId: string;\n serviceName: string;\n currentPlan: string | null;\n currentBudget: number | null;\n hasApiKey: boolean;\n keySource: string | null;\n tier: string;\n excluded: boolean;\n hasProbe: boolean;\n probeResult: import(\"./probes.js\").ProbeResult | null;\n availablePlans: Array<{\n index: number;\n name: string;\n type: string;\n monthlyCost: number | null;\n includedUnits: number | null;\n unitName: string | null;\n suggestedBudget: number | null;\n isDefault: boolean;\n }>;\n riskCategory: string;\n billingModel: string;\n apiKeyHint: string | null;\n allowance: { included: number; unitName: string } | null;\n }> = [];\n\n for (const [serviceId, tracked] of Object.entries(config.services)) {\n const definition = allRegistryServices.find((s) => s.id === serviceId);\n if (!definition) continue;\n\n // Determine key source\n let keySource: string | null = null;\n const globalKey = globalConfig.services[serviceId]?.apiKey;\n if (globalKey) keySource = \"global_config\";\n else {\n for (const pattern of definition.envPatterns) {\n if (process.env[pattern]) {\n keySource = `env:${pattern}`;\n break;\n }\n }\n }\n\n // Try probing if we have a key\n let probeResult: import(\"./probes.js\").ProbeResult | null = null;\n const { probeService: probe, hasProbe: checkProbe } = await import(\"./probes.js\");\n const apiKey = globalKey ?? (keySource?.startsWith(\"env:\") ? process.env[keySource.slice(4)] : undefined);\n if (apiKey && checkProbe(serviceId)) {\n probeResult = await probe(serviceId, apiKey, definition.plans ?? []);\n }\n\n // Determine tier\n let tier = \"blind\";\n if (tracked.excluded) tier = \"excluded\";\n else if (tracked.hasApiKey) tier = \"live\";\n else if (tracked.planCost !== undefined) tier = \"calc\";\n\n // Risk category\n let riskCategory = \"flat\";\n if (definition.billingModel === \"token_usage\") riskCategory = \"llm\";\n else if ([\"credit_pool\", \"percentage\", \"per_unit\"].includes(definition.billingModel)) riskCategory = \"usage\";\n else if (definition.billingModel === \"compute\") riskCategory = \"infra\";\n\n const keyHints: Record<string, string> = {\n anthropic: \"Admin key from console.anthropic.com → Settings → Admin API Keys (sk-ant-admin-*)\",\n openai: \"Admin key from platform.openai.com → Settings → API Keys (sk-admin-*)\",\n vercel: \"Token from vercel.com/account/tokens\",\n supabase: \"PAT from supabase.com/dashboard → Account → Access Tokens (not service_role key)\",\n stripe: \"Secret key from dashboard.stripe.com → Developers → API Keys (sk_live_*)\",\n scrapfly: \"API key from scrapfly.io/dashboard\",\n browserbase: \"API key from browserbase.com → Settings → API Keys\",\n upstash: \"email:api_key from console.upstash.com → Account → Management API\",\n posthog: \"Personal API key from posthog.com → Settings → Personal API Keys\",\n };\n\n serviceStates.push({\n serviceId,\n serviceName: definition.name,\n currentPlan: tracked.planName ?? null,\n currentBudget: tracked.budget ?? null,\n hasApiKey: tracked.hasApiKey,\n keySource,\n tier,\n excluded: tracked.excluded ?? false,\n hasProbe: checkProbe(serviceId),\n probeResult,\n availablePlans: (definition.plans ?? []).map((p, i) => ({\n index: i + 1,\n name: p.name,\n type: p.type,\n monthlyCost: p.monthlyBase ?? null,\n includedUnits: p.includedUnits ?? null,\n unitName: p.unitName ?? null,\n suggestedBudget: p.suggestedBudget ?? null,\n isDefault: p.default ?? false,\n })),\n riskCategory,\n billingModel: definition.billingModel,\n apiKeyHint: keyHints[serviceId] ?? null,\n allowance: tracked.allowance ?? null,\n });\n }\n\n // Sort: llm first, then usage, infra, flat\n const riskOrder = [\"llm\", \"usage\", \"infra\", \"flat\"];\n serviceStates.sort(\n (a, b) => riskOrder.indexOf(a.riskCategory) - riskOrder.indexOf(b.riskCategory),\n );\n\n const output = {\n projectName: config.projectName,\n serviceCount: serviceStates.length,\n totalBudget: serviceStates.reduce((sum, s) => sum + (s.currentBudget ?? 0), 0),\n liveCount: serviceStates.filter((s) => s.tier === \"live\").length,\n blindCount: serviceStates.filter((s) => s.tier === \"blind\").length,\n services: serviceStates,\n configureCommand: \"burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]\",\n };\n\n if (flags.has(\"--json\")) {\n console.log(JSON.stringify(output, null, 2));\n } else {\n // Human-readable summary pointing to --json\n console.log(`\\n📋 Interview state for ${config.projectName}\\n`);\n console.log(` ${serviceStates.length} services detected`);\n console.log(` ${output.liveCount} with API keys (LIVE)`);\n console.log(` ${output.blindCount} without tracking (BLIND)`);\n console.log(` Total budget: $${output.totalBudget}/mo\\n`);\n console.log(` Use --json for machine-readable output.`);\n console.log(` Use 'burnwatch configure' to update services.\\n`);\n }\n}\n\n/**\n * burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]\n *\n * Agent-friendly command to write back interview answers for a single service.\n * Designed to be called by the AI agent after conversing with the user.\n */\nasync function cmdConfigure(): 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 // Parse named options\n const options: Record<string, string> = {};\n for (let i = 1; 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 } else if (arg === \"--exclude\") {\n options[\"exclude\"] = \"true\";\n }\n }\n\n const serviceId = options[\"service\"];\n if (!serviceId) {\n console.error(\"Usage: burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]\");\n process.exit(1);\n }\n\n const config = readProjectConfig(projectRoot)!;\n const definition = getService(serviceId, projectRoot);\n const globalConfig = readGlobalConfig();\n\n // Get or create tracked service entry\n let tracked = config.services[serviceId];\n if (!tracked) {\n tracked = {\n serviceId,\n detectedVia: [\"manual\"],\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n }\n\n // Handle --exclude\n if (options[\"exclude\"] === \"true\") {\n tracked.excluded = true;\n tracked.planName = \"Don't track for this project\";\n delete tracked.budget;\n delete tracked.planCost;\n delete tracked.allowance;\n config.services[serviceId] = tracked;\n writeProjectConfig(config, projectRoot);\n console.log(JSON.stringify({ success: true, serviceId, action: \"excluded\" }));\n return;\n }\n\n // Handle --plan\n if (options[\"plan\"]) {\n const planSearch = options[\"plan\"].toLowerCase();\n const plans = definition?.plans ?? [];\n const matched = plans.find(\n (p) =>\n p.name.toLowerCase().includes(planSearch) ||\n p.name.toLowerCase().split(/[\\s(]/)[0] === planSearch,\n );\n\n if (matched) {\n tracked.planName = matched.name;\n tracked.excluded = false;\n\n if (matched.type === \"flat\" && matched.monthlyBase !== undefined) {\n tracked.planCost = matched.monthlyBase;\n // Default budget to plan cost if not explicitly set\n if (options[\"budget\"] === undefined && (tracked.budget === undefined || tracked.budget === 0)) {\n tracked.budget = matched.monthlyBase;\n }\n } else if (matched.suggestedBudget !== undefined && options[\"budget\"] === undefined) {\n if (tracked.budget === undefined || tracked.budget === 0) {\n tracked.budget = matched.suggestedBudget;\n }\n }\n\n // Set allowance for credit-pool plans\n if (matched.includedUnits !== undefined && matched.unitName) {\n tracked.allowance = {\n included: matched.includedUnits,\n unitName: matched.unitName,\n };\n } else {\n delete tracked.allowance;\n }\n } else {\n // Use as literal plan name\n tracked.planName = options[\"plan\"];\n }\n }\n\n // Handle --budget\n if (options[\"budget\"] !== undefined) {\n const parsed = parseFloat(options[\"budget\"]);\n if (!isNaN(parsed)) {\n tracked.budget = parsed;\n }\n }\n\n // Handle --key\n if (options[\"key\"]) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[serviceId]) {\n globalConfig.services[serviceId] = {};\n }\n globalConfig.services[serviceId]!.apiKey = options[\"key\"];\n writeGlobalConfig(globalConfig);\n\n // Probe with the new key if possible\n const { probeService: probe, hasProbe: checkProbe } = await import(\"./probes.js\");\n if (checkProbe(serviceId) && definition?.plans) {\n const probeResult = await probe(serviceId, options[\"key\"], definition.plans);\n if (probeResult?.matchedPlan && probeResult.confidence === \"high\" && !options[\"plan\"]) {\n // Auto-apply detected plan\n const mp = probeResult.matchedPlan;\n tracked.planName = mp.name;\n if (mp.type === \"flat\" && mp.monthlyBase !== undefined) {\n tracked.planCost = mp.monthlyBase;\n if (options[\"budget\"] === undefined) tracked.budget = mp.monthlyBase;\n }\n if (mp.includedUnits !== undefined && mp.unitName) {\n tracked.allowance = { included: mp.includedUnits, unitName: mp.unitName };\n }\n }\n }\n }\n\n config.services[serviceId] = tracked;\n writeProjectConfig(config, projectRoot);\n\n // Determine final tier\n let tier = \"blind\";\n if (tracked.excluded) tier = \"excluded\";\n else if (tracked.hasApiKey) tier = \"live\";\n else if (tracked.planCost !== undefined) tier = \"calc\";\n\n const result = {\n success: true,\n serviceId,\n plan: tracked.planName ?? null,\n budget: tracked.budget ?? null,\n tier,\n hasApiKey: tracked.hasApiKey,\n allowance: tracked.allowance ?? null,\n };\n\n console.log(JSON.stringify(result));\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 const tracked = config.services[r.serviceId];\n const allowanceData = r.unitsUsed !== undefined && r.unitsTotal !== undefined && r.unitName\n ? { used: r.unitsUsed, included: r.unitsTotal, unitName: r.unitName }\n : tracked?.allowance\n ? { used: 0, included: tracked.allowance.included, unitName: tracked.allowance.unitName }\n : undefined;\n return buildSnapshot(r.serviceId, r.tier, r.spend, tracked?.budget, allowanceData);\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 AI-assisted development\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 burnwatch interview --json Export state for agent-driven interview\n burnwatch configure --service <id> [opts] Agent writes back interview answers\n\nOptions for 'configure':\n --service <ID> Service to configure (required)\n --plan <NAME> Plan name (fuzzy matches against registry)\n --budget <AMOUNT> Monthly budget in USD\n --key <API_KEY> API key for LIVE tracking\n --exclude Exclude this service from tracking\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 interview --json\n burnwatch configure --service anthropic --plan \"API Usage\" --budget 100\n burnwatch configure --service supabase --plan pro --budget 25 --key sbp_xxx\n burnwatch configure --service posthog --plan free --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 { 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 unitsUsed: creditsUsed,\n unitsTotal: creditsTotal,\n unitName: \"credits\",\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 /** Suggested starting budget for usage plans (reasonable dev-stage default) */\n suggestedBudget?: 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 /** Included units for credit-pool plans (e.g., 1M credits on Scrapfly Pro) */\n includedUnits?: number;\n /** Unit name for credit-pool plans (e.g., \"credits\", \"sessions\") */\n unitName?: string;\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 /** For credit-pool services: the unit allowance included in the plan */\n allowance?: {\n /** Total units included in the plan (e.g., 1000000 credits) */\n included: number;\n /** Unit name (e.g., \"credits\", \"sessions\", \"commands\") */\n unitName: string;\n };\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 /** For credit-pool services: unit consumption tracking */\n allowance?: {\n /** Units consumed this period */\n used: number;\n /** Total units included in plan */\n included: number;\n /** Unit name (e.g., \"credits\") */\n unitName: string;\n /** Percentage of allowance consumed */\n percent: number;\n };\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 // Show allowance consumption for credit-pool services\n if (svc.allowance) {\n const usedStr = formatCompact(svc.allowance.used);\n const totalStr = formatCompact(svc.allowance.included);\n const pctStr = svc.allowance.percent.toFixed(0);\n const warn = svc.allowance.percent >= 75 ? \" ⚠️\" : \"\";\n lines.push(\n `║ ↳ ${usedStr}/${totalStr} ${svc.allowance.unitName} (${pctStr}%)${warn}`.padEnd(width + 1) + \"║\",\n );\n }\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 allowanceData?: { used: number; included: number; unitName: string },\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 // For credit-pool services, allowance consumption drives the status\n let allowance: SpendSnapshot[\"allowance\"] | undefined;\n if (allowanceData && allowanceData.included > 0) {\n const percent = (allowanceData.used / allowanceData.included) * 100;\n allowance = { ...allowanceData, percent };\n\n // Override status based on allowance consumption\n if (percent > 100) {\n status = \"over\";\n statusLabel = `⚠️ ${percent.toFixed(0)}% of ${formatCompact(allowanceData.included)} ${allowanceData.unitName} used`;\n } else if (percent >= 75) {\n status = \"caution\";\n statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left — caution`;\n } else {\n status = \"healthy\";\n statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left`;\n }\n } else 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 allowance,\n };\n}\n\nfunction formatCompact(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;\n return String(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 { probeService, hasProbe } from \"./probes.js\";\n\n/** Format large numbers with K/M suffixes */\nfunction formatUnits(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;\n return String(n);\n}\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/** 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 async function autoConfigureServices(\n detected: DetectionResult[],\n): Promise<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 } else if (defaultPlan.suggestedBudget !== undefined) {\n tracked.budget = defaultPlan.suggestedBudget;\n }\n\n // Credit-pool services: track unit allowance, not just dollars\n if (defaultPlan.includedUnits !== undefined && defaultPlan.unitName) {\n tracked.allowance = {\n included: defaultPlan.includedUnits,\n unitName: defaultPlan.unitName,\n };\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 let apiKey: string | undefined;\n\n if (existingKey) {\n tracked.hasApiKey = true;\n apiKey = existingKey;\n keySource = \" (key: global config)\";\n } else if (envKey) {\n tracked.hasApiKey = true;\n apiKey = envKey;\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 // If we have a key and a probe, try to auto-detect the plan\n if (apiKey && hasProbe(service.id)) {\n try {\n const probe = await probeService(service.id, apiKey, plans);\n if (probe?.matchedPlan && probe.confidence === \"high\") {\n const mp = probe.matchedPlan;\n tracked.planName = mp.name;\n if (mp.type === \"flat\" && mp.monthlyBase !== undefined) {\n tracked.planCost = mp.monthlyBase;\n tracked.budget = mp.monthlyBase;\n } else if (mp.suggestedBudget !== undefined) {\n tracked.budget = mp.suggestedBudget;\n }\n if (mp.includedUnits !== undefined && mp.unitName) {\n tracked.allowance = { included: mp.includedUnits, unitName: mp.unitName };\n }\n }\n } catch {\n // Probe failed — use defaults\n }\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 const trackingStr = tracked.allowance\n ? `$${tracked.budget}/mo | ${formatUnits(tracked.allowance.included)} ${tracked.allowance.unitName}`\n : `$${tracked.budget}/mo`;\n console.log(\n ` ${service.name}:${planStr} | ${tierLabel} | ${trackingStr}${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 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 // --- Step 1: Find API key (env vars, global config, or ask) ---\n let apiKey: string | undefined;\n\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n apiKey = existingKey;\n console.log(` API key: found in global config`);\n } else if (envKey) {\n apiKey = envKey;\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n }\n\n // --- Step 2: If we have a key AND a probe exists, try auto-discovery ---\n let chosen: PlanTier | undefined;\n\n if (apiKey && hasProbe(service.id)) {\n console.log(\" Probing API...\");\n const probe = await probeService(service.id, apiKey, plans);\n\n if (probe) {\n console.log(` ${probe.summary}`);\n\n if (probe.confidence === \"high\" && probe.matchedPlan) {\n // Plan detected — confirm with user\n const plan = probe.matchedPlan;\n const costStr = plan.monthlyBase !== undefined ? `$${plan.monthlyBase}/mo` : \"variable\";\n const unitsStr = plan.includedUnits && plan.unitName\n ? `, ${formatUnits(plan.includedUnits)} ${plan.unitName}`\n : \"\";\n const confirm = await ask(\n rl,\n ` Detected: ${plan.name} (${costStr}${unitsStr}). Correct? [Y/n]: `,\n );\n if (confirm === \"\" || confirm.toLowerCase().startsWith(\"y\")) {\n chosen = plan;\n }\n } else if (probe.confidence === \"medium\") {\n // Usage data found but plan not certain — show it, still ask\n if (probe.usage?.spend !== undefined) {\n console.log(` Current spend: $${probe.usage.spend.toFixed(2)}`);\n }\n // Fall through to plan selection with context\n }\n // \"low\" confidence — key works but no useful data, fall through\n }\n }\n\n // --- Step 3: If no auto-detect or user said no, show plan list ---\n if (!chosen) {\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 chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n }\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: !!apiKey,\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 // Credit-pool services: track unit allowance\n if (chosen.includedUnits !== undefined && chosen.unitName) {\n tracked.allowance = {\n included: chosen.includedUnits,\n unitName: chosen.unitName,\n };\n }\n\n // --- Step 4: If we still don't have a key, offer to provide one ---\n if (!apiKey && hasProbe(service.id)) {\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 apiKey = keyAnswer;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n\n // Now that we have a key, probe to enrich tracking data\n if (hasProbe(service.id)) {\n const probe = await probeService(service.id, keyAnswer, plans);\n if (probe?.usage) {\n console.log(` ${probe.summary}`);\n }\n }\n }\n }\n\n // --- Step 5: Budget (always set, never skip) ---\n const defaultBudget = chosen.monthlyBase ?? chosen.suggestedBudget ?? 0;\n\n const budgetAnswer = await ask(\n rl,\n ` Monthly budget [$${defaultBudget}]: $`,\n );\n if (budgetAnswer) {\n const parsed = parseFloat(budgetAnswer);\n tracked.budget = !isNaN(parsed) ? parsed : defaultBudget;\n } else {\n tracked.budget = defaultBudget;\n }\n\n services[service.id] = tracked;\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n const allowanceStr = tracked.allowance\n ? ` | ${formatUnits(tracked.allowance.included)} ${tracked.allowance.unitName}`\n : \"\";\n console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo${allowanceStr}`,\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":["url","fs","path","fs","path","fs","path","url","url","url","url","totalStr","fs","path","resolve","tracked","config"]}
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# burnwatch
|
|
2
2
|
|
|
3
|
-
> Passive cost memory for
|
|
3
|
+
> Passive cost memory for AI-assisted development. Detects paid services in your project, tracks spend across AI-native tools, and injects budget context into Claude Code sessions so the agent can factor cost into every recommendation.
|
|
4
4
|
|
|
5
5
|
burnwatch is an open-source CLI and Claude Code hooks integration that solves the problem of unpredictable SaaS costs in agentic development workflows. It passively monitors your project for paid services (via package.json, env vars, import statements, and prompt mentions), tracks spend through billing APIs where available, and injects real-time cost awareness into AI coding sessions.
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "burnwatch",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Passive cost memory for
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "Passive cost memory for AI-assisted development — detects paid services, tracks spend, injects budget context into your AI coding sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"burnwatch": "./dist/cli.js"
|
|
@@ -9,48 +9,86 @@ allowed-tools: Read, Bash, Glob, Grep
|
|
|
9
9
|
|
|
10
10
|
You are the interviewer. burnwatch has auto-detected services and applied defaults — your job is to walk the user through confirming or correcting each one in natural conversation.
|
|
11
11
|
|
|
12
|
+
## CRITICAL: Pacing Rules
|
|
13
|
+
|
|
14
|
+
**ONE service (or one logical batch) per message. Then STOP and WAIT for the user's response.**
|
|
15
|
+
|
|
16
|
+
- Do NOT dump all services in one message
|
|
17
|
+
- Do NOT ask about LLMs and infrastructure in the same breath
|
|
18
|
+
- Do NOT proceed to the next service until the user has responded
|
|
19
|
+
- The only exception: free-tier services can be batched into one "any of these need updating?" question
|
|
20
|
+
|
|
21
|
+
The rhythm is: **present → ask → wait → configure → next.**
|
|
22
|
+
|
|
12
23
|
## Step 1: Get the current state
|
|
13
24
|
|
|
14
25
|
Run this to get the structured state of all detected services:
|
|
15
26
|
|
|
16
27
|
```bash
|
|
17
|
-
cd $PROJECT_ROOT &&
|
|
28
|
+
cd $PROJECT_ROOT && burnwatch interview --json
|
|
18
29
|
```
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
Parse the JSON output. Do NOT show the raw JSON to the user.
|
|
21
32
|
|
|
22
|
-
## Step 2: Present the
|
|
33
|
+
## Step 2: Present a brief overview, then start the interview
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
Open with a short summary (2-3 lines max), then immediately start with the first service:
|
|
25
36
|
|
|
26
|
-
> I found **N services** in your project.
|
|
37
|
+
> I found **N services** in your project. Let me walk through each one to make sure we're tracking accurately.
|
|
27
38
|
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
39
|
+
> First up — **Anthropic**. You're defaulted to "API Usage" at $100/mo.
|
|
40
|
+
> [If probe data exists: "I checked your API — you've spent $47.23 this month."]
|
|
41
|
+
> Does $100/mo feel right, or want to adjust?
|
|
42
|
+
|
|
43
|
+
Then STOP. Wait for the user.
|
|
30
44
|
|
|
31
|
-
## Step 3:
|
|
45
|
+
## Step 3: Walk through services ONE AT A TIME
|
|
32
46
|
|
|
33
|
-
|
|
47
|
+
### Order: highest risk first
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
49
|
+
1. **LLM / AI Services** — one at a time (Anthropic, then OpenAI, then Gemini, etc.)
|
|
50
|
+
2. **Usage-Based Services** — one at a time (Scrapfly, Stripe, Browserbase)
|
|
51
|
+
3. **Infrastructure** — one at a time (Vercel, Supabase, AWS)
|
|
52
|
+
4. **Free-tier / flat-rate** — batch these: "PostHog, Inngest, and Resend are all on free tiers at $0. Any of those need updating? If not, moving on."
|
|
39
53
|
|
|
40
|
-
###
|
|
41
|
-
- Credit-pool services: "What Scrapfly plan are you on? The probe detected Pro with 1M credits — correct?"
|
|
42
|
-
- Ask about actual budget, not just plan cost
|
|
54
|
+
### For each paid service, present:
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
1. **What was detected**: service name, current default plan, default budget
|
|
57
|
+
2. **What the probe found** (if anything): actual spend, credits used, plan detected
|
|
58
|
+
3. **One clear question**: "Does this look right?" or "What plan are you actually on?"
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
- These are usually quick: "PostHog and Inngest are set to Free/$0 — correct? Moving on."
|
|
49
|
-
- Don't dwell on these unless the user has something to say
|
|
60
|
+
Example per-service message:
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
> **Scrapfly** — defaulted to Pro ($100/mo, 1M credits).
|
|
63
|
+
> Probe detected: Pro plan confirmed, 250K/1M credits used this month.
|
|
64
|
+
> Keep the $100/mo budget? [Y/adjust]
|
|
52
65
|
|
|
53
|
-
|
|
66
|
+
Then STOP. Wait for response.
|
|
67
|
+
|
|
68
|
+
### After the user responds:
|
|
69
|
+
|
|
70
|
+
1. Run `burnwatch configure` with their answer
|
|
71
|
+
2. Confirm what was written (one line)
|
|
72
|
+
3. Present the NEXT service
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
|
|
76
|
+
> Got it — Scrapfly set to Pro, $100/mo budget. ✅
|
|
77
|
+
>
|
|
78
|
+
> Next — **Stripe**. Defaulted to "Standard (2.9% + 30¢)" at $50/mo.
|
|
79
|
+
> I checked your balance: $1,247.83 available.
|
|
80
|
+
> $50/mo budget for Stripe's processing fees — sound right?
|
|
81
|
+
|
|
82
|
+
Then STOP again.
|
|
83
|
+
|
|
84
|
+
### For free-tier services, batch them:
|
|
85
|
+
|
|
86
|
+
> **PostHog**, **Inngest**, and **Resend** are all set to free tiers at $0/mo.
|
|
87
|
+
> Any of those need updating, or should I lock them in and move on?
|
|
88
|
+
|
|
89
|
+
## Step 4: Configure each service
|
|
90
|
+
|
|
91
|
+
After the user confirms or corrects, write it back immediately:
|
|
54
92
|
|
|
55
93
|
```bash
|
|
56
94
|
burnwatch configure --service <id> --plan "<plan name>" --budget <N>
|
|
@@ -66,78 +104,28 @@ To exclude a service:
|
|
|
66
104
|
burnwatch configure --service <id> --exclude
|
|
67
105
|
```
|
|
68
106
|
|
|
69
|
-
|
|
107
|
+
Always check the JSON output for `"success": true`.
|
|
70
108
|
|
|
71
|
-
## Step 5:
|
|
109
|
+
## Step 5: Wrap up
|
|
72
110
|
|
|
73
|
-
After all services are configured, run:
|
|
111
|
+
After all services are configured, run `burnwatch status` and present the brief:
|
|
74
112
|
|
|
75
|
-
|
|
76
|
-
burnwatch status
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Present the brief and celebrate what's tracked vs what's still blind.
|
|
113
|
+
> All done! Here's your updated spend brief:
|
|
114
|
+
> [burnwatch status output]
|
|
115
|
+
>
|
|
116
|
+
> N services tracked, total budget $X/mo.
|
|
80
117
|
|
|
81
118
|
## Key Behaviors
|
|
82
119
|
|
|
83
|
-
- **
|
|
84
|
-
- **
|
|
85
|
-
- **Be brief
|
|
86
|
-
- **
|
|
87
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
- **One `configure` call per service.** Don't batch. This makes it clear what was written and lets the user correct individual services.
|
|
90
|
-
|
|
91
|
-
## Example Flow
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
Agent: I found 8 services in your project. Let me walk through each one
|
|
95
|
-
to make sure we're tracking accurately.
|
|
96
|
-
|
|
97
|
-
Right now everything has default budgets — let's fix that.
|
|
98
|
-
|
|
99
|
-
First, your LLMs:
|
|
100
|
-
• Anthropic — defaulted to "API Usage" plan, $100/mo budget.
|
|
101
|
-
I probed your API and you've spent $47.23 this month.
|
|
102
|
-
• OpenAI — defaulted to "API Usage", $100/mo. No admin key,
|
|
103
|
-
so I can't check actual spend.
|
|
104
|
-
|
|
105
|
-
What are your actual budgets for these?
|
|
106
|
-
|
|
107
|
-
User: Anthropic $150 is better, I've been using it heavily.
|
|
108
|
-
OpenAI keep at $100, and here's my admin key: sk-admin-xxx
|
|
109
|
-
|
|
110
|
-
Agent: Got it. Let me update both...
|
|
111
|
-
|
|
112
|
-
[runs: burnwatch configure --service anthropic --budget 150]
|
|
113
|
-
[runs: burnwatch configure --service openai --key sk-admin-xxx --budget 100]
|
|
114
|
-
|
|
115
|
-
✅ Anthropic: API Usage | LIVE | $150/mo
|
|
116
|
-
✅ OpenAI: API Usage | LIVE | $100/mo (probed: 12.3K tokens this month)
|
|
117
|
-
|
|
118
|
-
Next, Scrapfly — I detected you're on Pro ($100/mo, 1M credits).
|
|
119
|
-
You've used 250K credits so far. Keep the $100 budget?
|
|
120
|
-
|
|
121
|
-
User: Yeah that's right.
|
|
122
|
-
|
|
123
|
-
Agent: [runs: burnwatch configure --service scrapfly --plan pro --budget 100]
|
|
124
|
-
✅ Scrapfly: Pro | LIVE | $100/mo | 1M credits
|
|
125
|
-
|
|
126
|
-
PostHog, Inngest, and Resend are all on free tiers at $0.
|
|
127
|
-
Sound right?
|
|
128
|
-
|
|
129
|
-
User: Yep.
|
|
130
|
-
|
|
131
|
-
Agent: [runs configure for each]
|
|
132
|
-
|
|
133
|
-
All done! Here's your updated spend brief:
|
|
134
|
-
[runs: burnwatch status]
|
|
135
|
-
```
|
|
120
|
+
- **Lead with what you know.** If the probe detected their plan, state it confidently and ask for confirmation — don't make them pick from a list.
|
|
121
|
+
- **One question at a time.** Never ask about budget AND plan AND API key in the same message. If the plan is confirmed, ask about budget. If budget is confirmed, offer the API key option.
|
|
122
|
+
- **Be brief.** Each message should be 2-4 lines, not paragraphs.
|
|
123
|
+
- **Respect shortcuts.** If the user says "defaults are fine for everything" — configure them all and show the summary. Don't force 14 rounds.
|
|
124
|
+
- **If they offer a key unprompted**, use it immediately and show what the probe found.
|
|
125
|
+
- **Surface concerns.** If Anthropic spend is $87 of $100 budget, say so. If Scrapfly credits are 85% consumed, flag it.
|
|
136
126
|
|
|
137
127
|
## Probe Data Reference
|
|
138
128
|
|
|
139
|
-
Services with probes and what they return:
|
|
140
|
-
|
|
141
129
|
| Service | Confidence | What's detected |
|
|
142
130
|
|---------|-----------|-----------------|
|
|
143
131
|
| Scrapfly | high | Plan name, credits used/total |
|
|
@@ -149,5 +137,4 @@ Services with probes and what they return:
|
|
|
149
137
|
| Browserbase | medium | Session count, browser hours |
|
|
150
138
|
| Upstash | low | Database count (key validation) |
|
|
151
139
|
| PostHog | low | Org found (key validation) |
|
|
152
|
-
| Gemini, Resend, Inngest, Voyage AI | none | No API — ask user directly |
|
|
153
|
-
| AWS | none | Too complex — flag as BLIND |
|
|
140
|
+
| Gemini, Resend, Inngest, Voyage AI, AWS | none | No API — ask user directly |
|