burnwatch 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +276 -214
- package/billing/anthropic.json +49 -0
- package/billing/billing.schema.json +213 -0
- package/billing/browserbase.json +68 -0
- package/billing/google-gemini.json +66 -0
- package/billing/inngest.json +45 -0
- package/billing/openai.json +70 -0
- package/billing/posthog.json +61 -0
- package/billing/resend.json +49 -0
- package/billing/scrapfly.json +38 -0
- package/billing/supabase.json +32 -0
- package/billing/upstash.json +65 -0
- package/billing/vercel.json +85 -0
- package/billing/voyage-ai.json +42 -0
- package/dist/cli.js +477 -286
- package/dist/cli.js.map +1 -1
- package/dist/cost-impact.d.ts +8 -4
- package/dist/cost-impact.js +261 -72
- package/dist/cost-impact.js.map +1 -1
- package/dist/{detector-myYS2eVC.d.ts → detector-DiBj3WjE.d.ts} +1 -1
- package/dist/hooks/on-file-change.js +271 -88
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +104 -79
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +65 -46
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +286 -113
- package/dist/index.js.map +1 -1
- package/dist/interactive-init.d.ts +2 -2
- package/dist/interactive-init.js +21 -19
- package/dist/interactive-init.js.map +1 -1
- package/dist/mcp-server.js +719 -88
- package/dist/mcp-server.js.map +1 -1
- package/dist/{types-BwIeWOYc.d.ts → types-CUAiYzmE.d.ts} +2 -0
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/registry.json +12 -68
- package/skills/burnwatch-interview/SKILL.md +2 -4
- package/skills/setup-burnwatch/SKILL.md +7 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/on-file-change.ts","../../src/core/config.ts","../../src/detection/detector.ts","../../src/core/registry.ts","../../src/core/ledger.ts","../../src/cost-impact.ts","../../src/utilization.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * PostToolUse hook (Edit|Write) — fires when files are changed.\n *\n * 1. Scans changed files for new service introductions\n * 2. Analyzes cost impact of SDK calls (invocation sites, multipliers, projected cost)\n * 3. Injects cost impact cards into Claude's context\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { HookInput, HookOutput } from \"../core/types.js\";\nimport {\n readProjectConfig,\n writeProjectConfig,\n isInitialized,\n projectDataDir,\n} from \"../core/config.js\";\nimport { detectInFileChange } from \"../detection/detector.js\";\nimport { logEvent } from \"../core/ledger.js\";\nimport { readLatestSnapshot } from \"../core/ledger.js\";\nimport type { TrackedService } from \"../core/types.js\";\nimport { analyzeCostImpact, formatCostImpactCard } from \"../cost-impact.js\";\nimport {\n readUtilizationModel,\n writeUtilizationModel,\n analyzeFileUtilization,\n updateUtilizationModel,\n} from \"../utilization.js\";\n\n/** Session cost impact accumulator file path */\nfunction sessionImpactPath(projectRoot: string, sessionId: string): string {\n return path.join(projectDataDir(projectRoot), \"cache\", `session-impact-${sessionId}.json`);\n}\n\n/** Read accumulated session cost impacts */\nfunction readSessionImpacts(\n projectRoot: string,\n sessionId: string,\n): Record<string, { costLow: number; costHigh: number }> {\n try {\n const raw = fs.readFileSync(sessionImpactPath(projectRoot, sessionId), \"utf-8\");\n return JSON.parse(raw) as Record<string, { costLow: number; costHigh: number }>;\n } catch {\n return {};\n }\n}\n\n/** Write accumulated session cost impacts */\nfunction writeSessionImpacts(\n projectRoot: string,\n sessionId: string,\n impacts: Record<string, { costLow: number; costHigh: number }>,\n): void {\n const dir = path.join(projectDataDir(projectRoot), \"cache\");\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(\n sessionImpactPath(projectRoot, sessionId),\n JSON.stringify(impacts, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\nfunction main(): void {\n // Read hook input from stdin\n let input: HookInput;\n try {\n const stdin = fs.readFileSync(0, \"utf-8\");\n input = JSON.parse(stdin) as HookInput;\n } catch {\n process.exit(0);\n return;\n }\n\n const projectRoot = input.cwd;\n\n // Guard: not initialized\n if (!isInitialized(projectRoot)) {\n process.exit(0);\n return;\n }\n\n // Get file path and content from tool input\n const filePath = input.tool_input?.file_path;\n if (!filePath) {\n process.exit(0);\n return;\n }\n\n // Read the current file content\n let content: string;\n try {\n content = fs.readFileSync(filePath, \"utf-8\");\n } catch {\n process.exit(0);\n return;\n }\n\n const config = readProjectConfig(projectRoot)!;\n const contextParts: string[] = [];\n\n // --- Part 1: Detect new services in this file change ---\n const detected = detectInFileChange(filePath, content, projectRoot);\n const newServices: string[] = [];\n\n for (const det of detected) {\n const serviceId = det.service.id;\n\n // Skip if already tracked\n if (config.services[serviceId]) continue;\n\n // Auto-register as a new tracked service (BLIND until configured)\n const tracked: TrackedService = {\n serviceId,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n };\n\n config.services[serviceId] = tracked;\n newServices.push(serviceId);\n\n // Log the detection\n logEvent(\n {\n timestamp: new Date().toISOString(),\n sessionId: input.session_id,\n type: \"service_detected\",\n data: {\n serviceId,\n sources: det.sources,\n details: det.details,\n file: filePath,\n },\n },\n projectRoot,\n );\n }\n\n // Save updated config\n if (newServices.length > 0) {\n writeProjectConfig(config, projectRoot);\n }\n\n // Alert about new services\n if (newServices.length > 0) {\n const alerts = newServices.map(\n (id) =>\n `[BURNWATCH] 🆕 New paid service detected: ${id}\\n Run 'burnwatch add ${id}' to configure budget and tracking.`,\n );\n contextParts.push(alerts.join(\"\\n\\n\"));\n }\n\n // --- Part 2: Cost impact analysis ---\n const impacts = analyzeCostImpact(filePath, content, projectRoot);\n\n if (impacts.length > 0) {\n // Build current budget status from latest snapshot\n const snapshot = readLatestSnapshot(projectRoot);\n const currentBudgets: Record<string, { spend: number; budget?: number }> = {};\n\n if (snapshot) {\n for (const svc of snapshot.services) {\n currentBudgets[svc.serviceId] = {\n spend: svc.spend,\n budget: svc.budget,\n };\n }\n }\n\n // Format and add cost impact card\n const card = formatCostImpactCard(impacts, currentBudgets);\n contextParts.push(card);\n\n // Accumulate session cost impacts\n const sessionImpacts = readSessionImpacts(projectRoot, input.session_id);\n for (const impact of impacts) {\n const existing = sessionImpacts[impact.serviceId];\n if (existing) {\n existing.costLow += impact.costLow;\n existing.costHigh += impact.costHigh;\n } else {\n sessionImpacts[impact.serviceId] = {\n costLow: impact.costLow,\n costHigh: impact.costHigh,\n };\n }\n }\n writeSessionImpacts(projectRoot, input.session_id, sessionImpacts);\n\n // Log cost impact event\n logEvent(\n {\n timestamp: new Date().toISOString(),\n sessionId: input.session_id,\n type: \"cost_impact\",\n data: {\n file: filePath,\n impacts: impacts.map((i) => ({\n serviceId: i.serviceId,\n callCount: i.callCount,\n monthlyInvocations: i.monthlyInvocations,\n costLow: i.costLow,\n costHigh: i.costHigh,\n })),\n },\n },\n projectRoot,\n );\n }\n\n // --- Part 3: Update utilization model incrementally ---\n try {\n const relPath = path.relative(projectRoot, filePath);\n const callSites = analyzeFileUtilization(filePath, content, projectRoot).map(\n (cs) => ({ ...cs, filePath: relPath }),\n );\n const model = readUtilizationModel(projectRoot);\n updateUtilizationModel(model, relPath, callSites, projectRoot);\n writeUtilizationModel(model, projectRoot);\n\n // Add overage warnings to the context\n for (const svc of Object.values(model.services)) {\n if (svc.projectedOverage > 0 && svc.projectedOverageCost > 5) {\n contextParts.push(\n `[BURNWATCH] ⚠️ ${svc.serviceName} utilization: ~${formatCompact(svc.totalMonthlyUnits)} ${svc.unitName}/mo` +\n (svc.planIncluded ? ` (plan includes ${formatCompact(svc.planIncluded)})` : \"\") +\n ` → ~$${svc.projectedOverageCost.toFixed(2)}/mo overage`,\n );\n }\n }\n } catch {\n // Utilization update is best-effort — don't block the hook\n }\n\n // --- Output ---\n if (contextParts.length > 0) {\n const output: HookOutput = {\n hookSpecificOutput: {\n hookEventName: \"PostToolUse\",\n additionalContext: contextParts.join(\"\\n\\n\"),\n },\n };\n\n process.stdout.write(JSON.stringify(output));\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(Math.round(n));\n}\n\nmain();\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 = parseEnvKeys(content);\n\n for (const [, service] of registry) {\n const matched = service.envPatterns.filter((p) => envKeys.has(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 for (const key of parseEnvKeys(content)) {\n envVars.add(key);\n }\n } catch {\n // Skip unreadable files\n }\n }\n\n return envVars;\n}\n\n/**\n * Parse an env file's content into a set of variable names.\n * Handles `export` prefix, quoted values, inline comments, whitespace.\n */\nexport function parseEnvKeys(content: string): Set<string> {\n const keys = new Set<string>();\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n // Strip optional \"export \" prefix\n const stripped = trimmed.startsWith(\"export \")\n ? trimmed.slice(7).trim()\n : trimmed;\n const eqIdx = stripped.indexOf(\"=\");\n if (eqIdx > 0) {\n keys.add(stripped.slice(0, eqIdx).trim());\n }\n }\n return keys;\n}\n\n/**\n * Find all .env* files recursively (but not in node_modules, .git, dist, etc.)\n */\nexport function 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;\nlet cachedRegistryPath: string | null = null;\nlet cachedRegistryMtime: number | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n * Invalidates cache when the registry file has been modified (for long-running processes like MCP server).\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n // Check cache validity — invalidate if the file has been modified\n if (cachedRegistry && cachedRegistryPath && cachedRegistryMtime !== null) {\n try {\n const stat = fs.statSync(cachedRegistryPath);\n if (stat.mtimeMs !== cachedRegistryMtime) {\n cachedRegistry = null;\n }\n } catch {\n // File gone — invalidate\n cachedRegistry = null;\n }\n }\n\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 let resolvedPath: string | null = null;\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 resolvedPath = candidate;\n break;\n }\n }\n\n // Load project-local override (if exists) — takes precedence for mtime tracking\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n resolvedPath = localPath;\n }\n }\n\n cachedRegistry = registry;\n\n // Track mtime for cache invalidation (important for long-running MCP server)\n if (resolvedPath) {\n try {\n cachedRegistryPath = resolvedPath;\n cachedRegistryMtime = fs.statSync(resolvedPath).mtimeMs;\n } catch {\n cachedRegistryPath = null;\n cachedRegistryMtime = null;\n }\n }\n\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 cachedRegistryPath = null;\n cachedRegistryMtime = 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 * 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 * Predictive cost impact analysis.\n *\n * Scans file content for SDK call sites, detects multipliers (loops, .map(), etc.),\n * and projects monthly cost using registry pricing data.\n */\n\nimport type {\n CostImpact,\n ServiceDefinition,\n} from \"./core/types.js\";\nimport { loadRegistry } from \"./core/registry.js\";\n\n/** SDK call patterns per service — maps serviceId to regex patterns for call sites */\nconst SERVICE_CALL_PATTERNS: Record<string, RegExp[]> = {\n anthropic: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /anthropic\\.\\w+\\.create\\s*\\(/g,\n ],\n openai: [\n /\\.chat\\.completions\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /\\.images\\.generate\\s*\\(/g,\n /\\.embeddings\\.create\\s*\\(/g,\n /openai\\.\\w+\\.create\\s*\\(/g,\n ],\n \"google-gemini\": [\n /\\.generateContent\\s*\\(/g,\n /\\.generateContentStream\\s*\\(/g,\n /model\\.generate\\w*\\s*\\(/g,\n ],\n \"voyage-ai\": [\n /\\.embed\\s*\\(/g,\n /voyageai\\.embed\\s*\\(/g,\n ],\n scrapfly: [\n /\\.scrape\\s*\\(/g,\n /scrapfly\\.scrape\\s*\\(/g,\n /\\.async_scrape\\s*\\(/g,\n /ScrapeConfig\\s*\\(/g,\n ],\n browserbase: [\n /\\.createSession\\s*\\(/g,\n /\\.sessions\\.create\\s*\\(/g,\n /stagehand\\.act\\s*\\(/g,\n /stagehand\\.extract\\s*\\(/g,\n ],\n upstash: [\n /redis\\.\\w+\\s*\\(/g,\n /\\.set\\s*\\(/g,\n /\\.get\\s*\\(/g,\n /\\.incr\\s*\\(/g,\n /\\.hset\\s*\\(/g,\n ],\n resend: [\n /resend\\.emails\\.send\\s*\\(/g,\n /\\.emails\\.send\\s*\\(/g,\n ],\n stripe: [\n /stripe\\.charges\\.create\\s*\\(/g,\n /stripe\\.paymentIntents\\.create\\s*\\(/g,\n /stripe\\.checkout\\.sessions\\.create\\s*\\(/g,\n ],\n supabase: [\n /supabase\\.from\\s*\\(/g,\n /\\.rpc\\s*\\(/g,\n /supabase\\.storage/g,\n ],\n inngest: [\n /inngest\\.send\\s*\\(/g,\n /\\.createFunction\\s*\\(/g,\n ],\n posthog: [\n /posthog\\.capture\\s*\\(/g,\n /\\.capture\\s*\\(/g,\n ],\n aws: [\n /\\.send\\s*\\(new\\s+\\w+Command/g,\n /s3Client\\.send\\s*\\(/g,\n /lambdaClient\\.send\\s*\\(/g,\n ],\n firebase: [\n /firestore\\.\\w+\\(\\s*[\"']/g,\n /\\.collection\\s*\\(/g,\n /\\.doc\\s*\\(/g,\n /admin\\.firestore\\(\\)/g,\n ],\n twilio: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.calls\\.create\\s*\\(/g,\n /twilio\\.messages/g,\n ],\n sendgrid: [\n /sgMail\\.send\\s*\\(/g,\n /\\.send\\s*\\(\\s*msg/g,\n ],\n \"mongodb-atlas\": [\n /\\.find\\s*\\(/g,\n /\\.insertOne\\s*\\(/g,\n /\\.insertMany\\s*\\(/g,\n /\\.updateOne\\s*\\(/g,\n /\\.aggregate\\s*\\(/g,\n ],\n clerk: [\n /clerkClient\\.\\w+/g,\n /auth\\(\\)/g,\n ],\n replicate: [\n /replicate\\.run\\s*\\(/g,\n /replicate\\.predictions\\.create\\s*\\(/g,\n ],\n};\n\n/** Multiplier patterns — things that make calls happen more than once */\ninterface MultiplierMatch {\n label: string;\n factor: number;\n}\n\nfunction detectMultipliers(content: string): MultiplierMatch[] {\n const multipliers: MultiplierMatch[] = [];\n\n // for loops — assume 10x as conservative estimate\n if (/for\\s*\\(.*;\\s*\\w+\\s*<\\s*(\\w+)/g.test(content)) {\n // Try to extract the loop bound\n const loopMatch = content.match(/for\\s*\\(.*;\\s*\\w+\\s*<\\s*(\\d+)/);\n if (loopMatch) {\n const bound = parseInt(loopMatch[1]!);\n if (bound > 1) {\n multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });\n }\n } else {\n multipliers.push({ label: \"for loop (variable bound)\", factor: 10 });\n }\n }\n\n // .map() calls\n if (/\\.\\s*map\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n multipliers.push({ label: \".map() iteration\", factor: 10 });\n }\n\n // .forEach() calls\n if (/\\.\\s*forEach\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n multipliers.push({ label: \".forEach() iteration\", factor: 10 });\n }\n\n // for...of / for...in\n if (/for\\s*\\(\\s*(const|let|var)\\s+\\w+\\s+(of|in)\\s+/g.test(content)) {\n multipliers.push({ label: \"for...of/in loop\", factor: 10 });\n }\n\n // Promise.all with array\n if (/Promise\\.all\\s*\\(/g.test(content)) {\n multipliers.push({ label: \"Promise.all (parallel batch)\", factor: 10 });\n }\n\n // Cron patterns in comments or configuration\n if (/cron|schedule|interval|setInterval|every\\s+\\d+\\s*(min|hour|day|sec)/gi.test(content)) {\n // Estimate: if hourly = 720/mo, daily = 30/mo, every 5 min = 8640/mo\n if (/every\\s+5\\s*min/gi.test(content) || /\\*\\/5\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 5 minutes\", factor: 8640 });\n } else if (/every\\s+1?\\s*hour/gi.test(content) || /0\\s+\\*\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: hourly\", factor: 720 });\n } else if (/every\\s+1?\\s*day/gi.test(content) || /0\\s+0\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: daily\", factor: 30 });\n } else {\n multipliers.push({ label: \"scheduled execution\", factor: 30 });\n }\n }\n\n // Batch size hints\n const batchMatch = content.match(/batch[_\\s]?size\\s*[=:]\\s*(\\d+)/i);\n if (batchMatch) {\n const batchSize = parseInt(batchMatch[1]!);\n if (batchSize > 1) {\n multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });\n }\n }\n\n return multipliers;\n}\n\n/** Gotcha-based cost multipliers per service */\nconst GOTCHA_MULTIPLIERS: Record<string, { low: number; high: number; explanation: string }> = {\n scrapfly: {\n low: 1,\n high: 25,\n explanation: \"anti-bot bypass consumes 5-25x base credits\",\n },\n browserbase: {\n low: 1,\n high: 5,\n explanation: \"session duration affects cost — long sessions burn more\",\n },\n anthropic: {\n low: 1,\n high: 60,\n explanation: \"Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)\",\n },\n openai: {\n low: 1,\n high: 30,\n explanation: \"GPT-4 mini vs GPT-5 (30x cost range)\",\n },\n stripe: {\n low: 1,\n high: 1.5,\n explanation: \"international cards add 1-1.5% extra\",\n },\n};\n\n/**\n * Analyze a file's content for cost-impacting SDK calls.\n * Returns cost impact estimates for each detected service.\n */\nexport function analyzeCostImpact(\n filePath: string,\n content: string,\n projectRoot?: string,\n): CostImpact[] {\n // Only analyze source files\n if (!/\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {\n return [];\n }\n\n const registry = loadRegistry(projectRoot);\n const impacts: CostImpact[] = [];\n const multipliers = detectMultipliers(content);\n\n for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {\n let totalCalls = 0;\n\n for (const pattern of patterns) {\n // Reset lastIndex for global regexes\n pattern.lastIndex = 0;\n const matches = content.match(pattern);\n if (matches) {\n totalCalls += matches.length;\n }\n }\n\n if (totalCalls === 0) continue;\n\n const service = registry.get(serviceId);\n if (!service) continue;\n\n // Calculate effective multiplier\n const multiplierFactor = multipliers.length > 0\n ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1)\n : 1;\n\n // Assume ~30 working days, ~8 dev hours/day as baseline for monthly projections\n // If no cron detected, assume the code runs during dev sessions: ~50 times/month\n const baseMonthlyRuns = multipliers.some((m) => m.label.startsWith(\"cron\"))\n ? 1 // cron multiplier already encodes frequency\n : 50; // ~2 dev runs per working day\n\n const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;\n\n // Get cost estimates\n const gotcha = GOTCHA_MULTIPLIERS[serviceId];\n const unitRate = service.pricing?.unitRate ?? 0;\n\n let costLow: number;\n let costHigh: number;\n\n if (unitRate > 0) {\n costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);\n } else if (service.pricing?.monthlyBase !== undefined) {\n // Flat-rate services — cost is the plan, not per-invocation\n costLow = 0;\n costHigh = 0;\n } else {\n // Estimate based on typical per-call costs\n const typicalCallCosts: Record<string, number> = {\n anthropic: 0.003, // ~$3/MTok * ~1K tokens average\n openai: 0.002,\n \"google-gemini\": 0.001,\n scrapfly: 0.00015,\n browserbase: 0.01,\n resend: 0.001,\n stripe: 0.30,\n };\n const perCall = typicalCallCosts[serviceId] ?? 0.001;\n costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);\n }\n\n // Skip if no meaningful cost\n if (costLow === 0 && costHigh === 0) continue;\n\n impacts.push({\n serviceId,\n serviceName: service.name,\n filePath,\n callCount: totalCalls,\n multipliers: multipliers.map((m) => m.label),\n multiplierFactor,\n monthlyInvocations,\n costLow,\n costHigh,\n rangeExplanation: gotcha?.explanation,\n });\n }\n\n return impacts;\n}\n\n/**\n * Format a cost impact card for injection into Claude's context.\n */\nexport function formatCostImpactCard(\n impacts: CostImpact[],\n currentBudgets: Record<string, { spend: number; budget?: number }>,\n): string {\n const fileName = impacts[0]?.filePath.split(\"/\").pop() ?? \"unknown\";\n const lines: string[] = [];\n\n lines.push(`[BURNWATCH] ⚠️ Cost impact estimate for ${fileName}`);\n\n for (const impact of impacts) {\n const lowStr = impact.costLow < 1\n ? `$${impact.costLow.toFixed(2)}`\n : `$${impact.costLow.toFixed(0)}`;\n const highStr = impact.costHigh < 1\n ? `$${impact.costHigh.toFixed(2)}`\n : `$${impact.costHigh.toFixed(0)}`;\n\n const rangeStr = impact.costLow === impact.costHigh\n ? lowStr\n : `${lowStr}-${highStr}`;\n\n lines.push(\n ` ${impact.serviceName}: ~${impact.monthlyInvocations.toLocaleString()} calls/mo → ${rangeStr}/mo` +\n (impact.rangeExplanation ? ` (${impact.rangeExplanation})` : \"\"),\n );\n\n // Show current budget status if available\n const current = currentBudgets[impact.serviceId];\n if (current) {\n const budgetStr = current.budget\n ? `$${current.spend.toFixed(0)}/$${current.budget} budget`\n : `$${current.spend.toFixed(0)} (no budget set)`;\n const pctStr = current.budget && current.budget > 0\n ? ` (${((current.spend / current.budget) * 100).toFixed(0)}%)`\n : \"\";\n lines.push(` Current: ${budgetStr}${pctStr}`);\n }\n\n // Suggest alternatives from registry\n const registry = loadRegistry();\n const service = registry.get(impact.serviceId);\n if (service?.alternatives && service.alternatives.length > 0 && impact.costHigh > 10) {\n const freeAlts = service.alternatives.filter(\n (a) => a.includes(\"free\") || a.includes(\"cheerio\") || a.includes(\"playwright\") || a.includes(\"self-hosted\"),\n );\n if (freeAlts.length > 0) {\n lines.push(` Consider: ${freeAlts.join(\", \")} for lower-cost alternative`);\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Utilization Engine — persistent, code-derived cost projection.\n *\n * Tracks SDK call sites across the project, persists them to disk,\n * and projects monthly utilization and overage costs. This is the\n * forward-looking early warning system that complements billing API data.\n *\n * Key design decisions:\n * - File is the unit of replacement: when a file changes, all its old call sites are replaced\n * - Reuses analyzeCostImpact() patterns from cost-impact.ts — no duplication\n * - Flat-rate-only services (no unitRate, no call patterns) are excluded from utilization\n * - For LIVE services, utilization runs silently unless divergence is significant\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { analyzeCostImpact } from \"./cost-impact.js\";\nimport { loadRegistry } from \"./core/registry.js\";\nimport { projectDataDir } from \"./core/config.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\n/** A single SDK call site detected in a source file. */\nexport interface CallSite {\n /** Absolute or project-relative file path */\n filePath: string;\n /** Service this call site belongs to */\n serviceId: string;\n /** Number of distinct call patterns matched in this file */\n callCount: number;\n /** Detected multiplier labels (e.g., \"for loop (100 iterations)\", \"cron: daily\") */\n multipliers: string[];\n /** The effective multiplier factor applied */\n multiplierFactor: number;\n /** Projected monthly invocations from this call site */\n monthlyInvocations: number;\n /** Low cost estimate per month */\n costLow: number;\n /** High cost estimate per month */\n costHigh: number;\n}\n\n/** Per-service utilization summary. */\nexport interface ServiceUtilization {\n serviceId: string;\n serviceName: string;\n /** All call sites across the project for this service */\n callSites: CallSite[];\n /** Total projected monthly units (sum of all callSites' monthlyInvocations) */\n totalMonthlyUnits: number;\n /** Unit name (e.g., \"sessions\", \"emails\", \"credits\", \"API calls\") */\n unitName: string;\n /** Units included in the user's plan (null = all units are overage) */\n planIncluded: number | null;\n /** Monthly units exceeding plan inclusion */\n projectedOverage: number;\n /** Cost of overage at unitRate */\n projectedOverageCost: number;\n /** Total projected monthly cost (plan base + overage) */\n projectedTotalCost: number;\n /** Plan base cost (flat monthly fee) */\n planBaseCost: number;\n /** Per-unit rate used for overage calculation */\n unitRate: number;\n}\n\n/** The full utilization model, persisted to disk. */\nexport interface UtilizationModel {\n /** Schema version for forward compatibility */\n version: 1;\n /** When this model was last updated */\n updatedAt: string;\n /** When the last full scan was performed */\n lastFullScan: string | null;\n /** Per-service utilization data */\n services: Record<string, ServiceUtilization>;\n}\n\n// ─── Model persistence ───────────────────────────────────────────────\n\n/** Path to the utilization model file. */\nexport function utilizationModelPath(projectRoot: string): string {\n return path.join(projectDataDir(projectRoot), \"utilization.json\");\n}\n\n/** Read the utilization model from disk. Returns empty model if not found. */\nexport function readUtilizationModel(projectRoot: string): UtilizationModel {\n try {\n const raw = fs.readFileSync(utilizationModelPath(projectRoot), \"utf-8\");\n return JSON.parse(raw) as UtilizationModel;\n } catch {\n return {\n version: 1,\n updatedAt: new Date().toISOString(),\n lastFullScan: null,\n services: {},\n };\n }\n}\n\n/** Write the utilization model to disk. */\nexport function writeUtilizationModel(\n model: UtilizationModel,\n projectRoot: string,\n): void {\n const dir = projectDataDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n model.updatedAt = new Date().toISOString();\n fs.writeFileSync(\n utilizationModelPath(projectRoot),\n JSON.stringify(model, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\n// ─── File analysis ───────────────────────────────────────────────────\n\n/**\n * Analyze a single file and return CallSite[] for each service detected.\n * Wraps analyzeCostImpact() and converts CostImpact[] → CallSite[].\n */\nexport function analyzeFileUtilization(\n filePath: string,\n content: string,\n projectRoot?: string,\n): CallSite[] {\n const impacts = analyzeCostImpact(filePath, content, projectRoot);\n\n return impacts.map((impact) => ({\n filePath,\n serviceId: impact.serviceId,\n callCount: impact.callCount,\n multipliers: impact.multipliers,\n multiplierFactor: impact.multiplierFactor,\n monthlyInvocations: impact.monthlyInvocations,\n costLow: impact.costLow,\n costHigh: impact.costHigh,\n }));\n}\n\n// ─── Model updates ───────────────────────────────────────────────────\n\n/**\n * Update the utilization model after a file changes.\n *\n * 1. Remove all existing call sites for this file path\n * 2. Insert new call sites\n * 3. Recalculate totals for affected services\n */\nexport function updateUtilizationModel(\n model: UtilizationModel,\n filePath: string,\n newCallSites: CallSite[],\n projectRoot?: string,\n): void {\n const registry = loadRegistry(projectRoot);\n\n // Track which services were affected (had old sites OR have new sites)\n const affectedServices = new Set<string>();\n\n // Step 1: Remove all old call sites for this file from all services\n for (const [serviceId, svc] of Object.entries(model.services)) {\n const before = svc.callSites.length;\n svc.callSites = svc.callSites.filter((cs) => cs.filePath !== filePath);\n if (svc.callSites.length !== before) {\n affectedServices.add(serviceId);\n }\n }\n\n // Step 2: Insert new call sites\n for (const cs of newCallSites) {\n affectedServices.add(cs.serviceId);\n\n if (!model.services[cs.serviceId]) {\n const def = registry.get(cs.serviceId);\n model.services[cs.serviceId] = {\n serviceId: cs.serviceId,\n serviceName: def?.name ?? cs.serviceId,\n callSites: [],\n totalMonthlyUnits: 0,\n unitName: def?.pricing?.unitName ?? \"API calls\",\n planIncluded: null,\n projectedOverage: 0,\n projectedOverageCost: 0,\n projectedTotalCost: 0,\n planBaseCost: def?.pricing?.monthlyBase ?? 0,\n unitRate: def?.pricing?.unitRate ?? 0,\n };\n }\n\n model.services[cs.serviceId]!.callSites.push(cs);\n }\n\n // Step 3: Recalculate totals for affected services\n for (const serviceId of affectedServices) {\n recalculateServiceTotals(model, serviceId);\n }\n\n // Clean up services with no remaining call sites\n for (const serviceId of Object.keys(model.services)) {\n if (model.services[serviceId]!.callSites.length === 0) {\n delete model.services[serviceId];\n }\n }\n}\n\n/**\n * Recalculate projection totals for a single service.\n */\nfunction recalculateServiceTotals(\n model: UtilizationModel,\n serviceId: string,\n): void {\n const svc = model.services[serviceId];\n if (!svc) return;\n\n // Sum monthly invocations across all call sites\n svc.totalMonthlyUnits = svc.callSites.reduce(\n (sum, cs) => sum + cs.monthlyInvocations,\n 0,\n );\n\n // Calculate overage\n const included = svc.planIncluded ?? 0;\n svc.projectedOverage = Math.max(0, svc.totalMonthlyUnits - included);\n\n // Calculate overage cost\n svc.projectedOverageCost = svc.projectedOverage * svc.unitRate;\n\n // Total = plan base + overage\n svc.projectedTotalCost = svc.planBaseCost + svc.projectedOverageCost;\n}\n\n/**\n * Apply user configuration (plan allowance, unit rate) to the model.\n * Called after reading project config to sync plan data into utilization.\n */\nexport function applyConfigToModel(\n model: UtilizationModel,\n serviceConfigs: Record<string, {\n planIncluded?: number | null;\n planBaseCost?: number;\n unitRate?: number;\n unitName?: string;\n }>,\n): void {\n for (const [serviceId, config] of Object.entries(serviceConfigs)) {\n const svc = model.services[serviceId];\n if (!svc) continue;\n\n if (config.planIncluded !== undefined) svc.planIncluded = config.planIncluded;\n if (config.planBaseCost !== undefined) svc.planBaseCost = config.planBaseCost;\n if (config.unitRate !== undefined) svc.unitRate = config.unitRate;\n if (config.unitName !== undefined) svc.unitName = config.unitName;\n\n recalculateServiceTotals(model, serviceId);\n }\n}\n\n// ─── Full project scan ───────────────────────────────────────────────\n\n/** Directories to scan for source files. */\nconst CODE_DIRS = [\"src\", \"app\", \"lib\", \"pages\", \"components\", \"utils\", \"services\", \"hooks\", \"api\", \"functions\"];\n\n/**\n * Build a complete utilization model by scanning all source files.\n * Called by `burnwatch init` and `burnwatch scan`.\n */\nexport function buildUtilizationModel(projectRoot: string): UtilizationModel {\n const model: UtilizationModel = {\n version: 1,\n updatedAt: new Date().toISOString(),\n lastFullScan: new Date().toISOString(),\n services: {},\n };\n\n const files = findSourceFiles(projectRoot);\n\n for (const file of files) {\n try {\n const content = fs.readFileSync(file, \"utf-8\");\n const callSites = analyzeFileUtilization(file, content, projectRoot);\n\n if (callSites.length > 0) {\n // Use relative paths in the model for portability\n const relPath = path.relative(projectRoot, file);\n const relativeSites = callSites.map((cs) => ({\n ...cs,\n filePath: relPath,\n }));\n updateUtilizationModel(model, relPath, relativeSites, projectRoot);\n }\n } catch {\n // Skip unreadable files\n }\n }\n\n return model;\n}\n\n/**\n * Find all source files in the project (same dirs as import scanner).\n */\nfunction findSourceFiles(projectRoot: string): string[] {\n const files: string[] = [];\n const dirsToScan: string[] = [];\n\n for (const dir of CODE_DIRS) {\n const fullPath = path.join(projectRoot, dir);\n if (fs.existsSync(fullPath)) {\n dirsToScan.push(fullPath);\n }\n }\n\n // Monorepo support: check subdirectories with their own package.json\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 const subPkgPath = path.join(projectRoot, entry.name, \"package.json\");\n if (fs.existsSync(subPkgPath)) {\n for (const dir of CODE_DIRS) {\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 walkDir(dir, /\\.(ts|tsx|js|jsx|mjs|cjs)$/, files);\n }\n\n return files;\n}\n\n/** Recursively walk a directory, collecting files matching the pattern. */\nfunction walkDir(dir: string, pattern: RegExp, results: string[], maxDepth = 5): void {\n if (maxDepth <= 0) return;\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 walkDir(fullPath, pattern, results, maxDepth - 1);\n } else if (pattern.test(entry.name)) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n}\n\n// ─── Divergence alerting ─────────────────────────────────────────────\n\nexport interface DivergenceAlert {\n serviceId: string;\n liveSpend: number;\n projectedTotalCost: number;\n delta: number;\n message: string;\n}\n\n/**\n * Check for divergence between LIVE billing data and code-projected costs.\n *\n * Only alerts when:\n * - Projected cost exceeds current LIVE spend by > 50% (relative threshold)\n * - AND the absolute difference is > $5 (absolute threshold)\n *\n * Both thresholds must be met to avoid noise.\n */\nexport function checkDivergence(\n serviceId: string,\n liveSpend: number,\n projectedTotalCost: number,\n): DivergenceAlert | null {\n const delta = projectedTotalCost - liveSpend;\n\n // Both thresholds must be met\n const relativeThreshold = liveSpend > 0 ? delta > liveSpend * 0.5 : delta > 0;\n const absoluteThreshold = delta > 5;\n\n if (relativeThreshold && absoluteThreshold) {\n return {\n serviceId,\n liveSpend,\n projectedTotalCost,\n delta,\n message: `Code changes project +$${delta.toFixed(2)}/mo above current $${liveSpend.toFixed(2)}/mo spend`,\n };\n }\n\n return null;\n}\n\n// ─── Brief formatting ────────────────────────────────────────────────\n\n/**\n * Format utilization data for the brief table.\n * Returns a map of serviceId → utilization string for the brief column.\n */\nexport function formatUtilizationForBrief(\n model: UtilizationModel,\n): Map<string, string> {\n const result = new Map<string, string>();\n\n for (const [serviceId, svc] of Object.entries(model.services)) {\n if (svc.totalMonthlyUnits === 0) continue;\n\n const unitsStr = formatCompact(svc.totalMonthlyUnits);\n\n if (svc.planIncluded !== null && svc.planIncluded > 0) {\n const includedStr = formatCompact(svc.planIncluded);\n const pct = ((svc.totalMonthlyUnits / svc.planIncluded) * 100).toFixed(0);\n\n if (svc.projectedOverage > 0) {\n const overageStr = formatCompact(svc.projectedOverage);\n result.set(\n serviceId,\n `${unitsStr}/${includedStr} ${svc.unitName} (${overageStr} overage)`,\n );\n } else {\n result.set(\n serviceId,\n `${unitsStr}/${includedStr} ${svc.unitName} (${pct}%)`,\n );\n }\n } else {\n result.set(serviceId, `${unitsStr} ${svc.unitName}/mo`);\n }\n }\n\n return result;\n}\n\n/**\n * Format a full utilization scan summary for CLI output.\n */\nexport function formatUtilizationSummary(model: UtilizationModel): string {\n const services = Object.values(model.services).filter(\n (s) => s.totalMonthlyUnits > 0,\n );\n\n if (services.length === 0) {\n return \"📊 No utilization-tracked call sites found in the project.\";\n }\n\n const lines: string[] = [];\n lines.push(\"📊 Utilization scan complete\\n\");\n lines.push(\"| Service | Call Sites | Monthly Units | Plan Included | Overage Cost |\");\n lines.push(\"|---------|-----------|---------------|---------------|-------------|\");\n\n for (const svc of services) {\n const fileCount = new Set(svc.callSites.map((cs) => cs.filePath)).size;\n const filesStr = `${fileCount} file${fileCount > 1 ? \"s\" : \"\"}`;\n const unitsStr = `${formatCompact(svc.totalMonthlyUnits)} ${svc.unitName}`;\n const includedStr = svc.planIncluded !== null\n ? formatCompact(svc.planIncluded)\n : \"—\";\n const overageStr = svc.projectedOverageCost > 0\n ? `~$${svc.projectedOverageCost.toFixed(2)}/mo`\n : \"$0\";\n\n lines.push(\n `| ${svc.serviceName} | ${filesStr} | ${unitsStr} | ${includedStr} | ${overageStr} |`,\n );\n }\n\n return lines.join(\"\\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(Math.round(n));\n}\n"],"mappings":"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","names":["fs","path","fs","path","fs","path","fs","path","fs","path"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/on-file-change.ts","../../src/core/config.ts","../../src/detection/detector.ts","../../src/core/registry.ts","../../src/core/ledger.ts","../../src/cost-impact.ts","../../src/utilization.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * PostToolUse hook (Edit|Write) — fires when files are changed.\n *\n * 1. Scans changed files for new service introductions\n * 2. Analyzes cost impact of SDK calls (invocation sites, multipliers, projected cost)\n * 3. Injects cost impact cards into Claude's context\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { HookInput, HookOutput } from \"../core/types.js\";\nimport {\n readProjectConfig,\n writeProjectConfig,\n isInitialized,\n projectDataDir,\n} from \"../core/config.js\";\nimport { detectInFileChange } from \"../detection/detector.js\";\nimport { logEvent } from \"../core/ledger.js\";\nimport { readLatestSnapshot } from \"../core/ledger.js\";\nimport type { TrackedService } from \"../core/types.js\";\nimport { analyzeCostImpact, formatCostImpactCard } from \"../cost-impact.js\";\nimport {\n readUtilizationModel,\n writeUtilizationModel,\n analyzeFileUtilization,\n updateUtilizationModel,\n} from \"../utilization.js\";\n\n/** Session cost impact accumulator file path */\nfunction sessionImpactPath(projectRoot: string, sessionId: string): string {\n return path.join(projectDataDir(projectRoot), \"cache\", `session-impact-${sessionId}.json`);\n}\n\n/** Read accumulated session cost impacts */\nfunction readSessionImpacts(\n projectRoot: string,\n sessionId: string,\n): Record<string, { costLow: number; costHigh: number }> {\n try {\n const raw = fs.readFileSync(sessionImpactPath(projectRoot, sessionId), \"utf-8\");\n return JSON.parse(raw) as Record<string, { costLow: number; costHigh: number }>;\n } catch {\n return {};\n }\n}\n\n/** Write accumulated session cost impacts */\nfunction writeSessionImpacts(\n projectRoot: string,\n sessionId: string,\n impacts: Record<string, { costLow: number; costHigh: number }>,\n): void {\n const dir = path.join(projectDataDir(projectRoot), \"cache\");\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(\n sessionImpactPath(projectRoot, sessionId),\n JSON.stringify(impacts, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\nfunction main(): void {\n // Read hook input from stdin\n let input: HookInput;\n try {\n const stdin = fs.readFileSync(0, \"utf-8\");\n input = JSON.parse(stdin) as HookInput;\n } catch {\n process.exit(0);\n return;\n }\n\n const projectRoot = input.cwd;\n\n // Guard: not initialized\n if (!isInitialized(projectRoot)) {\n process.exit(0);\n return;\n }\n\n // Get file path and content from tool input\n const filePath = input.tool_input?.file_path;\n if (!filePath) {\n process.exit(0);\n return;\n }\n\n // Read the current file content\n let content: string;\n try {\n content = fs.readFileSync(filePath, \"utf-8\");\n } catch {\n process.exit(0);\n return;\n }\n\n const config = readProjectConfig(projectRoot)!;\n const contextParts: string[] = [];\n\n // --- Part 1: Detect new services in this file change ---\n const detected = detectInFileChange(filePath, content, projectRoot);\n const newServices: string[] = [];\n\n for (const det of detected) {\n const serviceId = det.service.id;\n\n // Skip if already tracked\n if (config.services[serviceId]) continue;\n\n // Auto-register as a new tracked service (BLIND until configured)\n const tracked: TrackedService = {\n serviceId,\n detectedVia: det.sources,\n hasApiKey: false,\n firstDetected: new Date().toISOString(),\n };\n\n config.services[serviceId] = tracked;\n newServices.push(serviceId);\n\n // Log the detection\n logEvent(\n {\n timestamp: new Date().toISOString(),\n sessionId: input.session_id,\n type: \"service_detected\",\n data: {\n serviceId,\n sources: det.sources,\n details: det.details,\n file: filePath,\n },\n },\n projectRoot,\n );\n }\n\n // Save updated config\n if (newServices.length > 0) {\n writeProjectConfig(config, projectRoot);\n }\n\n // Alert about new services\n if (newServices.length > 0) {\n const alerts = newServices.map(\n (id) =>\n `[BURNWATCH] 🆕 New paid service detected: ${id}\\n Run 'burnwatch add ${id}' to configure budget and tracking.`,\n );\n contextParts.push(alerts.join(\"\\n\\n\"));\n }\n\n // --- Part 2: Cost impact analysis ---\n const impacts = analyzeCostImpact(filePath, content, projectRoot);\n\n if (impacts.length > 0) {\n // Build current budget status from latest snapshot\n const snapshot = readLatestSnapshot(projectRoot);\n const currentBudgets: Record<string, { spend: number; budget?: number }> = {};\n\n if (snapshot) {\n for (const svc of snapshot.services) {\n currentBudgets[svc.serviceId] = {\n spend: svc.spend,\n budget: svc.budget,\n };\n }\n }\n\n // Format and add cost impact card\n const card = formatCostImpactCard(impacts, currentBudgets);\n contextParts.push(card);\n\n // Accumulate session cost impacts\n const sessionImpacts = readSessionImpacts(projectRoot, input.session_id);\n for (const impact of impacts) {\n const existing = sessionImpacts[impact.serviceId];\n if (existing) {\n existing.costLow += impact.costLow;\n existing.costHigh += impact.costHigh;\n } else {\n sessionImpacts[impact.serviceId] = {\n costLow: impact.costLow,\n costHigh: impact.costHigh,\n };\n }\n }\n writeSessionImpacts(projectRoot, input.session_id, sessionImpacts);\n\n // Log cost impact event\n logEvent(\n {\n timestamp: new Date().toISOString(),\n sessionId: input.session_id,\n type: \"cost_impact\",\n data: {\n file: filePath,\n impacts: impacts.map((i) => ({\n serviceId: i.serviceId,\n callCount: i.callCount,\n monthlyInvocations: i.monthlyInvocations,\n costLow: i.costLow,\n costHigh: i.costHigh,\n })),\n },\n },\n projectRoot,\n );\n }\n\n // --- Part 3: Update utilization model incrementally ---\n try {\n const relPath = path.relative(projectRoot, filePath);\n const callSites = analyzeFileUtilization(filePath, content, projectRoot).map(\n (cs) => ({ ...cs, filePath: relPath }),\n );\n const model = readUtilizationModel(projectRoot);\n updateUtilizationModel(model, relPath, callSites, projectRoot);\n writeUtilizationModel(model, projectRoot);\n\n // Add overage warnings to the context\n for (const svc of Object.values(model.services)) {\n if (svc.projectedOverage > 0 && svc.projectedOverageCost > 5) {\n contextParts.push(\n `[BURNWATCH] ⚠️ ${svc.serviceName} utilization: ~${formatCompact(svc.totalMonthlyUnits)} ${svc.unitName}/mo` +\n (svc.planIncluded ? ` (plan includes ${formatCompact(svc.planIncluded)})` : \"\") +\n ` → ~$${svc.projectedOverageCost.toFixed(2)}/mo overage`,\n );\n }\n }\n } catch {\n // Utilization update is best-effort — don't block the hook\n }\n\n // --- Output ---\n if (contextParts.length > 0) {\n const output: HookOutput = {\n hookSpecificOutput: {\n hookEventName: \"PostToolUse\",\n additionalContext: contextParts.join(\"\\n\\n\"),\n },\n };\n\n process.stdout.write(JSON.stringify(output));\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(Math.round(n));\n}\n\nmain();\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 = parseEnvKeys(content);\n\n for (const [, service] of registry) {\n const matched = service.envPatterns.filter((p) => envKeys.has(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 for (const key of parseEnvKeys(content)) {\n envVars.add(key);\n }\n } catch {\n // Skip unreadable files\n }\n }\n\n return envVars;\n}\n\n/**\n * Parse an env file's content into a set of variable names.\n * Handles `export` prefix, quoted values, inline comments, whitespace.\n */\nexport function parseEnvKeys(content: string): Set<string> {\n const keys = new Set<string>();\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n // Strip optional \"export \" prefix\n const stripped = trimmed.startsWith(\"export \")\n ? trimmed.slice(7).trim()\n : trimmed;\n const eqIdx = stripped.indexOf(\"=\");\n if (eqIdx > 0) {\n keys.add(stripped.slice(0, eqIdx).trim());\n }\n }\n return keys;\n}\n\n/**\n * Find all .env* files recursively (but not in node_modules, .git, dist, etc.)\n */\nexport function 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;\nlet cachedRegistryPath: string | null = null;\nlet cachedRegistryMtime: number | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n * Invalidates cache when the registry file has been modified (for long-running processes like MCP server).\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n // Check cache validity — invalidate if the file has been modified\n if (cachedRegistry && cachedRegistryPath && cachedRegistryMtime !== null) {\n try {\n const stat = fs.statSync(cachedRegistryPath);\n if (stat.mtimeMs !== cachedRegistryMtime) {\n cachedRegistry = null;\n }\n } catch {\n // File gone — invalidate\n cachedRegistry = null;\n }\n }\n\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 let resolvedPath: string | null = null;\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 resolvedPath = candidate;\n break;\n }\n }\n\n // Load project-local override (if exists) — takes precedence for mtime tracking\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n resolvedPath = localPath;\n }\n }\n\n cachedRegistry = registry;\n\n // Track mtime for cache invalidation (important for long-running MCP server)\n if (resolvedPath) {\n try {\n cachedRegistryPath = resolvedPath;\n cachedRegistryMtime = fs.statSync(resolvedPath).mtimeMs;\n } catch {\n cachedRegistryPath = null;\n cachedRegistryMtime = null;\n }\n }\n\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 cachedRegistryPath = null;\n cachedRegistryMtime = 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 * 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 * Predictive cost impact analysis.\n *\n * Scans file content for SDK call sites, detects multipliers (loops, .map(), etc.),\n * and projects monthly cost using billing manifest data when available,\n * falling back to registry pricing data.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type {\n CostImpact,\n ServiceDefinition,\n} from \"./core/types.js\";\nimport { loadRegistry } from \"./core/registry.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\n// ── Billing manifest types ──────────────────────────────────────────\n\ninterface DimensionVariant {\n id: string;\n name: string;\n ratePerUnit: number;\n ratePer?: number;\n codePatterns?: string[];\n isDefault?: boolean;\n}\n\ninterface BillingDimension {\n id: string;\n name: string;\n unit: string;\n ratePerUnit: number;\n ratePer?: number;\n variants?: DimensionVariant[];\n}\n\ninterface CostMultiplier {\n id: string;\n name: string;\n factor: number;\n description: string;\n codePatterns?: string[];\n}\n\ninterface BillingManifest {\n serviceId: string;\n name: string;\n billingDimensions: BillingDimension[];\n plans?: Array<{\n id: string;\n name: string;\n monthlyBase: number;\n included?: Record<string, number>;\n overageRates?: Record<string, number>;\n hardCap?: boolean;\n isDefault?: boolean;\n }>;\n costMultipliers?: CostMultiplier[];\n typicalDevUsage?: {\n callsPerDevHour: number;\n unitsPerCall: number;\n unitName: string;\n };\n notes?: string;\n}\n\n// ── Manifest loader (cached) ────────────────────────────────────────\n\nlet manifestCache: Map<string, BillingManifest> | null = null;\n\nfunction loadBillingManifests(): Map<string, BillingManifest> {\n if (manifestCache) return manifestCache;\n\n manifestCache = new Map();\n\n // Try multiple possible locations for the billing/ directory\n const candidates = [\n path.resolve(__dirname, \"../billing\"), // from src/ during dev\n path.resolve(__dirname, \"../../billing\"), // from dist/\n ];\n\n for (const dir of candidates) {\n if (!fs.existsSync(dir)) continue;\n const files = fs.readdirSync(dir).filter((f) => f.endsWith(\".json\") && f !== \"billing.schema.json\");\n for (const file of files) {\n try {\n const raw = fs.readFileSync(path.join(dir, file), \"utf-8\");\n const manifest = JSON.parse(raw) as BillingManifest;\n if (manifest.serviceId) {\n manifestCache.set(manifest.serviceId, manifest);\n }\n } catch {\n // Skip malformed manifests\n }\n }\n break; // Use the first directory that exists\n }\n\n return manifestCache;\n}\n\n/** Clear manifest cache (for testing). */\nexport function clearManifestCache(): void {\n manifestCache = null;\n}\n\n// ── SDK call patterns per service ───────────────────────────────────\n\nconst SERVICE_CALL_PATTERNS: Record<string, RegExp[]> = {\n anthropic: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /anthropic\\.\\w+\\.create\\s*\\(/g,\n ],\n openai: [\n /\\.chat\\.completions\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /\\.images\\.generate\\s*\\(/g,\n /\\.embeddings\\.create\\s*\\(/g,\n /openai\\.\\w+\\.create\\s*\\(/g,\n ],\n \"google-gemini\": [\n /\\.generateContent\\s*\\(/g,\n /\\.generateContentStream\\s*\\(/g,\n /model\\.generate\\w*\\s*\\(/g,\n ],\n \"voyage-ai\": [\n /\\.embed\\s*\\(/g,\n /voyageai\\.embed\\s*\\(/g,\n ],\n scrapfly: [\n /\\.scrape\\s*\\(/g,\n /scrapfly\\.scrape\\s*\\(/g,\n /\\.async_scrape\\s*\\(/g,\n /ScrapeConfig\\s*\\(/g,\n ],\n browserbase: [\n /\\.createSession\\s*\\(/g,\n /\\.sessions\\.create\\s*\\(/g,\n /stagehand\\.act\\s*\\(/g,\n /stagehand\\.extract\\s*\\(/g,\n ],\n upstash: [\n /redis\\.\\w+\\s*\\(/g,\n /\\.set\\s*\\(/g,\n /\\.get\\s*\\(/g,\n /\\.incr\\s*\\(/g,\n /\\.hset\\s*\\(/g,\n ],\n resend: [\n /resend\\.emails\\.send\\s*\\(/g,\n /\\.emails\\.send\\s*\\(/g,\n ],\n supabase: [\n /supabase\\.from\\s*\\(/g,\n /\\.rpc\\s*\\(/g,\n /supabase\\.storage/g,\n ],\n inngest: [\n /inngest\\.send\\s*\\(/g,\n /\\.createFunction\\s*\\(/g,\n ],\n posthog: [\n /posthog\\.capture\\s*\\(/g,\n /\\.capture\\s*\\(/g,\n ],\n firebase: [\n /firestore\\.\\w+\\(\\s*[\"']/g,\n /\\.collection\\s*\\(/g,\n /\\.doc\\s*\\(/g,\n /admin\\.firestore\\(\\)/g,\n ],\n twilio: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.calls\\.create\\s*\\(/g,\n /twilio\\.messages/g,\n ],\n sendgrid: [\n /sgMail\\.send\\s*\\(/g,\n /\\.send\\s*\\(\\s*msg/g,\n ],\n \"mongodb-atlas\": [\n /\\.find\\s*\\(/g,\n /\\.insertOne\\s*\\(/g,\n /\\.insertMany\\s*\\(/g,\n /\\.updateOne\\s*\\(/g,\n /\\.aggregate\\s*\\(/g,\n ],\n clerk: [\n /clerkClient\\.\\w+/g,\n /auth\\(\\)/g,\n ],\n replicate: [\n /replicate\\.run\\s*\\(/g,\n /replicate\\.predictions\\.create\\s*\\(/g,\n ],\n vercel: [\n /\\.functions\\./g,\n /edge\\s+function/gi,\n ],\n};\n\n// ── Multiplier detection ────────────────────────────────────────────\n\ninterface MultiplierMatch {\n label: string;\n factor: number;\n}\n\n/**\n * Extract numeric constants and variable assignments from code.\n * Builds a map of variable name → numeric value for resolving loop bounds.\n *\n * Handles:\n * const COUNT = 1000;\n * let total = 500;\n * const urls = Array(200)\n * const items = [...]; // count commas\n * const pages = new Array(50);\n */\nfunction extractNumericContext(content: string): Map<string, number> {\n const ctx = new Map<string, number>();\n\n // const/let/var NAME = NUMBER\n const assignRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*(\\d+)\\s*[;,\\n]/g;\n let m: RegExpExecArray | null;\n while ((m = assignRegex.exec(content)) !== null) {\n ctx.set(m[1]!, parseInt(m[2]!, 10));\n }\n\n // Array(N) or new Array(N)\n const arrayCtorRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:new\\s+)?Array\\s*\\(\\s*(\\d+)\\s*\\)/g;\n while ((m = arrayCtorRegex.exec(content)) !== null) {\n ctx.set(m[1]!, parseInt(m[2]!, 10));\n }\n\n // Array literals — count elements: const x = [a, b, c]\n const arrayLitRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\[([^\\]]*)\\]/g;\n while ((m = arrayLitRegex.exec(content)) !== null) {\n const elements = m[2]!.split(\",\").filter((e) => e.trim().length > 0);\n if (elements.length > 1) {\n ctx.set(m[1]!, elements.length);\n }\n }\n\n return ctx;\n}\n\n/**\n * Try to resolve a variable name to a numeric value.\n * Checks: direct numeric literal, variable from context, .length property.\n */\nfunction resolveLoopBound(\n varName: string,\n content: string,\n ctx: Map<string, number>,\n): number | null {\n // Direct numeric literal\n const num = parseInt(varName, 10);\n if (!isNaN(num) && num > 0) return num;\n\n // Variable from extracted context\n if (ctx.has(varName)) return ctx.get(varName)!;\n\n // foo.length — try to resolve foo\n const lengthMatch = varName.match(/^(\\w+)\\.length$/);\n if (lengthMatch && ctx.has(lengthMatch[1]!)) {\n return ctx.get(lengthMatch[1]!)!;\n }\n\n return null;\n}\n\n/**\n * Detect iteration size from the iterable in for...of or .map()/.forEach().\n * Looks for the array variable and resolves its size from context.\n */\nfunction resolveIterableSize(\n content: string,\n ctx: Map<string, number>,\n): number | null {\n // for (const x of ARRAY) — extract ARRAY name\n const forOfMatch = content.match(/for\\s*\\(\\s*(?:const|let|var)\\s+\\w+\\s+of\\s+(\\w+)/);\n if (forOfMatch) {\n const resolved = ctx.get(forOfMatch[1]!);\n if (resolved) return resolved;\n }\n\n // ARRAY.map(...) or ARRAY.forEach(...)\n const iterMatch = content.match(/(\\w+)\\s*\\.\\s*(?:map|forEach)\\s*\\(/);\n if (iterMatch) {\n const resolved = ctx.get(iterMatch[1]!);\n if (resolved) return resolved;\n }\n\n return null;\n}\n\nfunction detectMultipliers(content: string): MultiplierMatch[] {\n const multipliers: MultiplierMatch[] = [];\n const ctx = extractNumericContext(content);\n\n // ── for (i = 0; i < BOUND; i++) loops ──\n const forLoopRegex = /for\\s*\\(.*;\\s*\\w+\\s*(?:<|<=)\\s*([\\w.]+)/g;\n const forLoopMatch = forLoopRegex.exec(content);\n if (forLoopMatch) {\n const boundExpr = forLoopMatch[1]!;\n const resolved = resolveLoopBound(boundExpr, content, ctx);\n if (resolved && resolved > 1) {\n multipliers.push({ label: `for loop (${resolved} iterations)`, factor: resolved });\n } else {\n // Couldn't resolve — check if the bound var name hints at size\n const hintMatch = content.match(new RegExp(`(?:const|let|var)\\\\s+${boundExpr}\\\\s*=\\\\s*(\\\\w+)\\\\.length`));\n if (hintMatch) {\n const arrayName = hintMatch[1]!;\n const arraySize = ctx.get(arrayName);\n if (arraySize && arraySize > 1) {\n multipliers.push({ label: `for loop (${arraySize} iterations via ${arrayName}.length)`, factor: arraySize });\n } else {\n multipliers.push({ label: `for loop (${arrayName}.length — variable bound)`, factor: 10 });\n }\n } else {\n multipliers.push({ label: \"for loop (variable bound)\", factor: 10 });\n }\n }\n }\n\n // ── .map() / .forEach() ──\n if (/\\.\\s*map\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n const size = resolveIterableSize(content, ctx);\n if (size && size > 1) {\n multipliers.push({ label: `.map() over ${size} items`, factor: size });\n } else {\n multipliers.push({ label: \".map() iteration\", factor: 10 });\n }\n } else if (/\\.\\s*forEach\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n const size = resolveIterableSize(content, ctx);\n if (size && size > 1) {\n multipliers.push({ label: `.forEach() over ${size} items`, factor: size });\n } else {\n multipliers.push({ label: \".forEach() iteration\", factor: 10 });\n }\n }\n\n // ── for...of / for...in ──\n if (/for\\s*\\(\\s*(const|let|var)\\s+\\w+\\s+(of|in)\\s+/g.test(content)) {\n const size = resolveIterableSize(content, ctx);\n if (size && size > 1) {\n multipliers.push({ label: `for...of over ${size} items`, factor: size });\n } else {\n multipliers.push({ label: \"for...of/in loop\", factor: 10 });\n }\n }\n\n // ── Promise.all ──\n if (/Promise\\.all\\s*\\(/g.test(content)) {\n // Promise.all usually wraps a .map() — don't double-count if .map() was already detected\n const hasMap = multipliers.some((m) => m.label.includes(\".map()\"));\n if (!hasMap) {\n multipliers.push({ label: \"Promise.all (parallel batch)\", factor: 10 });\n }\n }\n\n // ── Cron patterns ──\n if (/cron|schedule|interval|setInterval|every\\s+(?:\\d+|other)\\s*(min|hour|day|sec|week)/gi.test(content)) {\n if (/every\\s+5\\s*min/gi.test(content) || /\\*\\/5\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 5 minutes\", factor: 8640 });\n } else if (/every\\s+15\\s*min/gi.test(content) || /\\*\\/15\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 15 minutes\", factor: 2880 });\n } else if (/every\\s+30\\s*min/gi.test(content) || /\\*\\/30\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 30 minutes\", factor: 1440 });\n } else if (/every\\s+1?\\s*hour/gi.test(content) || /0\\s+\\*\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: hourly\", factor: 720 });\n } else if (/every\\s+(\\d+)\\s*hours?/gi.test(content)) {\n const hoursMatch = content.match(/every\\s+(\\d+)\\s*hours?/i);\n const hours = parseInt(hoursMatch![1]!, 10);\n const factor = Math.round(720 / hours);\n multipliers.push({ label: `cron: every ${hours} hours`, factor });\n } else if (/every\\s+other\\s+day|every\\s+2\\s*days?/gi.test(content) || /\\*\\/2\\s+/g.test(content)) {\n multipliers.push({ label: \"cron: every other day\", factor: 15 });\n } else if (/every\\s+(\\d+)\\s*days?/gi.test(content)) {\n const daysMatch = content.match(/every\\s+(\\d+)\\s*days?/i);\n const days = parseInt(daysMatch![1]!, 10);\n const factor = Math.round(30 / days);\n multipliers.push({ label: `cron: every ${days} days`, factor });\n } else if (/every\\s+1?\\s*day/gi.test(content) || /0\\s+0\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: daily\", factor: 30 });\n } else if (/every\\s+1?\\s*week/gi.test(content) || /0\\s+0\\s+\\*\\s+\\*\\s+0/g.test(content)) {\n multipliers.push({ label: \"cron: weekly\", factor: 4 });\n } else {\n multipliers.push({ label: \"scheduled execution\", factor: 30 });\n }\n }\n\n // ── Batch size hints ──\n const batchMatch = content.match(/batch[_\\s]?size\\s*[=:]\\s*(\\d+)/i);\n if (batchMatch) {\n const batchSize = parseInt(batchMatch[1]!);\n if (batchSize > 1) {\n multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });\n }\n }\n\n // ── Explicit count/total/limit constants ──\n // Catch patterns like: const TOTAL_PAGES = 1000, const NUM_REQUESTS = 500\n for (const [name, value] of ctx) {\n if (value >= 100 && /^(total|count|num|max|limit|size|pages|items|urls|batch)/i.test(name)) {\n // Only add if we didn't already detect this from a loop/map\n const alreadyCovered = multipliers.some((m) => m.factor === value);\n if (!alreadyCovered) {\n multipliers.push({ label: `${name} = ${value}`, factor: value });\n }\n }\n }\n\n return multipliers;\n}\n\n// ── Manifest-based cost computation ─────────────────────────────────\n\n/**\n * Detect which variant of a billing dimension is being used in the code.\n * Returns the matching variant, or the default variant, or undefined.\n */\nfunction detectVariant(\n dimension: BillingDimension,\n content: string,\n): DimensionVariant | undefined {\n if (!dimension.variants || dimension.variants.length === 0) return undefined;\n\n // Try to match code patterns for each variant\n for (const variant of dimension.variants) {\n if (!variant.codePatterns) continue;\n for (const pattern of variant.codePatterns) {\n try {\n if (new RegExp(pattern, \"i\").test(content)) {\n return variant;\n }\n } catch {\n // Skip invalid regex\n }\n }\n }\n\n // Fall back to default variant\n return dimension.variants.find((v) => v.isDefault);\n}\n\n/**\n * Detect active cost multipliers from the manifest based on code patterns.\n * Returns { low, high, explanation } similar to the old GOTCHA_MULTIPLIERS.\n */\nfunction detectManifestMultipliers(\n manifest: BillingManifest,\n content: string,\n): { low: number; high: number; explanation: string } | undefined {\n if (!manifest.costMultipliers || manifest.costMultipliers.length === 0) {\n return undefined;\n }\n\n let maxFactor = 1;\n const activeNames: string[] = [];\n\n for (const cm of manifest.costMultipliers) {\n if (!cm.codePatterns) continue;\n for (const pattern of cm.codePatterns) {\n try {\n if (new RegExp(pattern, \"i\").test(content)) {\n maxFactor = Math.max(maxFactor, cm.factor);\n activeNames.push(cm.name);\n break; // One match per multiplier is enough\n }\n } catch {\n // Skip invalid regex\n }\n }\n }\n\n if (activeNames.length === 0) return undefined;\n\n return {\n low: 1,\n high: maxFactor,\n explanation: activeNames.join(\", \"),\n };\n}\n\n/**\n * Compute the per-call cost using billing manifest data.\n * Uses variant detection to pick the right rate from the manifest.\n */\nfunction computeManifestPerCallCost(\n manifest: BillingManifest,\n content: string,\n): { perCall: number; explanation: string } {\n // Sum across all billing dimensions (e.g., input_tokens + output_tokens)\n let totalPerCall = 0;\n const parts: string[] = [];\n\n const unitsPerCall = manifest.typicalDevUsage?.unitsPerCall ?? 1;\n\n for (const dim of manifest.billingDimensions) {\n const variant = detectVariant(dim, content);\n const ratePerUnit = variant?.ratePerUnit ?? dim.ratePerUnit;\n const ratePer = variant?.ratePer ?? dim.ratePer ?? 1;\n\n // cost = (units / ratePer) * ratePerUnit\n const dimCostPerCall = (unitsPerCall / ratePer) * ratePerUnit;\n\n if (dimCostPerCall > 0) {\n totalPerCall += dimCostPerCall;\n const variantLabel = variant?.name ?? dim.name;\n parts.push(variantLabel);\n }\n }\n\n return {\n perCall: totalPerCall,\n explanation: parts.length > 0 ? parts.join(\" + \") : manifest.name,\n };\n}\n\n// ── Legacy fallback (services without manifests) ────────────────────\n\nconst LEGACY_GOTCHA_MULTIPLIERS: Record<string, { low: number; high: number; explanation: string }> = {};\n\nconst LEGACY_CALL_COSTS: Record<string, number> = {\n firebase: 0.0001,\n twilio: 0.01,\n sendgrid: 0.001,\n \"mongodb-atlas\": 0.0001,\n clerk: 0,\n replicate: 0.01,\n};\n\n// ── Main analysis function ──────────────────────────────────────────\n\n/**\n * Analyze a file's content for cost-impacting SDK calls.\n * Uses billing manifests for precise per-model/per-variant pricing.\n * Falls back to registry pricing for services without manifests.\n */\nexport function analyzeCostImpact(\n filePath: string,\n content: string,\n projectRoot?: string,\n): CostImpact[] {\n // Only analyze source files\n if (!/\\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(filePath)) {\n return [];\n }\n\n const registry = loadRegistry(projectRoot);\n const manifests = loadBillingManifests();\n const impacts: CostImpact[] = [];\n const multipliers = detectMultipliers(content);\n\n for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {\n let totalCalls = 0;\n\n for (const pattern of patterns) {\n pattern.lastIndex = 0;\n const matches = content.match(pattern);\n if (matches) {\n totalCalls += matches.length;\n }\n }\n\n if (totalCalls === 0) continue;\n\n const service = registry.get(serviceId);\n if (!service) continue;\n\n // Calculate effective multiplier from code structure (loops, map, cron, etc.)\n const multiplierFactor = multipliers.length > 0\n ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1)\n : 1;\n\n const manifest = manifests.get(serviceId);\n\n // Smarter monthly run estimation:\n // - Cron: multiplier already encodes frequency, just 1 base run\n // - Manifest typicalDevUsage: use callsPerDevHour × 8h × 22 days\n // - Fallback: 50 runs/month (conservative)\n let baseMonthlyRuns: number;\n const isCron = multipliers.some((m) => m.label.startsWith(\"cron\"));\n\n if (isCron) {\n baseMonthlyRuns = 1; // cron multiplier factor already encodes frequency\n } else if (manifest?.typicalDevUsage?.callsPerDevHour && manifest.typicalDevUsage.callsPerDevHour > 0) {\n // ~22 working days, ~6 active dev hours/day = 132 dev hours/month\n // But we're counting call sites, not dev hours, so normalize:\n // If the dev makes N calls/hour and we see K call sites, estimate K invocations per dev run\n baseMonthlyRuns = 132; // dev-hours per month\n } else {\n baseMonthlyRuns = 50; // fallback: ~2 runs per working day\n }\n\n const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;\n\n let costLow: number;\n let costHigh: number;\n let rangeExplanation: string | undefined;\n\n if (manifest) {\n // ── Manifest-based pricing ──\n const { perCall } = computeManifestPerCallCost(manifest, content);\n const gotcha = detectManifestMultipliers(manifest, content);\n\n costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);\n rangeExplanation = gotcha?.explanation;\n\n // If no cost multipliers detected but manifest has them, show the range\n if (!gotcha && manifest.costMultipliers && manifest.costMultipliers.length > 0) {\n const maxFactor = Math.max(...manifest.costMultipliers.map((m) => m.factor));\n if (maxFactor > 1) {\n costHigh = monthlyInvocations * perCall * maxFactor;\n rangeExplanation = manifest.costMultipliers.map((m) => m.description).join(\"; \");\n }\n }\n } else {\n // ── Legacy fallback (no manifest) ──\n const gotcha = LEGACY_GOTCHA_MULTIPLIERS[serviceId];\n const unitRate = service.pricing?.unitRate ?? 0;\n\n if (unitRate > 0) {\n costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);\n } else if (service.pricing?.monthlyBase !== undefined && service.pricing.monthlyBase > 0) {\n // Flat-rate services — cost is the plan, not per-invocation\n costLow = 0;\n costHigh = 0;\n } else {\n const perCall = LEGACY_CALL_COSTS[serviceId] ?? 0.001;\n costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);\n }\n\n rangeExplanation = gotcha?.explanation;\n }\n\n // Skip if no meaningful cost\n if (costLow === 0 && costHigh === 0) continue;\n\n impacts.push({\n serviceId,\n serviceName: service.name,\n filePath,\n callCount: totalCalls,\n multipliers: multipliers.map((m) => m.label),\n multiplierFactor,\n monthlyInvocations,\n costLow,\n costHigh,\n rangeExplanation,\n });\n }\n\n return impacts;\n}\n\n/**\n * Format a cost impact card for injection into Claude's context.\n */\nexport function formatCostImpactCard(\n impacts: CostImpact[],\n currentBudgets: Record<string, { spend: number; budget?: number }>,\n): string {\n const fileName = impacts[0]?.filePath.split(\"/\").pop() ?? \"unknown\";\n const lines: string[] = [];\n\n lines.push(`[BURNWATCH] ⚠️ Cost impact estimate for ${fileName}`);\n\n for (const impact of impacts) {\n const lowStr = impact.costLow < 1\n ? `$${impact.costLow.toFixed(2)}`\n : `$${impact.costLow.toFixed(0)}`;\n const highStr = impact.costHigh < 1\n ? `$${impact.costHigh.toFixed(2)}`\n : `$${impact.costHigh.toFixed(0)}`;\n\n const rangeStr = impact.costLow === impact.costHigh\n ? lowStr\n : `${lowStr}-${highStr}`;\n\n lines.push(\n ` ${impact.serviceName}: ~${impact.monthlyInvocations.toLocaleString()} calls/mo → ${rangeStr}/mo` +\n (impact.rangeExplanation ? ` (${impact.rangeExplanation})` : \"\"),\n );\n\n // Show current budget status if available\n const current = currentBudgets[impact.serviceId];\n if (current) {\n const budgetStr = current.budget\n ? `$${current.spend.toFixed(0)}/$${current.budget} budget`\n : `$${current.spend.toFixed(0)} (no budget set)`;\n const pctStr = current.budget && current.budget > 0\n ? ` (${((current.spend / current.budget) * 100).toFixed(0)}%)`\n : \"\";\n lines.push(` Current: ${budgetStr}${pctStr}`);\n }\n\n // Suggest alternatives from registry\n const registry = loadRegistry();\n const service = registry.get(impact.serviceId);\n if (service?.alternatives && service.alternatives.length > 0 && impact.costHigh > 10) {\n const freeAlts = service.alternatives.filter(\n (a) => a.includes(\"free\") || a.includes(\"cheerio\") || a.includes(\"playwright\") || a.includes(\"self-hosted\"),\n );\n if (freeAlts.length > 0) {\n lines.push(` Consider: ${freeAlts.join(\", \")} for lower-cost alternative`);\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Utilization Engine — persistent, code-derived cost projection.\n *\n * Tracks SDK call sites across the project, persists them to disk,\n * and projects monthly utilization and overage costs. This is the\n * forward-looking early warning system that complements billing API data.\n *\n * Key design decisions:\n * - File is the unit of replacement: when a file changes, all its old call sites are replaced\n * - Reuses analyzeCostImpact() patterns from cost-impact.ts — no duplication\n * - Flat-rate-only services (no unitRate, no call patterns) are excluded from utilization\n * - For LIVE services, utilization runs silently unless divergence is significant\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { analyzeCostImpact } from \"./cost-impact.js\";\nimport { loadRegistry } from \"./core/registry.js\";\nimport { projectDataDir } from \"./core/config.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\n/** A single SDK call site detected in a source file. */\nexport interface CallSite {\n /** Absolute or project-relative file path */\n filePath: string;\n /** Service this call site belongs to */\n serviceId: string;\n /** Number of distinct call patterns matched in this file */\n callCount: number;\n /** Detected multiplier labels (e.g., \"for loop (100 iterations)\", \"cron: daily\") */\n multipliers: string[];\n /** The effective multiplier factor applied */\n multiplierFactor: number;\n /** Projected monthly invocations from this call site */\n monthlyInvocations: number;\n /** Low cost estimate per month */\n costLow: number;\n /** High cost estimate per month */\n costHigh: number;\n}\n\n/** Per-service utilization summary. */\nexport interface ServiceUtilization {\n serviceId: string;\n serviceName: string;\n /** All call sites across the project for this service */\n callSites: CallSite[];\n /** Total projected monthly units (sum of all callSites' monthlyInvocations) */\n totalMonthlyUnits: number;\n /** Unit name (e.g., \"sessions\", \"emails\", \"credits\", \"API calls\") */\n unitName: string;\n /** Units included in the user's plan (null = all units are overage) */\n planIncluded: number | null;\n /** Monthly units exceeding plan inclusion */\n projectedOverage: number;\n /** Cost of overage at unitRate */\n projectedOverageCost: number;\n /** Total projected monthly cost (plan base + overage) */\n projectedTotalCost: number;\n /** Plan base cost (flat monthly fee) */\n planBaseCost: number;\n /** Per-unit rate used for overage calculation */\n unitRate: number;\n}\n\n/** The full utilization model, persisted to disk. */\nexport interface UtilizationModel {\n /** Schema version for forward compatibility */\n version: 1;\n /** When this model was last updated */\n updatedAt: string;\n /** When the last full scan was performed */\n lastFullScan: string | null;\n /** Per-service utilization data */\n services: Record<string, ServiceUtilization>;\n}\n\n// ─── Model persistence ───────────────────────────────────────────────\n\n/** Path to the utilization model file. */\nexport function utilizationModelPath(projectRoot: string): string {\n return path.join(projectDataDir(projectRoot), \"utilization.json\");\n}\n\n/** Read the utilization model from disk. Returns empty model if not found. */\nexport function readUtilizationModel(projectRoot: string): UtilizationModel {\n try {\n const raw = fs.readFileSync(utilizationModelPath(projectRoot), \"utf-8\");\n return JSON.parse(raw) as UtilizationModel;\n } catch {\n return {\n version: 1,\n updatedAt: new Date().toISOString(),\n lastFullScan: null,\n services: {},\n };\n }\n}\n\n/** Write the utilization model to disk. */\nexport function writeUtilizationModel(\n model: UtilizationModel,\n projectRoot: string,\n): void {\n const dir = projectDataDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n model.updatedAt = new Date().toISOString();\n fs.writeFileSync(\n utilizationModelPath(projectRoot),\n JSON.stringify(model, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\n// ─── File analysis ───────────────────────────────────────────────────\n\n/**\n * Analyze a single file and return CallSite[] for each service detected.\n * Wraps analyzeCostImpact() and converts CostImpact[] → CallSite[].\n */\nexport function analyzeFileUtilization(\n filePath: string,\n content: string,\n projectRoot?: string,\n): CallSite[] {\n const impacts = analyzeCostImpact(filePath, content, projectRoot);\n\n return impacts.map((impact) => ({\n filePath,\n serviceId: impact.serviceId,\n callCount: impact.callCount,\n multipliers: impact.multipliers,\n multiplierFactor: impact.multiplierFactor,\n monthlyInvocations: impact.monthlyInvocations,\n costLow: impact.costLow,\n costHigh: impact.costHigh,\n }));\n}\n\n// ─── Model updates ───────────────────────────────────────────────────\n\n/**\n * Update the utilization model after a file changes.\n *\n * 1. Remove all existing call sites for this file path\n * 2. Insert new call sites\n * 3. Recalculate totals for affected services\n */\nexport function updateUtilizationModel(\n model: UtilizationModel,\n filePath: string,\n newCallSites: CallSite[],\n projectRoot?: string,\n): void {\n const registry = loadRegistry(projectRoot);\n\n // Track which services were affected (had old sites OR have new sites)\n const affectedServices = new Set<string>();\n\n // Step 1: Remove all old call sites for this file from all services\n for (const [serviceId, svc] of Object.entries(model.services)) {\n const before = svc.callSites.length;\n svc.callSites = svc.callSites.filter((cs) => cs.filePath !== filePath);\n if (svc.callSites.length !== before) {\n affectedServices.add(serviceId);\n }\n }\n\n // Step 2: Insert new call sites\n for (const cs of newCallSites) {\n affectedServices.add(cs.serviceId);\n\n if (!model.services[cs.serviceId]) {\n const def = registry.get(cs.serviceId);\n model.services[cs.serviceId] = {\n serviceId: cs.serviceId,\n serviceName: def?.name ?? cs.serviceId,\n callSites: [],\n totalMonthlyUnits: 0,\n unitName: def?.pricing?.unitName ?? \"API calls\",\n planIncluded: null,\n projectedOverage: 0,\n projectedOverageCost: 0,\n projectedTotalCost: 0,\n planBaseCost: def?.pricing?.monthlyBase ?? 0,\n unitRate: def?.pricing?.unitRate ?? 0,\n };\n }\n\n model.services[cs.serviceId]!.callSites.push(cs);\n }\n\n // Step 3: Recalculate totals for affected services\n for (const serviceId of affectedServices) {\n recalculateServiceTotals(model, serviceId);\n }\n\n // Clean up services with no remaining call sites\n for (const serviceId of Object.keys(model.services)) {\n if (model.services[serviceId]!.callSites.length === 0) {\n delete model.services[serviceId];\n }\n }\n}\n\n/**\n * Recalculate projection totals for a single service.\n */\nfunction recalculateServiceTotals(\n model: UtilizationModel,\n serviceId: string,\n): void {\n const svc = model.services[serviceId];\n if (!svc) return;\n\n // Sum monthly invocations across all call sites\n svc.totalMonthlyUnits = svc.callSites.reduce(\n (sum, cs) => sum + cs.monthlyInvocations,\n 0,\n );\n\n // Calculate overage\n const included = svc.planIncluded ?? 0;\n svc.projectedOverage = Math.max(0, svc.totalMonthlyUnits - included);\n\n // Calculate overage cost\n svc.projectedOverageCost = svc.projectedOverage * svc.unitRate;\n\n // Total = plan base + overage\n svc.projectedTotalCost = svc.planBaseCost + svc.projectedOverageCost;\n}\n\n/**\n * Apply user configuration (plan allowance, unit rate) to the model.\n * Called after reading project config to sync plan data into utilization.\n */\nexport function applyConfigToModel(\n model: UtilizationModel,\n serviceConfigs: Record<string, {\n planIncluded?: number | null;\n planBaseCost?: number;\n unitRate?: number;\n unitName?: string;\n }>,\n): void {\n for (const [serviceId, config] of Object.entries(serviceConfigs)) {\n const svc = model.services[serviceId];\n if (!svc) continue;\n\n if (config.planIncluded !== undefined) svc.planIncluded = config.planIncluded;\n if (config.planBaseCost !== undefined) svc.planBaseCost = config.planBaseCost;\n if (config.unitRate !== undefined) svc.unitRate = config.unitRate;\n if (config.unitName !== undefined) svc.unitName = config.unitName;\n\n recalculateServiceTotals(model, serviceId);\n }\n}\n\n// ─── Full project scan ───────────────────────────────────────────────\n\n/** Directories to scan for source files. */\nconst CODE_DIRS = [\"src\", \"app\", \"lib\", \"pages\", \"components\", \"utils\", \"services\", \"hooks\", \"api\", \"functions\"];\n\n/**\n * Build a complete utilization model by scanning all source files.\n * Called by `burnwatch init` and `burnwatch scan`.\n */\nexport function buildUtilizationModel(projectRoot: string): UtilizationModel {\n const model: UtilizationModel = {\n version: 1,\n updatedAt: new Date().toISOString(),\n lastFullScan: new Date().toISOString(),\n services: {},\n };\n\n const files = findSourceFiles(projectRoot);\n\n for (const file of files) {\n try {\n const content = fs.readFileSync(file, \"utf-8\");\n const callSites = analyzeFileUtilization(file, content, projectRoot);\n\n if (callSites.length > 0) {\n // Use relative paths in the model for portability\n const relPath = path.relative(projectRoot, file);\n const relativeSites = callSites.map((cs) => ({\n ...cs,\n filePath: relPath,\n }));\n updateUtilizationModel(model, relPath, relativeSites, projectRoot);\n }\n } catch {\n // Skip unreadable files\n }\n }\n\n return model;\n}\n\n/**\n * Find all source files in the project (same dirs as import scanner).\n */\nfunction findSourceFiles(projectRoot: string): string[] {\n const files: string[] = [];\n const dirsToScan: string[] = [];\n\n for (const dir of CODE_DIRS) {\n const fullPath = path.join(projectRoot, dir);\n if (fs.existsSync(fullPath)) {\n dirsToScan.push(fullPath);\n }\n }\n\n // Monorepo support: check subdirectories with their own package.json\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 const subPkgPath = path.join(projectRoot, entry.name, \"package.json\");\n if (fs.existsSync(subPkgPath)) {\n for (const dir of CODE_DIRS) {\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 walkDir(dir, /\\.(ts|tsx|js|jsx|mjs|cjs)$/, files);\n }\n\n return files;\n}\n\n/** Recursively walk a directory, collecting files matching the pattern. */\nfunction walkDir(dir: string, pattern: RegExp, results: string[], maxDepth = 5): void {\n if (maxDepth <= 0) return;\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 walkDir(fullPath, pattern, results, maxDepth - 1);\n } else if (pattern.test(entry.name)) {\n results.push(fullPath);\n }\n }\n } catch {\n // Skip unreadable directories\n }\n}\n\n// ─── Divergence alerting ─────────────────────────────────────────────\n\nexport interface DivergenceAlert {\n serviceId: string;\n liveSpend: number;\n projectedTotalCost: number;\n delta: number;\n message: string;\n}\n\n/**\n * Check for divergence between LIVE billing data and code-projected costs.\n *\n * Only alerts when:\n * - Projected cost exceeds current LIVE spend by > 50% (relative threshold)\n * - AND the absolute difference is > $5 (absolute threshold)\n *\n * Both thresholds must be met to avoid noise.\n */\nexport function checkDivergence(\n serviceId: string,\n liveSpend: number,\n projectedTotalCost: number,\n): DivergenceAlert | null {\n const delta = projectedTotalCost - liveSpend;\n\n // Both thresholds must be met\n const relativeThreshold = liveSpend > 0 ? delta > liveSpend * 0.5 : delta > 0;\n const absoluteThreshold = delta > 5;\n\n if (relativeThreshold && absoluteThreshold) {\n return {\n serviceId,\n liveSpend,\n projectedTotalCost,\n delta,\n message: `Code changes project +$${delta.toFixed(2)}/mo above current $${liveSpend.toFixed(2)}/mo spend`,\n };\n }\n\n return null;\n}\n\n// ─── Brief formatting ────────────────────────────────────────────────\n\n/**\n * Format utilization data for the brief table.\n * Returns a map of serviceId → utilization string for the brief column.\n */\nexport function formatUtilizationForBrief(\n model: UtilizationModel,\n): Map<string, string> {\n const result = new Map<string, string>();\n\n for (const [serviceId, svc] of Object.entries(model.services)) {\n if (svc.totalMonthlyUnits === 0) continue;\n\n const unitsStr = formatCompact(svc.totalMonthlyUnits);\n\n if (svc.planIncluded !== null && svc.planIncluded > 0) {\n const includedStr = formatCompact(svc.planIncluded);\n const pct = ((svc.totalMonthlyUnits / svc.planIncluded) * 100).toFixed(0);\n\n if (svc.projectedOverage > 0) {\n const overageStr = formatCompact(svc.projectedOverage);\n result.set(\n serviceId,\n `${unitsStr}/${includedStr} ${svc.unitName} (${overageStr} overage)`,\n );\n } else {\n result.set(\n serviceId,\n `${unitsStr}/${includedStr} ${svc.unitName} (${pct}%)`,\n );\n }\n } else {\n result.set(serviceId, `${unitsStr} ${svc.unitName}/mo`);\n }\n }\n\n return result;\n}\n\n/**\n * Format a full utilization scan summary for CLI output.\n */\nexport function formatUtilizationSummary(model: UtilizationModel): string {\n const services = Object.values(model.services).filter(\n (s) => s.totalMonthlyUnits > 0,\n );\n\n if (services.length === 0) {\n return \"📊 No utilization-tracked call sites found in the project.\";\n }\n\n const lines: string[] = [];\n lines.push(\"📊 Utilization scan complete\\n\");\n lines.push(\"| Service | Call Sites | Monthly Units | Plan Included | Overage Cost |\");\n lines.push(\"|---------|-----------|---------------|---------------|-------------|\");\n\n for (const svc of services) {\n const fileCount = new Set(svc.callSites.map((cs) => cs.filePath)).size;\n const filesStr = `${fileCount} file${fileCount > 1 ? \"s\" : \"\"}`;\n const unitsStr = `${formatCompact(svc.totalMonthlyUnits)} ${svc.unitName}`;\n const includedStr = svc.planIncluded !== null\n ? formatCompact(svc.planIncluded)\n : \"—\";\n const overageStr = svc.projectedOverageCost > 0\n ? `~$${svc.projectedOverageCost.toFixed(2)}/mo`\n : \"$0\";\n\n lines.push(\n `| ${svc.serviceName} | ${filesStr} | ${unitsStr} | ${includedStr} | ${overageStr} |`,\n );\n }\n\n return lines.join(\"\\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(Math.round(n));\n}\n"],"mappings":"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","names":["fs","path","fs","path","fs","path","fs","path","fs","path","url","__dirname","fs","path"]}
|