burnwatch 0.12.1 → 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 +14 -0
- package/dist/cli.js +567 -55
- package/dist/cli.js.map +1 -1
- package/dist/hooks/on-file-change.js +125 -9
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-session-start.js +111 -19
- package/dist/hooks/on-session-start.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";
|
|
@@ -1622,6 +1622,439 @@ async function runInteractiveInit(detected) {
|
|
|
1622
1622
|
return { services };
|
|
1623
1623
|
}
|
|
1624
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
|
+
|
|
1625
2058
|
// src/cli.ts
|
|
1626
2059
|
var args = process.argv.slice(2);
|
|
1627
2060
|
var command = args[0];
|
|
@@ -1650,6 +2083,9 @@ async function main() {
|
|
|
1650
2083
|
case "configure":
|
|
1651
2084
|
await cmdConfigure();
|
|
1652
2085
|
break;
|
|
2086
|
+
case "scan":
|
|
2087
|
+
cmdScan();
|
|
2088
|
+
break;
|
|
1653
2089
|
case "reset":
|
|
1654
2090
|
cmdReset();
|
|
1655
2091
|
break;
|
|
@@ -1676,10 +2112,10 @@ async function cmdInit() {
|
|
|
1676
2112
|
const projectRoot = process.cwd();
|
|
1677
2113
|
const nonInteractive = flags.has("--non-interactive") || flags.has("--ni");
|
|
1678
2114
|
const alreadyInitialized = isInitialized(projectRoot);
|
|
1679
|
-
let projectName =
|
|
2115
|
+
let projectName = path7.basename(projectRoot);
|
|
1680
2116
|
try {
|
|
1681
|
-
const pkgPath =
|
|
1682
|
-
const pkg = JSON.parse(
|
|
2117
|
+
const pkgPath = path7.join(projectRoot, "package.json");
|
|
2118
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
1683
2119
|
if (pkg.name) projectName = pkg.name;
|
|
1684
2120
|
} catch {
|
|
1685
2121
|
}
|
|
@@ -1718,8 +2154,8 @@ async function cmdInit() {
|
|
|
1718
2154
|
config.services = result.services;
|
|
1719
2155
|
}
|
|
1720
2156
|
writeProjectConfig(config, projectRoot);
|
|
1721
|
-
const gitignorePath =
|
|
1722
|
-
|
|
2157
|
+
const gitignorePath = path7.join(projectConfigDir(projectRoot), ".gitignore");
|
|
2158
|
+
fs7.writeFileSync(
|
|
1723
2159
|
gitignorePath,
|
|
1724
2160
|
[
|
|
1725
2161
|
"# Burnwatch \u2014 ignore cache and snapshots, keep ledger and config",
|
|
@@ -1730,7 +2166,21 @@ async function cmdInit() {
|
|
|
1730
2166
|
].join("\n"),
|
|
1731
2167
|
"utf-8"
|
|
1732
2168
|
);
|
|
1733
|
-
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");
|
|
1734
2184
|
registerHooks(projectRoot);
|
|
1735
2185
|
console.log("\nburnwatch initialized.\n");
|
|
1736
2186
|
if (process.stdin.isTTY) {
|
|
@@ -1749,10 +2199,10 @@ async function cmdInterview() {
|
|
|
1749
2199
|
if (!isInitialized(projectRoot)) {
|
|
1750
2200
|
ensureProjectDirs(projectRoot);
|
|
1751
2201
|
const detected = detectServices(projectRoot);
|
|
1752
|
-
let projectName =
|
|
2202
|
+
let projectName = path7.basename(projectRoot);
|
|
1753
2203
|
try {
|
|
1754
2204
|
const pkg = JSON.parse(
|
|
1755
|
-
|
|
2205
|
+
fs7.readFileSync(path7.join(projectRoot, "package.json"), "utf-8")
|
|
1756
2206
|
);
|
|
1757
2207
|
if (pkg.name) projectName = pkg.name;
|
|
1758
2208
|
} catch {
|
|
@@ -1787,9 +2237,9 @@ async function cmdInterview() {
|
|
|
1787
2237
|
const envFiles = findEnvFiles(projectRoot, 3);
|
|
1788
2238
|
for (const envFilePath of envFiles) {
|
|
1789
2239
|
try {
|
|
1790
|
-
const envContent =
|
|
2240
|
+
const envContent = fs7.readFileSync(envFilePath, "utf-8");
|
|
1791
2241
|
const envKeys = parseEnvKeys(envContent);
|
|
1792
|
-
const envFileName =
|
|
2242
|
+
const envFileName = path7.relative(projectRoot, envFilePath);
|
|
1793
2243
|
for (const pattern of definition.envPatterns) {
|
|
1794
2244
|
if (envKeys.has(pattern)) {
|
|
1795
2245
|
const label = `${pattern} (in ${envFileName})`;
|
|
@@ -1812,7 +2262,7 @@ async function cmdInterview() {
|
|
|
1812
2262
|
const envFile = parts[1];
|
|
1813
2263
|
const envVar = parts[2];
|
|
1814
2264
|
try {
|
|
1815
|
-
const envContent =
|
|
2265
|
+
const envContent = fs7.readFileSync(path7.join(projectRoot, envFile), "utf-8");
|
|
1816
2266
|
const regex = new RegExp(`^(?:export\\s+)?${envVar}\\s*=\\s*(.+)$`, "m");
|
|
1817
2267
|
const match = envContent.match(regex);
|
|
1818
2268
|
if (match?.[1]) apiKey = match[1].trim().replace(/^["']|["']$/g, "");
|
|
@@ -1896,6 +2346,19 @@ async function cmdInterview() {
|
|
|
1896
2346
|
serviceStates.sort(
|
|
1897
2347
|
(a, b) => riskOrder.indexOf(a.riskCategory) - riskOrder.indexOf(b.riskCategory)
|
|
1898
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
|
+
}
|
|
1899
2362
|
const output = {
|
|
1900
2363
|
projectName: config.projectName,
|
|
1901
2364
|
serviceCount: serviceStates.length,
|
|
@@ -1904,11 +2367,13 @@ async function cmdInterview() {
|
|
|
1904
2367
|
blindCount: serviceStates.filter((s) => s.tier === "blind").length,
|
|
1905
2368
|
canGoLiveCount: serviceStates.filter((s) => s.canGoLive).length,
|
|
1906
2369
|
keysFoundInEnv: serviceStates.filter((s) => s.envKeysFound.length > 0).length,
|
|
2370
|
+
utilization: utilizationSummary,
|
|
1907
2371
|
services: serviceStates,
|
|
1908
2372
|
instructions: {
|
|
1909
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.",
|
|
1910
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).",
|
|
1911
|
-
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."
|
|
1912
2377
|
}
|
|
1913
2378
|
};
|
|
1914
2379
|
if (flags.has("--json")) {
|
|
@@ -2212,28 +2677,74 @@ async function cmdReconcile() {
|
|
|
2212
2677
|
}
|
|
2213
2678
|
console.log("");
|
|
2214
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
|
+
}
|
|
2215
2726
|
function cmdReset() {
|
|
2216
2727
|
const projectRoot = process.cwd();
|
|
2217
|
-
const burnwatchDir =
|
|
2218
|
-
const claudeSkillsDir =
|
|
2219
|
-
if (!
|
|
2728
|
+
const burnwatchDir = path7.join(projectRoot, ".burnwatch");
|
|
2729
|
+
const claudeSkillsDir = path7.join(projectRoot, ".claude", "skills");
|
|
2730
|
+
if (!fs7.existsSync(burnwatchDir)) {
|
|
2220
2731
|
console.log("burnwatch is not initialized in this project.");
|
|
2221
2732
|
return;
|
|
2222
2733
|
}
|
|
2223
|
-
|
|
2734
|
+
fs7.rmSync(burnwatchDir, { recursive: true, force: true });
|
|
2224
2735
|
console.log(`\u{1F5D1}\uFE0F Removed ${burnwatchDir}`);
|
|
2225
2736
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2226
2737
|
for (const skill of skillNames) {
|
|
2227
|
-
const skillDir =
|
|
2228
|
-
if (
|
|
2229
|
-
|
|
2738
|
+
const skillDir = path7.join(claudeSkillsDir, skill);
|
|
2739
|
+
if (fs7.existsSync(skillDir)) {
|
|
2740
|
+
fs7.rmSync(skillDir, { recursive: true, force: true });
|
|
2230
2741
|
}
|
|
2231
2742
|
}
|
|
2232
2743
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch skills from .claude/skills/");
|
|
2233
|
-
const settingsPath =
|
|
2234
|
-
if (
|
|
2744
|
+
const settingsPath = path7.join(projectRoot, ".claude", "settings.json");
|
|
2745
|
+
if (fs7.existsSync(settingsPath)) {
|
|
2235
2746
|
try {
|
|
2236
|
-
const settings = JSON.parse(
|
|
2747
|
+
const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
2237
2748
|
const hooks = settings["hooks"];
|
|
2238
2749
|
if (hooks) {
|
|
2239
2750
|
for (const [event, hookList] of Object.entries(hooks)) {
|
|
@@ -2243,7 +2754,7 @@ function cmdReset() {
|
|
|
2243
2754
|
if (hooks[event].length === 0) delete hooks[event];
|
|
2244
2755
|
}
|
|
2245
2756
|
if (Object.keys(hooks).length === 0) delete settings["hooks"];
|
|
2246
|
-
|
|
2757
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2247
2758
|
console.log("\u{1F5D1}\uFE0F Removed burnwatch hooks from .claude/settings.json");
|
|
2248
2759
|
}
|
|
2249
2760
|
} catch {
|
|
@@ -2267,6 +2778,7 @@ Usage:
|
|
|
2267
2778
|
burnwatch reconcile Scan for untracked services
|
|
2268
2779
|
burnwatch interview --json Export state for agent-driven interview
|
|
2269
2780
|
burnwatch configure --service <id> [opts] Agent writes back interview answers
|
|
2781
|
+
burnwatch scan [--verbose] Scan project for utilization patterns
|
|
2270
2782
|
burnwatch reset Remove all burnwatch config from this project
|
|
2271
2783
|
|
|
2272
2784
|
Options for 'configure':
|
|
@@ -2293,23 +2805,23 @@ Examples:
|
|
|
2293
2805
|
}
|
|
2294
2806
|
function cmdVersion() {
|
|
2295
2807
|
try {
|
|
2296
|
-
const pkgPath =
|
|
2297
|
-
|
|
2808
|
+
const pkgPath = path7.resolve(
|
|
2809
|
+
path7.dirname(new URL(import.meta.url).pathname),
|
|
2298
2810
|
"../package.json"
|
|
2299
2811
|
);
|
|
2300
|
-
const pkg = JSON.parse(
|
|
2812
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
2301
2813
|
console.log(`burnwatch v${pkg.version}`);
|
|
2302
2814
|
} catch {
|
|
2303
2815
|
console.log("burnwatch v0.1.0");
|
|
2304
2816
|
}
|
|
2305
2817
|
}
|
|
2306
2818
|
function registerHooks(projectRoot) {
|
|
2307
|
-
const sourceHooksDir =
|
|
2308
|
-
|
|
2819
|
+
const sourceHooksDir = path7.resolve(
|
|
2820
|
+
path7.dirname(new URL(import.meta.url).pathname),
|
|
2309
2821
|
"hooks"
|
|
2310
2822
|
);
|
|
2311
|
-
const localHooksDir =
|
|
2312
|
-
|
|
2823
|
+
const localHooksDir = path7.join(projectRoot, ".burnwatch", "hooks");
|
|
2824
|
+
fs7.mkdirSync(localHooksDir, { recursive: true });
|
|
2313
2825
|
const hookFiles = [
|
|
2314
2826
|
"on-session-start.js",
|
|
2315
2827
|
"on-prompt.js",
|
|
@@ -2317,45 +2829,45 @@ function registerHooks(projectRoot) {
|
|
|
2317
2829
|
"on-stop.js"
|
|
2318
2830
|
];
|
|
2319
2831
|
for (const file of hookFiles) {
|
|
2320
|
-
const src =
|
|
2321
|
-
const dest =
|
|
2832
|
+
const src = path7.join(sourceHooksDir, file);
|
|
2833
|
+
const dest = path7.join(localHooksDir, file);
|
|
2322
2834
|
try {
|
|
2323
|
-
|
|
2835
|
+
fs7.copyFileSync(src, dest);
|
|
2324
2836
|
const mapSrc = src + ".map";
|
|
2325
|
-
if (
|
|
2326
|
-
|
|
2837
|
+
if (fs7.existsSync(mapSrc)) {
|
|
2838
|
+
fs7.copyFileSync(mapSrc, dest + ".map");
|
|
2327
2839
|
}
|
|
2328
2840
|
} catch (err) {
|
|
2329
2841
|
console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);
|
|
2330
2842
|
}
|
|
2331
2843
|
}
|
|
2332
2844
|
console.log(` Hook scripts copied to ${localHooksDir}`);
|
|
2333
|
-
const sourceSkillsDir =
|
|
2334
|
-
|
|
2845
|
+
const sourceSkillsDir = path7.resolve(
|
|
2846
|
+
path7.dirname(new URL(import.meta.url).pathname),
|
|
2335
2847
|
"../skills"
|
|
2336
2848
|
);
|
|
2337
2849
|
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2338
|
-
const claudeSkillsDir =
|
|
2850
|
+
const claudeSkillsDir = path7.join(projectRoot, ".claude", "skills");
|
|
2339
2851
|
for (const skillName of skillNames) {
|
|
2340
|
-
const srcSkill =
|
|
2341
|
-
const destDir =
|
|
2342
|
-
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");
|
|
2343
2855
|
try {
|
|
2344
|
-
if (
|
|
2345
|
-
|
|
2346
|
-
|
|
2856
|
+
if (fs7.existsSync(srcSkill)) {
|
|
2857
|
+
fs7.mkdirSync(destDir, { recursive: true });
|
|
2858
|
+
fs7.copyFileSync(srcSkill, destSkill);
|
|
2347
2859
|
}
|
|
2348
2860
|
} catch (err) {
|
|
2349
2861
|
console.error(` Warning: Could not copy skill ${skillName}: ${err instanceof Error ? err.message : err}`);
|
|
2350
2862
|
}
|
|
2351
2863
|
}
|
|
2352
2864
|
console.log(` Skills installed to ${claudeSkillsDir}`);
|
|
2353
|
-
const claudeDir =
|
|
2354
|
-
const settingsPath =
|
|
2355
|
-
|
|
2865
|
+
const claudeDir = path7.join(projectRoot, ".claude");
|
|
2866
|
+
const settingsPath = path7.join(claudeDir, "settings.json");
|
|
2867
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
2356
2868
|
let settings = {};
|
|
2357
2869
|
try {
|
|
2358
|
-
const existing =
|
|
2870
|
+
const existing = fs7.readFileSync(settingsPath, "utf-8");
|
|
2359
2871
|
settings = JSON.parse(existing);
|
|
2360
2872
|
console.log(` Merging into existing ${settingsPath}`);
|
|
2361
2873
|
} catch {
|
|
@@ -2371,7 +2883,7 @@ function registerHooks(projectRoot) {
|
|
|
2371
2883
|
hooks: [
|
|
2372
2884
|
{
|
|
2373
2885
|
type: "command",
|
|
2374
|
-
command: `node "${
|
|
2886
|
+
command: `node "${path7.join(hooksDir, "on-session-start.js")}"`,
|
|
2375
2887
|
timeout: 15
|
|
2376
2888
|
}
|
|
2377
2889
|
]
|
|
@@ -2384,7 +2896,7 @@ function registerHooks(projectRoot) {
|
|
|
2384
2896
|
hooks: [
|
|
2385
2897
|
{
|
|
2386
2898
|
type: "command",
|
|
2387
|
-
command: `node "${
|
|
2899
|
+
command: `node "${path7.join(hooksDir, "on-prompt.js")}"`,
|
|
2388
2900
|
timeout: 5
|
|
2389
2901
|
}
|
|
2390
2902
|
]
|
|
@@ -2396,7 +2908,7 @@ function registerHooks(projectRoot) {
|
|
|
2396
2908
|
hooks: [
|
|
2397
2909
|
{
|
|
2398
2910
|
type: "command",
|
|
2399
|
-
command: `node "${
|
|
2911
|
+
command: `node "${path7.join(hooksDir, "on-file-change.js")}"`,
|
|
2400
2912
|
timeout: 5
|
|
2401
2913
|
}
|
|
2402
2914
|
]
|
|
@@ -2406,14 +2918,14 @@ function registerHooks(projectRoot) {
|
|
|
2406
2918
|
hooks: [
|
|
2407
2919
|
{
|
|
2408
2920
|
type: "command",
|
|
2409
|
-
command: `node "${
|
|
2921
|
+
command: `node "${path7.join(hooksDir, "on-stop.js")}"`,
|
|
2410
2922
|
timeout: 15,
|
|
2411
2923
|
async: true
|
|
2412
2924
|
}
|
|
2413
2925
|
]
|
|
2414
2926
|
});
|
|
2415
2927
|
settings["hooks"] = hooks;
|
|
2416
|
-
|
|
2928
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2417
2929
|
console.log(` Hooks registered in ${settingsPath}`);
|
|
2418
2930
|
}
|
|
2419
2931
|
function addHookIfMissing(hookArray, _eventName, hookConfig) {
|