burnwatch 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +144 -10
- package/dist/cli.js.map +1 -1
- package/dist/hooks/on-prompt.js.map +1 -1
- package/dist/hooks/on-session-start.js +58 -42
- package/dist/hooks/on-session-start.js.map +1 -1
- package/dist/hooks/on-stop.js +29 -1
- package/dist/hooks/on-stop.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +30 -2
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/skills/burnwatch-interview/SKILL.md +12 -6
package/dist/cli.js
CHANGED
|
@@ -925,9 +925,37 @@ async function pollService(tracked) {
|
|
|
925
925
|
serviceConfig
|
|
926
926
|
);
|
|
927
927
|
if (!result.error) return result;
|
|
928
|
-
|
|
928
|
+
return {
|
|
929
|
+
serviceId: tracked.serviceId,
|
|
930
|
+
spend: tracked.planCost ?? 0,
|
|
931
|
+
isEstimate: true,
|
|
932
|
+
tier: tracked.planCost !== void 0 ? "calc" : "blind",
|
|
933
|
+
error: `LIVE failed (${result.error}) \u2014 showing ${tracked.planCost !== void 0 ? "CALC" : "BLIND"} fallback`
|
|
934
|
+
};
|
|
935
|
+
} catch (err) {
|
|
936
|
+
return {
|
|
937
|
+
serviceId: tracked.serviceId,
|
|
938
|
+
spend: tracked.planCost ?? 0,
|
|
939
|
+
isEstimate: true,
|
|
940
|
+
tier: tracked.planCost !== void 0 ? "calc" : "blind",
|
|
941
|
+
error: `LIVE failed (${err instanceof Error ? err.message : "unknown"}) \u2014 showing ${tracked.planCost !== void 0 ? "CALC" : "BLIND"} fallback`
|
|
942
|
+
};
|
|
929
943
|
}
|
|
930
944
|
}
|
|
945
|
+
if (connector && tracked.hasApiKey && !serviceConfig?.apiKey) {
|
|
946
|
+
const projectedSpend = tracked.planCost !== void 0 ? (() => {
|
|
947
|
+
const now = /* @__PURE__ */ new Date();
|
|
948
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
949
|
+
return tracked.planCost / daysInMonth * now.getDate();
|
|
950
|
+
})() : 0;
|
|
951
|
+
return {
|
|
952
|
+
serviceId: tracked.serviceId,
|
|
953
|
+
spend: projectedSpend,
|
|
954
|
+
isEstimate: true,
|
|
955
|
+
tier: tracked.planCost !== void 0 ? "calc" : "blind",
|
|
956
|
+
error: "API key marked as configured but not found in ~/.config/burnwatch/ \u2014 re-run configure with --key"
|
|
957
|
+
};
|
|
958
|
+
}
|
|
931
959
|
if (tracked.planCost !== void 0) {
|
|
932
960
|
const now = /* @__PURE__ */ new Date();
|
|
933
961
|
const daysInMonth = new Date(
|
|
@@ -999,7 +1027,7 @@ function formatBrief(brief) {
|
|
|
999
1027
|
formatRow("Service", "Spend", "Conf", "Budget", "Left", width)
|
|
1000
1028
|
);
|
|
1001
1029
|
lines.push(`\u2551 ${hrSingle} \u2551`);
|
|
1002
|
-
for (const svc of brief.services) {
|
|
1030
|
+
for (const svc of brief.services.filter((s) => s.tier !== "excluded")) {
|
|
1003
1031
|
const spendStr = svc.tier === "blind" && svc.spend === 0 ? "\u2014" : svc.isEstimate ? `~$${svc.spend.toFixed(2)}` : `$${svc.spend.toFixed(2)}`;
|
|
1004
1032
|
const badge = CONFIDENCE_BADGES[svc.tier];
|
|
1005
1033
|
const budgetStr = svc.budget ? `$${svc.budget}` : "\u2014";
|
|
@@ -1568,6 +1596,9 @@ async function main() {
|
|
|
1568
1596
|
case "configure":
|
|
1569
1597
|
await cmdConfigure();
|
|
1570
1598
|
break;
|
|
1599
|
+
case "reset":
|
|
1600
|
+
cmdReset();
|
|
1601
|
+
break;
|
|
1571
1602
|
case "help":
|
|
1572
1603
|
case "--help":
|
|
1573
1604
|
case "-h":
|
|
@@ -1685,24 +1716,57 @@ async function cmdInterview() {
|
|
|
1685
1716
|
const config = readProjectConfig(projectRoot);
|
|
1686
1717
|
const globalConfig = readGlobalConfig();
|
|
1687
1718
|
const allRegistryServices = getAllServices(projectRoot);
|
|
1719
|
+
const connectorServices = ["anthropic", "openai", "vercel", "scrapfly", "supabase", "browserbase"];
|
|
1688
1720
|
const serviceStates = [];
|
|
1689
1721
|
for (const [serviceId, tracked] of Object.entries(config.services)) {
|
|
1690
1722
|
const definition = allRegistryServices.find((s) => s.id === serviceId);
|
|
1691
1723
|
if (!definition) continue;
|
|
1692
1724
|
let keySource = null;
|
|
1725
|
+
const envKeysFound = [];
|
|
1693
1726
|
const globalKey = globalConfig.services[serviceId]?.apiKey;
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
if (
|
|
1698
|
-
|
|
1699
|
-
|
|
1727
|
+
for (const pattern of definition.envPatterns) {
|
|
1728
|
+
if (process.env[pattern]) {
|
|
1729
|
+
envKeysFound.push(pattern);
|
|
1730
|
+
if (!keySource && !globalKey) keySource = `env:${pattern}`;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const envFiles = [".env", ".env.local", ".env.development"];
|
|
1734
|
+
for (const envFile of envFiles) {
|
|
1735
|
+
try {
|
|
1736
|
+
const envPath = path5.join(projectRoot, envFile);
|
|
1737
|
+
const envContent = fs5.readFileSync(envPath, "utf-8");
|
|
1738
|
+
for (const pattern of definition.envPatterns) {
|
|
1739
|
+
const regex = new RegExp(`^${pattern}=(.+)$`, "m");
|
|
1740
|
+
const match = envContent.match(regex);
|
|
1741
|
+
if (match?.[1]) {
|
|
1742
|
+
const label = `${pattern} (in ${envFile})`;
|
|
1743
|
+
if (!envKeysFound.some((k) => k.startsWith(pattern))) {
|
|
1744
|
+
envKeysFound.push(label);
|
|
1745
|
+
}
|
|
1746
|
+
if (!keySource && !globalKey) keySource = `file:${envFile}:${pattern}`;
|
|
1747
|
+
}
|
|
1700
1748
|
}
|
|
1749
|
+
} catch {
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (globalKey) keySource = "global_config";
|
|
1753
|
+
let apiKey = globalKey;
|
|
1754
|
+
if (!apiKey && keySource?.startsWith("env:")) {
|
|
1755
|
+
apiKey = process.env[keySource.slice(4)];
|
|
1756
|
+
}
|
|
1757
|
+
if (!apiKey && keySource?.startsWith("file:")) {
|
|
1758
|
+
const parts = keySource.split(":");
|
|
1759
|
+
const envFile = parts[1];
|
|
1760
|
+
const envVar = parts[2];
|
|
1761
|
+
try {
|
|
1762
|
+
const envContent = fs5.readFileSync(path5.join(projectRoot, envFile), "utf-8");
|
|
1763
|
+
const match = envContent.match(new RegExp(`^${envVar}=(.+)$`, "m"));
|
|
1764
|
+
if (match?.[1]) apiKey = match[1].trim().replace(/^["']|["']$/g, "");
|
|
1765
|
+
} catch {
|
|
1701
1766
|
}
|
|
1702
1767
|
}
|
|
1703
1768
|
let probeResult = null;
|
|
1704
1769
|
const { probeService: probe, hasProbe: checkProbe } = await Promise.resolve().then(() => (init_probes(), probes_exports));
|
|
1705
|
-
const apiKey = globalKey ?? (keySource?.startsWith("env:") ? process.env[keySource.slice(4)] : void 0);
|
|
1706
1770
|
if (apiKey && checkProbe(serviceId)) {
|
|
1707
1771
|
probeResult = await probe(serviceId, apiKey, definition.plans ?? []);
|
|
1708
1772
|
}
|
|
@@ -1725,6 +1789,24 @@ async function cmdInterview() {
|
|
|
1725
1789
|
upstash: "email:api_key from console.upstash.com \u2192 Account \u2192 Management API",
|
|
1726
1790
|
posthog: "Personal API key from posthog.com \u2192 Settings \u2192 Personal API Keys"
|
|
1727
1791
|
};
|
|
1792
|
+
const hasConnector = connectorServices.includes(serviceId);
|
|
1793
|
+
const canGoLive = hasConnector && !tracked.hasApiKey && !apiKey;
|
|
1794
|
+
let suggestedAction;
|
|
1795
|
+
if (tracked.excluded) {
|
|
1796
|
+
suggestedAction = "excluded \u2014 skip";
|
|
1797
|
+
} else if (probeResult && probeResult.confidence === "high") {
|
|
1798
|
+
suggestedAction = `confirm \u2014 probe detected ${probeResult.summary}`;
|
|
1799
|
+
} else if (apiKey && hasConnector) {
|
|
1800
|
+
suggestedAction = "configure with found key \u2014 LIVE tracking available";
|
|
1801
|
+
} else if (apiKey && !hasConnector) {
|
|
1802
|
+
suggestedAction = "key found but no billing connector \u2014 set plan cost for CALC tracking";
|
|
1803
|
+
} else if (canGoLive) {
|
|
1804
|
+
suggestedAction = `ask for API key \u2014 LIVE tracking possible (${keyHints[serviceId] ?? "check service dashboard"})`;
|
|
1805
|
+
} else if (!hasConnector) {
|
|
1806
|
+
suggestedAction = "no billing API \u2014 ask for plan tier and set budget as alert threshold";
|
|
1807
|
+
} else {
|
|
1808
|
+
suggestedAction = "ask user for plan details";
|
|
1809
|
+
}
|
|
1728
1810
|
serviceStates.push({
|
|
1729
1811
|
serviceId,
|
|
1730
1812
|
serviceName: definition.name,
|
|
@@ -1735,6 +1817,10 @@ async function cmdInterview() {
|
|
|
1735
1817
|
tier,
|
|
1736
1818
|
excluded: tracked.excluded ?? false,
|
|
1737
1819
|
hasProbe: checkProbe(serviceId),
|
|
1820
|
+
hasConnector,
|
|
1821
|
+
canGoLive,
|
|
1822
|
+
envKeysFound,
|
|
1823
|
+
suggestedAction,
|
|
1738
1824
|
probeResult,
|
|
1739
1825
|
availablePlans: (definition.plans ?? []).map((p, i) => ({
|
|
1740
1826
|
index: i + 1,
|
|
@@ -1762,8 +1848,14 @@ async function cmdInterview() {
|
|
|
1762
1848
|
totalBudget: serviceStates.reduce((sum, s) => sum + (s.currentBudget ?? 0), 0),
|
|
1763
1849
|
liveCount: serviceStates.filter((s) => s.tier === "live").length,
|
|
1764
1850
|
blindCount: serviceStates.filter((s) => s.tier === "blind").length,
|
|
1851
|
+
canGoLiveCount: serviceStates.filter((s) => s.canGoLive).length,
|
|
1852
|
+
keysFoundInEnv: serviceStates.filter((s) => s.envKeysFound.length > 0).length,
|
|
1765
1853
|
services: serviceStates,
|
|
1766
|
-
|
|
1854
|
+
instructions: {
|
|
1855
|
+
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.",
|
|
1856
|
+
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).",
|
|
1857
|
+
configureCommand: "burnwatch configure --service <id> [--plan <name>] [--budget <N>] [--key <KEY>] [--exclude]"
|
|
1858
|
+
}
|
|
1767
1859
|
};
|
|
1768
1860
|
if (flags.has("--json")) {
|
|
1769
1861
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -2066,6 +2158,47 @@ async function cmdReconcile() {
|
|
|
2066
2158
|
}
|
|
2067
2159
|
console.log("");
|
|
2068
2160
|
}
|
|
2161
|
+
function cmdReset() {
|
|
2162
|
+
const projectRoot = process.cwd();
|
|
2163
|
+
const burnwatchDir = path5.join(projectRoot, ".burnwatch");
|
|
2164
|
+
const claudeSkillsDir = path5.join(projectRoot, ".claude", "skills");
|
|
2165
|
+
if (!fs5.existsSync(burnwatchDir)) {
|
|
2166
|
+
console.log("burnwatch is not initialized in this project.");
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
fs5.rmSync(burnwatchDir, { recursive: true, force: true });
|
|
2170
|
+
console.log(`\u{1F5D1}\uFE0F Removed ${burnwatchDir}`);
|
|
2171
|
+
const skillNames = ["setup-burnwatch", "burnwatch-interview", "spend"];
|
|
2172
|
+
for (const skill of skillNames) {
|
|
2173
|
+
const skillDir = path5.join(claudeSkillsDir, skill);
|
|
2174
|
+
if (fs5.existsSync(skillDir)) {
|
|
2175
|
+
fs5.rmSync(skillDir, { recursive: true, force: true });
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
console.log("\u{1F5D1}\uFE0F Removed burnwatch skills from .claude/skills/");
|
|
2179
|
+
const settingsPath = path5.join(projectRoot, ".claude", "settings.json");
|
|
2180
|
+
if (fs5.existsSync(settingsPath)) {
|
|
2181
|
+
try {
|
|
2182
|
+
const settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
2183
|
+
const hooks = settings["hooks"];
|
|
2184
|
+
if (hooks) {
|
|
2185
|
+
for (const [event, hookList] of Object.entries(hooks)) {
|
|
2186
|
+
hooks[event] = hookList.filter(
|
|
2187
|
+
(h) => !h.hooks?.some((inner) => inner.command?.includes("burnwatch"))
|
|
2188
|
+
);
|
|
2189
|
+
if (hooks[event].length === 0) delete hooks[event];
|
|
2190
|
+
}
|
|
2191
|
+
if (Object.keys(hooks).length === 0) delete settings["hooks"];
|
|
2192
|
+
fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2193
|
+
console.log("\u{1F5D1}\uFE0F Removed burnwatch hooks from .claude/settings.json");
|
|
2194
|
+
}
|
|
2195
|
+
} catch {
|
|
2196
|
+
console.log("\u26A0\uFE0F Could not clean .claude/settings.json \u2014 remove burnwatch hooks manually");
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
console.log("\n\u2705 burnwatch fully reset. Global API keys in ~/.config/burnwatch/ were preserved.");
|
|
2200
|
+
console.log(" Run 'burnwatch init' to set up again.\n");
|
|
2201
|
+
}
|
|
2069
2202
|
function cmdHelp() {
|
|
2070
2203
|
console.log(`
|
|
2071
2204
|
burnwatch \u2014 Passive cost memory for AI-assisted development
|
|
@@ -2080,6 +2213,7 @@ Usage:
|
|
|
2080
2213
|
burnwatch reconcile Scan for untracked services
|
|
2081
2214
|
burnwatch interview --json Export state for agent-driven interview
|
|
2082
2215
|
burnwatch configure --service <id> [opts] Agent writes back interview answers
|
|
2216
|
+
burnwatch reset Remove all burnwatch config from this project
|
|
2083
2217
|
|
|
2084
2218
|
Options for 'configure':
|
|
2085
2219
|
--service <ID> Service to configure (required)
|