whoburnedmore 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -23
- package/dist/index.js +7 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# whoburnedmore
|
|
2
2
|
|
|
3
|
-
**who burned more tokens — you or them?** One command reads your local AI coding-agent usage logs,
|
|
3
|
+
**who burned more tokens — you or them?** One command reads your local AI coding-agent usage logs, adds up your burn, and puts you on the public leaderboard with a **shareable dashboard**.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/whoburnedmore)
|
|
6
6
|
[](https://www.npmjs.com/package/whoburnedmore)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
npx whoburnedmore
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
That's the whole thing. No install, no
|
|
14
|
+
That's the whole thing. No install, no config. Your **first run signs you in** — the CLI opens a page, you approve a short code, and this machine is bound to your account.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -28,13 +28,15 @@ whoburnedmore · https://whoburnedmore.com
|
|
|
28
28
|
total 2.31B tokens $2,656.91
|
|
29
29
|
today 92.8M tokens
|
|
30
30
|
|
|
31
|
-
You burned 2.31B tokens. Your
|
|
32
|
-
https://whoburnedmore.com/
|
|
31
|
+
You burned 2.31B tokens. Your dashboard:
|
|
32
|
+
https://whoburnedmore.com/u/<your-handle>
|
|
33
33
|
|
|
34
|
-
Re-run anytime to update it
|
|
34
|
+
Re-run anytime to update it. Background sync keeps it fresh every 15 min.
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Once you're signed in, your usage lands on the leaderboard under your handle. Run the command again anytime to refresh — or let the background sync do it for you.
|
|
38
|
+
|
|
39
|
+
**Want to stay off the board?** Run `npx whoburnedmore private` to hide your dashboard, or `npx whoburnedmore --local` to never upload anything at all.
|
|
38
40
|
|
|
39
41
|
## Privacy first
|
|
40
42
|
|
|
@@ -46,7 +48,7 @@ See exactly what would be sent, and send nothing:
|
|
|
46
48
|
npx whoburnedmore --dry-run
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
Or stay completely offline — `--local` builds the same dashboard as a self-contained HTML file on your machine and opens it in your browser, sending nothing anywhere:
|
|
51
|
+
Or stay completely offline — `--local` builds the same dashboard as a self-contained HTML file on your machine and opens it in your browser, sending nothing anywhere and never signing you in:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
npx whoburnedmore --local
|
|
@@ -56,14 +58,21 @@ npx whoburnedmore --local
|
|
|
56
58
|
|
|
57
59
|
| Command | What it does |
|
|
58
60
|
| --- | --- |
|
|
59
|
-
| `npx whoburnedmore` |
|
|
60
|
-
| `npx whoburnedmore --
|
|
61
|
+
| `npx whoburnedmore` | Sign in, submit your burn, land on the leaderboard, open your dashboard |
|
|
62
|
+
| `npx whoburnedmore --board=CODE` | Compare with friends — sign in and join their board |
|
|
63
|
+
| `npx whoburnedmore --org=SLUG` | Submit to your organization's board (companies / hackathons) |
|
|
64
|
+
| `npx whoburnedmore --local` | Build the dashboard locally and open it (fully offline, no sign-in) |
|
|
61
65
|
| `npx whoburnedmore --dry-run` | Print exactly what would be sent, send nothing |
|
|
62
66
|
| `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
|
|
63
|
-
| `npx whoburnedmore
|
|
64
|
-
| `npx whoburnedmore
|
|
65
|
-
| `npx whoburnedmore
|
|
66
|
-
| `npx whoburnedmore
|
|
67
|
+
| `npx whoburnedmore private` | Hide your dashboard from the leaderboard |
|
|
68
|
+
| `npx whoburnedmore public` | Put it back on the leaderboard |
|
|
69
|
+
| `npx whoburnedmore remove` | Delete your dashboard and all its data |
|
|
70
|
+
| `npx whoburnedmore verify` | Delisted? Re-verify your usage to get back on |
|
|
71
|
+
| `npx whoburnedmore link --token=TOKEN` | Bind a server / VM to your account (token from your profile) |
|
|
72
|
+
| `npx whoburnedmore daemon` | Keep syncing in the foreground (containers with no scheduler) |
|
|
73
|
+
| `npx whoburnedmore status` | Check background-sync health (last sync, staleness) |
|
|
74
|
+
| `npx whoburnedmore install-sync` | Turn the 15-minute background sync back on |
|
|
75
|
+
| `npx whoburnedmore uninstall-sync` | Turn the background sync off |
|
|
67
76
|
|
|
68
77
|
## Supported tools
|
|
69
78
|
|
|
@@ -73,29 +82,37 @@ Usage is read via [`ccusage`](https://www.npmjs.com/package/ccusage) from any of
|
|
|
73
82
|
|
|
74
83
|
Whatever you have logs for is picked up automatically — you don't pass any flags.
|
|
75
84
|
|
|
76
|
-
##
|
|
85
|
+
## Compete with friends and teams
|
|
77
86
|
|
|
78
|
-
|
|
87
|
+
- **Friends:** join someone's board with `npx whoburnedmore --board=CODE` (the code comes from their board's invite).
|
|
88
|
+
- **Companies / hackathons:** submit to an organization board with `npx whoburnedmore --org=SLUG` (add `--pass=CODE` if the org requires a join code).
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
npx whoburnedmore login
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
It uses a device flow — a code appears in your terminal, you approve it in the browser, and you're done. Your submissions then link to your handle at `whoburnedmore.com/u/<handle>`. You can delete your account and every row of data anytime from your profile.
|
|
90
|
+
Your submissions link to your handle at `whoburnedmore.com/u/<handle>`. You can delete your account and every row of data anytime from your profile.
|
|
85
91
|
|
|
86
92
|
## Keep it live
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
Background sync is on by default after your first run, so your dashboard stays fresh without re-running by hand. To manage it:
|
|
89
95
|
|
|
90
96
|
```bash
|
|
91
|
-
npx whoburnedmore
|
|
92
|
-
npx whoburnedmore uninstall-sync #
|
|
97
|
+
npx whoburnedmore status # last sync + staleness
|
|
98
|
+
npx whoburnedmore uninstall-sync # turn the background sync off
|
|
99
|
+
npx whoburnedmore install-sync # turn it back on (launchd / cron / scheduled task)
|
|
93
100
|
```
|
|
94
101
|
|
|
95
102
|
The installed background job runs the latest published `whoburnedmore` package on
|
|
96
103
|
each sync tick, so future CLI fixes are picked up automatically after the next
|
|
97
104
|
15-minute refresh.
|
|
98
105
|
|
|
106
|
+
## Servers & VMs
|
|
107
|
+
|
|
108
|
+
Generate a one-time `link` command from your profile on whoburnedmore.com and run it inside the machine to bind it to your account:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx whoburnedmore link --token=TOKEN
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
On a persistent VM, background sync uses cron or a systemd user timer automatically. In a container or any host without a scheduler, run `npx whoburnedmore daemon` under your process manager instead. Set `WHOBURNEDMORE_CONFIG_DIR` to a persistent path so the machine identity survives restarts.
|
|
115
|
+
|
|
99
116
|
## Links
|
|
100
117
|
|
|
101
118
|
- 🏆 Leaderboard — **[whoburnedmore.com](https://whoburnedmore.com)**
|
package/dist/index.js
CHANGED
|
@@ -127,8 +127,10 @@ async function verifyUsage(token, payload) {
|
|
|
127
127
|
}
|
|
128
128
|
return body;
|
|
129
129
|
}
|
|
130
|
+
var ANON_RETIRED_MESSAGE = "Anonymous mode has been retired. Run `npx whoburnedmore` and sign in, or `npx whoburnedmore link --token=\u2026` for a server/CI, to put your usage on the leaderboard.";
|
|
130
131
|
async function anonSubmit(anonKey, payload) {
|
|
131
132
|
const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
|
|
133
|
+
if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
|
|
132
134
|
if (status !== 200) {
|
|
133
135
|
const err = body;
|
|
134
136
|
const details = err.details?.length ? `
|
|
@@ -179,19 +181,12 @@ async function submit(token, payload) {
|
|
|
179
181
|
function claimUrl(dashboardUrl, anonKey) {
|
|
180
182
|
return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
|
|
181
183
|
}
|
|
182
|
-
function boardClaimUrl(boardUrl, slug, anonKey) {
|
|
183
|
-
return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
|
|
184
|
-
}
|
|
185
|
-
function resolveOpenTarget(result, anonKey) {
|
|
186
|
-
const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.dashboardUrl;
|
|
187
|
-
const target = result.orgBoardUrl ? boardClaimUrl(result.orgBoardUrl, result.slug, anonKey) : result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
|
|
188
|
-
return { baseUrl, target };
|
|
189
|
-
}
|
|
190
184
|
async function anonVisibility(anonKey, listed) {
|
|
191
185
|
const { status, body } = await post(
|
|
192
186
|
"/v1/anon/visibility",
|
|
193
187
|
{ anonKey, listed }
|
|
194
188
|
);
|
|
189
|
+
if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
|
|
195
190
|
if (status !== 200) {
|
|
196
191
|
throw new Error(body.error ?? `failed (HTTP ${status})`);
|
|
197
192
|
}
|
|
@@ -203,6 +198,7 @@ async function anonRemove(anonKey) {
|
|
|
203
198
|
body: JSON.stringify({ anonKey }),
|
|
204
199
|
signal: AbortSignal.timeout(3e4)
|
|
205
200
|
});
|
|
201
|
+
if (res.status === 410) throw new Error(ANON_RETIRED_MESSAGE);
|
|
206
202
|
if (res.status !== 200) {
|
|
207
203
|
const b = await res.json().catch(() => ({}));
|
|
208
204
|
throw new Error(b.error ?? `failed (HTTP ${res.status})`);
|
|
@@ -299,10 +295,6 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
|
|
|
299
295
|
const config = loadConfig(dir) ?? {};
|
|
300
296
|
saveConfig(dir, { ...config, lastSyncAt: when });
|
|
301
297
|
}
|
|
302
|
-
function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
|
|
303
|
-
const config = loadConfig(dir) ?? {};
|
|
304
|
-
saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
|
|
305
|
-
}
|
|
306
298
|
function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
|
|
307
299
|
const config = loadConfig(dir) ?? {};
|
|
308
300
|
saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
|
|
@@ -730,41 +722,6 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
|
|
|
730
722
|
return false;
|
|
731
723
|
}
|
|
732
724
|
}
|
|
733
|
-
function notifyLaunchLive(opts) {
|
|
734
|
-
const os = opts?.platform ?? platform();
|
|
735
|
-
const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
|
|
736
|
-
const title = "whoburnedmore is live";
|
|
737
|
-
const message = "Your dashboard is ready. Go to whoburnedmore.com";
|
|
738
|
-
if (os === "darwin") {
|
|
739
|
-
return run2("osascript", [
|
|
740
|
-
"-e",
|
|
741
|
-
`display notification "${message}" with title "${title}"`
|
|
742
|
-
]).status === 0;
|
|
743
|
-
}
|
|
744
|
-
if (os === "linux") {
|
|
745
|
-
return run2("notify-send", [title, message]).status === 0;
|
|
746
|
-
}
|
|
747
|
-
if (os === "win32") {
|
|
748
|
-
const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
749
|
-
const script = [
|
|
750
|
-
"Add-Type -AssemblyName System.Windows.Forms",
|
|
751
|
-
"$n = New-Object System.Windows.Forms.NotifyIcon",
|
|
752
|
-
"$n.Icon = [System.Drawing.SystemIcons]::Application",
|
|
753
|
-
`$n.BalloonTipTitle = ${psQuote(title)}`,
|
|
754
|
-
`$n.BalloonTipText = ${psQuote(message)}`,
|
|
755
|
-
"$n.Visible = $true",
|
|
756
|
-
"$n.ShowBalloonTip(10000)",
|
|
757
|
-
"Start-Sleep -Seconds 2",
|
|
758
|
-
"$n.Dispose()"
|
|
759
|
-
].join("; ");
|
|
760
|
-
return run2("powershell", [
|
|
761
|
-
"-NoProfile",
|
|
762
|
-
"-Command",
|
|
763
|
-
script
|
|
764
|
-
]).status === 0;
|
|
765
|
-
}
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
725
|
function autoSyncLoaded() {
|
|
769
726
|
if (platform() === "darwin") {
|
|
770
727
|
const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
|
|
@@ -6493,22 +6450,6 @@ function formatUSD(n) {
|
|
|
6493
6450
|
function sanitizeServerText(s) {
|
|
6494
6451
|
return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
6495
6452
|
}
|
|
6496
|
-
function submitNextStepLines(result) {
|
|
6497
|
-
if (result.boardUrl) {
|
|
6498
|
-
const code = result.boardCode ?? result.boardUrl.split("/").filter(Boolean).pop() ?? "";
|
|
6499
|
-
return [
|
|
6500
|
-
` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
|
|
6501
|
-
" \u2192 Open it to see who burned more.",
|
|
6502
|
-
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`,
|
|
6503
|
-
" \u2192 Sign in on the page and add your X to claim your spot and own your rank."
|
|
6504
|
-
];
|
|
6505
|
-
}
|
|
6506
|
-
return [
|
|
6507
|
-
` Your dashboard: ${sanitizeServerText(result.dashboardUrl)}`,
|
|
6508
|
-
" \u2192 Sign in and add your X on the page to get on the leaderboard and claim your rank.",
|
|
6509
|
-
" Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
|
|
6510
|
-
];
|
|
6511
|
-
}
|
|
6512
6453
|
function signedInNextStepLines(result) {
|
|
6513
6454
|
if (result.orgBoardUrl) {
|
|
6514
6455
|
return [
|
|
@@ -6910,8 +6851,6 @@ async function run(flags) {
|
|
|
6910
6851
|
const canSignIn = !flags.quiet && Boolean(process.stdout.isTTY);
|
|
6911
6852
|
if (cfg?.cliToken) {
|
|
6912
6853
|
await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
|
|
6913
|
-
} else if (cfg?.anonKey) {
|
|
6914
|
-
await submitWithDeviceKey(cfg.anonKey, payload, flags);
|
|
6915
6854
|
} else if (canSignIn) {
|
|
6916
6855
|
const auth = await ensureSignedIn();
|
|
6917
6856
|
if (!auth) return;
|
|
@@ -7031,44 +6970,6 @@ async function submitSignedIn(token, payload, flags, interactive) {
|
|
|
7031
6970
|
}
|
|
7032
6971
|
afterSubmitChores(flags);
|
|
7033
6972
|
}
|
|
7034
|
-
async function submitWithDeviceKey(anonKey, payload, flags) {
|
|
7035
|
-
const result = await anonSubmit(anonKey, payload);
|
|
7036
|
-
try {
|
|
7037
|
-
recordSync();
|
|
7038
|
-
} catch {
|
|
7039
|
-
}
|
|
7040
|
-
try {
|
|
7041
|
-
const config = loadConfig();
|
|
7042
|
-
if (result.launch?.live && !config?.launchNotificationDeliveredAt) {
|
|
7043
|
-
if (notifyLaunchLive()) {
|
|
7044
|
-
recordLaunchNotificationDelivered();
|
|
7045
|
-
}
|
|
7046
|
-
}
|
|
7047
|
-
} catch {
|
|
7048
|
-
}
|
|
7049
|
-
const { baseUrl, target } = resolveOpenTarget(result, anonKey);
|
|
7050
|
-
const trusted = isTrustedWebUrl(baseUrl);
|
|
7051
|
-
if (!flags.quiet) {
|
|
7052
|
-
console.log(
|
|
7053
|
-
pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
|
|
7054
|
-
);
|
|
7055
|
-
if (trusted) {
|
|
7056
|
-
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
7057
|
-
openBrowser(target);
|
|
7058
|
-
} else {
|
|
7059
|
-
console.log(
|
|
7060
|
-
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
7061
|
-
);
|
|
7062
|
-
console.log(` ${sanitizeServerText(baseUrl)}`);
|
|
7063
|
-
}
|
|
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);
|
|
7068
|
-
}
|
|
7069
|
-
}
|
|
7070
|
-
afterSubmitChores(flags);
|
|
7071
|
-
}
|
|
7072
6973
|
function afterSubmitChores(flags) {
|
|
7073
6974
|
if (flags.quiet) return;
|
|
7074
6975
|
try {
|
|
@@ -7086,6 +6987,9 @@ async function linkServerInstall(token) {
|
|
|
7086
6987
|
}
|
|
7087
6988
|
const anonKey = ensureAnonKey();
|
|
7088
6989
|
const linked = await redeemServerInstall(token, anonKey);
|
|
6990
|
+
if (linked.cliToken) {
|
|
6991
|
+
saveAuth(void 0, { cliToken: linked.cliToken, handle: linked.handle });
|
|
6992
|
+
}
|
|
7089
6993
|
const handle = sanitizeServerText(linked.handle);
|
|
7090
6994
|
console.log(
|
|
7091
6995
|
linked.alreadyLinked ? ` This machine is already linked to @${handle}.` : ` Linked this machine to @${handle}.`
|