burnwatch 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/cli.js +608 -66
- package/dist/cli.js.map +1 -1
- package/dist/{detector-CSgHJEdg.d.ts → detector-myYS2eVC.d.ts} +10 -1
- package/dist/hooks/on-file-change.js +140 -11
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +125 -21
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/interactive-init.d.ts +1 -1
- package/dist/interactive-init.js +64 -14
- package/dist/interactive-init.js.map +1 -1
- package/dist/mcp-server.js +14 -2
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/registry.json +6 -1
package/dist/cli.js
CHANGED
|
@@ -287,8 +287,8 @@ var init_probes = __esm({
|
|
|
287
287
|
});
|
|
288
288
|
|
|
289
289
|
// src/cli.ts
|
|
290
|
-
import * as
|
|
291
|
-
import * as
|
|
290
|
+
import * as fs7 from "fs";
|
|
291
|
+
import * as path7 from "path";
|
|
292
292
|
|
|
293
293
|
// src/core/config.ts
|
|
294
294
|
import * as fs from "fs";
|
|
@@ -502,8 +502,7 @@ function collectEnvVars(projectRoot) {
|
|
|
502
502
|
for (const envFile of envFiles) {
|
|
503
503
|
try {
|
|
504
504
|
const content = fs3.readFileSync(envFile, "utf-8");
|
|
505
|
-
|
|
506
|
-
for (const key of keys) {
|
|
505
|
+
for (const key of parseEnvKeys(content)) {
|
|
507
506
|
envVars.add(key);
|
|
508
507
|
}
|
|
509
508
|
} catch {
|
|
@@ -511,6 +510,19 @@ function collectEnvVars(projectRoot) {
|
|
|
511
510
|
}
|
|
512
511
|
return envVars;
|
|
513
512
|
}
|
|
513
|
+
function parseEnvKeys(content) {
|
|
514
|
+
const keys = /* @__PURE__ */ new Set();
|
|
515
|
+
for (const line of content.split("\n")) {
|
|
516
|
+
const trimmed = line.trim();
|
|
517
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
518
|
+
const stripped = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
519
|
+
const eqIdx = stripped.indexOf("=");
|
|
520
|
+
if (eqIdx > 0) {
|
|
521
|
+
keys.add(stripped.slice(0, eqIdx).trim());
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return keys;
|
|
525
|
+
}
|
|
514
526
|
function findEnvFiles(dir, maxDepth) {
|
|
515
527
|
const results = [];
|
|
516
528
|
if (maxDepth <= 0) return results;
|
|
@@ -1267,6 +1279,8 @@ function saveSnapshot(brief, projectRoot) {
|
|
|
1267
1279
|
|
|
1268
1280
|
// src/interactive-init.ts
|
|
1269
1281
|
import * as readline from "readline";
|
|
1282
|
+
import * as fs5 from "fs";
|
|
1283
|
+
import "path";
|
|
1270
1284
|
init_probes();
|
|
1271
1285
|
function formatUnits(n) {
|
|
1272
1286
|
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
@@ -1313,11 +1327,27 @@ function ask(rl, question) {
|
|
|
1313
1327
|
});
|
|
1314
1328
|
});
|
|
1315
1329
|
}
|
|
1316
|
-
function findEnvKey(service) {
|
|
1330
|
+
function findEnvKey(service, projectRoot = process.cwd()) {
|
|
1317
1331
|
for (const pattern of service.envPatterns) {
|
|
1318
1332
|
const val = process.env[pattern];
|
|
1319
1333
|
if (val && val.length > 0) return val;
|
|
1320
1334
|
}
|
|
1335
|
+
if (projectRoot) {
|
|
1336
|
+
const envFiles = findEnvFiles(projectRoot, 3);
|
|
1337
|
+
for (const filePath of envFiles) {
|
|
1338
|
+
try {
|
|
1339
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
1340
|
+
for (const pattern of service.envPatterns) {
|
|
1341
|
+
const regex = new RegExp(`^(?:export\\s+)?${pattern}\\s*=\\s*(.+)$`, "m");
|
|
1342
|
+
const match = content.match(regex);
|
|
1343
|
+
if (match?.[1]) {
|
|
1344
|
+
return match[1].trim().replace(/^["']|["']$/g, "");
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1321
1351
|
return void 0;
|
|
1322
1352
|
}
|
|
1323
1353
|
async function autoConfigureServices(detected) {
|
|
@@ -1592,6 +1622,439 @@ async function runInteractiveInit(detected) {
|
|
|
1592
1622
|
return { services };
|
|
1593
1623
|
}
|
|
1594
1624
|
|
|
1625
|
+
// src/utilization.ts
|
|
1626
|
+
import * as fs6 from "fs";
|
|
1627
|
+
import * as path6 from "path";
|
|
1628
|
+
|
|
1629
|
+
// src/cost-impact.ts
|
|
1630
|
+
var SERVICE_CALL_PATTERNS = {
|
|
1631
|
+
anthropic: [
|
|
1632
|
+
/\.messages\.create\s*\(/g,
|
|
1633
|
+
/\.completions\.create\s*\(/g,
|
|
1634
|
+
/anthropic\.\w+\.create\s*\(/g
|
|
1635
|
+
],
|
|
1636
|
+
openai: [
|
|
1637
|
+
/\.chat\.completions\.create\s*\(/g,
|
|
1638
|
+
/\.completions\.create\s*\(/g,
|
|
1639
|
+
/\.images\.generate\s*\(/g,
|
|
1640
|
+
/\.embeddings\.create\s*\(/g,
|
|
1641
|
+
/openai\.\w+\.create\s*\(/g
|
|
1642
|
+
],
|
|
1643
|
+
"google-gemini": [
|
|
1644
|
+
/\.generateContent\s*\(/g,
|
|
1645
|
+
/\.generateContentStream\s*\(/g,
|
|
1646
|
+
/model\.generate\w*\s*\(/g
|
|
1647
|
+
],
|
|
1648
|
+
"voyage-ai": [
|
|
1649
|
+
/\.embed\s*\(/g,
|
|
1650
|
+
/voyageai\.embed\s*\(/g
|
|
1651
|
+
],
|
|
1652
|
+
scrapfly: [
|
|
1653
|
+
/\.scrape\s*\(/g,
|
|
1654
|
+
/scrapfly\.scrape\s*\(/g,
|
|
1655
|
+
/\.async_scrape\s*\(/g,
|
|
1656
|
+
/ScrapeConfig\s*\(/g
|
|
1657
|
+
],
|
|
1658
|
+
browserbase: [
|
|
1659
|
+
/\.createSession\s*\(/g,
|
|
1660
|
+
/\.sessions\.create\s*\(/g,
|
|
1661
|
+
/stagehand\.act\s*\(/g,
|
|
1662
|
+
/stagehand\.extract\s*\(/g
|
|
1663
|
+
],
|
|
1664
|
+
upstash: [
|
|
1665
|
+
/redis\.\w+\s*\(/g,
|
|
1666
|
+
/\.set\s*\(/g,
|
|
1667
|
+
/\.get\s*\(/g,
|
|
1668
|
+
/\.incr\s*\(/g,
|
|
1669
|
+
/\.hset\s*\(/g
|
|
1670
|
+
],
|
|
1671
|
+
resend: [
|
|
1672
|
+
/resend\.emails\.send\s*\(/g,
|
|
1673
|
+
/\.emails\.send\s*\(/g
|
|
1674
|
+
],
|
|
1675
|
+
stripe: [
|
|
1676
|
+
/stripe\.charges\.create\s*\(/g,
|
|
1677
|
+
/stripe\.paymentIntents\.create\s*\(/g,
|
|
1678
|
+
/stripe\.checkout\.sessions\.create\s*\(/g
|
|
1679
|
+
],
|
|
1680
|
+
supabase: [
|
|
1681
|
+
/supabase\.from\s*\(/g,
|
|
1682
|
+
/\.rpc\s*\(/g,
|
|
1683
|
+
/supabase\.storage/g
|
|
1684
|
+
],
|
|
1685
|
+
inngest: [
|
|
1686
|
+
/inngest\.send\s*\(/g,
|
|
1687
|
+
/\.createFunction\s*\(/g
|
|
1688
|
+
],
|
|
1689
|
+
posthog: [
|
|
1690
|
+
/posthog\.capture\s*\(/g,
|
|
1691
|
+
/\.capture\s*\(/g
|
|
1692
|
+
],
|
|
1693
|
+
aws: [
|
|
1694
|
+
/\.send\s*\(new\s+\w+Command/g,
|
|
1695
|
+
/s3Client\.send\s*\(/g,
|
|
1696
|
+
/lambdaClient\.send\s*\(/g
|
|
1697
|
+
],
|
|
1698
|
+
firebase: [
|
|
1699
|
+
/firestore\.\w+\(\s*["']/g,
|
|
1700
|
+
/\.collection\s*\(/g,
|
|
1701
|
+
/\.doc\s*\(/g,
|
|
1702
|
+
/admin\.firestore\(\)/g
|
|
1703
|
+
],
|
|
1704
|
+
twilio: [
|
|
1705
|
+
/\.messages\.create\s*\(/g,
|
|
1706
|
+
/\.calls\.create\s*\(/g,
|
|
1707
|
+
/twilio\.messages/g
|
|
1708
|
+
],
|
|
1709
|
+
sendgrid: [
|
|
1710
|
+
/sgMail\.send\s*\(/g,
|
|
1711
|
+
/\.send\s*\(\s*msg/g
|
|
1712
|
+
],
|
|
1713
|
+
"mongodb-atlas": [
|
|
1714
|
+
/\.find\s*\(/g,
|
|
1715
|
+
/\.insertOne\s*\(/g,
|
|
1716
|
+
/\.insertMany\s*\(/g,
|
|
1717
|
+
/\.updateOne\s*\(/g,
|
|
1718
|
+
/\.aggregate\s*\(/g
|
|
1719
|
+
],
|
|
1720
|
+
clerk: [
|
|
1721
|
+
/clerkClient\.\w+/g,
|
|
1722
|
+
/auth\(\)/g
|
|
1723
|
+
],
|
|
1724
|
+
replicate: [
|
|
1725
|
+
/replicate\.run\s*\(/g,
|
|
1726
|
+
/replicate\.predictions\.create\s*\(/g
|
|
1727
|
+
]
|
|
1728
|
+
};
|
|
1729
|
+
function detectMultipliers(content) {
|
|
1730
|
+
const multipliers = [];
|
|
1731
|
+
if (/for\s*\(.*;\s*\w+\s*<\s*(\w+)/g.test(content)) {
|
|
1732
|
+
const loopMatch = content.match(/for\s*\(.*;\s*\w+\s*<\s*(\d+)/);
|
|
1733
|
+
if (loopMatch) {
|
|
1734
|
+
const bound = parseInt(loopMatch[1]);
|
|
1735
|
+
if (bound > 1) {
|
|
1736
|
+
multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });
|
|
1737
|
+
}
|
|
1738
|
+
} else {
|
|
1739
|
+
multipliers.push({ label: "for loop (variable bound)", factor: 10 });
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (/\.\s*map\s*\(\s*(async\s*)?\(/g.test(content)) {
|
|
1743
|
+
multipliers.push({ label: ".map() iteration", factor: 10 });
|
|
1744
|
+
}
|
|
1745
|
+
if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
|
|
1746
|
+
multipliers.push({ label: ".forEach() iteration", factor: 10 });
|
|
1747
|
+
}
|
|
1748
|
+
if (/for\s*\(\s*(const|let|var)\s+\w+\s+(of|in)\s+/g.test(content)) {
|
|
1749
|
+
multipliers.push({ label: "for...of/in loop", factor: 10 });
|
|
1750
|
+
}
|
|
1751
|
+
if (/Promise\.all\s*\(/g.test(content)) {
|
|
1752
|
+
multipliers.push({ label: "Promise.all (parallel batch)", factor: 10 });
|
|
1753
|
+
}
|
|
1754
|
+
if (/cron|schedule|interval|setInterval|every\s+\d+\s*(min|hour|day|sec)/gi.test(content)) {
|
|
1755
|
+
if (/every\s+5\s*min/gi.test(content) || /\*\/5\s+\*\s+\*/g.test(content)) {
|
|
1756
|
+
multipliers.push({ label: "cron: every 5 minutes", factor: 8640 });
|
|
1757
|
+
} else if (/every\s+1?\s*hour/gi.test(content) || /0\s+\*\s+\*\s+\*/g.test(content)) {
|
|
1758
|
+
multipliers.push({ label: "cron: hourly", factor: 720 });
|
|
1759
|
+
} else if (/every\s+1?\s*day/gi.test(content) || /0\s+0\s+\*\s+\*/g.test(content)) {
|
|
1760
|
+
multipliers.push({ label: "cron: daily", factor: 30 });
|
|
1761
|
+
} else {
|
|
1762
|
+
multipliers.push({ label: "scheduled execution", factor: 30 });
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const batchMatch = content.match(/batch[_\s]?size\s*[=:]\s*(\d+)/i);
|
|
1766
|
+
if (batchMatch) {
|
|
1767
|
+
const batchSize = parseInt(batchMatch[1]);
|
|
1768
|
+
if (batchSize > 1) {
|
|
1769
|
+
multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return multipliers;
|
|
1773
|
+
}
|
|
1774
|
+
var GOTCHA_MULTIPLIERS = {
|
|
1775
|
+
scrapfly: {
|
|
1776
|
+
low: 1,
|
|
1777
|
+
high: 25,
|
|
1778
|
+
explanation: "anti-bot bypass consumes 5-25x base credits"
|
|
1779
|
+
},
|
|
1780
|
+
browserbase: {
|
|
1781
|
+
low: 1,
|
|
1782
|
+
high: 5,
|
|
1783
|
+
explanation: "session duration affects cost \u2014 long sessions burn more"
|
|
1784
|
+
},
|
|
1785
|
+
anthropic: {
|
|
1786
|
+
low: 1,
|
|
1787
|
+
high: 60,
|
|
1788
|
+
explanation: "Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)"
|
|
1789
|
+
},
|
|
1790
|
+
openai: {
|
|
1791
|
+
low: 1,
|
|
1792
|
+
high: 30,
|
|
1793
|
+
explanation: "GPT-4 mini vs GPT-5 (30x cost range)"
|
|
1794
|
+
},
|
|
1795
|
+
stripe: {
|
|
1796
|
+
low: 1,
|
|
1797
|
+
high: 1.5,
|
|
1798
|
+
explanation: "international cards add 1-1.5% extra"
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
function analyzeCostImpact(filePath, content, projectRoot) {
|
|
1802
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {
|
|
1803
|
+
return [];
|
|
1804
|
+
}
|
|
1805
|
+
const registry = loadRegistry(projectRoot);
|
|
1806
|
+
const impacts = [];
|
|
1807
|
+
const multipliers = detectMultipliers(content);
|
|
1808
|
+
for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {
|
|
1809
|
+
let totalCalls = 0;
|
|
1810
|
+
for (const pattern of patterns) {
|
|
1811
|
+
pattern.lastIndex = 0;
|
|
1812
|
+
const matches = content.match(pattern);
|
|
1813
|
+
if (matches) {
|
|
1814
|
+
totalCalls += matches.length;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (totalCalls === 0) continue;
|
|
1818
|
+
const service = registry.get(serviceId);
|
|
1819
|
+
if (!service) continue;
|
|
1820
|
+
const multiplierFactor = multipliers.length > 0 ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1) : 1;
|
|
1821
|
+
const baseMonthlyRuns = multipliers.some((m) => m.label.startsWith("cron")) ? 1 : 50;
|
|
1822
|
+
const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;
|
|
1823
|
+
const gotcha = GOTCHA_MULTIPLIERS[serviceId];
|
|
1824
|
+
const unitRate = service.pricing?.unitRate ?? 0;
|
|
1825
|
+
let costLow;
|
|
1826
|
+
let costHigh;
|
|
1827
|
+
if (unitRate > 0) {
|
|
1828
|
+
costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
|
|
1829
|
+
costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
|
|
1830
|
+
} else if (service.pricing?.monthlyBase !== void 0) {
|
|
1831
|
+
costLow = 0;
|
|
1832
|
+
costHigh = 0;
|
|
1833
|
+
} else {
|
|
1834
|
+
const typicalCallCosts = {
|
|
1835
|
+
anthropic: 3e-3,
|
|
1836
|
+
// ~$3/MTok * ~1K tokens average
|
|
1837
|
+
openai: 2e-3,
|
|
1838
|
+
"google-gemini": 1e-3,
|
|
1839
|
+
scrapfly: 15e-5,
|
|
1840
|
+
browserbase: 0.01,
|
|
1841
|
+
resend: 1e-3,
|
|
1842
|
+
stripe: 0.3
|
|
1843
|
+
};
|
|
1844
|
+
const perCall = typicalCallCosts[serviceId] ?? 1e-3;
|
|
1845
|
+
costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
|
|
1846
|
+
costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
|
|
1847
|
+
}
|
|
1848
|
+
if (costLow === 0 && costHigh === 0) continue;
|
|
1849
|
+
impacts.push({
|
|
1850
|
+
serviceId,
|
|
1851
|
+
serviceName: service.name,
|
|
1852
|
+
filePath,
|
|
1853
|
+
callCount: totalCalls,
|
|
1854
|
+
multipliers: multipliers.map((m) => m.label),
|
|
1855
|
+
multiplierFactor,
|
|
1856
|
+
monthlyInvocations,
|
|
1857
|
+
costLow,
|
|
1858
|
+
costHigh,
|
|
1859
|
+
rangeExplanation: gotcha?.explanation
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
return impacts;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/utilization.ts
|
|
1866
|
+
function utilizationModelPath(projectRoot) {
|
|
1867
|
+
return path6.join(projectDataDir(projectRoot), "utilization.json");
|
|
1868
|
+
}
|
|
1869
|
+
function readUtilizationModel(projectRoot) {
|
|
1870
|
+
try {
|
|
1871
|
+
const raw = fs6.readFileSync(utilizationModelPath(projectRoot), "utf-8");
|
|
1872
|
+
return JSON.parse(raw);
|
|
1873
|
+
} catch {
|
|
1874
|
+
return {
|
|
1875
|
+
version: 1,
|
|
1876
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1877
|
+
lastFullScan: null,
|
|
1878
|
+
services: {}
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function writeUtilizationModel(model, projectRoot) {
|
|
1883
|
+
const dir = projectDataDir(projectRoot);
|
|
1884
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
1885
|
+
model.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1886
|
+
fs6.writeFileSync(
|
|
1887
|
+
utilizationModelPath(projectRoot),
|
|
1888
|
+
JSON.stringify(model, null, 2) + "\n",
|
|
1889
|
+
"utf-8"
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
function analyzeFileUtilization(filePath, content, projectRoot) {
|
|
1893
|
+
const impacts = analyzeCostImpact(filePath, content, projectRoot);
|
|
1894
|
+
return impacts.map((impact) => ({
|
|
1895
|
+
filePath,
|
|
1896
|
+
serviceId: impact.serviceId,
|
|
1897
|
+
callCount: impact.callCount,
|
|
1898
|
+
multipliers: impact.multipliers,
|
|
1899
|
+
multiplierFactor: impact.multiplierFactor,
|
|
1900
|
+
monthlyInvocations: impact.monthlyInvocations,
|
|
1901
|
+
costLow: impact.costLow,
|
|
1902
|
+
costHigh: impact.costHigh
|
|
1903
|
+
}));
|
|
1904
|
+
}
|
|
1905
|
+
function updateUtilizationModel(model, filePath, newCallSites, projectRoot) {
|
|
1906
|
+
const registry = loadRegistry(projectRoot);
|
|
1907
|
+
const affectedServices = /* @__PURE__ */ new Set();
|
|
1908
|
+
for (const [serviceId, svc] of Object.entries(model.services)) {
|
|
1909
|
+
const before = svc.callSites.length;
|
|
1910
|
+
svc.callSites = svc.callSites.filter((cs) => cs.filePath !== filePath);
|
|
1911
|
+
if (svc.callSites.length !== before) {
|
|
1912
|
+
affectedServices.add(serviceId);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
for (const cs of newCallSites) {
|
|
1916
|
+
affectedServices.add(cs.serviceId);
|
|
1917
|
+
if (!model.services[cs.serviceId]) {
|
|
1918
|
+
const def = registry.get(cs.serviceId);
|
|
1919
|
+
model.services[cs.serviceId] = {
|
|
1920
|
+
serviceId: cs.serviceId,
|
|
1921
|
+
serviceName: def?.name ?? cs.serviceId,
|
|
1922
|
+
callSites: [],
|
|
1923
|
+
totalMonthlyUnits: 0,
|
|
1924
|
+
unitName: def?.pricing?.unitName ?? "API calls",
|
|
1925
|
+
planIncluded: null,
|
|
1926
|
+
projectedOverage: 0,
|
|
1927
|
+
projectedOverageCost: 0,
|
|
1928
|
+
projectedTotalCost: 0,
|
|
1929
|
+
planBaseCost: def?.pricing?.monthlyBase ?? 0,
|
|
1930
|
+
unitRate: def?.pricing?.unitRate ?? 0
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
model.services[cs.serviceId].callSites.push(cs);
|
|
1934
|
+
}
|
|
1935
|
+
for (const serviceId of affectedServices) {
|
|
1936
|
+
recalculateServiceTotals(model, serviceId);
|
|
1937
|
+
}
|
|
1938
|
+
for (const serviceId of Object.keys(model.services)) {
|
|
1939
|
+
if (model.services[serviceId].callSites.length === 0) {
|
|
1940
|
+
delete model.services[serviceId];
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
function recalculateServiceTotals(model, serviceId) {
|
|
1945
|
+
const svc = model.services[serviceId];
|
|
1946
|
+
if (!svc) return;
|
|
1947
|
+
svc.totalMonthlyUnits = svc.callSites.reduce(
|
|
1948
|
+
(sum, cs) => sum + cs.monthlyInvocations,
|
|
1949
|
+
0
|
|
1950
|
+
);
|
|
1951
|
+
const included = svc.planIncluded ?? 0;
|
|
1952
|
+
svc.projectedOverage = Math.max(0, svc.totalMonthlyUnits - included);
|
|
1953
|
+
svc.projectedOverageCost = svc.projectedOverage * svc.unitRate;
|
|
1954
|
+
svc.projectedTotalCost = svc.planBaseCost + svc.projectedOverageCost;
|
|
1955
|
+
}
|
|
1956
|
+
var CODE_DIRS = ["src", "app", "lib", "pages", "components", "utils", "services", "hooks", "api", "functions"];
|
|
1957
|
+
function buildUtilizationModel(projectRoot) {
|
|
1958
|
+
const model = {
|
|
1959
|
+
version: 1,
|
|
1960
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1961
|
+
lastFullScan: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1962
|
+
services: {}
|
|
1963
|
+
};
|
|
1964
|
+
const files = findSourceFiles(projectRoot);
|
|
1965
|
+
for (const file of files) {
|
|
1966
|
+
try {
|
|
1967
|
+
const content = fs6.readFileSync(file, "utf-8");
|
|
1968
|
+
const callSites = analyzeFileUtilization(file, content, projectRoot);
|
|
1969
|
+
if (callSites.length > 0) {
|
|
1970
|
+
const relPath = path6.relative(projectRoot, file);
|
|
1971
|
+
const relativeSites = callSites.map((cs) => ({
|
|
1972
|
+
...cs,
|
|
1973
|
+
filePath: relPath
|
|
1974
|
+
}));
|
|
1975
|
+
updateUtilizationModel(model, relPath, relativeSites, projectRoot);
|
|
1976
|
+
}
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return model;
|
|
1981
|
+
}
|
|
1982
|
+
function findSourceFiles(projectRoot) {
|
|
1983
|
+
const files = [];
|
|
1984
|
+
const dirsToScan = [];
|
|
1985
|
+
for (const dir of CODE_DIRS) {
|
|
1986
|
+
const fullPath = path6.join(projectRoot, dir);
|
|
1987
|
+
if (fs6.existsSync(fullPath)) {
|
|
1988
|
+
dirsToScan.push(fullPath);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
const entries = fs6.readdirSync(projectRoot, { withFileTypes: true });
|
|
1993
|
+
for (const entry of entries) {
|
|
1994
|
+
if (!entry.isDirectory()) continue;
|
|
1995
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name.startsWith(".")) continue;
|
|
1996
|
+
const subPkgPath = path6.join(projectRoot, entry.name, "package.json");
|
|
1997
|
+
if (fs6.existsSync(subPkgPath)) {
|
|
1998
|
+
for (const dir of CODE_DIRS) {
|
|
1999
|
+
const fullPath = path6.join(projectRoot, entry.name, dir);
|
|
2000
|
+
if (fs6.existsSync(fullPath)) {
|
|
2001
|
+
dirsToScan.push(fullPath);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
} catch {
|
|
2007
|
+
}
|
|
2008
|
+
for (const dir of dirsToScan) {
|
|
2009
|
+
walkDir2(dir, /\.(ts|tsx|js|jsx|mjs|cjs)$/, files);
|
|
2010
|
+
}
|
|
2011
|
+
return files;
|
|
2012
|
+
}
|
|
2013
|
+
function walkDir2(dir, pattern, results, maxDepth = 5) {
|
|
2014
|
+
if (maxDepth <= 0) return;
|
|
2015
|
+
try {
|
|
2016
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
2017
|
+
for (const entry of entries) {
|
|
2018
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2019
|
+
const fullPath = path6.join(dir, entry.name);
|
|
2020
|
+
if (entry.isDirectory()) {
|
|
2021
|
+
walkDir2(fullPath, pattern, results, maxDepth - 1);
|
|
2022
|
+
} else if (pattern.test(entry.name)) {
|
|
2023
|
+
results.push(fullPath);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
} catch {
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
function formatUtilizationSummary(model) {
|
|
2030
|
+
const services = Object.values(model.services).filter(
|
|
2031
|
+
(s) => s.totalMonthlyUnits > 0
|
|
2032
|
+
);
|
|
2033
|
+
if (services.length === 0) {
|
|
2034
|
+
return "\u{1F4CA} No utilization-tracked call sites found in the project.";
|
|
2035
|
+
}
|
|
2036
|
+
const lines = [];
|
|
2037
|
+
lines.push("\u{1F4CA} Utilization scan complete\n");
|
|
2038
|
+
lines.push("| Service | Call Sites | Monthly Units | Plan Included | Overage Cost |");
|
|
2039
|
+
lines.push("|---------|-----------|---------------|---------------|-------------|");
|
|
2040
|
+
for (const svc of services) {
|
|
2041
|
+
const fileCount = new Set(svc.callSites.map((cs) => cs.filePath)).size;
|
|
2042
|
+
const filesStr = `${fileCount} file${fileCount > 1 ? "s" : ""}`;
|
|
2043
|
+
const unitsStr = `${formatCompact2(svc.totalMonthlyUnits)} ${svc.unitName}`;
|
|
2044
|
+
const includedStr = svc.planIncluded !== null ? formatCompact2(svc.planIncluded) : "\u2014";
|
|
2045
|
+
const overageStr = svc.projectedOverageCost > 0 ? `~$${svc.projectedOverageCost.toFixed(2)}/mo` : "$0";
|
|
2046
|
+
lines.push(
|
|
2047
|
+
`| ${svc.serviceName} | ${filesStr} | ${unitsStr} | ${includedStr} | ${overageStr} |`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
return lines.join("\n");
|
|
2051
|
+
}
|
|
2052
|
+
function formatCompact2(n) {
|
|
2053
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
|
|
2054
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1)}K`;
|
|
2055
|
+
return String(Math.round(n));
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1595
2058
|
// src/cli.ts
|
|
1596
2059
|
var args = process.argv.slice(2);
|
|
1597
2060
|
var command = args[0];
|
|
@@ -1620,6 +2083,9 @@ async function main() {
|
|
|
1620
2083
|
case "configure":
|
|
1621
2084
|
await cmdConfigure();
|
|
1622
2085
|
break;
|
|
2086
|
+
case "scan":
|
|
2087
|
+
cmdScan();
|
|
2088
|
+
break;
|
|
1623
2089
|
case "reset":
|
|
1624
2090
|
cmdReset();
|
|
1625
2091
|
break;
|
|
@@ -1646,10 +2112,10 @@ async function cmdInit() {
|
|
|
1646
2112
|
const projectRoot = process.cwd();
|
|
1647
2113
|
const nonInteractive = flags.has("--non-interactive") || flags.has("--ni");
|
|
1648
2114
|
const alreadyInitialized = isInitialized(projectRoot);
|
|
1649
|
-
let projectName =
|
|
2115
|
+
let projectName = path7.basename(projectRoot);
|
|
1650
2116
|
try {
|
|
1651
|
-
const pkgPath =
|
|
1652
|
-
const pkg = JSON.parse(
|
|
2117
|
+
const pkgPath = path7.join(projectRoot, "package.json");
|
|
2118
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
1653
2119
|
if (pkg.name) projectName = pkg.name;
|
|
1654
2120
|
} catch {
|
|
1655
2121
|
}
|
|
@@ -1688,8 +2154,8 @@ async function cmdInit() {
|
|
|
1688
2154
|
config.services = result.services;
|
|
1689
2155
|
}
|
|
1690
2156
|
writeProjectConfig(config, projectRoot);
|
|
1691
|
-
const gitignorePath =
|
|
1692
|
-
|
|
2157
|
+
const gitignorePath = path7.join(projectConfigDir(projectRoot), ".gitignore");
|
|
2158
|
+
fs7.writeFileSync(
|
|
1693
2159
|
gitignorePath,
|
|
1694
2160
|
[
|
|
1695
2161
|
"# Burnwatch \u2014 ignore cache and snapshots, keep ledger and config",
|
|
@@ -1700,7 +2166,21 @@ async function cmdInit() {
|
|
|
1700
2166
|
].join("\n"),
|
|
1701
2167
|
"utf-8"
|
|
1702
2168
|
);
|
|
1703
|
-
console.log("\
|
|
2169
|
+
console.log("\u{1F4CA} Scanning for utilization patterns...");
|
|
2170
|
+
try {
|
|
2171
|
+
const model = buildUtilizationModel(projectRoot);
|
|
2172
|
+
writeUtilizationModel(model, projectRoot);
|
|
2173
|
+
const serviceCount = Object.keys(model.services).length;
|
|
2174
|
+
if (serviceCount > 0) {
|
|
2175
|
+
console.log(` Found call sites for ${serviceCount} service(s).
|
|
2176
|
+
`);
|
|
2177
|
+
} else {
|
|
2178
|
+
console.log(" No SDK call sites detected yet.\n");
|
|
2179
|
+
}
|
|
2180
|
+
} catch {
|
|
2181
|
+
console.log(" Utilization scan skipped.\n");
|
|
2182
|
+
}
|
|
2183
|
+
console.log("\u{1F517} Registering Claude Code hooks...\n");
|
|
1704
2184
|
registerHooks(projectRoot);
|
|
1705
2185
|
console.log("\nburnwatch initialized.\n");
|
|
1706
2186
|
if (process.stdin.isTTY) {
|
|
@@ -1719,10 +2199,10 @@ async function cmdInterview() {
|
|
|
1719
2199
|
if (!isInitialized(projectRoot)) {
|
|
1720
2200
|
ensureProjectDirs(projectRoot);
|
|
1721
2201
|
const detected = detectServices(projectRoot);
|
|
1722
|
-
let projectName =
|
|
2202
|
+
let projectName = path7.basename(projectRoot);
|
|
1723
2203
|
try {
|
|
1724
2204
|
const pkg = JSON.parse(
|
|
1725
|
-
|
|
2205
|
+
fs7.readFileSync(path7.join(projectRoot, "package.json"), "utf-8")
|
|
1726
2206
|
);
|
|
1727
2207
|
if (pkg.name) projectName = pkg.name;
|
|
1728
2208
|
} catch {
|
|
@@ -1754,20 +2234,19 @@ async function cmdInterview() {
|
|
|
1754
2234
|
if (!keySource && !globalKey) keySource = `env:${pattern}`;
|
|
1755
2235
|
}
|
|
1756
2236
|
}
|
|
1757
|
-
const envFiles =
|
|
1758
|
-
for (const
|
|
2237
|
+
const envFiles = findEnvFiles(projectRoot, 3);
|
|
2238
|
+
for (const envFilePath of envFiles) {
|
|
1759
2239
|
try {
|
|
1760
|
-
const
|
|
1761
|
-
const
|
|
2240
|
+
const envContent = fs7.readFileSync(envFilePath, "utf-8");
|
|
2241
|
+
const envKeys = parseEnvKeys(envContent);
|
|
2242
|
+
const envFileName = path7.relative(projectRoot, envFilePath);
|
|
1762
2243
|
for (const pattern of definition.envPatterns) {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
if (match?.[1]) {
|
|
1766
|
-
const label = `${pattern} (in ${envFile})`;
|
|
2244
|
+
if (envKeys.has(pattern)) {
|
|
2245
|
+
const label = `${pattern} (in ${envFileName})`;
|
|
1767
2246
|
if (!envKeysFound.some((k) => k.startsWith(pattern))) {
|
|
1768
2247
|
envKeysFound.push(label);
|
|
1769
2248
|
}
|
|
1770
|
-
if (!keySource && !globalKey) keySource = `file:${
|
|
2249
|
+
if (!keySource && !globalKey) keySource = `file:${envFileName}:${pattern}`;
|
|
1771
2250
|
}
|
|
1772
2251
|
}
|
|
1773
2252
|
} catch {
|
|
@@ -1783,8 +2262,9 @@ async function cmdInterview() {
|
|
|
1783
2262
|
const envFile = parts[1];
|
|
1784
2263
|
const envVar = parts[2];
|
|
1785
2264
|
try {
|
|
1786
|
-
const envContent =
|
|
1787
|
-
const
|
|
2265
|
+
const envContent = fs7.readFileSync(path7.join(projectRoot, envFile), "utf-8");
|
|
2266
|
+
const regex = new RegExp(`^(?:export\\s+)?${envVar}\\s*=\\s*(.+)$`, "m");
|
|
2267
|
+
const match = envContent.match(regex);
|
|
1788
2268
|
if (match?.[1]) apiKey = match[1].trim().replace(/^["']|["']$/g, "");
|
|
1789
2269
|
} catch {
|
|
1790
2270
|
}
|
|
@@ -1866,6 +2346,19 @@ async function cmdInterview() {
|
|
|
1866
2346
|
serviceStates.sort(
|
|
1867
2347
|
(a, b) => riskOrder.indexOf(a.riskCategory) - riskOrder.indexOf(b.riskCategory)
|
|
1868
2348
|
);
|
|
2349
|
+
const utilizationModel = readUtilizationModel(projectRoot);
|
|
2350
|
+
const utilizationSummary = {};
|
|
2351
|
+
for (const [serviceId, svc] of Object.entries(utilizationModel.services)) {
|
|
2352
|
+
if (svc.totalMonthlyUnits > 0) {
|
|
2353
|
+
utilizationSummary[serviceId] = {
|
|
2354
|
+
totalMonthlyUnits: svc.totalMonthlyUnits,
|
|
2355
|
+
unitName: svc.unitName,
|
|
2356
|
+
planIncluded: svc.planIncluded,
|
|
2357
|
+
projectedOverageCost: svc.projectedOverageCost,
|
|
2358
|
+
topCallSites: svc.callSites.sort((a, b) => b.monthlyInvocations - a.monthlyInvocations).slice(0, 5).map((cs) => `${cs.filePath} (${cs.monthlyInvocations})`)
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
1869
2362
|
const output = {
|
|
1870
2363
|
projectName: config.projectName,
|
|
1871
2364
|
serviceCount: serviceStates.length,
|
|
@@ -1874,11 +2367,13 @@ async function cmdInterview() {
|
|
|
1874
2367
|
blindCount: serviceStates.filter((s) => s.tier === "blind").length,
|
|
1875
2368
|
canGoLiveCount: serviceStates.filter((s) => s.canGoLive).length,
|
|
1876
2369
|
keysFoundInEnv: serviceStates.filter((s) => s.envKeysFound.length > 0).length,
|
|
2370
|
+
utilization: utilizationSummary,
|
|
1877
2371
|
services: serviceStates,
|
|
1878
2372
|
instructions: {
|
|
1879
2373
|
keyStorage: "burnwatch stores API keys in ~/.config/burnwatch/ (chmod 600) \u2014 never in the project directory. Use --key flag with configure to save keys securely.",
|
|
1880
2374
|
liveTracking: "Services with hasConnector=true can do LIVE billing tracking when given an API key. Services without a connector can only do CALC (budget threshold alerts).",
|
|
1881
|
-
configureCommand: "burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]"
|
|
2375
|
+
configureCommand: "burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]",
|
|
2376
|
+
utilizationNote: "The 'utilization' field shows code-projected usage from SDK call sites. Services with high projectedOverageCost may need plan upgrades or budget alerts."
|
|
1882
2377
|
}
|
|
1883
2378
|
};
|
|
1884
2379
|
if (flags.has("--json")) {
|
|
@@ -2182,28 +2677,74 @@ async function cmdReconcile() {
|
|
|
2182
2677
|
}
|
|
2183
2678
|
console.log("");
|
|
2184
2679
|
}
|
|
2680
|
+
function cmdScan() {
|
|
2681
|
+
const projectRoot = process.cwd();
|
|
2682
|
+
if (!isInitialized(projectRoot)) {
|
|
2683
|
+
console.error("burnwatch is not initialized. Run 'burnwatch init' first.");
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
console.log("\u{1F4CA} Scanning project for utilization patterns...\n");
|
|
2688
|
+
const model = buildUtilizationModel(projectRoot);
|
|
2689
|
+
const config = readProjectConfig(projectRoot);
|
|
2690
|
+
for (const [serviceId, svc] of Object.entries(model.services)) {
|
|
2691
|
+
const tracked = config.services[serviceId];
|
|
2692
|
+
const definition = getService(serviceId, projectRoot);
|
|
2693
|
+
if (tracked?.allowance) {
|
|
2694
|
+
svc.planIncluded = tracked.allowance.included;
|
|
2695
|
+
svc.unitName = tracked.allowance.unitName;
|
|
2696
|
+
}
|
|
2697
|
+
if (definition?.pricing?.unitRate) {
|
|
2698
|
+
svc.unitRate = definition.pricing.unitRate;
|
|
2699
|
+
}
|
|
2700
|
+
if (tracked?.planCost !== void 0) {
|
|
2701
|
+
svc.planBaseCost = tracked.planCost;
|
|
2702
|
+
}
|
|
2703
|
+
svc.projectedOverage = Math.max(0, svc.totalMonthlyUnits - (svc.planIncluded ?? 0));
|
|
2704
|
+
svc.projectedOverageCost = svc.projectedOverage * svc.unitRate;
|
|
2705
|
+
svc.projectedTotalCost = svc.planBaseCost + svc.projectedOverageCost;
|
|
2706
|
+
}
|
|
2707
|
+
writeUtilizationModel(model, projectRoot);
|
|
2708
|
+
console.log(formatUtilizationSummary(model));
|
|
2709
|
+
if (flags.has("--verbose")) {
|
|
2710
|
+
console.log("\nDetailed call sites:\n");
|
|
2711
|
+
for (const svc of Object.values(model.services)) {
|
|
2712
|
+
if (svc.callSites.length === 0) continue;
|
|
2713
|
+
console.log(` ${svc.serviceName}:`);
|
|
2714
|
+
for (const cs of svc.callSites) {
|
|
2715
|
+
console.log(
|
|
2716
|
+
` ${cs.filePath} \u2014 ${cs.callCount} call(s) \xD7 ${cs.multiplierFactor}x \u2192 ~${cs.monthlyInvocations} invocations/mo`
|
|
2717
|
+
);
|
|
2718
|
+
if (cs.multipliers.length > 0) {
|
|
2719
|
+
console.log(` Multipliers: ${cs.multipliers.join(", ")}`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
console.log("");
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2185
2726
|
function cmdReset() {
|
|
2186
2727
|
const projectRoot = process.cwd();
|
|
2187
|
-
const burnwatchDir =
|
|
2188
|
-
const claudeSkillsDir =
|
|
2189
|
-
if (!
|
|
2728
|
+
const burnwatchDir = path7.join(projectRoot, ".burnwatch");
|
|
2729
|
+
const claudeSkillsDir = path7.join(projectRoot, ".claude", "skills");
|
|
2730
|
+
if (!fs7.existsSync(burnwatchDir)) {
|
|
2190
2731
|
console.log("burnwatch is not initialized in this project.");
|
|
2191
2732
|
return;
|
|
2192
2733
|
}
|
|
2193
|
-
|
|
2734
|
+
fs7.rmSync(burnwatchDir, { recursive: true, force: true });
|
|
2194
2735
|
console.log(`\u{1F5D1}\uFE0F Removed ${burnwatchDir}`);
|
|
2195
2736
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2196
2737
|
for (const skill of skillNames) {
|
|
2197
|
-
const skillDir =
|
|
2198
|
-
if (
|
|
2199
|
-
|
|
2738
|
+
const skillDir = path7.join(claudeSkillsDir, skill);
|
|
2739
|
+
if (fs7.existsSync(skillDir)) {
|
|
2740
|
+
fs7.rmSync(skillDir, { recursive: true, force: true });
|
|
2200
2741
|
}
|
|
2201
2742
|
}
|
|
2202
2743
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch skills from .claude/skills/");
|
|
2203
|
-
const settingsPath =
|
|
2204
|
-
if (
|
|
2744
|
+
const settingsPath = path7.join(projectRoot, ".claude", "settings.json");
|
|
2745
|
+
if (fs7.existsSync(settingsPath)) {
|
|
2205
2746
|
try {
|
|
2206
|
-
const settings = JSON.parse(
|
|
2747
|
+
const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
2207
2748
|
const hooks = settings["hooks"];
|
|
2208
2749
|
if (hooks) {
|
|
2209
2750
|
for (const [event, hookList] of Object.entries(hooks)) {
|
|
@@ -2213,7 +2754,7 @@ function cmdReset() {
|
|
|
2213
2754
|
if (hooks[event].length === 0) delete hooks[event];
|
|
2214
2755
|
}
|
|
2215
2756
|
if (Object.keys(hooks).length === 0) delete settings["hooks"];
|
|
2216
|
-
|
|
2757
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2217
2758
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch hooks from .claude/settings.json");
|
|
2218
2759
|
}
|
|
2219
2760
|
} catch {
|
|
@@ -2237,6 +2778,7 @@ Usage:
|
|
|
2237
2778
|
burnwatch reconcile Scan for untracked services
|
|
2238
2779
|
burnwatch interview --json Export state for agent-driven interview
|
|
2239
2780
|
burnwatch configure --service <id> [opts] Agent writes back interview answers
|
|
2781
|
+
burnwatch scan [--verbose] Scan project for utilization patterns
|
|
2240
2782
|
burnwatch reset Remove all burnwatch config from this project
|
|
2241
2783
|
|
|
2242
2784
|
Options for 'configure':
|
|
@@ -2263,23 +2805,23 @@ Examples:
|
|
|
2263
2805
|
}
|
|
2264
2806
|
function cmdVersion() {
|
|
2265
2807
|
try {
|
|
2266
|
-
const pkgPath =
|
|
2267
|
-
|
|
2808
|
+
const pkgPath = path7.resolve(
|
|
2809
|
+
path7.dirname(new URL(import.meta.url).pathname),
|
|
2268
2810
|
"../package.json"
|
|
2269
2811
|
);
|
|
2270
|
-
const pkg = JSON.parse(
|
|
2812
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
2271
2813
|
console.log(`burnwatch v${pkg.version}`);
|
|
2272
2814
|
} catch {
|
|
2273
2815
|
console.log("burnwatch v0.1.0");
|
|
2274
2816
|
}
|
|
2275
2817
|
}
|
|
2276
2818
|
function registerHooks(projectRoot) {
|
|
2277
|
-
const sourceHooksDir =
|
|
2278
|
-
|
|
2819
|
+
const sourceHooksDir = path7.resolve(
|
|
2820
|
+
path7.dirname(new URL(import.meta.url).pathname),
|
|
2279
2821
|
"hooks"
|
|
2280
2822
|
);
|
|
2281
|
-
const localHooksDir =
|
|
2282
|
-
|
|
2823
|
+
const localHooksDir = path7.join(projectRoot, ".burnwatch", "hooks");
|
|
2824
|
+
fs7.mkdirSync(localHooksDir, { recursive: true });
|
|
2283
2825
|
const hookFiles = [
|
|
2284
2826
|
"on-session-start.js",
|
|
2285
2827
|
"on-prompt.js",
|
|
@@ -2287,45 +2829,45 @@ function registerHooks(projectRoot) {
|
|
|
2287
2829
|
"on-stop.js"
|
|
2288
2830
|
];
|
|
2289
2831
|
for (const file of hookFiles) {
|
|
2290
|
-
const src =
|
|
2291
|
-
const dest =
|
|
2832
|
+
const src = path7.join(sourceHooksDir, file);
|
|
2833
|
+
const dest = path7.join(localHooksDir, file);
|
|
2292
2834
|
try {
|
|
2293
|
-
|
|
2835
|
+
fs7.copyFileSync(src, dest);
|
|
2294
2836
|
const mapSrc = src + ".map";
|
|
2295
|
-
if (
|
|
2296
|
-
|
|
2837
|
+
if (fs7.existsSync(mapSrc)) {
|
|
2838
|
+
fs7.copyFileSync(mapSrc, dest + ".map");
|
|
2297
2839
|
}
|
|
2298
2840
|
} catch (err) {
|
|
2299
2841
|
console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);
|
|
2300
2842
|
}
|
|
2301
2843
|
}
|
|
2302
2844
|
console.log(` Hook scripts copied to ${localHooksDir}`);
|
|
2303
|
-
const sourceSkillsDir =
|
|
2304
|
-
|
|
2845
|
+
const sourceSkillsDir = path7.resolve(
|
|
2846
|
+
path7.dirname(new URL(import.meta.url).pathname),
|
|
2305
2847
|
"../skills"
|
|
2306
2848
|
);
|
|
2307
2849
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2308
|
-
const claudeSkillsDir =
|
|
2850
|
+
const claudeSkillsDir = path7.join(projectRoot, ".claude", "skills");
|
|
2309
2851
|
for (const skillName of skillNames) {
|
|
2310
|
-
const srcSkill =
|
|
2311
|
-
const destDir =
|
|
2312
|
-
const destSkill =
|
|
2852
|
+
const srcSkill = path7.join(sourceSkillsDir, skillName, "SKILL.md");
|
|
2853
|
+
const destDir = path7.join(claudeSkillsDir, skillName);
|
|
2854
|
+
const destSkill = path7.join(destDir, "SKILL.md");
|
|
2313
2855
|
try {
|
|
2314
|
-
if (
|
|
2315
|
-
|
|
2316
|
-
|
|
2856
|
+
if (fs7.existsSync(srcSkill)) {
|
|
2857
|
+
fs7.mkdirSync(destDir, { recursive: true });
|
|
2858
|
+
fs7.copyFileSync(srcSkill, destSkill);
|
|
2317
2859
|
}
|
|
2318
2860
|
} catch (err) {
|
|
2319
2861
|
console.error(` Warning: Could not copy skill ${skillName}: ${err instanceof Error ? err.message : err}`);
|
|
2320
2862
|
}
|
|
2321
2863
|
}
|
|
2322
2864
|
console.log(` Skills installed to ${claudeSkillsDir}`);
|
|
2323
|
-
const claudeDir =
|
|
2324
|
-
const settingsPath =
|
|
2325
|
-
|
|
2865
|
+
const claudeDir = path7.join(projectRoot, ".claude");
|
|
2866
|
+
const settingsPath = path7.join(claudeDir, "settings.json");
|
|
2867
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
2326
2868
|
let settings = {};
|
|
2327
2869
|
try {
|
|
2328
|
-
const existing =
|
|
2870
|
+
const existing = fs7.readFileSync(settingsPath, "utf-8");
|
|
2329
2871
|
settings = JSON.parse(existing);
|
|
2330
2872
|
console.log(` Merging into existing ${settingsPath}`);
|
|
2331
2873
|
} catch {
|
|
@@ -2341,7 +2883,7 @@ function registerHooks(projectRoot) {
|
|
|
2341
2883
|
hooks: [
|
|
2342
2884
|
{
|
|
2343
2885
|
type: "command",
|
|
2344
|
-
command: `node "${
|
|
2886
|
+
command: `node "${path7.join(hooksDir, "on-session-start.js")}"`,
|
|
2345
2887
|
timeout: 15
|
|
2346
2888
|
}
|
|
2347
2889
|
]
|
|
@@ -2354,7 +2896,7 @@ function registerHooks(projectRoot) {
|
|
|
2354
2896
|
hooks: [
|
|
2355
2897
|
{
|
|
2356
2898
|
type: "command",
|
|
2357
|
-
command: `node "${
|
|
2899
|
+
command: `node "${path7.join(hooksDir, "on-prompt.js")}"`,
|
|
2358
2900
|
timeout: 5
|
|
2359
2901
|
}
|
|
2360
2902
|
]
|
|
@@ -2366,7 +2908,7 @@ function registerHooks(projectRoot) {
|
|
|
2366
2908
|
hooks: [
|
|
2367
2909
|
{
|
|
2368
2910
|
type: "command",
|
|
2369
|
-
command: `node "${
|
|
2911
|
+
command: `node "${path7.join(hooksDir, "on-file-change.js")}"`,
|
|
2370
2912
|
timeout: 5
|
|
2371
2913
|
}
|
|
2372
2914
|
]
|
|
@@ -2376,14 +2918,14 @@ function registerHooks(projectRoot) {
|
|
|
2376
2918
|
hooks: [
|
|
2377
2919
|
{
|
|
2378
2920
|
type: "command",
|
|
2379
|
-
command: `node "${
|
|
2921
|
+
command: `node "${path7.join(hooksDir, "on-stop.js")}"`,
|
|
2380
2922
|
timeout: 15,
|
|
2381
2923
|
async: true
|
|
2382
2924
|
}
|
|
2383
2925
|
]
|
|
2384
2926
|
});
|
|
2385
2927
|
settings["hooks"] = hooks;
|
|
2386
|
-
|
|
2928
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2387
2929
|
console.log(` Hooks registered in ${settingsPath}`);
|
|
2388
2930
|
}
|
|
2389
2931
|
function addHookIfMissing(hookArray, _eventName, hookConfig) {
|