burnwatch 0.10.1 → 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 CHANGED
@@ -925,9 +925,37 @@ async function pollService(tracked) {
925
925
  serviceConfig
926
926
  );
927
927
  if (!result.error) return result;
928
- } catch {
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":
@@ -1693,37 +1724,32 @@ async function cmdInterview() {
1693
1724
  let keySource = null;
1694
1725
  const envKeysFound = [];
1695
1726
  const globalKey = globalConfig.services[serviceId]?.apiKey;
1696
- if (globalKey) {
1697
- keySource = "global_config";
1698
- } else {
1699
- for (const pattern of definition.envPatterns) {
1700
- if (process.env[pattern]) {
1701
- keySource = `env:${pattern}`;
1702
- envKeysFound.push(pattern);
1703
- break;
1704
- }
1727
+ for (const pattern of definition.envPatterns) {
1728
+ if (process.env[pattern]) {
1729
+ envKeysFound.push(pattern);
1730
+ if (!keySource && !globalKey) keySource = `env:${pattern}`;
1705
1731
  }
1706
- if (!keySource) {
1707
- const envFiles = [".env", ".env.local", ".env.development"];
1708
- for (const envFile of envFiles) {
1709
- try {
1710
- const envPath = path5.join(projectRoot, envFile);
1711
- const envContent = fs5.readFileSync(envPath, "utf-8");
1712
- for (const pattern of definition.envPatterns) {
1713
- const regex = new RegExp(`^${pattern}=(.+)$`, "m");
1714
- const match = envContent.match(regex);
1715
- if (match?.[1]) {
1716
- keySource = `file:${envFile}:${pattern}`;
1717
- envKeysFound.push(`${pattern} (in ${envFile})`);
1718
- break;
1719
- }
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);
1720
1745
  }
1721
- if (keySource) break;
1722
- } catch {
1746
+ if (!keySource && !globalKey) keySource = `file:${envFile}:${pattern}`;
1723
1747
  }
1724
1748
  }
1749
+ } catch {
1725
1750
  }
1726
1751
  }
1752
+ if (globalKey) keySource = "global_config";
1727
1753
  let apiKey = globalKey;
1728
1754
  if (!apiKey && keySource?.startsWith("env:")) {
1729
1755
  apiKey = process.env[keySource.slice(4)];
@@ -2132,6 +2158,47 @@ async function cmdReconcile() {
2132
2158
  }
2133
2159
  console.log("");
2134
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
+ }
2135
2202
  function cmdHelp() {
2136
2203
  console.log(`
2137
2204
  burnwatch \u2014 Passive cost memory for AI-assisted development
@@ -2146,6 +2213,7 @@ Usage:
2146
2213
  burnwatch reconcile Scan for untracked services
2147
2214
  burnwatch interview --json Export state for agent-driven interview
2148
2215
  burnwatch configure --service <id> [opts] Agent writes back interview answers
2216
+ burnwatch reset Remove all burnwatch config from this project
2149
2217
 
2150
2218
  Options for 'configure':
2151
2219
  --service <ID> Service to configure (required)