burnwatch 0.1.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/dist/index.js ADDED
@@ -0,0 +1,573 @@
1
+ // src/core/types.ts
2
+ var CONFIDENCE_BADGES = {
3
+ live: "\u2705 LIVE",
4
+ calc: "\u{1F7E1} CALC",
5
+ est: "\u{1F7E0} EST",
6
+ blind: "\u{1F534} BLIND"
7
+ };
8
+
9
+ // src/core/registry.ts
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import * as url from "url";
13
+ var __dirname = path.dirname(url.fileURLToPath(import.meta.url));
14
+ var cachedRegistry = null;
15
+ function loadRegistry(projectRoot) {
16
+ if (cachedRegistry) return cachedRegistry;
17
+ const registry = /* @__PURE__ */ new Map();
18
+ const candidates = [
19
+ path.resolve(__dirname, "../../registry.json"),
20
+ // from src/core/
21
+ path.resolve(__dirname, "../registry.json")
22
+ // from dist/
23
+ ];
24
+ for (const candidate of candidates) {
25
+ if (fs.existsSync(candidate)) {
26
+ loadRegistryFile(candidate, registry);
27
+ break;
28
+ }
29
+ }
30
+ if (projectRoot) {
31
+ const localPath = path.join(projectRoot, ".burnwatch", "registry.json");
32
+ if (fs.existsSync(localPath)) {
33
+ loadRegistryFile(localPath, registry);
34
+ }
35
+ }
36
+ cachedRegistry = registry;
37
+ return registry;
38
+ }
39
+ function loadRegistryFile(filePath, registry) {
40
+ try {
41
+ const raw = fs.readFileSync(filePath, "utf-8");
42
+ const data = JSON.parse(raw);
43
+ for (const [id, service] of Object.entries(data.services)) {
44
+ registry.set(id, { ...service, id });
45
+ }
46
+ } catch {
47
+ }
48
+ }
49
+ function getService(id, projectRoot) {
50
+ return loadRegistry(projectRoot).get(id);
51
+ }
52
+ function getAllServices(projectRoot) {
53
+ return Array.from(loadRegistry(projectRoot).values());
54
+ }
55
+
56
+ // src/detection/detector.ts
57
+ import * as fs2 from "fs";
58
+ import * as path2 from "path";
59
+ function detectServices(projectRoot) {
60
+ const registry = loadRegistry(projectRoot);
61
+ const results = /* @__PURE__ */ new Map();
62
+ const pkgDeps = scanPackageJson(projectRoot);
63
+ for (const [serviceId, service] of registry) {
64
+ const matchedPkgs = service.packageNames.filter(
65
+ (pkg) => pkgDeps.has(pkg)
66
+ );
67
+ if (matchedPkgs.length > 0) {
68
+ getOrCreate(results, serviceId, service).sources.push("package_json");
69
+ getOrCreate(results, serviceId, service).details.push(
70
+ `package.json: ${matchedPkgs.join(", ")}`
71
+ );
72
+ }
73
+ }
74
+ const envVars = new Set(Object.keys(process.env));
75
+ for (const [serviceId, service] of registry) {
76
+ const matchedEnvs = service.envPatterns.filter(
77
+ (pattern) => envVars.has(pattern)
78
+ );
79
+ if (matchedEnvs.length > 0) {
80
+ getOrCreate(results, serviceId, service).sources.push("env_var");
81
+ getOrCreate(results, serviceId, service).details.push(
82
+ `env vars: ${matchedEnvs.join(", ")}`
83
+ );
84
+ }
85
+ }
86
+ const importHits = scanImports(projectRoot);
87
+ for (const [serviceId, service] of registry) {
88
+ const matchedImports = service.importPatterns.filter(
89
+ (pattern) => importHits.has(pattern)
90
+ );
91
+ if (matchedImports.length > 0) {
92
+ if (!getOrCreate(results, serviceId, service).sources.includes(
93
+ "import_scan"
94
+ )) {
95
+ getOrCreate(results, serviceId, service).sources.push("import_scan");
96
+ getOrCreate(results, serviceId, service).details.push(
97
+ `imports: ${matchedImports.join(", ")}`
98
+ );
99
+ }
100
+ }
101
+ }
102
+ return Array.from(results.values());
103
+ }
104
+ function detectMentions(prompt, projectRoot) {
105
+ const registry = loadRegistry(projectRoot);
106
+ const results = [];
107
+ const promptLower = prompt.toLowerCase();
108
+ for (const [, service] of registry) {
109
+ const matched = service.mentionKeywords.some(
110
+ (keyword) => promptLower.includes(keyword.toLowerCase())
111
+ );
112
+ if (matched) {
113
+ results.push({
114
+ service,
115
+ sources: ["prompt_mention"],
116
+ details: [`mentioned in prompt`]
117
+ });
118
+ }
119
+ }
120
+ return results;
121
+ }
122
+ function detectInFileChange(filePath, content, projectRoot) {
123
+ const registry = loadRegistry(projectRoot);
124
+ const results = [];
125
+ const fileName = path2.basename(filePath);
126
+ if (fileName === "package.json") {
127
+ try {
128
+ const pkg = JSON.parse(content);
129
+ const allDeps = /* @__PURE__ */ new Set([
130
+ ...Object.keys(pkg.dependencies ?? {}),
131
+ ...Object.keys(pkg.devDependencies ?? {})
132
+ ]);
133
+ for (const [, service] of registry) {
134
+ const matched = service.packageNames.filter((p) => allDeps.has(p));
135
+ if (matched.length > 0) {
136
+ results.push({
137
+ service,
138
+ sources: ["package_json"],
139
+ details: [`new dependency: ${matched.join(", ")}`]
140
+ });
141
+ }
142
+ }
143
+ } catch {
144
+ }
145
+ return results;
146
+ }
147
+ if (fileName.startsWith(".env")) {
148
+ const envKeys = content.split("\n").filter((line) => line.includes("=") && !line.startsWith("#")).map((line) => line.split("=")[0].trim());
149
+ for (const [, service] of registry) {
150
+ const matched = service.envPatterns.filter((p) => envKeys.includes(p));
151
+ if (matched.length > 0) {
152
+ results.push({
153
+ service,
154
+ sources: ["env_var"],
155
+ details: [`new env var: ${matched.join(", ")}`]
156
+ });
157
+ }
158
+ }
159
+ return results;
160
+ }
161
+ if (/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {
162
+ for (const [, service] of registry) {
163
+ const matched = service.importPatterns.filter(
164
+ (pattern) => content.includes(`from "${pattern}`) || content.includes(`from '${pattern}`) || content.includes(`require("${pattern}`) || content.includes(`require('${pattern}`)
165
+ );
166
+ if (matched.length > 0) {
167
+ results.push({
168
+ service,
169
+ sources: ["import_scan"],
170
+ details: [`import added: ${matched.join(", ")}`]
171
+ });
172
+ }
173
+ }
174
+ }
175
+ return results;
176
+ }
177
+ function getOrCreate(map, serviceId, service) {
178
+ let result = map.get(serviceId);
179
+ if (!result) {
180
+ result = { service, sources: [], details: [] };
181
+ map.set(serviceId, result);
182
+ }
183
+ return result;
184
+ }
185
+ function scanPackageJson(projectRoot) {
186
+ const deps = /* @__PURE__ */ new Set();
187
+ const pkgPath = path2.join(projectRoot, "package.json");
188
+ try {
189
+ const raw = fs2.readFileSync(pkgPath, "utf-8");
190
+ const pkg = JSON.parse(raw);
191
+ for (const name of Object.keys(pkg.dependencies ?? {})) deps.add(name);
192
+ for (const name of Object.keys(pkg.devDependencies ?? {})) deps.add(name);
193
+ } catch {
194
+ }
195
+ return deps;
196
+ }
197
+ function scanImports(projectRoot) {
198
+ const imports = /* @__PURE__ */ new Set();
199
+ const srcDir = path2.join(projectRoot, "src");
200
+ if (!fs2.existsSync(srcDir)) return imports;
201
+ const files = walkDir(srcDir, /\.(ts|tsx|js|jsx|mjs|cjs)$/);
202
+ for (const file of files) {
203
+ try {
204
+ const content = fs2.readFileSync(file, "utf-8");
205
+ const importRegex = /(?:from\s+["']|require\s*\(\s*["'])([^./][^"']*?)(?:["'])/g;
206
+ let match;
207
+ while ((match = importRegex.exec(content)) !== null) {
208
+ const pkg = match[1];
209
+ if (pkg) {
210
+ const parts = pkg.split("/");
211
+ if (parts[0]?.startsWith("@") && parts.length >= 2) {
212
+ imports.add(`${parts[0]}/${parts[1]}`);
213
+ } else if (parts[0]) {
214
+ imports.add(parts[0]);
215
+ }
216
+ }
217
+ }
218
+ } catch {
219
+ }
220
+ }
221
+ return imports;
222
+ }
223
+ function walkDir(dir, pattern, maxDepth = 5) {
224
+ const results = [];
225
+ if (maxDepth <= 0) return results;
226
+ try {
227
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
228
+ for (const entry of entries) {
229
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
230
+ const fullPath = path2.join(dir, entry.name);
231
+ if (entry.isDirectory()) {
232
+ results.push(...walkDir(fullPath, pattern, maxDepth - 1));
233
+ } else if (pattern.test(entry.name)) {
234
+ results.push(fullPath);
235
+ }
236
+ }
237
+ } catch {
238
+ }
239
+ return results;
240
+ }
241
+
242
+ // src/core/brief.ts
243
+ function formatBrief(brief) {
244
+ const lines = [];
245
+ const width = 62;
246
+ const hrDouble = "\u2550".repeat(width);
247
+ const hrSingle = "\u2500".repeat(width - 4);
248
+ lines.push(`\u2554${hrDouble}\u2557`);
249
+ lines.push(
250
+ `\u2551 BURNWATCH \u2014 ${brief.projectName} \u2014 ${brief.period}`.padEnd(
251
+ width + 1
252
+ ) + "\u2551"
253
+ );
254
+ lines.push(`\u2560${hrDouble}\u2563`);
255
+ lines.push(
256
+ formatRow("Service", "Spend", "Conf", "Budget", "Left", width)
257
+ );
258
+ lines.push(`\u2551 ${hrSingle} \u2551`);
259
+ for (const svc of brief.services) {
260
+ const spendStr = svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
261
+ const badge = CONFIDENCE_BADGES[svc.tier];
262
+ const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
263
+ const leftStr = formatLeft(svc);
264
+ lines.push(formatRow(svc.serviceId, spendStr, badge, budgetStr, leftStr, width));
265
+ }
266
+ lines.push(`\u2560${hrDouble}\u2563`);
267
+ const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
268
+ const marginStr = brief.estimateMargin > 0 ? ` Est margin: \xB1$${brief.estimateMargin.toFixed(0)}` : "";
269
+ const untrackedStr = brief.untrackedCount > 0 ? `Untracked: ${brief.untrackedCount} \u26A0\uFE0F` : `Untracked: 0 \u2705`;
270
+ lines.push(
271
+ `\u2551 TOTAL: ${totalStr} ${untrackedStr}${marginStr}`.padEnd(
272
+ width + 1
273
+ ) + "\u2551"
274
+ );
275
+ for (const alert of brief.alerts) {
276
+ const icon = alert.severity === "critical" ? "\u{1F6A8}" : "\u26A0\uFE0F";
277
+ lines.push(
278
+ `\u2551 ${icon} ${alert.message}`.padEnd(width + 1) + "\u2551"
279
+ );
280
+ }
281
+ lines.push(`\u255A${hrDouble}\u255D`);
282
+ return lines.join("\n");
283
+ }
284
+ function formatSpendCard(snapshot) {
285
+ const badge = CONFIDENCE_BADGES[snapshot.tier];
286
+ const spendStr = snapshot.isEstimate ? `~$${snapshot.spend.toFixed(2)}` : `$${snapshot.spend.toFixed(2)}`;
287
+ const budgetStr = snapshot.budget ? `Budget: $${snapshot.budget}` : "No budget set";
288
+ const statusStr = snapshot.statusLabel;
289
+ const lines = [
290
+ `[BURNWATCH] ${snapshot.serviceId} \u2014 current period`,
291
+ ` Spend: ${spendStr} | ${budgetStr} | ${statusStr}`,
292
+ ` Confidence: ${badge}`
293
+ ];
294
+ if (snapshot.status === "over" && snapshot.budgetPercent) {
295
+ lines.push(
296
+ ` \u26A0\uFE0F ${snapshot.budgetPercent.toFixed(0)}% of budget consumed`
297
+ );
298
+ }
299
+ return lines.join("\n");
300
+ }
301
+ function buildBrief(projectName, snapshots, blindCount) {
302
+ const now = /* @__PURE__ */ new Date();
303
+ const period = now.toLocaleDateString("en-US", {
304
+ month: "long",
305
+ year: "numeric"
306
+ });
307
+ let totalSpend = 0;
308
+ let hasEstimates = false;
309
+ let estimateMargin = 0;
310
+ const alerts = [];
311
+ for (const snap of snapshots) {
312
+ totalSpend += snap.spend;
313
+ if (snap.isEstimate) {
314
+ hasEstimates = true;
315
+ estimateMargin += snap.spend * 0.15;
316
+ }
317
+ if (snap.status === "over") {
318
+ alerts.push({
319
+ serviceId: snap.serviceId,
320
+ type: "over_budget",
321
+ message: `${snap.serviceId.toUpperCase()} ${snap.budgetPercent?.toFixed(0) ?? "?"}% OVER BUDGET \u2014 review before use`,
322
+ severity: "critical"
323
+ });
324
+ } else if (snap.status === "caution" && snap.budgetPercent && snap.budgetPercent >= 80) {
325
+ alerts.push({
326
+ serviceId: snap.serviceId,
327
+ type: "near_budget",
328
+ message: `${snap.serviceId} at ${snap.budgetPercent.toFixed(0)}% of budget`,
329
+ severity: "warning"
330
+ });
331
+ }
332
+ }
333
+ if (blindCount > 0) {
334
+ alerts.push({
335
+ serviceId: "_blind",
336
+ type: "blind_service",
337
+ message: `${blindCount} service${blindCount > 1 ? "s" : ""} detected but untracked \u2014 run 'burnwatch status' to see`,
338
+ severity: "warning"
339
+ });
340
+ }
341
+ return {
342
+ projectName,
343
+ generatedAt: now.toISOString(),
344
+ period,
345
+ services: snapshots,
346
+ totalSpend,
347
+ totalIsEstimate: hasEstimates,
348
+ estimateMargin,
349
+ untrackedCount: blindCount,
350
+ alerts
351
+ };
352
+ }
353
+ function formatRow(service, spend, conf, budget, left, width) {
354
+ const row = ` ${service.padEnd(14)} ${spend.padEnd(11)} ${conf.padEnd(7)} ${budget.padEnd(7)} ${left}`;
355
+ return `\u2551${row}`.padEnd(width + 1) + "\u2551";
356
+ }
357
+ function formatLeft(snap) {
358
+ if (!snap.budget) return "\u2014";
359
+ if (snap.status === "over") return "\u26A0\uFE0F OVR";
360
+ if (snap.budgetPercent !== void 0) {
361
+ const remaining = 100 - snap.budgetPercent;
362
+ return `${remaining.toFixed(0)}%`;
363
+ }
364
+ return "\u2014";
365
+ }
366
+ function buildSnapshot(serviceId, tier, spend, budget) {
367
+ const isEstimate = tier === "est" || tier === "calc";
368
+ const budgetPercent = budget ? spend / budget * 100 : void 0;
369
+ let status = "unknown";
370
+ let statusLabel = "no budget";
371
+ if (budget) {
372
+ if (budgetPercent > 100) {
373
+ status = "over";
374
+ statusLabel = `\u26A0\uFE0F ${budgetPercent.toFixed(0)}% over`;
375
+ } else if (budgetPercent >= 75) {
376
+ status = "caution";
377
+ statusLabel = `${(100 - budgetPercent).toFixed(0)}% \u2014 caution`;
378
+ } else {
379
+ status = "healthy";
380
+ statusLabel = `${(100 - budgetPercent).toFixed(0)}% \u2014 healthy`;
381
+ }
382
+ }
383
+ if (tier === "calc" && budget) {
384
+ statusLabel = `flat \u2014 on plan`;
385
+ status = "healthy";
386
+ }
387
+ return {
388
+ serviceId,
389
+ spend,
390
+ isEstimate,
391
+ tier,
392
+ budget,
393
+ budgetPercent,
394
+ status,
395
+ statusLabel,
396
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
397
+ };
398
+ }
399
+
400
+ // src/core/ledger.ts
401
+ import * as fs4 from "fs";
402
+ import * as path4 from "path";
403
+
404
+ // src/core/config.ts
405
+ import * as fs3 from "fs";
406
+ import * as path3 from "path";
407
+ import * as os from "os";
408
+ function globalConfigDir() {
409
+ const xdgConfig = process.env["XDG_CONFIG_HOME"];
410
+ if (xdgConfig) return path3.join(xdgConfig, "burnwatch");
411
+ return path3.join(os.homedir(), ".config", "burnwatch");
412
+ }
413
+ function projectConfigDir(projectRoot) {
414
+ const root = projectRoot ?? process.cwd();
415
+ return path3.join(root, ".burnwatch");
416
+ }
417
+ function projectDataDir(projectRoot) {
418
+ return path3.join(projectConfigDir(projectRoot), "data");
419
+ }
420
+ function readGlobalConfig() {
421
+ const configPath = path3.join(globalConfigDir(), "config.json");
422
+ try {
423
+ const raw = fs3.readFileSync(configPath, "utf-8");
424
+ return JSON.parse(raw);
425
+ } catch {
426
+ return { services: {} };
427
+ }
428
+ }
429
+ function writeGlobalConfig(config) {
430
+ const dir = globalConfigDir();
431
+ fs3.mkdirSync(dir, { recursive: true });
432
+ const configPath = path3.join(dir, "config.json");
433
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
434
+ fs3.chmodSync(configPath, 384);
435
+ }
436
+ function readProjectConfig(projectRoot) {
437
+ const configPath = path3.join(projectConfigDir(projectRoot), "config.json");
438
+ try {
439
+ const raw = fs3.readFileSync(configPath, "utf-8");
440
+ return JSON.parse(raw);
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+ function writeProjectConfig(config, projectRoot) {
446
+ const dir = projectConfigDir(projectRoot);
447
+ fs3.mkdirSync(dir, { recursive: true });
448
+ config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
449
+ const configPath = path3.join(dir, "config.json");
450
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
451
+ }
452
+ function ensureProjectDirs(projectRoot) {
453
+ const dirs = [
454
+ projectConfigDir(projectRoot),
455
+ projectDataDir(projectRoot),
456
+ path3.join(projectDataDir(projectRoot), "cache"),
457
+ path3.join(projectDataDir(projectRoot), "snapshots")
458
+ ];
459
+ for (const dir of dirs) {
460
+ fs3.mkdirSync(dir, { recursive: true });
461
+ }
462
+ }
463
+ function isInitialized(projectRoot) {
464
+ return readProjectConfig(projectRoot) !== null;
465
+ }
466
+
467
+ // src/core/ledger.ts
468
+ function writeLedger(brief, projectRoot) {
469
+ const now = /* @__PURE__ */ new Date();
470
+ const lines = [];
471
+ lines.push(`# Burnwatch Ledger \u2014 ${brief.projectName}`);
472
+ lines.push(`Last updated: ${now.toISOString()}`);
473
+ lines.push("");
474
+ lines.push(`## This Month (${brief.period})`);
475
+ lines.push("");
476
+ lines.push("| Service | Spend | Conf | Budget | Status |");
477
+ lines.push("|---------|-------|------|--------|--------|");
478
+ for (const svc of brief.services) {
479
+ const spendStr = svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
480
+ const badge = CONFIDENCE_BADGES[svc.tier];
481
+ const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
482
+ lines.push(
483
+ `| ${svc.serviceId} | ${spendStr} | ${badge} | ${budgetStr} | ${svc.statusLabel} |`
484
+ );
485
+ }
486
+ lines.push("");
487
+ const totalStr = brief.totalIsEstimate ? `~$${brief.totalSpend.toFixed(2)}` : `$${brief.totalSpend.toFixed(2)}`;
488
+ const marginStr = brief.estimateMargin > 0 ? ` (\xB1$${brief.estimateMargin.toFixed(0)} estimated margin)` : "";
489
+ lines.push(`## TOTAL: ${totalStr}${marginStr}`);
490
+ lines.push(`## Untracked services: ${brief.untrackedCount}`);
491
+ lines.push("");
492
+ if (brief.alerts.length > 0) {
493
+ lines.push("## Alerts");
494
+ for (const alert of brief.alerts) {
495
+ const icon = alert.severity === "critical" ? "\u{1F6A8}" : "\u26A0\uFE0F";
496
+ lines.push(`- ${icon} ${alert.message}`);
497
+ }
498
+ lines.push("");
499
+ }
500
+ const ledgerPath = path4.join(
501
+ projectConfigDir(projectRoot),
502
+ "spend-ledger.md"
503
+ );
504
+ fs4.mkdirSync(path4.dirname(ledgerPath), { recursive: true });
505
+ fs4.writeFileSync(ledgerPath, lines.join("\n") + "\n", "utf-8");
506
+ }
507
+ function logEvent(event, projectRoot) {
508
+ const logPath = path4.join(projectDataDir(projectRoot), "events.jsonl");
509
+ fs4.mkdirSync(path4.dirname(logPath), { recursive: true });
510
+ fs4.appendFileSync(logPath, JSON.stringify(event) + "\n", "utf-8");
511
+ }
512
+ function readRecentEvents(count, projectRoot) {
513
+ const logPath = path4.join(projectDataDir(projectRoot), "events.jsonl");
514
+ try {
515
+ const raw = fs4.readFileSync(logPath, "utf-8");
516
+ const lines = raw.trim().split("\n").filter(Boolean);
517
+ return lines.slice(-count).map((line) => JSON.parse(line));
518
+ } catch {
519
+ return [];
520
+ }
521
+ }
522
+ function saveSnapshot(brief, projectRoot) {
523
+ const snapshotDir = path4.join(projectDataDir(projectRoot), "snapshots");
524
+ fs4.mkdirSync(snapshotDir, { recursive: true });
525
+ const filename = `snapshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
526
+ fs4.writeFileSync(
527
+ path4.join(snapshotDir, filename),
528
+ JSON.stringify(brief, null, 2) + "\n",
529
+ "utf-8"
530
+ );
531
+ }
532
+ function readLatestSnapshot(projectRoot) {
533
+ const snapshotDir = path4.join(projectDataDir(projectRoot), "snapshots");
534
+ try {
535
+ const files = fs4.readdirSync(snapshotDir).filter((f) => f.startsWith("snapshot-") && f.endsWith(".json")).sort().reverse();
536
+ if (files.length === 0) return null;
537
+ const raw = fs4.readFileSync(
538
+ path4.join(snapshotDir, files[0]),
539
+ "utf-8"
540
+ );
541
+ return JSON.parse(raw);
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+ export {
547
+ CONFIDENCE_BADGES,
548
+ buildBrief,
549
+ buildSnapshot,
550
+ detectInFileChange,
551
+ detectMentions,
552
+ detectServices,
553
+ ensureProjectDirs,
554
+ formatBrief,
555
+ formatSpendCard,
556
+ getAllServices,
557
+ getService,
558
+ globalConfigDir,
559
+ isInitialized,
560
+ loadRegistry,
561
+ logEvent,
562
+ projectConfigDir,
563
+ projectDataDir,
564
+ readGlobalConfig,
565
+ readLatestSnapshot,
566
+ readProjectConfig,
567
+ readRecentEvents,
568
+ saveSnapshot,
569
+ writeGlobalConfig,
570
+ writeLedger,
571
+ writeProjectConfig
572
+ };
573
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/types.ts","../src/core/registry.ts","../src/detection/detector.ts","../src/core/brief.ts","../src/core/ledger.ts","../src/core/config.ts"],"sourcesContent":["/**\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\";\n\nexport const CONFIDENCE_BADGES: Record<ConfidenceTier, string> = {\n live: \"✅ LIVE\",\n calc: \"🟡 CALC\",\n est: \"🟠 EST\",\n blind: \"🔴 BLIND\",\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 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}\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}\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 data: Record<string, unknown>;\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 * 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 * 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\n * - environment variable patterns\n * - import statement scanning\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\n const pkgDeps = scanPackageJson(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 const envVars = new Set(Object.keys(process.env));\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 (lightweight — scan key files only)\n const importHits = scanImports(projectRoot);\n for (const [serviceId, service] of registry) {\n const matchedImports = service.importPatterns.filter((pattern) =>\n importHits.has(pattern),\n );\n if (matchedImports.length > 0) {\n if (\n !getOrCreate(results, serviceId, service).sources.includes(\n \"import_scan\",\n )\n ) {\n getOrCreate(results, serviceId, service).sources.push(\"import_scan\");\n getOrCreate(results, serviceId, service).details.push(\n `imports: ${matchedImports.join(\", \")}`,\n );\n }\n }\n }\n\n return Array.from(results.values());\n}\n\n/**\n * Detect services mentioned in a prompt string.\n * Used by the UserPromptSubmit hook.\n */\nexport function detectMentions(\n prompt: string,\n projectRoot?: string,\n): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results: DetectionResult[] = [];\n const promptLower = prompt.toLowerCase();\n\n for (const [, service] of registry) {\n const matched = service.mentionKeywords.some((keyword) =>\n promptLower.includes(keyword.toLowerCase()),\n );\n if (matched) {\n results.push({\n service,\n sources: [\"prompt_mention\"],\n details: [`mentioned in prompt`],\n });\n }\n }\n\n return results;\n}\n\n/**\n * Detect new services introduced in a file change.\n * Used by the PostToolUse hook for Write/Edit events.\n */\nexport function detectInFileChange(\n filePath: string,\n content: string,\n projectRoot?: string,\n): DetectionResult[] {\n const registry = loadRegistry(projectRoot);\n const results: DetectionResult[] = [];\n const fileName = path.basename(filePath);\n\n // Check if it's a package.json change\n if (fileName === \"package.json\") {\n try {\n const pkg = JSON.parse(content) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const allDeps = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ]);\n\n for (const [, service] of registry) {\n const matched = service.packageNames.filter((p) => allDeps.has(p));\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"package_json\"],\n details: [`new dependency: ${matched.join(\", \")}`],\n });\n }\n }\n } catch {\n // Not valid JSON, skip\n }\n return results;\n }\n\n // Check if it's an env file change\n if (fileName.startsWith(\".env\")) {\n const envKeys = content\n .split(\"\\n\")\n .filter((line) => line.includes(\"=\") && !line.startsWith(\"#\"))\n .map((line) => line.split(\"=\")[0]!.trim());\n\n for (const [, service] of registry) {\n const matched = service.envPatterns.filter((p) => envKeys.includes(p));\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"env_var\"],\n details: [`new env var: ${matched.join(\", \")}`],\n });\n }\n }\n return results;\n }\n\n // Check for import statements in source files\n if (/\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {\n for (const [, service] of registry) {\n const matched = service.importPatterns.filter(\n (pattern) =>\n content.includes(`from \"${pattern}`) ||\n content.includes(`from '${pattern}`) ||\n content.includes(`require(\"${pattern}`) ||\n content.includes(`require('${pattern}`),\n );\n if (matched.length > 0) {\n results.push({\n service,\n sources: [\"import_scan\"],\n details: [`import added: ${matched.join(\", \")}`],\n });\n }\n }\n }\n\n return results;\n}\n\n// --- Helpers ---\n\nfunction getOrCreate(\n map: Map<string, DetectionResult>,\n serviceId: string,\n service: ServiceDefinition,\n): DetectionResult {\n let result = map.get(serviceId);\n if (!result) {\n result = { service, sources: [], details: [] };\n map.set(serviceId, result);\n }\n return result;\n}\n\n/** Scan package.json for all dependencies. */\nfunction scanPackageJson(projectRoot: string): Set<string> {\n const deps = new Set<string>();\n const pkgPath = path.join(projectRoot, \"package.json\");\n\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 // No package.json or not valid JSON\n }\n\n return deps;\n}\n\n/**\n * Lightweight import scanning.\n * Scans src/ directory for import/require statements.\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 const srcDir = path.join(projectRoot, \"src\");\n\n if (!fs.existsSync(srcDir)) return imports;\n\n const files = walkDir(srcDir, /\\.(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 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 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 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","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"],"mappings":"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","names":["fs","path","fs","path","fs","path"]}