whoburnedmore 0.6.0 → 0.8.1
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 +409 -147
- 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,10 +256,29 @@ function autoSyncInstalled() {
|
|
|
250
256
|
return false;
|
|
251
257
|
}
|
|
252
258
|
|
|
259
|
+
// src/banner.ts
|
|
260
|
+
import pc from "picocolors";
|
|
261
|
+
var WORD = "whoburnedmore";
|
|
262
|
+
var SHADES = [226, 220, 214, 208, 202, 196];
|
|
263
|
+
function paintWordmark() {
|
|
264
|
+
if (!pc.isColorSupported) return `${WORD}?`;
|
|
265
|
+
const letters = [...WORD].map((ch, i) => `\x1B[1;38;5;${SHADES[i % SHADES.length]}m${ch}`).join("");
|
|
266
|
+
return `${letters}\x1B[1;38;5;208m?\x1B[0m`;
|
|
267
|
+
}
|
|
268
|
+
function printBanner() {
|
|
269
|
+
const rule = pc.dim("\u2500".repeat(30));
|
|
270
|
+
console.log();
|
|
271
|
+
console.log(` \u{1F525} ${paintWordmark()}`);
|
|
272
|
+
console.log(` ${rule}`);
|
|
273
|
+
console.log(` ${pc.dim("who burned more \u2014 you, or them?")}`);
|
|
274
|
+
console.log();
|
|
275
|
+
}
|
|
276
|
+
|
|
253
277
|
// src/collect.ts
|
|
254
|
-
import {
|
|
278
|
+
import { execFile } from "node:child_process";
|
|
255
279
|
import { createRequire as createRequire3 } from "node:module";
|
|
256
280
|
import { dirname as dirname2, join as join6 } from "node:path";
|
|
281
|
+
import { promisify } from "node:util";
|
|
257
282
|
|
|
258
283
|
// src/attribution.ts
|
|
259
284
|
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
@@ -281,9 +306,10 @@ function estimateCostUSD(model, t) {
|
|
|
281
306
|
|
|
282
307
|
// src/attribution.ts
|
|
283
308
|
var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
309
|
+
var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
|
|
284
310
|
var MAX_FILES = 5e3;
|
|
285
311
|
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
286
|
-
var TIME_BUDGET_MS =
|
|
312
|
+
var TIME_BUDGET_MS = 12e3;
|
|
287
313
|
var MAX_STATS = 300;
|
|
288
314
|
var MAX_PROJECTS = 500;
|
|
289
315
|
function createAccumulator() {
|
|
@@ -421,9 +447,95 @@ function accumulatorToResult(acc) {
|
|
|
421
447
|
projects: toProjectStats(acc.projects),
|
|
422
448
|
agent: { ...acc.agent },
|
|
423
449
|
titles: acc.titles,
|
|
424
|
-
sessionMessages: acc.sessionMessages
|
|
450
|
+
sessionMessages: acc.sessionMessages,
|
|
451
|
+
complete: true
|
|
425
452
|
};
|
|
426
453
|
}
|
|
454
|
+
function numTok(v) {
|
|
455
|
+
const x = Math.round(Number(v));
|
|
456
|
+
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
457
|
+
}
|
|
458
|
+
function createCodexContext() {
|
|
459
|
+
return { cwd: "", model: "unknown", pending: [] };
|
|
460
|
+
}
|
|
461
|
+
function processCodexRecord(rec, acc, ctx) {
|
|
462
|
+
if (!rec || typeof rec !== "object") return;
|
|
463
|
+
const r = rec;
|
|
464
|
+
if (!r.payload || typeof r.payload !== "object") return;
|
|
465
|
+
const pl = r.payload;
|
|
466
|
+
const ptype = pl.type;
|
|
467
|
+
if (r.type === "session_meta" || r.type === "turn_context") {
|
|
468
|
+
if (typeof pl.cwd === "string" && pl.cwd) ctx.cwd = pl.cwd;
|
|
469
|
+
if (typeof pl.model === "string" && pl.model) ctx.model = pl.model;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
|
|
473
|
+
const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
|
|
474
|
+
const name = raw.slice(0, 128);
|
|
475
|
+
if (!name) return;
|
|
476
|
+
const id = typeof pl.call_id === "string" ? pl.call_id : void 0;
|
|
477
|
+
ctx.pending.push({ name, id });
|
|
478
|
+
const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
479
|
+
t.count += 1;
|
|
480
|
+
acc.tools.set(name, t);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (ptype === "token_count") {
|
|
484
|
+
const info = pl.info;
|
|
485
|
+
const last = info?.last_token_usage;
|
|
486
|
+
if (!last) return;
|
|
487
|
+
const inputTokens = numTok(last.input_tokens);
|
|
488
|
+
const cacheReadTokens = numTok(last.cached_input_tokens);
|
|
489
|
+
const outputTokens = numTok(last.output_tokens) + numTok(last.reasoning_output_tokens);
|
|
490
|
+
const tokens = numTok(last.total_tokens) || inputTokens + cacheReadTokens + outputTokens;
|
|
491
|
+
if (tokens <= 0) {
|
|
492
|
+
ctx.pending = [];
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
acc.agent.messageCount += 1;
|
|
496
|
+
acc.agent.totalTokens += tokens;
|
|
497
|
+
if (ctx.cwd) {
|
|
498
|
+
const name = basename(ctx.cwd).slice(0, 128) || "unknown";
|
|
499
|
+
const cost = estimateCostUSD(ctx.model, {
|
|
500
|
+
inputTokens,
|
|
501
|
+
outputTokens,
|
|
502
|
+
cacheCreationTokens: 0,
|
|
503
|
+
cacheReadTokens
|
|
504
|
+
});
|
|
505
|
+
const proj = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
|
|
506
|
+
proj.tokens += tokens;
|
|
507
|
+
proj.costUSD += cost;
|
|
508
|
+
acc.projects.set(name, proj);
|
|
509
|
+
}
|
|
510
|
+
const per = ctx.pending.length > 0 ? Math.floor(tokens / ctx.pending.length) : 0;
|
|
511
|
+
for (const tu of ctx.pending) {
|
|
512
|
+
const t = acc.tools.get(tu.name);
|
|
513
|
+
if (t) t.tokens += per;
|
|
514
|
+
}
|
|
515
|
+
ctx.pending = [];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function readLines(file) {
|
|
519
|
+
let size = 0;
|
|
520
|
+
try {
|
|
521
|
+
size = statSync(file).size;
|
|
522
|
+
} catch {
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
if (size > MAX_FILE_BYTES) return [];
|
|
526
|
+
try {
|
|
527
|
+
return readFileSync2(file, "utf8").split("\n");
|
|
528
|
+
} catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function claudeProjectDirs() {
|
|
533
|
+
const dirs = [CLAUDE_PROJECTS];
|
|
534
|
+
const cfg = process.env.CLAUDE_CONFIG_DIR;
|
|
535
|
+
if (cfg) dirs.push(join3(cfg, "projects"));
|
|
536
|
+
dirs.push(join3(homedir3(), ".config", "claude", "projects"));
|
|
537
|
+
return [...new Set(dirs)];
|
|
538
|
+
}
|
|
427
539
|
function listTranscripts(dir) {
|
|
428
540
|
const out = [];
|
|
429
541
|
const walk = (d) => {
|
|
@@ -449,37 +561,54 @@ function listTranscripts(dir) {
|
|
|
449
561
|
walk(dir);
|
|
450
562
|
return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
|
|
451
563
|
}
|
|
452
|
-
function collectAttribution() {
|
|
564
|
+
async function collectAttribution() {
|
|
453
565
|
const acc = createAccumulator();
|
|
454
566
|
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
567
|
+
let complete = true;
|
|
568
|
+
let sinceYield = 0;
|
|
569
|
+
const breathe = async () => {
|
|
570
|
+
if (++sinceYield >= 8) {
|
|
571
|
+
sinceYield = 0;
|
|
572
|
+
await new Promise((r) => setImmediate(r));
|
|
573
|
+
}
|
|
574
|
+
};
|
|
455
575
|
try {
|
|
456
|
-
for (const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
576
|
+
for (const dir of claudeProjectDirs()) {
|
|
577
|
+
for (const file of listTranscripts(dir)) {
|
|
578
|
+
if (Date.now() > deadline) {
|
|
579
|
+
complete = false;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
const ctx = createFileContext();
|
|
583
|
+
for (const line of readLines(file)) {
|
|
584
|
+
if (!line) continue;
|
|
585
|
+
try {
|
|
586
|
+
processRecord(JSON.parse(line), acc, ctx);
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
await breathe();
|
|
463
591
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
continue;
|
|
592
|
+
}
|
|
593
|
+
for (const file of listTranscripts(CODEX_SESSIONS)) {
|
|
594
|
+
if (Date.now() > deadline) {
|
|
595
|
+
complete = false;
|
|
596
|
+
break;
|
|
470
597
|
}
|
|
471
|
-
const ctx =
|
|
472
|
-
for (const line of
|
|
598
|
+
const ctx = createCodexContext();
|
|
599
|
+
for (const line of readLines(file)) {
|
|
473
600
|
if (!line) continue;
|
|
474
601
|
try {
|
|
475
|
-
|
|
602
|
+
processCodexRecord(JSON.parse(line), acc, ctx);
|
|
476
603
|
} catch {
|
|
477
604
|
}
|
|
478
605
|
}
|
|
606
|
+
await breathe();
|
|
479
607
|
}
|
|
480
608
|
} catch {
|
|
609
|
+
complete = false;
|
|
481
610
|
}
|
|
482
|
-
return accumulatorToResult(acc);
|
|
611
|
+
return { ...accumulatorToResult(acc), complete };
|
|
483
612
|
}
|
|
484
613
|
|
|
485
614
|
// src/cursor.ts
|
|
@@ -702,7 +831,9 @@ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
|
|
|
702
831
|
body: JSON.stringify({ page, pageSize }),
|
|
703
832
|
signal: AbortSignal.timeout(2e4)
|
|
704
833
|
});
|
|
705
|
-
if (!res.ok)
|
|
834
|
+
if (!res.ok) {
|
|
835
|
+
throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
|
|
836
|
+
}
|
|
706
837
|
const body = await res.json();
|
|
707
838
|
const batch = body.usageEventsDisplay ?? [];
|
|
708
839
|
all.push(...batch);
|
|
@@ -731,6 +862,7 @@ async function collectCursor() {
|
|
|
731
862
|
}
|
|
732
863
|
|
|
733
864
|
// src/collect.ts
|
|
865
|
+
var execFileAsync = promisify(execFile);
|
|
734
866
|
var SOURCES = [
|
|
735
867
|
"claude",
|
|
736
868
|
"codex",
|
|
@@ -912,49 +1044,85 @@ function dedupeBlocks(blocks) {
|
|
|
912
1044
|
}
|
|
913
1045
|
return [...byStart.values()];
|
|
914
1046
|
}
|
|
915
|
-
function
|
|
916
|
-
const res = spawnSync4(cmd, args, {
|
|
917
|
-
encoding: "utf8",
|
|
918
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
919
|
-
timeout: 12e4
|
|
920
|
-
});
|
|
921
|
-
if (res.status !== 0 || !res.stdout) return null;
|
|
1047
|
+
async function runCcusageOnce(cmd, args) {
|
|
922
1048
|
try {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1049
|
+
const { stdout } = await execFileAsync(cmd, args, {
|
|
1050
|
+
encoding: "utf8",
|
|
1051
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1052
|
+
// A single source shouldn't be able to hang the whole run. 25s is plenty
|
|
1053
|
+
// for a healthy local read; a hung source gets killed and (if transient)
|
|
1054
|
+
// retried once below rather than stalling everything for minutes.
|
|
1055
|
+
timeout: 25e3
|
|
1056
|
+
});
|
|
1057
|
+
if (!stdout) return { json: null, transient: false };
|
|
1058
|
+
try {
|
|
1059
|
+
return { json: JSON.parse(stdout), transient: false };
|
|
1060
|
+
} catch {
|
|
1061
|
+
return { json: null, transient: false };
|
|
1062
|
+
}
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
const e = err;
|
|
1065
|
+
const transient = e.killed === true || e.signal != null || typeof e.code === "string";
|
|
1066
|
+
return { json: null, transient };
|
|
926
1067
|
}
|
|
927
1068
|
}
|
|
928
|
-
async function
|
|
1069
|
+
async function runCcusage(cmd, args) {
|
|
1070
|
+
const first = await runCcusageOnce(cmd, args);
|
|
1071
|
+
if (first.json !== null || !first.transient) return first.json;
|
|
1072
|
+
return (await runCcusageOnce(cmd, args)).json;
|
|
1073
|
+
}
|
|
1074
|
+
var COLLECT_STAGES = SOURCES.length + 4;
|
|
1075
|
+
async function collectAll(onProgress) {
|
|
929
1076
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
1077
|
+
let done = 0;
|
|
1078
|
+
const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
|
|
1079
|
+
const sourceTasks = SOURCES.map(async (source) => {
|
|
1080
|
+
const json = await runCcusage(cmd, [...prefixArgs, source, "daily", "--json", "--offline"]);
|
|
1081
|
+
tick();
|
|
1082
|
+
return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
|
|
1083
|
+
});
|
|
1084
|
+
const sessionTask = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]).then(
|
|
1085
|
+
(json) => {
|
|
1086
|
+
tick();
|
|
1087
|
+
return json ? mapCcusageSessions(json) : [];
|
|
1088
|
+
}
|
|
1089
|
+
);
|
|
1090
|
+
const blockTask = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]).then(
|
|
1091
|
+
(json) => {
|
|
1092
|
+
tick();
|
|
1093
|
+
return json ? mapCcusageBlocks(json) : [];
|
|
1094
|
+
}
|
|
1095
|
+
);
|
|
1096
|
+
const cursorTask = collectCursor().then((c) => {
|
|
1097
|
+
tick();
|
|
1098
|
+
return c;
|
|
1099
|
+
});
|
|
1100
|
+
const attributionTask = collectAttribution().then((a) => {
|
|
1101
|
+
tick();
|
|
1102
|
+
return a;
|
|
1103
|
+
});
|
|
1104
|
+
const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
|
|
1105
|
+
Promise.all(sourceTasks),
|
|
1106
|
+
sessionTask,
|
|
1107
|
+
blockTask,
|
|
1108
|
+
cursorTask,
|
|
1109
|
+
attributionTask
|
|
1110
|
+
]);
|
|
930
1111
|
const entries = [];
|
|
931
1112
|
const toolsFound = [];
|
|
932
|
-
for (const source of
|
|
933
|
-
const json = runCcusage(cmd, [
|
|
934
|
-
...prefixArgs,
|
|
935
|
-
source,
|
|
936
|
-
"daily",
|
|
937
|
-
"--json",
|
|
938
|
-
"--offline"
|
|
939
|
-
]);
|
|
940
|
-
if (!json) continue;
|
|
941
|
-
const mapped = mapCcusageDaily(source, json);
|
|
1113
|
+
for (const { source, mapped } of sourceResults) {
|
|
942
1114
|
if (mapped.length > 0) {
|
|
943
1115
|
entries.push(...mapped);
|
|
944
1116
|
toolsFound.push(source);
|
|
945
1117
|
}
|
|
946
1118
|
}
|
|
947
|
-
const sessionJson = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]);
|
|
948
|
-
const blockJson = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]);
|
|
949
|
-
const sessions = sessionJson ? mapCcusageSessions(sessionJson) : [];
|
|
950
|
-
const blocks = blockJson ? mapCcusageBlocks(blockJson) : [];
|
|
951
|
-
const cursor = await collectCursor();
|
|
952
1119
|
if (cursor.found) {
|
|
953
1120
|
entries.push(...cursor.entries);
|
|
954
1121
|
blocks.push(...cursor.blocks);
|
|
955
1122
|
toolsFound.push("cursor");
|
|
956
1123
|
}
|
|
957
|
-
const { tools, skills, projects, agent, titles, sessionMessages } =
|
|
1124
|
+
const { tools, skills, projects, agent, titles, sessionMessages, complete } = attribution;
|
|
1125
|
+
onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
|
|
958
1126
|
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
959
1127
|
const title = titles.get(s.sessionId);
|
|
960
1128
|
const messageCount = sessionMessages.get(s.sessionId);
|
|
@@ -972,7 +1140,8 @@ async function collectAll() {
|
|
|
972
1140
|
tools,
|
|
973
1141
|
skills,
|
|
974
1142
|
projects,
|
|
975
|
-
agent
|
|
1143
|
+
agent,
|
|
1144
|
+
attributionComplete: complete
|
|
976
1145
|
};
|
|
977
1146
|
}
|
|
978
1147
|
|
|
@@ -5118,6 +5287,13 @@ var SubmitPayload = external_exports.object({
|
|
|
5118
5287
|
projects: external_exports.array(ProjectStat).max(500).optional(),
|
|
5119
5288
|
/** Optional subagent-vs-main rollup parsed from local transcripts. */
|
|
5120
5289
|
agent: AgentStat.optional(),
|
|
5290
|
+
/**
|
|
5291
|
+
* Set when the transcript scan completed within its time budget, i.e. the
|
|
5292
|
+
* tool/skill/project/agent rollups are a FULL snapshot. The server refreshes
|
|
5293
|
+
* the dashboard breakdowns unconditionally for a full snapshot; for a partial
|
|
5294
|
+
* one (flag absent/false) it keeps its no-shrink guard. Back-compat: omittable.
|
|
5295
|
+
*/
|
|
5296
|
+
attributionComplete: external_exports.boolean().optional(),
|
|
5121
5297
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
5122
5298
|
board: external_exports.string().min(1).max(32).optional()
|
|
5123
5299
|
});
|
|
@@ -5132,7 +5308,7 @@ var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
|
|
|
5132
5308
|
var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
|
|
5133
5309
|
|
|
5134
5310
|
// src/output.ts
|
|
5135
|
-
import
|
|
5311
|
+
import pc2 from "picocolors";
|
|
5136
5312
|
function formatTokens(n) {
|
|
5137
5313
|
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
5138
5314
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
@@ -5160,20 +5336,20 @@ function printSummary(entries) {
|
|
|
5160
5336
|
byTool.set(e.tool, agg);
|
|
5161
5337
|
}
|
|
5162
5338
|
console.log();
|
|
5163
|
-
console.log(
|
|
5339
|
+
console.log(pc2.bold(pc2.yellow(" \u{1F525} your burn report")));
|
|
5164
5340
|
console.log();
|
|
5165
5341
|
const rows = [...byTool.entries()].sort((a, b) => b[1].tokens - a[1].tokens);
|
|
5166
5342
|
for (const [tool, agg] of rows) {
|
|
5167
5343
|
console.log(
|
|
5168
|
-
` ${
|
|
5344
|
+
` ${pc2.cyan(tool.padEnd(10))} ${formatTokens(agg.tokens).padStart(9)} tokens ${formatUSD(agg.cost).padStart(10)} ${String(agg.days.size).padStart(4)} days`
|
|
5169
5345
|
);
|
|
5170
5346
|
}
|
|
5171
|
-
console.log(
|
|
5347
|
+
console.log(pc2.dim(" " + "\u2500".repeat(46)));
|
|
5172
5348
|
console.log(
|
|
5173
|
-
` ${
|
|
5349
|
+
` ${pc2.bold("total".padEnd(10))} ${pc2.bold(formatTokens(totalTokens).padStart(9))} tokens ${pc2.bold(formatUSD(totalCost).padStart(10))}`
|
|
5174
5350
|
);
|
|
5175
5351
|
if (todayTokens > 0) {
|
|
5176
|
-
console.log(` ${
|
|
5352
|
+
console.log(` ${pc2.dim("today".padEnd(10))} ${formatTokens(todayTokens).padStart(9)} tokens`);
|
|
5177
5353
|
}
|
|
5178
5354
|
console.log();
|
|
5179
5355
|
}
|
|
@@ -5233,7 +5409,18 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
5233
5409
|
const modelRows = [...byModel.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, 12).map(
|
|
5234
5410
|
([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
5411
|
).join("");
|
|
5236
|
-
const
|
|
5412
|
+
const railRow = (label, value, accent = false) => `<div class="rrow"><span class="rlabel">${esc(label)}</span><span class="rval${accent ? " accent" : ""}">${esc(value)}</span></div>`;
|
|
5413
|
+
const tb = [
|
|
5414
|
+
{ label: "input", value: totals.input, color: "#3b82f6" },
|
|
5415
|
+
{ label: "output", value: totals.output, color: "#22c55e" },
|
|
5416
|
+
{ label: "cache write", value: totals.cacheWrite, color: "#a855f7" },
|
|
5417
|
+
{ label: "cache read", value: totals.cacheRead, color: "#ea580c" }
|
|
5418
|
+
];
|
|
5419
|
+
const tbSum = tb.reduce((a, p) => a + p.value, 0) || 1;
|
|
5420
|
+
const tbBar = tb.map((p) => `<div style="width:${p.value / tbSum * 100}%;background:${p.color}"></div>`).join("");
|
|
5421
|
+
const tbCells = tb.map(
|
|
5422
|
+
(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>`
|
|
5423
|
+
).join("");
|
|
5237
5424
|
const connectCta = connect ? `
|
|
5238
5425
|
<form class="connect" method="POST" action="${esc(connect.webBaseUrl)}/connect">
|
|
5239
5426
|
<input type="hidden" name="payload" value="${Buffer.from(JSON.stringify(connect.payload)).toString("base64")}">
|
|
@@ -5260,19 +5447,36 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
5260
5447
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
5261
5448
|
line-height: 1.5;
|
|
5262
5449
|
}
|
|
5263
|
-
.wrap { max-width:
|
|
5264
|
-
h1 { font-size:
|
|
5450
|
+
.wrap { max-width: 1100px; margin: 0 auto; padding: 40px 20px 80px; }
|
|
5451
|
+
h1 { font-size: 22px; margin: 0; line-height: 1.2; }
|
|
5265
5452
|
h1 .q { color: #ea580c; }
|
|
5266
|
-
.sub { color: #a8a29e; font-size:
|
|
5453
|
+
.sub { color: #a8a29e; font-size: 13px; margin-top: 4px; }
|
|
5267
5454
|
.mono, code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
5268
|
-
.
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
.
|
|
5273
|
-
.
|
|
5274
|
-
.
|
|
5455
|
+
.head { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; }
|
|
5456
|
+
.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; }
|
|
5457
|
+
|
|
5458
|
+
/* Split layout: narrow sticky rail + wide scrolling column (mirrors the web). */
|
|
5459
|
+
.layout { display: grid; gap: 24px; align-items: start; }
|
|
5460
|
+
@media (min-width: 900px) { .layout { grid-template-columns: 280px minmax(0, 1fr); } .rail { position: sticky; top: 20px; } }
|
|
5461
|
+
.rail { display: flex; flex-direction: column; gap: 16px; }
|
|
5462
|
+
.rail-card { background: #1c1917; border: 1px solid #292524; border-radius: 14px; padding: 16px; }
|
|
5463
|
+
.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; }
|
|
5464
|
+
.rdiv { height: 1px; background: #292524; margin: 12px 0; }
|
|
5465
|
+
.rrow { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; padding: 5px 0; }
|
|
5466
|
+
.rlabel { font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: #a8a29e; }
|
|
5467
|
+
.rval { font-family: ui-monospace, monospace; font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
5468
|
+
.rval.accent { color: #4ade80; }
|
|
5469
|
+
.col { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
|
|
5470
|
+
.panel { background: #1c1917; border: 1px solid #292524; border-radius: 14px; padding: 18px 20px; }
|
|
5275
5471
|
.panel h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: #d6d3d1; margin: 0 0 14px; }
|
|
5472
|
+
.tbbar { display: flex; height: 12px; width: 100%; border-radius: 999px; overflow: hidden; background: #292524; }
|
|
5473
|
+
.tbgrid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 14px; }
|
|
5474
|
+
@media (min-width: 520px) { .tbgrid { grid-template-columns: repeat(4, 1fr); } }
|
|
5475
|
+
.tbcell { border: 1px solid #292524; background: #0c0a09; border-radius: 10px; padding: 8px 10px; }
|
|
5476
|
+
.tblabel { display: flex; align-items: center; gap: 6px; font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: #a8a29e; }
|
|
5477
|
+
.dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; flex-shrink: 0; }
|
|
5478
|
+
.tbval { font-family: ui-monospace, monospace; font-weight: 700; font-size: 16px; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
|
5479
|
+
.tbpct { font-size: 11px; color: #a8a29e; }
|
|
5276
5480
|
.chart { display: flex; align-items: flex-end; gap: 2px; height: 140px; }
|
|
5277
5481
|
.bar { flex: 1; background: linear-gradient(to top, #ea580c, #f97316); border-radius: 2px 2px 0 0; min-height: 0; transition: opacity .15s; }
|
|
5278
5482
|
.bar:hover { opacity: .7; }
|
|
@@ -5296,48 +5500,55 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
5296
5500
|
</head>
|
|
5297
5501
|
<body>
|
|
5298
5502
|
<div class="wrap">
|
|
5299
|
-
<
|
|
5300
|
-
|
|
5503
|
+
<header class="head">
|
|
5504
|
+
<div class="flame">\u{1F525}</div>
|
|
5505
|
+
<div>
|
|
5506
|
+
<h1>your local burn</h1>
|
|
5507
|
+
<div class="sub">generated ${esc(generatedAt.toISOString().slice(0, 16).replace("T", " "))} \xB7 nothing left your machine</div>
|
|
5508
|
+
</div>
|
|
5509
|
+
</header>
|
|
5301
5510
|
${connectCta}
|
|
5302
|
-
<div class="
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5511
|
+
<div class="layout">
|
|
5512
|
+
<aside class="rail">
|
|
5513
|
+
<div class="rail-card">
|
|
5514
|
+
<div class="rlabel">total tokens</div>
|
|
5515
|
+
<div class="hero-val">${esc(formatTokens(totals.tokens))}</div>
|
|
5516
|
+
<div class="rdiv"></div>
|
|
5517
|
+
${railRow("avg / day", formatTokens(avgPerDay), true)}
|
|
5518
|
+
${railRow("est. cost", formatUSD(totals.cost))}
|
|
5519
|
+
${railRow("active days", String(activeDays))}
|
|
5520
|
+
${railRow("today", formatTokens(todayTokens))}
|
|
5521
|
+
</div>
|
|
5522
|
+
</aside>
|
|
5309
5523
|
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5524
|
+
<div class="col">
|
|
5525
|
+
<div class="panel">
|
|
5526
|
+
<h2>daily burn (last 60 days)</h2>
|
|
5527
|
+
<div class="chart">${bars}</div>
|
|
5528
|
+
</div>
|
|
5314
5529
|
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5530
|
+
<div class="panel">
|
|
5531
|
+
<h2>by tool</h2>
|
|
5532
|
+
<table>
|
|
5533
|
+
<thead><tr><th>tool</th><th class="num">tokens</th><th class="num">est. cost</th></tr></thead>
|
|
5534
|
+
<tbody>${toolRows || '<tr><td colspan="3">no usage found</td></tr>'}</tbody>
|
|
5535
|
+
</table>
|
|
5536
|
+
</div>
|
|
5322
5537
|
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5538
|
+
<div class="panel">
|
|
5539
|
+
<h2>by model</h2>
|
|
5540
|
+
<table>
|
|
5541
|
+
<thead><tr><th>model</th><th class="num">tokens</th><th class="num">est. cost</th></tr></thead>
|
|
5542
|
+
<tbody>${modelRows || '<tr><td colspan="3">no usage found</td></tr>'}</tbody>
|
|
5543
|
+
</table>
|
|
5544
|
+
</div>
|
|
5330
5545
|
|
|
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>
|
|
5546
|
+
<div class="panel">
|
|
5547
|
+
<h2>token breakdown</h2>
|
|
5548
|
+
<div class="tbbar">${tbBar}</div>
|
|
5549
|
+
<div class="tbgrid">${tbCells}</div>
|
|
5550
|
+
</div>
|
|
5551
|
+
</div>
|
|
5341
5552
|
</div>
|
|
5342
5553
|
|
|
5343
5554
|
<div class="foot">
|
|
@@ -5369,23 +5580,62 @@ async function publishLocal(payload, deps) {
|
|
|
5369
5580
|
// src/index.ts
|
|
5370
5581
|
var require2 = createRequire4(import.meta.url);
|
|
5371
5582
|
var VERSION = require2("../package.json").version;
|
|
5372
|
-
|
|
5583
|
+
var LOADING_VIBES = [
|
|
5584
|
+
"counting up your token usage, right here on your machine\u2026",
|
|
5585
|
+
"tallying tokens across every coding agent you use\u2026",
|
|
5586
|
+
"adding up cache reads, writes & all the burn\u2026",
|
|
5587
|
+
"tokens & totals only \u2014 never your prompts or code\u2026",
|
|
5588
|
+
"summing up your usage, model by model\u2026",
|
|
5589
|
+
"working out what all that burn cost you \u{1F525}",
|
|
5590
|
+
"crunching your daily token totals\u2026",
|
|
5591
|
+
"almost there \u2014 adding it all up\u2026"
|
|
5592
|
+
];
|
|
5593
|
+
function startProgress() {
|
|
5373
5594
|
if (!process.stdout.isTTY) {
|
|
5374
|
-
|
|
5375
|
-
return
|
|
5595
|
+
let lastLogged = -1;
|
|
5596
|
+
return {
|
|
5597
|
+
onProgress: (done, total) => {
|
|
5598
|
+
const pct = Math.round(done / total * 100);
|
|
5599
|
+
if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
|
|
5600
|
+
lastLogged = pct;
|
|
5601
|
+
console.log(pc3.dim(` counting local token usage\u2026 ${pct}%`));
|
|
5602
|
+
}
|
|
5603
|
+
},
|
|
5604
|
+
stop: () => {
|
|
5605
|
+
}
|
|
5376
5606
|
};
|
|
5377
5607
|
}
|
|
5378
|
-
const
|
|
5379
|
-
let
|
|
5608
|
+
const width = 24;
|
|
5609
|
+
let target = 0;
|
|
5610
|
+
let shown = 0;
|
|
5611
|
+
let ticks = 0;
|
|
5380
5612
|
process.stdout.write("\x1B[?25l");
|
|
5381
|
-
const
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5613
|
+
const vibe = () => {
|
|
5614
|
+
if (shown >= 0.9) return LOADING_VIBES[LOADING_VIBES.length - 1];
|
|
5615
|
+
const i = Math.floor(ticks / 18) % (LOADING_VIBES.length - 1);
|
|
5616
|
+
return LOADING_VIBES[i];
|
|
5617
|
+
};
|
|
5618
|
+
const render = () => {
|
|
5619
|
+
ticks++;
|
|
5620
|
+
shown += (target - shown) * 0.3;
|
|
5621
|
+
if (target - shown < 4e-3) shown = target;
|
|
5622
|
+
const filled = Math.round(shown * width);
|
|
5623
|
+
const bar = pc3.yellow("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
|
|
5624
|
+
const pct = String(Math.round(shown * 100)).padStart(3);
|
|
5625
|
+
process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(vibe())}\x1B[K`);
|
|
5626
|
+
};
|
|
5627
|
+
render();
|
|
5628
|
+
const timer = setInterval(render, 60);
|
|
5629
|
+
return {
|
|
5630
|
+
// Stage labels are no longer surfaced (the narration is friendlier); we only
|
|
5631
|
+
// consume the done/total fraction to drive the bar.
|
|
5632
|
+
onProgress: (done, total) => {
|
|
5633
|
+
target = total > 0 ? done / total : 0;
|
|
5634
|
+
},
|
|
5635
|
+
stop: () => {
|
|
5636
|
+
clearInterval(timer);
|
|
5637
|
+
process.stdout.write("\r\x1B[2K\x1B[?25h");
|
|
5638
|
+
}
|
|
5389
5639
|
};
|
|
5390
5640
|
}
|
|
5391
5641
|
function openBrowser(url) {
|
|
@@ -5396,7 +5646,7 @@ function openBrowser(url) {
|
|
|
5396
5646
|
async function confirm(question) {
|
|
5397
5647
|
if (!process.stdin.isTTY) return false;
|
|
5398
5648
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5399
|
-
const answer = (await rl.question(`${question} ${
|
|
5649
|
+
const answer = (await rl.question(`${question} ${pc3.dim("[Y/n]")} `)).trim();
|
|
5400
5650
|
rl.close();
|
|
5401
5651
|
return answer === "" || /^y(es)?$/i.test(answer);
|
|
5402
5652
|
}
|
|
@@ -5412,27 +5662,37 @@ function showLocalDashboard(payload) {
|
|
|
5412
5662
|
})
|
|
5413
5663
|
);
|
|
5414
5664
|
console.log();
|
|
5415
|
-
console.log(` Local dashboard: ${
|
|
5416
|
-
console.log(
|
|
5665
|
+
console.log(` Local dashboard: ${pc3.cyan(`file://${file}`)}`);
|
|
5666
|
+
console.log(pc3.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
|
|
5417
5667
|
openBrowser(`file://${file}`);
|
|
5418
5668
|
}
|
|
5419
5669
|
async function run(flags) {
|
|
5420
5670
|
if (!flags.quiet) {
|
|
5421
|
-
|
|
5671
|
+
printBanner();
|
|
5672
|
+
console.log(pc3.dim(` whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
|
|
5673
|
+
if (!flags.dryRun && !flags.noSubmit && !flags.local) {
|
|
5674
|
+
console.log(
|
|
5675
|
+
pc3.dim(" Counting your token usage and posting your rank \u2014 only daily totals leave your machine, never your prompts or code.")
|
|
5676
|
+
);
|
|
5677
|
+
console.log(
|
|
5678
|
+
pc3.dim(" (`--local` keeps it fully offline \xB7 `private`/`remove` pull it anytime \xB7 details: whoburnedmore.com/trust)")
|
|
5679
|
+
);
|
|
5680
|
+
}
|
|
5681
|
+
console.log();
|
|
5422
5682
|
}
|
|
5423
|
-
const
|
|
5424
|
-
} :
|
|
5683
|
+
const progress = flags.quiet ? { onProgress: void 0, stop: () => {
|
|
5684
|
+
} } : startProgress();
|
|
5425
5685
|
let collected;
|
|
5426
5686
|
try {
|
|
5427
|
-
collected = await collectAll();
|
|
5687
|
+
collected = await collectAll(progress.onProgress);
|
|
5428
5688
|
} finally {
|
|
5429
|
-
stop();
|
|
5689
|
+
progress.stop();
|
|
5430
5690
|
}
|
|
5431
|
-
const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent } = collected;
|
|
5691
|
+
const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent, attributionComplete } = collected;
|
|
5432
5692
|
if (entries.length === 0) {
|
|
5433
5693
|
console.log();
|
|
5434
5694
|
console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
|
|
5435
|
-
console.log(
|
|
5695
|
+
console.log(pc3.dim(" Use Claude Code, Codex, Gemini CLI (or friends) and come back."));
|
|
5436
5696
|
return;
|
|
5437
5697
|
}
|
|
5438
5698
|
const payload = { cliVersion: VERSION, entries };
|
|
@@ -5442,9 +5702,11 @@ async function run(flags) {
|
|
|
5442
5702
|
if (skills.length > 0) payload.skills = skills;
|
|
5443
5703
|
if (projects.length > 0) payload.projects = projects;
|
|
5444
5704
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
5705
|
+
if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
|
|
5706
|
+
payload.attributionComplete = true;
|
|
5445
5707
|
if (flags.board) payload.board = flags.board;
|
|
5446
5708
|
if (flags.dryRun) {
|
|
5447
|
-
console.log(
|
|
5709
|
+
console.log(pc3.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
5448
5710
|
console.log(JSON.stringify(payload, null, 2));
|
|
5449
5711
|
return;
|
|
5450
5712
|
}
|
|
@@ -5457,42 +5719,42 @@ async function run(flags) {
|
|
|
5457
5719
|
ensureAnonKey,
|
|
5458
5720
|
anonSubmit,
|
|
5459
5721
|
openBrowser,
|
|
5460
|
-
log: (line) => console.log(
|
|
5722
|
+
log: (line) => console.log(pc3.dim(line))
|
|
5461
5723
|
});
|
|
5462
5724
|
}
|
|
5463
5725
|
return;
|
|
5464
5726
|
}
|
|
5465
5727
|
if (flags.noSubmit) {
|
|
5466
|
-
console.log(
|
|
5728
|
+
console.log(pc3.dim(" --no-submit: skipped the dashboard."));
|
|
5467
5729
|
return;
|
|
5468
5730
|
}
|
|
5469
5731
|
const anonKey = ensureAnonKey();
|
|
5470
5732
|
const result = await anonSubmit(anonKey, payload);
|
|
5471
5733
|
const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
|
|
5472
5734
|
if (!flags.quiet) {
|
|
5473
|
-
console.log(
|
|
5735
|
+
console.log(pc3.dim(" Opening your dashboard in your browser\u2026"));
|
|
5474
5736
|
openBrowser(target);
|
|
5475
5737
|
}
|
|
5476
5738
|
console.log(
|
|
5477
|
-
` Submitted ${
|
|
5739
|
+
` Submitted ${pc3.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
|
|
5478
5740
|
);
|
|
5479
5741
|
if (result.boardUrl) {
|
|
5480
5742
|
console.log(
|
|
5481
|
-
` You burned ${
|
|
5743
|
+
` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
|
|
5482
5744
|
);
|
|
5483
|
-
console.log(` ${
|
|
5484
|
-
console.log(
|
|
5745
|
+
console.log(` ${pc3.cyan(result.boardUrl)}`);
|
|
5746
|
+
console.log(pc3.dim(` Your dashboard: ${result.dashboardUrl}`));
|
|
5485
5747
|
} else {
|
|
5486
5748
|
console.log(
|
|
5487
|
-
` You burned ${
|
|
5749
|
+
` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
|
|
5488
5750
|
);
|
|
5489
|
-
console.log(` ${
|
|
5751
|
+
console.log(` ${pc3.cyan(result.dashboardUrl)}`);
|
|
5490
5752
|
if (!flags.quiet) {
|
|
5491
5753
|
console.log(
|
|
5492
|
-
|
|
5754
|
+
pc3.dim(" Claim it (name + X) on the web to own your rank, or make it private / remove it.")
|
|
5493
5755
|
);
|
|
5494
5756
|
console.log(
|
|
5495
|
-
|
|
5757
|
+
pc3.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
|
|
5496
5758
|
);
|
|
5497
5759
|
}
|
|
5498
5760
|
}
|
|
@@ -5505,7 +5767,7 @@ async function run(flags) {
|
|
|
5505
5767
|
if (!flags.quiet) {
|
|
5506
5768
|
console.log();
|
|
5507
5769
|
console.log(
|
|
5508
|
-
autoSyncInstalled() ?
|
|
5770
|
+
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
5771
|
);
|
|
5510
5772
|
}
|
|
5511
5773
|
}
|
|
@@ -5580,9 +5842,9 @@ async function main() {
|
|
|
5580
5842
|
}
|
|
5581
5843
|
function printHelp() {
|
|
5582
5844
|
console.log(`
|
|
5583
|
-
${
|
|
5845
|
+
${pc3.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
|
|
5584
5846
|
|
|
5585
|
-
${
|
|
5847
|
+
${pc3.bold("usage")}
|
|
5586
5848
|
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
5587
5849
|
npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
|
|
5588
5850
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
@@ -5595,7 +5857,7 @@ function printHelp() {
|
|
|
5595
5857
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
5596
5858
|
|
|
5597
5859
|
Background sync is on by default: after your first run, your page refreshes
|
|
5598
|
-
automatically every
|
|
5860
|
+
automatically every hour (\`uninstall-sync\` to stop). Your dashboard is public on
|
|
5599
5861
|
the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
|
|
5600
5862
|
it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
|
|
5601
5863
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
@@ -5604,7 +5866,7 @@ function printHelp() {
|
|
|
5604
5866
|
`);
|
|
5605
5867
|
}
|
|
5606
5868
|
main().catch((err) => {
|
|
5607
|
-
console.error(
|
|
5869
|
+
console.error(pc3.red(`
|
|
5608
5870
|
${err.message}
|
|
5609
5871
|
`));
|
|
5610
5872
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whoburnedmore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|