whoburnedmore 0.9.4 → 0.9.5
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/dist/index.js +444 -67
- package/package.json +1 -1
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,6 +116,17 @@ 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
|
+
}
|
|
116
130
|
async function anonSubmit(anonKey, payload) {
|
|
117
131
|
const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
|
|
118
132
|
if (status !== 200) {
|
|
@@ -123,6 +137,45 @@ async function anonSubmit(anonKey, payload) {
|
|
|
123
137
|
}
|
|
124
138
|
return body;
|
|
125
139
|
}
|
|
140
|
+
var UnauthorizedError = class extends Error {
|
|
141
|
+
constructor(message = "your sign-in has expired") {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "UnauthorizedError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
async function deviceStart() {
|
|
147
|
+
const { status, body } = await post(
|
|
148
|
+
"/v1/auth/device",
|
|
149
|
+
{}
|
|
150
|
+
);
|
|
151
|
+
if (status !== 200) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
body.error ?? `sign-in failed (HTTP ${status})`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return body;
|
|
157
|
+
}
|
|
158
|
+
async function devicePoll(deviceCode) {
|
|
159
|
+
const { status, body } = await post(
|
|
160
|
+
"/v1/auth/device/token",
|
|
161
|
+
{ deviceCode }
|
|
162
|
+
);
|
|
163
|
+
if (status !== 200) {
|
|
164
|
+
throw new Error(`sign-in failed (HTTP ${status})`);
|
|
165
|
+
}
|
|
166
|
+
return body;
|
|
167
|
+
}
|
|
168
|
+
async function submit(token, payload) {
|
|
169
|
+
const { status, body } = await post("/v1/submit", payload, token);
|
|
170
|
+
if (status === 401) throw new UnauthorizedError();
|
|
171
|
+
if (status !== 200) {
|
|
172
|
+
const err = body;
|
|
173
|
+
const details = err.details?.length ? `
|
|
174
|
+
- ${err.details.join("\n - ")}` : "";
|
|
175
|
+
throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
|
|
176
|
+
}
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
126
179
|
function claimUrl(dashboardUrl, anonKey) {
|
|
127
180
|
return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
|
|
128
181
|
}
|
|
@@ -204,6 +257,8 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
204
257
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
205
258
|
const config = {};
|
|
206
259
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
260
|
+
if (typeof parsed.cliToken === "string") config.cliToken = parsed.cliToken;
|
|
261
|
+
if (typeof parsed.handle === "string") config.handle = parsed.handle;
|
|
207
262
|
if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
|
|
208
263
|
config.lastSyncAt = parsed.lastSyncAt;
|
|
209
264
|
if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
|
|
@@ -248,6 +303,16 @@ function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date
|
|
|
248
303
|
const config = loadConfig(dir) ?? {};
|
|
249
304
|
saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
|
|
250
305
|
}
|
|
306
|
+
function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
|
|
307
|
+
const config = loadConfig(dir) ?? {};
|
|
308
|
+
saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
|
|
309
|
+
}
|
|
310
|
+
function clearAuth(dir = defaultConfigDir()) {
|
|
311
|
+
const config = loadConfig(dir);
|
|
312
|
+
if (!config?.cliToken && !config?.handle) return;
|
|
313
|
+
const { cliToken: _t, handle: _h, ...rest } = config;
|
|
314
|
+
saveConfig(dir, rest);
|
|
315
|
+
}
|
|
251
316
|
|
|
252
317
|
// src/autosync.ts
|
|
253
318
|
var SYNC_INTERVAL_MINUTES = 15;
|
|
@@ -1332,6 +1397,8 @@ function parseClaudeLine(raw) {
|
|
|
1332
1397
|
if (message.role !== void 0 && message.role !== "assistant") return null;
|
|
1333
1398
|
const date = localDate(String(obj.timestamp ?? ""));
|
|
1334
1399
|
if (!date) return null;
|
|
1400
|
+
const tsParsed = Date.parse(String(obj.timestamp ?? ""));
|
|
1401
|
+
const ts = Number.isFinite(tsParsed) ? tsParsed : 0;
|
|
1335
1402
|
const messageId = typeof message.id === "string" ? message.id : "";
|
|
1336
1403
|
const requestId = typeof obj.requestId === "string" ? obj.requestId : "";
|
|
1337
1404
|
const hasRealId = messageId !== "" || requestId !== "";
|
|
@@ -1340,6 +1407,7 @@ function parseClaudeLine(raw) {
|
|
|
1340
1407
|
key,
|
|
1341
1408
|
hasRealId,
|
|
1342
1409
|
date,
|
|
1410
|
+
ts,
|
|
1343
1411
|
model: typeof message.model === "string" ? message.model : "unknown",
|
|
1344
1412
|
inputTokens: num3(usage.input_tokens),
|
|
1345
1413
|
outputTokens: num3(usage.output_tokens),
|
|
@@ -1458,6 +1526,28 @@ async function collectClaudeNative(env = process.env, opts = {}) {
|
|
|
1458
1526
|
}
|
|
1459
1527
|
return { entries: finalizeClaudeEntries(acc), found: true, filesScanned: scanned };
|
|
1460
1528
|
}
|
|
1529
|
+
async function collectClaudeRequests(env = process.env, opts = {}) {
|
|
1530
|
+
const dirs = resolveClaudeProjectDirs(env);
|
|
1531
|
+
const files = [];
|
|
1532
|
+
for (const dir of dirs) files.push(...await listJsonl(dir));
|
|
1533
|
+
if (files.length === 0) return { requests: [], found: false };
|
|
1534
|
+
const now = opts.now ?? Date.now;
|
|
1535
|
+
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
|
|
1536
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1537
|
+
for (const f of files) {
|
|
1538
|
+
if (now() > deadline) {
|
|
1539
|
+
return { requests: [...acc.values()], found: true, timedOut: true };
|
|
1540
|
+
}
|
|
1541
|
+
let content;
|
|
1542
|
+
try {
|
|
1543
|
+
content = await readFile(f, "utf8");
|
|
1544
|
+
} catch {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
accumulateClaudeLines(acc, splitLines(content));
|
|
1548
|
+
}
|
|
1549
|
+
return { requests: [...acc.values()], found: true };
|
|
1550
|
+
}
|
|
1461
1551
|
|
|
1462
1552
|
// src/native/codex.ts
|
|
1463
1553
|
import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
@@ -1491,9 +1581,8 @@ function readTokenFields(payload) {
|
|
|
1491
1581
|
}
|
|
1492
1582
|
function parseCodexRollout(lines) {
|
|
1493
1583
|
let model = "unknown";
|
|
1494
|
-
|
|
1495
|
-
let
|
|
1496
|
-
let turnCount = 0;
|
|
1584
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
1585
|
+
let lastSeenDate = null;
|
|
1497
1586
|
for (const raw of lines) {
|
|
1498
1587
|
const trimmed = raw.trim();
|
|
1499
1588
|
if (!trimmed) continue;
|
|
@@ -1512,49 +1601,68 @@ function parseCodexRollout(lines) {
|
|
|
1512
1601
|
if (payload.type === "token_count") {
|
|
1513
1602
|
const fields = readTokenFields(payload);
|
|
1514
1603
|
if (!fields) continue;
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1604
|
+
const parsed = localDate2(String(obj.timestamp ?? ""));
|
|
1605
|
+
const day = parsed ?? lastSeenDate;
|
|
1606
|
+
if (!day) continue;
|
|
1607
|
+
lastSeenDate = day;
|
|
1608
|
+
const e = perDay.get(day);
|
|
1609
|
+
if (e) {
|
|
1610
|
+
e.cum = fields;
|
|
1611
|
+
e.turns += 1;
|
|
1612
|
+
} else {
|
|
1613
|
+
perDay.set(day, { cum: fields, turns: 1 });
|
|
1614
|
+
}
|
|
1519
1615
|
}
|
|
1520
1616
|
}
|
|
1521
|
-
if (
|
|
1522
|
-
const
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
cacheReadTokens
|
|
1533
|
-
|
|
1534
|
-
|
|
1617
|
+
if (perDay.size === 0) return [];
|
|
1618
|
+
const dates = [...perDay.keys()].sort();
|
|
1619
|
+
const out = [];
|
|
1620
|
+
let prev = { input: 0, cached: 0, output: 0, reasoning: 0 };
|
|
1621
|
+
for (const date of dates) {
|
|
1622
|
+
const { cum, turns } = perDay.get(date);
|
|
1623
|
+
const dInput = Math.max(0, cum.input - prev.input);
|
|
1624
|
+
const dCached = Math.max(0, cum.cached - prev.cached);
|
|
1625
|
+
const dOutput = Math.max(0, cum.output - prev.output);
|
|
1626
|
+
const dReasoning = Math.max(0, cum.reasoning - prev.reasoning);
|
|
1627
|
+
prev = cum;
|
|
1628
|
+
const cacheReadTokens = dCached;
|
|
1629
|
+
const inputTokens = Math.max(0, dInput - dCached);
|
|
1630
|
+
const outputTokens = dOutput + dReasoning;
|
|
1631
|
+
if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
|
|
1632
|
+
out.push({
|
|
1633
|
+
date,
|
|
1634
|
+
model,
|
|
1635
|
+
inputTokens,
|
|
1636
|
+
outputTokens,
|
|
1637
|
+
cacheCreationTokens: 0,
|
|
1638
|
+
cacheReadTokens,
|
|
1639
|
+
turnCount: turns
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
return out;
|
|
1535
1643
|
}
|
|
1536
1644
|
function accumulateCodexSession(acc, lines) {
|
|
1537
|
-
const s
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1645
|
+
for (const s of parseCodexRollout(lines)) {
|
|
1646
|
+
const k = `${s.date}|${s.model}`;
|
|
1647
|
+
let b = acc.get(k);
|
|
1648
|
+
if (!b) {
|
|
1649
|
+
b = {
|
|
1650
|
+
date: s.date,
|
|
1651
|
+
model: s.model,
|
|
1652
|
+
inputTokens: 0,
|
|
1653
|
+
outputTokens: 0,
|
|
1654
|
+
cacheCreationTokens: 0,
|
|
1655
|
+
cacheReadTokens: 0,
|
|
1656
|
+
requestCount: 0
|
|
1657
|
+
};
|
|
1658
|
+
acc.set(k, b);
|
|
1659
|
+
}
|
|
1660
|
+
b.inputTokens += s.inputTokens;
|
|
1661
|
+
b.outputTokens += s.outputTokens;
|
|
1662
|
+
b.cacheCreationTokens += s.cacheCreationTokens;
|
|
1663
|
+
b.cacheReadTokens += s.cacheReadTokens;
|
|
1664
|
+
b.requestCount += s.turnCount;
|
|
1552
1665
|
}
|
|
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
1666
|
}
|
|
1559
1667
|
function finalizeCodexEntries(acc) {
|
|
1560
1668
|
const entries = [];
|
|
@@ -6241,6 +6349,32 @@ var AnonSubmitPayload = SubmitPayload.extend({
|
|
|
6241
6349
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
6242
6350
|
anonKey: external_exports.string().min(16).max(128)
|
|
6243
6351
|
});
|
|
6352
|
+
var VerifyRequestRecord = external_exports.object({
|
|
6353
|
+
/** Local calendar date (YYYY-MM-DD) this request is bucketed under. */
|
|
6354
|
+
date: DateString,
|
|
6355
|
+
/** Epoch milliseconds of the request's final (max-token) transcript line. */
|
|
6356
|
+
ts: external_exports.number().int().nonnegative(),
|
|
6357
|
+
tool: external_exports.string().min(1).max(64),
|
|
6358
|
+
model: external_exports.string().min(1).max(128),
|
|
6359
|
+
inputTokens: tokenCount,
|
|
6360
|
+
outputTokens: tokenCount,
|
|
6361
|
+
cacheCreationTokens: tokenCount,
|
|
6362
|
+
cacheReadTokens: tokenCount,
|
|
6363
|
+
/** sha256(msgId|reqId), truncated — preserves uniqueness without revealing the id. */
|
|
6364
|
+
reqHash: external_exports.string().min(1).max(64)
|
|
6365
|
+
});
|
|
6366
|
+
var VerifyPayload = external_exports.object({
|
|
6367
|
+
cliVersion: external_exports.string().min(1).max(32),
|
|
6368
|
+
/** The per-request skeleton (bounded/sampled — see `truncated`). */
|
|
6369
|
+
requests: external_exports.array(VerifyRequestRecord).min(1).max(1e5),
|
|
6370
|
+
/**
|
|
6371
|
+
* True when the local corpus exceeded the client upload cap and was sampled to
|
|
6372
|
+
* the most recent N requests. The server treats a truncated upload conservatively
|
|
6373
|
+
* (it never AUTO-passes a truncated one — it can still auto-FAIL on a physical
|
|
6374
|
+
* impossibility, or route to a human).
|
|
6375
|
+
*/
|
|
6376
|
+
truncated: external_exports.boolean().optional()
|
|
6377
|
+
});
|
|
6244
6378
|
function entryTotalTokens(e) {
|
|
6245
6379
|
return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
|
|
6246
6380
|
}
|
|
@@ -6375,6 +6509,26 @@ function submitNextStepLines(result) {
|
|
|
6375
6509
|
" Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
|
|
6376
6510
|
];
|
|
6377
6511
|
}
|
|
6512
|
+
function signedInNextStepLines(result) {
|
|
6513
|
+
if (result.orgBoardUrl) {
|
|
6514
|
+
return [
|
|
6515
|
+
` \u{1F3E2} You're on your team board: ${sanitizeServerText(result.orgBoardUrl)}`,
|
|
6516
|
+
" \u2192 Open it to see who burned more."
|
|
6517
|
+
];
|
|
6518
|
+
}
|
|
6519
|
+
if (result.boardUrl) {
|
|
6520
|
+
const code = result.boardUrl.split("/").filter(Boolean).pop() ?? "";
|
|
6521
|
+
return [
|
|
6522
|
+
` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
|
|
6523
|
+
" \u2192 Open it to see who burned more.",
|
|
6524
|
+
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`
|
|
6525
|
+
];
|
|
6526
|
+
}
|
|
6527
|
+
return [
|
|
6528
|
+
` Your dashboard: ${sanitizeServerText(result.profileUrl)}`,
|
|
6529
|
+
" \u2192 Open it to see your rank and share your profile."
|
|
6530
|
+
];
|
|
6531
|
+
}
|
|
6378
6532
|
|
|
6379
6533
|
// src/local-dashboard.ts
|
|
6380
6534
|
function esc(s) {
|
|
@@ -6752,7 +6906,132 @@ async function run(flags) {
|
|
|
6752
6906
|
console.log(pc2.dim(" --no-submit: skipped the dashboard."));
|
|
6753
6907
|
return;
|
|
6754
6908
|
}
|
|
6755
|
-
const
|
|
6909
|
+
const cfg = loadConfig();
|
|
6910
|
+
const canSignIn = !flags.quiet && Boolean(process.stdout.isTTY);
|
|
6911
|
+
if (cfg?.cliToken) {
|
|
6912
|
+
await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
|
|
6913
|
+
} else if (cfg?.anonKey) {
|
|
6914
|
+
await submitWithDeviceKey(cfg.anonKey, payload, flags);
|
|
6915
|
+
} else if (canSignIn) {
|
|
6916
|
+
const auth = await ensureSignedIn();
|
|
6917
|
+
if (!auth) return;
|
|
6918
|
+
await submitSignedIn(auth.token, payload, flags, canSignIn);
|
|
6919
|
+
} else if (!flags.quiet) {
|
|
6920
|
+
console.log(pc2.yellow(" Sign in to put your usage on the leaderboard."));
|
|
6921
|
+
console.log(
|
|
6922
|
+
pc2.dim(
|
|
6923
|
+
" Run `npx whoburnedmore` in an interactive terminal to sign in, or `npx whoburnedmore link --token=\u2026` (from your signed-in profile) for servers/CI."
|
|
6924
|
+
)
|
|
6925
|
+
);
|
|
6926
|
+
} else {
|
|
6927
|
+
return;
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
function sleep(ms) {
|
|
6931
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6932
|
+
}
|
|
6933
|
+
async function ensureSignedIn() {
|
|
6934
|
+
const cfg = loadConfig();
|
|
6935
|
+
if (cfg?.cliToken) return { token: cfg.cliToken, handle: cfg.handle ?? "" };
|
|
6936
|
+
let dev;
|
|
6937
|
+
try {
|
|
6938
|
+
dev = await deviceStart();
|
|
6939
|
+
} catch (err) {
|
|
6940
|
+
console.log(pc2.yellow(` Couldn't start sign-in: ${err.message}`));
|
|
6941
|
+
return null;
|
|
6942
|
+
}
|
|
6943
|
+
console.log();
|
|
6944
|
+
console.log(pc2.bold(" Sign in to whoburnedmore to put your usage on the board."));
|
|
6945
|
+
console.log(` 1. Opening ${pc2.cyan(sanitizeServerText(dev.verifyUrl))} \u2014 or go there yourself.`);
|
|
6946
|
+
console.log(` 2. Approve this code: ${pc2.bold(sanitizeServerText(dev.userCode))}`);
|
|
6947
|
+
if (isTrustedWebUrl(dev.verifyUrl)) openBrowser(dev.verifyUrl);
|
|
6948
|
+
console.log(pc2.dim(" Waiting for you to approve in the browser\u2026"));
|
|
6949
|
+
const deadline = Date.now() + dev.expiresInSeconds * 1e3;
|
|
6950
|
+
const intervalMs = Math.max(1, dev.pollIntervalSeconds) * 1e3;
|
|
6951
|
+
while (Date.now() < deadline) {
|
|
6952
|
+
await sleep(intervalMs);
|
|
6953
|
+
let res;
|
|
6954
|
+
try {
|
|
6955
|
+
res = await devicePoll(dev.deviceCode);
|
|
6956
|
+
} catch {
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
if (res.status === "ok") {
|
|
6960
|
+
saveAuth(void 0, { cliToken: res.token, handle: res.handle });
|
|
6961
|
+
console.log(pc2.green(` \u2713 Signed in as @${sanitizeServerText(res.handle)}.`));
|
|
6962
|
+
return { token: res.token, handle: res.handle };
|
|
6963
|
+
}
|
|
6964
|
+
if (res.status === "expired") break;
|
|
6965
|
+
}
|
|
6966
|
+
console.log(
|
|
6967
|
+
pc2.yellow(" Sign-in timed out. Run `npx whoburnedmore` again to retry.")
|
|
6968
|
+
);
|
|
6969
|
+
return null;
|
|
6970
|
+
}
|
|
6971
|
+
async function submitSignedIn(token, payload, flags, interactive) {
|
|
6972
|
+
let result;
|
|
6973
|
+
try {
|
|
6974
|
+
result = await submit(token, payload);
|
|
6975
|
+
} catch (err) {
|
|
6976
|
+
if (err instanceof UnauthorizedError) {
|
|
6977
|
+
clearAuth();
|
|
6978
|
+
if (!interactive) return;
|
|
6979
|
+
const auth = await ensureSignedIn();
|
|
6980
|
+
if (!auth) return;
|
|
6981
|
+
result = await submit(auth.token, payload);
|
|
6982
|
+
} else {
|
|
6983
|
+
throw err;
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
try {
|
|
6987
|
+
recordSync();
|
|
6988
|
+
} catch {
|
|
6989
|
+
}
|
|
6990
|
+
const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.profileUrl;
|
|
6991
|
+
if (!flags.quiet) {
|
|
6992
|
+
console.log(
|
|
6993
|
+
pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
|
|
6994
|
+
);
|
|
6995
|
+
if (result.suppressed) {
|
|
6996
|
+
console.log();
|
|
6997
|
+
console.log(
|
|
6998
|
+
pc2.yellow(
|
|
6999
|
+
" \u26A0 You're currently OFF the leaderboard \u2014 we couldn't verify these numbers."
|
|
7000
|
+
)
|
|
7001
|
+
);
|
|
7002
|
+
console.log(
|
|
7003
|
+
pc2.dim(
|
|
7004
|
+
" Verify your usage to get back on \u2014 this sends a detailed per-request breakdown (timestamps + token counts, never your prompts or code)."
|
|
7005
|
+
)
|
|
7006
|
+
);
|
|
7007
|
+
if (process.stdin.isTTY && await confirm(" Verify now?")) {
|
|
7008
|
+
await runVerify();
|
|
7009
|
+
afterSubmitChores(flags);
|
|
7010
|
+
return;
|
|
7011
|
+
}
|
|
7012
|
+
console.log(
|
|
7013
|
+
pc2.dim(
|
|
7014
|
+
" Run `npx whoburnedmore verify` anytime, or appeal at whoburnedmore.com/appeal."
|
|
7015
|
+
)
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
if (isTrustedWebUrl(baseUrl)) {
|
|
7019
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
7020
|
+
openBrowser(baseUrl);
|
|
7021
|
+
} else {
|
|
7022
|
+
console.log(
|
|
7023
|
+
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
7024
|
+
);
|
|
7025
|
+
console.log(` ${sanitizeServerText(baseUrl)}`);
|
|
7026
|
+
}
|
|
7027
|
+
for (const line of signedInNextStepLines(result)) {
|
|
7028
|
+
if (line.includes("\u2192")) console.log(pc2.bold(line));
|
|
7029
|
+
else console.log(line);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
afterSubmitChores(flags);
|
|
7033
|
+
}
|
|
7034
|
+
async function submitWithDeviceKey(anonKey, payload, flags) {
|
|
6756
7035
|
const result = await anonSubmit(anonKey, payload);
|
|
6757
7036
|
try {
|
|
6758
7037
|
recordSync();
|
|
@@ -6782,26 +7061,24 @@ async function run(flags) {
|
|
|
6782
7061
|
);
|
|
6783
7062
|
console.log(` ${sanitizeServerText(baseUrl)}`);
|
|
6784
7063
|
}
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
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 {
|
|
7064
|
+
for (const line of submitNextStepLines(result)) {
|
|
7065
|
+
if (line.includes("\u2192")) console.log(pc2.bold(line));
|
|
7066
|
+
else if (line.startsWith(" Private until you do")) console.log(pc2.dim(line));
|
|
7067
|
+
else console.log(line);
|
|
6797
7068
|
}
|
|
6798
7069
|
}
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
7070
|
+
afterSubmitChores(flags);
|
|
7071
|
+
}
|
|
7072
|
+
function afterSubmitChores(flags) {
|
|
7073
|
+
if (flags.quiet) return;
|
|
7074
|
+
try {
|
|
7075
|
+
reconcileAutoSync();
|
|
7076
|
+
} catch {
|
|
6804
7077
|
}
|
|
7078
|
+
console.log();
|
|
7079
|
+
console.log(
|
|
7080
|
+
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.")
|
|
7081
|
+
);
|
|
6805
7082
|
}
|
|
6806
7083
|
async function linkServerInstall(token) {
|
|
6807
7084
|
if (!token) {
|
|
@@ -6883,6 +7160,102 @@ async function runDaemon() {
|
|
|
6883
7160
|
console.log();
|
|
6884
7161
|
console.log(pc2.dim(` Daemon stopped after ${cycles} sync cycle${cycles === 1 ? "" : "s"}.`));
|
|
6885
7162
|
}
|
|
7163
|
+
async function runVerify() {
|
|
7164
|
+
printBanner();
|
|
7165
|
+
console.log();
|
|
7166
|
+
console.log(pc2.bold(" Verify your usage to get back on the leaderboard."));
|
|
7167
|
+
console.log(
|
|
7168
|
+
pc2.dim(
|
|
7169
|
+
" 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."
|
|
7170
|
+
)
|
|
7171
|
+
);
|
|
7172
|
+
const cfg = loadConfig();
|
|
7173
|
+
let token = cfg?.cliToken;
|
|
7174
|
+
if (!token) {
|
|
7175
|
+
if (!process.stdout.isTTY) {
|
|
7176
|
+
console.log(
|
|
7177
|
+
pc2.yellow(
|
|
7178
|
+
" Sign in first \u2014 run `npx whoburnedmore verify` in an interactive terminal."
|
|
7179
|
+
)
|
|
7180
|
+
);
|
|
7181
|
+
return;
|
|
7182
|
+
}
|
|
7183
|
+
const auth = await ensureSignedIn();
|
|
7184
|
+
if (!auth) return;
|
|
7185
|
+
token = auth.token;
|
|
7186
|
+
}
|
|
7187
|
+
if (process.stdin.isTTY) {
|
|
7188
|
+
const ok = await confirm("\n Send this breakdown to verify your usage?");
|
|
7189
|
+
if (!ok) {
|
|
7190
|
+
console.log(pc2.dim(" Cancelled \u2014 nothing was sent."));
|
|
7191
|
+
return;
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
console.log(pc2.dim(" Reading your local Claude Code logs\u2026"));
|
|
7195
|
+
const { requests, found } = await collectClaudeRequests();
|
|
7196
|
+
if (!found || requests.length === 0) {
|
|
7197
|
+
console.log(
|
|
7198
|
+
pc2.yellow(" No local Claude Code logs found on this machine to verify.")
|
|
7199
|
+
);
|
|
7200
|
+
console.log(
|
|
7201
|
+
pc2.dim(
|
|
7202
|
+
" Run this on the machine where you actually use Claude Code, or file a written appeal at whoburnedmore.com/appeal."
|
|
7203
|
+
)
|
|
7204
|
+
);
|
|
7205
|
+
return;
|
|
7206
|
+
}
|
|
7207
|
+
const CAP = 5e4;
|
|
7208
|
+
const sorted = requests.slice().sort((a, b) => b.ts - a.ts);
|
|
7209
|
+
const truncated = sorted.length > CAP;
|
|
7210
|
+
const capped = truncated ? sorted.slice(0, CAP) : sorted;
|
|
7211
|
+
const records = capped.map((r) => ({
|
|
7212
|
+
date: r.date,
|
|
7213
|
+
ts: r.ts,
|
|
7214
|
+
tool: "claude",
|
|
7215
|
+
model: r.model,
|
|
7216
|
+
inputTokens: r.inputTokens,
|
|
7217
|
+
outputTokens: r.outputTokens,
|
|
7218
|
+
cacheCreationTokens: r.cacheCreationTokens,
|
|
7219
|
+
cacheReadTokens: r.cacheReadTokens,
|
|
7220
|
+
reqHash: createHash("sha256").update(r.key).digest("hex").slice(0, 32)
|
|
7221
|
+
}));
|
|
7222
|
+
console.log(
|
|
7223
|
+
pc2.dim(
|
|
7224
|
+
` Sending ${records.length} request records for analysis${truncated ? " (most recent, sampled)" : ""}\u2026`
|
|
7225
|
+
)
|
|
7226
|
+
);
|
|
7227
|
+
let result;
|
|
7228
|
+
try {
|
|
7229
|
+
result = await verifyUsage(token, {
|
|
7230
|
+
cliVersion: VERSION,
|
|
7231
|
+
requests: records,
|
|
7232
|
+
...truncated ? { truncated: true } : {}
|
|
7233
|
+
});
|
|
7234
|
+
} catch (err) {
|
|
7235
|
+
if (err instanceof UnauthorizedError) {
|
|
7236
|
+
clearAuth();
|
|
7237
|
+
console.log(
|
|
7238
|
+
pc2.yellow(
|
|
7239
|
+
" Your sign-in expired \u2014 run `npx whoburnedmore verify` again to retry."
|
|
7240
|
+
)
|
|
7241
|
+
);
|
|
7242
|
+
return;
|
|
7243
|
+
}
|
|
7244
|
+
throw err;
|
|
7245
|
+
}
|
|
7246
|
+
console.log();
|
|
7247
|
+
const line = ` ${sanitizeServerText(result.message)}`;
|
|
7248
|
+
if (result.relisted) {
|
|
7249
|
+
console.log(pc2.green(` \u2713${line.slice(1)}`));
|
|
7250
|
+
} else {
|
|
7251
|
+
console.log(result.verdict === "fail" ? pc2.yellow(line) : line);
|
|
7252
|
+
console.log(
|
|
7253
|
+
pc2.dim(
|
|
7254
|
+
" We'll relist you automatically if it checks out \u2014 re-run `npx whoburnedmore` anytime to check."
|
|
7255
|
+
)
|
|
7256
|
+
);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
6886
7259
|
async function main() {
|
|
6887
7260
|
const major = Number(process.versions.node.split(".")[0]);
|
|
6888
7261
|
if (major < 20) {
|
|
@@ -6918,6 +7291,9 @@ async function main() {
|
|
|
6918
7291
|
case "daemon":
|
|
6919
7292
|
await runDaemon();
|
|
6920
7293
|
break;
|
|
7294
|
+
case "verify":
|
|
7295
|
+
await runVerify();
|
|
7296
|
+
break;
|
|
6921
7297
|
case "status":
|
|
6922
7298
|
case "doctor": {
|
|
6923
7299
|
for (const line of agentStatusReport()) console.log(line);
|
|
@@ -6969,8 +7345,8 @@ function printHelp() {
|
|
|
6969
7345
|
${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
|
|
6970
7346
|
|
|
6971
7347
|
${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
|
|
7348
|
+
npx whoburnedmore sign in, burn + land on the public leaderboard, open your dashboard
|
|
7349
|
+
npx whoburnedmore --board=CODE compare with friends \u2014 sign in and join their board
|
|
6974
7350
|
npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
|
|
6975
7351
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
6976
7352
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
@@ -6980,14 +7356,15 @@ function printHelp() {
|
|
|
6980
7356
|
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
6981
7357
|
npx whoburnedmore public put it back on the leaderboard
|
|
6982
7358
|
npx whoburnedmore remove delete your dashboard and its data
|
|
7359
|
+
npx whoburnedmore verify delisted? re-verify your usage to get back on (sends a detailed breakdown)
|
|
6983
7360
|
npx whoburnedmore status check background-sync health (last sync, staleness)
|
|
6984
7361
|
npx whoburnedmore uninstall-sync turn off the background sync
|
|
6985
7362
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
6986
7363
|
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
7364
|
+
Your first run signs you in (we open a page, you approve a short code) and binds
|
|
7365
|
+
this machine to your account \u2014 your usage lands on the leaderboard under your
|
|
7366
|
+
handle. Background sync is on by default: your page then refreshes automatically
|
|
7367
|
+
every 15 min (\`uninstall-sync\` to stop); run \`private\`/\`remove\` to pull it. Only
|
|
6991
7368
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
6992
7369
|
your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
|
|
6993
7370
|
your machine at all.
|