burnwatch 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/on-stop.ts","../../src/core/config.ts","../../src/services/base.ts","../../src/services/anthropic.ts","../../src/services/openai.ts","../../src/services/vercel.ts","../../src/services/scrapfly.ts","../../src/core/registry.ts","../../src/services/index.ts","../../src/core/types.ts","../../src/core/brief.ts","../../src/core/ledger.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Stop hook — fires when Claude Code finishes a response.\n *\n * Updates the ledger and saves a fresh snapshot.\n * Includes cumulative session cost impact from file changes.\n * Runs async to avoid blocking the session end.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { HookInput } from \"../core/types.js\";\nimport { readProjectConfig, isInitialized, projectDataDir } from \"../core/config.js\";\nimport { pollAllServices } from \"../services/index.js\";\nimport { buildSnapshot, buildBrief } from \"../core/brief.js\";\nimport { writeLedger, saveSnapshot, logEvent } from \"../core/ledger.js\";\n\n/** Read accumulated session cost impacts */\nfunction readSessionImpacts(\n projectRoot: string,\n sessionId: string,\n): Record<string, { costLow: number; costHigh: number }> | null {\n try {\n const filePath = path.join(\n projectDataDir(projectRoot),\n \"cache\",\n `session-impact-${sessionId}.json`,\n );\n const raw = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(raw) as Record<string, { costLow: number; costHigh: number }>;\n } catch {\n return null;\n }\n}\n\nasync function main(): Promise<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 const config = readProjectConfig(projectRoot)!;\n const trackedServices = Object.values(config.services).filter(\n (s) => !s.excluded,\n );\n\n if (trackedServices.length === 0) {\n process.exit(0);\n return;\n }\n\n try {\n // Poll all services for current spend\n const results = await pollAllServices(trackedServices);\n\n // Build snapshots\n const snapshots = results.map((r) =>\n buildSnapshot(\n r.serviceId,\n r.tier,\n r.spend,\n config.services[r.serviceId]?.budget,\n ),\n );\n\n const blindCount = snapshots.filter((s) => s.tier === \"blind\").length;\n const brief = buildBrief(config.projectName, snapshots, blindCount);\n\n // Add session cost impact data if available\n const sessionImpacts = readSessionImpacts(projectRoot, input.session_id);\n if (sessionImpacts && Object.keys(sessionImpacts).length > 0) {\n let totalImpactLow = 0;\n let totalImpactHigh = 0;\n\n for (const impact of Object.values(sessionImpacts)) {\n totalImpactLow += impact.costLow;\n totalImpactHigh += impact.costHigh;\n }\n\n // Add session impact as an alert in the brief\n if (totalImpactHigh > 0) {\n const rangeStr =\n totalImpactLow === totalImpactHigh\n ? `$${totalImpactLow.toFixed(2)}`\n : `$${totalImpactLow.toFixed(2)}-$${totalImpactHigh.toFixed(2)}`;\n\n brief.alerts.push({\n serviceId: \"_session_impact\",\n type: \"near_budget\",\n message: `Session projected cost impact: ${rangeStr}/mo across ${Object.keys(sessionImpacts).length} service(s)`,\n severity: \"info\",\n });\n }\n }\n\n // Write ledger and save snapshot\n writeLedger(brief, projectRoot);\n saveSnapshot(brief, projectRoot);\n\n // Log session end\n logEvent(\n {\n timestamp: new Date().toISOString(),\n sessionId: input.session_id,\n type: \"session_end\",\n data: {\n totalSpend: brief.totalSpend,\n serviceCount: snapshots.length,\n blindCount,\n sessionImpacts: sessionImpacts ?? undefined,\n },\n },\n projectRoot,\n );\n } catch {\n // Don't block on errors — ledger update is best-effort\n }\n\n process.exit(0);\n}\n\nmain().catch(() => process.exit(0));\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Anthropic billing connector.\n * Uses the /v1/organizations/usage endpoint.\n * Requires an admin API key.\n */\nexport const anthropicConnector: BillingConnector = {\n serviceId: \"anthropic\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n // Get current month date range\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const startDate = startOfMonth.toISOString().split(\"T\")[0]!;\n const endDate = now.toISOString().split(\"T\")[0]!;\n\n const url = `https://api.anthropic.com/v1/organizations/usage?start_date=${startDate}&end_date=${endDate}`;\n\n const result = await fetchJson<{\n data?: Array<{ total_cost_usd?: number; spend?: number }>;\n total_cost_usd?: number;\n }>(url, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"anthropic\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Anthropic usage\",\n };\n }\n\n // Sum up usage across the period\n let totalSpend = 0;\n if (result.data.total_cost_usd !== undefined) {\n totalSpend = result.data.total_cost_usd;\n } else if (result.data.data) {\n totalSpend = result.data.data.reduce(\n (sum, entry) => sum + (entry.total_cost_usd ?? entry.spend ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"anthropic\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * OpenAI billing connector.\n * Uses the /v1/organization/costs endpoint.\n * Requires an organization-level API key.\n */\nexport const openaiConnector: BillingConnector = {\n serviceId: \"openai\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n // OpenAI uses Unix timestamps\n const startTime = Math.floor(startOfMonth.getTime() / 1000);\n\n const url = `https://api.openai.com/v1/organization/costs?start_time=${startTime}`;\n\n const result = await fetchJson<{\n data?: Array<{\n results?: Array<{\n amount?: { value?: number };\n }>;\n }>;\n object?: string;\n }>(url, {\n headers: {\n Authorization: `Bearer ${apiKey}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"openai\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch OpenAI usage\",\n };\n }\n\n // Sum all cost buckets\n let totalSpend = 0;\n if (result.data.data) {\n for (const bucket of result.data.data) {\n if (bucket.results) {\n for (const r of bucket.results) {\n totalSpend += r.amount?.value ?? 0;\n }\n }\n }\n }\n\n // OpenAI returns costs in cents, convert to dollars\n totalSpend = totalSpend / 100;\n\n return {\n serviceId: \"openai\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Vercel billing connector.\n * Uses the Vercel billing API.\n * Requires a Vercel token (personal or team-scoped).\n */\nexport const vercelConnector: BillingConnector = {\n serviceId: \"vercel\",\n\n async fetchSpend(\n token: string,\n options?: Record<string, string>,\n ): Promise<BillingResult> {\n const teamId = options?.[\"teamId\"] ?? \"\";\n const teamParam = teamId ? `?teamId=${teamId}` : \"\";\n\n // Fetch current billing period usage\n const url = `https://api.vercel.com/v2/usage${teamParam}`;\n\n const result = await fetchJson<{\n usage?: {\n total?: number;\n bandwidth?: { total?: number };\n serverlessFunctionExecution?: { total?: number };\n edgeFunctionExecution?: { total?: number };\n imageOptimization?: { total?: number };\n };\n billing?: {\n plan?: string;\n period?: { start?: string; end?: string };\n invoiceItems?: Array<{ amount?: number }>;\n };\n }>(url, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"vercel\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Vercel usage\",\n };\n }\n\n // Sum up usage costs\n let totalSpend = 0;\n if (result.data.usage?.total !== undefined) {\n totalSpend = result.data.usage.total;\n } else if (result.data.billing?.invoiceItems) {\n totalSpend = result.data.billing.invoiceItems.reduce(\n (sum, item) => sum + (item.amount ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"vercel\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Scrapfly billing connector.\n * Uses the /account endpoint which returns credits used/remaining.\n * Works with the standard API key — no special billing key needed.\n */\nexport const scrapflyConnector: BillingConnector = {\n serviceId: \"scrapfly\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const url = `https://api.scrapfly.io/account?key=${apiKey}`;\n\n const result = await fetchJson<{\n subscription?: {\n usage?: {\n scrape?: { used?: number; allowed?: number };\n };\n };\n account?: {\n credits_used?: number;\n credits_total?: number;\n };\n }>(url);\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"scrapfly\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Scrapfly account\",\n };\n }\n\n // Extract credits used from the response\n let creditsUsed = 0;\n let creditsTotal = 0;\n\n if (result.data.subscription?.usage?.scrape) {\n creditsUsed = result.data.subscription.usage.scrape.used ?? 0;\n creditsTotal = result.data.subscription.usage.scrape.allowed ?? 0;\n } else if (result.data.account) {\n creditsUsed = result.data.account.credits_used ?? 0;\n creditsTotal = result.data.account.credits_total ?? 0;\n }\n\n // Convert credits to USD at registry rate\n const creditRate = 0.00015; // $0.00015 per credit\n const spend = creditsUsed * creditRate;\n\n return {\n serviceId: \"scrapfly\",\n spend,\n isEstimate: false,\n tier: \"live\",\n raw: {\n credits_used: creditsUsed,\n credits_total: creditsTotal,\n credit_rate: creditRate,\n ...(result.data as unknown as Record<string, unknown>),\n },\n };\n },\n};\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type { ServiceDefinition } from \"./types.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\ninterface RegistryFile {\n version: string;\n lastUpdated: string;\n services: Record<string, ServiceDefinition>;\n}\n\nlet cachedRegistry: Map<string, ServiceDefinition> | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n if (cachedRegistry) return cachedRegistry;\n\n const registry = new Map<string, ServiceDefinition>();\n\n // Load bundled registry (shipped with package)\n // Try multiple possible locations — depends on whether running from src/ or dist/\n const candidates = [\n path.resolve(__dirname, \"../../registry.json\"), // from src/core/\n path.resolve(__dirname, \"../registry.json\"), // from dist/\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n loadRegistryFile(candidate, registry);\n break;\n }\n }\n\n // Load project-local override (if exists)\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n }\n }\n\n cachedRegistry = registry;\n return registry;\n}\n\nfunction loadRegistryFile(\n filePath: string,\n registry: Map<string, ServiceDefinition>,\n): void {\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(raw) as RegistryFile;\n for (const [id, service] of Object.entries(data.services)) {\n registry.set(id, { ...service, id });\n }\n } catch {\n // Silently skip missing or malformed registry files\n }\n}\n\n/** Clear the cached registry (for testing). */\nexport function clearRegistryCache(): void {\n cachedRegistry = null;\n}\n\n/** Get a single service definition by ID. */\nexport function getService(\n id: string,\n projectRoot?: string,\n): ServiceDefinition | undefined {\n return loadRegistry(projectRoot).get(id);\n}\n\n/** Get all service definitions. */\nexport function getAllServices(\n projectRoot?: string,\n): ServiceDefinition[] {\n return Array.from(loadRegistry(projectRoot).values());\n}\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { anthropicConnector } from \"./anthropic.js\";\nimport { openaiConnector } from \"./openai.js\";\nimport { vercelConnector } from \"./vercel.js\";\nimport { scrapflyConnector } from \"./scrapfly.js\";\nimport type { TrackedService, ConfidenceTier } from \"../core/types.js\";\nimport { readGlobalConfig } from \"../core/config.js\";\nimport { getService } from \"../core/registry.js\";\n\n/** All available billing connectors, keyed by service ID. */\nconst connectors: Map<string, BillingConnector> = new Map([\n [\"anthropic\", anthropicConnector],\n [\"openai\", openaiConnector],\n [\"vercel\", vercelConnector],\n [\"scrapfly\", scrapflyConnector],\n]);\n\n/**\n * Poll spend for a single tracked service.\n * Returns the best available data based on connector availability and API keys.\n */\nexport async function pollService(\n tracked: TrackedService,\n): Promise<BillingResult> {\n const globalConfig = readGlobalConfig();\n const serviceConfig = globalConfig.services[tracked.serviceId];\n const connector = connectors.get(tracked.serviceId);\n const definition = getService(tracked.serviceId);\n\n // If we have a connector and an API key, try LIVE\n if (connector && serviceConfig?.apiKey) {\n try {\n const result = await connector.fetchSpend(\n serviceConfig.apiKey,\n serviceConfig as unknown as Record<string, string>,\n );\n if (!result.error) return result;\n // Fall through to lower tiers on error\n } catch {\n // Fall through\n }\n }\n\n // If user provided a plan cost, use CALC\n if (tracked.planCost !== undefined) {\n const now = new Date();\n const daysInMonth = new Date(\n now.getFullYear(),\n now.getMonth() + 1,\n 0,\n ).getDate();\n const dayOfMonth = now.getDate();\n const projectedSpend = (tracked.planCost / daysInMonth) * dayOfMonth;\n\n return {\n serviceId: tracked.serviceId,\n spend: projectedSpend,\n isEstimate: true,\n tier: \"calc\",\n };\n }\n\n // If service is in registry but we have no key and no plan cost\n if (definition) {\n let tier: ConfidenceTier;\n if (tracked.tierOverride) {\n tier = tracked.tierOverride;\n } else if (definition.apiTier === \"live\") {\n // Has a LIVE API but we don't have the key — mark as BLIND\n tier = \"blind\";\n } else {\n // EST, CALC, or BLIND — use the registry's declared tier\n tier = definition.apiTier;\n }\n\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: tier !== \"live\",\n tier,\n error: tier === \"blind\" ? \"No API key configured\" : undefined,\n };\n }\n\n // Completely unknown service\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: true,\n tier: \"blind\",\n error: \"Unknown service — not in registry\",\n };\n}\n\n/**\n * Poll all tracked services concurrently.\n * Returns results in the same order as input.\n */\nexport async function pollAllServices(\n services: TrackedService[],\n): Promise<BillingResult[]> {\n return Promise.all(services.map(pollService));\n}\n\nexport { type BillingConnector, type BillingResult } from \"./base.js\";\n","/**\n * Confidence tiers for spend tracking.\n *\n * LIVE — Real billing API data\n * CALC — Fixed monthly cost, user-entered\n * EST — Estimated from usage signals + pricing formula\n * BLIND — Detected in project, no tracking configured\n */\nexport type ConfidenceTier = \"live\" | \"calc\" | \"est\" | \"blind\" | \"excluded\";\n\nexport const CONFIDENCE_BADGES: Record<ConfidenceTier, string> = {\n live: \"✅ LIVE\",\n calc: \"🟡 CALC\",\n est: \"🟠 EST\",\n blind: \"🔴 BLIND\",\n excluded: \"⬚ SKIP\",\n};\n\n/** How a service charges — determines tracking strategy. */\nexport type BillingModel =\n | \"token_usage\" // Per-token (Anthropic, OpenAI, Gemini)\n | \"credit_pool\" // Fixed credit bucket (Scrapfly)\n | \"per_unit\" // Per-email, per-session, per-command (Resend, Browserbase, Upstash)\n | \"percentage\" // Percentage of transaction (Stripe)\n | \"flat_monthly\" // Fixed monthly subscription (PostHog, Inngest free tier)\n | \"tiered\" // Free up to X, then jumps (PostHog, Supabase)\n | \"compute\" // Compute-time based (Vercel, AWS)\n | \"unknown\";\n\n/** How cost scales — helps the agent reason about future spend. */\nexport type ScalingShape =\n | \"linear\" // Each unit costs the same\n | \"linear_burndown\" // Fixed pool, each use depletes it\n | \"tiered_jump\" // Free until threshold, then expensive\n | \"percentage\" // Proportional to revenue/volume\n | \"fixed\" // Flat monthly, no scaling\n | \"unknown\";\n\n/** A plan tier option for a service in the registry. */\nexport interface PlanTier {\n /** Human-readable plan name */\n name: string;\n /** Plan type: usage (pay-as-you-go), flat (fixed monthly), exclude (don't track) */\n type: \"usage\" | \"flat\" | \"exclude\";\n /** Monthly base cost for flat plans */\n monthlyBase?: number;\n /** Whether this plan requires an API key for tracking */\n requiresKey?: boolean;\n /** Whether this is the default/most common plan */\n default?: boolean;\n}\n\n/** Risk category for service grouping in interactive init. */\nexport type ServiceRiskCategory = \"llm\" | \"usage\" | \"infra\" | \"flat\";\n\n/** A service definition from the registry. */\nexport interface ServiceDefinition {\n /** Unique service identifier */\n id: string;\n /** Human-readable name */\n name: string;\n /** Package names in npm/pip that indicate this service */\n packageNames: string[];\n /** Env var patterns that indicate this service */\n envPatterns: string[];\n /** Import patterns to scan for (regex strings) */\n importPatterns: string[];\n /** Keywords that indicate mentions in prompts */\n mentionKeywords: string[];\n /** Billing model */\n billingModel: BillingModel;\n /** How cost scales */\n scalingShape: ScalingShape;\n /** What tier of tracking is available */\n apiTier: ConfidenceTier;\n /** Billing API endpoint, if available */\n apiEndpoint?: string;\n /** Pricing details */\n pricing?: {\n /** Human-readable formula */\n formula?: string;\n /** Rate per unit, if applicable */\n unitRate?: number;\n /** Unit name (token, credit, email, session, etc.) */\n unitName?: string;\n /** Monthly base cost, if flat */\n monthlyBase?: number;\n };\n /** Known gotchas that affect cost */\n gotchas?: string[];\n /** Alternative services (free or cheaper) */\n alternatives?: string[];\n /** Documentation URL */\n docsUrl?: string;\n /** Last time pricing was verified */\n lastVerified?: string;\n /** Notes about recent pricing changes */\n pricingNotes?: string;\n /** Available plan tiers for interactive init */\n plans?: PlanTier[];\n /** Whether the plan can be auto-detected from an API key */\n autoDetectPlan?: boolean;\n}\n\n/** A tracked service instance — a service definition + user config. */\nexport interface TrackedService {\n /** Service definition ID */\n serviceId: string;\n /** How this service was detected */\n detectedVia: DetectionSource[];\n /** User-configured monthly budget */\n budget?: number;\n /** Whether the user has provided an API/billing key */\n hasApiKey: boolean;\n /** Override confidence tier (e.g., user provided billing key upgrades to LIVE) */\n tierOverride?: ConfidenceTier;\n /** User-entered monthly plan cost (for CALC tier) */\n planCost?: number;\n /** When this service was first detected */\n firstDetected: string;\n /** Explicitly excluded from tracking by user */\n excluded?: boolean;\n /** Plan name selected during interactive init */\n planName?: string;\n}\n\nexport type DetectionSource =\n | \"package_json\"\n | \"env_var\"\n | \"import_scan\"\n | \"prompt_mention\"\n | \"git_diff\"\n | \"manual\";\n\n/** A spend snapshot for a single service at a point in time. */\nexport interface SpendSnapshot {\n serviceId: string;\n /** Current period spend (or estimate) */\n spend: number;\n /** Is the spend figure exact or estimated? */\n isEstimate: boolean;\n /** Confidence tier for this reading */\n tier: ConfidenceTier;\n /** Budget allocated */\n budget?: number;\n /** Percentage of budget consumed */\n budgetPercent?: number;\n /** Budget status */\n status: \"healthy\" | \"caution\" | \"over\" | \"unknown\";\n /** Human-readable status label */\n statusLabel: string;\n /** Raw data from billing API, if available */\n raw?: Record<string, unknown>;\n /** Timestamp of this snapshot */\n timestamp: string;\n}\n\n/** The full spend brief, injected at session start. */\nexport interface SpendBrief {\n projectName: string;\n generatedAt: string;\n period: string;\n services: SpendSnapshot[];\n totalSpend: number;\n totalIsEstimate: boolean;\n estimateMargin: number;\n untrackedCount: number;\n alerts: SpendAlert[];\n}\n\nexport interface SpendAlert {\n serviceId: string;\n type: \"over_budget\" | \"near_budget\" | \"new_service\" | \"stale_data\" | \"blind_service\";\n message: string;\n severity: \"warning\" | \"critical\" | \"info\";\n}\n\n/** Ledger entry — one row in spend-ledger.md */\nexport interface LedgerEntry {\n serviceId: string;\n serviceName: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n budget?: number;\n statusLabel: string;\n}\n\n/** Event logged to events.jsonl */\nexport interface SpendEvent {\n timestamp: string;\n sessionId: string;\n type:\n | \"session_start\"\n | \"session_end\"\n | \"service_detected\"\n | \"service_mentioned\"\n | \"spend_polled\"\n | \"budget_alert\"\n | \"ledger_written\"\n | \"cost_impact\";\n data: Record<string, unknown>;\n}\n\n/** A cost impact estimate for a file change. */\nexport interface CostImpact {\n serviceId: string;\n serviceName: string;\n filePath: string;\n /** Number of SDK call sites found */\n callCount: number;\n /** Detected multipliers (loops, .map(), etc.) */\n multipliers: string[];\n /** Effective multiplier applied to call count */\n multiplierFactor: number;\n /** Estimated monthly invocations */\n monthlyInvocations: number;\n /** Low estimate monthly cost */\n costLow: number;\n /** High estimate monthly cost */\n costHigh: number;\n /** Gotcha-based cost range explanation */\n rangeExplanation?: string;\n}\n\n/**\n * Hook input — the JSON received via stdin from Claude Code.\n * Subset of fields we care about.\n */\nexport interface HookInput {\n session_id: string;\n transcript_path?: string;\n cwd: string;\n hook_event_name: string;\n // SessionStart\n source?: string;\n // UserPromptSubmit\n prompt?: string;\n // PostToolUse\n tool_name?: string;\n tool_input?: {\n file_path?: string;\n command?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n };\n}\n\n/**\n * Hook output — the JSON we write to stdout for Claude Code.\n */\nexport interface HookOutput {\n hookSpecificOutput?: {\n hookEventName: string;\n additionalContext?: string;\n };\n}\n","import type {\n SpendBrief,\n SpendSnapshot,\n SpendAlert,\n ConfidenceTier,\n} from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\n\n/**\n * Format a spend brief as a text block for injection into Claude's context.\n */\nexport function formatBrief(brief: SpendBrief): string {\n const lines: string[] = [];\n const width = 62;\n const hrDouble = \"═\".repeat(width);\n const hrSingle = \"─\".repeat(width - 4);\n\n lines.push(`╔${hrDouble}╗`);\n lines.push(\n `║ BURNWATCH — ${brief.projectName} — ${brief.period}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n lines.push(`╠${hrDouble}╣`);\n\n // Header\n lines.push(\n formatRow(\"Service\", \"Spend\", \"Conf\", \"Budget\", \"Left\", width),\n );\n lines.push(`║ ${hrSingle} ║`);\n\n // Service rows\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n const leftStr = formatLeft(svc);\n\n lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));\n }\n\n // Footer\n lines.push(`╠${hrDouble}╣`);\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr = brief.estimateMargin > 0\n ? ` Est margin: ±$${brief.estimateMargin.toFixed(0)}`\n : \"\";\n const untrackedStr =\n brief.untrackedCount > 0\n ? `Untracked: ${brief.untrackedCount} ⚠️`\n : `Untracked: 0 ✅`;\n\n lines.push(\n `║ TOTAL: ${totalStr} ${untrackedStr}${marginStr}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n\n // Alerts\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(\n `║ ${icon} ${alert.message}`.padEnd(width + 1) + \"║\",\n );\n }\n\n lines.push(`╚${hrDouble}╝`);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Format a single-service spend card for injection on mention.\n */\nexport function formatSpendCard(snapshot: SpendSnapshot): string {\n const badge = CONFIDENCE_BADGES[snapshot.tier];\n const spendStr = snapshot.isEstimate\n ? `~$${snapshot.spend.toFixed(2)}`\n : `$${snapshot.spend.toFixed(2)}`;\n const budgetStr = snapshot.budget\n ? `Budget: $${snapshot.budget}`\n : \"No budget set\";\n const statusStr = snapshot.statusLabel;\n\n const lines = [\n `[BURNWATCH] ${snapshot.serviceId} — current period`,\n ` Spend: ${spendStr} | ${budgetStr} | ${statusStr}`,\n ` Confidence: ${badge}`,\n ];\n\n if (snapshot.status === \"over\" && snapshot.budgetPercent) {\n lines.push(\n ` ⚠️ ${snapshot.budgetPercent.toFixed(0)}% of budget consumed`,\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Build a SpendBrief from snapshots and project config.\n */\nexport function buildBrief(\n projectName: string,\n snapshots: SpendSnapshot[],\n blindCount: number,\n): SpendBrief {\n const now = new Date();\n const period = now.toLocaleDateString(\"en-US\", {\n month: \"long\",\n year: \"numeric\",\n });\n\n let totalSpend = 0;\n let hasEstimates = false;\n let estimateMargin = 0;\n const alerts: SpendAlert[] = [];\n\n for (const snap of snapshots) {\n totalSpend += snap.spend;\n if (snap.isEstimate) {\n hasEstimates = true;\n estimateMargin += snap.spend * 0.15; // ±15% margin on estimates\n }\n\n if (snap.status === \"over\") {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"over_budget\",\n message: `${snap.serviceId.toUpperCase()} ${snap.budgetPercent?.toFixed(0) ?? \"?\"}% OVER BUDGET — review before use`,\n severity: \"critical\",\n });\n } else if (snap.status === \"caution\" && snap.budgetPercent && snap.budgetPercent >= 80) {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"near_budget\",\n message: `${snap.serviceId} at ${snap.budgetPercent.toFixed(0)}% of budget`,\n severity: \"warning\",\n });\n }\n }\n\n if (blindCount > 0) {\n alerts.push({\n serviceId: \"_blind\",\n type: \"blind_service\",\n message: `${blindCount} service${blindCount > 1 ? \"s\" : \"\"} detected but untracked — run 'burnwatch status' to see`,\n severity: \"warning\",\n });\n }\n\n return {\n projectName,\n generatedAt: now.toISOString(),\n period,\n services: snapshots,\n totalSpend,\n totalIsEstimate: hasEstimates,\n estimateMargin,\n untrackedCount: blindCount,\n alerts,\n };\n}\n\n// --- Helpers ---\n\nfunction formatRow(\n service: string,\n spend: string,\n conf: string,\n budget: string,\n left: string,\n width: number,\n): string {\n const row = ` ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(7)} ${budget.padEnd(7)} ${left}`;\n return `║${row}`.padEnd(width + 1) + \"║\";\n}\n\nfunction formatLeft(snap: SpendSnapshot): string {\n if (!snap.budget) return \"—\";\n if (snap.status === \"over\") return \"⚠️ OVR\";\n if (snap.budgetPercent !== undefined) {\n const remaining = 100 - snap.budgetPercent;\n return `${remaining.toFixed(0)}%`;\n }\n return \"—\";\n}\n\n/**\n * Build a SpendSnapshot from tracked service data.\n */\nexport function buildSnapshot(\n serviceId: string,\n tier: ConfidenceTier,\n spend: number,\n budget?: number,\n): SpendSnapshot {\n const isEstimate = tier === \"est\" || tier === \"calc\";\n const budgetPercent = budget ? (spend / budget) * 100 : undefined;\n\n let status: SpendSnapshot[\"status\"] = \"unknown\";\n let statusLabel = \"no budget\";\n\n if (budget) {\n if (budgetPercent! > 100) {\n status = \"over\";\n statusLabel = `⚠️ ${budgetPercent!.toFixed(0)}% over`;\n } else if (budgetPercent! >= 75) {\n status = \"caution\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — caution`;\n } else {\n status = \"healthy\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — healthy`;\n }\n }\n\n if (tier === \"calc\" && budget) {\n statusLabel = `flat — on plan`;\n status = \"healthy\";\n }\n\n return {\n serviceId,\n spend,\n isEstimate,\n tier,\n budget,\n budgetPercent,\n status,\n statusLabel,\n timestamp: new Date().toISOString(),\n };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { SpendBrief, SpendEvent } from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\nimport { projectConfigDir, projectDataDir } from \"./config.js\";\n\n/**\n * Write the spend ledger as a human-readable markdown file.\n * Designed to be git-committable and readable in 10 seconds.\n */\nexport function writeLedger(brief: SpendBrief, projectRoot?: string): void {\n const now = new Date();\n const lines: string[] = [];\n\n lines.push(`# Burnwatch Ledger — ${brief.projectName}`);\n lines.push(`Last updated: ${now.toISOString()}`);\n lines.push(\"\");\n lines.push(`## This Month (${brief.period})`);\n lines.push(\"\");\n lines.push(\"| Service | Spend | Conf | Budget | Status |\");\n lines.push(\"|---------|-------|------|--------|--------|\");\n\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n\n lines.push(\n `| ${svc.serviceId} | ${spendStr} | ${badge} | ${budgetStr} | ${svc.statusLabel} |`,\n );\n }\n\n // Add projected impact row if session impacts exist in alerts\n const impactAlert = brief.alerts.find(\n (a) => a.serviceId === \"_session_impact\",\n );\n if (impactAlert) {\n lines.push(\n `| _projected impact_ | — | 📈 EST | — | ${impactAlert.message} |`,\n );\n }\n\n lines.push(\"\");\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr =\n brief.estimateMargin > 0\n ? ` (±$${brief.estimateMargin.toFixed(0)} estimated margin)`\n : \"\";\n lines.push(`## TOTAL: ${totalStr}${marginStr}`);\n lines.push(`## Untracked services: ${brief.untrackedCount}`);\n lines.push(\"\");\n\n if (brief.alerts.length > 0) {\n lines.push(\"## Alerts\");\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(`- ${icon} ${alert.message}`);\n }\n lines.push(\"\");\n }\n\n const ledgerPath = path.join(\n projectConfigDir(projectRoot),\n \"spend-ledger.md\",\n );\n fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });\n fs.writeFileSync(ledgerPath, lines.join(\"\\n\") + \"\\n\", \"utf-8\");\n}\n\n/**\n * Append an event to the append-only event log.\n */\nexport function logEvent(event: SpendEvent, projectRoot?: string): void {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n fs.mkdirSync(path.dirname(logPath), { recursive: true });\n fs.appendFileSync(logPath, JSON.stringify(event) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Read recent events from the event log.\n */\nexport function readRecentEvents(\n count: number,\n projectRoot?: string,\n): SpendEvent[] {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n try {\n const raw = fs.readFileSync(logPath, \"utf-8\");\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n return lines\n .slice(-count)\n .map((line) => JSON.parse(line) as SpendEvent);\n } catch {\n return [];\n }\n}\n\n/**\n * Save a spend snapshot to the snapshots directory.\n * Used for delta computation across sessions.\n */\nexport function saveSnapshot(brief: SpendBrief, projectRoot?: string): void {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n fs.mkdirSync(snapshotDir, { recursive: true });\n const filename = `snapshot-${new Date().toISOString().replace(/[:.]/g, \"-\")}.json`;\n fs.writeFileSync(\n path.join(snapshotDir, filename),\n JSON.stringify(brief, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\n/**\n * Read the most recent snapshot, if any.\n */\nexport function readLatestSnapshot(\n projectRoot?: string,\n): SpendBrief | null {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n try {\n const files = fs\n .readdirSync(snapshotDir)\n .filter((f) => f.startsWith(\"snapshot-\") && f.endsWith(\".json\"))\n .sort()\n .reverse();\n\n if (files.length === 0) return null;\n\n const raw = fs.readFileSync(\n path.join(snapshotDir, files[0]!),\n \"utf-8\",\n );\n return JSON.parse(raw) as SpendBrief;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":["fs","path","url","url","url","url","url","fs","path","fs","path"]}
1
+ {"version":3,"sources":["../../src/hooks/on-stop.ts","../../src/core/config.ts","../../src/services/base.ts","../../src/services/anthropic.ts","../../src/services/openai.ts","../../src/services/vercel.ts","../../src/services/scrapfly.ts","../../src/core/registry.ts","../../src/services/index.ts","../../src/core/types.ts","../../src/core/brief.ts","../../src/core/ledger.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Stop hook — fires when Claude Code finishes a response.\n *\n * Updates the ledger and saves a fresh snapshot.\n * Includes cumulative session cost impact from file changes.\n * Runs async to avoid blocking the session end.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { HookInput } from \"../core/types.js\";\nimport { readProjectConfig, isInitialized, projectDataDir } from \"../core/config.js\";\nimport { pollAllServices } from \"../services/index.js\";\nimport { buildSnapshot, buildBrief } from \"../core/brief.js\";\nimport { writeLedger, saveSnapshot, logEvent } from \"../core/ledger.js\";\n\n/** Read accumulated session cost impacts */\nfunction readSessionImpacts(\n projectRoot: string,\n sessionId: string,\n): Record<string, { costLow: number; costHigh: number }> | null {\n try {\n const filePath = path.join(\n projectDataDir(projectRoot),\n \"cache\",\n `session-impact-${sessionId}.json`,\n );\n const raw = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(raw) as Record<string, { costLow: number; costHigh: number }>;\n } catch {\n return null;\n }\n}\n\nasync function main(): Promise<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 const config = readProjectConfig(projectRoot)!;\n const trackedServices = Object.values(config.services).filter(\n (s) => !s.excluded,\n );\n\n if (trackedServices.length === 0) {\n process.exit(0);\n return;\n }\n\n try {\n // Poll all services for current spend\n const results = await pollAllServices(trackedServices);\n\n // Build snapshots\n const snapshots = results.map((r) =>\n buildSnapshot(\n r.serviceId,\n r.tier,\n r.spend,\n config.services[r.serviceId]?.budget,\n ),\n );\n\n const blindCount = snapshots.filter((s) => s.tier === \"blind\").length;\n const brief = buildBrief(config.projectName, snapshots, blindCount);\n\n // Add session cost impact data if available\n const sessionImpacts = readSessionImpacts(projectRoot, input.session_id);\n if (sessionImpacts && Object.keys(sessionImpacts).length > 0) {\n let totalImpactLow = 0;\n let totalImpactHigh = 0;\n\n for (const impact of Object.values(sessionImpacts)) {\n totalImpactLow += impact.costLow;\n totalImpactHigh += impact.costHigh;\n }\n\n // Add session impact as an alert in the brief\n if (totalImpactHigh > 0) {\n const rangeStr =\n totalImpactLow === totalImpactHigh\n ? `$${totalImpactLow.toFixed(2)}`\n : `$${totalImpactLow.toFixed(2)}-$${totalImpactHigh.toFixed(2)}`;\n\n brief.alerts.push({\n serviceId: \"_session_impact\",\n type: \"near_budget\",\n message: `Session projected cost impact: ${rangeStr}/mo across ${Object.keys(sessionImpacts).length} service(s)`,\n severity: \"info\",\n });\n }\n }\n\n // Write ledger and save snapshot\n writeLedger(brief, projectRoot);\n saveSnapshot(brief, projectRoot);\n\n // Log session end\n logEvent(\n {\n timestamp: new Date().toISOString(),\n sessionId: input.session_id,\n type: \"session_end\",\n data: {\n totalSpend: brief.totalSpend,\n serviceCount: snapshots.length,\n blindCount,\n sessionImpacts: sessionImpacts ?? undefined,\n },\n },\n projectRoot,\n );\n } catch {\n // Don't block on errors — ledger update is best-effort\n }\n\n process.exit(0);\n}\n\nmain().catch(() => process.exit(0));\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport type { TrackedService } from \"./types.js\";\n\n/**\n * Paths for burnwatch configuration and data.\n *\n * Hybrid model:\n * - Global config (API keys, service credentials): ~/.config/burnwatch/\n * - Project config (budgets, tracked services): .burnwatch/\n * - Project data (ledger, events, cache): .burnwatch/data/\n */\n\n/** Global config directory — stores API keys, never in project dirs. */\nexport function globalConfigDir(): string {\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return path.join(xdgConfig, \"burnwatch\");\n return path.join(os.homedir(), \".config\", \"burnwatch\");\n}\n\n/** Project config directory — stores budgets, tracked services. */\nexport function projectConfigDir(projectRoot?: string): string {\n const root = projectRoot ?? process.cwd();\n return path.join(root, \".burnwatch\");\n}\n\n/** Project data directory — stores ledger, events, cache. */\nexport function projectDataDir(projectRoot?: string): string {\n return path.join(projectConfigDir(projectRoot), \"data\");\n}\n\n// --- Global config (API keys) ---\n\nexport interface GlobalConfig {\n services: Record<\n string,\n {\n apiKey?: string;\n token?: string;\n orgId?: string;\n }\n >;\n}\n\nexport function readGlobalConfig(): GlobalConfig {\n const configPath = path.join(globalConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as GlobalConfig;\n } catch {\n return { services: {} };\n }\n}\n\nexport function writeGlobalConfig(config: GlobalConfig): void {\n const dir = globalConfigDir();\n fs.mkdirSync(dir, { recursive: true });\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n // Restrict permissions — this file contains API keys\n fs.chmodSync(configPath, 0o600);\n}\n\n// --- Project config (budgets, tracked services) ---\n\nexport interface ProjectConfig {\n projectName: string;\n services: Record<string, TrackedService>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport function readProjectConfig(projectRoot?: string): ProjectConfig | null {\n const configPath = path.join(projectConfigDir(projectRoot), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n return JSON.parse(raw) as ProjectConfig;\n } catch {\n return null;\n }\n}\n\nexport function writeProjectConfig(\n config: ProjectConfig,\n projectRoot?: string,\n): void {\n const dir = projectConfigDir(projectRoot);\n fs.mkdirSync(dir, { recursive: true });\n config.updatedAt = new Date().toISOString();\n const configPath = path.join(dir, \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Ensure all project directories exist. */\nexport function ensureProjectDirs(projectRoot?: string): void {\n const dirs = [\n projectConfigDir(projectRoot),\n projectDataDir(projectRoot),\n path.join(projectDataDir(projectRoot), \"cache\"),\n path.join(projectDataDir(projectRoot), \"snapshots\"),\n ];\n for (const dir of dirs) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/** Check if burnwatch is initialized in the given project. */\nexport function isInitialized(projectRoot?: string): boolean {\n return readProjectConfig(projectRoot) !== null;\n}\n","import type { ConfidenceTier } from \"../core/types.js\";\n\n/** Result from polling a billing API. */\nexport interface BillingResult {\n serviceId: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n raw?: Record<string, unknown>;\n error?: string;\n}\n\n/**\n * Base interface for service billing connectors.\n * Each LIVE service implements this to fetch real spend data.\n */\nexport interface BillingConnector {\n serviceId: string;\n /** Fetch current period spend. */\n fetchSpend(apiKey: string, options?: Record<string, string>): Promise<BillingResult>;\n}\n\n/**\n * Make an HTTP request and return JSON.\n * Uses native fetch (Node 18+). No external dependencies.\n */\nexport async function fetchJson<T>(\n url: string,\n options: {\n headers?: Record<string, string>;\n method?: string;\n body?: string;\n timeout?: number;\n } = {},\n): Promise<{ ok: boolean; status: number; data?: T; error?: string }> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n options.timeout ?? 10_000,\n );\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n body: options.body,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n return {\n ok: false,\n status: response.status,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as T;\n return { ok: true, status: response.status, data };\n } catch (err) {\n return {\n ok: false,\n status: 0,\n error: err instanceof Error ? err.message : \"Unknown error\",\n };\n }\n}\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Anthropic billing connector.\n * Uses the /v1/organizations/usage endpoint.\n * Requires an admin API key.\n */\nexport const anthropicConnector: BillingConnector = {\n serviceId: \"anthropic\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n // Get current month date range\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n const startDate = startOfMonth.toISOString().split(\"T\")[0]!;\n const endDate = now.toISOString().split(\"T\")[0]!;\n\n const url = `https://api.anthropic.com/v1/organizations/usage?start_date=${startDate}&end_date=${endDate}`;\n\n const result = await fetchJson<{\n data?: Array<{ total_cost_usd?: number; spend?: number }>;\n total_cost_usd?: number;\n }>(url, {\n headers: {\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"anthropic\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Anthropic usage\",\n };\n }\n\n // Sum up usage across the period\n let totalSpend = 0;\n if (result.data.total_cost_usd !== undefined) {\n totalSpend = result.data.total_cost_usd;\n } else if (result.data.data) {\n totalSpend = result.data.data.reduce(\n (sum, entry) => sum + (entry.total_cost_usd ?? entry.spend ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"anthropic\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * OpenAI billing connector.\n * Uses the /v1/organization/costs endpoint.\n * Requires an organization-level API key.\n */\nexport const openaiConnector: BillingConnector = {\n serviceId: \"openai\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const now = new Date();\n const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);\n // OpenAI uses Unix timestamps\n const startTime = Math.floor(startOfMonth.getTime() / 1000);\n\n const url = `https://api.openai.com/v1/organization/costs?start_time=${startTime}`;\n\n const result = await fetchJson<{\n data?: Array<{\n results?: Array<{\n amount?: { value?: number };\n }>;\n }>;\n object?: string;\n }>(url, {\n headers: {\n Authorization: `Bearer ${apiKey}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"openai\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch OpenAI usage\",\n };\n }\n\n // Sum all cost buckets\n let totalSpend = 0;\n if (result.data.data) {\n for (const bucket of result.data.data) {\n if (bucket.results) {\n for (const r of bucket.results) {\n totalSpend += r.amount?.value ?? 0;\n }\n }\n }\n }\n\n // OpenAI returns costs in cents, convert to dollars\n totalSpend = totalSpend / 100;\n\n return {\n serviceId: \"openai\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Vercel billing connector.\n * Uses the Vercel billing API.\n * Requires a Vercel token (personal or team-scoped).\n */\nexport const vercelConnector: BillingConnector = {\n serviceId: \"vercel\",\n\n async fetchSpend(\n token: string,\n options?: Record<string, string>,\n ): Promise<BillingResult> {\n const teamId = options?.[\"teamId\"] ?? \"\";\n const teamParam = teamId ? `?teamId=${teamId}` : \"\";\n\n // Fetch current billing period usage\n const url = `https://api.vercel.com/v2/usage${teamParam}`;\n\n const result = await fetchJson<{\n usage?: {\n total?: number;\n bandwidth?: { total?: number };\n serverlessFunctionExecution?: { total?: number };\n edgeFunctionExecution?: { total?: number };\n imageOptimization?: { total?: number };\n };\n billing?: {\n plan?: string;\n period?: { start?: string; end?: string };\n invoiceItems?: Array<{ amount?: number }>;\n };\n }>(url, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"vercel\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Vercel usage\",\n };\n }\n\n // Sum up usage costs\n let totalSpend = 0;\n if (result.data.usage?.total !== undefined) {\n totalSpend = result.data.usage.total;\n } else if (result.data.billing?.invoiceItems) {\n totalSpend = result.data.billing.invoiceItems.reduce(\n (sum, item) => sum + (item.amount ?? 0),\n 0,\n );\n }\n\n return {\n serviceId: \"vercel\",\n spend: totalSpend,\n isEstimate: false,\n tier: \"live\",\n raw: result.data as unknown as Record<string, unknown>,\n };\n },\n};\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { fetchJson } from \"./base.js\";\n\n/**\n * Scrapfly billing connector.\n * Uses the /account endpoint which returns credits used/remaining.\n * Works with the standard API key — no special billing key needed.\n */\nexport const scrapflyConnector: BillingConnector = {\n serviceId: \"scrapfly\",\n\n async fetchSpend(apiKey: string): Promise<BillingResult> {\n const url = `https://api.scrapfly.io/account?key=${apiKey}`;\n\n const result = await fetchJson<{\n subscription?: {\n usage?: {\n scrape?: { used?: number; allowed?: number };\n };\n };\n account?: {\n credits_used?: number;\n credits_total?: number;\n };\n }>(url);\n\n if (!result.ok || !result.data) {\n return {\n serviceId: \"scrapfly\",\n spend: 0,\n isEstimate: true,\n tier: \"est\",\n error: result.error ?? \"Failed to fetch Scrapfly account\",\n };\n }\n\n // Extract credits used from the response\n let creditsUsed = 0;\n let creditsTotal = 0;\n\n if (result.data.subscription?.usage?.scrape) {\n creditsUsed = result.data.subscription.usage.scrape.used ?? 0;\n creditsTotal = result.data.subscription.usage.scrape.allowed ?? 0;\n } else if (result.data.account) {\n creditsUsed = result.data.account.credits_used ?? 0;\n creditsTotal = result.data.account.credits_total ?? 0;\n }\n\n // Convert credits to USD at registry rate\n const creditRate = 0.00015; // $0.00015 per credit\n const spend = creditsUsed * creditRate;\n\n return {\n serviceId: \"scrapfly\",\n spend,\n isEstimate: false,\n tier: \"live\",\n raw: {\n credits_used: creditsUsed,\n credits_total: creditsTotal,\n credit_rate: creditRate,\n ...(result.data as unknown as Record<string, unknown>),\n },\n };\n },\n};\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport type { ServiceDefinition } from \"./types.js\";\n\nconst __dirname = path.dirname(url.fileURLToPath(import.meta.url));\n\ninterface RegistryFile {\n version: string;\n lastUpdated: string;\n services: Record<string, ServiceDefinition>;\n}\n\nlet cachedRegistry: Map<string, ServiceDefinition> | null = null;\n\n/**\n * Load the service registry.\n * Checks project-local override first, then falls back to bundled registry.\n */\nexport function loadRegistry(projectRoot?: string): Map<string, ServiceDefinition> {\n if (cachedRegistry) return cachedRegistry;\n\n const registry = new Map<string, ServiceDefinition>();\n\n // Load bundled registry (shipped with package)\n // Try multiple possible locations — depends on whether running from src/ or dist/\n const candidates = [\n path.resolve(__dirname, \"../../registry.json\"), // from src/core/\n path.resolve(__dirname, \"../registry.json\"), // from dist/\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n loadRegistryFile(candidate, registry);\n break;\n }\n }\n\n // Load project-local override (if exists)\n if (projectRoot) {\n const localPath = path.join(projectRoot, \".burnwatch\", \"registry.json\");\n if (fs.existsSync(localPath)) {\n loadRegistryFile(localPath, registry);\n }\n }\n\n cachedRegistry = registry;\n return registry;\n}\n\nfunction loadRegistryFile(\n filePath: string,\n registry: Map<string, ServiceDefinition>,\n): void {\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n const data = JSON.parse(raw) as RegistryFile;\n for (const [id, service] of Object.entries(data.services)) {\n registry.set(id, { ...service, id });\n }\n } catch {\n // Silently skip missing or malformed registry files\n }\n}\n\n/** Clear the cached registry (for testing). */\nexport function clearRegistryCache(): void {\n cachedRegistry = null;\n}\n\n/** Get a single service definition by ID. */\nexport function getService(\n id: string,\n projectRoot?: string,\n): ServiceDefinition | undefined {\n return loadRegistry(projectRoot).get(id);\n}\n\n/** Get all service definitions. */\nexport function getAllServices(\n projectRoot?: string,\n): ServiceDefinition[] {\n return Array.from(loadRegistry(projectRoot).values());\n}\n","import type { BillingConnector, BillingResult } from \"./base.js\";\nimport { anthropicConnector } from \"./anthropic.js\";\nimport { openaiConnector } from \"./openai.js\";\nimport { vercelConnector } from \"./vercel.js\";\nimport { scrapflyConnector } from \"./scrapfly.js\";\nimport type { TrackedService, ConfidenceTier } from \"../core/types.js\";\nimport { readGlobalConfig } from \"../core/config.js\";\nimport { getService } from \"../core/registry.js\";\n\n/** All available billing connectors, keyed by service ID. */\nconst connectors: Map<string, BillingConnector> = new Map([\n [\"anthropic\", anthropicConnector],\n [\"openai\", openaiConnector],\n [\"vercel\", vercelConnector],\n [\"scrapfly\", scrapflyConnector],\n]);\n\n/**\n * Poll spend for a single tracked service.\n * Returns the best available data based on connector availability and API keys.\n */\nexport async function pollService(\n tracked: TrackedService,\n): Promise<BillingResult> {\n const globalConfig = readGlobalConfig();\n const serviceConfig = globalConfig.services[tracked.serviceId];\n const connector = connectors.get(tracked.serviceId);\n const definition = getService(tracked.serviceId);\n\n // If we have a connector and an API key, try LIVE\n if (connector && serviceConfig?.apiKey) {\n try {\n const result = await connector.fetchSpend(\n serviceConfig.apiKey,\n serviceConfig as unknown as Record<string, string>,\n );\n if (!result.error) return result;\n // Fall through to lower tiers on error\n } catch {\n // Fall through\n }\n }\n\n // If user provided a plan cost, use CALC\n if (tracked.planCost !== undefined) {\n const now = new Date();\n const daysInMonth = new Date(\n now.getFullYear(),\n now.getMonth() + 1,\n 0,\n ).getDate();\n const dayOfMonth = now.getDate();\n const projectedSpend = (tracked.planCost / daysInMonth) * dayOfMonth;\n\n return {\n serviceId: tracked.serviceId,\n spend: projectedSpend,\n isEstimate: true,\n tier: \"calc\",\n };\n }\n\n // If service is in registry but we have no key and no plan cost\n if (definition) {\n let tier: ConfidenceTier;\n if (tracked.tierOverride) {\n tier = tracked.tierOverride;\n } else if (definition.apiTier === \"live\") {\n // Has a LIVE API but we don't have the key — mark as BLIND\n tier = \"blind\";\n } else {\n // EST, CALC, or BLIND — use the registry's declared tier\n tier = definition.apiTier;\n }\n\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: tier !== \"live\",\n tier,\n error: tier === \"blind\" ? \"No API key configured\" : undefined,\n };\n }\n\n // Completely unknown service\n return {\n serviceId: tracked.serviceId,\n spend: 0,\n isEstimate: true,\n tier: \"blind\",\n error: \"Unknown service — not in registry\",\n };\n}\n\n/**\n * Poll all tracked services concurrently.\n * Returns results in the same order as input.\n */\nexport async function pollAllServices(\n services: TrackedService[],\n): Promise<BillingResult[]> {\n return Promise.all(services.map(pollService));\n}\n\nexport { type BillingConnector, type BillingResult } from \"./base.js\";\n","/**\n * Confidence tiers for spend tracking.\n *\n * LIVE — Real billing API data\n * CALC — Fixed monthly cost, user-entered\n * EST — Estimated from usage signals + pricing formula\n * BLIND — Detected in project, no tracking configured\n */\nexport type ConfidenceTier = \"live\" | \"calc\" | \"est\" | \"blind\" | \"excluded\";\n\nexport const CONFIDENCE_BADGES: Record<ConfidenceTier, string> = {\n live: \"✅ LIVE\",\n calc: \"🟡 CALC\",\n est: \"🟠 EST\",\n blind: \"🔴 BLIND\",\n excluded: \"⬚ SKIP\",\n};\n\n/** How a service charges — determines tracking strategy. */\nexport type BillingModel =\n | \"token_usage\" // Per-token (Anthropic, OpenAI, Gemini)\n | \"credit_pool\" // Fixed credit bucket (Scrapfly)\n | \"per_unit\" // Per-email, per-session, per-command (Resend, Browserbase, Upstash)\n | \"percentage\" // Percentage of transaction (Stripe)\n | \"flat_monthly\" // Fixed monthly subscription (PostHog, Inngest free tier)\n | \"tiered\" // Free up to X, then jumps (PostHog, Supabase)\n | \"compute\" // Compute-time based (Vercel, AWS)\n | \"unknown\";\n\n/** How cost scales — helps the agent reason about future spend. */\nexport type ScalingShape =\n | \"linear\" // Each unit costs the same\n | \"linear_burndown\" // Fixed pool, each use depletes it\n | \"tiered_jump\" // Free until threshold, then expensive\n | \"percentage\" // Proportional to revenue/volume\n | \"fixed\" // Flat monthly, no scaling\n | \"unknown\";\n\n/** A plan tier option for a service in the registry. */\nexport interface PlanTier {\n /** Human-readable plan name */\n name: string;\n /** Plan type: usage (pay-as-you-go), flat (fixed monthly), exclude (don't track) */\n type: \"usage\" | \"flat\" | \"exclude\";\n /** Monthly base cost for flat plans */\n monthlyBase?: number;\n /** Whether this plan requires an API key for tracking */\n requiresKey?: boolean;\n /** Whether this is the default/most common plan */\n default?: boolean;\n}\n\n/** Risk category for service grouping in interactive init. */\nexport type ServiceRiskCategory = \"llm\" | \"usage\" | \"infra\" | \"flat\";\n\n/** A service definition from the registry. */\nexport interface ServiceDefinition {\n /** Unique service identifier */\n id: string;\n /** Human-readable name */\n name: string;\n /** Package names in npm/pip that indicate this service */\n packageNames: string[];\n /** Env var patterns that indicate this service */\n envPatterns: string[];\n /** Import patterns to scan for (regex strings) */\n importPatterns: string[];\n /** Keywords that indicate mentions in prompts */\n mentionKeywords: string[];\n /** Billing model */\n billingModel: BillingModel;\n /** How cost scales */\n scalingShape: ScalingShape;\n /** What tier of tracking is available */\n apiTier: ConfidenceTier;\n /** Billing API endpoint, if available */\n apiEndpoint?: string;\n /** Pricing details */\n pricing?: {\n /** Human-readable formula */\n formula?: string;\n /** Rate per unit, if applicable */\n unitRate?: number;\n /** Unit name (token, credit, email, session, etc.) */\n unitName?: string;\n /** Monthly base cost, if flat */\n monthlyBase?: number;\n };\n /** Known gotchas that affect cost */\n gotchas?: string[];\n /** Alternative services (free or cheaper) */\n alternatives?: string[];\n /** Documentation URL */\n docsUrl?: string;\n /** Last time pricing was verified */\n lastVerified?: string;\n /** Notes about recent pricing changes */\n pricingNotes?: string;\n /** Available plan tiers for interactive init */\n plans?: PlanTier[];\n /** Whether the plan can be auto-detected from an API key */\n autoDetectPlan?: boolean;\n}\n\n/** A tracked service instance — a service definition + user config. */\nexport interface TrackedService {\n /** Service definition ID */\n serviceId: string;\n /** How this service was detected */\n detectedVia: DetectionSource[];\n /** User-configured monthly budget */\n budget?: number;\n /** Whether the user has provided an API/billing key */\n hasApiKey: boolean;\n /** Override confidence tier (e.g., user provided billing key upgrades to LIVE) */\n tierOverride?: ConfidenceTier;\n /** User-entered monthly plan cost (for CALC tier) */\n planCost?: number;\n /** When this service was first detected */\n firstDetected: string;\n /** Explicitly excluded from tracking by user */\n excluded?: boolean;\n /** Plan name selected during interactive init */\n planName?: string;\n}\n\nexport type DetectionSource =\n | \"package_json\"\n | \"env_var\"\n | \"import_scan\"\n | \"prompt_mention\"\n | \"git_diff\"\n | \"manual\";\n\n/** A spend snapshot for a single service at a point in time. */\nexport interface SpendSnapshot {\n serviceId: string;\n /** Current period spend (or estimate) */\n spend: number;\n /** Is the spend figure exact or estimated? */\n isEstimate: boolean;\n /** Confidence tier for this reading */\n tier: ConfidenceTier;\n /** Budget allocated */\n budget?: number;\n /** Percentage of budget consumed */\n budgetPercent?: number;\n /** Budget status */\n status: \"healthy\" | \"caution\" | \"over\" | \"unknown\";\n /** Human-readable status label */\n statusLabel: string;\n /** Raw data from billing API, if available */\n raw?: Record<string, unknown>;\n /** Timestamp of this snapshot */\n timestamp: string;\n}\n\n/** The full spend brief, injected at session start. */\nexport interface SpendBrief {\n projectName: string;\n generatedAt: string;\n period: string;\n services: SpendSnapshot[];\n totalSpend: number;\n totalIsEstimate: boolean;\n estimateMargin: number;\n untrackedCount: number;\n alerts: SpendAlert[];\n}\n\nexport interface SpendAlert {\n serviceId: string;\n type: \"over_budget\" | \"near_budget\" | \"new_service\" | \"stale_data\" | \"blind_service\";\n message: string;\n severity: \"warning\" | \"critical\" | \"info\";\n}\n\n/** Ledger entry — one row in spend-ledger.md */\nexport interface LedgerEntry {\n serviceId: string;\n serviceName: string;\n spend: number;\n isEstimate: boolean;\n tier: ConfidenceTier;\n budget?: number;\n statusLabel: string;\n}\n\n/** Event logged to events.jsonl */\nexport interface SpendEvent {\n timestamp: string;\n sessionId: string;\n type:\n | \"session_start\"\n | \"session_end\"\n | \"service_detected\"\n | \"service_mentioned\"\n | \"spend_polled\"\n | \"budget_alert\"\n | \"ledger_written\"\n | \"cost_impact\";\n data: Record<string, unknown>;\n}\n\n/** A cost impact estimate for a file change. */\nexport interface CostImpact {\n serviceId: string;\n serviceName: string;\n filePath: string;\n /** Number of SDK call sites found */\n callCount: number;\n /** Detected multipliers (loops, .map(), etc.) */\n multipliers: string[];\n /** Effective multiplier applied to call count */\n multiplierFactor: number;\n /** Estimated monthly invocations */\n monthlyInvocations: number;\n /** Low estimate monthly cost */\n costLow: number;\n /** High estimate monthly cost */\n costHigh: number;\n /** Gotcha-based cost range explanation */\n rangeExplanation?: string;\n}\n\n/**\n * Hook input — the JSON received via stdin from Claude Code.\n * Subset of fields we care about.\n */\nexport interface HookInput {\n session_id: string;\n transcript_path?: string;\n cwd: string;\n hook_event_name: string;\n // SessionStart\n source?: string;\n // UserPromptSubmit\n prompt?: string;\n // PostToolUse\n tool_name?: string;\n tool_input?: {\n file_path?: string;\n command?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n };\n}\n\n/**\n * Hook output — the JSON we write to stdout for Claude Code.\n */\nexport interface HookOutput {\n hookSpecificOutput?: {\n hookEventName: string;\n additionalContext?: string;\n };\n}\n","import type {\n SpendBrief,\n SpendSnapshot,\n SpendAlert,\n ConfidenceTier,\n} from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\n\n/**\n * Format a spend brief as a text block for injection into Claude's context.\n */\nexport function formatBrief(brief: SpendBrief): string {\n const lines: string[] = [];\n const width = 62;\n const hrDouble = \"═\".repeat(width);\n const hrSingle = \"─\".repeat(width - 4);\n\n lines.push(`╔${hrDouble}╗`);\n lines.push(\n `║ BURNWATCH — ${brief.projectName} — ${brief.period}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n lines.push(`╠${hrDouble}╣`);\n\n // Header\n lines.push(\n formatRow(\"Service\", \"Spend\", \"Conf\", \"Budget\", \"Left\", width),\n );\n lines.push(`║ ${hrSingle} ║`);\n\n // Service rows\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n const leftStr = formatLeft(svc);\n\n lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));\n }\n\n // Footer\n lines.push(`╠${hrDouble}╣`);\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr = brief.estimateMargin > 0\n ? ` Est margin: ±$${brief.estimateMargin.toFixed(0)}`\n : \"\";\n const untrackedStr =\n brief.untrackedCount > 0\n ? `Untracked: ${brief.untrackedCount} ⚠️`\n : `Untracked: 0 ✅`;\n\n lines.push(\n `║ TOTAL: ${totalStr} ${untrackedStr}${marginStr}`.padEnd(\n width + 1,\n ) + \"║\",\n );\n\n // Alerts\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(\n `║ ${icon} ${alert.message}`.padEnd(width + 1) + \"║\",\n );\n }\n\n lines.push(`╚${hrDouble}╝`);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Format a single-service spend card for injection on mention.\n */\nexport function formatSpendCard(snapshot: SpendSnapshot): string {\n const badge = CONFIDENCE_BADGES[snapshot.tier];\n const spendStr = snapshot.isEstimate\n ? `~$${snapshot.spend.toFixed(2)}`\n : `$${snapshot.spend.toFixed(2)}`;\n const budgetStr = snapshot.budget\n ? `Budget: $${snapshot.budget}`\n : \"No budget set\";\n const statusStr = snapshot.statusLabel;\n\n const lines = [\n `[BURNWATCH] ${snapshot.serviceId} — current period`,\n ` Spend: ${spendStr} | ${budgetStr} | ${statusStr}`,\n ` Confidence: ${badge}`,\n ];\n\n if (snapshot.status === \"over\" && snapshot.budgetPercent) {\n lines.push(\n ` ⚠️ ${snapshot.budgetPercent.toFixed(0)}% of budget consumed`,\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Build a SpendBrief from snapshots and project config.\n */\nexport function buildBrief(\n projectName: string,\n snapshots: SpendSnapshot[],\n blindCount: number,\n): SpendBrief {\n const now = new Date();\n const period = now.toLocaleDateString(\"en-US\", {\n month: \"long\",\n year: \"numeric\",\n });\n\n let totalSpend = 0;\n let hasEstimates = false;\n let estimateMargin = 0;\n const alerts: SpendAlert[] = [];\n\n for (const snap of snapshots) {\n totalSpend += snap.spend;\n if (snap.isEstimate) {\n hasEstimates = true;\n estimateMargin += snap.spend * 0.15; // ±15% margin on estimates\n }\n\n if (snap.status === \"over\") {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"over_budget\",\n message: `${snap.serviceId.toUpperCase()} ${snap.budgetPercent?.toFixed(0) ?? \"?\"}% OVER BUDGET — review before use`,\n severity: \"critical\",\n });\n } else if (snap.status === \"caution\" && snap.budgetPercent && snap.budgetPercent >= 80) {\n alerts.push({\n serviceId: snap.serviceId,\n type: \"near_budget\",\n message: `${snap.serviceId} at ${snap.budgetPercent.toFixed(0)}% of budget`,\n severity: \"warning\",\n });\n }\n }\n\n if (blindCount > 0) {\n alerts.push({\n serviceId: \"_blind\",\n type: \"blind_service\",\n message: `${blindCount} service${blindCount > 1 ? \"s\" : \"\"} detected but untracked - run 'burnwatch init' to configure`,\n severity: \"warning\",\n });\n }\n\n return {\n projectName,\n generatedAt: now.toISOString(),\n period,\n services: snapshots,\n totalSpend,\n totalIsEstimate: hasEstimates,\n estimateMargin,\n untrackedCount: blindCount,\n alerts,\n };\n}\n\n// --- Helpers ---\n\nfunction formatRow(\n service: string,\n spend: string,\n conf: string,\n budget: string,\n left: string,\n width: number,\n): string {\n const row = ` ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(7)} ${budget.padEnd(7)} ${left}`;\n return `║${row}`.padEnd(width + 1) + \"║\";\n}\n\nfunction formatLeft(snap: SpendSnapshot): string {\n if (!snap.budget) return \"—\";\n if (snap.status === \"over\") return \"⚠️ OVR\";\n if (snap.budgetPercent !== undefined) {\n const remaining = 100 - snap.budgetPercent;\n return `${remaining.toFixed(0)}%`;\n }\n return \"—\";\n}\n\n/**\n * Build a SpendSnapshot from tracked service data.\n */\nexport function buildSnapshot(\n serviceId: string,\n tier: ConfidenceTier,\n spend: number,\n budget?: number,\n): SpendSnapshot {\n const isEstimate = tier === \"est\" || tier === \"calc\";\n const budgetPercent = budget ? (spend / budget) * 100 : undefined;\n\n let status: SpendSnapshot[\"status\"] = \"unknown\";\n let statusLabel = \"no budget\";\n\n if (budget) {\n if (budgetPercent! > 100) {\n status = \"over\";\n statusLabel = `⚠️ ${budgetPercent!.toFixed(0)}% over`;\n } else if (budgetPercent! >= 75) {\n status = \"caution\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — caution`;\n } else {\n status = \"healthy\";\n statusLabel = `${(100 - budgetPercent!).toFixed(0)}% — healthy`;\n }\n }\n\n if (tier === \"calc\" && budget) {\n statusLabel = `flat — on plan`;\n status = \"healthy\";\n }\n\n return {\n serviceId,\n spend,\n isEstimate,\n tier,\n budget,\n budgetPercent,\n status,\n statusLabel,\n timestamp: new Date().toISOString(),\n };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { SpendBrief, SpendEvent } from \"./types.js\";\nimport { CONFIDENCE_BADGES } from \"./types.js\";\nimport { projectConfigDir, projectDataDir } from \"./config.js\";\n\n/**\n * Write the spend ledger as a human-readable markdown file.\n * Designed to be git-committable and readable in 10 seconds.\n */\nexport function writeLedger(brief: SpendBrief, projectRoot?: string): void {\n const now = new Date();\n const lines: string[] = [];\n\n lines.push(`# Burnwatch Ledger — ${brief.projectName}`);\n lines.push(`Last updated: ${now.toISOString()}`);\n lines.push(\"\");\n lines.push(`## This Month (${brief.period})`);\n lines.push(\"\");\n lines.push(\"| Service | Spend | Conf | Budget | Status |\");\n lines.push(\"|---------|-------|------|--------|--------|\");\n\n for (const svc of brief.services) {\n const spendStr = svc.isEstimate\n ? `~$${svc.spend.toFixed(2)}`\n : `$${svc.spend.toFixed(2)}`;\n const badge = CONFIDENCE_BADGES[svc.tier];\n const budgetStr = svc.budget ? `$${svc.budget}` : \"—\";\n\n lines.push(\n `| ${svc.serviceId} | ${spendStr} | ${badge} | ${budgetStr} | ${svc.statusLabel} |`,\n );\n }\n\n // Add projected impact row if session impacts exist in alerts\n const impactAlert = brief.alerts.find(\n (a) => a.serviceId === \"_session_impact\",\n );\n if (impactAlert) {\n lines.push(\n `| _projected impact_ | — | 📈 EST | — | ${impactAlert.message} |`,\n );\n }\n\n lines.push(\"\");\n const totalStr = brief.totalIsEstimate\n ? `~$${brief.totalSpend.toFixed(2)}`\n : `$${brief.totalSpend.toFixed(2)}`;\n const marginStr =\n brief.estimateMargin > 0\n ? ` (±$${brief.estimateMargin.toFixed(0)} estimated margin)`\n : \"\";\n lines.push(`## TOTAL: ${totalStr}${marginStr}`);\n lines.push(`## Untracked services: ${brief.untrackedCount}`);\n lines.push(\"\");\n\n if (brief.alerts.length > 0) {\n lines.push(\"## Alerts\");\n for (const alert of brief.alerts) {\n const icon = alert.severity === \"critical\" ? \"🚨\" : \"⚠️\";\n lines.push(`- ${icon} ${alert.message}`);\n }\n lines.push(\"\");\n }\n\n const ledgerPath = path.join(\n projectConfigDir(projectRoot),\n \"spend-ledger.md\",\n );\n fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });\n fs.writeFileSync(ledgerPath, lines.join(\"\\n\") + \"\\n\", \"utf-8\");\n}\n\n/**\n * Append an event to the append-only event log.\n */\nexport function logEvent(event: SpendEvent, projectRoot?: string): void {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n fs.mkdirSync(path.dirname(logPath), { recursive: true });\n fs.appendFileSync(logPath, JSON.stringify(event) + \"\\n\", \"utf-8\");\n}\n\n/**\n * Read recent events from the event log.\n */\nexport function readRecentEvents(\n count: number,\n projectRoot?: string,\n): SpendEvent[] {\n const logPath = path.join(projectDataDir(projectRoot), \"events.jsonl\");\n try {\n const raw = fs.readFileSync(logPath, \"utf-8\");\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n return lines\n .slice(-count)\n .map((line) => JSON.parse(line) as SpendEvent);\n } catch {\n return [];\n }\n}\n\n/**\n * Save a spend snapshot to the snapshots directory.\n * Used for delta computation across sessions.\n */\nexport function saveSnapshot(brief: SpendBrief, projectRoot?: string): void {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n fs.mkdirSync(snapshotDir, { recursive: true });\n const filename = `snapshot-${new Date().toISOString().replace(/[:.]/g, \"-\")}.json`;\n fs.writeFileSync(\n path.join(snapshotDir, filename),\n JSON.stringify(brief, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\n/**\n * Read the most recent snapshot, if any.\n */\nexport function readLatestSnapshot(\n projectRoot?: string,\n): SpendBrief | null {\n const snapshotDir = path.join(projectDataDir(projectRoot), \"snapshots\");\n try {\n const files = fs\n .readdirSync(snapshotDir)\n .filter((f) => f.startsWith(\"snapshot-\") && f.endsWith(\".json\"))\n .sort()\n .reverse();\n\n if (files.length === 0) return null;\n\n const raw = fs.readFileSync(\n path.join(snapshotDir, files[0]!),\n \"utf-8\",\n );\n return JSON.parse(raw) as SpendBrief;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":["fs","path","url","url","url","url","url","fs","path","fs","path"]}
package/dist/index.js CHANGED
@@ -413,7 +413,7 @@ function buildBrief(projectName, snapshots, blindCount) {
413
413
  alerts.push({
414
414
  serviceId: "_blind",
415
415
  type: "blind_service",
416
- message: `${blindCount} service${blindCount > 1 ? "s" : ""} detected but untracked \u2014 run 'burnwatch status' to see`,
416
+ message: `${blindCount} service${blindCount > 1 ? "s" : ""} detected but untracked - run 'burnwatch init' to configure`,
417
417
  severity: "warning"
418
418
  });
419
419
  }