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/pricing.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const WEB_SEARCH_USD = 10 / 1000;
|
|
4
|
+
export const BUILTIN_PRICES = {
|
|
5
|
+
// Anthropic — Claude 5 family
|
|
6
|
+
"claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25 },
|
|
7
|
+
"claude-mythos-5-1": { input: 10, output: 50, cacheRead: 0.25 },
|
|
8
|
+
"claude-fable-5": { input: 10, output: 50, cacheRead: 1 },
|
|
9
|
+
"claude-mythos-5": { input: 10, output: 50, cacheRead: 1 },
|
|
10
|
+
"claude-opus-5-5": { input: 4, output: 20, cacheRead: 0.2 },
|
|
11
|
+
"claude-opus-5": { input: 5, output: 25, cacheRead: 0.5 },
|
|
12
|
+
"claude-sonnet-5-5": { input: 2, output: 10, cacheRead: 0.2 },
|
|
13
|
+
"claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2 },
|
|
14
|
+
"claude-haiku-5-5": { input: 0.1, output: 0.5, cacheRead: 0.01 },
|
|
15
|
+
// Anthropic — Claude 4 family
|
|
16
|
+
"claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5 },
|
|
17
|
+
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5 },
|
|
18
|
+
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5 },
|
|
19
|
+
"claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5 },
|
|
20
|
+
"claude-opus-4-1": { input: 15, output: 75, cacheRead: 1.5 },
|
|
21
|
+
"claude-opus-4": { input: 15, output: 75, cacheRead: 1.5 },
|
|
22
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3 },
|
|
23
|
+
"claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3 },
|
|
24
|
+
"claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3 },
|
|
25
|
+
"claude-3-7-sonnet": { input: 3, output: 15, cacheRead: 0.3 },
|
|
26
|
+
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1 },
|
|
27
|
+
"claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08 },
|
|
28
|
+
// OpenAI — models used by Codex CLI (cached input = cacheRead)
|
|
29
|
+
"gpt-5-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
30
|
+
"gpt-5.1-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
31
|
+
"gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
32
|
+
"gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
|
|
33
|
+
"gpt-5-nano": { input: 0.05, output: 0.4, cacheRead: 0.005 },
|
|
34
|
+
"gpt-5": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
35
|
+
"o4-mini": { input: 1.1, output: 4.4, cacheRead: 0.275 },
|
|
36
|
+
"o3": { input: 2, output: 8, cacheRead: 0.5 },
|
|
37
|
+
"gpt-4.1": { input: 2, output: 8, cacheRead: 0.5 },
|
|
38
|
+
};
|
|
39
|
+
export class PriceBook {
|
|
40
|
+
table;
|
|
41
|
+
keys;
|
|
42
|
+
cache = new Map();
|
|
43
|
+
constructor(overrides = {}) {
|
|
44
|
+
this.table = { ...BUILTIN_PRICES, ...overrides };
|
|
45
|
+
// Longest key first so "gpt-5-codex" wins over "gpt-5".
|
|
46
|
+
this.keys = Object.keys(this.table).sort((a, b) => b.length - a.length);
|
|
47
|
+
}
|
|
48
|
+
static load(dir) {
|
|
49
|
+
const file = path.join(dir, "prices.json");
|
|
50
|
+
try {
|
|
51
|
+
return new PriceBook(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return new PriceBook();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
lookup(model) {
|
|
58
|
+
if (this.cache.has(model))
|
|
59
|
+
return this.cache.get(model);
|
|
60
|
+
const m = model.toLowerCase().replace(/^(anthropic\.|us\.anthropic\.|openai\/)/, "").replace(/@.*$/, "");
|
|
61
|
+
let found = this.table[m] ?? null;
|
|
62
|
+
if (!found) {
|
|
63
|
+
const k = this.keys.find((key) => m.startsWith(key) || m.startsWith(key + "-"));
|
|
64
|
+
found = k ? this.table[k] : null;
|
|
65
|
+
}
|
|
66
|
+
this.cache.set(model, found);
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
/** USD cost of one event; null when the model is unpriced. */
|
|
70
|
+
cost(e) {
|
|
71
|
+
const p = this.lookup(e.model);
|
|
72
|
+
if (!p)
|
|
73
|
+
return null;
|
|
74
|
+
const w5 = p.cacheWrite5m ?? p.input * 1.25;
|
|
75
|
+
const w1h = p.cacheWrite1h ?? p.input * 2;
|
|
76
|
+
return ((e.input * p.input +
|
|
77
|
+
e.output * p.output +
|
|
78
|
+
e.cacheRead * p.cacheRead +
|
|
79
|
+
e.cacheWrite5m * w5 +
|
|
80
|
+
e.cacheWrite1h * w1h) /
|
|
81
|
+
1e6 +
|
|
82
|
+
e.webSearches * WEB_SEARCH_USD);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { summarize } from "./aggregate.js";
|
|
6
|
+
import { loadConfig, PLAN_PRESETS, saveConfig } from "./config.js";
|
|
7
|
+
import { PriceBook } from "./pricing.js";
|
|
8
|
+
import { Store } from "./store.js";
|
|
9
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ASSETS = {
|
|
11
|
+
"/": "index.html",
|
|
12
|
+
"/index.html": "index.html",
|
|
13
|
+
"/app.js": "app.js",
|
|
14
|
+
"/styles.css": "styles.css",
|
|
15
|
+
"/favicon.svg": "favicon.svg",
|
|
16
|
+
};
|
|
17
|
+
const TYPES = {
|
|
18
|
+
".html": "text/html; charset=utf-8",
|
|
19
|
+
".js": "text/javascript; charset=utf-8",
|
|
20
|
+
".css": "text/css; charset=utf-8",
|
|
21
|
+
".svg": "image/svg+xml",
|
|
22
|
+
};
|
|
23
|
+
function send(res, status, body, type = "application/json") {
|
|
24
|
+
res.writeHead(status, {
|
|
25
|
+
"Content-Type": type,
|
|
26
|
+
"Cache-Control": "no-store",
|
|
27
|
+
"X-Content-Type-Options": "nosniff",
|
|
28
|
+
"Content-Security-Policy": "default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'",
|
|
29
|
+
"Referrer-Policy": "no-referrer",
|
|
30
|
+
});
|
|
31
|
+
res.end(body);
|
|
32
|
+
}
|
|
33
|
+
function readBody(req, limit = 64 * 1024) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
let size = 0;
|
|
36
|
+
const chunks = [];
|
|
37
|
+
req.on("data", (c) => {
|
|
38
|
+
size += c.length;
|
|
39
|
+
if (size > limit) {
|
|
40
|
+
reject(new Error("too large"));
|
|
41
|
+
req.destroy();
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
chunks.push(c);
|
|
45
|
+
});
|
|
46
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
47
|
+
req.on("error", reject);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
51
|
+
export function createServer(opts) {
|
|
52
|
+
const store = new Store(opts.dataDir).load();
|
|
53
|
+
let lastScan = 0;
|
|
54
|
+
const allowedHosts = new Set([`127.0.0.1:${opts.port}`, `localhost:${opts.port}`, `[::1]:${opts.port}`]);
|
|
55
|
+
return http.createServer(async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
// DNS-rebinding protection: only answer to localhost host headers.
|
|
58
|
+
if (!req.headers.host || !allowedHosts.has(req.headers.host))
|
|
59
|
+
return send(res, 403, '{"error":"forbidden host"}');
|
|
60
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
61
|
+
if (req.method === "GET" && ASSETS[url.pathname]) {
|
|
62
|
+
const file = path.join(here, "dashboard", ASSETS[url.pathname]);
|
|
63
|
+
return send(res, 200, fs.readFileSync(file), TYPES[path.extname(file)]);
|
|
64
|
+
}
|
|
65
|
+
if (req.method === "GET" && url.pathname === "/api/summary") {
|
|
66
|
+
if (Date.now() - lastScan > 2000) {
|
|
67
|
+
store.scan({ extraPaths: opts.extraPaths, titles: opts.titles, defaults: opts.defaults });
|
|
68
|
+
lastScan = Date.now();
|
|
69
|
+
}
|
|
70
|
+
const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 30, 1), 365);
|
|
71
|
+
const cfg = loadConfig(opts.dataDir);
|
|
72
|
+
const s = summarize(store.events, store.meta.sessions, store.meta.limits, store.meta.rateLimits, cfg, PriceBook.load(opts.dataDir), { days });
|
|
73
|
+
return send(res, 200, JSON.stringify({ ...s, config: cfg, planPresets: PLAN_PRESETS }));
|
|
74
|
+
}
|
|
75
|
+
if (req.method === "POST" && url.pathname === "/api/config") {
|
|
76
|
+
// CSRF: require same-origin JSON.
|
|
77
|
+
const origin = req.headers.origin;
|
|
78
|
+
if (!origin || !allowedHosts.has(new URL(origin).host) || !String(req.headers["content-type"]).startsWith("application/json")) {
|
|
79
|
+
return send(res, 403, '{"error":"forbidden"}');
|
|
80
|
+
}
|
|
81
|
+
const body = JSON.parse(await readBody(req));
|
|
82
|
+
const cfg = loadConfig(opts.dataDir);
|
|
83
|
+
if (body.budgets && typeof body.budgets === "object") {
|
|
84
|
+
cfg.budgets = { blockUsd: num(body.budgets.blockUsd), weekUsd: num(body.budgets.weekUsd), monthUsd: num(body.budgets.monthUsd) };
|
|
85
|
+
}
|
|
86
|
+
if ("plan" in body) {
|
|
87
|
+
const p = body.plan;
|
|
88
|
+
cfg.plan = p && typeof p.name === "string" && num(p.monthlyUsd) ? { name: p.name.slice(0, 40), monthlyUsd: num(p.monthlyUsd) } : undefined;
|
|
89
|
+
}
|
|
90
|
+
saveConfig(opts.dataDir, cfg);
|
|
91
|
+
return send(res, 200, JSON.stringify({ ok: true, config: cfg }));
|
|
92
|
+
}
|
|
93
|
+
send(res, 404, '{"error":"not found"}');
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
send(res, 500, JSON.stringify({ error: err instanceof Error ? err.message : "error" }));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseClaudeLine } from "./parsers/claude.js";
|
|
4
|
+
import { parseCodexLine } from "./parsers/codex.js";
|
|
5
|
+
import { logRoots } from "./paths.js";
|
|
6
|
+
const STATE_VERSION = 1;
|
|
7
|
+
function walk(dir, out = []) {
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
for (const e of entries) {
|
|
16
|
+
const p = path.join(dir, e.name);
|
|
17
|
+
if (e.isDirectory())
|
|
18
|
+
walk(p, out);
|
|
19
|
+
else if (e.isFile() && e.name.endsWith(".jsonl"))
|
|
20
|
+
out.push(p);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Append-only local store in ~/.burnrate:
|
|
26
|
+
* events.ndjson one UsageEvent per line (unique by key)
|
|
27
|
+
* state.json per-file byte offsets for incremental parsing
|
|
28
|
+
* meta.json sessions, limit hits, latest rate-limit snapshots
|
|
29
|
+
*/
|
|
30
|
+
export class Store {
|
|
31
|
+
dir;
|
|
32
|
+
eventsFile;
|
|
33
|
+
stateFile;
|
|
34
|
+
metaFile;
|
|
35
|
+
files = {};
|
|
36
|
+
meta = { sessions: {}, limits: [], rateLimits: {} };
|
|
37
|
+
events = [];
|
|
38
|
+
keys = new Set();
|
|
39
|
+
constructor(dir) {
|
|
40
|
+
this.dir = dir;
|
|
41
|
+
this.eventsFile = path.join(dir, "events.ndjson");
|
|
42
|
+
this.stateFile = path.join(dir, "state.json");
|
|
43
|
+
this.metaFile = path.join(dir, "meta.json");
|
|
44
|
+
}
|
|
45
|
+
load() {
|
|
46
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
47
|
+
try {
|
|
48
|
+
const s = JSON.parse(fs.readFileSync(this.stateFile, "utf8"));
|
|
49
|
+
if (s.version === STATE_VERSION)
|
|
50
|
+
this.files = s.files ?? {};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* fresh */
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
this.meta = { sessions: {}, limits: [], rateLimits: {}, ...JSON.parse(fs.readFileSync(this.metaFile, "utf8")) };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* fresh */
|
|
60
|
+
}
|
|
61
|
+
if (!Object.keys(this.files).length) {
|
|
62
|
+
// No state → rebuild events from scratch to stay consistent.
|
|
63
|
+
fs.rmSync(this.eventsFile, { force: true });
|
|
64
|
+
this.meta = { sessions: {}, limits: [], rateLimits: {} };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
for (const line of fs.readFileSync(this.eventsFile, "utf8").split("\n")) {
|
|
68
|
+
if (!line)
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
const e = JSON.parse(line);
|
|
72
|
+
if (!this.keys.has(e.key)) {
|
|
73
|
+
this.keys.add(e.key);
|
|
74
|
+
this.events.push(e);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* skip torn line */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* no events yet */
|
|
84
|
+
}
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
apply(rec, fresh) {
|
|
88
|
+
switch (rec.kind) {
|
|
89
|
+
case "usage":
|
|
90
|
+
if (!this.keys.has(rec.event.key)) {
|
|
91
|
+
this.keys.add(rec.event.key);
|
|
92
|
+
this.events.push(rec.event);
|
|
93
|
+
fresh.push(rec.event);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case "session": {
|
|
97
|
+
const cur = this.meta.sessions[rec.session];
|
|
98
|
+
if (!cur) {
|
|
99
|
+
this.meta.sessions[rec.session] = {
|
|
100
|
+
agent: rec.meta.agent,
|
|
101
|
+
title: rec.meta.title ?? null,
|
|
102
|
+
project: rec.meta.project ?? "",
|
|
103
|
+
firstTs: rec.meta.firstTs ?? Date.now(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (!cur.title && rec.meta.title)
|
|
108
|
+
cur.title = rec.meta.title;
|
|
109
|
+
if (!cur.project && rec.meta.project)
|
|
110
|
+
cur.project = rec.meta.project;
|
|
111
|
+
if (rec.meta.firstTs && Number.isFinite(rec.meta.firstTs) && rec.meta.firstTs < cur.firstTs)
|
|
112
|
+
cur.firstTs = rec.meta.firstTs;
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "limit": {
|
|
117
|
+
const dup = this.meta.limits.some((l) => l.agent === rec.hit.agent && Math.abs(l.ts - rec.hit.ts) < 60_000);
|
|
118
|
+
if (!dup)
|
|
119
|
+
this.meta.limits.push(rec.hit);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "ratelimit": {
|
|
123
|
+
const cur = this.meta.rateLimits[rec.snapshot.agent];
|
|
124
|
+
if (!cur || rec.snapshot.ts >= cur.ts)
|
|
125
|
+
this.meta.rateLimits[rec.snapshot.agent] = rec.snapshot;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
readFile(root, file, fresh, titles) {
|
|
131
|
+
let st;
|
|
132
|
+
try {
|
|
133
|
+
st = fs.statSync(file);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let fs0 = this.files[file];
|
|
139
|
+
if (fs0 && st.size === fs0.size && st.mtimeMs === fs0.mtimeMs)
|
|
140
|
+
return;
|
|
141
|
+
if (!fs0 || st.size < fs0.offset)
|
|
142
|
+
fs0 = { size: 0, mtimeMs: 0, offset: 0 };
|
|
143
|
+
if (root.agent === "codex" && !fs0.codex)
|
|
144
|
+
fs0.codex = { session: path.basename(file, ".jsonl"), project: "", model: "" };
|
|
145
|
+
const fd = fs.openSync(file, "r");
|
|
146
|
+
const CHUNK = 8 * 1024 * 1024;
|
|
147
|
+
let offset = fs0.offset;
|
|
148
|
+
let carry = Buffer.alloc(0);
|
|
149
|
+
try {
|
|
150
|
+
while (offset + carry.length < st.size) {
|
|
151
|
+
const want = Math.min(CHUNK, st.size - offset - carry.length);
|
|
152
|
+
const chunk = Buffer.alloc(want);
|
|
153
|
+
const read = fs.readSync(fd, chunk, 0, want, offset + carry.length);
|
|
154
|
+
if (read <= 0)
|
|
155
|
+
break;
|
|
156
|
+
const buf = carry.length ? Buffer.concat([carry, chunk.subarray(0, read)]) : chunk.subarray(0, read);
|
|
157
|
+
const lastNl = buf.lastIndexOf(0x0a);
|
|
158
|
+
if (lastNl === -1) {
|
|
159
|
+
carry = buf;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
let pos = 0;
|
|
163
|
+
while (pos <= lastNl) {
|
|
164
|
+
const nl = buf.indexOf(0x0a, pos);
|
|
165
|
+
const line = buf.subarray(pos, nl).toString("utf8");
|
|
166
|
+
const lineOffset = offset + pos;
|
|
167
|
+
pos = nl + 1;
|
|
168
|
+
if (!line.trim())
|
|
169
|
+
continue;
|
|
170
|
+
const recs = root.agent === "codex" ? parseCodexLine(line, file, lineOffset, fs0.codex) : parseClaudeLine(line, file);
|
|
171
|
+
for (const r of recs) {
|
|
172
|
+
if (!titles && r.kind === "session")
|
|
173
|
+
r.meta.title = undefined;
|
|
174
|
+
this.apply(r, fresh);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
offset += lastNl + 1;
|
|
178
|
+
carry = Buffer.from(buf.subarray(lastNl + 1));
|
|
179
|
+
}
|
|
180
|
+
// Only complete lines are consumed; a partial trailing line is re-read next scan.
|
|
181
|
+
this.files[file] = { ...fs0, size: st.size, mtimeMs: st.mtimeMs, offset };
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
fs.closeSync(fd);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
scan(opts = {}) {
|
|
188
|
+
const fresh = [];
|
|
189
|
+
const roots = logRoots(opts.extraPaths, opts.defaults !== false);
|
|
190
|
+
const summary = [];
|
|
191
|
+
let filesScanned = 0;
|
|
192
|
+
for (const root of roots) {
|
|
193
|
+
const files = walk(root.dir);
|
|
194
|
+
if (files.length)
|
|
195
|
+
summary.push({ agent: root.agent, dir: root.dir, files: files.length });
|
|
196
|
+
for (const f of files) {
|
|
197
|
+
this.readFile(root, f, fresh, opts.titles !== false);
|
|
198
|
+
filesScanned++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (fresh.length)
|
|
202
|
+
fs.appendFileSync(this.eventsFile, fresh.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
203
|
+
this.meta.limits.sort((a, b) => a.ts - b.ts);
|
|
204
|
+
fs.writeFileSync(this.stateFile, JSON.stringify({ version: STATE_VERSION, files: this.files }));
|
|
205
|
+
fs.writeFileSync(this.metaFile, JSON.stringify(this.meta));
|
|
206
|
+
return { filesScanned, newEvents: fresh.length, roots: summary };
|
|
207
|
+
}
|
|
208
|
+
reset() {
|
|
209
|
+
for (const f of [this.eventsFile, this.stateFile, this.metaFile])
|
|
210
|
+
fs.rmSync(f, { force: true });
|
|
211
|
+
this.files = {};
|
|
212
|
+
this.events = [];
|
|
213
|
+
this.keys.clear();
|
|
214
|
+
this.meta = { sessions: {}, limits: [], rateLimits: {} };
|
|
215
|
+
}
|
|
216
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "burnrate-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "See what your coding agents really cost \u2014 and when you'll hit the limit. Local, multi-agent spend & quota meter for Claude Code and Codex.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"burnrate": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
|
|
19
|
+
"dev": "npm run build && node dist/cli.js",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build && npm test"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"claude-code",
|
|
26
|
+
"codex",
|
|
27
|
+
"ai",
|
|
28
|
+
"cost",
|
|
29
|
+
"usage",
|
|
30
|
+
"tokens",
|
|
31
|
+
"quota",
|
|
32
|
+
"cli",
|
|
33
|
+
"statusline"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.20.5",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vitest": "^5.1.0"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/pidoxy/burnrate.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/pidoxy/burnrate#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/pidoxy/burnrate/issues"
|
|
48
|
+
}
|
|
49
|
+
}
|