whoburnedmore 0.8.7 → 0.8.9
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 +6 -2
- package/dist/index.js +387 -44
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ npx whoburnedmore --local
|
|
|
62
62
|
| `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
|
|
63
63
|
| `npx whoburnedmore login` | Sign in to claim a public handle + join the leaderboard |
|
|
64
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 (
|
|
65
|
+
| `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (every 15 min) |
|
|
66
66
|
| `npx whoburnedmore uninstall-sync` | Remove the background sync |
|
|
67
67
|
|
|
68
68
|
## Supported tools
|
|
@@ -88,10 +88,14 @@ It uses a device flow — a code appears in your terminal, you approve it in the
|
|
|
88
88
|
Want your dashboard to stay fresh without re-running by hand?
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
npx whoburnedmore install-sync # background sync
|
|
91
|
+
npx whoburnedmore install-sync # background sync every 15 min (launchd / cron / scheduled task)
|
|
92
92
|
npx whoburnedmore uninstall-sync # remove it
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
The installed background job runs the latest published `whoburnedmore` package on
|
|
96
|
+
each sync tick, so future CLI fixes are picked up automatically after the next
|
|
97
|
+
15-minute refresh.
|
|
98
|
+
|
|
95
99
|
## Links
|
|
96
100
|
|
|
97
101
|
- 🏆 Leaderboard — **[whoburnedmore.com](https://whoburnedmore.com)**
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,24 @@ import pc2 from "picocolors";
|
|
|
16
16
|
|
|
17
17
|
// src/args.ts
|
|
18
18
|
function parseBoard(args) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
return parseValueFlag(args, "--board");
|
|
20
|
+
}
|
|
21
|
+
function parseOrg(args) {
|
|
22
|
+
return parseValueFlag(args, "--org");
|
|
23
|
+
}
|
|
24
|
+
function parseInstallToken(args) {
|
|
25
|
+
return parseValueFlag(args, "--token");
|
|
26
|
+
}
|
|
27
|
+
function applyScope(payload, flags) {
|
|
28
|
+
if (flags.board) payload.board = flags.board;
|
|
29
|
+
if (flags.org) payload.org = flags.org;
|
|
30
|
+
return payload;
|
|
31
|
+
}
|
|
32
|
+
function parseValueFlag(args, name) {
|
|
33
|
+
const prefix = `${name}=`;
|
|
34
|
+
const eq = args.find((a) => a.startsWith(prefix));
|
|
35
|
+
if (eq) return eq.slice(prefix.length).trim() || void 0;
|
|
36
|
+
const i = args.indexOf(name);
|
|
22
37
|
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
23
38
|
return args[i + 1].trim() || void 0;
|
|
24
39
|
}
|
|
@@ -31,7 +46,16 @@ function resolveCommand(args) {
|
|
|
31
46
|
if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
|
|
32
47
|
return "version";
|
|
33
48
|
}
|
|
34
|
-
|
|
49
|
+
const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token"]);
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
const a = args[i];
|
|
52
|
+
if (a.startsWith("-")) {
|
|
53
|
+
if (valueFlags.has(a)) i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
return a;
|
|
57
|
+
}
|
|
58
|
+
return "run";
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
// src/api.ts
|
|
@@ -41,6 +65,20 @@ function apiBase() {
|
|
|
41
65
|
function webBase() {
|
|
42
66
|
return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
|
|
43
67
|
}
|
|
68
|
+
function isTrustedWebUrl(url) {
|
|
69
|
+
let u;
|
|
70
|
+
let base;
|
|
71
|
+
try {
|
|
72
|
+
u = new URL(url);
|
|
73
|
+
base = new URL(webBase());
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.protocol === base.protocol && u.host === base.host;
|
|
78
|
+
}
|
|
79
|
+
function isOpenableUrl(url) {
|
|
80
|
+
return /^(https?|file):\/\//.test(url);
|
|
81
|
+
}
|
|
44
82
|
async function readJson(res) {
|
|
45
83
|
const text = await res.text();
|
|
46
84
|
if (!text) return {};
|
|
@@ -103,6 +141,15 @@ async function anonRemove(anonKey) {
|
|
|
103
141
|
throw new Error(b.error ?? `failed (HTTP ${res.status})`);
|
|
104
142
|
}
|
|
105
143
|
}
|
|
144
|
+
async function redeemServerInstall(token, anonKey) {
|
|
145
|
+
const { status, body } = await post("/v1/server-install/redeem", { token, anonKey });
|
|
146
|
+
if (status !== 200) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
body.error ?? `server install failed (HTTP ${status})`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return body;
|
|
152
|
+
}
|
|
106
153
|
|
|
107
154
|
// src/autosync.ts
|
|
108
155
|
import { spawnSync } from "node:child_process";
|
|
@@ -116,8 +163,7 @@ import {
|
|
|
116
163
|
writeFileSync as writeFileSync2
|
|
117
164
|
} from "node:fs";
|
|
118
165
|
import { homedir as homedir2, platform } from "node:os";
|
|
119
|
-
import { join as join2 } from "node:path";
|
|
120
|
-
import { fileURLToPath } from "node:url";
|
|
166
|
+
import { dirname, join as join2, win32 } from "node:path";
|
|
121
167
|
|
|
122
168
|
// src/config.ts
|
|
123
169
|
import { randomBytes } from "node:crypto";
|
|
@@ -131,6 +177,8 @@ import {
|
|
|
131
177
|
import { homedir } from "node:os";
|
|
132
178
|
import { join } from "node:path";
|
|
133
179
|
function defaultConfigDir() {
|
|
180
|
+
const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
|
|
181
|
+
if (override) return override;
|
|
134
182
|
return join(homedir(), ".config", "whoburnedmore");
|
|
135
183
|
}
|
|
136
184
|
function loadConfig(dir = defaultConfigDir()) {
|
|
@@ -142,6 +190,9 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
142
190
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
143
191
|
if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
|
|
144
192
|
config.lastSyncAt = parsed.lastSyncAt;
|
|
193
|
+
if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
|
|
194
|
+
config.launchNotificationDeliveredAt = parsed.launchNotificationDeliveredAt;
|
|
195
|
+
}
|
|
145
196
|
return Object.keys(config).length > 0 ? config : null;
|
|
146
197
|
} catch {
|
|
147
198
|
return null;
|
|
@@ -167,19 +218,33 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
|
|
|
167
218
|
const config = loadConfig(dir) ?? {};
|
|
168
219
|
saveConfig(dir, { ...config, lastSyncAt: when });
|
|
169
220
|
}
|
|
221
|
+
function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
|
|
222
|
+
const config = loadConfig(dir) ?? {};
|
|
223
|
+
saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
|
|
224
|
+
}
|
|
170
225
|
|
|
171
226
|
// src/autosync.ts
|
|
172
|
-
var
|
|
227
|
+
var SYNC_INTERVAL_MINUTES = 15;
|
|
228
|
+
function syncIntervalLabel(mins = SYNC_INTERVAL_MINUTES) {
|
|
229
|
+
return mins % 60 === 0 ? `${mins / 60}h` : `${mins}m`;
|
|
230
|
+
}
|
|
173
231
|
var LABEL = "com.whoburnedmore.sync";
|
|
174
232
|
var STABLE_NODE_CANDIDATES = [
|
|
175
233
|
"/opt/homebrew/bin/node",
|
|
176
234
|
"/usr/local/bin/node",
|
|
177
235
|
"/usr/bin/node"
|
|
178
236
|
];
|
|
237
|
+
var STABLE_NPM_CANDIDATES = [
|
|
238
|
+
"/opt/homebrew/bin/npm",
|
|
239
|
+
"/usr/local/bin/npm",
|
|
240
|
+
"/usr/bin/npm"
|
|
241
|
+
];
|
|
242
|
+
var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
|
|
179
243
|
function syncLogPath() {
|
|
180
244
|
return join2(defaultConfigDir(), "sync.log");
|
|
181
245
|
}
|
|
182
|
-
function buildLaunchdPlist(
|
|
246
|
+
function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
|
|
247
|
+
const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
183
248
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
184
249
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
185
250
|
<plist version="1.0">
|
|
@@ -188,12 +253,10 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
188
253
|
<string>${LABEL}</string>
|
|
189
254
|
<key>ProgramArguments</key>
|
|
190
255
|
<array>
|
|
191
|
-
|
|
192
|
-
<string>${scriptPath}</string>
|
|
193
|
-
<string>sync</string>
|
|
256
|
+
${programArguments}
|
|
194
257
|
</array>
|
|
195
258
|
<key>StartInterval</key>
|
|
196
|
-
<integer>${
|
|
259
|
+
<integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
|
|
197
260
|
<!-- Run once right after login/reboot so a machine that was off (or asleep)
|
|
198
261
|
through a scheduled tick catches up immediately, then keeps to the
|
|
199
262
|
interval. Submits are idempotent server-side, so an extra run is safe. -->
|
|
@@ -203,9 +266,9 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
203
266
|
<key>ProcessType</key>
|
|
204
267
|
<string>Background</string>
|
|
205
268
|
<key>StandardOutPath</key>
|
|
206
|
-
<string>${logPath}</string>
|
|
269
|
+
<string>${xmlEscape(logPath)}</string>
|
|
207
270
|
<key>StandardErrorPath</key>
|
|
208
|
-
<string>${logPath}</string>
|
|
271
|
+
<string>${xmlEscape(logPath)}</string>
|
|
209
272
|
</dict>
|
|
210
273
|
</plist>
|
|
211
274
|
`;
|
|
@@ -213,9 +276,6 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
|
|
|
213
276
|
function launchAgentPath() {
|
|
214
277
|
return join2(homedir2(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
215
278
|
}
|
|
216
|
-
function cliScriptPath() {
|
|
217
|
-
return fileURLToPath(new URL("./index.js", import.meta.url));
|
|
218
|
-
}
|
|
219
279
|
function isUsableNode(p) {
|
|
220
280
|
if (!existsSync2(p)) return false;
|
|
221
281
|
const res = spawnSync(p, ["-v"], { encoding: "utf8" });
|
|
@@ -232,8 +292,51 @@ function resolveNodePath(opts) {
|
|
|
232
292
|
}
|
|
233
293
|
return execPath;
|
|
234
294
|
}
|
|
295
|
+
function isUsableNpm(p) {
|
|
296
|
+
if (!existsSync2(p)) return false;
|
|
297
|
+
const res = spawnSync(p, ["--version"], { encoding: "utf8" });
|
|
298
|
+
return res.status === 0;
|
|
299
|
+
}
|
|
300
|
+
function resolveNpmPath(opts) {
|
|
301
|
+
const candidates = opts?.candidates ?? STABLE_NPM_CANDIDATES;
|
|
302
|
+
const check = opts?.check ?? isUsableNpm;
|
|
303
|
+
const execPath = opts?.execPath ?? process.execPath;
|
|
304
|
+
const os = opts?.platform ?? platform();
|
|
305
|
+
for (const c of candidates) {
|
|
306
|
+
if (check(c)) return c;
|
|
307
|
+
}
|
|
308
|
+
const sibling = os === "win32" ? win32.join(win32.dirname(execPath), "npm.cmd") : join2(dirname(execPath), "npm");
|
|
309
|
+
if (check(sibling)) return sibling;
|
|
310
|
+
return os === "win32" ? "npm.cmd" : "npm";
|
|
311
|
+
}
|
|
312
|
+
function syncCommandArgs(npmPath = resolveNpmPath()) {
|
|
313
|
+
return [
|
|
314
|
+
npmPath,
|
|
315
|
+
"exec",
|
|
316
|
+
"--yes",
|
|
317
|
+
"--ignore-scripts",
|
|
318
|
+
"--package",
|
|
319
|
+
LATEST_PACKAGE_SPEC,
|
|
320
|
+
"--",
|
|
321
|
+
"whoburnedmore",
|
|
322
|
+
"sync"
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
function xmlEscape(value) {
|
|
326
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll('"', """).replaceAll("'", "'").replaceAll(">", ">");
|
|
327
|
+
}
|
|
328
|
+
function shellQuote(value) {
|
|
329
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
330
|
+
}
|
|
331
|
+
function windowsQuote(value) {
|
|
332
|
+
const escaped = value.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, (slashes) => `${slashes}${slashes}`);
|
|
333
|
+
return `"${escaped}"`;
|
|
334
|
+
}
|
|
335
|
+
function windowsCommandLine(args) {
|
|
336
|
+
return args.map(windowsQuote).join(" ");
|
|
337
|
+
}
|
|
235
338
|
function expectedDarwinPlist() {
|
|
236
|
-
return buildLaunchdPlist(
|
|
339
|
+
return buildLaunchdPlist(syncCommandArgs());
|
|
237
340
|
}
|
|
238
341
|
function plistDrift(installed, expected) {
|
|
239
342
|
if (installed === null) return "absent";
|
|
@@ -251,7 +354,7 @@ function installAutoSync() {
|
|
|
251
354
|
writeFileSync2(plistPath, expectedDarwinPlist());
|
|
252
355
|
spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
253
356
|
spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
|
|
254
|
-
return `launchd agent installed (${plistPath}), syncing every ${
|
|
357
|
+
return `launchd agent installed (${plistPath}), syncing every ${syncIntervalLabel()}`;
|
|
255
358
|
}
|
|
256
359
|
if (os === "linux") {
|
|
257
360
|
const line = expectedLinuxCronLine();
|
|
@@ -263,28 +366,32 @@ ${line}
|
|
|
263
366
|
`.replace(/^\n+/, "");
|
|
264
367
|
const res = spawnSync("crontab", ["-"], { input: next });
|
|
265
368
|
if (res.status !== 0) throw new Error("could not install crontab entry");
|
|
266
|
-
return `cron entry installed, syncing every ${
|
|
369
|
+
return `cron entry installed, syncing every ${syncIntervalLabel()}`;
|
|
267
370
|
}
|
|
268
371
|
if (os === "win32") {
|
|
269
372
|
const res = spawnSync("schtasks", [
|
|
270
373
|
"/Create",
|
|
271
374
|
"/F",
|
|
272
375
|
"/SC",
|
|
273
|
-
"
|
|
376
|
+
"MINUTE",
|
|
274
377
|
"/MO",
|
|
275
|
-
String(
|
|
378
|
+
String(SYNC_INTERVAL_MINUTES),
|
|
276
379
|
"/TN",
|
|
277
380
|
"whoburnedmore-sync",
|
|
278
381
|
"/TR",
|
|
279
|
-
|
|
382
|
+
windowsCommandLine(syncCommandArgs())
|
|
280
383
|
]);
|
|
281
384
|
if (res.status !== 0) throw new Error("could not create scheduled task");
|
|
282
|
-
return `scheduled task installed, syncing every ${
|
|
385
|
+
return `scheduled task installed, syncing every ${syncIntervalLabel()}`;
|
|
283
386
|
}
|
|
284
387
|
throw new Error(`auto-sync is not supported on ${os}`);
|
|
285
388
|
}
|
|
286
|
-
function
|
|
287
|
-
return `0 */${
|
|
389
|
+
function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
|
|
390
|
+
return mins % 60 === 0 ? `0 */${mins / 60} * * *` : `*/${mins} * * * *`;
|
|
391
|
+
}
|
|
392
|
+
function expectedLinuxCronLine(opts) {
|
|
393
|
+
const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
|
|
394
|
+
return `${cronSchedule()} ${command} >${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
|
|
288
395
|
}
|
|
289
396
|
function uninstallAutoSync() {
|
|
290
397
|
const os = platform();
|
|
@@ -363,6 +470,41 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
|
|
|
363
470
|
return false;
|
|
364
471
|
}
|
|
365
472
|
}
|
|
473
|
+
function notifyLaunchLive(opts) {
|
|
474
|
+
const os = opts?.platform ?? platform();
|
|
475
|
+
const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
|
|
476
|
+
const title = "whoburnedmore is live";
|
|
477
|
+
const message = "Your dashboard is ready. Go to whoburnedmore.com";
|
|
478
|
+
if (os === "darwin") {
|
|
479
|
+
return run2("osascript", [
|
|
480
|
+
"-e",
|
|
481
|
+
`display notification "${message}" with title "${title}"`
|
|
482
|
+
]).status === 0;
|
|
483
|
+
}
|
|
484
|
+
if (os === "linux") {
|
|
485
|
+
return run2("notify-send", [title, message]).status === 0;
|
|
486
|
+
}
|
|
487
|
+
if (os === "win32") {
|
|
488
|
+
const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
489
|
+
const script = [
|
|
490
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
491
|
+
"$n = New-Object System.Windows.Forms.NotifyIcon",
|
|
492
|
+
"$n.Icon = [System.Drawing.SystemIcons]::Application",
|
|
493
|
+
`$n.BalloonTipTitle = ${psQuote(title)}`,
|
|
494
|
+
`$n.BalloonTipText = ${psQuote(message)}`,
|
|
495
|
+
"$n.Visible = $true",
|
|
496
|
+
"$n.ShowBalloonTip(10000)",
|
|
497
|
+
"Start-Sleep -Seconds 2",
|
|
498
|
+
"$n.Dispose()"
|
|
499
|
+
].join("; ");
|
|
500
|
+
return run2("powershell", [
|
|
501
|
+
"-NoProfile",
|
|
502
|
+
"-Command",
|
|
503
|
+
script
|
|
504
|
+
]).status === 0;
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
366
508
|
function autoSyncLoaded() {
|
|
367
509
|
if (platform() === "darwin") {
|
|
368
510
|
const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
|
|
@@ -392,7 +534,7 @@ function printBanner() {
|
|
|
392
534
|
// src/collect.ts
|
|
393
535
|
import { execFile } from "node:child_process";
|
|
394
536
|
import { createRequire as createRequire3 } from "node:module";
|
|
395
|
-
import { dirname as
|
|
537
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
396
538
|
import { promisify } from "node:util";
|
|
397
539
|
|
|
398
540
|
// src/attribution.ts
|
|
@@ -751,7 +893,7 @@ import { join as join5 } from "node:path";
|
|
|
751
893
|
// src/tokscale.ts
|
|
752
894
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
753
895
|
import { createRequire } from "node:module";
|
|
754
|
-
import { dirname, join as join4 } from "node:path";
|
|
896
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
755
897
|
var LOOKBACK_DAYS = 30;
|
|
756
898
|
function num(n) {
|
|
757
899
|
const v = Math.round(Number(n));
|
|
@@ -795,7 +937,7 @@ function resolveTokscaleBin() {
|
|
|
795
937
|
const pkg = require3("tokscale/package.json");
|
|
796
938
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
|
|
797
939
|
if (!rel) return null;
|
|
798
|
-
const binPath = join4(
|
|
940
|
+
const binPath = join4(dirname2(pkgPath), rel);
|
|
799
941
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
800
942
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
801
943
|
}
|
|
@@ -1128,7 +1270,7 @@ function resolveCcusageBin() {
|
|
|
1128
1270
|
const pkgPath = require3.resolve("ccusage/package.json");
|
|
1129
1271
|
const pkg = require3("ccusage/package.json");
|
|
1130
1272
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
|
|
1131
|
-
const binPath = join6(
|
|
1273
|
+
const binPath = join6(dirname3(pkgPath), rel);
|
|
1132
1274
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
1133
1275
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
1134
1276
|
}
|
|
@@ -1152,6 +1294,13 @@ function dedupeDaily(entries) {
|
|
|
1152
1294
|
}
|
|
1153
1295
|
return [...byKey.values()];
|
|
1154
1296
|
}
|
|
1297
|
+
function entryTokens(e) {
|
|
1298
|
+
return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
|
|
1299
|
+
}
|
|
1300
|
+
function capByTokens(rows, max, tokens) {
|
|
1301
|
+
if (rows.length <= max) return rows;
|
|
1302
|
+
return [...rows].sort((a, b) => tokens(b) - tokens(a)).slice(0, max);
|
|
1303
|
+
}
|
|
1155
1304
|
function dedupeSessions(sessions) {
|
|
1156
1305
|
const byId = /* @__PURE__ */ new Map();
|
|
1157
1306
|
const total = (s) => s.inputTokens + s.outputTokens + s.cacheCreationTokens + s.cacheReadTokens;
|
|
@@ -1263,9 +1412,14 @@ async function collectAll(onProgress) {
|
|
|
1263
1412
|
};
|
|
1264
1413
|
});
|
|
1265
1414
|
return {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1415
|
+
// Cap each array to the server's accepted maximum (the shared SubmitPayload
|
|
1416
|
+
// schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
|
|
1417
|
+
// highest-token rows. Without this, a power user with >10000 distinct sessions
|
|
1418
|
+
// would have their ENTIRE submit rejected with a 400 instead of a capped one.
|
|
1419
|
+
// tools/skills/projects are already bounded upstream (attribution caps).
|
|
1420
|
+
entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
|
|
1421
|
+
sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
|
|
1422
|
+
blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
|
|
1269
1423
|
toolsFound,
|
|
1270
1424
|
tools,
|
|
1271
1425
|
skills,
|
|
@@ -1296,8 +1450,8 @@ function buildStatusReport(s) {
|
|
|
1296
1450
|
" \u21B3 config is out of date \u2014 it will self-repair on your next run"
|
|
1297
1451
|
);
|
|
1298
1452
|
}
|
|
1299
|
-
lines.push(` \u2022 Interval: every ${s.
|
|
1300
|
-
const staleAfterMs = s.
|
|
1453
|
+
lines.push(` \u2022 Interval: every ${syncIntervalLabel(s.intervalMinutes)}`);
|
|
1454
|
+
const staleAfterMs = s.intervalMinutes * 2 * 60 * 1e3;
|
|
1301
1455
|
if (s.lastSyncAt === null) {
|
|
1302
1456
|
lines.push(" \u2022 Last sync: never recorded");
|
|
1303
1457
|
lines.push(" \u26A0 STALE: no successful sync recorded yet \u2014 run `npx whoburnedmore`");
|
|
@@ -1306,7 +1460,7 @@ function buildStatusReport(s) {
|
|
|
1306
1460
|
lines.push(` \u2022 Last sync: ${ago(age)}`);
|
|
1307
1461
|
if (age > staleAfterMs) {
|
|
1308
1462
|
lines.push(
|
|
1309
|
-
` \u26A0 STALE: last sync was over ${s.
|
|
1463
|
+
` \u26A0 STALE: last sync was over ${syncIntervalLabel(s.intervalMinutes * 2)} ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
|
|
1310
1464
|
);
|
|
1311
1465
|
} else {
|
|
1312
1466
|
lines.push(" \u2713 Fresh \u2014 your dashboard is up to date.");
|
|
@@ -1328,7 +1482,7 @@ function agentStatusReport(now = Date.now()) {
|
|
|
1328
1482
|
installed: autoSyncInstalled(),
|
|
1329
1483
|
loaded: autoSyncLoaded(),
|
|
1330
1484
|
drift: autoSyncDrift(),
|
|
1331
|
-
|
|
1485
|
+
intervalMinutes: SYNC_INTERVAL_MINUTES,
|
|
1332
1486
|
lastSyncAt: typeof cfg?.lastSyncAt === "number" ? cfg.lastSyncAt : null,
|
|
1333
1487
|
now,
|
|
1334
1488
|
nodePath,
|
|
@@ -5378,6 +5532,48 @@ var coerce = {
|
|
|
5378
5532
|
};
|
|
5379
5533
|
var NEVER = INVALID;
|
|
5380
5534
|
|
|
5535
|
+
// ../shared/dist/tenant.js
|
|
5536
|
+
var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
5537
|
+
"www",
|
|
5538
|
+
"api",
|
|
5539
|
+
"app",
|
|
5540
|
+
"admin",
|
|
5541
|
+
"mail",
|
|
5542
|
+
"email",
|
|
5543
|
+
"cdn",
|
|
5544
|
+
"static",
|
|
5545
|
+
"assets",
|
|
5546
|
+
"blog",
|
|
5547
|
+
"docs",
|
|
5548
|
+
"status",
|
|
5549
|
+
"help",
|
|
5550
|
+
"support",
|
|
5551
|
+
"ingest",
|
|
5552
|
+
"vercel",
|
|
5553
|
+
"preview",
|
|
5554
|
+
"staging",
|
|
5555
|
+
"dev",
|
|
5556
|
+
"test"
|
|
5557
|
+
]);
|
|
5558
|
+
|
|
5559
|
+
// ../shared/dist/launch-gate.js
|
|
5560
|
+
var LaunchAccessMode = external_exports.enum(["full", "invited", "waitlisted"]);
|
|
5561
|
+
var LaunchStatusResponse = external_exports.object({
|
|
5562
|
+
mode: LaunchAccessMode,
|
|
5563
|
+
launchAt: external_exports.string().datetime(),
|
|
5564
|
+
now: external_exports.string().datetime(),
|
|
5565
|
+
remainingSeconds: external_exports.number().int().nonnegative(),
|
|
5566
|
+
live: external_exports.boolean()
|
|
5567
|
+
});
|
|
5568
|
+
var LaunchRedeemRequest = external_exports.object({
|
|
5569
|
+
code: external_exports.string().trim().min(2).max(64)
|
|
5570
|
+
});
|
|
5571
|
+
var LaunchRedeemResponse = external_exports.object({
|
|
5572
|
+
ok: external_exports.literal(true),
|
|
5573
|
+
mode: LaunchAccessMode,
|
|
5574
|
+
expiresAt: external_exports.string().datetime().nullable()
|
|
5575
|
+
});
|
|
5576
|
+
|
|
5381
5577
|
// ../shared/dist/index.js
|
|
5382
5578
|
var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
|
|
5383
5579
|
var tokenCount = external_exports.number().int().nonnegative();
|
|
@@ -5493,7 +5689,9 @@ var SubmitPayload = external_exports.object({
|
|
|
5493
5689
|
*/
|
|
5494
5690
|
attributionComplete: external_exports.boolean().optional(),
|
|
5495
5691
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
5496
|
-
board: external_exports.string().min(1).max(32).optional()
|
|
5692
|
+
board: external_exports.string().min(1).max(32).optional(),
|
|
5693
|
+
/** Optional organization slug (from `--org=<slug>`): auto-join this org on submit. */
|
|
5694
|
+
org: external_exports.string().min(2).max(32).optional()
|
|
5497
5695
|
});
|
|
5498
5696
|
var AnonSubmitPayload = SubmitPayload.extend({
|
|
5499
5697
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
@@ -5504,6 +5702,94 @@ function entryTotalTokens(e) {
|
|
|
5504
5702
|
}
|
|
5505
5703
|
var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
|
|
5506
5704
|
var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
|
|
5705
|
+
var OrgType = external_exports.enum(["company", "hackathon", "hackerhouse"]);
|
|
5706
|
+
var MemberRole = external_exports.enum(["owner", "admin", "member"]);
|
|
5707
|
+
var OrgBoardVisibility = external_exports.enum(["public", "members"]);
|
|
5708
|
+
var HexColor = external_exports.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "must be a hex color like #f97316");
|
|
5709
|
+
var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
5710
|
+
...RESERVED_SUBDOMAINS,
|
|
5711
|
+
"o",
|
|
5712
|
+
"d",
|
|
5713
|
+
"u",
|
|
5714
|
+
"boards",
|
|
5715
|
+
"board",
|
|
5716
|
+
"claim",
|
|
5717
|
+
"signin",
|
|
5718
|
+
"signout",
|
|
5719
|
+
"login",
|
|
5720
|
+
"logout",
|
|
5721
|
+
"dashboard",
|
|
5722
|
+
"install",
|
|
5723
|
+
"for-teams",
|
|
5724
|
+
"teams",
|
|
5725
|
+
"about",
|
|
5726
|
+
"contact",
|
|
5727
|
+
"trust",
|
|
5728
|
+
"cli",
|
|
5729
|
+
"feedback",
|
|
5730
|
+
"guides",
|
|
5731
|
+
"guide",
|
|
5732
|
+
"join",
|
|
5733
|
+
"leaderboard",
|
|
5734
|
+
"settings",
|
|
5735
|
+
"account",
|
|
5736
|
+
"me",
|
|
5737
|
+
"new",
|
|
5738
|
+
"robots",
|
|
5739
|
+
"sitemap"
|
|
5740
|
+
]);
|
|
5741
|
+
var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
5742
|
+
function isValidSlug(slug) {
|
|
5743
|
+
return typeof slug === "string" && slug.length >= 2 && slug.length <= 32 && SLUG_RE.test(slug) && !RESERVED_SLUGS.has(slug);
|
|
5744
|
+
}
|
|
5745
|
+
var OrgSlug = external_exports.string().min(2).max(32).refine(isValidSlug, "invalid or reserved slug");
|
|
5746
|
+
var OrgWindow = external_exports.object({
|
|
5747
|
+
startDate: DateString.nullable().optional(),
|
|
5748
|
+
endDate: DateString.nullable().optional()
|
|
5749
|
+
});
|
|
5750
|
+
var OrgJoinPolicy = external_exports.object({
|
|
5751
|
+
allowCodeJoin: external_exports.boolean().default(true),
|
|
5752
|
+
allowDomainJoin: external_exports.boolean().default(false),
|
|
5753
|
+
/** Verified email domains that auto-join, e.g. ["acme.com"]. */
|
|
5754
|
+
emailDomains: external_exports.array(external_exports.string().min(3).max(253).toLowerCase()).max(20).default([])
|
|
5755
|
+
});
|
|
5756
|
+
var OrgApplicationInput = external_exports.object({
|
|
5757
|
+
type: OrgType,
|
|
5758
|
+
orgName: external_exports.string().min(1).max(120),
|
|
5759
|
+
desiredSlug: external_exports.string().min(2).max(32).optional(),
|
|
5760
|
+
contactName: external_exports.string().min(1).max(120),
|
|
5761
|
+
contactEmail: external_exports.string().email().max(254),
|
|
5762
|
+
website: external_exports.string().url().max(300).optional(),
|
|
5763
|
+
/** Rough headcount / attendee estimate, free text. */
|
|
5764
|
+
size: external_exports.string().max(60).optional(),
|
|
5765
|
+
message: external_exports.string().max(2e3).optional()
|
|
5766
|
+
});
|
|
5767
|
+
var OrgProvisionInput = external_exports.object({
|
|
5768
|
+
/** When provisioning straight from an application. */
|
|
5769
|
+
applicationId: external_exports.string().min(1).max(64).optional(),
|
|
5770
|
+
slug: OrgSlug,
|
|
5771
|
+
name: external_exports.string().min(1).max(120),
|
|
5772
|
+
type: OrgType,
|
|
5773
|
+
/** Handle (or email) of the user who becomes Owner. */
|
|
5774
|
+
ownerHandle: external_exports.string().min(1).max(120).optional(),
|
|
5775
|
+
ownerEmail: external_exports.string().email().max(254).optional(),
|
|
5776
|
+
description: external_exports.string().max(2e3).optional(),
|
|
5777
|
+
boardVisibility: OrgBoardVisibility.optional(),
|
|
5778
|
+
window: OrgWindow.optional()
|
|
5779
|
+
});
|
|
5780
|
+
var OrgSettingsInput = external_exports.object({
|
|
5781
|
+
name: external_exports.string().min(1).max(120).optional(),
|
|
5782
|
+
description: external_exports.string().max(2e3).nullable().optional(),
|
|
5783
|
+
accentColor: HexColor.optional(),
|
|
5784
|
+
logoUrl: external_exports.string().url().max(500).nullable().optional(),
|
|
5785
|
+
boardVisibility: OrgBoardVisibility.optional(),
|
|
5786
|
+
window: OrgWindow.optional(),
|
|
5787
|
+
joinPolicy: OrgJoinPolicy.partial().optional()
|
|
5788
|
+
});
|
|
5789
|
+
var OrgJoinInput = external_exports.object({
|
|
5790
|
+
/** Required only when joining via a code; domain/admin joins omit it. */
|
|
5791
|
+
code: external_exports.string().min(1).max(64).optional()
|
|
5792
|
+
});
|
|
5507
5793
|
|
|
5508
5794
|
// src/output.ts
|
|
5509
5795
|
function formatTokens(n) {
|
|
@@ -5815,6 +6101,7 @@ function startProgress() {
|
|
|
5815
6101
|
};
|
|
5816
6102
|
}
|
|
5817
6103
|
function openBrowser(url) {
|
|
6104
|
+
if (!isOpenableUrl(url)) return;
|
|
5818
6105
|
const os = platform3();
|
|
5819
6106
|
const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5820
6107
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
@@ -5880,7 +6167,7 @@ async function run(flags) {
|
|
|
5880
6167
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
5881
6168
|
if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
|
|
5882
6169
|
payload.attributionComplete = true;
|
|
5883
|
-
|
|
6170
|
+
applyScope(payload, flags);
|
|
5884
6171
|
if (flags.dryRun) {
|
|
5885
6172
|
console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
|
|
5886
6173
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -5909,13 +6196,31 @@ async function run(flags) {
|
|
|
5909
6196
|
recordSync();
|
|
5910
6197
|
} catch {
|
|
5911
6198
|
}
|
|
6199
|
+
try {
|
|
6200
|
+
const config = loadConfig();
|
|
6201
|
+
if (result.launch?.live && !config?.launchNotificationDeliveredAt) {
|
|
6202
|
+
if (notifyLaunchLive()) {
|
|
6203
|
+
recordLaunchNotificationDelivered();
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
} catch {
|
|
6207
|
+
}
|
|
6208
|
+
const baseUrl = result.boardUrl ?? result.dashboardUrl;
|
|
5912
6209
|
const target = result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
|
|
6210
|
+
const trusted = isTrustedWebUrl(baseUrl);
|
|
5913
6211
|
if (!flags.quiet) {
|
|
5914
6212
|
console.log(
|
|
5915
6213
|
pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
|
|
5916
6214
|
);
|
|
5917
|
-
|
|
5918
|
-
|
|
6215
|
+
if (trusted) {
|
|
6216
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
6217
|
+
openBrowser(target);
|
|
6218
|
+
} else {
|
|
6219
|
+
console.log(
|
|
6220
|
+
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
6221
|
+
);
|
|
6222
|
+
console.log(` ${baseUrl}`);
|
|
6223
|
+
}
|
|
5919
6224
|
}
|
|
5920
6225
|
const lines = submitNextStepLines(result);
|
|
5921
6226
|
for (const line of lines) {
|
|
@@ -5933,10 +6238,42 @@ async function run(flags) {
|
|
|
5933
6238
|
if (!flags.quiet) {
|
|
5934
6239
|
console.log();
|
|
5935
6240
|
console.log(
|
|
5936
|
-
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every
|
|
6241
|
+
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
|
|
5937
6242
|
);
|
|
5938
6243
|
}
|
|
5939
6244
|
}
|
|
6245
|
+
async function linkServerInstall(token) {
|
|
6246
|
+
if (!token) {
|
|
6247
|
+
throw new Error("missing install token \u2014 use `npx whoburnedmore link --token=<token>`");
|
|
6248
|
+
}
|
|
6249
|
+
const anonKey = ensureAnonKey();
|
|
6250
|
+
const linked = await redeemServerInstall(token, anonKey);
|
|
6251
|
+
console.log(
|
|
6252
|
+
linked.alreadyLinked ? ` This machine is already linked to @${linked.handle}.` : ` Linked this machine to @${linked.handle}.`
|
|
6253
|
+
);
|
|
6254
|
+
if (linked.mergedDays > 0) {
|
|
6255
|
+
console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
|
|
6256
|
+
}
|
|
6257
|
+
await run({
|
|
6258
|
+
dryRun: false,
|
|
6259
|
+
noSubmit: false,
|
|
6260
|
+
local: false,
|
|
6261
|
+
quiet: true
|
|
6262
|
+
});
|
|
6263
|
+
try {
|
|
6264
|
+
const action = reconcileAutoSync();
|
|
6265
|
+
if (action === "installed") {
|
|
6266
|
+
console.log(pc2.dim(" Background sync installed; this machine will refresh every 15 min."));
|
|
6267
|
+
} else if (action === "reinstalled") {
|
|
6268
|
+
console.log(pc2.dim(" Background sync repaired; this machine will refresh every 15 min."));
|
|
6269
|
+
} else {
|
|
6270
|
+
console.log(pc2.dim(" Background sync is already configured."));
|
|
6271
|
+
}
|
|
6272
|
+
} catch {
|
|
6273
|
+
console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
|
|
6274
|
+
}
|
|
6275
|
+
console.log(` Profile: ${linked.profileUrl}`);
|
|
6276
|
+
}
|
|
5940
6277
|
async function main() {
|
|
5941
6278
|
const major = Number(process.versions.node.split(".")[0]);
|
|
5942
6279
|
if (major < 20) {
|
|
@@ -5951,7 +6288,8 @@ async function main() {
|
|
|
5951
6288
|
noSubmit: args.includes("--no-submit"),
|
|
5952
6289
|
local: args.includes("--local"),
|
|
5953
6290
|
quiet: command === "sync",
|
|
5954
|
-
board: parseBoard(args)
|
|
6291
|
+
board: parseBoard(args),
|
|
6292
|
+
org: parseOrg(args)
|
|
5955
6293
|
};
|
|
5956
6294
|
switch (command) {
|
|
5957
6295
|
case "run":
|
|
@@ -5963,6 +6301,9 @@ async function main() {
|
|
|
5963
6301
|
await run({ ...flags, noSubmit: false, dryRun: false, local: false });
|
|
5964
6302
|
break;
|
|
5965
6303
|
}
|
|
6304
|
+
case "link":
|
|
6305
|
+
await linkServerInstall(parseInstallToken(args));
|
|
6306
|
+
break;
|
|
5966
6307
|
case "status":
|
|
5967
6308
|
case "doctor": {
|
|
5968
6309
|
for (const line of agentStatusReport()) console.log(line);
|
|
@@ -6016,9 +6357,11 @@ function printHelp() {
|
|
|
6016
6357
|
${pc2.bold("usage")}
|
|
6017
6358
|
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
6018
6359
|
npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
|
|
6360
|
+
npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
|
|
6019
6361
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
6020
6362
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
6021
6363
|
npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
|
|
6364
|
+
npx whoburnedmore link --token=TOKEN link this server to your signed-in account
|
|
6022
6365
|
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
6023
6366
|
npx whoburnedmore public put it back on the leaderboard
|
|
6024
6367
|
npx whoburnedmore remove delete your dashboard and its data
|
|
@@ -6027,7 +6370,7 @@ function printHelp() {
|
|
|
6027
6370
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
6028
6371
|
|
|
6029
6372
|
Background sync is on by default: after your first run, your page refreshes
|
|
6030
|
-
automatically every
|
|
6373
|
+
automatically every 15 min (\`uninstall-sync\` to stop). Your dashboard is public on
|
|
6031
6374
|
the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
|
|
6032
6375
|
it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
|
|
6033
6376
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whoburnedmore",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
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": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
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
|
+
"smoke:package": "node scripts/smoke-package.mjs",
|
|
17
|
+
"prepublishOnly": "pnpm run lint && pnpm run test && pnpm run build && pnpm run smoke:package",
|
|
17
18
|
"release:patch": "npm version patch && npm publish",
|
|
18
19
|
"release:minor": "npm version minor && npm publish",
|
|
19
20
|
"release:major": "npm version major && npm publish"
|