whoburnedmore 0.9.4 → 0.9.6
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 +40 -23
- package/dist/index.js +412 -131
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# whoburnedmore
|
|
2
2
|
|
|
3
|
-
**who burned more tokens — you or them?** One command reads your local AI coding-agent usage logs,
|
|
3
|
+
**who burned more tokens — you or them?** One command reads your local AI coding-agent usage logs, adds up your burn, and puts you on the public leaderboard with a **shareable dashboard**.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/whoburnedmore)
|
|
6
6
|
[](https://www.npmjs.com/package/whoburnedmore)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
npx whoburnedmore
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
That's the whole thing. No install, no
|
|
14
|
+
That's the whole thing. No install, no config. Your **first run signs you in** — the CLI opens a page, you approve a short code, and this machine is bound to your account.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -28,13 +28,15 @@ whoburnedmore · https://whoburnedmore.com
|
|
|
28
28
|
total 2.31B tokens $2,656.91
|
|
29
29
|
today 92.8M tokens
|
|
30
30
|
|
|
31
|
-
You burned 2.31B tokens. Your
|
|
32
|
-
https://whoburnedmore.com/
|
|
31
|
+
You burned 2.31B tokens. Your dashboard:
|
|
32
|
+
https://whoburnedmore.com/u/<your-handle>
|
|
33
33
|
|
|
34
|
-
Re-run anytime to update it
|
|
34
|
+
Re-run anytime to update it. Background sync keeps it fresh every 15 min.
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Once you're signed in, your usage lands on the leaderboard under your handle. Run the command again anytime to refresh — or let the background sync do it for you.
|
|
38
|
+
|
|
39
|
+
**Want to stay off the board?** Run `npx whoburnedmore private` to hide your dashboard, or `npx whoburnedmore --local` to never upload anything at all.
|
|
38
40
|
|
|
39
41
|
## Privacy first
|
|
40
42
|
|
|
@@ -46,7 +48,7 @@ See exactly what would be sent, and send nothing:
|
|
|
46
48
|
npx whoburnedmore --dry-run
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
Or stay completely offline — `--local` builds the same dashboard as a self-contained HTML file on your machine and opens it in your browser, sending nothing anywhere:
|
|
51
|
+
Or stay completely offline — `--local` builds the same dashboard as a self-contained HTML file on your machine and opens it in your browser, sending nothing anywhere and never signing you in:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
npx whoburnedmore --local
|
|
@@ -56,14 +58,21 @@ npx whoburnedmore --local
|
|
|
56
58
|
|
|
57
59
|
| Command | What it does |
|
|
58
60
|
| --- | --- |
|
|
59
|
-
| `npx whoburnedmore` |
|
|
60
|
-
| `npx whoburnedmore --
|
|
61
|
+
| `npx whoburnedmore` | Sign in, submit your burn, land on the leaderboard, open your dashboard |
|
|
62
|
+
| `npx whoburnedmore --board=CODE` | Compare with friends — sign in and join their board |
|
|
63
|
+
| `npx whoburnedmore --org=SLUG` | Submit to your organization's board (companies / hackathons) |
|
|
64
|
+
| `npx whoburnedmore --local` | Build the dashboard locally and open it (fully offline, no sign-in) |
|
|
61
65
|
| `npx whoburnedmore --dry-run` | Print exactly what would be sent, send nothing |
|
|
62
66
|
| `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
|
|
63
|
-
| `npx whoburnedmore
|
|
64
|
-
| `npx whoburnedmore
|
|
65
|
-
| `npx whoburnedmore
|
|
66
|
-
| `npx whoburnedmore
|
|
67
|
+
| `npx whoburnedmore private` | Hide your dashboard from the leaderboard |
|
|
68
|
+
| `npx whoburnedmore public` | Put it back on the leaderboard |
|
|
69
|
+
| `npx whoburnedmore remove` | Delete your dashboard and all its data |
|
|
70
|
+
| `npx whoburnedmore verify` | Delisted? Re-verify your usage to get back on |
|
|
71
|
+
| `npx whoburnedmore link --token=TOKEN` | Bind a server / VM to your account (token from your profile) |
|
|
72
|
+
| `npx whoburnedmore daemon` | Keep syncing in the foreground (containers with no scheduler) |
|
|
73
|
+
| `npx whoburnedmore status` | Check background-sync health (last sync, staleness) |
|
|
74
|
+
| `npx whoburnedmore install-sync` | Turn the 15-minute background sync back on |
|
|
75
|
+
| `npx whoburnedmore uninstall-sync` | Turn the background sync off |
|
|
67
76
|
|
|
68
77
|
## Supported tools
|
|
69
78
|
|
|
@@ -73,29 +82,37 @@ Usage is read via [`ccusage`](https://www.npmjs.com/package/ccusage) from any of
|
|
|
73
82
|
|
|
74
83
|
Whatever you have logs for is picked up automatically — you don't pass any flags.
|
|
75
84
|
|
|
76
|
-
##
|
|
85
|
+
## Compete with friends and teams
|
|
77
86
|
|
|
78
|
-
|
|
87
|
+
- **Friends:** join someone's board with `npx whoburnedmore --board=CODE` (the code comes from their board's invite).
|
|
88
|
+
- **Companies / hackathons:** submit to an organization board with `npx whoburnedmore --org=SLUG` (add `--pass=CODE` if the org requires a join code).
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
npx whoburnedmore login
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
It uses a device flow — a code appears in your terminal, you approve it in the browser, and you're done. Your submissions then link to your handle at `whoburnedmore.com/u/<handle>`. You can delete your account and every row of data anytime from your profile.
|
|
90
|
+
Your submissions link to your handle at `whoburnedmore.com/u/<handle>`. You can delete your account and every row of data anytime from your profile.
|
|
85
91
|
|
|
86
92
|
## Keep it live
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
Background sync is on by default after your first run, so your dashboard stays fresh without re-running by hand. To manage it:
|
|
89
95
|
|
|
90
96
|
```bash
|
|
91
|
-
npx whoburnedmore
|
|
92
|
-
npx whoburnedmore uninstall-sync #
|
|
97
|
+
npx whoburnedmore status # last sync + staleness
|
|
98
|
+
npx whoburnedmore uninstall-sync # turn the background sync off
|
|
99
|
+
npx whoburnedmore install-sync # turn it back on (launchd / cron / scheduled task)
|
|
93
100
|
```
|
|
94
101
|
|
|
95
102
|
The installed background job runs the latest published `whoburnedmore` package on
|
|
96
103
|
each sync tick, so future CLI fixes are picked up automatically after the next
|
|
97
104
|
15-minute refresh.
|
|
98
105
|
|
|
106
|
+
## Servers & VMs
|
|
107
|
+
|
|
108
|
+
Generate a one-time `link` command from your profile on whoburnedmore.com and run it inside the machine to bind it to your account:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx whoburnedmore link --token=TOKEN
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
On a persistent VM, background sync uses cron or a systemd user timer automatically. In a container or any host without a scheduler, run `npx whoburnedmore daemon` under your process manager instead. Set `WHOBURNEDMORE_CONFIG_DIR` to a persistent path so the machine identity survives restarts.
|
|
115
|
+
|
|
99
116
|
## Links
|
|
100
117
|
|
|
101
118
|
- 🏆 Leaderboard — **[whoburnedmore.com](https://whoburnedmore.com)**
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
11
12
|
import { createRequire as createRequire4 } from "node:module";
|
|
12
13
|
import { platform as platform3 } from "node:os";
|
|
@@ -95,12 +96,14 @@ async function readJson(res) {
|
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
|
-
async function post(path, body) {
|
|
99
|
+
async function post(path, body, token) {
|
|
100
|
+
const headers = { "Content-Type": "application/json" };
|
|
101
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
99
102
|
let res;
|
|
100
103
|
try {
|
|
101
104
|
res = await fetch(`${apiBase()}${path}`, {
|
|
102
105
|
method: "POST",
|
|
103
|
-
headers
|
|
106
|
+
headers,
|
|
104
107
|
body: JSON.stringify(body),
|
|
105
108
|
// Bound the request so a slow/black-holing/hostile server can't hang the CLI
|
|
106
109
|
// — or the unattended 15-minute background sync — indefinitely.
|
|
@@ -113,8 +116,21 @@ async function post(path, body) {
|
|
|
113
116
|
}
|
|
114
117
|
return { status: res.status, body: await readJson(res) };
|
|
115
118
|
}
|
|
119
|
+
async function verifyUsage(token, payload) {
|
|
120
|
+
const { status, body } = await post("/v1/verify", payload, token);
|
|
121
|
+
if (status === 401) throw new UnauthorizedError();
|
|
122
|
+
if (status !== 200) {
|
|
123
|
+
const err = body;
|
|
124
|
+
const details = err.details?.length ? `
|
|
125
|
+
- ${err.details.join("\n - ")}` : "";
|
|
126
|
+
throw new Error(`${err.error ?? "verification failed"}${details}`);
|
|
127
|
+
}
|
|
128
|
+
return body;
|
|
129
|
+
}
|
|
130
|
+
var ANON_RETIRED_MESSAGE = "Anonymous mode has been retired. Run `npx whoburnedmore` and sign in, or `npx whoburnedmore link --token=\u2026` for a server/CI, to put your usage on the leaderboard.";
|
|
116
131
|
async function anonSubmit(anonKey, payload) {
|
|
117
132
|
const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
|
|
133
|
+
if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
|
|
118
134
|
if (status !== 200) {
|
|
119
135
|
const err = body;
|
|
120
136
|
const details = err.details?.length ? `
|
|
@@ -123,22 +139,54 @@ async function anonSubmit(anonKey, payload) {
|
|
|
123
139
|
}
|
|
124
140
|
return body;
|
|
125
141
|
}
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
var UnauthorizedError = class extends Error {
|
|
143
|
+
constructor(message = "your sign-in has expired") {
|
|
144
|
+
super(message);
|
|
145
|
+
this.name = "UnauthorizedError";
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
async function deviceStart() {
|
|
149
|
+
const { status, body } = await post(
|
|
150
|
+
"/v1/auth/device",
|
|
151
|
+
{}
|
|
152
|
+
);
|
|
153
|
+
if (status !== 200) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
body.error ?? `sign-in failed (HTTP ${status})`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return body;
|
|
128
159
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
160
|
+
async function devicePoll(deviceCode) {
|
|
161
|
+
const { status, body } = await post(
|
|
162
|
+
"/v1/auth/device/token",
|
|
163
|
+
{ deviceCode }
|
|
164
|
+
);
|
|
165
|
+
if (status !== 200) {
|
|
166
|
+
throw new Error(`sign-in failed (HTTP ${status})`);
|
|
167
|
+
}
|
|
168
|
+
return body;
|
|
131
169
|
}
|
|
132
|
-
function
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
async function submit(token, payload) {
|
|
171
|
+
const { status, body } = await post("/v1/submit", payload, token);
|
|
172
|
+
if (status === 401) throw new UnauthorizedError();
|
|
173
|
+
if (status !== 200) {
|
|
174
|
+
const err = body;
|
|
175
|
+
const details = err.details?.length ? `
|
|
176
|
+
- ${err.details.join("\n - ")}` : "";
|
|
177
|
+
throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
|
|
178
|
+
}
|
|
179
|
+
return body;
|
|
180
|
+
}
|
|
181
|
+
function claimUrl(dashboardUrl, anonKey) {
|
|
182
|
+
return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
|
|
136
183
|
}
|
|
137
184
|
async function anonVisibility(anonKey, listed) {
|
|
138
185
|
const { status, body } = await post(
|
|
139
186
|
"/v1/anon/visibility",
|
|
140
187
|
{ anonKey, listed }
|
|
141
188
|
);
|
|
189
|
+
if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
|
|
142
190
|
if (status !== 200) {
|
|
143
191
|
throw new Error(body.error ?? `failed (HTTP ${status})`);
|
|
144
192
|
}
|
|
@@ -150,6 +198,7 @@ async function anonRemove(anonKey) {
|
|
|
150
198
|
body: JSON.stringify({ anonKey }),
|
|
151
199
|
signal: AbortSignal.timeout(3e4)
|
|
152
200
|
});
|
|
201
|
+
if (res.status === 410) throw new Error(ANON_RETIRED_MESSAGE);
|
|
153
202
|
if (res.status !== 200) {
|
|
154
203
|
const b = await res.json().catch(() => ({}));
|
|
155
204
|
throw new Error(b.error ?? `failed (HTTP ${res.status})`);
|
|
@@ -204,6 +253,8 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
204
253
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
205
254
|
const config = {};
|
|
206
255
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
256
|
+
if (typeof parsed.cliToken === "string") config.cliToken = parsed.cliToken;
|
|
257
|
+
if (typeof parsed.handle === "string") config.handle = parsed.handle;
|
|
207
258
|
if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
|
|
208
259
|
config.lastSyncAt = parsed.lastSyncAt;
|
|
209
260
|
if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
|
|
@@ -244,9 +295,15 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
|
|
|
244
295
|
const config = loadConfig(dir) ?? {};
|
|
245
296
|
saveConfig(dir, { ...config, lastSyncAt: when });
|
|
246
297
|
}
|
|
247
|
-
function
|
|
298
|
+
function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
|
|
248
299
|
const config = loadConfig(dir) ?? {};
|
|
249
|
-
saveConfig(dir, { ...config,
|
|
300
|
+
saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
|
|
301
|
+
}
|
|
302
|
+
function clearAuth(dir = defaultConfigDir()) {
|
|
303
|
+
const config = loadConfig(dir);
|
|
304
|
+
if (!config?.cliToken && !config?.handle) return;
|
|
305
|
+
const { cliToken: _t, handle: _h, ...rest } = config;
|
|
306
|
+
saveConfig(dir, rest);
|
|
250
307
|
}
|
|
251
308
|
|
|
252
309
|
// src/autosync.ts
|
|
@@ -665,41 +722,6 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
|
|
|
665
722
|
return false;
|
|
666
723
|
}
|
|
667
724
|
}
|
|
668
|
-
function notifyLaunchLive(opts) {
|
|
669
|
-
const os = opts?.platform ?? platform();
|
|
670
|
-
const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
|
|
671
|
-
const title = "whoburnedmore is live";
|
|
672
|
-
const message = "Your dashboard is ready. Go to whoburnedmore.com";
|
|
673
|
-
if (os === "darwin") {
|
|
674
|
-
return run2("osascript", [
|
|
675
|
-
"-e",
|
|
676
|
-
`display notification "${message}" with title "${title}"`
|
|
677
|
-
]).status === 0;
|
|
678
|
-
}
|
|
679
|
-
if (os === "linux") {
|
|
680
|
-
return run2("notify-send", [title, message]).status === 0;
|
|
681
|
-
}
|
|
682
|
-
if (os === "win32") {
|
|
683
|
-
const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
684
|
-
const script = [
|
|
685
|
-
"Add-Type -AssemblyName System.Windows.Forms",
|
|
686
|
-
"$n = New-Object System.Windows.Forms.NotifyIcon",
|
|
687
|
-
"$n.Icon = [System.Drawing.SystemIcons]::Application",
|
|
688
|
-
`$n.BalloonTipTitle = ${psQuote(title)}`,
|
|
689
|
-
`$n.BalloonTipText = ${psQuote(message)}`,
|
|
690
|
-
"$n.Visible = $true",
|
|
691
|
-
"$n.ShowBalloonTip(10000)",
|
|
692
|
-
"Start-Sleep -Seconds 2",
|
|
693
|
-
"$n.Dispose()"
|
|
694
|
-
].join("; ");
|
|
695
|
-
return run2("powershell", [
|
|
696
|
-
"-NoProfile",
|
|
697
|
-
"-Command",
|
|
698
|
-
script
|
|
699
|
-
]).status === 0;
|
|
700
|
-
}
|
|
701
|
-
return false;
|
|
702
|
-
}
|
|
703
725
|
function autoSyncLoaded() {
|
|
704
726
|
if (platform() === "darwin") {
|
|
705
727
|
const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
|
|
@@ -1332,6 +1354,8 @@ function parseClaudeLine(raw) {
|
|
|
1332
1354
|
if (message.role !== void 0 && message.role !== "assistant") return null;
|
|
1333
1355
|
const date = localDate(String(obj.timestamp ?? ""));
|
|
1334
1356
|
if (!date) return null;
|
|
1357
|
+
const tsParsed = Date.parse(String(obj.timestamp ?? ""));
|
|
1358
|
+
const ts = Number.isFinite(tsParsed) ? tsParsed : 0;
|
|
1335
1359
|
const messageId = typeof message.id === "string" ? message.id : "";
|
|
1336
1360
|
const requestId = typeof obj.requestId === "string" ? obj.requestId : "";
|
|
1337
1361
|
const hasRealId = messageId !== "" || requestId !== "";
|
|
@@ -1340,6 +1364,7 @@ function parseClaudeLine(raw) {
|
|
|
1340
1364
|
key,
|
|
1341
1365
|
hasRealId,
|
|
1342
1366
|
date,
|
|
1367
|
+
ts,
|
|
1343
1368
|
model: typeof message.model === "string" ? message.model : "unknown",
|
|
1344
1369
|
inputTokens: num3(usage.input_tokens),
|
|
1345
1370
|
outputTokens: num3(usage.output_tokens),
|
|
@@ -1458,6 +1483,28 @@ async function collectClaudeNative(env = process.env, opts = {}) {
|
|
|
1458
1483
|
}
|
|
1459
1484
|
return { entries: finalizeClaudeEntries(acc), found: true, filesScanned: scanned };
|
|
1460
1485
|
}
|
|
1486
|
+
async function collectClaudeRequests(env = process.env, opts = {}) {
|
|
1487
|
+
const dirs = resolveClaudeProjectDirs(env);
|
|
1488
|
+
const files = [];
|
|
1489
|
+
for (const dir of dirs) files.push(...await listJsonl(dir));
|
|
1490
|
+
if (files.length === 0) return { requests: [], found: false };
|
|
1491
|
+
const now = opts.now ?? Date.now;
|
|
1492
|
+
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
|
|
1493
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1494
|
+
for (const f of files) {
|
|
1495
|
+
if (now() > deadline) {
|
|
1496
|
+
return { requests: [...acc.values()], found: true, timedOut: true };
|
|
1497
|
+
}
|
|
1498
|
+
let content;
|
|
1499
|
+
try {
|
|
1500
|
+
content = await readFile(f, "utf8");
|
|
1501
|
+
} catch {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
accumulateClaudeLines(acc, splitLines(content));
|
|
1505
|
+
}
|
|
1506
|
+
return { requests: [...acc.values()], found: true };
|
|
1507
|
+
}
|
|
1461
1508
|
|
|
1462
1509
|
// src/native/codex.ts
|
|
1463
1510
|
import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
@@ -1491,9 +1538,8 @@ function readTokenFields(payload) {
|
|
|
1491
1538
|
}
|
|
1492
1539
|
function parseCodexRollout(lines) {
|
|
1493
1540
|
let model = "unknown";
|
|
1494
|
-
|
|
1495
|
-
let
|
|
1496
|
-
let turnCount = 0;
|
|
1541
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
1542
|
+
let lastSeenDate = null;
|
|
1497
1543
|
for (const raw of lines) {
|
|
1498
1544
|
const trimmed = raw.trim();
|
|
1499
1545
|
if (!trimmed) continue;
|
|
@@ -1512,49 +1558,68 @@ function parseCodexRollout(lines) {
|
|
|
1512
1558
|
if (payload.type === "token_count") {
|
|
1513
1559
|
const fields = readTokenFields(payload);
|
|
1514
1560
|
if (!fields) continue;
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1561
|
+
const parsed = localDate2(String(obj.timestamp ?? ""));
|
|
1562
|
+
const day = parsed ?? lastSeenDate;
|
|
1563
|
+
if (!day) continue;
|
|
1564
|
+
lastSeenDate = day;
|
|
1565
|
+
const e = perDay.get(day);
|
|
1566
|
+
if (e) {
|
|
1567
|
+
e.cum = fields;
|
|
1568
|
+
e.turns += 1;
|
|
1569
|
+
} else {
|
|
1570
|
+
perDay.set(day, { cum: fields, turns: 1 });
|
|
1571
|
+
}
|
|
1519
1572
|
}
|
|
1520
1573
|
}
|
|
1521
|
-
if (
|
|
1522
|
-
const
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
cacheReadTokens
|
|
1533
|
-
|
|
1534
|
-
|
|
1574
|
+
if (perDay.size === 0) return [];
|
|
1575
|
+
const dates = [...perDay.keys()].sort();
|
|
1576
|
+
const out = [];
|
|
1577
|
+
let prev = { input: 0, cached: 0, output: 0, reasoning: 0 };
|
|
1578
|
+
for (const date of dates) {
|
|
1579
|
+
const { cum, turns } = perDay.get(date);
|
|
1580
|
+
const dInput = Math.max(0, cum.input - prev.input);
|
|
1581
|
+
const dCached = Math.max(0, cum.cached - prev.cached);
|
|
1582
|
+
const dOutput = Math.max(0, cum.output - prev.output);
|
|
1583
|
+
const dReasoning = Math.max(0, cum.reasoning - prev.reasoning);
|
|
1584
|
+
prev = cum;
|
|
1585
|
+
const cacheReadTokens = dCached;
|
|
1586
|
+
const inputTokens = Math.max(0, dInput - dCached);
|
|
1587
|
+
const outputTokens = dOutput + dReasoning;
|
|
1588
|
+
if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
|
|
1589
|
+
out.push({
|
|
1590
|
+
date,
|
|
1591
|
+
model,
|
|
1592
|
+
inputTokens,
|
|
1593
|
+
outputTokens,
|
|
1594
|
+
cacheCreationTokens: 0,
|
|
1595
|
+
cacheReadTokens,
|
|
1596
|
+
turnCount: turns
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
return out;
|
|
1535
1600
|
}
|
|
1536
1601
|
function accumulateCodexSession(acc, lines) {
|
|
1537
|
-
const s
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1602
|
+
for (const s of parseCodexRollout(lines)) {
|
|
1603
|
+
const k = `${s.date}|${s.model}`;
|
|
1604
|
+
let b = acc.get(k);
|
|
1605
|
+
if (!b) {
|
|
1606
|
+
b = {
|
|
1607
|
+
date: s.date,
|
|
1608
|
+
model: s.model,
|
|
1609
|
+
inputTokens: 0,
|
|
1610
|
+
outputTokens: 0,
|
|
1611
|
+
cacheCreationTokens: 0,
|
|
1612
|
+
cacheReadTokens: 0,
|
|
1613
|
+
requestCount: 0
|
|
1614
|
+
};
|
|
1615
|
+
acc.set(k, b);
|
|
1616
|
+
}
|
|
1617
|
+
b.inputTokens += s.inputTokens;
|
|
1618
|
+
b.outputTokens += s.outputTokens;
|
|
1619
|
+
b.cacheCreationTokens += s.cacheCreationTokens;
|
|
1620
|
+
b.cacheReadTokens += s.cacheReadTokens;
|
|
1621
|
+
b.requestCount += s.turnCount;
|
|
1552
1622
|
}
|
|
1553
|
-
b.inputTokens += s.inputTokens;
|
|
1554
|
-
b.outputTokens += s.outputTokens;
|
|
1555
|
-
b.cacheCreationTokens += s.cacheCreationTokens;
|
|
1556
|
-
b.cacheReadTokens += s.cacheReadTokens;
|
|
1557
|
-
b.requestCount += s.turnCount;
|
|
1558
1623
|
}
|
|
1559
1624
|
function finalizeCodexEntries(acc) {
|
|
1560
1625
|
const entries = [];
|
|
@@ -6241,6 +6306,32 @@ var AnonSubmitPayload = SubmitPayload.extend({
|
|
|
6241
6306
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
6242
6307
|
anonKey: external_exports.string().min(16).max(128)
|
|
6243
6308
|
});
|
|
6309
|
+
var VerifyRequestRecord = external_exports.object({
|
|
6310
|
+
/** Local calendar date (YYYY-MM-DD) this request is bucketed under. */
|
|
6311
|
+
date: DateString,
|
|
6312
|
+
/** Epoch milliseconds of the request's final (max-token) transcript line. */
|
|
6313
|
+
ts: external_exports.number().int().nonnegative(),
|
|
6314
|
+
tool: external_exports.string().min(1).max(64),
|
|
6315
|
+
model: external_exports.string().min(1).max(128),
|
|
6316
|
+
inputTokens: tokenCount,
|
|
6317
|
+
outputTokens: tokenCount,
|
|
6318
|
+
cacheCreationTokens: tokenCount,
|
|
6319
|
+
cacheReadTokens: tokenCount,
|
|
6320
|
+
/** sha256(msgId|reqId), truncated — preserves uniqueness without revealing the id. */
|
|
6321
|
+
reqHash: external_exports.string().min(1).max(64)
|
|
6322
|
+
});
|
|
6323
|
+
var VerifyPayload = external_exports.object({
|
|
6324
|
+
cliVersion: external_exports.string().min(1).max(32),
|
|
6325
|
+
/** The per-request skeleton (bounded/sampled — see `truncated`). */
|
|
6326
|
+
requests: external_exports.array(VerifyRequestRecord).min(1).max(1e5),
|
|
6327
|
+
/**
|
|
6328
|
+
* True when the local corpus exceeded the client upload cap and was sampled to
|
|
6329
|
+
* the most recent N requests. The server treats a truncated upload conservatively
|
|
6330
|
+
* (it never AUTO-passes a truncated one — it can still auto-FAIL on a physical
|
|
6331
|
+
* impossibility, or route to a human).
|
|
6332
|
+
*/
|
|
6333
|
+
truncated: external_exports.boolean().optional()
|
|
6334
|
+
});
|
|
6244
6335
|
function entryTotalTokens(e) {
|
|
6245
6336
|
return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
|
|
6246
6337
|
}
|
|
@@ -6359,20 +6450,24 @@ function formatUSD(n) {
|
|
|
6359
6450
|
function sanitizeServerText(s) {
|
|
6360
6451
|
return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
6361
6452
|
}
|
|
6362
|
-
function
|
|
6453
|
+
function signedInNextStepLines(result) {
|
|
6454
|
+
if (result.orgBoardUrl) {
|
|
6455
|
+
return [
|
|
6456
|
+
` \u{1F3E2} You're on your team board: ${sanitizeServerText(result.orgBoardUrl)}`,
|
|
6457
|
+
" \u2192 Open it to see who burned more."
|
|
6458
|
+
];
|
|
6459
|
+
}
|
|
6363
6460
|
if (result.boardUrl) {
|
|
6364
|
-
const code = result.
|
|
6461
|
+
const code = result.boardUrl.split("/").filter(Boolean).pop() ?? "";
|
|
6365
6462
|
return [
|
|
6366
6463
|
` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
|
|
6367
6464
|
" \u2192 Open it to see who burned more.",
|
|
6368
|
-
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}
|
|
6369
|
-
" \u2192 Sign in on the page and add your X to claim your spot and own your rank."
|
|
6465
|
+
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`
|
|
6370
6466
|
];
|
|
6371
6467
|
}
|
|
6372
6468
|
return [
|
|
6373
|
-
` Your dashboard: ${sanitizeServerText(result.
|
|
6374
|
-
" \u2192
|
|
6375
|
-
" Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
|
|
6469
|
+
` Your dashboard: ${sanitizeServerText(result.profileUrl)}`,
|
|
6470
|
+
" \u2192 Open it to see your rank and share your profile."
|
|
6376
6471
|
];
|
|
6377
6472
|
}
|
|
6378
6473
|
|
|
@@ -6752,56 +6847,139 @@ async function run(flags) {
|
|
|
6752
6847
|
console.log(pc2.dim(" --no-submit: skipped the dashboard."));
|
|
6753
6848
|
return;
|
|
6754
6849
|
}
|
|
6755
|
-
const
|
|
6756
|
-
const
|
|
6850
|
+
const cfg = loadConfig();
|
|
6851
|
+
const canSignIn = !flags.quiet && Boolean(process.stdout.isTTY);
|
|
6852
|
+
if (cfg?.cliToken) {
|
|
6853
|
+
await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
|
|
6854
|
+
} else if (canSignIn) {
|
|
6855
|
+
const auth = await ensureSignedIn();
|
|
6856
|
+
if (!auth) return;
|
|
6857
|
+
await submitSignedIn(auth.token, payload, flags, canSignIn);
|
|
6858
|
+
} else if (!flags.quiet) {
|
|
6859
|
+
console.log(pc2.yellow(" Sign in to put your usage on the leaderboard."));
|
|
6860
|
+
console.log(
|
|
6861
|
+
pc2.dim(
|
|
6862
|
+
" Run `npx whoburnedmore` in an interactive terminal to sign in, or `npx whoburnedmore link --token=\u2026` (from your signed-in profile) for servers/CI."
|
|
6863
|
+
)
|
|
6864
|
+
);
|
|
6865
|
+
} else {
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
function sleep(ms) {
|
|
6870
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6871
|
+
}
|
|
6872
|
+
async function ensureSignedIn() {
|
|
6873
|
+
const cfg = loadConfig();
|
|
6874
|
+
if (cfg?.cliToken) return { token: cfg.cliToken, handle: cfg.handle ?? "" };
|
|
6875
|
+
let dev;
|
|
6757
6876
|
try {
|
|
6758
|
-
|
|
6759
|
-
} catch {
|
|
6877
|
+
dev = await deviceStart();
|
|
6878
|
+
} catch (err) {
|
|
6879
|
+
console.log(pc2.yellow(` Couldn't start sign-in: ${err.message}`));
|
|
6880
|
+
return null;
|
|
6760
6881
|
}
|
|
6882
|
+
console.log();
|
|
6883
|
+
console.log(pc2.bold(" Sign in to whoburnedmore to put your usage on the board."));
|
|
6884
|
+
console.log(` 1. Opening ${pc2.cyan(sanitizeServerText(dev.verifyUrl))} \u2014 or go there yourself.`);
|
|
6885
|
+
console.log(` 2. Approve this code: ${pc2.bold(sanitizeServerText(dev.userCode))}`);
|
|
6886
|
+
if (isTrustedWebUrl(dev.verifyUrl)) openBrowser(dev.verifyUrl);
|
|
6887
|
+
console.log(pc2.dim(" Waiting for you to approve in the browser\u2026"));
|
|
6888
|
+
const deadline = Date.now() + dev.expiresInSeconds * 1e3;
|
|
6889
|
+
const intervalMs = Math.max(1, dev.pollIntervalSeconds) * 1e3;
|
|
6890
|
+
while (Date.now() < deadline) {
|
|
6891
|
+
await sleep(intervalMs);
|
|
6892
|
+
let res;
|
|
6893
|
+
try {
|
|
6894
|
+
res = await devicePoll(dev.deviceCode);
|
|
6895
|
+
} catch {
|
|
6896
|
+
continue;
|
|
6897
|
+
}
|
|
6898
|
+
if (res.status === "ok") {
|
|
6899
|
+
saveAuth(void 0, { cliToken: res.token, handle: res.handle });
|
|
6900
|
+
console.log(pc2.green(` \u2713 Signed in as @${sanitizeServerText(res.handle)}.`));
|
|
6901
|
+
return { token: res.token, handle: res.handle };
|
|
6902
|
+
}
|
|
6903
|
+
if (res.status === "expired") break;
|
|
6904
|
+
}
|
|
6905
|
+
console.log(
|
|
6906
|
+
pc2.yellow(" Sign-in timed out. Run `npx whoburnedmore` again to retry.")
|
|
6907
|
+
);
|
|
6908
|
+
return null;
|
|
6909
|
+
}
|
|
6910
|
+
async function submitSignedIn(token, payload, flags, interactive) {
|
|
6911
|
+
let result;
|
|
6761
6912
|
try {
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6913
|
+
result = await submit(token, payload);
|
|
6914
|
+
} catch (err) {
|
|
6915
|
+
if (err instanceof UnauthorizedError) {
|
|
6916
|
+
clearAuth();
|
|
6917
|
+
if (!interactive) return;
|
|
6918
|
+
const auth = await ensureSignedIn();
|
|
6919
|
+
if (!auth) return;
|
|
6920
|
+
result = await submit(auth.token, payload);
|
|
6921
|
+
} else {
|
|
6922
|
+
throw err;
|
|
6767
6923
|
}
|
|
6924
|
+
}
|
|
6925
|
+
try {
|
|
6926
|
+
recordSync();
|
|
6768
6927
|
} catch {
|
|
6769
6928
|
}
|
|
6770
|
-
const
|
|
6771
|
-
const trusted = isTrustedWebUrl(baseUrl);
|
|
6929
|
+
const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.profileUrl;
|
|
6772
6930
|
if (!flags.quiet) {
|
|
6773
6931
|
console.log(
|
|
6774
6932
|
pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
|
|
6775
6933
|
);
|
|
6776
|
-
if (
|
|
6934
|
+
if (result.suppressed) {
|
|
6935
|
+
console.log();
|
|
6936
|
+
console.log(
|
|
6937
|
+
pc2.yellow(
|
|
6938
|
+
" \u26A0 You're currently OFF the leaderboard \u2014 we couldn't verify these numbers."
|
|
6939
|
+
)
|
|
6940
|
+
);
|
|
6941
|
+
console.log(
|
|
6942
|
+
pc2.dim(
|
|
6943
|
+
" Verify your usage to get back on \u2014 this sends a detailed per-request breakdown (timestamps + token counts, never your prompts or code)."
|
|
6944
|
+
)
|
|
6945
|
+
);
|
|
6946
|
+
if (process.stdin.isTTY && await confirm(" Verify now?")) {
|
|
6947
|
+
await runVerify();
|
|
6948
|
+
afterSubmitChores(flags);
|
|
6949
|
+
return;
|
|
6950
|
+
}
|
|
6951
|
+
console.log(
|
|
6952
|
+
pc2.dim(
|
|
6953
|
+
" Run `npx whoburnedmore verify` anytime, or appeal at whoburnedmore.com/appeal."
|
|
6954
|
+
)
|
|
6955
|
+
);
|
|
6956
|
+
}
|
|
6957
|
+
if (isTrustedWebUrl(baseUrl)) {
|
|
6777
6958
|
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
6778
|
-
openBrowser(
|
|
6959
|
+
openBrowser(baseUrl);
|
|
6779
6960
|
} else {
|
|
6780
6961
|
console.log(
|
|
6781
6962
|
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
6782
6963
|
);
|
|
6783
6964
|
console.log(` ${sanitizeServerText(baseUrl)}`);
|
|
6784
6965
|
}
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
if (line.includes("\u2192")) console.log(pc2.bold(line));
|
|
6789
|
-
else if (line.startsWith(" Private until you do")) {
|
|
6790
|
-
if (!flags.quiet) console.log(pc2.dim(line));
|
|
6791
|
-
} else console.log(line);
|
|
6792
|
-
}
|
|
6793
|
-
if (!flags.quiet) {
|
|
6794
|
-
try {
|
|
6795
|
-
reconcileAutoSync();
|
|
6796
|
-
} catch {
|
|
6966
|
+
for (const line of signedInNextStepLines(result)) {
|
|
6967
|
+
if (line.includes("\u2192")) console.log(pc2.bold(line));
|
|
6968
|
+
else console.log(line);
|
|
6797
6969
|
}
|
|
6798
6970
|
}
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6971
|
+
afterSubmitChores(flags);
|
|
6972
|
+
}
|
|
6973
|
+
function afterSubmitChores(flags) {
|
|
6974
|
+
if (flags.quiet) return;
|
|
6975
|
+
try {
|
|
6976
|
+
reconcileAutoSync();
|
|
6977
|
+
} catch {
|
|
6804
6978
|
}
|
|
6979
|
+
console.log();
|
|
6980
|
+
console.log(
|
|
6981
|
+
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
|
|
6982
|
+
);
|
|
6805
6983
|
}
|
|
6806
6984
|
async function linkServerInstall(token) {
|
|
6807
6985
|
if (!token) {
|
|
@@ -6809,6 +6987,9 @@ async function linkServerInstall(token) {
|
|
|
6809
6987
|
}
|
|
6810
6988
|
const anonKey = ensureAnonKey();
|
|
6811
6989
|
const linked = await redeemServerInstall(token, anonKey);
|
|
6990
|
+
if (linked.cliToken) {
|
|
6991
|
+
saveAuth(void 0, { cliToken: linked.cliToken, handle: linked.handle });
|
|
6992
|
+
}
|
|
6812
6993
|
const handle = sanitizeServerText(linked.handle);
|
|
6813
6994
|
console.log(
|
|
6814
6995
|
linked.alreadyLinked ? ` This machine is already linked to @${handle}.` : ` Linked this machine to @${handle}.`
|
|
@@ -6883,6 +7064,102 @@ async function runDaemon() {
|
|
|
6883
7064
|
console.log();
|
|
6884
7065
|
console.log(pc2.dim(` Daemon stopped after ${cycles} sync cycle${cycles === 1 ? "" : "s"}.`));
|
|
6885
7066
|
}
|
|
7067
|
+
async function runVerify() {
|
|
7068
|
+
printBanner();
|
|
7069
|
+
console.log();
|
|
7070
|
+
console.log(pc2.bold(" Verify your usage to get back on the leaderboard."));
|
|
7071
|
+
console.log(
|
|
7072
|
+
pc2.dim(
|
|
7073
|
+
" This sends a DETAILED per-request breakdown \u2014 timestamps and token counts, still never your prompts, code, or file names \u2014 so we can confirm your usage is real. It's deleted after review."
|
|
7074
|
+
)
|
|
7075
|
+
);
|
|
7076
|
+
const cfg = loadConfig();
|
|
7077
|
+
let token = cfg?.cliToken;
|
|
7078
|
+
if (!token) {
|
|
7079
|
+
if (!process.stdout.isTTY) {
|
|
7080
|
+
console.log(
|
|
7081
|
+
pc2.yellow(
|
|
7082
|
+
" Sign in first \u2014 run `npx whoburnedmore verify` in an interactive terminal."
|
|
7083
|
+
)
|
|
7084
|
+
);
|
|
7085
|
+
return;
|
|
7086
|
+
}
|
|
7087
|
+
const auth = await ensureSignedIn();
|
|
7088
|
+
if (!auth) return;
|
|
7089
|
+
token = auth.token;
|
|
7090
|
+
}
|
|
7091
|
+
if (process.stdin.isTTY) {
|
|
7092
|
+
const ok = await confirm("\n Send this breakdown to verify your usage?");
|
|
7093
|
+
if (!ok) {
|
|
7094
|
+
console.log(pc2.dim(" Cancelled \u2014 nothing was sent."));
|
|
7095
|
+
return;
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
console.log(pc2.dim(" Reading your local Claude Code logs\u2026"));
|
|
7099
|
+
const { requests, found } = await collectClaudeRequests();
|
|
7100
|
+
if (!found || requests.length === 0) {
|
|
7101
|
+
console.log(
|
|
7102
|
+
pc2.yellow(" No local Claude Code logs found on this machine to verify.")
|
|
7103
|
+
);
|
|
7104
|
+
console.log(
|
|
7105
|
+
pc2.dim(
|
|
7106
|
+
" Run this on the machine where you actually use Claude Code, or file a written appeal at whoburnedmore.com/appeal."
|
|
7107
|
+
)
|
|
7108
|
+
);
|
|
7109
|
+
return;
|
|
7110
|
+
}
|
|
7111
|
+
const CAP = 5e4;
|
|
7112
|
+
const sorted = requests.slice().sort((a, b) => b.ts - a.ts);
|
|
7113
|
+
const truncated = sorted.length > CAP;
|
|
7114
|
+
const capped = truncated ? sorted.slice(0, CAP) : sorted;
|
|
7115
|
+
const records = capped.map((r) => ({
|
|
7116
|
+
date: r.date,
|
|
7117
|
+
ts: r.ts,
|
|
7118
|
+
tool: "claude",
|
|
7119
|
+
model: r.model,
|
|
7120
|
+
inputTokens: r.inputTokens,
|
|
7121
|
+
outputTokens: r.outputTokens,
|
|
7122
|
+
cacheCreationTokens: r.cacheCreationTokens,
|
|
7123
|
+
cacheReadTokens: r.cacheReadTokens,
|
|
7124
|
+
reqHash: createHash("sha256").update(r.key).digest("hex").slice(0, 32)
|
|
7125
|
+
}));
|
|
7126
|
+
console.log(
|
|
7127
|
+
pc2.dim(
|
|
7128
|
+
` Sending ${records.length} request records for analysis${truncated ? " (most recent, sampled)" : ""}\u2026`
|
|
7129
|
+
)
|
|
7130
|
+
);
|
|
7131
|
+
let result;
|
|
7132
|
+
try {
|
|
7133
|
+
result = await verifyUsage(token, {
|
|
7134
|
+
cliVersion: VERSION,
|
|
7135
|
+
requests: records,
|
|
7136
|
+
...truncated ? { truncated: true } : {}
|
|
7137
|
+
});
|
|
7138
|
+
} catch (err) {
|
|
7139
|
+
if (err instanceof UnauthorizedError) {
|
|
7140
|
+
clearAuth();
|
|
7141
|
+
console.log(
|
|
7142
|
+
pc2.yellow(
|
|
7143
|
+
" Your sign-in expired \u2014 run `npx whoburnedmore verify` again to retry."
|
|
7144
|
+
)
|
|
7145
|
+
);
|
|
7146
|
+
return;
|
|
7147
|
+
}
|
|
7148
|
+
throw err;
|
|
7149
|
+
}
|
|
7150
|
+
console.log();
|
|
7151
|
+
const line = ` ${sanitizeServerText(result.message)}`;
|
|
7152
|
+
if (result.relisted) {
|
|
7153
|
+
console.log(pc2.green(` \u2713${line.slice(1)}`));
|
|
7154
|
+
} else {
|
|
7155
|
+
console.log(result.verdict === "fail" ? pc2.yellow(line) : line);
|
|
7156
|
+
console.log(
|
|
7157
|
+
pc2.dim(
|
|
7158
|
+
" We'll relist you automatically if it checks out \u2014 re-run `npx whoburnedmore` anytime to check."
|
|
7159
|
+
)
|
|
7160
|
+
);
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
6886
7163
|
async function main() {
|
|
6887
7164
|
const major = Number(process.versions.node.split(".")[0]);
|
|
6888
7165
|
if (major < 20) {
|
|
@@ -6918,6 +7195,9 @@ async function main() {
|
|
|
6918
7195
|
case "daemon":
|
|
6919
7196
|
await runDaemon();
|
|
6920
7197
|
break;
|
|
7198
|
+
case "verify":
|
|
7199
|
+
await runVerify();
|
|
7200
|
+
break;
|
|
6921
7201
|
case "status":
|
|
6922
7202
|
case "doctor": {
|
|
6923
7203
|
for (const line of agentStatusReport()) console.log(line);
|
|
@@ -6969,8 +7249,8 @@ function printHelp() {
|
|
|
6969
7249
|
${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
|
|
6970
7250
|
|
|
6971
7251
|
${pc2.bold("usage")}
|
|
6972
|
-
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
6973
|
-
npx whoburnedmore --board=CODE compare with friends \u2014 join their board
|
|
7252
|
+
npx whoburnedmore sign in, burn + land on the public leaderboard, open your dashboard
|
|
7253
|
+
npx whoburnedmore --board=CODE compare with friends \u2014 sign in and join their board
|
|
6974
7254
|
npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
|
|
6975
7255
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
6976
7256
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
@@ -6980,14 +7260,15 @@ function printHelp() {
|
|
|
6980
7260
|
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
6981
7261
|
npx whoburnedmore public put it back on the leaderboard
|
|
6982
7262
|
npx whoburnedmore remove delete your dashboard and its data
|
|
7263
|
+
npx whoburnedmore verify delisted? re-verify your usage to get back on (sends a detailed breakdown)
|
|
6983
7264
|
npx whoburnedmore status check background-sync health (last sync, staleness)
|
|
6984
7265
|
npx whoburnedmore uninstall-sync turn off the background sync
|
|
6985
7266
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
6986
7267
|
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
7268
|
+
Your first run signs you in (we open a page, you approve a short code) and binds
|
|
7269
|
+
this machine to your account \u2014 your usage lands on the leaderboard under your
|
|
7270
|
+
handle. Background sync is on by default: your page then refreshes automatically
|
|
7271
|
+
every 15 min (\`uninstall-sync\` to stop); run \`private\`/\`remove\` to pull it. Only
|
|
6991
7272
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
6992
7273
|
your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
|
|
6993
7274
|
your machine at all.
|