whoburnedmore 0.5.0 → 0.8.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/README.md +2 -2
- package/dist/index.js +436 -252
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ npx whoburnedmore --local
|
|
|
62
62
|
| `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
|
|
63
63
|
| `npx whoburnedmore login` | Sign in to claim a public handle + join the leaderboard |
|
|
64
64
|
| `npx whoburnedmore logout` | Forget the local token (your data is untouched) |
|
|
65
|
-
| `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (
|
|
65
|
+
| `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (hourly) |
|
|
66
66
|
| `npx whoburnedmore uninstall-sync` | Remove the background sync |
|
|
67
67
|
|
|
68
68
|
## Supported tools
|
|
@@ -88,7 +88,7 @@ It uses a device flow — a code appears in your terminal, you approve it in the
|
|
|
88
88
|
Want your dashboard to stay fresh without re-running by hand?
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
npx whoburnedmore install-sync # background sync
|
|
91
|
+
npx whoburnedmore install-sync # background sync hourly (launchd / cron / scheduled task)
|
|
92
92
|
npx whoburnedmore uninstall-sync # remove it
|
|
93
93
|
```
|
|
94
94
|
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createRequire as createRequire4 } from "node:module";
|
|
|
12
12
|
import { platform as platform3 } from "node:os";
|
|
13
13
|
import { join as join7 } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
15
|
-
import
|
|
15
|
+
import pc3 from "picocolors";
|
|
16
16
|
|
|
17
17
|
// src/args.ts
|
|
18
18
|
function parseBoard(args) {
|
|
@@ -29,6 +29,9 @@ function parseBoard(args) {
|
|
|
29
29
|
function apiBase() {
|
|
30
30
|
return process.env.WHOBURNEDMORE_API ?? "https://api.whoburnedmore.com";
|
|
31
31
|
}
|
|
32
|
+
function webBase() {
|
|
33
|
+
return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
|
|
34
|
+
}
|
|
32
35
|
async function readJson(res) {
|
|
33
36
|
const text = await res.text();
|
|
34
37
|
if (!text) return {};
|
|
@@ -40,15 +43,12 @@ async function readJson(res) {
|
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
|
-
async function post(path, body
|
|
46
|
+
async function post(path, body) {
|
|
44
47
|
let res;
|
|
45
48
|
try {
|
|
46
49
|
res = await fetch(`${apiBase()}${path}`, {
|
|
47
50
|
method: "POST",
|
|
48
|
-
headers: {
|
|
49
|
-
"Content-Type": "application/json",
|
|
50
|
-
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
51
|
-
},
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
52
|
body: JSON.stringify(body)
|
|
53
53
|
});
|
|
54
54
|
} catch {
|
|
@@ -58,34 +58,6 @@ async function post(path, body, token) {
|
|
|
58
58
|
}
|
|
59
59
|
return { status: res.status, body: await readJson(res) };
|
|
60
60
|
}
|
|
61
|
-
async function deviceStart() {
|
|
62
|
-
const { status, body } = await post("/v1/auth/device", {});
|
|
63
|
-
if (status !== 200) throw new Error(`device auth failed (HTTP ${status})`);
|
|
64
|
-
return body;
|
|
65
|
-
}
|
|
66
|
-
async function devicePoll(deviceCode) {
|
|
67
|
-
const { body } = await post("/v1/auth/device/token", {
|
|
68
|
-
deviceCode
|
|
69
|
-
});
|
|
70
|
-
return body;
|
|
71
|
-
}
|
|
72
|
-
async function submitUsage(token, payload) {
|
|
73
|
-
const { status, body } = await post(
|
|
74
|
-
"/v1/submit",
|
|
75
|
-
payload,
|
|
76
|
-
token
|
|
77
|
-
);
|
|
78
|
-
if (status === 401) {
|
|
79
|
-
throw new Error("session expired \u2014 run `npx whoburnedmore login` again");
|
|
80
|
-
}
|
|
81
|
-
if (status !== 200) {
|
|
82
|
-
const err = body;
|
|
83
|
-
const details = err.details?.length ? `
|
|
84
|
-
- ${err.details.join("\n - ")}` : "";
|
|
85
|
-
throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
|
|
86
|
-
}
|
|
87
|
-
return body;
|
|
88
|
-
}
|
|
89
61
|
async function anonSubmit(anonKey, payload) {
|
|
90
62
|
const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
|
|
91
63
|
if (status !== 200) {
|
|
@@ -122,14 +94,14 @@ async function anonRemove(anonKey) {
|
|
|
122
94
|
|
|
123
95
|
// src/autosync.ts
|
|
124
96
|
import { spawnSync } from "node:child_process";
|
|
125
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync
|
|
97
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
126
98
|
import { homedir as homedir2, platform } from "node:os";
|
|
127
99
|
import { join as join2 } from "node:path";
|
|
128
100
|
import { fileURLToPath } from "node:url";
|
|
129
101
|
|
|
130
102
|
// src/config.ts
|
|
131
103
|
import { randomBytes } from "node:crypto";
|
|
132
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
104
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
133
105
|
import { homedir } from "node:os";
|
|
134
106
|
import { join } from "node:path";
|
|
135
107
|
function defaultConfigDir() {
|
|
@@ -141,8 +113,6 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
141
113
|
try {
|
|
142
114
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
143
115
|
const config = {};
|
|
144
|
-
if (typeof parsed.token === "string") config.token = parsed.token;
|
|
145
|
-
if (typeof parsed.handle === "string") config.handle = parsed.handle;
|
|
146
116
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
147
117
|
return Object.keys(config).length > 0 ? config : null;
|
|
148
118
|
} catch {
|
|
@@ -154,9 +124,6 @@ function saveConfig(dir = defaultConfigDir(), config = {}) {
|
|
|
154
124
|
const file = join(dir, "config.json");
|
|
155
125
|
writeFileSync(file, JSON.stringify(config, null, 2), { mode: 384 });
|
|
156
126
|
}
|
|
157
|
-
function clearConfig(dir = defaultConfigDir()) {
|
|
158
|
-
rmSync(join(dir, "config.json"), { force: true });
|
|
159
|
-
}
|
|
160
127
|
function ensureAnonKey(dir = defaultConfigDir()) {
|
|
161
128
|
const config = loadConfig(dir) ?? {};
|
|
162
129
|
if (config.anonKey) return config.anonKey;
|
|
@@ -166,7 +133,7 @@ function ensureAnonKey(dir = defaultConfigDir()) {
|
|
|
166
133
|
}
|
|
167
134
|
|
|
168
135
|
// src/autosync.ts
|
|
169
|
-
var SYNC_INTERVAL_HOURS =
|
|
136
|
+
var SYNC_INTERVAL_HOURS = 1;
|
|
170
137
|
var LABEL = "com.whoburnedmore.sync";
|
|
171
138
|
function syncLogPath() {
|
|
172
139
|
return join2(defaultConfigDir(), "sync.log");
|
|
@@ -186,8 +153,14 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
186
153
|
</array>
|
|
187
154
|
<key>StartInterval</key>
|
|
188
155
|
<integer>${SYNC_INTERVAL_HOURS * 3600}</integer>
|
|
156
|
+
<!-- Run once right after login/reboot so a machine that was off (or asleep)
|
|
157
|
+
through a scheduled tick catches up immediately, then keeps to the
|
|
158
|
+
interval. Submits are idempotent server-side, so an extra run is safe. -->
|
|
189
159
|
<key>RunAtLoad</key>
|
|
190
|
-
<
|
|
160
|
+
<true/>
|
|
161
|
+
<!-- Be a good citizen: macOS schedules this with background priority. -->
|
|
162
|
+
<key>ProcessType</key>
|
|
163
|
+
<string>Background</string>
|
|
191
164
|
<key>StandardOutPath</key>
|
|
192
165
|
<string>${logPath}</string>
|
|
193
166
|
<key>StandardErrorPath</key>
|
|
@@ -250,7 +223,7 @@ function uninstallAutoSync() {
|
|
|
250
223
|
const plistPath = launchAgentPath();
|
|
251
224
|
if (existsSync2(plistPath)) {
|
|
252
225
|
spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
253
|
-
|
|
226
|
+
rmSync(plistPath, { force: true });
|
|
254
227
|
}
|
|
255
228
|
return "launchd agent removed";
|
|
256
229
|
}
|
|
@@ -283,6 +256,27 @@ function autoSyncInstalled() {
|
|
|
283
256
|
return false;
|
|
284
257
|
}
|
|
285
258
|
|
|
259
|
+
// src/banner.ts
|
|
260
|
+
import pc from "picocolors";
|
|
261
|
+
var ART = [
|
|
262
|
+
"\u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588\u2588 \u2588 \u2588",
|
|
263
|
+
"\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2588 \u2588",
|
|
264
|
+
"\u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588",
|
|
265
|
+
"\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2588",
|
|
266
|
+
"\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588"
|
|
267
|
+
];
|
|
268
|
+
var SHADES = [226, 220, 214, 208, 202];
|
|
269
|
+
function printBanner() {
|
|
270
|
+
const color = pc.isColorSupported;
|
|
271
|
+
console.log();
|
|
272
|
+
ART.forEach((row, i) => {
|
|
273
|
+
console.log(" " + (color ? `\x1B[1;38;5;${SHADES[i]}m${row}\x1B[0m` : row));
|
|
274
|
+
});
|
|
275
|
+
const mark = color ? `\x1B[1;38;5;208m\u{1F525} whoburnedmore\x1B[0m` : "\u{1F525} whoburnedmore";
|
|
276
|
+
console.log(` ${mark} ${pc.dim("\xB7 who burned more?")}`);
|
|
277
|
+
console.log();
|
|
278
|
+
}
|
|
279
|
+
|
|
286
280
|
// src/collect.ts
|
|
287
281
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
288
282
|
import { createRequire as createRequire3 } from "node:module";
|
|
@@ -314,6 +308,7 @@ function estimateCostUSD(model, t) {
|
|
|
314
308
|
|
|
315
309
|
// src/attribution.ts
|
|
316
310
|
var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
311
|
+
var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
|
|
317
312
|
var MAX_FILES = 5e3;
|
|
318
313
|
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
319
314
|
var TIME_BUDGET_MS = 3e4;
|
|
@@ -349,9 +344,13 @@ function recordTokens(usage) {
|
|
|
349
344
|
function processRecord(rec, acc, ctx) {
|
|
350
345
|
if (!rec || typeof rec !== "object") return;
|
|
351
346
|
const r = rec;
|
|
347
|
+
const recTokens = recordTokens(r.message?.usage);
|
|
352
348
|
if (typeof r.attributionSkill === "string" && r.attributionSkill) {
|
|
353
349
|
const s = r.attributionSkill.slice(0, 128);
|
|
354
|
-
|
|
350
|
+
const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
|
|
351
|
+
sk.count += 1;
|
|
352
|
+
sk.tokens += recTokens;
|
|
353
|
+
acc.skills.set(s, sk);
|
|
355
354
|
}
|
|
356
355
|
if (r.type === "ai-title" && typeof r.aiTitle === "string" && r.aiTitle && typeof r.sessionId === "string" && r.sessionId) {
|
|
357
356
|
acc.titles.set(r.sessionId, r.aiTitle.slice(0, 200));
|
|
@@ -361,18 +360,24 @@ function processRecord(rec, acc, ctx) {
|
|
|
361
360
|
const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
|
|
362
361
|
const isUser = r.type === "user" || r.message?.role === "user";
|
|
363
362
|
if (isAssistant) {
|
|
363
|
+
const toolUses = [];
|
|
364
364
|
for (const block of content) {
|
|
365
365
|
if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
|
|
366
366
|
const name = block.name.slice(0, 128);
|
|
367
367
|
if (!name) continue;
|
|
368
|
-
const t = acc.tools.get(name) ?? { count: 0, errors: 0 };
|
|
369
|
-
t.count += 1;
|
|
370
|
-
acc.tools.set(name, t);
|
|
371
368
|
const id = block.id;
|
|
372
|
-
|
|
369
|
+
toolUses.push({ name, id: typeof id === "string" ? id : void 0 });
|
|
373
370
|
}
|
|
374
371
|
}
|
|
375
|
-
const
|
|
372
|
+
const perToolTokens = toolUses.length > 0 ? Math.floor(recTokens / toolUses.length) : 0;
|
|
373
|
+
for (const tu of toolUses) {
|
|
374
|
+
const t = acc.tools.get(tu.name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
375
|
+
t.count += 1;
|
|
376
|
+
t.tokens += perToolTokens;
|
|
377
|
+
acc.tools.set(tu.name, t);
|
|
378
|
+
if (tu.id) ctx.toolNames.set(tu.id, tu.name);
|
|
379
|
+
}
|
|
380
|
+
const tokens = recTokens;
|
|
376
381
|
acc.agent.messageCount += 1;
|
|
377
382
|
acc.agent.totalTokens += tokens;
|
|
378
383
|
const sidechain = r.isSidechain === true;
|
|
@@ -411,7 +416,7 @@ function processRecord(rec, acc, ctx) {
|
|
|
411
416
|
const id = block.tool_use_id;
|
|
412
417
|
const name = typeof id === "string" ? ctx.toolNames.get(id) : void 0;
|
|
413
418
|
if (name) {
|
|
414
|
-
const t = acc.tools.get(name) ?? { count: 0, errors: 0 };
|
|
419
|
+
const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
415
420
|
t.errors += 1;
|
|
416
421
|
acc.tools.set(name, t);
|
|
417
422
|
}
|
|
@@ -420,10 +425,15 @@ function processRecord(rec, acc, ctx) {
|
|
|
420
425
|
}
|
|
421
426
|
}
|
|
422
427
|
function toSkillStats(map) {
|
|
423
|
-
return [...map.entries()].map(([name,
|
|
428
|
+
return [...map.entries()].map(([name, v]) => ({ name, count: v.count, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => s.tokens > 0 ? s : { name: s.name, count: s.count });
|
|
424
429
|
}
|
|
425
430
|
function toToolStats(map) {
|
|
426
|
-
return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors })).filter((s) => s.count > 0).sort((a, b) => b.count - a.count).slice(0, MAX_STATS).map((s) =>
|
|
431
|
+
return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => {
|
|
432
|
+
const base = { name: s.name, count: s.count };
|
|
433
|
+
if (s.errors > 0) base.errors = s.errors;
|
|
434
|
+
if (s.tokens > 0) base.tokens = s.tokens;
|
|
435
|
+
return base;
|
|
436
|
+
});
|
|
427
437
|
}
|
|
428
438
|
function toProjectStats(map) {
|
|
429
439
|
return [...map.entries()].map(([name, v]) => ({
|
|
@@ -439,9 +449,95 @@ function accumulatorToResult(acc) {
|
|
|
439
449
|
projects: toProjectStats(acc.projects),
|
|
440
450
|
agent: { ...acc.agent },
|
|
441
451
|
titles: acc.titles,
|
|
442
|
-
sessionMessages: acc.sessionMessages
|
|
452
|
+
sessionMessages: acc.sessionMessages,
|
|
453
|
+
complete: true
|
|
443
454
|
};
|
|
444
455
|
}
|
|
456
|
+
function numTok(v) {
|
|
457
|
+
const x = Math.round(Number(v));
|
|
458
|
+
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
459
|
+
}
|
|
460
|
+
function createCodexContext() {
|
|
461
|
+
return { cwd: "", model: "unknown", pending: [] };
|
|
462
|
+
}
|
|
463
|
+
function processCodexRecord(rec, acc, ctx) {
|
|
464
|
+
if (!rec || typeof rec !== "object") return;
|
|
465
|
+
const r = rec;
|
|
466
|
+
if (!r.payload || typeof r.payload !== "object") return;
|
|
467
|
+
const pl = r.payload;
|
|
468
|
+
const ptype = pl.type;
|
|
469
|
+
if (r.type === "session_meta" || r.type === "turn_context") {
|
|
470
|
+
if (typeof pl.cwd === "string" && pl.cwd) ctx.cwd = pl.cwd;
|
|
471
|
+
if (typeof pl.model === "string" && pl.model) ctx.model = pl.model;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
|
|
475
|
+
const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
|
|
476
|
+
const name = raw.slice(0, 128);
|
|
477
|
+
if (!name) return;
|
|
478
|
+
const id = typeof pl.call_id === "string" ? pl.call_id : void 0;
|
|
479
|
+
ctx.pending.push({ name, id });
|
|
480
|
+
const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
481
|
+
t.count += 1;
|
|
482
|
+
acc.tools.set(name, t);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (ptype === "token_count") {
|
|
486
|
+
const info = pl.info;
|
|
487
|
+
const last = info?.last_token_usage;
|
|
488
|
+
if (!last) return;
|
|
489
|
+
const inputTokens = numTok(last.input_tokens);
|
|
490
|
+
const cacheReadTokens = numTok(last.cached_input_tokens);
|
|
491
|
+
const outputTokens = numTok(last.output_tokens) + numTok(last.reasoning_output_tokens);
|
|
492
|
+
const tokens = numTok(last.total_tokens) || inputTokens + cacheReadTokens + outputTokens;
|
|
493
|
+
if (tokens <= 0) {
|
|
494
|
+
ctx.pending = [];
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
acc.agent.messageCount += 1;
|
|
498
|
+
acc.agent.totalTokens += tokens;
|
|
499
|
+
if (ctx.cwd) {
|
|
500
|
+
const name = basename(ctx.cwd).slice(0, 128) || "unknown";
|
|
501
|
+
const cost = estimateCostUSD(ctx.model, {
|
|
502
|
+
inputTokens,
|
|
503
|
+
outputTokens,
|
|
504
|
+
cacheCreationTokens: 0,
|
|
505
|
+
cacheReadTokens
|
|
506
|
+
});
|
|
507
|
+
const proj = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
|
|
508
|
+
proj.tokens += tokens;
|
|
509
|
+
proj.costUSD += cost;
|
|
510
|
+
acc.projects.set(name, proj);
|
|
511
|
+
}
|
|
512
|
+
const per = ctx.pending.length > 0 ? Math.floor(tokens / ctx.pending.length) : 0;
|
|
513
|
+
for (const tu of ctx.pending) {
|
|
514
|
+
const t = acc.tools.get(tu.name);
|
|
515
|
+
if (t) t.tokens += per;
|
|
516
|
+
}
|
|
517
|
+
ctx.pending = [];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function readLines(file) {
|
|
521
|
+
let size = 0;
|
|
522
|
+
try {
|
|
523
|
+
size = statSync(file).size;
|
|
524
|
+
} catch {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
if (size > MAX_FILE_BYTES) return [];
|
|
528
|
+
try {
|
|
529
|
+
return readFileSync2(file, "utf8").split("\n");
|
|
530
|
+
} catch {
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function claudeProjectDirs() {
|
|
535
|
+
const dirs = [CLAUDE_PROJECTS];
|
|
536
|
+
const cfg = process.env.CLAUDE_CONFIG_DIR;
|
|
537
|
+
if (cfg) dirs.push(join3(cfg, "projects"));
|
|
538
|
+
dirs.push(join3(homedir3(), ".config", "claude", "projects"));
|
|
539
|
+
return [...new Set(dirs)];
|
|
540
|
+
}
|
|
445
541
|
function listTranscripts(dir) {
|
|
446
542
|
const out = [];
|
|
447
543
|
const walk = (d) => {
|
|
@@ -470,34 +566,42 @@ function listTranscripts(dir) {
|
|
|
470
566
|
function collectAttribution() {
|
|
471
567
|
const acc = createAccumulator();
|
|
472
568
|
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
569
|
+
let complete = true;
|
|
473
570
|
try {
|
|
474
|
-
for (const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
571
|
+
for (const dir of claudeProjectDirs()) {
|
|
572
|
+
for (const file of listTranscripts(dir)) {
|
|
573
|
+
if (Date.now() > deadline) {
|
|
574
|
+
complete = false;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
const ctx = createFileContext();
|
|
578
|
+
for (const line of readLines(file)) {
|
|
579
|
+
if (!line) continue;
|
|
580
|
+
try {
|
|
581
|
+
processRecord(JSON.parse(line), acc, ctx);
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
481
585
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
continue;
|
|
586
|
+
}
|
|
587
|
+
for (const file of listTranscripts(CODEX_SESSIONS)) {
|
|
588
|
+
if (Date.now() > deadline) {
|
|
589
|
+
complete = false;
|
|
590
|
+
break;
|
|
488
591
|
}
|
|
489
|
-
const ctx =
|
|
490
|
-
for (const line of
|
|
592
|
+
const ctx = createCodexContext();
|
|
593
|
+
for (const line of readLines(file)) {
|
|
491
594
|
if (!line) continue;
|
|
492
595
|
try {
|
|
493
|
-
|
|
596
|
+
processCodexRecord(JSON.parse(line), acc, ctx);
|
|
494
597
|
} catch {
|
|
495
598
|
}
|
|
496
599
|
}
|
|
497
600
|
}
|
|
498
601
|
} catch {
|
|
602
|
+
complete = false;
|
|
499
603
|
}
|
|
500
|
-
return accumulatorToResult(acc);
|
|
604
|
+
return { ...accumulatorToResult(acc), complete };
|
|
501
605
|
}
|
|
502
606
|
|
|
503
607
|
// src/cursor.ts
|
|
@@ -720,7 +824,9 @@ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
|
|
|
720
824
|
body: JSON.stringify({ page, pageSize }),
|
|
721
825
|
signal: AbortSignal.timeout(2e4)
|
|
722
826
|
});
|
|
723
|
-
if (!res.ok)
|
|
827
|
+
if (!res.ok) {
|
|
828
|
+
throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
|
|
829
|
+
}
|
|
724
830
|
const body = await res.json();
|
|
725
831
|
const batch = body.usageEventsDisplay ?? [];
|
|
726
832
|
all.push(...batch);
|
|
@@ -930,24 +1036,34 @@ function dedupeBlocks(blocks) {
|
|
|
930
1036
|
}
|
|
931
1037
|
return [...byStart.values()];
|
|
932
1038
|
}
|
|
933
|
-
function
|
|
1039
|
+
function runCcusageOnce(cmd, args) {
|
|
934
1040
|
const res = spawnSync4(cmd, args, {
|
|
935
1041
|
encoding: "utf8",
|
|
936
1042
|
maxBuffer: 64 * 1024 * 1024,
|
|
937
1043
|
timeout: 12e4
|
|
938
1044
|
});
|
|
939
|
-
|
|
1045
|
+
const transient = res.signal != null || res.error != null;
|
|
1046
|
+
if (res.status !== 0 || !res.stdout) return { json: null, transient };
|
|
940
1047
|
try {
|
|
941
|
-
return JSON.parse(res.stdout);
|
|
1048
|
+
return { json: JSON.parse(res.stdout), transient: false };
|
|
942
1049
|
} catch {
|
|
943
|
-
return null;
|
|
1050
|
+
return { json: null, transient: false };
|
|
944
1051
|
}
|
|
945
1052
|
}
|
|
946
|
-
|
|
1053
|
+
function runCcusage(cmd, args) {
|
|
1054
|
+
const first = runCcusageOnce(cmd, args);
|
|
1055
|
+
if (first.json !== null || !first.transient) return first.json;
|
|
1056
|
+
return runCcusageOnce(cmd, args).json;
|
|
1057
|
+
}
|
|
1058
|
+
var COLLECT_STAGES = SOURCES.length + 4;
|
|
1059
|
+
async function collectAll(onProgress) {
|
|
947
1060
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
948
1061
|
const entries = [];
|
|
949
1062
|
const toolsFound = [];
|
|
1063
|
+
let done = 0;
|
|
1064
|
+
const tick = (label) => onProgress?.(done++, COLLECT_STAGES, label);
|
|
950
1065
|
for (const source of SOURCES) {
|
|
1066
|
+
tick(`reading ${source}`);
|
|
951
1067
|
const json = runCcusage(cmd, [
|
|
952
1068
|
...prefixArgs,
|
|
953
1069
|
source,
|
|
@@ -962,17 +1078,22 @@ async function collectAll() {
|
|
|
962
1078
|
toolsFound.push(source);
|
|
963
1079
|
}
|
|
964
1080
|
}
|
|
1081
|
+
tick("reading conversations");
|
|
965
1082
|
const sessionJson = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]);
|
|
1083
|
+
tick("reading active blocks");
|
|
966
1084
|
const blockJson = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]);
|
|
967
1085
|
const sessions = sessionJson ? mapCcusageSessions(sessionJson) : [];
|
|
968
1086
|
const blocks = blockJson ? mapCcusageBlocks(blockJson) : [];
|
|
1087
|
+
tick("reading cursor");
|
|
969
1088
|
const cursor = await collectCursor();
|
|
970
1089
|
if (cursor.found) {
|
|
971
1090
|
entries.push(...cursor.entries);
|
|
972
1091
|
blocks.push(...cursor.blocks);
|
|
973
1092
|
toolsFound.push("cursor");
|
|
974
1093
|
}
|
|
975
|
-
|
|
1094
|
+
tick("reading agent transcripts");
|
|
1095
|
+
const { tools, skills, projects, agent, titles, sessionMessages, complete } = collectAttribution();
|
|
1096
|
+
onProgress?.(done, COLLECT_STAGES, "done");
|
|
976
1097
|
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
977
1098
|
const title = titles.get(s.sessionId);
|
|
978
1099
|
const messageCount = sessionMessages.get(s.sessionId);
|
|
@@ -990,7 +1111,8 @@ async function collectAll() {
|
|
|
990
1111
|
tools,
|
|
991
1112
|
skills,
|
|
992
1113
|
projects,
|
|
993
|
-
agent
|
|
1114
|
+
agent,
|
|
1115
|
+
attributionComplete: complete
|
|
994
1116
|
};
|
|
995
1117
|
}
|
|
996
1118
|
|
|
@@ -5096,7 +5218,9 @@ var ToolStat = external_exports.object({
|
|
|
5096
5218
|
name: external_exports.string().min(1).max(128),
|
|
5097
5219
|
count: external_exports.number().int().nonnegative(),
|
|
5098
5220
|
/** How many of those calls returned an error/interrupt (tool reliability). Optional. */
|
|
5099
|
-
errors: external_exports.number().int().nonnegative().optional()
|
|
5221
|
+
errors: external_exports.number().int().nonnegative().optional(),
|
|
5222
|
+
/** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
|
|
5223
|
+
tokens: external_exports.number().int().nonnegative().optional()
|
|
5100
5224
|
});
|
|
5101
5225
|
var ProjectStat = external_exports.object({
|
|
5102
5226
|
name: external_exports.string().min(1).max(128),
|
|
@@ -5115,7 +5239,9 @@ var AgentStat = external_exports.object({
|
|
|
5115
5239
|
});
|
|
5116
5240
|
var SkillStat = external_exports.object({
|
|
5117
5241
|
name: external_exports.string().min(1).max(128),
|
|
5118
|
-
count: external_exports.number().int().nonnegative()
|
|
5242
|
+
count: external_exports.number().int().nonnegative(),
|
|
5243
|
+
/** Tokens burned in records produced while this skill was active. Optional. */
|
|
5244
|
+
tokens: external_exports.number().int().nonnegative().optional()
|
|
5119
5245
|
});
|
|
5120
5246
|
var SubmitPayload = external_exports.object({
|
|
5121
5247
|
cliVersion: external_exports.string().min(1).max(32),
|
|
@@ -5132,6 +5258,13 @@ var SubmitPayload = external_exports.object({
|
|
|
5132
5258
|
projects: external_exports.array(ProjectStat).max(500).optional(),
|
|
5133
5259
|
/** Optional subagent-vs-main rollup parsed from local transcripts. */
|
|
5134
5260
|
agent: AgentStat.optional(),
|
|
5261
|
+
/**
|
|
5262
|
+
* Set when the transcript scan completed within its time budget, i.e. the
|
|
5263
|
+
* tool/skill/project/agent rollups are a FULL snapshot. The server refreshes
|
|
5264
|
+
* the dashboard breakdowns unconditionally for a full snapshot; for a partial
|
|
5265
|
+
* one (flag absent/false) it keeps its no-shrink guard. Back-compat: omittable.
|
|
5266
|
+
*/
|
|
5267
|
+
attributionComplete: external_exports.boolean().optional(),
|
|
5135
5268
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
5136
5269
|
board: external_exports.string().min(1).max(32).optional()
|
|
5137
5270
|
});
|
|
@@ -5146,7 +5279,7 @@ var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
|
|
|
5146
5279
|
var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
|
|
5147
5280
|
|
|
5148
5281
|
// src/output.ts
|
|
5149
|
-
import
|
|
5282
|
+
import pc2 from "picocolors";
|
|
5150
5283
|
function formatTokens(n) {
|
|
5151
5284
|
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
5152
5285
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
@@ -5174,20 +5307,20 @@ function printSummary(entries) {
|
|
|
5174
5307
|
byTool.set(e.tool, agg);
|
|
5175
5308
|
}
|
|
5176
5309
|
console.log();
|
|
5177
|
-
console.log(
|
|
5310
|
+
console.log(pc2.bold(pc2.yellow(" \u{1F525} your burn report")));
|
|
5178
5311
|
console.log();
|
|
5179
5312
|
const rows = [...byTool.entries()].sort((a, b) => b[1].tokens - a[1].tokens);
|
|
5180
5313
|
for (const [tool, agg] of rows) {
|
|
5181
5314
|
console.log(
|
|
5182
|
-
` ${
|
|
5315
|
+
` ${pc2.cyan(tool.padEnd(10))} ${formatTokens(agg.tokens).padStart(9)} tokens ${formatUSD(agg.cost).padStart(10)} ${String(agg.days.size).padStart(4)} days`
|
|
5183
5316
|
);
|
|
5184
5317
|
}
|
|
5185
|
-
console.log(
|
|
5318
|
+
console.log(pc2.dim(" " + "\u2500".repeat(46)));
|
|
5186
5319
|
console.log(
|
|
5187
|
-
` ${
|
|
5320
|
+
` ${pc2.bold("total".padEnd(10))} ${pc2.bold(formatTokens(totalTokens).padStart(9))} tokens ${pc2.bold(formatUSD(totalCost).padStart(10))}`
|
|
5188
5321
|
);
|
|
5189
5322
|
if (todayTokens > 0) {
|
|
5190
|
-
console.log(` ${
|
|
5323
|
+
console.log(` ${pc2.dim("today".padEnd(10))} ${formatTokens(todayTokens).padStart(9)} tokens`);
|
|
5191
5324
|
}
|
|
5192
5325
|
console.log();
|
|
5193
5326
|
}
|
|
@@ -5196,7 +5329,7 @@ function printSummary(entries) {
|
|
|
5196
5329
|
function esc(s) {
|
|
5197
5330
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5198
5331
|
}
|
|
5199
|
-
function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date()) {
|
|
5332
|
+
function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
|
|
5200
5333
|
const today = generatedAt.toISOString().slice(0, 10);
|
|
5201
5334
|
const totals = {
|
|
5202
5335
|
tokens: 0,
|
|
@@ -5247,7 +5380,30 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
|
|
|
5247
5380
|
const modelRows = [...byModel.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, 12).map(
|
|
5248
5381
|
([model, a]) => `<tr><td class="mono">${esc(model)}</td><td class="num">${esc(formatTokens(a.tokens))}</td><td class="num">${esc(formatUSD(a.cost))}</td></tr>`
|
|
5249
5382
|
).join("");
|
|
5250
|
-
const
|
|
5383
|
+
const railRow = (label, value, accent = false) => `<div class="rrow"><span class="rlabel">${esc(label)}</span><span class="rval${accent ? " accent" : ""}">${esc(value)}</span></div>`;
|
|
5384
|
+
const tb = [
|
|
5385
|
+
{ label: "input", value: totals.input, color: "#3b82f6" },
|
|
5386
|
+
{ label: "output", value: totals.output, color: "#22c55e" },
|
|
5387
|
+
{ label: "cache write", value: totals.cacheWrite, color: "#a855f7" },
|
|
5388
|
+
{ label: "cache read", value: totals.cacheRead, color: "#ea580c" }
|
|
5389
|
+
];
|
|
5390
|
+
const tbSum = tb.reduce((a, p) => a + p.value, 0) || 1;
|
|
5391
|
+
const tbBar = tb.map((p) => `<div style="width:${p.value / tbSum * 100}%;background:${p.color}"></div>`).join("");
|
|
5392
|
+
const tbCells = tb.map(
|
|
5393
|
+
(p) => `<div class="tbcell"><div class="tblabel"><span class="dot" style="background:${p.color}"></span>${p.label}</div><div class="tbval">${esc(formatTokens(p.value))}</div><div class="tbpct">${Math.round(p.value / tbSum * 100)}%</div></div>`
|
|
5394
|
+
).join("");
|
|
5395
|
+
const connectCta = connect ? `
|
|
5396
|
+
<form class="connect" method="POST" action="${esc(connect.webBaseUrl)}/connect">
|
|
5397
|
+
<input type="hidden" name="payload" value="${Buffer.from(JSON.stringify(connect.payload)).toString("base64")}">
|
|
5398
|
+
<div class="connect-row">
|
|
5399
|
+
<div>
|
|
5400
|
+
<div class="connect-title">Connect your account</div>
|
|
5401
|
+
<div class="connect-sub">Save this dashboard to your account and claim your spot on the public leaderboard. The local numbers above become your starting point \u2014 nothing has left your machine yet.</div>
|
|
5402
|
+
</div>
|
|
5403
|
+
<button type="submit">Connect your account \u2192</button>
|
|
5404
|
+
</div>
|
|
5405
|
+
<div class="connect-note">After connecting, run <code>npx whoburnedmore</code> (no flag) once so it keeps syncing automatically in the background.</div>
|
|
5406
|
+
</form>` : "";
|
|
5251
5407
|
return `<!doctype html>
|
|
5252
5408
|
<html lang="en">
|
|
5253
5409
|
<head>
|
|
@@ -5262,19 +5418,36 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
|
|
|
5262
5418
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
5263
5419
|
line-height: 1.5;
|
|
5264
5420
|
}
|
|
5265
|
-
.wrap { max-width:
|
|
5266
|
-
h1 { font-size:
|
|
5421
|
+
.wrap { max-width: 1100px; margin: 0 auto; padding: 40px 20px 80px; }
|
|
5422
|
+
h1 { font-size: 22px; margin: 0; line-height: 1.2; }
|
|
5267
5423
|
h1 .q { color: #ea580c; }
|
|
5268
|
-
.sub { color: #a8a29e; font-size:
|
|
5424
|
+
.sub { color: #a8a29e; font-size: 13px; margin-top: 4px; }
|
|
5269
5425
|
.mono, code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
5270
|
-
.
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
.
|
|
5275
|
-
.
|
|
5276
|
-
.
|
|
5426
|
+
.head { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; }
|
|
5427
|
+
.flame { width: 46px; height: 46px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: 12px; border: 1px solid #292524; background: #1c1917; font-size: 22px; }
|
|
5428
|
+
|
|
5429
|
+
/* Split layout: narrow sticky rail + wide scrolling column (mirrors the web). */
|
|
5430
|
+
.layout { display: grid; gap: 24px; align-items: start; }
|
|
5431
|
+
@media (min-width: 900px) { .layout { grid-template-columns: 280px minmax(0, 1fr); } .rail { position: sticky; top: 20px; } }
|
|
5432
|
+
.rail { display: flex; flex-direction: column; gap: 16px; }
|
|
5433
|
+
.rail-card { background: #1c1917; border: 1px solid #292524; border-radius: 14px; padding: 16px; }
|
|
5434
|
+
.hero-val { font-size: 30px; font-weight: 800; font-family: ui-monospace, monospace; color: #4ade80; font-variant-numeric: tabular-nums; line-height: 1.1; margin-top: 2px; }
|
|
5435
|
+
.rdiv { height: 1px; background: #292524; margin: 12px 0; }
|
|
5436
|
+
.rrow { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; padding: 5px 0; }
|
|
5437
|
+
.rlabel { font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: #a8a29e; }
|
|
5438
|
+
.rval { font-family: ui-monospace, monospace; font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
5439
|
+
.rval.accent { color: #4ade80; }
|
|
5440
|
+
.col { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
|
|
5441
|
+
.panel { background: #1c1917; border: 1px solid #292524; border-radius: 14px; padding: 18px 20px; }
|
|
5277
5442
|
.panel h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: #d6d3d1; margin: 0 0 14px; }
|
|
5443
|
+
.tbbar { display: flex; height: 12px; width: 100%; border-radius: 999px; overflow: hidden; background: #292524; }
|
|
5444
|
+
.tbgrid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 14px; }
|
|
5445
|
+
@media (min-width: 520px) { .tbgrid { grid-template-columns: repeat(4, 1fr); } }
|
|
5446
|
+
.tbcell { border: 1px solid #292524; background: #0c0a09; border-radius: 10px; padding: 8px 10px; }
|
|
5447
|
+
.tblabel { display: flex; align-items: center; gap: 6px; font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: #a8a29e; }
|
|
5448
|
+
.dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; flex-shrink: 0; }
|
|
5449
|
+
.tbval { font-family: ui-monospace, monospace; font-weight: 700; font-size: 16px; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
|
5450
|
+
.tbpct { font-size: 11px; color: #a8a29e; }
|
|
5278
5451
|
.chart { display: flex; align-items: flex-end; gap: 2px; height: 140px; }
|
|
5279
5452
|
.bar { flex: 1; background: linear-gradient(to top, #ea580c, #f97316); border-radius: 2px 2px 0 0; min-height: 0; transition: opacity .15s; }
|
|
5280
5453
|
.bar:hover { opacity: .7; }
|
|
@@ -5285,57 +5458,73 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
|
|
|
5285
5458
|
td.mono { font-family: ui-monospace, monospace; font-size: 12px; }
|
|
5286
5459
|
.foot { color: #78716c; font-size: 12px; margin-top: 32px; }
|
|
5287
5460
|
.foot code { color: #d6d3d1; }
|
|
5461
|
+
.connect { display: block; margin: 24px 0 4px; background: linear-gradient(135deg, rgba(234,88,12,.16), rgba(249,115,22,.06)); border: 1px solid rgba(234,88,12,.5); border-radius: 14px; padding: 18px 20px; }
|
|
5462
|
+
.connect-row { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
|
|
5463
|
+
@media (min-width: 640px) { .connect-row { flex-direction: row; align-items: center; justify-content: space-between; } }
|
|
5464
|
+
.connect-title { font-size: 16px; font-weight: 700; }
|
|
5465
|
+
.connect-sub { color: #d6d3d1; font-size: 13px; margin-top: 4px; max-width: 60ch; }
|
|
5466
|
+
.connect button { flex-shrink: 0; cursor: pointer; border: 0; border-radius: 10px; background: #ea580c; color: #fff; font-size: 14px; font-weight: 600; padding: 11px 18px; font-family: inherit; transition: background .15s; }
|
|
5467
|
+
.connect button:hover { background: #f97316; }
|
|
5468
|
+
.connect-note { color: #a8a29e; font-size: 12px; margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(234,88,12,.25); }
|
|
5469
|
+
.connect-note code { color: #fed7aa; background: rgba(0,0,0,.25); padding: 1px 6px; border-radius: 5px; }
|
|
5288
5470
|
</style>
|
|
5289
5471
|
</head>
|
|
5290
5472
|
<body>
|
|
5291
5473
|
<div class="wrap">
|
|
5292
|
-
<
|
|
5293
|
-
|
|
5474
|
+
<header class="head">
|
|
5475
|
+
<div class="flame">\u{1F525}</div>
|
|
5476
|
+
<div>
|
|
5477
|
+
<h1>your local burn</h1>
|
|
5478
|
+
<div class="sub">generated ${esc(generatedAt.toISOString().slice(0, 16).replace("T", " "))} \xB7 nothing left your machine</div>
|
|
5479
|
+
</div>
|
|
5480
|
+
</header>
|
|
5481
|
+
${connectCta}
|
|
5482
|
+
<div class="layout">
|
|
5483
|
+
<aside class="rail">
|
|
5484
|
+
<div class="rail-card">
|
|
5485
|
+
<div class="rlabel">total tokens</div>
|
|
5486
|
+
<div class="hero-val">${esc(formatTokens(totals.tokens))}</div>
|
|
5487
|
+
<div class="rdiv"></div>
|
|
5488
|
+
${railRow("avg / day", formatTokens(avgPerDay), true)}
|
|
5489
|
+
${railRow("est. cost", formatUSD(totals.cost))}
|
|
5490
|
+
${railRow("active days", String(activeDays))}
|
|
5491
|
+
${railRow("today", formatTokens(todayTokens))}
|
|
5492
|
+
</div>
|
|
5493
|
+
</aside>
|
|
5294
5494
|
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
${stat("today", formatTokens(todayTokens))}
|
|
5301
|
-
</div>
|
|
5495
|
+
<div class="col">
|
|
5496
|
+
<div class="panel">
|
|
5497
|
+
<h2>daily burn (last 60 days)</h2>
|
|
5498
|
+
<div class="chart">${bars}</div>
|
|
5499
|
+
</div>
|
|
5302
5500
|
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5501
|
+
<div class="panel">
|
|
5502
|
+
<h2>by tool</h2>
|
|
5503
|
+
<table>
|
|
5504
|
+
<thead><tr><th>tool</th><th class="num">tokens</th><th class="num">est. cost</th></tr></thead>
|
|
5505
|
+
<tbody>${toolRows || '<tr><td colspan="3">no usage found</td></tr>'}</tbody>
|
|
5506
|
+
</table>
|
|
5507
|
+
</div>
|
|
5307
5508
|
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5509
|
+
<div class="panel">
|
|
5510
|
+
<h2>by model</h2>
|
|
5511
|
+
<table>
|
|
5512
|
+
<thead><tr><th>model</th><th class="num">tokens</th><th class="num">est. cost</th></tr></thead>
|
|
5513
|
+
<tbody>${modelRows || '<tr><td colspan="3">no usage found</td></tr>'}</tbody>
|
|
5514
|
+
</table>
|
|
5515
|
+
</div>
|
|
5315
5516
|
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
</
|
|
5322
|
-
</div>
|
|
5323
|
-
|
|
5324
|
-
<div class="panel">
|
|
5325
|
-
<h2>token breakdown</h2>
|
|
5326
|
-
<table>
|
|
5327
|
-
<tbody>
|
|
5328
|
-
<tr><td>input</td><td class="num">${esc(formatTokens(totals.input))}</td></tr>
|
|
5329
|
-
<tr><td>output</td><td class="num">${esc(formatTokens(totals.output))}</td></tr>
|
|
5330
|
-
<tr><td>cache write</td><td class="num">${esc(formatTokens(totals.cacheWrite))}</td></tr>
|
|
5331
|
-
<tr><td>cache read</td><td class="num">${esc(formatTokens(totals.cacheRead))}</td></tr>
|
|
5332
|
-
</tbody>
|
|
5333
|
-
</table>
|
|
5517
|
+
<div class="panel">
|
|
5518
|
+
<h2>token breakdown</h2>
|
|
5519
|
+
<div class="tbbar">${tbBar}</div>
|
|
5520
|
+
<div class="tbgrid">${tbCells}</div>
|
|
5521
|
+
</div>
|
|
5522
|
+
</div>
|
|
5334
5523
|
</div>
|
|
5335
5524
|
|
|
5336
5525
|
<div class="foot">
|
|
5337
5526
|
Re-run <code>npx whoburnedmore --local</code> to refresh this page.<br>
|
|
5338
|
-
Run <code>npx whoburnedmore</code> (no flag) to get a shareable dashboard at whoburnedmore.com \u2014 no sign-in.
|
|
5527
|
+
${connect ? "Use \u201CConnect your account\u201D above to save it to whoburnedmore.com and join the leaderboard." : "Run <code>npx whoburnedmore</code> (no flag) to get a shareable dashboard at whoburnedmore.com \u2014 no sign-in."}
|
|
5339
5528
|
</div>
|
|
5340
5529
|
</div>
|
|
5341
5530
|
</body>
|
|
@@ -5362,23 +5551,45 @@ async function publishLocal(payload, deps) {
|
|
|
5362
5551
|
// src/index.ts
|
|
5363
5552
|
var require2 = createRequire4(import.meta.url);
|
|
5364
5553
|
var VERSION = require2("../package.json").version;
|
|
5365
|
-
function
|
|
5554
|
+
function startProgress() {
|
|
5366
5555
|
if (!process.stdout.isTTY) {
|
|
5367
|
-
|
|
5368
|
-
return
|
|
5556
|
+
let lastLogged = -1;
|
|
5557
|
+
return {
|
|
5558
|
+
onProgress: (done, total) => {
|
|
5559
|
+
const pct = Math.round(done / total * 100);
|
|
5560
|
+
if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
|
|
5561
|
+
lastLogged = pct;
|
|
5562
|
+
console.log(pc3.dim(` reading local usage\u2026 ${pct}%`));
|
|
5563
|
+
}
|
|
5564
|
+
},
|
|
5565
|
+
stop: () => {
|
|
5566
|
+
}
|
|
5369
5567
|
};
|
|
5370
5568
|
}
|
|
5371
|
-
const
|
|
5372
|
-
let
|
|
5569
|
+
const width = 24;
|
|
5570
|
+
let target = 0;
|
|
5571
|
+
let shown = 0;
|
|
5572
|
+
let label = "starting\u2026";
|
|
5373
5573
|
process.stdout.write("\x1B[?25l");
|
|
5374
|
-
const
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
process.stdout.write(
|
|
5381
|
-
|
|
5574
|
+
const render = () => {
|
|
5575
|
+
shown += (target - shown) * 0.3;
|
|
5576
|
+
if (target - shown < 4e-3) shown = target;
|
|
5577
|
+
const filled = Math.round(shown * width);
|
|
5578
|
+
const bar = pc3.yellow("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
|
|
5579
|
+
const pct = String(Math.round(shown * 100)).padStart(3);
|
|
5580
|
+
process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(label)}\x1B[K`);
|
|
5581
|
+
};
|
|
5582
|
+
render();
|
|
5583
|
+
const timer = setInterval(render, 60);
|
|
5584
|
+
return {
|
|
5585
|
+
onProgress: (done, total, l) => {
|
|
5586
|
+
target = total > 0 ? done / total : 0;
|
|
5587
|
+
label = l;
|
|
5588
|
+
},
|
|
5589
|
+
stop: () => {
|
|
5590
|
+
clearInterval(timer);
|
|
5591
|
+
process.stdout.write("\r\x1B[2K\x1B[?25h");
|
|
5592
|
+
}
|
|
5382
5593
|
};
|
|
5383
5594
|
}
|
|
5384
5595
|
function openBrowser(url) {
|
|
@@ -5386,61 +5597,55 @@ function openBrowser(url) {
|
|
|
5386
5597
|
const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5387
5598
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
5388
5599
|
}
|
|
5389
|
-
async function login() {
|
|
5390
|
-
const device = await deviceStart();
|
|
5391
|
-
console.log();
|
|
5392
|
-
console.log(` Opening ${pc2.cyan(device.verifyUrl)}`);
|
|
5393
|
-
console.log(` Your code: ${pc2.bold(pc2.yellow(device.userCode))}`);
|
|
5394
|
-
console.log(pc2.dim(" Sign in with Google or GitHub and approve this device."));
|
|
5395
|
-
openBrowser(device.verifyUrl);
|
|
5396
|
-
const deadline = Date.now() + device.expiresInSeconds * 1e3;
|
|
5397
|
-
while (Date.now() < deadline) {
|
|
5398
|
-
await new Promise((r) => setTimeout(r, device.pollIntervalSeconds * 1e3));
|
|
5399
|
-
const poll = await devicePoll(device.deviceCode);
|
|
5400
|
-
if (poll.status === "ok") {
|
|
5401
|
-
const config = { token: poll.token, handle: poll.handle };
|
|
5402
|
-
saveConfig(void 0, config);
|
|
5403
|
-
console.log(` Signed in as ${pc2.bold(poll.handle)} \u2713`);
|
|
5404
|
-
return config;
|
|
5405
|
-
}
|
|
5406
|
-
if (poll.status === "expired") break;
|
|
5407
|
-
}
|
|
5408
|
-
throw new Error("login timed out \u2014 run `npx whoburnedmore` to try again");
|
|
5409
|
-
}
|
|
5410
5600
|
async function confirm(question) {
|
|
5411
5601
|
if (!process.stdin.isTTY) return false;
|
|
5412
5602
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5413
|
-
const answer = (await rl.question(`${question} ${
|
|
5603
|
+
const answer = (await rl.question(`${question} ${pc3.dim("[Y/n]")} `)).trim();
|
|
5414
5604
|
rl.close();
|
|
5415
5605
|
return answer === "" || /^y(es)?$/i.test(answer);
|
|
5416
5606
|
}
|
|
5417
|
-
function showLocalDashboard(
|
|
5607
|
+
function showLocalDashboard(payload) {
|
|
5418
5608
|
const dir = defaultConfigDir();
|
|
5419
5609
|
mkdirSync3(dir, { recursive: true });
|
|
5420
5610
|
const file = join7(dir, "dashboard.html");
|
|
5421
|
-
writeFileSync3(
|
|
5611
|
+
writeFileSync3(
|
|
5612
|
+
file,
|
|
5613
|
+
renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
|
|
5614
|
+
payload,
|
|
5615
|
+
webBaseUrl: webBase()
|
|
5616
|
+
})
|
|
5617
|
+
);
|
|
5422
5618
|
console.log();
|
|
5423
|
-
console.log(` Local dashboard: ${
|
|
5424
|
-
console.log(
|
|
5619
|
+
console.log(` Local dashboard: ${pc3.cyan(`file://${file}`)}`);
|
|
5620
|
+
console.log(pc3.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
|
|
5425
5621
|
openBrowser(`file://${file}`);
|
|
5426
5622
|
}
|
|
5427
5623
|
async function run(flags) {
|
|
5428
5624
|
if (!flags.quiet) {
|
|
5429
|
-
|
|
5625
|
+
printBanner();
|
|
5626
|
+
console.log(pc3.dim(`whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
|
|
5627
|
+
}
|
|
5628
|
+
if (!flags.quiet && !flags.dryRun && !flags.noSubmit && !flags.local && process.stdin.isTTY) {
|
|
5629
|
+
const ok = await confirm(" Read your local usage and post your rank to the leaderboard?");
|
|
5630
|
+
if (!ok) {
|
|
5631
|
+
console.log(pc3.dim(" No worries \u2014 nothing read, nothing sent."));
|
|
5632
|
+
console.log(pc3.dim(" Tip: `--local` builds a private dashboard, `--dry-run` previews the payload."));
|
|
5633
|
+
return;
|
|
5634
|
+
}
|
|
5430
5635
|
}
|
|
5431
|
-
const
|
|
5432
|
-
} :
|
|
5636
|
+
const progress = flags.quiet ? { onProgress: void 0, stop: () => {
|
|
5637
|
+
} } : startProgress();
|
|
5433
5638
|
let collected;
|
|
5434
5639
|
try {
|
|
5435
|
-
collected = await collectAll();
|
|
5640
|
+
collected = await collectAll(progress.onProgress);
|
|
5436
5641
|
} finally {
|
|
5437
|
-
stop();
|
|
5642
|
+
progress.stop();
|
|
5438
5643
|
}
|
|
5439
|
-
const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent } = collected;
|
|
5644
|
+
const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent, attributionComplete } = collected;
|
|
5440
5645
|
if (entries.length === 0) {
|
|
5441
5646
|
console.log();
|
|
5442
5647
|
console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
|
|
5443
|
-
console.log(
|
|
5648
|
+
console.log(pc3.dim(" Use Claude Code, Codex, Gemini CLI (or friends) and come back."));
|
|
5444
5649
|
return;
|
|
5445
5650
|
}
|
|
5446
5651
|
const payload = { cliVersion: VERSION, entries };
|
|
@@ -5450,85 +5655,72 @@ async function run(flags) {
|
|
|
5450
5655
|
if (skills.length > 0) payload.skills = skills;
|
|
5451
5656
|
if (projects.length > 0) payload.projects = projects;
|
|
5452
5657
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
5658
|
+
if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
|
|
5659
|
+
payload.attributionComplete = true;
|
|
5453
5660
|
if (flags.board) payload.board = flags.board;
|
|
5454
5661
|
if (flags.dryRun) {
|
|
5455
|
-
console.log(
|
|
5662
|
+
console.log(pc3.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
5456
5663
|
console.log(JSON.stringify(payload, null, 2));
|
|
5457
5664
|
return;
|
|
5458
5665
|
}
|
|
5459
5666
|
if (!flags.quiet) printSummary(entries);
|
|
5460
5667
|
if (flags.local) {
|
|
5461
|
-
showLocalDashboard(
|
|
5668
|
+
showLocalDashboard(payload);
|
|
5462
5669
|
if (!flags.quiet && process.stdin.isTTY) {
|
|
5463
5670
|
await publishLocal(payload, {
|
|
5464
5671
|
confirm,
|
|
5465
5672
|
ensureAnonKey,
|
|
5466
5673
|
anonSubmit,
|
|
5467
5674
|
openBrowser,
|
|
5468
|
-
log: (line) => console.log(
|
|
5675
|
+
log: (line) => console.log(pc3.dim(line))
|
|
5469
5676
|
});
|
|
5470
5677
|
}
|
|
5471
5678
|
return;
|
|
5472
5679
|
}
|
|
5473
5680
|
if (flags.noSubmit) {
|
|
5474
|
-
console.log(
|
|
5681
|
+
console.log(pc3.dim(" --no-submit: skipped the dashboard."));
|
|
5475
5682
|
return;
|
|
5476
5683
|
}
|
|
5477
|
-
const
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5684
|
+
const anonKey = ensureAnonKey();
|
|
5685
|
+
const result = await anonSubmit(anonKey, payload);
|
|
5686
|
+
const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
|
|
5687
|
+
if (!flags.quiet) {
|
|
5688
|
+
console.log(pc3.dim(" Opening your dashboard in your browser\u2026"));
|
|
5689
|
+
openBrowser(target);
|
|
5690
|
+
}
|
|
5691
|
+
console.log(
|
|
5692
|
+
` Submitted ${pc3.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
|
|
5693
|
+
);
|
|
5694
|
+
if (result.boardUrl) {
|
|
5484
5695
|
console.log(
|
|
5485
|
-
`
|
|
5696
|
+
` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
|
|
5486
5697
|
);
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
` You are ${pc2.bold(pc2.yellow(`#${result.rank}`))} with ${pc2.bold(formatTokens(result.totalTokens))} tokens burned.`
|
|
5490
|
-
);
|
|
5491
|
-
}
|
|
5492
|
-
console.log(` ${pc2.cyan(result.profileUrl)}`);
|
|
5493
|
-
if (result.boardUrl) {
|
|
5494
|
-
console.log(` \u{1F91D} You're on the friends board: ${pc2.cyan(result.boardUrl)}`);
|
|
5495
|
-
}
|
|
5698
|
+
console.log(` ${pc3.cyan(result.boardUrl)}`);
|
|
5699
|
+
console.log(pc3.dim(` Your dashboard: ${result.dashboardUrl}`));
|
|
5496
5700
|
} else {
|
|
5497
|
-
const anonKey = ensureAnonKey();
|
|
5498
|
-
const result = await anonSubmit(anonKey, payload);
|
|
5499
|
-
const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
|
|
5500
|
-
if (!flags.quiet) {
|
|
5501
|
-
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
5502
|
-
openBrowser(target);
|
|
5503
|
-
}
|
|
5504
5701
|
console.log(
|
|
5505
|
-
`
|
|
5702
|
+
` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
|
|
5506
5703
|
);
|
|
5507
|
-
|
|
5704
|
+
console.log(` ${pc3.cyan(result.dashboardUrl)}`);
|
|
5705
|
+
if (!flags.quiet) {
|
|
5508
5706
|
console.log(
|
|
5509
|
-
|
|
5707
|
+
pc3.dim(" Claim it (name + X) on the web to own your rank, or make it private / remove it.")
|
|
5510
5708
|
);
|
|
5511
|
-
console.log(` ${pc2.cyan(result.boardUrl)}`);
|
|
5512
|
-
console.log(pc2.dim(` Your dashboard: ${result.dashboardUrl}`));
|
|
5513
|
-
} else {
|
|
5514
5709
|
console.log(
|
|
5515
|
-
|
|
5710
|
+
pc3.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
|
|
5516
5711
|
);
|
|
5517
|
-
console.log(` ${pc2.cyan(result.dashboardUrl)}`);
|
|
5518
|
-
if (!flags.quiet) {
|
|
5519
|
-
console.log(
|
|
5520
|
-
pc2.dim(" Claim it (name + X) to own your rank, or make it private / remove it.")
|
|
5521
|
-
);
|
|
5522
|
-
console.log(
|
|
5523
|
-
pc2.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
|
|
5524
|
-
);
|
|
5525
|
-
}
|
|
5526
5712
|
}
|
|
5527
5713
|
}
|
|
5528
|
-
|
|
5714
|
+
if (!flags.quiet && !autoSyncInstalled()) {
|
|
5715
|
+
try {
|
|
5716
|
+
installAutoSync();
|
|
5717
|
+
} catch {
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5529
5720
|
if (!flags.quiet) {
|
|
5721
|
+
console.log();
|
|
5530
5722
|
console.log(
|
|
5531
|
-
autoSyncInstalled() ?
|
|
5723
|
+
autoSyncInstalled() ? pc3.dim(" Background sync is on \u2014 your page updates automatically every hour (`npx whoburnedmore uninstall-sync` to stop).") : pc3.dim(" Re-run anytime to update your page.")
|
|
5532
5724
|
);
|
|
5533
5725
|
}
|
|
5534
5726
|
}
|
|
@@ -5557,14 +5749,6 @@ async function main() {
|
|
|
5557
5749
|
await run({ ...flags, noSubmit: false, dryRun: false, local: false });
|
|
5558
5750
|
break;
|
|
5559
5751
|
}
|
|
5560
|
-
case "login":
|
|
5561
|
-
await login();
|
|
5562
|
-
break;
|
|
5563
|
-
case "logout":
|
|
5564
|
-
clearConfig();
|
|
5565
|
-
console.log(" Logged out. Your leaderboard data is untouched.");
|
|
5566
|
-
console.log(pc2.dim(" Delete your data anytime from your profile page."));
|
|
5567
|
-
break;
|
|
5568
5752
|
case "private":
|
|
5569
5753
|
case "public": {
|
|
5570
5754
|
const cfg = loadConfig();
|
|
@@ -5611,31 +5795,31 @@ async function main() {
|
|
|
5611
5795
|
}
|
|
5612
5796
|
function printHelp() {
|
|
5613
5797
|
console.log(`
|
|
5614
|
-
${
|
|
5798
|
+
${pc3.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
|
|
5615
5799
|
|
|
5616
|
-
${
|
|
5800
|
+
${pc3.bold("usage")}
|
|
5617
5801
|
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
5618
5802
|
npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
|
|
5619
5803
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
5620
5804
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
5621
5805
|
npx whoburnedmore --no-submit print local stats only, send nothing
|
|
5622
|
-
npx whoburnedmore private hide your
|
|
5806
|
+
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
5623
5807
|
npx whoburnedmore public put it back on the leaderboard
|
|
5624
|
-
npx whoburnedmore remove delete your
|
|
5625
|
-
npx whoburnedmore
|
|
5626
|
-
npx whoburnedmore
|
|
5627
|
-
npx whoburnedmore install-sync keep your dashboard live (background sync, 3h)
|
|
5628
|
-
npx whoburnedmore uninstall-sync remove background sync
|
|
5808
|
+
npx whoburnedmore remove delete your dashboard and its data
|
|
5809
|
+
npx whoburnedmore uninstall-sync turn off the background sync
|
|
5810
|
+
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
5629
5811
|
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5812
|
+
Background sync is on by default: after your first run, your page refreshes
|
|
5813
|
+
automatically every hour (\`uninstall-sync\` to stop). Your dashboard is public on
|
|
5814
|
+
the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
|
|
5815
|
+
it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
|
|
5816
|
+
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
5817
|
+
your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
|
|
5818
|
+
your machine at all.
|
|
5635
5819
|
`);
|
|
5636
5820
|
}
|
|
5637
5821
|
main().catch((err) => {
|
|
5638
|
-
console.error(
|
|
5822
|
+
console.error(pc3.red(`
|
|
5639
5823
|
${err.message}
|
|
5640
5824
|
`));
|
|
5641
5825
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whoburnedmore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Find out who burned more — submit your AI coding-agent token usage to the public leaderboard at whoburnedmore.com",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"ccusage": "20.0.9",
|
|
23
|
-
"picocolors": "^1.1.1"
|
|
24
|
-
"tokscale": "^1.2.7"
|
|
23
|
+
"picocolors": "^1.1.1"
|
|
25
24
|
},
|
|
26
25
|
"devDependencies": {
|
|
27
26
|
"@types/node": "^22.10.0",
|