burnwatch 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/interactive-init.ts","../src/core/config.ts","../src/services/base.ts"],"sourcesContent":["/**\n * Interactive init flow for burnwatch.\n *\n * Conducts a per-service interview: detects what it can automatically\n * (existing API keys, env vars), asks for plan selection, collects\n * API keys for LIVE tracking, and ensures every service exits with\n * a budget. No skipping.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { fetchJson } from \"./services/base.js\";\n\n/** Risk categories in display order: LLMs first, then usage-based, infra, flat-rate */\nconst RISK_ORDER: ServiceRiskCategory[] = [\"llm\", \"usage\", \"infra\", \"flat\"];\n\nconst RISK_LABELS: Record<ServiceRiskCategory, string> = {\n llm: \"LLM / AI Services (highest variable cost)\",\n usage: \"Usage-Based Services\",\n infra: \"Infrastructure & Compute\",\n flat: \"Flat-Rate / Free Tier Services\",\n};\n\n/** Where to find API keys for LIVE-capable services */\nconst API_KEY_HINTS: Record<string, string> = {\n anthropic: \"Admin key: console.anthropic.com -> Settings -> Admin API Keys\",\n openai: \"Org key: platform.openai.com -> Settings -> API Keys\",\n vercel: \"Token: vercel.com/account/tokens\",\n supabase: \"Service role key: supabase.com/dashboard -> Settings -> API\",\n stripe: \"Secret key: dashboard.stripe.com -> Developers -> API Keys\",\n scrapfly: \"API key: scrapfly.io/dashboard\",\n};\n\n/** Map service IDs to risk categories */\nfunction classifyRisk(service: ServiceDefinition): ServiceRiskCategory {\n if (service.billingModel === \"token_usage\") return \"llm\";\n if (\n service.billingModel === \"credit_pool\" ||\n service.billingModel === \"percentage\" ||\n service.billingModel === \"per_unit\"\n )\n return \"usage\";\n if (service.billingModel === \"compute\") return \"infra\";\n return \"flat\";\n}\n\n/** Group detection results by risk category */\nfunction groupByRisk(\n detected: DetectionResult[],\n): Map<ServiceRiskCategory, DetectionResult[]> {\n const groups = new Map<ServiceRiskCategory, DetectionResult[]>();\n for (const cat of RISK_ORDER) {\n groups.set(cat, []);\n }\n\n for (const det of detected) {\n const cat = classifyRisk(det.service);\n groups.get(cat)!.push(det);\n }\n\n return groups;\n}\n\n/** Prompt the user with a question and return their answer */\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/** Try to auto-detect plan from Scrapfly API */\nasync function autoDetectScrapflyPlan(\n apiKey: string,\n): Promise<string | null> {\n try {\n const result = await fetchJson<{\n subscription?: { plan?: { name?: string } };\n }>(`https://api.scrapfly.io/account?key=${apiKey}`);\n\n if (result.ok && result.data?.subscription?.plan?.name) {\n return result.data.subscription.plan.name;\n }\n } catch {\n // Ignore errors\n }\n return null;\n}\n\n/** Scan environment for API keys matching service env patterns */\nfunction findEnvKey(service: ServiceDefinition): string | undefined {\n for (const pattern of service.envPatterns) {\n const val = process.env[pattern];\n if (val && val.length > 0) return val;\n }\n return undefined;\n}\n\nexport interface InteractiveInitResult {\n services: Record<string, TrackedService>;\n}\n\n/**\n * Auto-configure all services without prompts.\n *\n * Applies the same logic as the interactive interview but picks\n * defaults automatically: default plan, env var keys, budget = plan cost.\n * Used when stdin is not a TTY (e.g., Claude Code, piped input).\n */\nexport function autoConfigureServices(\n detected: DetectionResult[],\n): InteractiveInitResult {\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Auto-configuring with defaults.\\n`,\n );\n console.log(\" Run 'burnwatch init' from your terminal for interactive setup.\\n\");\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(` ${RISK_LABELS[category]}`);\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans ?? [];\n const defaultPlan = plans.find((p) => p.default) ?? plans[0];\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n\n if (defaultPlan && defaultPlan.type !== \"exclude\") {\n tracked.planName = defaultPlan.name;\n\n if (defaultPlan.type === \"flat\" && defaultPlan.monthlyBase !== undefined) {\n tracked.planCost = defaultPlan.monthlyBase;\n tracked.budget = defaultPlan.monthlyBase;\n } else if (defaultPlan.suggestedBudget !== undefined) {\n tracked.budget = defaultPlan.suggestedBudget;\n }\n }\n\n // Check for existing API key in global config or environment\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n let keySource = \"\";\n\n if (existingKey) {\n tracked.hasApiKey = true;\n keySource = \" (key: global config)\";\n } else if (envKey) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n keySource = ` (key: ${service.envPatterns[0]})`;\n }\n\n const tierLabel = tracked.hasApiKey\n ? \"LIVE\"\n : tracked.planCost !== undefined\n ? \"CALC\"\n : \"BLIND\";\n const planStr = tracked.planName ? ` ${tracked.planName}` : \"\";\n console.log(\n ` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`,\n );\n\n services[service.id] = tracked;\n }\n console.log(\"\");\n }\n\n // Summary\n const trackedList = Object.values(services);\n const liveCount = trackedList.filter((s) => s.hasApiKey).length;\n const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\" \" + \"-\".repeat(48));\n console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n console.log(\"\");\n\n // Save discovered keys\n writeGlobalConfig(globalConfig);\n\n return { services };\n}\n\n/**\n * Run the interactive init flow.\n *\n * For each detected service:\n * 1. Ask which plan they're on\n * 2. If LIVE-capable, check for existing key or ask for one\n * 3. Set budget (defaults to plan cost, $0 for free - never skipped)\n */\nexport async function runInteractiveInit(\n detected: DetectionResult[],\n): Promise<InteractiveInitResult> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const services: Record<string, TrackedService> = {};\n const groups = groupByRisk(detected);\n const globalConfig = readGlobalConfig();\n\n console.log(\n `\\n Found ${detected.length} paid service${detected.length !== 1 ? \"s\" : \"\"}. Let's configure each one.\\n`,\n );\n\n for (const category of RISK_ORDER) {\n const group = groups.get(category)!;\n if (group.length === 0) continue;\n\n console.log(`\\n ${RISK_LABELS[category]}`);\n console.log(\" \" + \"-\".repeat(48));\n\n for (const det of group) {\n const service = det.service;\n const plans = service.plans;\n\n console.log(`\\n ${service.name}`);\n console.log(` Detected via: ${det.details.join(\", \")}`);\n\n if (!plans || plans.length === 0) {\n // No plans defined - basic tracking with $0 budget\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n budget: 0,\n };\n console.log(\" -> Configured (no plan tiers in registry, budget: $0)\");\n continue;\n }\n\n // --- Plan selection ---\n const defaultIndex = plans.findIndex((p) => p.default);\n console.log(\"\");\n for (let i = 0; i < plans.length; i++) {\n const plan = plans[i]!;\n const marker = i === defaultIndex ? \" *\" : \"\";\n const costStr =\n plan.type === \"exclude\"\n ? \"\"\n : plan.monthlyBase !== undefined\n ? ` - $${plan.monthlyBase}/mo`\n : \" - variable\";\n console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);\n }\n\n const defaultChoice =\n defaultIndex >= 0 ? String(defaultIndex + 1) : \"1\";\n const answer = await ask(\n rl,\n ` Which plan? [${defaultChoice}]: `,\n );\n\n const choiceIndex = (answer === \"\" ? parseInt(defaultChoice) : parseInt(answer)) - 1;\n const chosen =\n plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0]!;\n\n if (chosen.type === \"exclude\") {\n services[service.id] = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n excluded: true,\n planName: chosen.name,\n };\n console.log(` -> ${service.name}: excluded`);\n continue;\n }\n\n const tracked: TrackedService = {\n serviceId: service.id,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n planName: chosen.name,\n };\n\n if (chosen.type === \"flat\" && chosen.monthlyBase !== undefined) {\n tracked.planCost = chosen.monthlyBase;\n }\n\n // --- API key (LIVE-capable services) ---\n if (service.apiTier === \"live\") {\n const existingKey = globalConfig.services[service.id]?.apiKey;\n const envKey = findEnvKey(service);\n\n if (existingKey) {\n console.log(` API key: found in global config`);\n tracked.hasApiKey = true;\n } else if (envKey) {\n console.log(` API key: found in environment (${service.envPatterns[0]})`);\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = envKey;\n } else {\n const hint = API_KEY_HINTS[service.id];\n if (hint) console.log(` ${hint}`);\n const keyAnswer = await ask(\n rl,\n ` API key for real-time tracking (Enter to skip): `,\n );\n if (keyAnswer) {\n tracked.hasApiKey = true;\n if (!globalConfig.services[service.id]) {\n globalConfig.services[service.id] = {};\n }\n globalConfig.services[service.id]!.apiKey = keyAnswer;\n }\n }\n\n // Auto-detect plan for Scrapfly\n if (service.autoDetectPlan && service.id === \"scrapfly\" && tracked.hasApiKey) {\n const key = globalConfig.services[service.id]?.apiKey;\n if (key) {\n console.log(\" Detecting plan from API...\");\n const planName = await autoDetectScrapflyPlan(key);\n if (planName) {\n console.log(` -> Detected plan: ${planName}`);\n tracked.planName = planName;\n }\n }\n }\n }\n\n // --- Budget (always set, never skip) ---\n const 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 console.log(\n ` -> ${service.name}: ${tracked.planName} | ${tierLabel} | $${tracked.budget}/mo`,\n );\n }\n }\n\n // --- Summary ---\n const tracked = Object.values(services).filter((s) => !s.excluded);\n const excluded = Object.values(services).filter((s) => s.excluded);\n const liveCount = tracked.filter((s) => s.hasApiKey).length;\n const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);\n\n console.log(\"\\n \" + \"=\".repeat(48));\n console.log(` ${tracked.length} services configured`);\n if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);\n if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);\n if (excluded.length > 0) console.log(` ${excluded.length} excluded`);\n console.log(` Total monthly budget: $${totalBudget}`);\n console.log(\" \" + \"=\".repeat(48));\n\n // Save any collected API keys\n writeGlobalConfig(globalConfig);\n\n rl.close();\n\n return { services };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n"],"mappings":"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","names":["tracked"]}
1
+ {"version":3,"sources":["../src/interactive-init.ts","../src/core/config.ts","../src/services/base.ts","../src/probes.ts"],"sourcesContent":["/**\n * Interactive init flow for burnwatch.\n *\n * Conducts a per-service interview: detects what it can automatically\n * (existing API keys, env vars), asks for plan selection, collects\n * API keys for LIVE tracking, and ensures every service exits with\n * a budget. No skipping.\n */\n\nimport * as readline from \"node:readline\";\nimport type {\n ServiceDefinition,\n PlanTier,\n TrackedService,\n ServiceRiskCategory,\n} from \"./core/types.js\";\nimport type { DetectionResult } from \"./detection/detector.js\";\nimport { readGlobalConfig, writeGlobalConfig } from \"./core/config.js\";\nimport { 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","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n /** 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"],"mappings":"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","names":["tracked"]}
@@ -262,6 +262,9 @@ var scrapflyConnector = {
262
262
  spend,
263
263
  isEstimate: false,
264
264
  tier: "live",
265
+ unitsUsed: creditsUsed,
266
+ unitsTotal: creditsTotal,
267
+ unitName: "credits",
265
268
  raw: {
266
269
  credits_used: creditsUsed,
267
270
  credits_total: creditsTotal,
@@ -409,6 +412,15 @@ function formatBrief(brief) {
409
412
  const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
410
413
  const leftStr = formatLeft(svc);
411
414
  lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));
415
+ if (svc.allowance) {
416
+ const usedStr = formatCompact(svc.allowance.used);
417
+ const totalStr2 = formatCompact(svc.allowance.included);
418
+ const pctStr = svc.allowance.percent.toFixed(0);
419
+ const warn = svc.allowance.percent >= 75 ? " \u26A0\uFE0F" : "";
420
+ lines.push(
421
+ `\u2551 \u21B3 ${usedStr}/${totalStr2} ${svc.allowance.unitName} (${pctStr}%)${warn}`.padEnd(width + 1) + "\u2551"
422
+ );
423
+ }
412
424
  }
413
425
  lines.push(`\u2560${hrDouble}\u2563`);
414
426
  const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
@@ -510,7 +522,7 @@ function formatLeft(snap) {
510
522
  }
511
523
  return "\u2014";
512
524
  }
513
- function buildSnapshot(serviceId, tier, spend, budget) {
525
+ function buildSnapshot(serviceId, tier, spend, budget, allowanceData) {
514
526
  const isEstimate = tier === "est" || tier === "calc";
515
527
  const budgetPercent = budget ? spend / budget * 100 : void 0;
516
528
  let status = "unknown";
@@ -527,7 +539,21 @@ function buildSnapshot(serviceId, tier, spend, budget) {
527
539
  statusLabel = `${(100 - budgetPercent).toFixed(0)}% \u2014 healthy`;
528
540
  }
529
541
  }
530
- if (tier === "calc" && budget) {
542
+ let allowance;
543
+ if (allowanceData && allowanceData.included > 0) {
544
+ const percent = allowanceData.used / allowanceData.included * 100;
545
+ allowance = { ...allowanceData, percent };
546
+ if (percent > 100) {
547
+ status = "over";
548
+ statusLabel = `\u26A0\uFE0F ${percent.toFixed(0)}% of ${formatCompact(allowanceData.included)} ${allowanceData.unitName} used`;
549
+ } else if (percent >= 75) {
550
+ status = "caution";
551
+ statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left \u2014 caution`;
552
+ } else {
553
+ status = "healthy";
554
+ statusLabel = `${formatCompact(allowanceData.included - allowanceData.used)} ${allowanceData.unitName} left`;
555
+ }
556
+ } else if (tier === "calc" && budget) {
531
557
  statusLabel = `flat \u2014 on plan`;
532
558
  status = "healthy";
533
559
  }
@@ -540,9 +566,15 @@ function buildSnapshot(serviceId, tier, spend, budget) {
540
566
  budgetPercent,
541
567
  status,
542
568
  statusLabel,
543
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
569
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
570
+ allowance
544
571
  };
545
572
  }
573
+ function formatCompact(n) {
574
+ if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
575
+ if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
576
+ return String(n);
577
+ }
546
578
 
547
579
  // src/detection/detector.ts
548
580
  import * as fs4 from "fs";