burnrate-cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +129 -2
- package/dist/aggregate.js +213 -0
- package/dist/cli.js +387 -0
- package/dist/config.js +23 -0
- package/dist/dashboard/app.js +440 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +131 -0
- package/dist/dashboard/styles.css +200 -0
- package/dist/demo.js +123 -0
- package/dist/format.js +47 -0
- package/dist/notify.js +32 -0
- package/dist/parsers/claude.js +80 -0
- package/dist/parsers/codex.js +93 -0
- package/dist/paths.js +26 -0
- package/dist/pricing.js +84 -0
- package/dist/server.js +99 -0
- package/dist/store.js +216 -0
- package/dist/types.js +1 -0
- package/package.json +47 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { localDate, priceAll, summarize } from "./aggregate.js";
|
|
5
|
+
import { loadConfig, PLAN_PRESETS, saveConfig } from "./config.js";
|
|
6
|
+
import { clock, duration, shortProject, tokens, usd, when } from "./format.js";
|
|
7
|
+
import { notify, openBrowser } from "./notify.js";
|
|
8
|
+
import { generateDemo } from "./demo.js";
|
|
9
|
+
import { dataDir } from "./paths.js";
|
|
10
|
+
import { PriceBook } from "./pricing.js";
|
|
11
|
+
import { createServer } from "./server.js";
|
|
12
|
+
import { Store } from "./store.js";
|
|
13
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
14
|
+
// ───────────── args ─────────────
|
|
15
|
+
const argv = process.argv.slice(2);
|
|
16
|
+
const BOOL_FLAGS = new Set(["csv", "json", "clear", "open", "no-open", "no-titles", "no-color", "verbose", "help", "version", "demo", "no-plan"]);
|
|
17
|
+
const flags = {};
|
|
18
|
+
const positional = [];
|
|
19
|
+
const extraPaths = [];
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (a.startsWith("--")) {
|
|
23
|
+
const [k, v] = a.slice(2).split("=", 2);
|
|
24
|
+
const next = argv[i + 1];
|
|
25
|
+
const isBool = BOOL_FLAGS.has(k);
|
|
26
|
+
const val = v ?? (!isBool && next !== undefined && !next.startsWith("--") ? (i++, next) : true);
|
|
27
|
+
if (k === "path")
|
|
28
|
+
extraPaths.push(String(val));
|
|
29
|
+
else
|
|
30
|
+
flags[k] = val;
|
|
31
|
+
}
|
|
32
|
+
else if (a === "-h")
|
|
33
|
+
flags.help = true;
|
|
34
|
+
else if (a === "-v")
|
|
35
|
+
flags.version = true;
|
|
36
|
+
else
|
|
37
|
+
positional.push(a);
|
|
38
|
+
}
|
|
39
|
+
const cmd = positional[0] ?? "dashboard";
|
|
40
|
+
let DIR = typeof flags.home === "string" ? flags.home : dataDir();
|
|
41
|
+
let useDefaults = true;
|
|
42
|
+
const titles = !flags["no-titles"];
|
|
43
|
+
// ───────────── colour ─────────────
|
|
44
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR && !flags["no-color"];
|
|
45
|
+
const c = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
46
|
+
const bold = c("1"), dim = c("2"), red = c("31"), green = c("32"), yellow = c("33"), orange = c("38;5;208"), cyan = c("36");
|
|
47
|
+
function meter(frac, width = 24) {
|
|
48
|
+
const f = Math.max(0, Math.min(1, frac));
|
|
49
|
+
const full = Math.round(f * width);
|
|
50
|
+
const col = f >= 1 ? red : f >= 0.8 ? yellow : green;
|
|
51
|
+
return col("█".repeat(full)) + dim("░".repeat(width - full));
|
|
52
|
+
}
|
|
53
|
+
function load(opts = {}) {
|
|
54
|
+
const store = new Store(DIR).load();
|
|
55
|
+
const res = store.scan({ extraPaths, titles, defaults: useDefaults });
|
|
56
|
+
if (!opts.quiet && flags.verbose)
|
|
57
|
+
console.error(dim(`scanned ${res.filesScanned} files, ${res.newEvents} new events`));
|
|
58
|
+
return { store, res };
|
|
59
|
+
}
|
|
60
|
+
function summary(days = 30, now) {
|
|
61
|
+
const { store } = load({ quiet: true });
|
|
62
|
+
const s = summarize(store.events, store.meta.sessions, store.meta.limits, store.meta.rateLimits, loadConfig(DIR), PriceBook.load(DIR), { days, now });
|
|
63
|
+
return { s, store };
|
|
64
|
+
}
|
|
65
|
+
// ───────────── commands ─────────────
|
|
66
|
+
function help() {
|
|
67
|
+
console.log(`${bold("burnrate")} ${dim("v" + pkg.version)} — what your coding agents cost, and when you'll hit the wall.
|
|
68
|
+
|
|
69
|
+
${bold("Usage")}
|
|
70
|
+
burnrate scan logs and open the dashboard (http://127.0.0.1:4317)
|
|
71
|
+
burnrate status spend summary + current 5-hour block forecast
|
|
72
|
+
burnrate statusline one line for Claude Code's statusLine (reads its JSON on stdin)
|
|
73
|
+
burnrate bar xbar / SwiftBar menu-bar plugin output
|
|
74
|
+
burnrate sessions [--days 30] [--limit 20] most expensive sessions
|
|
75
|
+
burnrate export [--csv|--json] [--days 30] per-message usage with cost
|
|
76
|
+
burnrate budget [--block 40] [--week 300] [--month 1000] [--clear]
|
|
77
|
+
burnrate plan <${Object.keys(PLAN_PRESETS).join("|")}> | --name X --usd N | --clear
|
|
78
|
+
burnrate watch [--interval 60] desktop alerts at 80% / 100% of budgets
|
|
79
|
+
burnrate demo try it with 30 days of synthetic usage (your data is untouched)
|
|
80
|
+
burnrate scan | reset | serve [--port 4317] [--no-open]
|
|
81
|
+
|
|
82
|
+
${bold("Options")}
|
|
83
|
+
--path <dir> extra log directory (prefix "codex:" for Codex logs); repeatable
|
|
84
|
+
--home <dir> data directory (default ~/.burnrate, or $BURNRATE_HOME)
|
|
85
|
+
--no-titles don't store session titles (first prompt)
|
|
86
|
+
--no-color plain output
|
|
87
|
+
|
|
88
|
+
Logs read: ~/.claude/projects (Claude Code, incl. subagents) and ~/.codex/sessions (Codex CLI).
|
|
89
|
+
Costs are API-equivalent estimates using public per-token prices; override in ~/.burnrate/prices.json.`);
|
|
90
|
+
}
|
|
91
|
+
function printStatus() {
|
|
92
|
+
const { s } = summary(30);
|
|
93
|
+
const b = s.block;
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(` ${orange("🔥")} ${bold("Today")} ${bold(usd(s.totals.today))} ${dim("yesterday")} ${usd(s.totals.yesterday)} ${dim("7d")} ${usd(s.totals.last7)} ${dim("month")} ${usd(s.totals.mtd)} ${dim(`→ ~${usd(s.totals.monthProjection)}`)}`);
|
|
96
|
+
if (s.plan?.valueMultiple) {
|
|
97
|
+
console.log(` ${dim("plan")} ${s.plan.name} ${usd(s.plan.monthlyUsd)}/mo ${dim("→")} ${green(`${s.plan.valueMultiple.toFixed(1)}× value`)} ${dim("(projected API-equivalent)")}`);
|
|
98
|
+
}
|
|
99
|
+
console.log();
|
|
100
|
+
if (b && b.active) {
|
|
101
|
+
console.log(` ${bold("5-hour block")} ${dim(`${clock(b.start)}–${clock(b.end)} · ${duration(b.remainingMs)} left`)}`);
|
|
102
|
+
if (b.budget) {
|
|
103
|
+
console.log(` ${meter(b.percent ?? 0)} ${bold(usd(b.cost))} ${dim("of")} ${usd(b.budget)} ${dim(b.budgetSource === "learned" ? "(learned cap)" : "")}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(` ${bold(usd(b.cost))} ${dim("spent · no block budget set (burnrate budget --block 40)")}`);
|
|
107
|
+
}
|
|
108
|
+
const rate = `${usd(b.burnPerHour)}/h`;
|
|
109
|
+
if (b.hitsBudgetAt)
|
|
110
|
+
console.log(` ${red("▲")} At ${rate} you hit the budget at ${bold(clock(b.hitsBudgetAt))} ${dim(`(in ${duration(b.hitsBudgetAt - Date.now())})`)}`);
|
|
111
|
+
else
|
|
112
|
+
console.log(` ${dim("burn")} ${rate} ${dim("· projected block total")} ${usd(b.projected)}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(` ${dim("No active 5-hour block.")}${b ? dim(` Last block ${when(b.start)}: ${usd(b.cost)}`) : ""}`);
|
|
116
|
+
}
|
|
117
|
+
for (const r of s.rateLimits) {
|
|
118
|
+
const parts = [r.primary && `5h ${r.primary.usedPercent.toFixed(0)}%`, r.secondary && `week ${r.secondary.usedPercent.toFixed(0)}%`].filter(Boolean);
|
|
119
|
+
if (parts.length)
|
|
120
|
+
console.log(` ${cyan(r.agent)} ${dim("reported limits:")} ${parts.join(dim(" · "))}${r.primary?.resetsAt ? dim(` · resets ${when(r.primary.resetsAt)}`) : ""}`);
|
|
121
|
+
}
|
|
122
|
+
if (s.week.budget)
|
|
123
|
+
console.log(` ${dim("7 days")} ${meter(s.week.percent ?? 0, 16)} ${usd(s.week.cost)} / ${usd(s.week.budget)}`);
|
|
124
|
+
if (s.month.budget)
|
|
125
|
+
console.log(` ${dim("month")} ${meter(s.month.percent ?? 0, 16)} ${usd(s.month.cost)} / ${usd(s.month.budget)}`);
|
|
126
|
+
console.log();
|
|
127
|
+
const top = s.sessions.slice(0, 5);
|
|
128
|
+
if (top.length) {
|
|
129
|
+
console.log(` ${bold("Top sessions")} ${dim("(30 days)")}`);
|
|
130
|
+
for (const x of top) {
|
|
131
|
+
console.log(` ${usd(x.cost).padStart(9)} ${(x.title ?? x.id.slice(0, 8)).slice(0, 52).padEnd(52)} ${dim(shortProject(x.project))}`);
|
|
132
|
+
}
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
if (s.totals.cacheSavings30d > 1)
|
|
136
|
+
console.log(` ${dim("Prompt caching saved")} ${green(usd(s.totals.cacheSavings30d))} ${dim("in 30 days.")}`);
|
|
137
|
+
if (s.unpricedModels.length)
|
|
138
|
+
console.log(` ${yellow("!")} ${dim("unpriced models (add to ~/.burnrate/prices.json):")} ${s.unpricedModels.join(", ")}`);
|
|
139
|
+
if (!s.eventCount)
|
|
140
|
+
console.log(dim(" No agent logs found yet. Run Claude Code or Codex, or pass --path <dir>."));
|
|
141
|
+
console.log();
|
|
142
|
+
}
|
|
143
|
+
async function readStdin(timeoutMs = 300) {
|
|
144
|
+
if (process.stdin.isTTY)
|
|
145
|
+
return "";
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
let data = "";
|
|
148
|
+
const t = setTimeout(() => resolve(data), timeoutMs);
|
|
149
|
+
process.stdin.setEncoding("utf8");
|
|
150
|
+
process.stdin.on("data", (d) => (data += d));
|
|
151
|
+
process.stdin.on("end", () => {
|
|
152
|
+
clearTimeout(t);
|
|
153
|
+
resolve(data);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async function statusline() {
|
|
158
|
+
const raw = await readStdin();
|
|
159
|
+
let sessionId = null;
|
|
160
|
+
try {
|
|
161
|
+
sessionId = JSON.parse(raw).session_id ?? null;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* not JSON */
|
|
165
|
+
}
|
|
166
|
+
const { s, store } = summary(1);
|
|
167
|
+
const parts = [];
|
|
168
|
+
if (sessionId) {
|
|
169
|
+
const prices = PriceBook.load(DIR);
|
|
170
|
+
const sc = store.events.filter((e) => e.session === sessionId).reduce((a, e) => a + (prices.cost(e) ?? 0), 0);
|
|
171
|
+
parts.push(`${orange("◆")} ${usd(sc)} ${dim("session")}`);
|
|
172
|
+
}
|
|
173
|
+
parts.push(`${usd(s.totals.today)} ${dim("today")}`);
|
|
174
|
+
const b = s.block;
|
|
175
|
+
if (b?.active) {
|
|
176
|
+
if (b.percent !== null) {
|
|
177
|
+
const pct = Math.round(b.percent * 100);
|
|
178
|
+
const col = b.percent >= 1 ? red : b.percent >= 0.8 ? yellow : green;
|
|
179
|
+
parts.push(`${col(`${pct}%`)} ${dim("block")}`);
|
|
180
|
+
}
|
|
181
|
+
parts.push(`${usd(b.burnPerHour)}/h`);
|
|
182
|
+
if (b.hitsBudgetAt)
|
|
183
|
+
parts.push(red(`limit ~${clock(b.hitsBudgetAt)}`));
|
|
184
|
+
else
|
|
185
|
+
parts.push(dim(`resets ${clock(b.end)}`));
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write(parts.join(dim(" · ")) + "\n");
|
|
188
|
+
}
|
|
189
|
+
function bar() {
|
|
190
|
+
const { s } = summary(7);
|
|
191
|
+
const b = s.block;
|
|
192
|
+
const pct = b?.active && b.percent !== null ? ` ${Math.round(b.percent * 100)}%` : "";
|
|
193
|
+
const warn = b?.hitsBudgetAt ? " ⚠︎" : "";
|
|
194
|
+
console.log(`🔥 ${usd(s.totals.today)}${pct}${warn}`);
|
|
195
|
+
console.log("---");
|
|
196
|
+
console.log(`Today ${usd(s.totals.today)} · 7 days ${usd(s.totals.last7)} | font=Menlo`);
|
|
197
|
+
console.log(`Month ${usd(s.totals.mtd)} → ~${usd(s.totals.monthProjection)} | font=Menlo`);
|
|
198
|
+
if (b?.active) {
|
|
199
|
+
console.log("---");
|
|
200
|
+
console.log(`Block ${clock(b.start)}–${clock(b.end)}: ${usd(b.cost)}${b.budget ? ` of ${usd(b.budget)}` : ""}`);
|
|
201
|
+
console.log(`Burn ${usd(b.burnPerHour)}/h · ${duration(b.remainingMs)} left`);
|
|
202
|
+
if (b.hitsBudgetAt)
|
|
203
|
+
console.log(`Hits budget at ${clock(b.hitsBudgetAt)} | color=#d03b3b`);
|
|
204
|
+
}
|
|
205
|
+
console.log("---");
|
|
206
|
+
for (const x of s.sessions.slice(0, 5))
|
|
207
|
+
console.log(`${usd(x.cost)} ${(x.title ?? x.id).slice(0, 40).replace(/\|/g, "/")} | font=Menlo size=12`);
|
|
208
|
+
console.log("---");
|
|
209
|
+
console.log("Open dashboard | href=http://127.0.0.1:4317");
|
|
210
|
+
}
|
|
211
|
+
function sessions() {
|
|
212
|
+
const days = Number(flags.days) || 30;
|
|
213
|
+
const limit = Number(flags.limit) || 20;
|
|
214
|
+
const { s } = summary(days);
|
|
215
|
+
console.log(`\n ${bold(`Most expensive sessions — last ${days} days`)}\n`);
|
|
216
|
+
for (const x of s.sessions.slice(0, limit)) {
|
|
217
|
+
console.log(` ${bold(usd(x.cost).padStart(9))} ${dim(when(x.start).padEnd(10))} ${(x.title ?? x.id.slice(0, 8)).slice(0, 56).padEnd(56)} ${dim(`${shortProject(x.project)} · ${tokens(x.tokens)} tok`)}`);
|
|
218
|
+
}
|
|
219
|
+
console.log();
|
|
220
|
+
}
|
|
221
|
+
function exportData() {
|
|
222
|
+
const days = Number(flags.days) || 30;
|
|
223
|
+
const { store } = load({ quiet: true });
|
|
224
|
+
const since = Date.now() - days * 86400_000;
|
|
225
|
+
const rows = priceAll(store.events, PriceBook.load(DIR)).filter((e) => e.ts >= since);
|
|
226
|
+
if (flags.json) {
|
|
227
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const cols = ["timestamp", "date", "agent", "session", "title", "project", "model", "input", "output", "cache_read", "cache_write_5m", "cache_write_1h", "web_searches", "cost_usd"];
|
|
231
|
+
const q = (v) => {
|
|
232
|
+
const str = String(v ?? "");
|
|
233
|
+
return /[",\n]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str;
|
|
234
|
+
};
|
|
235
|
+
const out = [cols.join(",")];
|
|
236
|
+
for (const e of rows) {
|
|
237
|
+
out.push([new Date(e.ts).toISOString(), localDate(e.ts), e.agent, e.session, store.meta.sessions[e.session]?.title ?? "", e.project, e.model, e.input, e.output, e.cacheRead, e.cacheWrite5m, e.cacheWrite1h, e.webSearches, e.priced ? e.cost.toFixed(6) : ""]
|
|
238
|
+
.map(q)
|
|
239
|
+
.join(","));
|
|
240
|
+
}
|
|
241
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
242
|
+
}
|
|
243
|
+
function budget() {
|
|
244
|
+
const cfg = loadConfig(DIR);
|
|
245
|
+
if (flags.clear)
|
|
246
|
+
cfg.budgets = {};
|
|
247
|
+
const set = (k, f) => {
|
|
248
|
+
if (flags[f] !== undefined) {
|
|
249
|
+
const v = Number(flags[f]);
|
|
250
|
+
cfg.budgets[k] = v > 0 ? v : undefined;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
set("blockUsd", "block");
|
|
254
|
+
set("weekUsd", "week");
|
|
255
|
+
set("monthUsd", "month");
|
|
256
|
+
saveConfig(DIR, cfg);
|
|
257
|
+
const b = cfg.budgets;
|
|
258
|
+
console.log(`Budgets — 5h block: ${usd(b.blockUsd ?? null)} · 7 days: ${usd(b.weekUsd ?? null)} · month: ${usd(b.monthUsd ?? null)}`);
|
|
259
|
+
if (!b.blockUsd)
|
|
260
|
+
console.log(dim("Tip: without a block budget, Burnrate uses the cap it learned from your last limit hit (if any)."));
|
|
261
|
+
}
|
|
262
|
+
function plan() {
|
|
263
|
+
const cfg = loadConfig(DIR);
|
|
264
|
+
const preset = positional[1];
|
|
265
|
+
if (flags.clear)
|
|
266
|
+
cfg.plan = undefined;
|
|
267
|
+
else if (preset && PLAN_PRESETS[preset])
|
|
268
|
+
cfg.plan = PLAN_PRESETS[preset];
|
|
269
|
+
else if (flags.usd)
|
|
270
|
+
cfg.plan = { name: String(flags.name ?? "My plan"), monthlyUsd: Number(flags.usd) };
|
|
271
|
+
else if (preset) {
|
|
272
|
+
console.error(`Unknown plan "${preset}". Presets: ${Object.keys(PLAN_PRESETS).join(", ")}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
saveConfig(DIR, cfg);
|
|
276
|
+
console.log(cfg.plan ? `Plan: ${cfg.plan.name} (${usd(cfg.plan.monthlyUsd)}/month)` : "No plan set.");
|
|
277
|
+
}
|
|
278
|
+
async function watch() {
|
|
279
|
+
const interval = Math.max(Number(flags.interval) || 60, 10) * 1000;
|
|
280
|
+
const fired = new Set();
|
|
281
|
+
console.log(dim(`Watching every ${interval / 1000}s. Ctrl+C to stop.`));
|
|
282
|
+
const tick = () => {
|
|
283
|
+
const { s } = summary(7);
|
|
284
|
+
const cfg = loadConfig(DIR);
|
|
285
|
+
const checks = [
|
|
286
|
+
[`block:${s.block?.start}`, s.block?.active ? s.block.percent : null, "5-hour block"],
|
|
287
|
+
[`week`, s.week.percent, "7-day budget"],
|
|
288
|
+
[`month:${new Date().getMonth()}`, s.month.percent, "monthly budget"],
|
|
289
|
+
];
|
|
290
|
+
for (const [key, pct, label] of checks) {
|
|
291
|
+
if (pct === null)
|
|
292
|
+
continue;
|
|
293
|
+
for (const t of cfg.alertAt) {
|
|
294
|
+
const k = `${key}:${t}`;
|
|
295
|
+
if (pct >= t && !fired.has(k)) {
|
|
296
|
+
fired.add(k);
|
|
297
|
+
const extra = label === "5-hour block" && s.block?.hitsBudgetAt ? ` Limit ~${clock(s.block.hitsBudgetAt)}.` : "";
|
|
298
|
+
notify(`Burnrate: ${Math.round(pct * 100)}% of ${label}`, `${usd(label === "5-hour block" ? s.block.cost : label === "7-day budget" ? s.week.cost : s.month.cost)} spent.${extra}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
tick();
|
|
304
|
+
setInterval(tick, interval);
|
|
305
|
+
}
|
|
306
|
+
function serve(open) {
|
|
307
|
+
const port = Number(flags.port) || 4317;
|
|
308
|
+
const { res } = load();
|
|
309
|
+
const server = createServer({ dataDir: DIR, extraPaths, titles, defaults: useDefaults, port });
|
|
310
|
+
server.on("error", (err) => {
|
|
311
|
+
if (err.code === "EADDRINUSE") {
|
|
312
|
+
console.log(`Burnrate is already running → http://127.0.0.1:${port}`);
|
|
313
|
+
if (open)
|
|
314
|
+
openBrowser(`http://127.0.0.1:${port}`);
|
|
315
|
+
process.exit(0);
|
|
316
|
+
}
|
|
317
|
+
throw err;
|
|
318
|
+
});
|
|
319
|
+
server.listen(port, "127.0.0.1", () => {
|
|
320
|
+
const url = `http://127.0.0.1:${port}`;
|
|
321
|
+
console.log(`\n ${orange("🔥")} ${bold("Burnrate")} ${dim("v" + pkg.version)} ${url}`);
|
|
322
|
+
for (const r of res.roots)
|
|
323
|
+
console.log(dim(` ${r.agent.padEnd(12)} ${r.files} log files ${r.dir}`));
|
|
324
|
+
if (!res.roots.length)
|
|
325
|
+
console.log(yellow(" No Claude Code or Codex logs found yet."));
|
|
326
|
+
console.log(dim(" Ctrl+C to stop\n"));
|
|
327
|
+
if (open)
|
|
328
|
+
openBrowser(url);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async function main() {
|
|
332
|
+
if (flags.version)
|
|
333
|
+
return console.log(pkg.version);
|
|
334
|
+
if (flags.help || cmd === "help")
|
|
335
|
+
return help();
|
|
336
|
+
if (cmd === "demo" || flags.demo) {
|
|
337
|
+
// Synthetic 30-day history in a temp dir — never touches your real data.
|
|
338
|
+
const demo = generateDemo(Number(flags.days) || 30);
|
|
339
|
+
DIR = `${demo.root}/home`;
|
|
340
|
+
useDefaults = false;
|
|
341
|
+
extraPaths.push(demo.claude, `codex:${demo.codex}`);
|
|
342
|
+
if (!flags["no-plan"])
|
|
343
|
+
saveConfig(DIR, { budgets: { blockUsd: 28, monthUsd: 600 }, plan: PLAN_PRESETS["claude-max-20x"], alertAt: [0.8, 1] });
|
|
344
|
+
if (cmd === "demo")
|
|
345
|
+
return serve(!flags["no-open"]);
|
|
346
|
+
}
|
|
347
|
+
switch (cmd) {
|
|
348
|
+
case "dashboard":
|
|
349
|
+
return serve(!flags["no-open"]);
|
|
350
|
+
case "serve":
|
|
351
|
+
return serve(Boolean(flags.open));
|
|
352
|
+
case "status":
|
|
353
|
+
return printStatus();
|
|
354
|
+
case "statusline":
|
|
355
|
+
return statusline();
|
|
356
|
+
case "bar":
|
|
357
|
+
return bar();
|
|
358
|
+
case "sessions":
|
|
359
|
+
return sessions();
|
|
360
|
+
case "export":
|
|
361
|
+
return exportData();
|
|
362
|
+
case "budget":
|
|
363
|
+
return budget();
|
|
364
|
+
case "plan":
|
|
365
|
+
return plan();
|
|
366
|
+
case "watch":
|
|
367
|
+
return watch();
|
|
368
|
+
case "scan": {
|
|
369
|
+
const { res, store } = load();
|
|
370
|
+
for (const r of res.roots)
|
|
371
|
+
console.log(`${r.agent.padEnd(12)} ${String(r.files).padStart(5)} files ${r.dir}`);
|
|
372
|
+
console.log(`${store.events.length} usage events (${res.newEvents} new) · data in ${DIR}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
case "reset":
|
|
376
|
+
new Store(DIR).reset();
|
|
377
|
+
fs.rmSync(DIR + "/events.ndjson", { force: true });
|
|
378
|
+
return console.log(`Cleared cached usage in ${DIR} (config kept).`);
|
|
379
|
+
default:
|
|
380
|
+
console.error(`Unknown command "${cmd}". Run burnrate --help.`);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
main().catch((err) => {
|
|
385
|
+
console.error(red(String(err?.stack ?? err)));
|
|
386
|
+
process.exit(1);
|
|
387
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const DEFAULT_CONFIG = { budgets: {}, alertAt: [0.8, 1] };
|
|
4
|
+
export function loadConfig(dir) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, "config.json"), "utf8"));
|
|
7
|
+
return { ...DEFAULT_CONFIG, ...raw, budgets: { ...raw.budgets } };
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function saveConfig(dir, cfg) {
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(dir, "config.json"), JSON.stringify(cfg, null, 2) + "\n");
|
|
16
|
+
}
|
|
17
|
+
export const PLAN_PRESETS = {
|
|
18
|
+
"claude-pro": { name: "Claude Pro", monthlyUsd: 20 },
|
|
19
|
+
"claude-max-5x": { name: "Claude Max 5x", monthlyUsd: 100 },
|
|
20
|
+
"claude-max-20x": { name: "Claude Max 20x", monthlyUsd: 200 },
|
|
21
|
+
"chatgpt-plus": { name: "ChatGPT Plus", monthlyUsd: 20 },
|
|
22
|
+
"chatgpt-pro": { name: "ChatGPT Pro", monthlyUsd: 200 },
|
|
23
|
+
};
|