whoburnedmore 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +379 -143
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
11
11
|
import { createRequire as createRequire4 } from "node:module";
|
|
12
12
|
import { platform as platform3 } from "node:os";
|
|
13
|
-
import { join as
|
|
13
|
+
import { join as join7 } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
15
15
|
import pc2 from "picocolors";
|
|
16
16
|
|
|
@@ -29,6 +29,9 @@ function parseBoard(args) {
|
|
|
29
29
|
function apiBase() {
|
|
30
30
|
return process.env.WHOBURNEDMORE_API ?? "https://api.whoburnedmore.com";
|
|
31
31
|
}
|
|
32
|
+
function webBase() {
|
|
33
|
+
return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
|
|
34
|
+
}
|
|
32
35
|
async function readJson(res) {
|
|
33
36
|
const text = await res.text();
|
|
34
37
|
if (!text) return {};
|
|
@@ -40,15 +43,12 @@ async function readJson(res) {
|
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
|
-
async function post(path, body
|
|
46
|
+
async function post(path, body) {
|
|
44
47
|
let res;
|
|
45
48
|
try {
|
|
46
49
|
res = await fetch(`${apiBase()}${path}`, {
|
|
47
50
|
method: "POST",
|
|
48
|
-
headers: {
|
|
49
|
-
"Content-Type": "application/json",
|
|
50
|
-
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
51
|
-
},
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
52
|
body: JSON.stringify(body)
|
|
53
53
|
});
|
|
54
54
|
} catch {
|
|
@@ -58,34 +58,6 @@ async function post(path, body, token) {
|
|
|
58
58
|
}
|
|
59
59
|
return { status: res.status, body: await readJson(res) };
|
|
60
60
|
}
|
|
61
|
-
async function deviceStart() {
|
|
62
|
-
const { status, body } = await post("/v1/auth/device", {});
|
|
63
|
-
if (status !== 200) throw new Error(`device auth failed (HTTP ${status})`);
|
|
64
|
-
return body;
|
|
65
|
-
}
|
|
66
|
-
async function devicePoll(deviceCode) {
|
|
67
|
-
const { body } = await post("/v1/auth/device/token", {
|
|
68
|
-
deviceCode
|
|
69
|
-
});
|
|
70
|
-
return body;
|
|
71
|
-
}
|
|
72
|
-
async function submitUsage(token, payload) {
|
|
73
|
-
const { status, body } = await post(
|
|
74
|
-
"/v1/submit",
|
|
75
|
-
payload,
|
|
76
|
-
token
|
|
77
|
-
);
|
|
78
|
-
if (status === 401) {
|
|
79
|
-
throw new Error("session expired \u2014 run `npx whoburnedmore login` again");
|
|
80
|
-
}
|
|
81
|
-
if (status !== 200) {
|
|
82
|
-
const err = body;
|
|
83
|
-
const details = err.details?.length ? `
|
|
84
|
-
- ${err.details.join("\n - ")}` : "";
|
|
85
|
-
throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
|
|
86
|
-
}
|
|
87
|
-
return body;
|
|
88
|
-
}
|
|
89
61
|
async function anonSubmit(anonKey, payload) {
|
|
90
62
|
const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
|
|
91
63
|
if (status !== 200) {
|
|
@@ -122,14 +94,14 @@ async function anonRemove(anonKey) {
|
|
|
122
94
|
|
|
123
95
|
// src/autosync.ts
|
|
124
96
|
import { spawnSync } from "node:child_process";
|
|
125
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync
|
|
97
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
126
98
|
import { homedir as homedir2, platform } from "node:os";
|
|
127
99
|
import { join as join2 } from "node:path";
|
|
128
100
|
import { fileURLToPath } from "node:url";
|
|
129
101
|
|
|
130
102
|
// src/config.ts
|
|
131
103
|
import { randomBytes } from "node:crypto";
|
|
132
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
104
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
133
105
|
import { homedir } from "node:os";
|
|
134
106
|
import { join } from "node:path";
|
|
135
107
|
function defaultConfigDir() {
|
|
@@ -141,8 +113,6 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
141
113
|
try {
|
|
142
114
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
143
115
|
const config = {};
|
|
144
|
-
if (typeof parsed.token === "string") config.token = parsed.token;
|
|
145
|
-
if (typeof parsed.handle === "string") config.handle = parsed.handle;
|
|
146
116
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
147
117
|
return Object.keys(config).length > 0 ? config : null;
|
|
148
118
|
} catch {
|
|
@@ -154,9 +124,6 @@ function saveConfig(dir = defaultConfigDir(), config = {}) {
|
|
|
154
124
|
const file = join(dir, "config.json");
|
|
155
125
|
writeFileSync(file, JSON.stringify(config, null, 2), { mode: 384 });
|
|
156
126
|
}
|
|
157
|
-
function clearConfig(dir = defaultConfigDir()) {
|
|
158
|
-
rmSync(join(dir, "config.json"), { force: true });
|
|
159
|
-
}
|
|
160
127
|
function ensureAnonKey(dir = defaultConfigDir()) {
|
|
161
128
|
const config = loadConfig(dir) ?? {};
|
|
162
129
|
if (config.anonKey) return config.anonKey;
|
|
@@ -250,7 +217,7 @@ function uninstallAutoSync() {
|
|
|
250
217
|
const plistPath = launchAgentPath();
|
|
251
218
|
if (existsSync2(plistPath)) {
|
|
252
219
|
spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
253
|
-
|
|
220
|
+
rmSync(plistPath, { force: true });
|
|
254
221
|
}
|
|
255
222
|
return "launchd agent removed";
|
|
256
223
|
}
|
|
@@ -286,19 +253,246 @@ function autoSyncInstalled() {
|
|
|
286
253
|
// src/collect.ts
|
|
287
254
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
288
255
|
import { createRequire as createRequire3 } from "node:module";
|
|
289
|
-
import { dirname as dirname2, join as
|
|
256
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
257
|
+
|
|
258
|
+
// src/attribution.ts
|
|
259
|
+
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
260
|
+
import { homedir as homedir3 } from "node:os";
|
|
261
|
+
import { basename, join as join3 } from "node:path";
|
|
262
|
+
|
|
263
|
+
// src/pricing.ts
|
|
264
|
+
var TABLE = [
|
|
265
|
+
{ match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
266
|
+
{ match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
|
|
267
|
+
{ match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
|
|
268
|
+
{ match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
269
|
+
{ match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
|
|
270
|
+
{ match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
|
|
271
|
+
{ match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
|
|
272
|
+
{ match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
|
|
273
|
+
];
|
|
274
|
+
function estimateCostUSD(model, t) {
|
|
275
|
+
const row = TABLE.find((r) => r.match.test(model));
|
|
276
|
+
if (!row) return 0;
|
|
277
|
+
const p = row.price;
|
|
278
|
+
const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
|
|
279
|
+
return usd > 0 ? usd : 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/attribution.ts
|
|
283
|
+
var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
284
|
+
var MAX_FILES = 5e3;
|
|
285
|
+
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
286
|
+
var TIME_BUDGET_MS = 3e4;
|
|
287
|
+
var MAX_STATS = 300;
|
|
288
|
+
var MAX_PROJECTS = 500;
|
|
289
|
+
function createAccumulator() {
|
|
290
|
+
return {
|
|
291
|
+
tools: /* @__PURE__ */ new Map(),
|
|
292
|
+
skills: /* @__PURE__ */ new Map(),
|
|
293
|
+
projects: /* @__PURE__ */ new Map(),
|
|
294
|
+
agent: {
|
|
295
|
+
messageCount: 0,
|
|
296
|
+
subagentMessages: 0,
|
|
297
|
+
subagentTokens: 0,
|
|
298
|
+
totalTokens: 0
|
|
299
|
+
},
|
|
300
|
+
titles: /* @__PURE__ */ new Map(),
|
|
301
|
+
sessionMessages: /* @__PURE__ */ new Map()
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function createFileContext() {
|
|
305
|
+
return { toolNames: /* @__PURE__ */ new Map() };
|
|
306
|
+
}
|
|
307
|
+
function recordTokens(usage) {
|
|
308
|
+
if (!usage || typeof usage !== "object") return 0;
|
|
309
|
+
const u = usage;
|
|
310
|
+
const n = (v) => {
|
|
311
|
+
const x = Math.round(Number(v));
|
|
312
|
+
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
313
|
+
};
|
|
314
|
+
return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
|
|
315
|
+
}
|
|
316
|
+
function processRecord(rec, acc, ctx) {
|
|
317
|
+
if (!rec || typeof rec !== "object") return;
|
|
318
|
+
const r = rec;
|
|
319
|
+
const recTokens = recordTokens(r.message?.usage);
|
|
320
|
+
if (typeof r.attributionSkill === "string" && r.attributionSkill) {
|
|
321
|
+
const s = r.attributionSkill.slice(0, 128);
|
|
322
|
+
const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
|
|
323
|
+
sk.count += 1;
|
|
324
|
+
sk.tokens += recTokens;
|
|
325
|
+
acc.skills.set(s, sk);
|
|
326
|
+
}
|
|
327
|
+
if (r.type === "ai-title" && typeof r.aiTitle === "string" && r.aiTitle && typeof r.sessionId === "string" && r.sessionId) {
|
|
328
|
+
acc.titles.set(r.sessionId, r.aiTitle.slice(0, 200));
|
|
329
|
+
}
|
|
330
|
+
const content = r.message?.content;
|
|
331
|
+
if (!Array.isArray(content)) return;
|
|
332
|
+
const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
|
|
333
|
+
const isUser = r.type === "user" || r.message?.role === "user";
|
|
334
|
+
if (isAssistant) {
|
|
335
|
+
const toolUses = [];
|
|
336
|
+
for (const block of content) {
|
|
337
|
+
if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
|
|
338
|
+
const name = block.name.slice(0, 128);
|
|
339
|
+
if (!name) continue;
|
|
340
|
+
const id = block.id;
|
|
341
|
+
toolUses.push({ name, id: typeof id === "string" ? id : void 0 });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const perToolTokens = toolUses.length > 0 ? Math.floor(recTokens / toolUses.length) : 0;
|
|
345
|
+
for (const tu of toolUses) {
|
|
346
|
+
const t = acc.tools.get(tu.name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
347
|
+
t.count += 1;
|
|
348
|
+
t.tokens += perToolTokens;
|
|
349
|
+
acc.tools.set(tu.name, t);
|
|
350
|
+
if (tu.id) ctx.toolNames.set(tu.id, tu.name);
|
|
351
|
+
}
|
|
352
|
+
const tokens = recTokens;
|
|
353
|
+
acc.agent.messageCount += 1;
|
|
354
|
+
acc.agent.totalTokens += tokens;
|
|
355
|
+
const sidechain = r.isSidechain === true;
|
|
356
|
+
if (sidechain) {
|
|
357
|
+
acc.agent.subagentMessages += 1;
|
|
358
|
+
acc.agent.subagentTokens += tokens;
|
|
359
|
+
}
|
|
360
|
+
if (typeof r.sessionId === "string" && r.sessionId) {
|
|
361
|
+
acc.sessionMessages.set(
|
|
362
|
+
r.sessionId,
|
|
363
|
+
(acc.sessionMessages.get(r.sessionId) ?? 0) + 1
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (typeof r.cwd === "string" && r.cwd && tokens > 0) {
|
|
367
|
+
const name = basename(r.cwd).slice(0, 128) || "unknown";
|
|
368
|
+
const model = typeof r.message?.model === "string" ? r.message.model : "unknown";
|
|
369
|
+
const u = r.message?.usage;
|
|
370
|
+
const num3 = (v) => {
|
|
371
|
+
const x = Math.round(Number(v));
|
|
372
|
+
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
373
|
+
};
|
|
374
|
+
const cost = estimateCostUSD(model, {
|
|
375
|
+
inputTokens: num3(u?.input_tokens),
|
|
376
|
+
outputTokens: num3(u?.output_tokens),
|
|
377
|
+
cacheCreationTokens: num3(u?.cache_creation_input_tokens),
|
|
378
|
+
cacheReadTokens: num3(u?.cache_read_input_tokens)
|
|
379
|
+
});
|
|
380
|
+
const p = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
|
|
381
|
+
p.tokens += tokens;
|
|
382
|
+
p.costUSD += cost;
|
|
383
|
+
acc.projects.set(name, p);
|
|
384
|
+
}
|
|
385
|
+
} else if (isUser) {
|
|
386
|
+
for (const block of content) {
|
|
387
|
+
if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
|
|
388
|
+
const id = block.tool_use_id;
|
|
389
|
+
const name = typeof id === "string" ? ctx.toolNames.get(id) : void 0;
|
|
390
|
+
if (name) {
|
|
391
|
+
const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
392
|
+
t.errors += 1;
|
|
393
|
+
acc.tools.set(name, t);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function toSkillStats(map) {
|
|
400
|
+
return [...map.entries()].map(([name, v]) => ({ name, count: v.count, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => s.tokens > 0 ? s : { name: s.name, count: s.count });
|
|
401
|
+
}
|
|
402
|
+
function toToolStats(map) {
|
|
403
|
+
return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => {
|
|
404
|
+
const base = { name: s.name, count: s.count };
|
|
405
|
+
if (s.errors > 0) base.errors = s.errors;
|
|
406
|
+
if (s.tokens > 0) base.tokens = s.tokens;
|
|
407
|
+
return base;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function toProjectStats(map) {
|
|
411
|
+
return [...map.entries()].map(([name, v]) => ({
|
|
412
|
+
name,
|
|
413
|
+
tokens: v.tokens,
|
|
414
|
+
costUSD: Number(v.costUSD.toFixed(6))
|
|
415
|
+
})).filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, MAX_PROJECTS);
|
|
416
|
+
}
|
|
417
|
+
function accumulatorToResult(acc) {
|
|
418
|
+
return {
|
|
419
|
+
tools: toToolStats(acc.tools),
|
|
420
|
+
skills: toSkillStats(acc.skills),
|
|
421
|
+
projects: toProjectStats(acc.projects),
|
|
422
|
+
agent: { ...acc.agent },
|
|
423
|
+
titles: acc.titles,
|
|
424
|
+
sessionMessages: acc.sessionMessages
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function listTranscripts(dir) {
|
|
428
|
+
const out = [];
|
|
429
|
+
const walk = (d) => {
|
|
430
|
+
if (out.length >= MAX_FILES) return;
|
|
431
|
+
let entries;
|
|
432
|
+
try {
|
|
433
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
434
|
+
} catch {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
for (const e of entries) {
|
|
438
|
+
if (out.length >= MAX_FILES) return;
|
|
439
|
+
const p = join3(d, e.name);
|
|
440
|
+
if (e.isDirectory()) walk(p);
|
|
441
|
+
else if (e.isFile() && e.name.endsWith(".jsonl")) {
|
|
442
|
+
try {
|
|
443
|
+
out.push({ path: p, mtime: statSync(p).mtimeMs });
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
walk(dir);
|
|
450
|
+
return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
|
|
451
|
+
}
|
|
452
|
+
function collectAttribution() {
|
|
453
|
+
const acc = createAccumulator();
|
|
454
|
+
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
455
|
+
try {
|
|
456
|
+
for (const file of listTranscripts(CLAUDE_PROJECTS)) {
|
|
457
|
+
if (Date.now() > deadline) break;
|
|
458
|
+
let size = 0;
|
|
459
|
+
try {
|
|
460
|
+
size = statSync(file).size;
|
|
461
|
+
} catch {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (size > MAX_FILE_BYTES) continue;
|
|
465
|
+
let text;
|
|
466
|
+
try {
|
|
467
|
+
text = readFileSync2(file, "utf8");
|
|
468
|
+
} catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const ctx = createFileContext();
|
|
472
|
+
for (const line of text.split("\n")) {
|
|
473
|
+
if (!line) continue;
|
|
474
|
+
try {
|
|
475
|
+
processRecord(JSON.parse(line), acc, ctx);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
return accumulatorToResult(acc);
|
|
483
|
+
}
|
|
290
484
|
|
|
291
485
|
// src/cursor.ts
|
|
292
486
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
293
487
|
import { existsSync as existsSync3 } from "node:fs";
|
|
294
488
|
import { createRequire as createRequire2 } from "node:module";
|
|
295
|
-
import { homedir as
|
|
296
|
-
import { join as
|
|
489
|
+
import { homedir as homedir4, platform as platform2 } from "node:os";
|
|
490
|
+
import { join as join5 } from "node:path";
|
|
297
491
|
|
|
298
492
|
// src/tokscale.ts
|
|
299
493
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
300
494
|
import { createRequire } from "node:module";
|
|
301
|
-
import { dirname, join as
|
|
495
|
+
import { dirname, join as join4 } from "node:path";
|
|
302
496
|
var LOOKBACK_DAYS = 30;
|
|
303
497
|
function num(n) {
|
|
304
498
|
const v = Math.round(Number(n));
|
|
@@ -342,7 +536,7 @@ function resolveTokscaleBin() {
|
|
|
342
536
|
const pkg = require3("tokscale/package.json");
|
|
343
537
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
|
|
344
538
|
if (!rel) return null;
|
|
345
|
-
const binPath =
|
|
539
|
+
const binPath = join4(dirname(pkgPath), rel);
|
|
346
540
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
347
541
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
348
542
|
}
|
|
@@ -397,9 +591,9 @@ function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
|
|
|
397
591
|
// src/cursor.ts
|
|
398
592
|
var EVENTS_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events";
|
|
399
593
|
function cursorDbPath() {
|
|
400
|
-
const home =
|
|
594
|
+
const home = homedir4();
|
|
401
595
|
const os = platform2();
|
|
402
|
-
const p = os === "darwin" ?
|
|
596
|
+
const p = os === "darwin" ? join5(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join5(process.env.APPDATA ?? join5(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join5(process.env.XDG_CONFIG_HOME ?? join5(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
|
|
403
597
|
return existsSync3(p) ? p : null;
|
|
404
598
|
}
|
|
405
599
|
function readCursorToken(db) {
|
|
@@ -672,7 +866,7 @@ function resolveCcusageBin() {
|
|
|
672
866
|
const pkgPath = require3.resolve("ccusage/package.json");
|
|
673
867
|
const pkg = require3("ccusage/package.json");
|
|
674
868
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
|
|
675
|
-
const binPath =
|
|
869
|
+
const binPath = join6(dirname2(pkgPath), rel);
|
|
676
870
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
677
871
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
678
872
|
}
|
|
@@ -760,11 +954,25 @@ async function collectAll() {
|
|
|
760
954
|
blocks.push(...cursor.blocks);
|
|
761
955
|
toolsFound.push("cursor");
|
|
762
956
|
}
|
|
957
|
+
const { tools, skills, projects, agent, titles, sessionMessages } = collectAttribution();
|
|
958
|
+
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
959
|
+
const title = titles.get(s.sessionId);
|
|
960
|
+
const messageCount = sessionMessages.get(s.sessionId);
|
|
961
|
+
return {
|
|
962
|
+
...s,
|
|
963
|
+
...title ? { title } : {},
|
|
964
|
+
...messageCount ? { messageCount } : {}
|
|
965
|
+
};
|
|
966
|
+
});
|
|
763
967
|
return {
|
|
764
968
|
entries: dedupeDaily(entries),
|
|
765
|
-
sessions:
|
|
969
|
+
sessions: dedupedSessions,
|
|
766
970
|
blocks: dedupeBlocks(blocks),
|
|
767
|
-
toolsFound
|
|
971
|
+
toolsFound,
|
|
972
|
+
tools,
|
|
973
|
+
skills,
|
|
974
|
+
projects,
|
|
975
|
+
agent
|
|
768
976
|
};
|
|
769
977
|
}
|
|
770
978
|
|
|
@@ -4855,13 +5063,46 @@ var SessionEntry = external_exports.object({
|
|
|
4855
5063
|
cacheCreationTokens: tokenCount,
|
|
4856
5064
|
cacheReadTokens: tokenCount,
|
|
4857
5065
|
costUSD: external_exports.number().nonnegative(),
|
|
4858
|
-
lastActivity: Timestamp
|
|
5066
|
+
lastActivity: Timestamp,
|
|
5067
|
+
/** Human-readable AI-generated session title (from transcripts). Optional. */
|
|
5068
|
+
title: external_exports.string().max(200).optional(),
|
|
5069
|
+
/** Number of assistant messages in this session (from transcripts). Optional. */
|
|
5070
|
+
messageCount: external_exports.number().int().nonnegative().optional()
|
|
4859
5071
|
});
|
|
4860
5072
|
var BlockEntry = external_exports.object({
|
|
4861
5073
|
startTime: Timestamp,
|
|
4862
5074
|
totalTokens: tokenCount,
|
|
4863
5075
|
costUSD: external_exports.number().nonnegative()
|
|
4864
5076
|
});
|
|
5077
|
+
var ToolStat = external_exports.object({
|
|
5078
|
+
name: external_exports.string().min(1).max(128),
|
|
5079
|
+
count: external_exports.number().int().nonnegative(),
|
|
5080
|
+
/** How many of those calls returned an error/interrupt (tool reliability). Optional. */
|
|
5081
|
+
errors: external_exports.number().int().nonnegative().optional(),
|
|
5082
|
+
/** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
|
|
5083
|
+
tokens: external_exports.number().int().nonnegative().optional()
|
|
5084
|
+
});
|
|
5085
|
+
var ProjectStat = external_exports.object({
|
|
5086
|
+
name: external_exports.string().min(1).max(128),
|
|
5087
|
+
tokens: external_exports.number().int().nonnegative(),
|
|
5088
|
+
costUSD: external_exports.number().nonnegative()
|
|
5089
|
+
});
|
|
5090
|
+
var AgentStat = external_exports.object({
|
|
5091
|
+
/** Total assistant messages across transcripts. */
|
|
5092
|
+
messageCount: external_exports.number().int().nonnegative(),
|
|
5093
|
+
/** Assistant messages that ran inside a subagent sidechain. */
|
|
5094
|
+
subagentMessages: external_exports.number().int().nonnegative(),
|
|
5095
|
+
/** Tokens spent inside subagent sidechains. */
|
|
5096
|
+
subagentTokens: external_exports.number().int().nonnegative(),
|
|
5097
|
+
/** Total tokens observed across transcripts (denominator for the share). */
|
|
5098
|
+
totalTokens: external_exports.number().int().nonnegative()
|
|
5099
|
+
});
|
|
5100
|
+
var SkillStat = external_exports.object({
|
|
5101
|
+
name: external_exports.string().min(1).max(128),
|
|
5102
|
+
count: external_exports.number().int().nonnegative(),
|
|
5103
|
+
/** Tokens burned in records produced while this skill was active. Optional. */
|
|
5104
|
+
tokens: external_exports.number().int().nonnegative().optional()
|
|
5105
|
+
});
|
|
4865
5106
|
var SubmitPayload = external_exports.object({
|
|
4866
5107
|
cliVersion: external_exports.string().min(1).max(32),
|
|
4867
5108
|
entries: external_exports.array(DailyUsageEntry).min(1).max(2e4),
|
|
@@ -4869,6 +5110,14 @@ var SubmitPayload = external_exports.object({
|
|
|
4869
5110
|
sessions: external_exports.array(SessionEntry).max(1e4).optional(),
|
|
4870
5111
|
/** Optional time-window rollups (ccusage blocks) for peak-hours analysis. */
|
|
4871
5112
|
blocks: external_exports.array(BlockEntry).max(1e4).optional(),
|
|
5113
|
+
/** Optional tool-call frequencies parsed from local transcripts (names + counts). */
|
|
5114
|
+
tools: external_exports.array(ToolStat).max(300).optional(),
|
|
5115
|
+
/** Optional skill-usage frequencies parsed from local transcripts. */
|
|
5116
|
+
skills: external_exports.array(SkillStat).max(300).optional(),
|
|
5117
|
+
/** Optional per-project usage totals parsed from local transcripts. */
|
|
5118
|
+
projects: external_exports.array(ProjectStat).max(500).optional(),
|
|
5119
|
+
/** Optional subagent-vs-main rollup parsed from local transcripts. */
|
|
5120
|
+
agent: AgentStat.optional(),
|
|
4872
5121
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
4873
5122
|
board: external_exports.string().min(1).max(32).optional()
|
|
4874
5123
|
});
|
|
@@ -4933,7 +5182,7 @@ function printSummary(entries) {
|
|
|
4933
5182
|
function esc(s) {
|
|
4934
5183
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4935
5184
|
}
|
|
4936
|
-
function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date()) {
|
|
5185
|
+
function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
|
|
4937
5186
|
const today = generatedAt.toISOString().slice(0, 10);
|
|
4938
5187
|
const totals = {
|
|
4939
5188
|
tokens: 0,
|
|
@@ -4985,6 +5234,18 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
|
|
|
4985
5234
|
([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>`
|
|
4986
5235
|
).join("");
|
|
4987
5236
|
const stat = (label, value, accent = false) => `<div class="card"><div class="label">${label}</div><div class="value${accent ? " accent" : ""}">${esc(value)}</div></div>`;
|
|
5237
|
+
const connectCta = connect ? `
|
|
5238
|
+
<form class="connect" method="POST" action="${esc(connect.webBaseUrl)}/connect">
|
|
5239
|
+
<input type="hidden" name="payload" value="${Buffer.from(JSON.stringify(connect.payload)).toString("base64")}">
|
|
5240
|
+
<div class="connect-row">
|
|
5241
|
+
<div>
|
|
5242
|
+
<div class="connect-title">Connect your account</div>
|
|
5243
|
+
<div class="connect-sub">Save this dashboard to your account and claim your spot on the public leaderboard. The local numbers above become your starting point \u2014 nothing has left your machine yet.</div>
|
|
5244
|
+
</div>
|
|
5245
|
+
<button type="submit">Connect your account \u2192</button>
|
|
5246
|
+
</div>
|
|
5247
|
+
<div class="connect-note">After connecting, run <code>npx whoburnedmore</code> (no flag) once so it keeps syncing automatically in the background.</div>
|
|
5248
|
+
</form>` : "";
|
|
4988
5249
|
return `<!doctype html>
|
|
4989
5250
|
<html lang="en">
|
|
4990
5251
|
<head>
|
|
@@ -5022,13 +5283,22 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
|
|
|
5022
5283
|
td.mono { font-family: ui-monospace, monospace; font-size: 12px; }
|
|
5023
5284
|
.foot { color: #78716c; font-size: 12px; margin-top: 32px; }
|
|
5024
5285
|
.foot code { color: #d6d3d1; }
|
|
5286
|
+
.connect { display: block; margin: 24px 0 4px; background: linear-gradient(135deg, rgba(234,88,12,.16), rgba(249,115,22,.06)); border: 1px solid rgba(234,88,12,.5); border-radius: 14px; padding: 18px 20px; }
|
|
5287
|
+
.connect-row { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
|
|
5288
|
+
@media (min-width: 640px) { .connect-row { flex-direction: row; align-items: center; justify-content: space-between; } }
|
|
5289
|
+
.connect-title { font-size: 16px; font-weight: 700; }
|
|
5290
|
+
.connect-sub { color: #d6d3d1; font-size: 13px; margin-top: 4px; max-width: 60ch; }
|
|
5291
|
+
.connect button { flex-shrink: 0; cursor: pointer; border: 0; border-radius: 10px; background: #ea580c; color: #fff; font-size: 14px; font-weight: 600; padding: 11px 18px; font-family: inherit; transition: background .15s; }
|
|
5292
|
+
.connect button:hover { background: #f97316; }
|
|
5293
|
+
.connect-note { color: #a8a29e; font-size: 12px; margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(234,88,12,.25); }
|
|
5294
|
+
.connect-note code { color: #fed7aa; background: rgba(0,0,0,.25); padding: 1px 6px; border-radius: 5px; }
|
|
5025
5295
|
</style>
|
|
5026
5296
|
</head>
|
|
5027
5297
|
<body>
|
|
5028
5298
|
<div class="wrap">
|
|
5029
5299
|
<h1>who burned more<span class="q">?</span></h1>
|
|
5030
5300
|
<div class="sub">your local burn report \xB7 generated ${esc(generatedAt.toISOString().slice(0, 16).replace("T", " "))} \xB7 nothing left your machine</div>
|
|
5031
|
-
|
|
5301
|
+
${connectCta}
|
|
5032
5302
|
<div class="grid">
|
|
5033
5303
|
${stat("total tokens", formatTokens(totals.tokens), true)}
|
|
5034
5304
|
${stat("est. cost", formatUSD(totals.cost))}
|
|
@@ -5072,7 +5342,7 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
|
|
|
5072
5342
|
|
|
5073
5343
|
<div class="foot">
|
|
5074
5344
|
Re-run <code>npx whoburnedmore --local</code> to refresh this page.<br>
|
|
5075
|
-
Run <code>npx whoburnedmore</code> (no flag) to get a shareable dashboard at whoburnedmore.com \u2014 no sign-in.
|
|
5345
|
+
${connect ? "Use \u201CConnect your account\u201D above to save it to whoburnedmore.com and join the leaderboard." : "Run <code>npx whoburnedmore</code> (no flag) to get a shareable dashboard at whoburnedmore.com \u2014 no sign-in."}
|
|
5076
5346
|
</div>
|
|
5077
5347
|
</div>
|
|
5078
5348
|
</body>
|
|
@@ -5123,27 +5393,6 @@ function openBrowser(url) {
|
|
|
5123
5393
|
const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5124
5394
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
5125
5395
|
}
|
|
5126
|
-
async function login() {
|
|
5127
|
-
const device = await deviceStart();
|
|
5128
|
-
console.log();
|
|
5129
|
-
console.log(` Opening ${pc2.cyan(device.verifyUrl)}`);
|
|
5130
|
-
console.log(` Your code: ${pc2.bold(pc2.yellow(device.userCode))}`);
|
|
5131
|
-
console.log(pc2.dim(" Sign in with Google or GitHub and approve this device."));
|
|
5132
|
-
openBrowser(device.verifyUrl);
|
|
5133
|
-
const deadline = Date.now() + device.expiresInSeconds * 1e3;
|
|
5134
|
-
while (Date.now() < deadline) {
|
|
5135
|
-
await new Promise((r) => setTimeout(r, device.pollIntervalSeconds * 1e3));
|
|
5136
|
-
const poll = await devicePoll(device.deviceCode);
|
|
5137
|
-
if (poll.status === "ok") {
|
|
5138
|
-
const config = { token: poll.token, handle: poll.handle };
|
|
5139
|
-
saveConfig(void 0, config);
|
|
5140
|
-
console.log(` Signed in as ${pc2.bold(poll.handle)} \u2713`);
|
|
5141
|
-
return config;
|
|
5142
|
-
}
|
|
5143
|
-
if (poll.status === "expired") break;
|
|
5144
|
-
}
|
|
5145
|
-
throw new Error("login timed out \u2014 run `npx whoburnedmore` to try again");
|
|
5146
|
-
}
|
|
5147
5396
|
async function confirm(question) {
|
|
5148
5397
|
if (!process.stdin.isTTY) return false;
|
|
5149
5398
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -5151,11 +5400,17 @@ async function confirm(question) {
|
|
|
5151
5400
|
rl.close();
|
|
5152
5401
|
return answer === "" || /^y(es)?$/i.test(answer);
|
|
5153
5402
|
}
|
|
5154
|
-
function showLocalDashboard(
|
|
5403
|
+
function showLocalDashboard(payload) {
|
|
5155
5404
|
const dir = defaultConfigDir();
|
|
5156
5405
|
mkdirSync3(dir, { recursive: true });
|
|
5157
|
-
const file =
|
|
5158
|
-
writeFileSync3(
|
|
5406
|
+
const file = join7(dir, "dashboard.html");
|
|
5407
|
+
writeFileSync3(
|
|
5408
|
+
file,
|
|
5409
|
+
renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
|
|
5410
|
+
payload,
|
|
5411
|
+
webBaseUrl: webBase()
|
|
5412
|
+
})
|
|
5413
|
+
);
|
|
5159
5414
|
console.log();
|
|
5160
5415
|
console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
|
|
5161
5416
|
console.log(pc2.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
|
|
@@ -5173,7 +5428,7 @@ async function run(flags) {
|
|
|
5173
5428
|
} finally {
|
|
5174
5429
|
stop();
|
|
5175
5430
|
}
|
|
5176
|
-
const { entries, sessions, blocks, toolsFound } = collected;
|
|
5431
|
+
const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent } = collected;
|
|
5177
5432
|
if (entries.length === 0) {
|
|
5178
5433
|
console.log();
|
|
5179
5434
|
console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
|
|
@@ -5183,6 +5438,10 @@ async function run(flags) {
|
|
|
5183
5438
|
const payload = { cliVersion: VERSION, entries };
|
|
5184
5439
|
if (sessions.length > 0) payload.sessions = sessions;
|
|
5185
5440
|
if (blocks.length > 0) payload.blocks = blocks;
|
|
5441
|
+
if (tools.length > 0) payload.tools = tools;
|
|
5442
|
+
if (skills.length > 0) payload.skills = skills;
|
|
5443
|
+
if (projects.length > 0) payload.projects = projects;
|
|
5444
|
+
if (agent.messageCount > 0) payload.agent = agent;
|
|
5186
5445
|
if (flags.board) payload.board = flags.board;
|
|
5187
5446
|
if (flags.dryRun) {
|
|
5188
5447
|
console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
@@ -5191,7 +5450,7 @@ async function run(flags) {
|
|
|
5191
5450
|
}
|
|
5192
5451
|
if (!flags.quiet) printSummary(entries);
|
|
5193
5452
|
if (flags.local) {
|
|
5194
|
-
showLocalDashboard(
|
|
5453
|
+
showLocalDashboard(payload);
|
|
5195
5454
|
if (!flags.quiet && process.stdin.isTTY) {
|
|
5196
5455
|
await publishLocal(payload, {
|
|
5197
5456
|
confirm,
|
|
@@ -5207,61 +5466,46 @@ async function run(flags) {
|
|
|
5207
5466
|
console.log(pc2.dim(" --no-submit: skipped the dashboard."));
|
|
5208
5467
|
return;
|
|
5209
5468
|
}
|
|
5210
|
-
const
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5469
|
+
const anonKey = ensureAnonKey();
|
|
5470
|
+
const result = await anonSubmit(anonKey, payload);
|
|
5471
|
+
const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
|
|
5472
|
+
if (!flags.quiet) {
|
|
5473
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
5474
|
+
openBrowser(target);
|
|
5475
|
+
}
|
|
5476
|
+
console.log(
|
|
5477
|
+
` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
|
|
5478
|
+
);
|
|
5479
|
+
if (result.boardUrl) {
|
|
5217
5480
|
console.log(
|
|
5218
|
-
`
|
|
5481
|
+
` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
|
|
5219
5482
|
);
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
` You are ${pc2.bold(pc2.yellow(`#${result.rank}`))} with ${pc2.bold(formatTokens(result.totalTokens))} tokens burned.`
|
|
5223
|
-
);
|
|
5224
|
-
}
|
|
5225
|
-
console.log(` ${pc2.cyan(result.profileUrl)}`);
|
|
5226
|
-
if (result.boardUrl) {
|
|
5227
|
-
console.log(` \u{1F91D} You're on the friends board: ${pc2.cyan(result.boardUrl)}`);
|
|
5228
|
-
}
|
|
5483
|
+
console.log(` ${pc2.cyan(result.boardUrl)}`);
|
|
5484
|
+
console.log(pc2.dim(` Your dashboard: ${result.dashboardUrl}`));
|
|
5229
5485
|
} else {
|
|
5230
|
-
const anonKey = ensureAnonKey();
|
|
5231
|
-
const result = await anonSubmit(anonKey, payload);
|
|
5232
|
-
const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
|
|
5233
|
-
if (!flags.quiet) {
|
|
5234
|
-
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
5235
|
-
openBrowser(target);
|
|
5236
|
-
}
|
|
5237
5486
|
console.log(
|
|
5238
|
-
`
|
|
5487
|
+
` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
|
|
5239
5488
|
);
|
|
5240
|
-
|
|
5489
|
+
console.log(` ${pc2.cyan(result.dashboardUrl)}`);
|
|
5490
|
+
if (!flags.quiet) {
|
|
5241
5491
|
console.log(
|
|
5242
|
-
|
|
5492
|
+
pc2.dim(" Claim it (name + X) on the web to own your rank, or make it private / remove it.")
|
|
5243
5493
|
);
|
|
5244
|
-
console.log(` ${pc2.cyan(result.boardUrl)}`);
|
|
5245
|
-
console.log(pc2.dim(` Your dashboard: ${result.dashboardUrl}`));
|
|
5246
|
-
} else {
|
|
5247
5494
|
console.log(
|
|
5248
|
-
|
|
5495
|
+
pc2.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
|
|
5249
5496
|
);
|
|
5250
|
-
console.log(` ${pc2.cyan(result.dashboardUrl)}`);
|
|
5251
|
-
if (!flags.quiet) {
|
|
5252
|
-
console.log(
|
|
5253
|
-
pc2.dim(" Claim it (name + X) to own your rank, or make it private / remove it.")
|
|
5254
|
-
);
|
|
5255
|
-
console.log(
|
|
5256
|
-
pc2.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
|
|
5257
|
-
);
|
|
5258
|
-
}
|
|
5259
5497
|
}
|
|
5260
5498
|
}
|
|
5261
|
-
|
|
5499
|
+
if (!flags.quiet && !autoSyncInstalled()) {
|
|
5500
|
+
try {
|
|
5501
|
+
installAutoSync();
|
|
5502
|
+
} catch {
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5262
5505
|
if (!flags.quiet) {
|
|
5506
|
+
console.log();
|
|
5263
5507
|
console.log(
|
|
5264
|
-
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page
|
|
5508
|
+
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 3h (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
|
|
5265
5509
|
);
|
|
5266
5510
|
}
|
|
5267
5511
|
}
|
|
@@ -5290,14 +5534,6 @@ async function main() {
|
|
|
5290
5534
|
await run({ ...flags, noSubmit: false, dryRun: false, local: false });
|
|
5291
5535
|
break;
|
|
5292
5536
|
}
|
|
5293
|
-
case "login":
|
|
5294
|
-
await login();
|
|
5295
|
-
break;
|
|
5296
|
-
case "logout":
|
|
5297
|
-
clearConfig();
|
|
5298
|
-
console.log(" Logged out. Your leaderboard data is untouched.");
|
|
5299
|
-
console.log(pc2.dim(" Delete your data anytime from your profile page."));
|
|
5300
|
-
break;
|
|
5301
5537
|
case "private":
|
|
5302
5538
|
case "public": {
|
|
5303
5539
|
const cfg = loadConfig();
|
|
@@ -5352,19 +5588,19 @@ function printHelp() {
|
|
|
5352
5588
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
5353
5589
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
5354
5590
|
npx whoburnedmore --no-submit print local stats only, send nothing
|
|
5355
|
-
npx whoburnedmore private hide your
|
|
5591
|
+
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
5356
5592
|
npx whoburnedmore public put it back on the leaderboard
|
|
5357
|
-
npx whoburnedmore remove delete your
|
|
5358
|
-
npx whoburnedmore
|
|
5359
|
-
npx whoburnedmore
|
|
5360
|
-
npx whoburnedmore install-sync keep your dashboard live (background sync, 3h)
|
|
5361
|
-
npx whoburnedmore uninstall-sync remove background sync
|
|
5593
|
+
npx whoburnedmore remove delete your dashboard and its data
|
|
5594
|
+
npx whoburnedmore uninstall-sync turn off the background sync
|
|
5595
|
+
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
5362
5596
|
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5597
|
+
Background sync is on by default: after your first run, your page refreshes
|
|
5598
|
+
automatically every 3h (\`uninstall-sync\` to stop). Your dashboard is public on
|
|
5599
|
+
the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
|
|
5600
|
+
it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
|
|
5601
|
+
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
5602
|
+
your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
|
|
5603
|
+
your machine at all.
|
|
5368
5604
|
`);
|
|
5369
5605
|
}
|
|
5370
5606
|
main().catch((err) => {
|