whoburnedmore 0.6.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 +338 -123
- 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) {
|
|
@@ -133,7 +133,7 @@ function ensureAnonKey(dir = defaultConfigDir()) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// src/autosync.ts
|
|
136
|
-
var SYNC_INTERVAL_HOURS =
|
|
136
|
+
var SYNC_INTERVAL_HOURS = 1;
|
|
137
137
|
var LABEL = "com.whoburnedmore.sync";
|
|
138
138
|
function syncLogPath() {
|
|
139
139
|
return join2(defaultConfigDir(), "sync.log");
|
|
@@ -153,8 +153,14 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
153
153
|
</array>
|
|
154
154
|
<key>StartInterval</key>
|
|
155
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. -->
|
|
156
159
|
<key>RunAtLoad</key>
|
|
157
|
-
<
|
|
160
|
+
<true/>
|
|
161
|
+
<!-- Be a good citizen: macOS schedules this with background priority. -->
|
|
162
|
+
<key>ProcessType</key>
|
|
163
|
+
<string>Background</string>
|
|
158
164
|
<key>StandardOutPath</key>
|
|
159
165
|
<string>${logPath}</string>
|
|
160
166
|
<key>StandardErrorPath</key>
|
|
@@ -250,6 +256,27 @@ function autoSyncInstalled() {
|
|
|
250
256
|
return false;
|
|
251
257
|
}
|
|
252
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
|
+
|
|
253
280
|
// src/collect.ts
|
|
254
281
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
255
282
|
import { createRequire as createRequire3 } from "node:module";
|
|
@@ -281,6 +308,7 @@ function estimateCostUSD(model, t) {
|
|
|
281
308
|
|
|
282
309
|
// src/attribution.ts
|
|
283
310
|
var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
311
|
+
var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
|
|
284
312
|
var MAX_FILES = 5e3;
|
|
285
313
|
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
286
314
|
var TIME_BUDGET_MS = 3e4;
|
|
@@ -421,9 +449,95 @@ function accumulatorToResult(acc) {
|
|
|
421
449
|
projects: toProjectStats(acc.projects),
|
|
422
450
|
agent: { ...acc.agent },
|
|
423
451
|
titles: acc.titles,
|
|
424
|
-
sessionMessages: acc.sessionMessages
|
|
452
|
+
sessionMessages: acc.sessionMessages,
|
|
453
|
+
complete: true
|
|
425
454
|
};
|
|
426
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
|
+
}
|
|
427
541
|
function listTranscripts(dir) {
|
|
428
542
|
const out = [];
|
|
429
543
|
const walk = (d) => {
|
|
@@ -452,34 +566,42 @@ function listTranscripts(dir) {
|
|
|
452
566
|
function collectAttribution() {
|
|
453
567
|
const acc = createAccumulator();
|
|
454
568
|
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
569
|
+
let complete = true;
|
|
455
570
|
try {
|
|
456
|
-
for (const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
}
|
|
463
585
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
continue;
|
|
586
|
+
}
|
|
587
|
+
for (const file of listTranscripts(CODEX_SESSIONS)) {
|
|
588
|
+
if (Date.now() > deadline) {
|
|
589
|
+
complete = false;
|
|
590
|
+
break;
|
|
470
591
|
}
|
|
471
|
-
const ctx =
|
|
472
|
-
for (const line of
|
|
592
|
+
const ctx = createCodexContext();
|
|
593
|
+
for (const line of readLines(file)) {
|
|
473
594
|
if (!line) continue;
|
|
474
595
|
try {
|
|
475
|
-
|
|
596
|
+
processCodexRecord(JSON.parse(line), acc, ctx);
|
|
476
597
|
} catch {
|
|
477
598
|
}
|
|
478
599
|
}
|
|
479
600
|
}
|
|
480
601
|
} catch {
|
|
602
|
+
complete = false;
|
|
481
603
|
}
|
|
482
|
-
return accumulatorToResult(acc);
|
|
604
|
+
return { ...accumulatorToResult(acc), complete };
|
|
483
605
|
}
|
|
484
606
|
|
|
485
607
|
// src/cursor.ts
|
|
@@ -702,7 +824,9 @@ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
|
|
|
702
824
|
body: JSON.stringify({ page, pageSize }),
|
|
703
825
|
signal: AbortSignal.timeout(2e4)
|
|
704
826
|
});
|
|
705
|
-
if (!res.ok)
|
|
827
|
+
if (!res.ok) {
|
|
828
|
+
throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
|
|
829
|
+
}
|
|
706
830
|
const body = await res.json();
|
|
707
831
|
const batch = body.usageEventsDisplay ?? [];
|
|
708
832
|
all.push(...batch);
|
|
@@ -912,24 +1036,34 @@ function dedupeBlocks(blocks) {
|
|
|
912
1036
|
}
|
|
913
1037
|
return [...byStart.values()];
|
|
914
1038
|
}
|
|
915
|
-
function
|
|
1039
|
+
function runCcusageOnce(cmd, args) {
|
|
916
1040
|
const res = spawnSync4(cmd, args, {
|
|
917
1041
|
encoding: "utf8",
|
|
918
1042
|
maxBuffer: 64 * 1024 * 1024,
|
|
919
1043
|
timeout: 12e4
|
|
920
1044
|
});
|
|
921
|
-
|
|
1045
|
+
const transient = res.signal != null || res.error != null;
|
|
1046
|
+
if (res.status !== 0 || !res.stdout) return { json: null, transient };
|
|
922
1047
|
try {
|
|
923
|
-
return JSON.parse(res.stdout);
|
|
1048
|
+
return { json: JSON.parse(res.stdout), transient: false };
|
|
924
1049
|
} catch {
|
|
925
|
-
return null;
|
|
1050
|
+
return { json: null, transient: false };
|
|
926
1051
|
}
|
|
927
1052
|
}
|
|
928
|
-
|
|
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) {
|
|
929
1060
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
930
1061
|
const entries = [];
|
|
931
1062
|
const toolsFound = [];
|
|
1063
|
+
let done = 0;
|
|
1064
|
+
const tick = (label) => onProgress?.(done++, COLLECT_STAGES, label);
|
|
932
1065
|
for (const source of SOURCES) {
|
|
1066
|
+
tick(`reading ${source}`);
|
|
933
1067
|
const json = runCcusage(cmd, [
|
|
934
1068
|
...prefixArgs,
|
|
935
1069
|
source,
|
|
@@ -944,17 +1078,22 @@ async function collectAll() {
|
|
|
944
1078
|
toolsFound.push(source);
|
|
945
1079
|
}
|
|
946
1080
|
}
|
|
1081
|
+
tick("reading conversations");
|
|
947
1082
|
const sessionJson = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]);
|
|
1083
|
+
tick("reading active blocks");
|
|
948
1084
|
const blockJson = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]);
|
|
949
1085
|
const sessions = sessionJson ? mapCcusageSessions(sessionJson) : [];
|
|
950
1086
|
const blocks = blockJson ? mapCcusageBlocks(blockJson) : [];
|
|
1087
|
+
tick("reading cursor");
|
|
951
1088
|
const cursor = await collectCursor();
|
|
952
1089
|
if (cursor.found) {
|
|
953
1090
|
entries.push(...cursor.entries);
|
|
954
1091
|
blocks.push(...cursor.blocks);
|
|
955
1092
|
toolsFound.push("cursor");
|
|
956
1093
|
}
|
|
957
|
-
|
|
1094
|
+
tick("reading agent transcripts");
|
|
1095
|
+
const { tools, skills, projects, agent, titles, sessionMessages, complete } = collectAttribution();
|
|
1096
|
+
onProgress?.(done, COLLECT_STAGES, "done");
|
|
958
1097
|
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
959
1098
|
const title = titles.get(s.sessionId);
|
|
960
1099
|
const messageCount = sessionMessages.get(s.sessionId);
|
|
@@ -972,7 +1111,8 @@ async function collectAll() {
|
|
|
972
1111
|
tools,
|
|
973
1112
|
skills,
|
|
974
1113
|
projects,
|
|
975
|
-
agent
|
|
1114
|
+
agent,
|
|
1115
|
+
attributionComplete: complete
|
|
976
1116
|
};
|
|
977
1117
|
}
|
|
978
1118
|
|
|
@@ -5118,6 +5258,13 @@ var SubmitPayload = external_exports.object({
|
|
|
5118
5258
|
projects: external_exports.array(ProjectStat).max(500).optional(),
|
|
5119
5259
|
/** Optional subagent-vs-main rollup parsed from local transcripts. */
|
|
5120
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(),
|
|
5121
5268
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
5122
5269
|
board: external_exports.string().min(1).max(32).optional()
|
|
5123
5270
|
});
|
|
@@ -5132,7 +5279,7 @@ var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
|
|
|
5132
5279
|
var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
|
|
5133
5280
|
|
|
5134
5281
|
// src/output.ts
|
|
5135
|
-
import
|
|
5282
|
+
import pc2 from "picocolors";
|
|
5136
5283
|
function formatTokens(n) {
|
|
5137
5284
|
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
5138
5285
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
@@ -5160,20 +5307,20 @@ function printSummary(entries) {
|
|
|
5160
5307
|
byTool.set(e.tool, agg);
|
|
5161
5308
|
}
|
|
5162
5309
|
console.log();
|
|
5163
|
-
console.log(
|
|
5310
|
+
console.log(pc2.bold(pc2.yellow(" \u{1F525} your burn report")));
|
|
5164
5311
|
console.log();
|
|
5165
5312
|
const rows = [...byTool.entries()].sort((a, b) => b[1].tokens - a[1].tokens);
|
|
5166
5313
|
for (const [tool, agg] of rows) {
|
|
5167
5314
|
console.log(
|
|
5168
|
-
` ${
|
|
5315
|
+
` ${pc2.cyan(tool.padEnd(10))} ${formatTokens(agg.tokens).padStart(9)} tokens ${formatUSD(agg.cost).padStart(10)} ${String(agg.days.size).padStart(4)} days`
|
|
5169
5316
|
);
|
|
5170
5317
|
}
|
|
5171
|
-
console.log(
|
|
5318
|
+
console.log(pc2.dim(" " + "\u2500".repeat(46)));
|
|
5172
5319
|
console.log(
|
|
5173
|
-
` ${
|
|
5320
|
+
` ${pc2.bold("total".padEnd(10))} ${pc2.bold(formatTokens(totalTokens).padStart(9))} tokens ${pc2.bold(formatUSD(totalCost).padStart(10))}`
|
|
5174
5321
|
);
|
|
5175
5322
|
if (todayTokens > 0) {
|
|
5176
|
-
console.log(` ${
|
|
5323
|
+
console.log(` ${pc2.dim("today".padEnd(10))} ${formatTokens(todayTokens).padStart(9)} tokens`);
|
|
5177
5324
|
}
|
|
5178
5325
|
console.log();
|
|
5179
5326
|
}
|
|
@@ -5233,7 +5380,18 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
5233
5380
|
const modelRows = [...byModel.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, 12).map(
|
|
5234
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>`
|
|
5235
5382
|
).join("");
|
|
5236
|
-
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("");
|
|
5237
5395
|
const connectCta = connect ? `
|
|
5238
5396
|
<form class="connect" method="POST" action="${esc(connect.webBaseUrl)}/connect">
|
|
5239
5397
|
<input type="hidden" name="payload" value="${Buffer.from(JSON.stringify(connect.payload)).toString("base64")}">
|
|
@@ -5260,19 +5418,36 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
5260
5418
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
5261
5419
|
line-height: 1.5;
|
|
5262
5420
|
}
|
|
5263
|
-
.wrap { max-width:
|
|
5264
|
-
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; }
|
|
5265
5423
|
h1 .q { color: #ea580c; }
|
|
5266
|
-
.sub { color: #a8a29e; font-size:
|
|
5424
|
+
.sub { color: #a8a29e; font-size: 13px; margin-top: 4px; }
|
|
5267
5425
|
.mono, code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
5268
|
-
.
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
.
|
|
5273
|
-
.
|
|
5274
|
-
.
|
|
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; }
|
|
5275
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; }
|
|
5276
5451
|
.chart { display: flex; align-items: flex-end; gap: 2px; height: 140px; }
|
|
5277
5452
|
.bar { flex: 1; background: linear-gradient(to top, #ea580c, #f97316); border-radius: 2px 2px 0 0; min-height: 0; transition: opacity .15s; }
|
|
5278
5453
|
.bar:hover { opacity: .7; }
|
|
@@ -5296,48 +5471,55 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
5296
5471
|
</head>
|
|
5297
5472
|
<body>
|
|
5298
5473
|
<div class="wrap">
|
|
5299
|
-
<
|
|
5300
|
-
|
|
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>
|
|
5301
5481
|
${connectCta}
|
|
5302
|
-
<div class="
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
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>
|
|
5309
5494
|
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5495
|
+
<div class="col">
|
|
5496
|
+
<div class="panel">
|
|
5497
|
+
<h2>daily burn (last 60 days)</h2>
|
|
5498
|
+
<div class="chart">${bars}</div>
|
|
5499
|
+
</div>
|
|
5314
5500
|
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
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>
|
|
5322
5508
|
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
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>
|
|
5330
5516
|
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
<tr><td>cache write</td><td class="num">${esc(formatTokens(totals.cacheWrite))}</td></tr>
|
|
5338
|
-
<tr><td>cache read</td><td class="num">${esc(formatTokens(totals.cacheRead))}</td></tr>
|
|
5339
|
-
</tbody>
|
|
5340
|
-
</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>
|
|
5341
5523
|
</div>
|
|
5342
5524
|
|
|
5343
5525
|
<div class="foot">
|
|
@@ -5369,23 +5551,45 @@ async function publishLocal(payload, deps) {
|
|
|
5369
5551
|
// src/index.ts
|
|
5370
5552
|
var require2 = createRequire4(import.meta.url);
|
|
5371
5553
|
var VERSION = require2("../package.json").version;
|
|
5372
|
-
function
|
|
5554
|
+
function startProgress() {
|
|
5373
5555
|
if (!process.stdout.isTTY) {
|
|
5374
|
-
|
|
5375
|
-
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
|
+
}
|
|
5376
5567
|
};
|
|
5377
5568
|
}
|
|
5378
|
-
const
|
|
5379
|
-
let
|
|
5569
|
+
const width = 24;
|
|
5570
|
+
let target = 0;
|
|
5571
|
+
let shown = 0;
|
|
5572
|
+
let label = "starting\u2026";
|
|
5380
5573
|
process.stdout.write("\x1B[?25l");
|
|
5381
|
-
const
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
process.stdout.write(
|
|
5388
|
-
|
|
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
|
+
}
|
|
5389
5593
|
};
|
|
5390
5594
|
}
|
|
5391
5595
|
function openBrowser(url) {
|
|
@@ -5396,7 +5600,7 @@ function openBrowser(url) {
|
|
|
5396
5600
|
async function confirm(question) {
|
|
5397
5601
|
if (!process.stdin.isTTY) return false;
|
|
5398
5602
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5399
|
-
const answer = (await rl.question(`${question} ${
|
|
5603
|
+
const answer = (await rl.question(`${question} ${pc3.dim("[Y/n]")} `)).trim();
|
|
5400
5604
|
rl.close();
|
|
5401
5605
|
return answer === "" || /^y(es)?$/i.test(answer);
|
|
5402
5606
|
}
|
|
@@ -5412,27 +5616,36 @@ function showLocalDashboard(payload) {
|
|
|
5412
5616
|
})
|
|
5413
5617
|
);
|
|
5414
5618
|
console.log();
|
|
5415
|
-
console.log(` Local dashboard: ${
|
|
5416
|
-
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."));
|
|
5417
5621
|
openBrowser(`file://${file}`);
|
|
5418
5622
|
}
|
|
5419
5623
|
async function run(flags) {
|
|
5420
5624
|
if (!flags.quiet) {
|
|
5421
|
-
|
|
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
|
+
}
|
|
5422
5635
|
}
|
|
5423
|
-
const
|
|
5424
|
-
} :
|
|
5636
|
+
const progress = flags.quiet ? { onProgress: void 0, stop: () => {
|
|
5637
|
+
} } : startProgress();
|
|
5425
5638
|
let collected;
|
|
5426
5639
|
try {
|
|
5427
|
-
collected = await collectAll();
|
|
5640
|
+
collected = await collectAll(progress.onProgress);
|
|
5428
5641
|
} finally {
|
|
5429
|
-
stop();
|
|
5642
|
+
progress.stop();
|
|
5430
5643
|
}
|
|
5431
|
-
const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent } = collected;
|
|
5644
|
+
const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent, attributionComplete } = collected;
|
|
5432
5645
|
if (entries.length === 0) {
|
|
5433
5646
|
console.log();
|
|
5434
5647
|
console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
|
|
5435
|
-
console.log(
|
|
5648
|
+
console.log(pc3.dim(" Use Claude Code, Codex, Gemini CLI (or friends) and come back."));
|
|
5436
5649
|
return;
|
|
5437
5650
|
}
|
|
5438
5651
|
const payload = { cliVersion: VERSION, entries };
|
|
@@ -5442,9 +5655,11 @@ async function run(flags) {
|
|
|
5442
5655
|
if (skills.length > 0) payload.skills = skills;
|
|
5443
5656
|
if (projects.length > 0) payload.projects = projects;
|
|
5444
5657
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
5658
|
+
if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
|
|
5659
|
+
payload.attributionComplete = true;
|
|
5445
5660
|
if (flags.board) payload.board = flags.board;
|
|
5446
5661
|
if (flags.dryRun) {
|
|
5447
|
-
console.log(
|
|
5662
|
+
console.log(pc3.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
5448
5663
|
console.log(JSON.stringify(payload, null, 2));
|
|
5449
5664
|
return;
|
|
5450
5665
|
}
|
|
@@ -5457,42 +5672,42 @@ async function run(flags) {
|
|
|
5457
5672
|
ensureAnonKey,
|
|
5458
5673
|
anonSubmit,
|
|
5459
5674
|
openBrowser,
|
|
5460
|
-
log: (line) => console.log(
|
|
5675
|
+
log: (line) => console.log(pc3.dim(line))
|
|
5461
5676
|
});
|
|
5462
5677
|
}
|
|
5463
5678
|
return;
|
|
5464
5679
|
}
|
|
5465
5680
|
if (flags.noSubmit) {
|
|
5466
|
-
console.log(
|
|
5681
|
+
console.log(pc3.dim(" --no-submit: skipped the dashboard."));
|
|
5467
5682
|
return;
|
|
5468
5683
|
}
|
|
5469
5684
|
const anonKey = ensureAnonKey();
|
|
5470
5685
|
const result = await anonSubmit(anonKey, payload);
|
|
5471
5686
|
const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
|
|
5472
5687
|
if (!flags.quiet) {
|
|
5473
|
-
console.log(
|
|
5688
|
+
console.log(pc3.dim(" Opening your dashboard in your browser\u2026"));
|
|
5474
5689
|
openBrowser(target);
|
|
5475
5690
|
}
|
|
5476
5691
|
console.log(
|
|
5477
|
-
` Submitted ${
|
|
5692
|
+
` Submitted ${pc3.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
|
|
5478
5693
|
);
|
|
5479
5694
|
if (result.boardUrl) {
|
|
5480
5695
|
console.log(
|
|
5481
|
-
` You burned ${
|
|
5696
|
+
` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
|
|
5482
5697
|
);
|
|
5483
|
-
console.log(` ${
|
|
5484
|
-
console.log(
|
|
5698
|
+
console.log(` ${pc3.cyan(result.boardUrl)}`);
|
|
5699
|
+
console.log(pc3.dim(` Your dashboard: ${result.dashboardUrl}`));
|
|
5485
5700
|
} else {
|
|
5486
5701
|
console.log(
|
|
5487
|
-
` You burned ${
|
|
5702
|
+
` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
|
|
5488
5703
|
);
|
|
5489
|
-
console.log(` ${
|
|
5704
|
+
console.log(` ${pc3.cyan(result.dashboardUrl)}`);
|
|
5490
5705
|
if (!flags.quiet) {
|
|
5491
5706
|
console.log(
|
|
5492
|
-
|
|
5707
|
+
pc3.dim(" Claim it (name + X) on the web to own your rank, or make it private / remove it.")
|
|
5493
5708
|
);
|
|
5494
5709
|
console.log(
|
|
5495
|
-
|
|
5710
|
+
pc3.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
|
|
5496
5711
|
);
|
|
5497
5712
|
}
|
|
5498
5713
|
}
|
|
@@ -5505,7 +5720,7 @@ async function run(flags) {
|
|
|
5505
5720
|
if (!flags.quiet) {
|
|
5506
5721
|
console.log();
|
|
5507
5722
|
console.log(
|
|
5508
|
-
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.")
|
|
5509
5724
|
);
|
|
5510
5725
|
}
|
|
5511
5726
|
}
|
|
@@ -5580,9 +5795,9 @@ async function main() {
|
|
|
5580
5795
|
}
|
|
5581
5796
|
function printHelp() {
|
|
5582
5797
|
console.log(`
|
|
5583
|
-
${
|
|
5798
|
+
${pc3.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
|
|
5584
5799
|
|
|
5585
|
-
${
|
|
5800
|
+
${pc3.bold("usage")}
|
|
5586
5801
|
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
5587
5802
|
npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
|
|
5588
5803
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
@@ -5595,7 +5810,7 @@ function printHelp() {
|
|
|
5595
5810
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
5596
5811
|
|
|
5597
5812
|
Background sync is on by default: after your first run, your page refreshes
|
|
5598
|
-
automatically every
|
|
5813
|
+
automatically every hour (\`uninstall-sync\` to stop). Your dashboard is public on
|
|
5599
5814
|
the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
|
|
5600
5815
|
it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
|
|
5601
5816
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
@@ -5604,7 +5819,7 @@ function printHelp() {
|
|
|
5604
5819
|
`);
|
|
5605
5820
|
}
|
|
5606
5821
|
main().catch((err) => {
|
|
5607
|
-
console.error(
|
|
5822
|
+
console.error(pc3.red(`
|
|
5608
5823
|
${err.message}
|
|
5609
5824
|
`));
|
|
5610
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",
|