burnwatch 0.3.0 → 0.4.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 +24 -0
- package/dist/cli.js +237 -31
- package/dist/cli.js.map +1 -1
- package/dist/cost-impact.d.ts +23 -0
- package/dist/cost-impact.js +281 -0
- package/dist/cost-impact.js.map +1 -0
- package/dist/detector-C4LnLT-O.d.ts +28 -0
- package/dist/hooks/on-file-change.js +324 -6
- package/dist/hooks/on-file-change.js.map +1 -1
- package/dist/hooks/on-prompt.js +2 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +10 -1
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +47 -3
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.d.ts +5 -159
- package/dist/index.js +248 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive-init.d.ts +20 -0
- package/dist/interactive-init.js +239 -0
- package/dist/interactive-init.js.map +1 -0
- package/dist/mcp-server.js +2 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/types-fDMu4rOd.d.ts +178 -0
- package/package.json +1 -1
- package/registry.json +89 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// src/core/registry.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as url from "url";
|
|
5
|
+
var __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
6
|
+
var cachedRegistry = null;
|
|
7
|
+
function loadRegistry(projectRoot) {
|
|
8
|
+
if (cachedRegistry) return cachedRegistry;
|
|
9
|
+
const registry = /* @__PURE__ */ new Map();
|
|
10
|
+
const candidates = [
|
|
11
|
+
path.resolve(__dirname, "../../registry.json"),
|
|
12
|
+
// from src/core/
|
|
13
|
+
path.resolve(__dirname, "../registry.json")
|
|
14
|
+
// from dist/
|
|
15
|
+
];
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (fs.existsSync(candidate)) {
|
|
18
|
+
loadRegistryFile(candidate, registry);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (projectRoot) {
|
|
23
|
+
const localPath = path.join(projectRoot, ".burnwatch", "registry.json");
|
|
24
|
+
if (fs.existsSync(localPath)) {
|
|
25
|
+
loadRegistryFile(localPath, registry);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
cachedRegistry = registry;
|
|
29
|
+
return registry;
|
|
30
|
+
}
|
|
31
|
+
function loadRegistryFile(filePath, registry) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
34
|
+
const data = JSON.parse(raw);
|
|
35
|
+
for (const [id, service] of Object.entries(data.services)) {
|
|
36
|
+
registry.set(id, { ...service, id });
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/cost-impact.ts
|
|
43
|
+
var SERVICE_CALL_PATTERNS = {
|
|
44
|
+
anthropic: [
|
|
45
|
+
/\.messages\.create\s*\(/g,
|
|
46
|
+
/\.completions\.create\s*\(/g,
|
|
47
|
+
/anthropic\.\w+\.create\s*\(/g
|
|
48
|
+
],
|
|
49
|
+
openai: [
|
|
50
|
+
/\.chat\.completions\.create\s*\(/g,
|
|
51
|
+
/\.completions\.create\s*\(/g,
|
|
52
|
+
/\.images\.generate\s*\(/g,
|
|
53
|
+
/\.embeddings\.create\s*\(/g,
|
|
54
|
+
/openai\.\w+\.create\s*\(/g
|
|
55
|
+
],
|
|
56
|
+
"google-gemini": [
|
|
57
|
+
/\.generateContent\s*\(/g,
|
|
58
|
+
/\.generateContentStream\s*\(/g,
|
|
59
|
+
/model\.generate\w*\s*\(/g
|
|
60
|
+
],
|
|
61
|
+
"voyage-ai": [
|
|
62
|
+
/\.embed\s*\(/g,
|
|
63
|
+
/voyageai\.embed\s*\(/g
|
|
64
|
+
],
|
|
65
|
+
scrapfly: [
|
|
66
|
+
/\.scrape\s*\(/g,
|
|
67
|
+
/scrapfly\.scrape\s*\(/g,
|
|
68
|
+
/\.async_scrape\s*\(/g,
|
|
69
|
+
/ScrapeConfig\s*\(/g
|
|
70
|
+
],
|
|
71
|
+
browserbase: [
|
|
72
|
+
/\.createSession\s*\(/g,
|
|
73
|
+
/\.sessions\.create\s*\(/g,
|
|
74
|
+
/stagehand\.act\s*\(/g,
|
|
75
|
+
/stagehand\.extract\s*\(/g
|
|
76
|
+
],
|
|
77
|
+
upstash: [
|
|
78
|
+
/redis\.\w+\s*\(/g,
|
|
79
|
+
/\.set\s*\(/g,
|
|
80
|
+
/\.get\s*\(/g,
|
|
81
|
+
/\.incr\s*\(/g,
|
|
82
|
+
/\.hset\s*\(/g
|
|
83
|
+
],
|
|
84
|
+
resend: [
|
|
85
|
+
/resend\.emails\.send\s*\(/g,
|
|
86
|
+
/\.emails\.send\s*\(/g
|
|
87
|
+
],
|
|
88
|
+
stripe: [
|
|
89
|
+
/stripe\.charges\.create\s*\(/g,
|
|
90
|
+
/stripe\.paymentIntents\.create\s*\(/g,
|
|
91
|
+
/stripe\.checkout\.sessions\.create\s*\(/g
|
|
92
|
+
],
|
|
93
|
+
supabase: [
|
|
94
|
+
/supabase\.from\s*\(/g,
|
|
95
|
+
/\.rpc\s*\(/g,
|
|
96
|
+
/supabase\.storage/g
|
|
97
|
+
],
|
|
98
|
+
inngest: [
|
|
99
|
+
/inngest\.send\s*\(/g,
|
|
100
|
+
/\.createFunction\s*\(/g
|
|
101
|
+
],
|
|
102
|
+
posthog: [
|
|
103
|
+
/posthog\.capture\s*\(/g,
|
|
104
|
+
/\.capture\s*\(/g
|
|
105
|
+
],
|
|
106
|
+
aws: [
|
|
107
|
+
/\.send\s*\(new\s+\w+Command/g,
|
|
108
|
+
/s3Client\.send\s*\(/g,
|
|
109
|
+
/lambdaClient\.send\s*\(/g
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
function detectMultipliers(content) {
|
|
113
|
+
const multipliers = [];
|
|
114
|
+
if (/for\s*\(.*;\s*\w+\s*<\s*(\w+)/g.test(content)) {
|
|
115
|
+
const loopMatch = content.match(/for\s*\(.*;\s*\w+\s*<\s*(\d+)/);
|
|
116
|
+
if (loopMatch) {
|
|
117
|
+
const bound = parseInt(loopMatch[1]);
|
|
118
|
+
if (bound > 1) {
|
|
119
|
+
multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
multipliers.push({ label: "for loop (variable bound)", factor: 10 });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (/\.\s*map\s*\(\s*(async\s*)?\(/g.test(content)) {
|
|
126
|
+
multipliers.push({ label: ".map() iteration", factor: 10 });
|
|
127
|
+
}
|
|
128
|
+
if (/\.\s*forEach\s*\(\s*(async\s*)?\(/g.test(content)) {
|
|
129
|
+
multipliers.push({ label: ".forEach() iteration", factor: 10 });
|
|
130
|
+
}
|
|
131
|
+
if (/for\s*\(\s*(const|let|var)\s+\w+\s+(of|in)\s+/g.test(content)) {
|
|
132
|
+
multipliers.push({ label: "for...of/in loop", factor: 10 });
|
|
133
|
+
}
|
|
134
|
+
if (/Promise\.all\s*\(/g.test(content)) {
|
|
135
|
+
multipliers.push({ label: "Promise.all (parallel batch)", factor: 10 });
|
|
136
|
+
}
|
|
137
|
+
if (/cron|schedule|interval|setInterval|every\s+\d+\s*(min|hour|day|sec)/gi.test(content)) {
|
|
138
|
+
if (/every\s+5\s*min/gi.test(content) || /\*\/5\s+\*\s+\*/g.test(content)) {
|
|
139
|
+
multipliers.push({ label: "cron: every 5 minutes", factor: 8640 });
|
|
140
|
+
} else if (/every\s+1?\s*hour/gi.test(content) || /0\s+\*\s+\*\s+\*/g.test(content)) {
|
|
141
|
+
multipliers.push({ label: "cron: hourly", factor: 720 });
|
|
142
|
+
} else if (/every\s+1?\s*day/gi.test(content) || /0\s+0\s+\*\s+\*/g.test(content)) {
|
|
143
|
+
multipliers.push({ label: "cron: daily", factor: 30 });
|
|
144
|
+
} else {
|
|
145
|
+
multipliers.push({ label: "scheduled execution", factor: 30 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const batchMatch = content.match(/batch[_\s]?size\s*[=:]\s*(\d+)/i);
|
|
149
|
+
if (batchMatch) {
|
|
150
|
+
const batchSize = parseInt(batchMatch[1]);
|
|
151
|
+
if (batchSize > 1) {
|
|
152
|
+
multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return multipliers;
|
|
156
|
+
}
|
|
157
|
+
var GOTCHA_MULTIPLIERS = {
|
|
158
|
+
scrapfly: {
|
|
159
|
+
low: 1,
|
|
160
|
+
high: 25,
|
|
161
|
+
explanation: "anti-bot bypass consumes 5-25x base credits"
|
|
162
|
+
},
|
|
163
|
+
browserbase: {
|
|
164
|
+
low: 1,
|
|
165
|
+
high: 5,
|
|
166
|
+
explanation: "session duration affects cost \u2014 long sessions burn more"
|
|
167
|
+
},
|
|
168
|
+
anthropic: {
|
|
169
|
+
low: 1,
|
|
170
|
+
high: 60,
|
|
171
|
+
explanation: "Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)"
|
|
172
|
+
},
|
|
173
|
+
openai: {
|
|
174
|
+
low: 1,
|
|
175
|
+
high: 30,
|
|
176
|
+
explanation: "GPT-4 mini vs GPT-5 (30x cost range)"
|
|
177
|
+
},
|
|
178
|
+
stripe: {
|
|
179
|
+
low: 1,
|
|
180
|
+
high: 1.5,
|
|
181
|
+
explanation: "international cards add 1-1.5% extra"
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
function analyzeCostImpact(filePath, content, projectRoot) {
|
|
185
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const registry = loadRegistry(projectRoot);
|
|
189
|
+
const impacts = [];
|
|
190
|
+
const multipliers = detectMultipliers(content);
|
|
191
|
+
for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {
|
|
192
|
+
let totalCalls = 0;
|
|
193
|
+
for (const pattern of patterns) {
|
|
194
|
+
pattern.lastIndex = 0;
|
|
195
|
+
const matches = content.match(pattern);
|
|
196
|
+
if (matches) {
|
|
197
|
+
totalCalls += matches.length;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (totalCalls === 0) continue;
|
|
201
|
+
const service = registry.get(serviceId);
|
|
202
|
+
if (!service) continue;
|
|
203
|
+
const multiplierFactor = multipliers.length > 0 ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1) : 1;
|
|
204
|
+
const baseMonthlyRuns = multipliers.some((m) => m.label.startsWith("cron")) ? 1 : 50;
|
|
205
|
+
const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;
|
|
206
|
+
const gotcha = GOTCHA_MULTIPLIERS[serviceId];
|
|
207
|
+
const unitRate = service.pricing?.unitRate ?? 0;
|
|
208
|
+
let costLow;
|
|
209
|
+
let costHigh;
|
|
210
|
+
if (unitRate > 0) {
|
|
211
|
+
costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);
|
|
212
|
+
costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);
|
|
213
|
+
} else if (service.pricing?.monthlyBase !== void 0) {
|
|
214
|
+
costLow = 0;
|
|
215
|
+
costHigh = 0;
|
|
216
|
+
} else {
|
|
217
|
+
const typicalCallCosts = {
|
|
218
|
+
anthropic: 3e-3,
|
|
219
|
+
// ~$3/MTok * ~1K tokens average
|
|
220
|
+
openai: 2e-3,
|
|
221
|
+
"google-gemini": 1e-3,
|
|
222
|
+
scrapfly: 15e-5,
|
|
223
|
+
browserbase: 0.01,
|
|
224
|
+
resend: 1e-3,
|
|
225
|
+
stripe: 0.3
|
|
226
|
+
};
|
|
227
|
+
const perCall = typicalCallCosts[serviceId] ?? 1e-3;
|
|
228
|
+
costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);
|
|
229
|
+
costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);
|
|
230
|
+
}
|
|
231
|
+
if (costLow === 0 && costHigh === 0) continue;
|
|
232
|
+
impacts.push({
|
|
233
|
+
serviceId,
|
|
234
|
+
serviceName: service.name,
|
|
235
|
+
filePath,
|
|
236
|
+
callCount: totalCalls,
|
|
237
|
+
multipliers: multipliers.map((m) => m.label),
|
|
238
|
+
multiplierFactor,
|
|
239
|
+
monthlyInvocations,
|
|
240
|
+
costLow,
|
|
241
|
+
costHigh,
|
|
242
|
+
rangeExplanation: gotcha?.explanation
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return impacts;
|
|
246
|
+
}
|
|
247
|
+
function formatCostImpactCard(impacts, currentBudgets) {
|
|
248
|
+
const fileName = impacts[0]?.filePath.split("/").pop() ?? "unknown";
|
|
249
|
+
const lines = [];
|
|
250
|
+
lines.push(`[BURNWATCH] \u26A0\uFE0F Cost impact estimate for ${fileName}`);
|
|
251
|
+
for (const impact of impacts) {
|
|
252
|
+
const lowStr = impact.costLow < 1 ? `$${impact.costLow.toFixed(2)}` : `$${impact.costLow.toFixed(0)}`;
|
|
253
|
+
const highStr = impact.costHigh < 1 ? `$${impact.costHigh.toFixed(2)}` : `$${impact.costHigh.toFixed(0)}`;
|
|
254
|
+
const rangeStr = impact.costLow === impact.costHigh ? lowStr : `${lowStr}-${highStr}`;
|
|
255
|
+
lines.push(
|
|
256
|
+
` ${impact.serviceName}: ~${impact.monthlyInvocations.toLocaleString()} calls/mo \u2192 ${rangeStr}/mo` + (impact.rangeExplanation ? ` (${impact.rangeExplanation})` : "")
|
|
257
|
+
);
|
|
258
|
+
const current = currentBudgets[impact.serviceId];
|
|
259
|
+
if (current) {
|
|
260
|
+
const budgetStr = current.budget ? `$${current.spend.toFixed(0)}/$${current.budget} budget` : `$${current.spend.toFixed(0)} (no budget set)`;
|
|
261
|
+
const pctStr = current.budget && current.budget > 0 ? ` (${(current.spend / current.budget * 100).toFixed(0)}%)` : "";
|
|
262
|
+
lines.push(` Current: ${budgetStr}${pctStr}`);
|
|
263
|
+
}
|
|
264
|
+
const registry = loadRegistry();
|
|
265
|
+
const service = registry.get(impact.serviceId);
|
|
266
|
+
if (service?.alternatives && service.alternatives.length > 0 && impact.costHigh > 10) {
|
|
267
|
+
const freeAlts = service.alternatives.filter(
|
|
268
|
+
(a) => a.includes("free") || a.includes("cheerio") || a.includes("playwright") || a.includes("self-hosted")
|
|
269
|
+
);
|
|
270
|
+
if (freeAlts.length > 0) {
|
|
271
|
+
lines.push(` Consider: ${freeAlts.join(", ")} for lower-cost alternative`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
analyzeCostImpact,
|
|
279
|
+
formatCostImpactCard
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=cost-impact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/registry.ts","../src/cost-impact.ts"],"sourcesContent":["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","/**\n * Predictive cost impact analysis.\n *\n * Scans file content for SDK call sites, detects multipliers (loops, .map(), etc.),\n * and projects monthly cost using registry pricing data.\n */\n\nimport type {\n CostImpact,\n ServiceDefinition,\n} from \"./core/types.js\";\nimport { loadRegistry } from \"./core/registry.js\";\n\n/** SDK call patterns per service — maps serviceId to regex patterns for call sites */\nconst SERVICE_CALL_PATTERNS: Record<string, RegExp[]> = {\n anthropic: [\n /\\.messages\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /anthropic\\.\\w+\\.create\\s*\\(/g,\n ],\n openai: [\n /\\.chat\\.completions\\.create\\s*\\(/g,\n /\\.completions\\.create\\s*\\(/g,\n /\\.images\\.generate\\s*\\(/g,\n /\\.embeddings\\.create\\s*\\(/g,\n /openai\\.\\w+\\.create\\s*\\(/g,\n ],\n \"google-gemini\": [\n /\\.generateContent\\s*\\(/g,\n /\\.generateContentStream\\s*\\(/g,\n /model\\.generate\\w*\\s*\\(/g,\n ],\n \"voyage-ai\": [\n /\\.embed\\s*\\(/g,\n /voyageai\\.embed\\s*\\(/g,\n ],\n scrapfly: [\n /\\.scrape\\s*\\(/g,\n /scrapfly\\.scrape\\s*\\(/g,\n /\\.async_scrape\\s*\\(/g,\n /ScrapeConfig\\s*\\(/g,\n ],\n browserbase: [\n /\\.createSession\\s*\\(/g,\n /\\.sessions\\.create\\s*\\(/g,\n /stagehand\\.act\\s*\\(/g,\n /stagehand\\.extract\\s*\\(/g,\n ],\n upstash: [\n /redis\\.\\w+\\s*\\(/g,\n /\\.set\\s*\\(/g,\n /\\.get\\s*\\(/g,\n /\\.incr\\s*\\(/g,\n /\\.hset\\s*\\(/g,\n ],\n resend: [\n /resend\\.emails\\.send\\s*\\(/g,\n /\\.emails\\.send\\s*\\(/g,\n ],\n stripe: [\n /stripe\\.charges\\.create\\s*\\(/g,\n /stripe\\.paymentIntents\\.create\\s*\\(/g,\n /stripe\\.checkout\\.sessions\\.create\\s*\\(/g,\n ],\n supabase: [\n /supabase\\.from\\s*\\(/g,\n /\\.rpc\\s*\\(/g,\n /supabase\\.storage/g,\n ],\n inngest: [\n /inngest\\.send\\s*\\(/g,\n /\\.createFunction\\s*\\(/g,\n ],\n posthog: [\n /posthog\\.capture\\s*\\(/g,\n /\\.capture\\s*\\(/g,\n ],\n aws: [\n /\\.send\\s*\\(new\\s+\\w+Command/g,\n /s3Client\\.send\\s*\\(/g,\n /lambdaClient\\.send\\s*\\(/g,\n ],\n};\n\n/** Multiplier patterns — things that make calls happen more than once */\ninterface MultiplierMatch {\n label: string;\n factor: number;\n}\n\nfunction detectMultipliers(content: string): MultiplierMatch[] {\n const multipliers: MultiplierMatch[] = [];\n\n // for loops — assume 10x as conservative estimate\n if (/for\\s*\\(.*;\\s*\\w+\\s*<\\s*(\\w+)/g.test(content)) {\n // Try to extract the loop bound\n const loopMatch = content.match(/for\\s*\\(.*;\\s*\\w+\\s*<\\s*(\\d+)/);\n if (loopMatch) {\n const bound = parseInt(loopMatch[1]!);\n if (bound > 1) {\n multipliers.push({ label: `for loop (${bound} iterations)`, factor: bound });\n }\n } else {\n multipliers.push({ label: \"for loop (variable bound)\", factor: 10 });\n }\n }\n\n // .map() calls\n if (/\\.\\s*map\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n multipliers.push({ label: \".map() iteration\", factor: 10 });\n }\n\n // .forEach() calls\n if (/\\.\\s*forEach\\s*\\(\\s*(async\\s*)?\\(/g.test(content)) {\n multipliers.push({ label: \".forEach() iteration\", factor: 10 });\n }\n\n // for...of / for...in\n if (/for\\s*\\(\\s*(const|let|var)\\s+\\w+\\s+(of|in)\\s+/g.test(content)) {\n multipliers.push({ label: \"for...of/in loop\", factor: 10 });\n }\n\n // Promise.all with array\n if (/Promise\\.all\\s*\\(/g.test(content)) {\n multipliers.push({ label: \"Promise.all (parallel batch)\", factor: 10 });\n }\n\n // Cron patterns in comments or configuration\n if (/cron|schedule|interval|setInterval|every\\s+\\d+\\s*(min|hour|day|sec)/gi.test(content)) {\n // Estimate: if hourly = 720/mo, daily = 30/mo, every 5 min = 8640/mo\n if (/every\\s+5\\s*min/gi.test(content) || /\\*\\/5\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: every 5 minutes\", factor: 8640 });\n } else if (/every\\s+1?\\s*hour/gi.test(content) || /0\\s+\\*\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: hourly\", factor: 720 });\n } else if (/every\\s+1?\\s*day/gi.test(content) || /0\\s+0\\s+\\*\\s+\\*/g.test(content)) {\n multipliers.push({ label: \"cron: daily\", factor: 30 });\n } else {\n multipliers.push({ label: \"scheduled execution\", factor: 30 });\n }\n }\n\n // Batch size hints\n const batchMatch = content.match(/batch[_\\s]?size\\s*[=:]\\s*(\\d+)/i);\n if (batchMatch) {\n const batchSize = parseInt(batchMatch[1]!);\n if (batchSize > 1) {\n multipliers.push({ label: `batch size: ${batchSize}`, factor: batchSize });\n }\n }\n\n return multipliers;\n}\n\n/** Gotcha-based cost multipliers per service */\nconst GOTCHA_MULTIPLIERS: Record<string, { low: number; high: number; explanation: string }> = {\n scrapfly: {\n low: 1,\n high: 25,\n explanation: \"anti-bot bypass consumes 5-25x base credits\",\n },\n browserbase: {\n low: 1,\n high: 5,\n explanation: \"session duration affects cost — long sessions burn more\",\n },\n anthropic: {\n low: 1,\n high: 60,\n explanation: \"Haiku ~$0.25/MTok vs Opus ~$15/MTok (60x range)\",\n },\n openai: {\n low: 1,\n high: 30,\n explanation: \"GPT-4 mini vs GPT-5 (30x cost range)\",\n },\n stripe: {\n low: 1,\n high: 1.5,\n explanation: \"international cards add 1-1.5% extra\",\n },\n};\n\n/**\n * Analyze a file's content for cost-impacting SDK calls.\n * Returns cost impact estimates for each detected service.\n */\nexport function analyzeCostImpact(\n filePath: string,\n content: string,\n projectRoot?: string,\n): CostImpact[] {\n // Only analyze source files\n if (!/\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) {\n return [];\n }\n\n const registry = loadRegistry(projectRoot);\n const impacts: CostImpact[] = [];\n const multipliers = detectMultipliers(content);\n\n for (const [serviceId, patterns] of Object.entries(SERVICE_CALL_PATTERNS)) {\n let totalCalls = 0;\n\n for (const pattern of patterns) {\n // Reset lastIndex for global regexes\n pattern.lastIndex = 0;\n const matches = content.match(pattern);\n if (matches) {\n totalCalls += matches.length;\n }\n }\n\n if (totalCalls === 0) continue;\n\n const service = registry.get(serviceId);\n if (!service) continue;\n\n // Calculate effective multiplier\n const multiplierFactor = multipliers.length > 0\n ? multipliers.reduce((max, m) => Math.max(max, m.factor), 1)\n : 1;\n\n // Assume ~30 working days, ~8 dev hours/day as baseline for monthly projections\n // If no cron detected, assume the code runs during dev sessions: ~50 times/month\n const baseMonthlyRuns = multipliers.some((m) => m.label.startsWith(\"cron\"))\n ? 1 // cron multiplier already encodes frequency\n : 50; // ~2 dev runs per working day\n\n const monthlyInvocations = totalCalls * multiplierFactor * baseMonthlyRuns;\n\n // Get cost estimates\n const gotcha = GOTCHA_MULTIPLIERS[serviceId];\n const unitRate = service.pricing?.unitRate ?? 0;\n\n let costLow: number;\n let costHigh: number;\n\n if (unitRate > 0) {\n costLow = monthlyInvocations * unitRate * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * unitRate * (gotcha?.high ?? 1);\n } else if (service.pricing?.monthlyBase !== undefined) {\n // Flat-rate services — cost is the plan, not per-invocation\n costLow = 0;\n costHigh = 0;\n } else {\n // Estimate based on typical per-call costs\n const typicalCallCosts: Record<string, number> = {\n anthropic: 0.003, // ~$3/MTok * ~1K tokens average\n openai: 0.002,\n \"google-gemini\": 0.001,\n scrapfly: 0.00015,\n browserbase: 0.01,\n resend: 0.001,\n stripe: 0.30,\n };\n const perCall = typicalCallCosts[serviceId] ?? 0.001;\n costLow = monthlyInvocations * perCall * (gotcha?.low ?? 1);\n costHigh = monthlyInvocations * perCall * (gotcha?.high ?? 1);\n }\n\n // Skip if no meaningful cost\n if (costLow === 0 && costHigh === 0) continue;\n\n impacts.push({\n serviceId,\n serviceName: service.name,\n filePath,\n callCount: totalCalls,\n multipliers: multipliers.map((m) => m.label),\n multiplierFactor,\n monthlyInvocations,\n costLow,\n costHigh,\n rangeExplanation: gotcha?.explanation,\n });\n }\n\n return impacts;\n}\n\n/**\n * Format a cost impact card for injection into Claude's context.\n */\nexport function formatCostImpactCard(\n impacts: CostImpact[],\n currentBudgets: Record<string, { spend: number; budget?: number }>,\n): string {\n const fileName = impacts[0]?.filePath.split(\"/\").pop() ?? \"unknown\";\n const lines: string[] = [];\n\n lines.push(`[BURNWATCH] ⚠️ Cost impact estimate for ${fileName}`);\n\n for (const impact of impacts) {\n const lowStr = impact.costLow < 1\n ? `$${impact.costLow.toFixed(2)}`\n : `$${impact.costLow.toFixed(0)}`;\n const highStr = impact.costHigh < 1\n ? `$${impact.costHigh.toFixed(2)}`\n : `$${impact.costHigh.toFixed(0)}`;\n\n const rangeStr = impact.costLow === impact.costHigh\n ? lowStr\n : `${lowStr}-${highStr}`;\n\n lines.push(\n ` ${impact.serviceName}: ~${impact.monthlyInvocations.toLocaleString()} calls/mo → ${rangeStr}/mo` +\n (impact.rangeExplanation ? ` (${impact.rangeExplanation})` : \"\"),\n );\n\n // Show current budget status if available\n const current = currentBudgets[impact.serviceId];\n if (current) {\n const budgetStr = current.budget\n ? `$${current.spend.toFixed(0)}/$${current.budget} budget`\n : `$${current.spend.toFixed(0)} (no budget set)`;\n const pctStr = current.budget && current.budget > 0\n ? ` (${((current.spend / current.budget) * 100).toFixed(0)}%)`\n : \"\";\n lines.push(` Current: ${budgetStr}${pctStr}`);\n }\n\n // Suggest alternatives from registry\n const registry = loadRegistry();\n const service = registry.get(impact.serviceId);\n if (service?.alternatives && service.alternatives.length > 0 && impact.costHigh > 10) {\n const freeAlts = service.alternatives.filter(\n (a) => a.includes(\"free\") || a.includes(\"cheerio\") || a.includes(\"playwright\") || a.includes(\"self-hosted\"),\n );\n if (freeAlts.length > 0) {\n lines.push(` Consider: ${freeAlts.join(\", \")} for lower-cost alternative`);\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { S as ServiceDefinition, D as DetectionSource } from './types-fDMu4rOd.js';
|
|
2
|
+
|
|
3
|
+
interface DetectionResult {
|
|
4
|
+
service: ServiceDefinition;
|
|
5
|
+
sources: DetectionSource[];
|
|
6
|
+
details: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run all detection surfaces against the current project.
|
|
10
|
+
* Returns services detected via any combination of:
|
|
11
|
+
* - package.json dependencies (recursive — finds monorepo subdirectories)
|
|
12
|
+
* - environment variable patterns (process.env + .env* files recursive)
|
|
13
|
+
* - import statement scanning (recursive from project root)
|
|
14
|
+
* - (prompt mention scanning is handled separately in hooks)
|
|
15
|
+
*/
|
|
16
|
+
declare function detectServices(projectRoot: string): DetectionResult[];
|
|
17
|
+
/**
|
|
18
|
+
* Detect services mentioned in a prompt string.
|
|
19
|
+
* Used by the UserPromptSubmit hook.
|
|
20
|
+
*/
|
|
21
|
+
declare function detectMentions(prompt: string, projectRoot?: string): DetectionResult[];
|
|
22
|
+
/**
|
|
23
|
+
* Detect new services introduced in a file change.
|
|
24
|
+
* Used by the PostToolUse hook for Write/Edit events.
|
|
25
|
+
*/
|
|
26
|
+
declare function detectInFileChange(filePath: string, content: string, projectRoot?: string): DetectionResult[];
|
|
27
|
+
|
|
28
|
+
export { type DetectionResult as D, detectMentions as a, detectServices as b, detectInFileChange as d };
|