whoburnedmore 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/dist/index.js +519 -45
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# whoburnedmore
|
|
2
|
+
|
|
3
|
+
**who burned more tokens — you or them?** One command reads your local AI coding-agent usage logs, shows your burn, and gives you a **shareable dashboard with no sign-in required**.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/whoburnedmore)
|
|
6
|
+
[](https://www.npmjs.com/package/whoburnedmore)
|
|
7
|
+
[](https://www.npmjs.com/package/whoburnedmore)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx whoburnedmore
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That's the whole thing. No install, no account, no config.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What you'll see
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
whoburnedmore · https://whoburnedmore.com
|
|
22
|
+
|
|
23
|
+
🔥 your burn report
|
|
24
|
+
|
|
25
|
+
claude 2.26B tokens $2,640.07 19 days
|
|
26
|
+
codex 58.2M tokens $16.85 5 days
|
|
27
|
+
──────────────────────────────────────────────
|
|
28
|
+
total 2.31B tokens $2,656.91
|
|
29
|
+
today 92.8M tokens
|
|
30
|
+
|
|
31
|
+
You burned 2.31B tokens. Your shareable dashboard:
|
|
32
|
+
https://whoburnedmore.com/d/5twjwi-jtga4r
|
|
33
|
+
|
|
34
|
+
Re-run anytime to update it · `npx whoburnedmore login` to claim a public handle and compete.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Your dashboard is **anonymous and unlisted by default** — only people you send the link to can see it. Run the command again anytime to refresh it.
|
|
38
|
+
|
|
39
|
+
## Privacy first
|
|
40
|
+
|
|
41
|
+
Only **daily aggregate numbers** ever leave your machine — date, tool, model, token counts, and estimated cost. **Never your prompts, your code, or your file names.**
|
|
42
|
+
|
|
43
|
+
See exactly what would be sent, and send nothing:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx whoburnedmore --dry-run
|
|
47
|
+
```
|
|
48
|
+
|
|
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:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx whoburnedmore --local
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
| Command | What it does |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `npx whoburnedmore` | Show your burn + get a shareable dashboard (no sign-in) |
|
|
60
|
+
| `npx whoburnedmore --local` | Build the dashboard locally and open it (fully offline) |
|
|
61
|
+
| `npx whoburnedmore --dry-run` | Print exactly what would be sent, send nothing |
|
|
62
|
+
| `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
|
|
63
|
+
| `npx whoburnedmore login` | Sign in to claim a public handle + join the leaderboard |
|
|
64
|
+
| `npx whoburnedmore logout` | Forget the local token (your data is untouched) |
|
|
65
|
+
| `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (every 3h) |
|
|
66
|
+
| `npx whoburnedmore uninstall-sync` | Remove the background sync |
|
|
67
|
+
|
|
68
|
+
## Supported tools
|
|
69
|
+
|
|
70
|
+
Usage is read via [`ccusage`](https://www.npmjs.com/package/ccusage) from any of these agents you use locally:
|
|
71
|
+
|
|
72
|
+
`claude` · `codex` · `gemini` · `copilot` · `opencode` · `amp` · `droid` · `goose` · `kimi` · `qwen` · `kilo` · `openclaw` · `hermes` · `pi` · `codebuff`
|
|
73
|
+
|
|
74
|
+
Whatever you have logs for is picked up automatically — you don't pass any flags.
|
|
75
|
+
|
|
76
|
+
## Claim a public handle (optional)
|
|
77
|
+
|
|
78
|
+
Want on the public leaderboard? Sign in with **GitHub or Google**:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
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.
|
|
85
|
+
|
|
86
|
+
## Keep it live
|
|
87
|
+
|
|
88
|
+
Want your dashboard to stay fresh without re-running by hand?
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx whoburnedmore install-sync # background sync every 3h (launchd / cron / scheduled task)
|
|
92
|
+
npx whoburnedmore uninstall-sync # remove it
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
- 🏆 Leaderboard — **[whoburnedmore.com](https://whoburnedmore.com)**
|
|
98
|
+
- 📊 Your dashboard — printed in the terminal after your first run
|
|
99
|
+
|
|
100
|
+
## Requirements
|
|
101
|
+
|
|
102
|
+
Node.js >= 20.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT © Arham Wani
|
package/dist/index.js
CHANGED
|
@@ -8,12 +8,23 @@ var __export = (target, all) => {
|
|
|
8
8
|
// src/index.ts
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
11
|
-
import { createRequire as
|
|
12
|
-
import { platform as
|
|
13
|
-
import { join as
|
|
11
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
12
|
+
import { platform as platform3 } from "node:os";
|
|
13
|
+
import { join as join6 } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
15
15
|
import pc2 from "picocolors";
|
|
16
16
|
|
|
17
|
+
// src/args.ts
|
|
18
|
+
function parseBoard(args) {
|
|
19
|
+
const eq = args.find((a) => a.startsWith("--board="));
|
|
20
|
+
if (eq) return eq.slice("--board=".length).trim() || void 0;
|
|
21
|
+
const i = args.indexOf("--board");
|
|
22
|
+
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
23
|
+
return args[i + 1].trim() || void 0;
|
|
24
|
+
}
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
// src/api.ts
|
|
18
29
|
function apiBase() {
|
|
19
30
|
return process.env.WHOBURNEDMORE_API ?? "https://api.whoburnedmore.com";
|
|
@@ -67,6 +78,29 @@ async function anonSubmit(anonKey, payload) {
|
|
|
67
78
|
}
|
|
68
79
|
return body;
|
|
69
80
|
}
|
|
81
|
+
function claimUrl(dashboardUrl, anonKey) {
|
|
82
|
+
return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
|
|
83
|
+
}
|
|
84
|
+
async function anonVisibility(anonKey, listed) {
|
|
85
|
+
const { status, body } = await post(
|
|
86
|
+
"/v1/anon/visibility",
|
|
87
|
+
{ anonKey, listed }
|
|
88
|
+
);
|
|
89
|
+
if (status !== 200) {
|
|
90
|
+
throw new Error(body.error ?? `failed (HTTP ${status})`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function anonRemove(anonKey) {
|
|
94
|
+
const res = await fetch(`${apiBase()}/v1/anon`, {
|
|
95
|
+
method: "DELETE",
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ anonKey })
|
|
98
|
+
});
|
|
99
|
+
if (res.status !== 200) {
|
|
100
|
+
const b = await res.json().catch(() => ({}));
|
|
101
|
+
throw new Error(b.error ?? `failed (HTTP ${res.status})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
70
104
|
|
|
71
105
|
// src/autosync.ts
|
|
72
106
|
import { spawnSync } from "node:child_process";
|
|
@@ -232,9 +266,259 @@ function autoSyncInstalled() {
|
|
|
232
266
|
}
|
|
233
267
|
|
|
234
268
|
// src/collect.ts
|
|
269
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
270
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
271
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
272
|
+
|
|
273
|
+
// src/cursor.ts
|
|
274
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
275
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
276
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
277
|
+
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
278
|
+
import { join as join4 } from "node:path";
|
|
279
|
+
|
|
280
|
+
// src/tokscale.ts
|
|
235
281
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
236
282
|
import { createRequire } from "node:module";
|
|
237
283
|
import { dirname, join as join3 } from "node:path";
|
|
284
|
+
var LOOKBACK_DAYS = 30;
|
|
285
|
+
function num(n) {
|
|
286
|
+
const v = Math.round(Number(n));
|
|
287
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
288
|
+
}
|
|
289
|
+
function numCost(n) {
|
|
290
|
+
const v = Number(n);
|
|
291
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
292
|
+
}
|
|
293
|
+
function mapTokscaleDay(date, json) {
|
|
294
|
+
const entries = json?.entries;
|
|
295
|
+
if (!Array.isArray(entries)) return [];
|
|
296
|
+
const out = [];
|
|
297
|
+
for (const e of entries) {
|
|
298
|
+
const inputTokens = num(e.input);
|
|
299
|
+
const outputTokens = num(e.output) + num(e.reasoning);
|
|
300
|
+
const cacheCreationTokens = num(e.cacheWrite);
|
|
301
|
+
const cacheReadTokens = num(e.cacheRead);
|
|
302
|
+
const costUSD = numCost(e.cost);
|
|
303
|
+
const total = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
|
|
304
|
+
if (total === 0 && costUSD === 0) continue;
|
|
305
|
+
out.push({
|
|
306
|
+
date,
|
|
307
|
+
tool: "cursor",
|
|
308
|
+
model: typeof e.model === "string" && e.model ? e.model : "cursor",
|
|
309
|
+
inputTokens,
|
|
310
|
+
outputTokens,
|
|
311
|
+
cacheCreationTokens,
|
|
312
|
+
cacheReadTokens,
|
|
313
|
+
costUSD,
|
|
314
|
+
origin: "cli",
|
|
315
|
+
verified: false
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
function resolveTokscaleBin() {
|
|
321
|
+
try {
|
|
322
|
+
const require3 = createRequire(import.meta.url);
|
|
323
|
+
const pkgPath = require3.resolve("tokscale/package.json");
|
|
324
|
+
const pkg = require3("tokscale/package.json");
|
|
325
|
+
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
|
|
326
|
+
if (!rel) return null;
|
|
327
|
+
const binPath = join3(dirname(pkgPath), rel);
|
|
328
|
+
if (/\.(c|m)?js$/.test(binPath)) {
|
|
329
|
+
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
330
|
+
}
|
|
331
|
+
return { cmd: binPath, prefixArgs: [] };
|
|
332
|
+
} catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function runTokscaleDay(bin, day) {
|
|
337
|
+
const res = spawnSync2(
|
|
338
|
+
bin.cmd,
|
|
339
|
+
[
|
|
340
|
+
...bin.prefixArgs,
|
|
341
|
+
"--client",
|
|
342
|
+
"cursor",
|
|
343
|
+
"--json",
|
|
344
|
+
"--since",
|
|
345
|
+
day,
|
|
346
|
+
"--until",
|
|
347
|
+
day,
|
|
348
|
+
"--group-by",
|
|
349
|
+
"model",
|
|
350
|
+
"--no-spinner"
|
|
351
|
+
],
|
|
352
|
+
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 6e4 }
|
|
353
|
+
);
|
|
354
|
+
if (res.status !== 0 || !res.stdout) return null;
|
|
355
|
+
try {
|
|
356
|
+
return JSON.parse(res.stdout);
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
|
|
362
|
+
const bin = resolveTokscaleBin();
|
|
363
|
+
if (!bin) return [];
|
|
364
|
+
const today = /* @__PURE__ */ new Date();
|
|
365
|
+
const day = (offset) => new Date(today.getTime() - offset * 864e5).toISOString().slice(0, 10);
|
|
366
|
+
if (mapTokscaleDay(day(0), runTokscaleDay(bin, day(0))).length === 0) {
|
|
367
|
+
if (mapTokscaleDay(day(1), runTokscaleDay(bin, day(1))).length === 0) {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const out = [];
|
|
372
|
+
for (let i = 0; i < lookbackDays; i++) {
|
|
373
|
+
const d = day(i);
|
|
374
|
+
out.push(...mapTokscaleDay(d, runTokscaleDay(bin, d)));
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/cursor.ts
|
|
380
|
+
var EVENTS_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events";
|
|
381
|
+
function cursorDbPath() {
|
|
382
|
+
const home = homedir3();
|
|
383
|
+
const os = platform2();
|
|
384
|
+
const p = os === "darwin" ? join4(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join4(process.env.APPDATA ?? join4(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join4(process.env.XDG_CONFIG_HOME ?? join4(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
|
|
385
|
+
return existsSync3(p) ? p : null;
|
|
386
|
+
}
|
|
387
|
+
function readCursorToken(db) {
|
|
388
|
+
const require3 = createRequire2(import.meta.url);
|
|
389
|
+
try {
|
|
390
|
+
const { DatabaseSync } = require3("node:sqlite");
|
|
391
|
+
const d = new DatabaseSync(db, { readOnly: true });
|
|
392
|
+
const row = d.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken");
|
|
393
|
+
d.close();
|
|
394
|
+
if (row?.value) return String(row.value);
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const res = spawnSync3(
|
|
399
|
+
"sqlite3",
|
|
400
|
+
[db, "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken';"],
|
|
401
|
+
{ encoding: "utf8", timeout: 1e4 }
|
|
402
|
+
);
|
|
403
|
+
const out = res.stdout?.trim();
|
|
404
|
+
if (res.status === 0 && out) return out;
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
function cursorCookie(token) {
|
|
410
|
+
try {
|
|
411
|
+
const part = token.split(".")[1];
|
|
412
|
+
if (!part) return null;
|
|
413
|
+
const json = JSON.parse(
|
|
414
|
+
Buffer.from(part, "base64url").toString("utf8")
|
|
415
|
+
);
|
|
416
|
+
if (!json.sub) return null;
|
|
417
|
+
return `WorkosCursorSessionToken=${json.sub}%3A%3A${token}`;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function num2(n) {
|
|
423
|
+
const v = Math.round(Number(n));
|
|
424
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
425
|
+
}
|
|
426
|
+
function mapCursorEvents(events) {
|
|
427
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
428
|
+
const byHour = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const e of events) {
|
|
430
|
+
const tu = e.tokenUsage;
|
|
431
|
+
const ms = Number(e.timestamp);
|
|
432
|
+
if (!tu || !Number.isFinite(ms)) continue;
|
|
433
|
+
const d = new Date(ms);
|
|
434
|
+
const date = d.toISOString().slice(0, 10);
|
|
435
|
+
const model = e.model || "cursor";
|
|
436
|
+
const input = num2(tu.inputTokens);
|
|
437
|
+
const output = num2(tu.outputTokens);
|
|
438
|
+
const cacheWrite = num2(tu.cacheWriteTokens);
|
|
439
|
+
const cacheRead = num2(tu.cacheReadTokens);
|
|
440
|
+
const cost = Math.max(0, (Number(tu.totalCents) || 0) / 100);
|
|
441
|
+
const total = input + output + cacheWrite + cacheRead;
|
|
442
|
+
if (total === 0 && cost === 0) continue;
|
|
443
|
+
const key = `${date}|${model}`;
|
|
444
|
+
const day = byDay.get(key) ?? {
|
|
445
|
+
date,
|
|
446
|
+
tool: "cursor",
|
|
447
|
+
model,
|
|
448
|
+
inputTokens: 0,
|
|
449
|
+
outputTokens: 0,
|
|
450
|
+
cacheCreationTokens: 0,
|
|
451
|
+
cacheReadTokens: 0,
|
|
452
|
+
costUSD: 0,
|
|
453
|
+
origin: "cli",
|
|
454
|
+
verified: false
|
|
455
|
+
};
|
|
456
|
+
day.inputTokens += input;
|
|
457
|
+
day.outputTokens += output;
|
|
458
|
+
day.cacheCreationTokens += cacheWrite;
|
|
459
|
+
day.cacheReadTokens += cacheRead;
|
|
460
|
+
day.costUSD += cost;
|
|
461
|
+
byDay.set(key, day);
|
|
462
|
+
const hour = new Date(
|
|
463
|
+
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours())
|
|
464
|
+
).toISOString();
|
|
465
|
+
const blk = byHour.get(hour) ?? { startTime: hour, totalTokens: 0, costUSD: 0 };
|
|
466
|
+
blk.totalTokens += total;
|
|
467
|
+
blk.costUSD += cost;
|
|
468
|
+
byHour.set(hour, blk);
|
|
469
|
+
}
|
|
470
|
+
const entries = [...byDay.values()].map((e) => ({
|
|
471
|
+
...e,
|
|
472
|
+
costUSD: Number(e.costUSD.toFixed(4))
|
|
473
|
+
}));
|
|
474
|
+
const blocks = [...byHour.values()].map((b) => ({
|
|
475
|
+
...b,
|
|
476
|
+
costUSD: Number(b.costUSD.toFixed(4))
|
|
477
|
+
}));
|
|
478
|
+
return { entries, blocks };
|
|
479
|
+
}
|
|
480
|
+
async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
|
|
481
|
+
const all = [];
|
|
482
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
483
|
+
const res = await fetch(EVENTS_URL, {
|
|
484
|
+
method: "POST",
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
Origin: "https://cursor.com",
|
|
488
|
+
Cookie: cookie
|
|
489
|
+
},
|
|
490
|
+
body: JSON.stringify({ page, pageSize }),
|
|
491
|
+
signal: AbortSignal.timeout(2e4)
|
|
492
|
+
});
|
|
493
|
+
if (!res.ok) break;
|
|
494
|
+
const body = await res.json();
|
|
495
|
+
const batch = body.usageEventsDisplay ?? [];
|
|
496
|
+
all.push(...batch);
|
|
497
|
+
if (batch.length < pageSize) break;
|
|
498
|
+
}
|
|
499
|
+
return all;
|
|
500
|
+
}
|
|
501
|
+
async function collectCursor() {
|
|
502
|
+
try {
|
|
503
|
+
const db = cursorDbPath();
|
|
504
|
+
const token = db ? readCursorToken(db) : null;
|
|
505
|
+
const cookie = token ? cursorCookie(token) : null;
|
|
506
|
+
if (cookie) {
|
|
507
|
+
const events = await fetchCursorEvents(cookie);
|
|
508
|
+
const { entries, blocks } = mapCursorEvents(events);
|
|
509
|
+
if (entries.length > 0) return { entries, blocks, found: true };
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const entries = collectCursorViaTokscale();
|
|
515
|
+
if (entries.length > 0) return { entries, blocks: [], found: true };
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
return { entries: [], blocks: [], found: false };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/collect.ts
|
|
238
522
|
var SOURCES = [
|
|
239
523
|
"claude",
|
|
240
524
|
"codex",
|
|
@@ -315,43 +599,110 @@ function mapCcusageDaily(tool, json) {
|
|
|
315
599
|
for (const c of candidates) {
|
|
316
600
|
const tokens = c.inputTokens + c.outputTokens + c.cacheCreationTokens + c.cacheReadTokens;
|
|
317
601
|
if (tokens === 0 && c.costUSD === 0) continue;
|
|
318
|
-
entries.push({ date, tool, ...c });
|
|
602
|
+
entries.push({ date, tool, ...c, origin: "cli", verified: false });
|
|
319
603
|
}
|
|
320
604
|
}
|
|
321
605
|
return entries;
|
|
322
606
|
}
|
|
607
|
+
function mapCcusageSessions(json) {
|
|
608
|
+
const sessions = json?.session;
|
|
609
|
+
if (!Array.isArray(sessions)) return [];
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const raw of sessions) {
|
|
612
|
+
const s = raw;
|
|
613
|
+
const sessionId = typeof s.period === "string" ? s.period : typeof s.sessionId === "string" ? s.sessionId : null;
|
|
614
|
+
if (!sessionId) continue;
|
|
615
|
+
const inputTokens = norm(s.inputTokens);
|
|
616
|
+
const outputTokens = norm(s.outputTokens);
|
|
617
|
+
const cacheCreationTokens = norm(s.cacheCreationTokens);
|
|
618
|
+
const cacheReadTokens = norm(s.cacheReadTokens);
|
|
619
|
+
if (inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens === 0)
|
|
620
|
+
continue;
|
|
621
|
+
const models = Array.isArray(s.modelsUsed) ? s.modelsUsed.filter((m) => typeof m === "string") : [];
|
|
622
|
+
const meta = s.metadata ?? {};
|
|
623
|
+
out.push({
|
|
624
|
+
sessionId: sessionId.slice(0, 128),
|
|
625
|
+
tool: typeof s.agent === "string" ? s.agent : "unknown",
|
|
626
|
+
model: models[0] ?? "unknown",
|
|
627
|
+
inputTokens,
|
|
628
|
+
outputTokens,
|
|
629
|
+
cacheCreationTokens,
|
|
630
|
+
cacheReadTokens,
|
|
631
|
+
costUSD: normCost(s.totalCost ?? s.costUSD),
|
|
632
|
+
lastActivity: typeof meta.lastActivity === "string" ? meta.lastActivity : (/* @__PURE__ */ new Date()).toISOString()
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
function mapCcusageBlocks(json) {
|
|
638
|
+
const blocks = json?.blocks;
|
|
639
|
+
if (!Array.isArray(blocks)) return [];
|
|
640
|
+
const out = [];
|
|
641
|
+
for (const raw of blocks) {
|
|
642
|
+
const b = raw;
|
|
643
|
+
if (b.isGap === true) continue;
|
|
644
|
+
if (typeof b.startTime !== "string") continue;
|
|
645
|
+
const totalTokens = norm(b.totalTokens);
|
|
646
|
+
const costUSD = normCost(b.costUSD);
|
|
647
|
+
if (totalTokens === 0 && costUSD === 0) continue;
|
|
648
|
+
out.push({ startTime: b.startTime, totalTokens, costUSD });
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
323
652
|
function resolveCcusageBin() {
|
|
324
|
-
const require3 =
|
|
653
|
+
const require3 = createRequire3(import.meta.url);
|
|
325
654
|
const pkgPath = require3.resolve("ccusage/package.json");
|
|
326
655
|
const pkg = require3("ccusage/package.json");
|
|
327
656
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
|
|
328
|
-
const binPath =
|
|
657
|
+
const binPath = join5(dirname2(pkgPath), rel);
|
|
329
658
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
330
659
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
331
660
|
}
|
|
332
661
|
return { cmd: binPath, prefixArgs: [] };
|
|
333
662
|
}
|
|
334
|
-
function
|
|
663
|
+
function runCcusage(cmd, args) {
|
|
664
|
+
const res = spawnSync4(cmd, args, {
|
|
665
|
+
encoding: "utf8",
|
|
666
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
667
|
+
timeout: 12e4
|
|
668
|
+
});
|
|
669
|
+
if (res.status !== 0 || !res.stdout) return null;
|
|
670
|
+
try {
|
|
671
|
+
return JSON.parse(res.stdout);
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function collectAll() {
|
|
335
677
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
336
678
|
const entries = [];
|
|
337
679
|
const toolsFound = [];
|
|
338
680
|
for (const source of SOURCES) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
} catch {
|
|
681
|
+
const json = runCcusage(cmd, [
|
|
682
|
+
...prefixArgs,
|
|
683
|
+
source,
|
|
684
|
+
"daily",
|
|
685
|
+
"--json",
|
|
686
|
+
"--offline"
|
|
687
|
+
]);
|
|
688
|
+
if (!json) continue;
|
|
689
|
+
const mapped = mapCcusageDaily(source, json);
|
|
690
|
+
if (mapped.length > 0) {
|
|
691
|
+
entries.push(...mapped);
|
|
692
|
+
toolsFound.push(source);
|
|
352
693
|
}
|
|
353
694
|
}
|
|
354
|
-
|
|
695
|
+
const sessionJson = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]);
|
|
696
|
+
const blockJson = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]);
|
|
697
|
+
const sessions = sessionJson ? mapCcusageSessions(sessionJson) : [];
|
|
698
|
+
const blocks = blockJson ? mapCcusageBlocks(blockJson) : [];
|
|
699
|
+
const cursor = await collectCursor();
|
|
700
|
+
if (cursor.found) {
|
|
701
|
+
entries.push(...cursor.entries);
|
|
702
|
+
blocks.push(...cursor.blocks);
|
|
703
|
+
toolsFound.push("cursor");
|
|
704
|
+
}
|
|
705
|
+
return { entries, sessions, blocks, toolsFound };
|
|
355
706
|
}
|
|
356
707
|
|
|
357
708
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
@@ -4398,6 +4749,22 @@ var NEVER = INVALID;
|
|
|
4398
4749
|
// ../shared/dist/index.js
|
|
4399
4750
|
var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
|
|
4400
4751
|
var tokenCount = external_exports.number().int().nonnegative();
|
|
4752
|
+
var ConnectorProvider = external_exports.enum([
|
|
4753
|
+
"anthropic-api",
|
|
4754
|
+
"openai-api",
|
|
4755
|
+
"openrouter",
|
|
4756
|
+
"google-api",
|
|
4757
|
+
"cursor"
|
|
4758
|
+
]);
|
|
4759
|
+
var UsageOrigin = external_exports.enum([
|
|
4760
|
+
"cli",
|
|
4761
|
+
"anthropic-api",
|
|
4762
|
+
"openai-api",
|
|
4763
|
+
"openrouter",
|
|
4764
|
+
"google-api",
|
|
4765
|
+
"cursor",
|
|
4766
|
+
"import"
|
|
4767
|
+
]);
|
|
4401
4768
|
var DailyUsageEntry = external_exports.object({
|
|
4402
4769
|
date: DateString,
|
|
4403
4770
|
/** Source agent id, e.g. "claude", "codex", "gemini", "copilot". */
|
|
@@ -4409,11 +4776,38 @@ var DailyUsageEntry = external_exports.object({
|
|
|
4409
4776
|
cacheCreationTokens: tokenCount,
|
|
4410
4777
|
cacheReadTokens: tokenCount,
|
|
4411
4778
|
/** Estimated cost in USD for this entry. */
|
|
4779
|
+
costUSD: external_exports.number().nonnegative(),
|
|
4780
|
+
/** Where this entry came from. Defaults to the local CLI for back-compat. */
|
|
4781
|
+
origin: UsageOrigin.default("cli"),
|
|
4782
|
+
/** True when the numbers come from a provider's authoritative usage API. */
|
|
4783
|
+
verified: external_exports.boolean().default(false)
|
|
4784
|
+
});
|
|
4785
|
+
var Timestamp = external_exports.string().min(1).max(40);
|
|
4786
|
+
var SessionEntry = external_exports.object({
|
|
4787
|
+
sessionId: external_exports.string().min(1).max(128),
|
|
4788
|
+
tool: external_exports.string().min(1).max(64),
|
|
4789
|
+
model: external_exports.string().min(1).max(128),
|
|
4790
|
+
inputTokens: tokenCount,
|
|
4791
|
+
outputTokens: tokenCount,
|
|
4792
|
+
cacheCreationTokens: tokenCount,
|
|
4793
|
+
cacheReadTokens: tokenCount,
|
|
4794
|
+
costUSD: external_exports.number().nonnegative(),
|
|
4795
|
+
lastActivity: Timestamp
|
|
4796
|
+
});
|
|
4797
|
+
var BlockEntry = external_exports.object({
|
|
4798
|
+
startTime: Timestamp,
|
|
4799
|
+
totalTokens: tokenCount,
|
|
4412
4800
|
costUSD: external_exports.number().nonnegative()
|
|
4413
4801
|
});
|
|
4414
4802
|
var SubmitPayload = external_exports.object({
|
|
4415
4803
|
cliVersion: external_exports.string().min(1).max(32),
|
|
4416
|
-
entries: external_exports.array(DailyUsageEntry).min(1).max(2e4)
|
|
4804
|
+
entries: external_exports.array(DailyUsageEntry).min(1).max(2e4),
|
|
4805
|
+
/** Optional per-conversation rollups (ccusage session). Back-compat: omittable. */
|
|
4806
|
+
sessions: external_exports.array(SessionEntry).max(1e4).optional(),
|
|
4807
|
+
/** Optional time-window rollups (ccusage blocks) for peak-hours analysis. */
|
|
4808
|
+
blocks: external_exports.array(BlockEntry).max(1e4).optional(),
|
|
4809
|
+
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
4810
|
+
board: external_exports.string().min(1).max(32).optional()
|
|
4417
4811
|
});
|
|
4418
4812
|
var AnonSubmitPayload = SubmitPayload.extend({
|
|
4419
4813
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
@@ -4623,11 +5017,27 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
|
|
|
4623
5017
|
`;
|
|
4624
5018
|
}
|
|
4625
5019
|
|
|
5020
|
+
// src/publish.ts
|
|
5021
|
+
async function publishLocal(payload, deps) {
|
|
5022
|
+
if (!await deps.confirm(" Publish this to the public leaderboard?")) {
|
|
5023
|
+
deps.log(" Kept local. Nothing left your machine.");
|
|
5024
|
+
return false;
|
|
5025
|
+
}
|
|
5026
|
+
const key = deps.ensureAnonKey();
|
|
5027
|
+
const res = await deps.anonSubmit(key, payload);
|
|
5028
|
+
deps.log(` Published \u2014 you're on the board: ${res.dashboardUrl}`);
|
|
5029
|
+
deps.openBrowser(claimUrl(res.dashboardUrl, key));
|
|
5030
|
+
deps.log(
|
|
5031
|
+
" Sign in there to claim it, or `npx whoburnedmore private` to hide it again."
|
|
5032
|
+
);
|
|
5033
|
+
return true;
|
|
5034
|
+
}
|
|
5035
|
+
|
|
4626
5036
|
// src/index.ts
|
|
4627
|
-
var require2 =
|
|
5037
|
+
var require2 = createRequire4(import.meta.url);
|
|
4628
5038
|
var VERSION = require2("../package.json").version;
|
|
4629
5039
|
function openBrowser(url) {
|
|
4630
|
-
const os =
|
|
5040
|
+
const os = platform3();
|
|
4631
5041
|
const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
4632
5042
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
4633
5043
|
}
|
|
@@ -4662,7 +5072,7 @@ async function confirm(question) {
|
|
|
4662
5072
|
function showLocalDashboard(entries) {
|
|
4663
5073
|
const dir = defaultConfigDir();
|
|
4664
5074
|
mkdirSync3(dir, { recursive: true });
|
|
4665
|
-
const file =
|
|
5075
|
+
const file = join6(dir, "dashboard.html");
|
|
4666
5076
|
writeFileSync3(file, renderDashboardHtml(entries));
|
|
4667
5077
|
console.log();
|
|
4668
5078
|
console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
|
|
@@ -4672,8 +5082,9 @@ function showLocalDashboard(entries) {
|
|
|
4672
5082
|
async function run(flags) {
|
|
4673
5083
|
if (!flags.quiet) {
|
|
4674
5084
|
console.log(pc2.dim(`whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
|
|
5085
|
+
console.log(pc2.dim(" Calculating your burn from local usage\u2026"));
|
|
4675
5086
|
}
|
|
4676
|
-
const { entries, toolsFound } = collectAll();
|
|
5087
|
+
const { entries, sessions, blocks, toolsFound } = await collectAll();
|
|
4677
5088
|
if (entries.length === 0) {
|
|
4678
5089
|
console.log();
|
|
4679
5090
|
console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
|
|
@@ -4681,6 +5092,9 @@ async function run(flags) {
|
|
|
4681
5092
|
return;
|
|
4682
5093
|
}
|
|
4683
5094
|
const payload = { cliVersion: VERSION, entries };
|
|
5095
|
+
if (sessions.length > 0) payload.sessions = sessions;
|
|
5096
|
+
if (blocks.length > 0) payload.blocks = blocks;
|
|
5097
|
+
if (flags.board) payload.board = flags.board;
|
|
4684
5098
|
if (flags.dryRun) {
|
|
4685
5099
|
console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
4686
5100
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -4689,6 +5103,15 @@ async function run(flags) {
|
|
|
4689
5103
|
if (!flags.quiet) printSummary(entries);
|
|
4690
5104
|
if (flags.local) {
|
|
4691
5105
|
showLocalDashboard(entries);
|
|
5106
|
+
if (!flags.quiet && process.stdin.isTTY) {
|
|
5107
|
+
await publishLocal(payload, {
|
|
5108
|
+
confirm,
|
|
5109
|
+
ensureAnonKey,
|
|
5110
|
+
anonSubmit,
|
|
5111
|
+
openBrowser,
|
|
5112
|
+
log: (line) => console.log(pc2.dim(line))
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
4692
5115
|
return;
|
|
4693
5116
|
}
|
|
4694
5117
|
if (flags.noSubmit) {
|
|
@@ -4698,6 +5121,10 @@ async function run(flags) {
|
|
|
4698
5121
|
const config = loadConfig();
|
|
4699
5122
|
if (config?.token) {
|
|
4700
5123
|
const result = await submitUsage(config.token, payload);
|
|
5124
|
+
if (!flags.quiet) {
|
|
5125
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
5126
|
+
openBrowser(result.boardUrl ?? result.profileUrl);
|
|
5127
|
+
}
|
|
4701
5128
|
console.log(
|
|
4702
5129
|
` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
|
|
4703
5130
|
);
|
|
@@ -4707,28 +5134,46 @@ async function run(flags) {
|
|
|
4707
5134
|
);
|
|
4708
5135
|
}
|
|
4709
5136
|
console.log(` ${pc2.cyan(result.profileUrl)}`);
|
|
5137
|
+
if (result.boardUrl) {
|
|
5138
|
+
console.log(` \u{1F91D} You're on the friends board: ${pc2.cyan(result.boardUrl)}`);
|
|
5139
|
+
}
|
|
4710
5140
|
} else {
|
|
4711
|
-
const
|
|
5141
|
+
const anonKey = ensureAnonKey();
|
|
5142
|
+
const result = await anonSubmit(anonKey, payload);
|
|
5143
|
+
const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
|
|
5144
|
+
if (!flags.quiet) {
|
|
5145
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
5146
|
+
openBrowser(target);
|
|
5147
|
+
}
|
|
4712
5148
|
console.log(
|
|
4713
5149
|
` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
|
|
4714
5150
|
);
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
5151
|
+
if (result.boardUrl) {
|
|
5152
|
+
console.log(
|
|
5153
|
+
` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
|
|
5154
|
+
);
|
|
5155
|
+
console.log(` ${pc2.cyan(result.boardUrl)}`);
|
|
5156
|
+
console.log(pc2.dim(` Your dashboard: ${result.dashboardUrl}`));
|
|
5157
|
+
} else {
|
|
4720
5158
|
console.log(
|
|
4721
|
-
pc2.
|
|
5159
|
+
` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
|
|
4722
5160
|
);
|
|
5161
|
+
console.log(` ${pc2.cyan(result.dashboardUrl)}`);
|
|
5162
|
+
if (!flags.quiet) {
|
|
5163
|
+
console.log(
|
|
5164
|
+
pc2.dim(" Claim it (name + X) to own your rank, or make it private / remove it.")
|
|
5165
|
+
);
|
|
5166
|
+
console.log(
|
|
5167
|
+
pc2.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
|
|
5168
|
+
);
|
|
5169
|
+
}
|
|
4723
5170
|
}
|
|
4724
5171
|
}
|
|
4725
5172
|
console.log();
|
|
4726
|
-
if (!flags.quiet
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
console.log(pc2.dim(" OK \u2014 re-run `npx whoburnedmore` anytime, or `npx whoburnedmore install-sync` later."));
|
|
4731
|
-
}
|
|
5173
|
+
if (!flags.quiet) {
|
|
5174
|
+
console.log(
|
|
5175
|
+
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page keeps updating automatically (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update \xB7 `npx whoburnedmore install-sync` to keep it live in the background.")
|
|
5176
|
+
);
|
|
4732
5177
|
}
|
|
4733
5178
|
}
|
|
4734
5179
|
async function main() {
|
|
@@ -4744,7 +5189,8 @@ async function main() {
|
|
|
4744
5189
|
dryRun: args.includes("--dry-run"),
|
|
4745
5190
|
noSubmit: args.includes("--no-submit"),
|
|
4746
5191
|
local: args.includes("--local"),
|
|
4747
|
-
quiet: command === "sync"
|
|
5192
|
+
quiet: command === "sync",
|
|
5193
|
+
board: parseBoard(args)
|
|
4748
5194
|
};
|
|
4749
5195
|
switch (command) {
|
|
4750
5196
|
case "run":
|
|
@@ -4763,6 +5209,29 @@ async function main() {
|
|
|
4763
5209
|
console.log(" Logged out. Your leaderboard data is untouched.");
|
|
4764
5210
|
console.log(pc2.dim(" Delete your data anytime from your profile page."));
|
|
4765
5211
|
break;
|
|
5212
|
+
case "private":
|
|
5213
|
+
case "public": {
|
|
5214
|
+
const cfg = loadConfig();
|
|
5215
|
+
if (!cfg?.anonKey) {
|
|
5216
|
+
console.log(" No anonymous dashboard on this machine \u2014 run `npx whoburnedmore` first.");
|
|
5217
|
+
break;
|
|
5218
|
+
}
|
|
5219
|
+
await anonVisibility(cfg.anonKey, command === "public");
|
|
5220
|
+
console.log(
|
|
5221
|
+
command === "public" ? " Your dashboard is public on the leaderboard again." : " Your dashboard is now private \u2014 off the public leaderboard."
|
|
5222
|
+
);
|
|
5223
|
+
break;
|
|
5224
|
+
}
|
|
5225
|
+
case "remove": {
|
|
5226
|
+
const cfg = loadConfig();
|
|
5227
|
+
if (!cfg?.anonKey) {
|
|
5228
|
+
console.log(" No anonymous dashboard on this machine \u2014 nothing to remove.");
|
|
5229
|
+
break;
|
|
5230
|
+
}
|
|
5231
|
+
await anonRemove(cfg.anonKey);
|
|
5232
|
+
console.log(" Removed your anonymous dashboard and all its data.");
|
|
5233
|
+
break;
|
|
5234
|
+
}
|
|
4766
5235
|
case "install-sync":
|
|
4767
5236
|
console.log(` ${installAutoSync()}`);
|
|
4768
5237
|
break;
|
|
@@ -4789,19 +5258,24 @@ function printHelp() {
|
|
|
4789
5258
|
${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
|
|
4790
5259
|
|
|
4791
5260
|
${pc2.bold("usage")}
|
|
4792
|
-
npx whoburnedmore
|
|
5261
|
+
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
5262
|
+
npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
|
|
4793
5263
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
4794
5264
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
4795
5265
|
npx whoburnedmore --no-submit print local stats only, send nothing
|
|
5266
|
+
npx whoburnedmore private hide your anonymous dashboard from the leaderboard
|
|
5267
|
+
npx whoburnedmore public put it back on the leaderboard
|
|
5268
|
+
npx whoburnedmore remove delete your anonymous dashboard and its data
|
|
4796
5269
|
npx whoburnedmore login sign in to claim a public handle + join the leaderboard
|
|
4797
5270
|
npx whoburnedmore logout forget the local token
|
|
4798
5271
|
npx whoburnedmore install-sync keep your dashboard live (background sync, 3h)
|
|
4799
5272
|
npx whoburnedmore uninstall-sync remove background sync
|
|
4800
5273
|
|
|
4801
|
-
By default your dashboard is
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
file names. With --local,
|
|
5274
|
+
By default your dashboard is public on the leaderboard as an anonymous burner \u2014
|
|
5275
|
+
claim it (sign in for a handle + X) to own your rank, or run \`private\`/\`remove\`
|
|
5276
|
+
to pull it. Only daily aggregate numbers (date, tool, model, token counts, est.
|
|
5277
|
+
cost) ever leave your machine \u2014 never prompts, code, or file names. With --local,
|
|
5278
|
+
nothing leaves your machine at all.
|
|
4805
5279
|
`);
|
|
4806
5280
|
}
|
|
4807
5281
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whoburnedmore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Find out who burned more — submit your AI coding-agent token usage to the public leaderboard at whoburnedmore.com",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"dist/index.js"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:ccusage --external:picocolors",
|
|
13
|
+
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:ccusage --external:picocolors --external:tokscale",
|
|
14
14
|
"test": "vitest run",
|
|
15
15
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
16
16
|
"prepublishOnly": "pnpm run build",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"ccusage": "20.0.9",
|
|
23
|
-
"picocolors": "^1.1.1"
|
|
23
|
+
"picocolors": "^1.1.1",
|
|
24
|
+
"tokscale": "^1.2.7"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/node": "^22.10.0",
|