whoburnedmore 0.9.2 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +957 -165
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,10 +7,11 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
11
12
|
import { createRequire as createRequire4 } from "node:module";
|
|
12
13
|
import { platform as platform3 } from "node:os";
|
|
13
|
-
import { join as
|
|
14
|
+
import { join as join9 } from "node:path";
|
|
14
15
|
import { createInterface } from "node:readline/promises";
|
|
15
16
|
import pc2 from "picocolors";
|
|
16
17
|
|
|
@@ -95,13 +96,18 @@ async function readJson(res) {
|
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
|
-
async function post(path, body) {
|
|
99
|
+
async function post(path, body, token) {
|
|
100
|
+
const headers = { "Content-Type": "application/json" };
|
|
101
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
99
102
|
let res;
|
|
100
103
|
try {
|
|
101
104
|
res = await fetch(`${apiBase()}${path}`, {
|
|
102
105
|
method: "POST",
|
|
103
|
-
headers
|
|
104
|
-
body: JSON.stringify(body)
|
|
106
|
+
headers,
|
|
107
|
+
body: JSON.stringify(body),
|
|
108
|
+
// Bound the request so a slow/black-holing/hostile server can't hang the CLI
|
|
109
|
+
// — or the unattended 15-minute background sync — indefinitely.
|
|
110
|
+
signal: AbortSignal.timeout(3e4)
|
|
105
111
|
});
|
|
106
112
|
} catch {
|
|
107
113
|
throw new Error(
|
|
@@ -110,6 +116,17 @@ async function post(path, body) {
|
|
|
110
116
|
}
|
|
111
117
|
return { status: res.status, body: await readJson(res) };
|
|
112
118
|
}
|
|
119
|
+
async function verifyUsage(token, payload) {
|
|
120
|
+
const { status, body } = await post("/v1/verify", payload, token);
|
|
121
|
+
if (status === 401) throw new UnauthorizedError();
|
|
122
|
+
if (status !== 200) {
|
|
123
|
+
const err = body;
|
|
124
|
+
const details = err.details?.length ? `
|
|
125
|
+
- ${err.details.join("\n - ")}` : "";
|
|
126
|
+
throw new Error(`${err.error ?? "verification failed"}${details}`);
|
|
127
|
+
}
|
|
128
|
+
return body;
|
|
129
|
+
}
|
|
113
130
|
async function anonSubmit(anonKey, payload) {
|
|
114
131
|
const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
|
|
115
132
|
if (status !== 200) {
|
|
@@ -120,6 +137,45 @@ async function anonSubmit(anonKey, payload) {
|
|
|
120
137
|
}
|
|
121
138
|
return body;
|
|
122
139
|
}
|
|
140
|
+
var UnauthorizedError = class extends Error {
|
|
141
|
+
constructor(message = "your sign-in has expired") {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "UnauthorizedError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
async function deviceStart() {
|
|
147
|
+
const { status, body } = await post(
|
|
148
|
+
"/v1/auth/device",
|
|
149
|
+
{}
|
|
150
|
+
);
|
|
151
|
+
if (status !== 200) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
body.error ?? `sign-in failed (HTTP ${status})`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return body;
|
|
157
|
+
}
|
|
158
|
+
async function devicePoll(deviceCode) {
|
|
159
|
+
const { status, body } = await post(
|
|
160
|
+
"/v1/auth/device/token",
|
|
161
|
+
{ deviceCode }
|
|
162
|
+
);
|
|
163
|
+
if (status !== 200) {
|
|
164
|
+
throw new Error(`sign-in failed (HTTP ${status})`);
|
|
165
|
+
}
|
|
166
|
+
return body;
|
|
167
|
+
}
|
|
168
|
+
async function submit(token, payload) {
|
|
169
|
+
const { status, body } = await post("/v1/submit", payload, token);
|
|
170
|
+
if (status === 401) throw new UnauthorizedError();
|
|
171
|
+
if (status !== 200) {
|
|
172
|
+
const err = body;
|
|
173
|
+
const details = err.details?.length ? `
|
|
174
|
+
- ${err.details.join("\n - ")}` : "";
|
|
175
|
+
throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
|
|
176
|
+
}
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
123
179
|
function claimUrl(dashboardUrl, anonKey) {
|
|
124
180
|
return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
|
|
125
181
|
}
|
|
@@ -144,7 +200,8 @@ async function anonRemove(anonKey) {
|
|
|
144
200
|
const res = await fetch(`${apiBase()}/v1/anon`, {
|
|
145
201
|
method: "DELETE",
|
|
146
202
|
headers: { "Content-Type": "application/json" },
|
|
147
|
-
body: JSON.stringify({ anonKey })
|
|
203
|
+
body: JSON.stringify({ anonKey }),
|
|
204
|
+
signal: AbortSignal.timeout(3e4)
|
|
148
205
|
});
|
|
149
206
|
if (res.status !== 200) {
|
|
150
207
|
const b = await res.json().catch(() => ({}));
|
|
@@ -167,8 +224,8 @@ import {
|
|
|
167
224
|
existsSync as existsSync2,
|
|
168
225
|
mkdirSync as mkdirSync2,
|
|
169
226
|
readFileSync as readFileSync2,
|
|
170
|
-
renameSync,
|
|
171
|
-
rmSync,
|
|
227
|
+
renameSync as renameSync2,
|
|
228
|
+
rmSync as rmSync2,
|
|
172
229
|
statSync,
|
|
173
230
|
writeFileSync as writeFileSync2
|
|
174
231
|
} from "node:fs";
|
|
@@ -182,6 +239,8 @@ import {
|
|
|
182
239
|
existsSync,
|
|
183
240
|
mkdirSync,
|
|
184
241
|
readFileSync,
|
|
242
|
+
renameSync,
|
|
243
|
+
rmSync,
|
|
185
244
|
writeFileSync
|
|
186
245
|
} from "node:fs";
|
|
187
246
|
import { homedir } from "node:os";
|
|
@@ -198,6 +257,8 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
198
257
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
199
258
|
const config = {};
|
|
200
259
|
if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
|
|
260
|
+
if (typeof parsed.cliToken === "string") config.cliToken = parsed.cliToken;
|
|
261
|
+
if (typeof parsed.handle === "string") config.handle = parsed.handle;
|
|
201
262
|
if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
|
|
202
263
|
config.lastSyncAt = parsed.lastSyncAt;
|
|
203
264
|
if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
|
|
@@ -211,10 +272,20 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
211
272
|
function saveConfig(dir = defaultConfigDir(), config = {}) {
|
|
212
273
|
mkdirSync(dir, { recursive: true });
|
|
213
274
|
const file = join(dir, "config.json");
|
|
214
|
-
|
|
275
|
+
const tmp = join(dir, `config.json.${process.pid}.tmp`);
|
|
215
276
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
277
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
278
|
+
try {
|
|
279
|
+
chmodSync(tmp, 384);
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
renameSync(tmp, file);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
try {
|
|
285
|
+
rmSync(tmp, { force: true });
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
throw err;
|
|
218
289
|
}
|
|
219
290
|
}
|
|
220
291
|
function ensureAnonKey(dir = defaultConfigDir()) {
|
|
@@ -232,6 +303,16 @@ function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date
|
|
|
232
303
|
const config = loadConfig(dir) ?? {};
|
|
233
304
|
saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
|
|
234
305
|
}
|
|
306
|
+
function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
|
|
307
|
+
const config = loadConfig(dir) ?? {};
|
|
308
|
+
saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
|
|
309
|
+
}
|
|
310
|
+
function clearAuth(dir = defaultConfigDir()) {
|
|
311
|
+
const config = loadConfig(dir);
|
|
312
|
+
if (!config?.cliToken && !config?.handle) return;
|
|
313
|
+
const { cliToken: _t, handle: _h, ...rest } = config;
|
|
314
|
+
saveConfig(dir, rest);
|
|
315
|
+
}
|
|
235
316
|
|
|
236
317
|
// src/autosync.ts
|
|
237
318
|
var SYNC_INTERVAL_MINUTES = 15;
|
|
@@ -250,11 +331,55 @@ var STABLE_NPM_CANDIDATES = [
|
|
|
250
331
|
"/usr/bin/npm"
|
|
251
332
|
];
|
|
252
333
|
var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
|
|
334
|
+
var SYNC_PATH_DIRS = [
|
|
335
|
+
"/opt/homebrew/bin",
|
|
336
|
+
"/usr/local/bin",
|
|
337
|
+
"/usr/bin",
|
|
338
|
+
"/bin",
|
|
339
|
+
"/usr/sbin",
|
|
340
|
+
"/sbin"
|
|
341
|
+
];
|
|
342
|
+
function syncPathEnv(npmPath = resolveNpmPath()) {
|
|
343
|
+
const dir = dirname(npmPath);
|
|
344
|
+
const dirs = [];
|
|
345
|
+
if (dir && dir !== "." && dir !== "/" && dir !== npmPath) dirs.push(dir);
|
|
346
|
+
for (const d of SYNC_PATH_DIRS) {
|
|
347
|
+
if (!dirs.includes(d)) dirs.push(d);
|
|
348
|
+
}
|
|
349
|
+
return dirs.join(":");
|
|
350
|
+
}
|
|
351
|
+
var FORWARDED_ENV_VARS = [
|
|
352
|
+
"WHOBURNEDMORE_CONFIG_DIR",
|
|
353
|
+
// identity: where the anonKey lives
|
|
354
|
+
"WHOBURNEDMORE_API",
|
|
355
|
+
// endpoint: where submits go
|
|
356
|
+
"WHOBURNEDMORE_WEB",
|
|
357
|
+
// dashboard URL shown to the user
|
|
358
|
+
"XDG_CONFIG_HOME",
|
|
359
|
+
// base for the default config dir
|
|
360
|
+
"CLAUDE_CONFIG_DIR"
|
|
361
|
+
// a primary usage-collection source
|
|
362
|
+
];
|
|
363
|
+
function syncEnv(opts) {
|
|
364
|
+
const env = opts?.env ?? process.env;
|
|
365
|
+
const pairs = [["PATH", syncPathEnv(opts?.npmPath)]];
|
|
366
|
+
for (const key of FORWARDED_ENV_VARS) {
|
|
367
|
+
const value = env[key];
|
|
368
|
+
if (typeof value === "string" && value.length > 0 && !/[\r\n]/.test(value)) {
|
|
369
|
+
pairs.push([key, value]);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return pairs;
|
|
373
|
+
}
|
|
253
374
|
function syncLogPath() {
|
|
254
375
|
return join2(defaultConfigDir(), "sync.log");
|
|
255
376
|
}
|
|
256
|
-
function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
|
|
377
|
+
function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath(), envPairs = syncEnv({ npmPath: commandArgs[0] })) {
|
|
257
378
|
const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
379
|
+
const envEntries = envPairs.map(
|
|
380
|
+
([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
381
|
+
<string>${xmlEscape(v)}</string>`
|
|
382
|
+
).join("\n");
|
|
258
383
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
259
384
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
260
385
|
<plist version="1.0">
|
|
@@ -265,6 +390,15 @@ function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPat
|
|
|
265
390
|
<array>
|
|
266
391
|
${programArguments}
|
|
267
392
|
</array>
|
|
393
|
+
<!-- launchd runs jobs with a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin)
|
|
394
|
+
that excludes Homebrew. npm's shebang (and the package bin npm exec
|
|
395
|
+
spawns) is #!/usr/bin/env node, so node must be on PATH or every tick
|
|
396
|
+
dies with "env: node: No such file or directory". We also forward the
|
|
397
|
+
user's identity/endpoint/collection env so background == foreground. -->
|
|
398
|
+
<key>EnvironmentVariables</key>
|
|
399
|
+
<dict>
|
|
400
|
+
${envEntries}
|
|
401
|
+
</dict>
|
|
268
402
|
<key>StartInterval</key>
|
|
269
403
|
<integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
|
|
270
404
|
<!-- Run once right after login/reboot so a machine that was off (or asleep)
|
|
@@ -398,7 +532,10 @@ function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
|
|
|
398
532
|
}
|
|
399
533
|
function expectedLinuxCronLine(opts) {
|
|
400
534
|
const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
|
|
401
|
-
|
|
535
|
+
const envPrefix = syncEnv({ npmPath: opts?.npmPath ?? resolveNpmPath() }).map(([k, v]) => `${k}=${shellQuote(v)}`).join(" ");
|
|
536
|
+
const redirect = `>${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
|
|
537
|
+
const commandField = `${envPrefix} ${command} ${redirect}`.replaceAll("%", "\\%");
|
|
538
|
+
return `${cronSchedule()} ${commandField}`;
|
|
402
539
|
}
|
|
403
540
|
var SYSTEMD_UNIT = "whoburnedmore-sync";
|
|
404
541
|
function systemdUserDir() {
|
|
@@ -412,15 +549,17 @@ function systemdTimerPath() {
|
|
|
412
549
|
return join2(systemdUserDir(), `${SYSTEMD_UNIT}.timer`);
|
|
413
550
|
}
|
|
414
551
|
function systemdQuote(value) {
|
|
415
|
-
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
552
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("%", "%%").replaceAll('"', '\\"')}"`;
|
|
416
553
|
}
|
|
417
|
-
function buildSystemdService(commandArgs = syncCommandArgs()) {
|
|
554
|
+
function buildSystemdService(commandArgs = syncCommandArgs(), envPairs = syncEnv({ npmPath: commandArgs[0] })) {
|
|
418
555
|
const execStart = commandArgs.map(systemdQuote).join(" ");
|
|
556
|
+
const envLines = envPairs.map(([k, v]) => `Environment=${systemdQuote(`${k}=${v}`)}`).join("\n");
|
|
419
557
|
return `[Unit]
|
|
420
558
|
Description=whoburnedmore background token-usage sync
|
|
421
559
|
|
|
422
560
|
[Service]
|
|
423
561
|
Type=oneshot
|
|
562
|
+
${envLines}
|
|
424
563
|
ExecStart=${execStart}
|
|
425
564
|
`;
|
|
426
565
|
}
|
|
@@ -478,8 +617,8 @@ function tryInstallSystemd() {
|
|
|
478
617
|
{ stdio: "ignore" }
|
|
479
618
|
);
|
|
480
619
|
if (res.status !== 0) {
|
|
481
|
-
|
|
482
|
-
|
|
620
|
+
rmSync2(systemdServicePath(), { force: true });
|
|
621
|
+
rmSync2(systemdTimerPath(), { force: true });
|
|
483
622
|
return null;
|
|
484
623
|
}
|
|
485
624
|
return `systemd user timer installed, syncing every ${syncIntervalLabel()} (run \`loginctl enable-linger\` to keep syncing while logged out)`;
|
|
@@ -490,7 +629,7 @@ function uninstallAutoSync() {
|
|
|
490
629
|
const plistPath = launchAgentPath();
|
|
491
630
|
if (existsSync2(plistPath)) {
|
|
492
631
|
spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
493
|
-
|
|
632
|
+
rmSync2(plistPath, { force: true });
|
|
494
633
|
}
|
|
495
634
|
return "launchd agent removed";
|
|
496
635
|
}
|
|
@@ -508,8 +647,8 @@ function uninstallAutoSync() {
|
|
|
508
647
|
["--user", "disable", "--now", `${SYSTEMD_UNIT}.timer`],
|
|
509
648
|
{ stdio: "ignore" }
|
|
510
649
|
);
|
|
511
|
-
|
|
512
|
-
|
|
650
|
+
rmSync2(systemdServicePath(), { force: true });
|
|
651
|
+
rmSync2(systemdTimerPath(), { force: true });
|
|
513
652
|
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
514
653
|
removed = true;
|
|
515
654
|
}
|
|
@@ -585,7 +724,7 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
|
|
|
585
724
|
try {
|
|
586
725
|
if (!existsSync2(path)) return false;
|
|
587
726
|
if (statSync(path).size <= capBytes) return false;
|
|
588
|
-
|
|
727
|
+
renameSync2(path, `${path}.1`);
|
|
589
728
|
return true;
|
|
590
729
|
} catch {
|
|
591
730
|
return false;
|
|
@@ -673,46 +812,23 @@ async function daemonLoop(deps) {
|
|
|
673
812
|
// src/collect.ts
|
|
674
813
|
import { execFile } from "node:child_process";
|
|
675
814
|
import { createRequire as createRequire3 } from "node:module";
|
|
676
|
-
import { dirname as dirname3, join as
|
|
815
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
677
816
|
import { promisify } from "node:util";
|
|
678
817
|
|
|
679
818
|
// src/attribution.ts
|
|
680
819
|
import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
|
|
681
820
|
import { homedir as homedir3 } from "node:os";
|
|
682
|
-
import {
|
|
683
|
-
|
|
684
|
-
// src/pricing.ts
|
|
685
|
-
var TABLE = [
|
|
686
|
-
{ match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
687
|
-
{ match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
|
|
688
|
-
{ match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
|
|
689
|
-
{ match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
690
|
-
{ match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
|
|
691
|
-
{ match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
|
|
692
|
-
{ match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
|
|
693
|
-
{ match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
|
|
694
|
-
];
|
|
695
|
-
function estimateCostUSD(model, t) {
|
|
696
|
-
const row = TABLE.find((r) => r.match.test(model));
|
|
697
|
-
if (!row) return 0;
|
|
698
|
-
const p = row.price;
|
|
699
|
-
const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
|
|
700
|
-
return usd > 0 ? usd : 0;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// src/attribution.ts
|
|
821
|
+
import { join as join3 } from "node:path";
|
|
704
822
|
var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
705
823
|
var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
|
|
706
824
|
var MAX_FILES = 5e3;
|
|
707
825
|
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
708
826
|
var TIME_BUDGET_MS = 12e3;
|
|
709
827
|
var MAX_STATS = 300;
|
|
710
|
-
var MAX_PROJECTS = 500;
|
|
711
828
|
function createAccumulator() {
|
|
712
829
|
return {
|
|
713
830
|
tools: /* @__PURE__ */ new Map(),
|
|
714
831
|
skills: /* @__PURE__ */ new Map(),
|
|
715
|
-
projects: /* @__PURE__ */ new Map(),
|
|
716
832
|
agent: {
|
|
717
833
|
messageCount: 0,
|
|
718
834
|
subagentMessages: 0,
|
|
@@ -720,7 +836,6 @@ function createAccumulator() {
|
|
|
720
836
|
totalTokens: 0,
|
|
721
837
|
userMessageCount: 0
|
|
722
838
|
},
|
|
723
|
-
titles: /* @__PURE__ */ new Map(),
|
|
724
839
|
sessionMessages: /* @__PURE__ */ new Map()
|
|
725
840
|
};
|
|
726
841
|
}
|
|
@@ -761,9 +876,6 @@ function processRecord(rec, acc, ctx) {
|
|
|
761
876
|
sk.tokens += recTokens;
|
|
762
877
|
acc.skills.set(s, sk);
|
|
763
878
|
}
|
|
764
|
-
if (r.type === "ai-title" && typeof r.aiTitle === "string" && r.aiTitle && typeof r.sessionId === "string" && r.sessionId) {
|
|
765
|
-
acc.titles.set(r.sessionId, r.aiTitle.slice(0, 200));
|
|
766
|
-
}
|
|
767
879
|
const content = r.message?.content;
|
|
768
880
|
if (!Array.isArray(content)) return;
|
|
769
881
|
const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
|
|
@@ -800,25 +912,6 @@ function processRecord(rec, acc, ctx) {
|
|
|
800
912
|
(acc.sessionMessages.get(r.sessionId) ?? 0) + 1
|
|
801
913
|
);
|
|
802
914
|
}
|
|
803
|
-
if (typeof r.cwd === "string" && r.cwd && tokens > 0) {
|
|
804
|
-
const name = basename(r.cwd).slice(0, 128) || "unknown";
|
|
805
|
-
const model = typeof r.message?.model === "string" ? r.message.model : "unknown";
|
|
806
|
-
const u = r.message?.usage;
|
|
807
|
-
const num3 = (v) => {
|
|
808
|
-
const x = Math.round(Number(v));
|
|
809
|
-
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
810
|
-
};
|
|
811
|
-
const cost = estimateCostUSD(model, {
|
|
812
|
-
inputTokens: num3(u?.input_tokens),
|
|
813
|
-
outputTokens: num3(u?.output_tokens),
|
|
814
|
-
cacheCreationTokens: num3(u?.cache_creation_input_tokens),
|
|
815
|
-
cacheReadTokens: num3(u?.cache_read_input_tokens)
|
|
816
|
-
});
|
|
817
|
-
const p = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
|
|
818
|
-
p.tokens += tokens;
|
|
819
|
-
p.costUSD += cost;
|
|
820
|
-
acc.projects.set(name, p);
|
|
821
|
-
}
|
|
822
915
|
} else if (isUser) {
|
|
823
916
|
for (const block of content) {
|
|
824
917
|
if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
|
|
@@ -844,20 +937,11 @@ function toToolStats(map) {
|
|
|
844
937
|
return base;
|
|
845
938
|
});
|
|
846
939
|
}
|
|
847
|
-
function toProjectStats(map) {
|
|
848
|
-
return [...map.entries()].map(([name, v]) => ({
|
|
849
|
-
name,
|
|
850
|
-
tokens: v.tokens,
|
|
851
|
-
costUSD: Number(v.costUSD.toFixed(6))
|
|
852
|
-
})).filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, MAX_PROJECTS);
|
|
853
|
-
}
|
|
854
940
|
function accumulatorToResult(acc) {
|
|
855
941
|
return {
|
|
856
942
|
tools: toToolStats(acc.tools),
|
|
857
943
|
skills: toSkillStats(acc.skills),
|
|
858
|
-
projects: toProjectStats(acc.projects),
|
|
859
944
|
agent: { ...acc.agent },
|
|
860
|
-
titles: acc.titles,
|
|
861
945
|
sessionMessages: acc.sessionMessages,
|
|
862
946
|
complete: true
|
|
863
947
|
};
|
|
@@ -867,7 +951,7 @@ function numTok(v) {
|
|
|
867
951
|
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
868
952
|
}
|
|
869
953
|
function createCodexContext() {
|
|
870
|
-
return {
|
|
954
|
+
return { pending: [] };
|
|
871
955
|
}
|
|
872
956
|
function processCodexRecord(rec, acc, ctx) {
|
|
873
957
|
if (!rec || typeof rec !== "object") return;
|
|
@@ -875,11 +959,7 @@ function processCodexRecord(rec, acc, ctx) {
|
|
|
875
959
|
if (!r.payload || typeof r.payload !== "object") return;
|
|
876
960
|
const pl = r.payload;
|
|
877
961
|
const ptype = pl.type;
|
|
878
|
-
if (r.type === "session_meta" || r.type === "turn_context")
|
|
879
|
-
if (typeof pl.cwd === "string" && pl.cwd) ctx.cwd = pl.cwd;
|
|
880
|
-
if (typeof pl.model === "string" && pl.model) ctx.model = pl.model;
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
962
|
+
if (r.type === "session_meta" || r.type === "turn_context") return;
|
|
883
963
|
if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
|
|
884
964
|
const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
|
|
885
965
|
const name = raw.slice(0, 128);
|
|
@@ -905,19 +985,6 @@ function processCodexRecord(rec, acc, ctx) {
|
|
|
905
985
|
}
|
|
906
986
|
acc.agent.messageCount += 1;
|
|
907
987
|
acc.agent.totalTokens += tokens;
|
|
908
|
-
if (ctx.cwd) {
|
|
909
|
-
const name = basename(ctx.cwd).slice(0, 128) || "unknown";
|
|
910
|
-
const cost = estimateCostUSD(ctx.model, {
|
|
911
|
-
inputTokens,
|
|
912
|
-
outputTokens,
|
|
913
|
-
cacheCreationTokens: 0,
|
|
914
|
-
cacheReadTokens
|
|
915
|
-
});
|
|
916
|
-
const proj = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
|
|
917
|
-
proj.tokens += tokens;
|
|
918
|
-
proj.costUSD += cost;
|
|
919
|
-
acc.projects.set(name, proj);
|
|
920
|
-
}
|
|
921
988
|
const per = ctx.pending.length > 0 ? Math.floor(tokens / ctx.pending.length) : 0;
|
|
922
989
|
for (const tu of ctx.pending) {
|
|
923
990
|
const t = acc.tools.get(tu.name);
|
|
@@ -1272,6 +1339,404 @@ async function collectCursor() {
|
|
|
1272
1339
|
return { entries: [], blocks: [], found: false };
|
|
1273
1340
|
}
|
|
1274
1341
|
|
|
1342
|
+
// src/native/claude.ts
|
|
1343
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
1344
|
+
import { homedir as homedir5 } from "node:os";
|
|
1345
|
+
import { join as join6 } from "node:path";
|
|
1346
|
+
|
|
1347
|
+
// src/pricing.ts
|
|
1348
|
+
var TABLE = [
|
|
1349
|
+
{ match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
1350
|
+
{ match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
|
|
1351
|
+
{ match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
|
|
1352
|
+
{ match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
1353
|
+
{ match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
|
|
1354
|
+
{ match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
|
|
1355
|
+
{ match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
|
|
1356
|
+
{ match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
|
|
1357
|
+
];
|
|
1358
|
+
function estimateCostUSD(model, t) {
|
|
1359
|
+
const row = TABLE.find((r) => r.match.test(model));
|
|
1360
|
+
if (!row) return 0;
|
|
1361
|
+
const p = row.price;
|
|
1362
|
+
const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
|
|
1363
|
+
return usd > 0 ? usd : 0;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/native/claude.ts
|
|
1367
|
+
function num3(n) {
|
|
1368
|
+
const v = Math.round(Number(n));
|
|
1369
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
1370
|
+
}
|
|
1371
|
+
function reqTokens(r) {
|
|
1372
|
+
return r.inputTokens + r.outputTokens + r.cacheCreationTokens + r.cacheReadTokens;
|
|
1373
|
+
}
|
|
1374
|
+
function localDate(iso) {
|
|
1375
|
+
const t = Date.parse(iso);
|
|
1376
|
+
if (!Number.isFinite(t)) return null;
|
|
1377
|
+
const d = new Date(t);
|
|
1378
|
+
const y = d.getFullYear();
|
|
1379
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1380
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1381
|
+
return `${y}-${m}-${day}`;
|
|
1382
|
+
}
|
|
1383
|
+
var syntheticCounter = 0;
|
|
1384
|
+
function parseClaudeLine(raw) {
|
|
1385
|
+
const trimmed = raw.trim();
|
|
1386
|
+
if (!trimmed) return null;
|
|
1387
|
+
let obj;
|
|
1388
|
+
try {
|
|
1389
|
+
obj = JSON.parse(trimmed);
|
|
1390
|
+
} catch {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
const message = obj.message;
|
|
1394
|
+
if (!message || typeof message !== "object") return null;
|
|
1395
|
+
const usage = message.usage;
|
|
1396
|
+
if (!usage || typeof usage !== "object") return null;
|
|
1397
|
+
if (message.role !== void 0 && message.role !== "assistant") return null;
|
|
1398
|
+
const date = localDate(String(obj.timestamp ?? ""));
|
|
1399
|
+
if (!date) return null;
|
|
1400
|
+
const tsParsed = Date.parse(String(obj.timestamp ?? ""));
|
|
1401
|
+
const ts = Number.isFinite(tsParsed) ? tsParsed : 0;
|
|
1402
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
1403
|
+
const requestId = typeof obj.requestId === "string" ? obj.requestId : "";
|
|
1404
|
+
const hasRealId = messageId !== "" || requestId !== "";
|
|
1405
|
+
const key = hasRealId ? `${messageId}|${requestId}` : `synthetic|${date}|${syntheticCounter += 1}`;
|
|
1406
|
+
return {
|
|
1407
|
+
key,
|
|
1408
|
+
hasRealId,
|
|
1409
|
+
date,
|
|
1410
|
+
ts,
|
|
1411
|
+
model: typeof message.model === "string" ? message.model : "unknown",
|
|
1412
|
+
inputTokens: num3(usage.input_tokens),
|
|
1413
|
+
outputTokens: num3(usage.output_tokens),
|
|
1414
|
+
cacheCreationTokens: num3(usage.cache_creation_input_tokens),
|
|
1415
|
+
cacheReadTokens: num3(usage.cache_read_input_tokens)
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function accumulateClaudeLines(acc, lines) {
|
|
1419
|
+
for (const line of lines) {
|
|
1420
|
+
const r = parseClaudeLine(line);
|
|
1421
|
+
if (!r) continue;
|
|
1422
|
+
const prev = acc.get(r.key);
|
|
1423
|
+
if (!prev || reqTokens(r) > reqTokens(prev)) acc.set(r.key, r);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function finalizeClaudeEntries(acc) {
|
|
1427
|
+
const byDayModel = /* @__PURE__ */ new Map();
|
|
1428
|
+
for (const r of acc.values()) {
|
|
1429
|
+
const k = `${r.date}|${r.model}`;
|
|
1430
|
+
let b = byDayModel.get(k);
|
|
1431
|
+
if (!b) {
|
|
1432
|
+
b = {
|
|
1433
|
+
date: r.date,
|
|
1434
|
+
model: r.model,
|
|
1435
|
+
inputTokens: 0,
|
|
1436
|
+
outputTokens: 0,
|
|
1437
|
+
cacheCreationTokens: 0,
|
|
1438
|
+
cacheReadTokens: 0,
|
|
1439
|
+
requestCount: 0
|
|
1440
|
+
};
|
|
1441
|
+
byDayModel.set(k, b);
|
|
1442
|
+
}
|
|
1443
|
+
b.inputTokens += r.inputTokens;
|
|
1444
|
+
b.outputTokens += r.outputTokens;
|
|
1445
|
+
b.cacheCreationTokens += r.cacheCreationTokens;
|
|
1446
|
+
b.cacheReadTokens += r.cacheReadTokens;
|
|
1447
|
+
if (r.hasRealId) b.requestCount += 1;
|
|
1448
|
+
}
|
|
1449
|
+
const entries = [];
|
|
1450
|
+
for (const b of byDayModel.values()) {
|
|
1451
|
+
const tokens = b.inputTokens + b.outputTokens + b.cacheCreationTokens + b.cacheReadTokens;
|
|
1452
|
+
if (tokens === 0) continue;
|
|
1453
|
+
entries.push({
|
|
1454
|
+
date: b.date,
|
|
1455
|
+
tool: "claude",
|
|
1456
|
+
model: b.model,
|
|
1457
|
+
inputTokens: b.inputTokens,
|
|
1458
|
+
outputTokens: b.outputTokens,
|
|
1459
|
+
cacheCreationTokens: b.cacheCreationTokens,
|
|
1460
|
+
cacheReadTokens: b.cacheReadTokens,
|
|
1461
|
+
costUSD: estimateCostUSD(b.model, b),
|
|
1462
|
+
origin: "cli",
|
|
1463
|
+
verified: false,
|
|
1464
|
+
requestCount: b.requestCount
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
return entries;
|
|
1468
|
+
}
|
|
1469
|
+
function resolveClaudeConfigRoots(env = process.env) {
|
|
1470
|
+
const override = env.CLAUDE_CONFIG_DIR;
|
|
1471
|
+
return override ? override.split(",").map((s) => s.trim()).filter(Boolean) : [join6(homedir5(), ".claude"), join6(homedir5(), ".config", "claude")];
|
|
1472
|
+
}
|
|
1473
|
+
function resolveClaudeProjectDirs(env = process.env) {
|
|
1474
|
+
return resolveClaudeConfigRoots(env).map((r) => join6(r, "projects"));
|
|
1475
|
+
}
|
|
1476
|
+
async function listJsonl(dir) {
|
|
1477
|
+
let dirents;
|
|
1478
|
+
try {
|
|
1479
|
+
dirents = await readdir(dir, { withFileTypes: true });
|
|
1480
|
+
} catch {
|
|
1481
|
+
return [];
|
|
1482
|
+
}
|
|
1483
|
+
const out = [];
|
|
1484
|
+
for (const d of dirents) {
|
|
1485
|
+
const full = join6(dir, d.name);
|
|
1486
|
+
if (d.isDirectory()) {
|
|
1487
|
+
out.push(...await listJsonl(full));
|
|
1488
|
+
} else if (d.isFile() && d.name.endsWith(".jsonl")) {
|
|
1489
|
+
out.push(full);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return out;
|
|
1493
|
+
}
|
|
1494
|
+
var NATIVE_READ_BUDGET_MS = 2e4;
|
|
1495
|
+
function* splitLines(content) {
|
|
1496
|
+
let start = 0;
|
|
1497
|
+
for (let i = 0; i < content.length; i++) {
|
|
1498
|
+
if (content[i] === "\n") {
|
|
1499
|
+
yield content.slice(start, i);
|
|
1500
|
+
start = i + 1;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (start < content.length) yield content.slice(start);
|
|
1504
|
+
}
|
|
1505
|
+
async function collectClaudeNative(env = process.env, opts = {}) {
|
|
1506
|
+
const dirs = resolveClaudeProjectDirs(env);
|
|
1507
|
+
const files = [];
|
|
1508
|
+
for (const dir of dirs) files.push(...await listJsonl(dir));
|
|
1509
|
+
if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
|
|
1510
|
+
const now = opts.now ?? Date.now;
|
|
1511
|
+
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
|
|
1512
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1513
|
+
let scanned = 0;
|
|
1514
|
+
for (const f of files) {
|
|
1515
|
+
if (now() > deadline) {
|
|
1516
|
+
return { entries: [], found: false, filesScanned: scanned, timedOut: true };
|
|
1517
|
+
}
|
|
1518
|
+
let content;
|
|
1519
|
+
try {
|
|
1520
|
+
content = await readFile(f, "utf8");
|
|
1521
|
+
} catch {
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
accumulateClaudeLines(acc, splitLines(content));
|
|
1525
|
+
scanned += 1;
|
|
1526
|
+
}
|
|
1527
|
+
return { entries: finalizeClaudeEntries(acc), found: true, filesScanned: scanned };
|
|
1528
|
+
}
|
|
1529
|
+
async function collectClaudeRequests(env = process.env, opts = {}) {
|
|
1530
|
+
const dirs = resolveClaudeProjectDirs(env);
|
|
1531
|
+
const files = [];
|
|
1532
|
+
for (const dir of dirs) files.push(...await listJsonl(dir));
|
|
1533
|
+
if (files.length === 0) return { requests: [], found: false };
|
|
1534
|
+
const now = opts.now ?? Date.now;
|
|
1535
|
+
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
|
|
1536
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1537
|
+
for (const f of files) {
|
|
1538
|
+
if (now() > deadline) {
|
|
1539
|
+
return { requests: [...acc.values()], found: true, timedOut: true };
|
|
1540
|
+
}
|
|
1541
|
+
let content;
|
|
1542
|
+
try {
|
|
1543
|
+
content = await readFile(f, "utf8");
|
|
1544
|
+
} catch {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
accumulateClaudeLines(acc, splitLines(content));
|
|
1548
|
+
}
|
|
1549
|
+
return { requests: [...acc.values()], found: true };
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/native/codex.ts
|
|
1553
|
+
import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
1554
|
+
import { homedir as homedir6 } from "node:os";
|
|
1555
|
+
import { join as join7 } from "node:path";
|
|
1556
|
+
function num4(n) {
|
|
1557
|
+
const v = Math.round(Number(n));
|
|
1558
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
1559
|
+
}
|
|
1560
|
+
function localDate2(iso) {
|
|
1561
|
+
const t = Date.parse(iso);
|
|
1562
|
+
if (!Number.isFinite(t)) return null;
|
|
1563
|
+
const d = new Date(t);
|
|
1564
|
+
const y = d.getFullYear();
|
|
1565
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1566
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1567
|
+
return `${y}-${m}-${day}`;
|
|
1568
|
+
}
|
|
1569
|
+
function readTokenFields(payload) {
|
|
1570
|
+
const info = payload.info;
|
|
1571
|
+
const src = info?.total_token_usage ?? payload;
|
|
1572
|
+
const input = src.input_tokens;
|
|
1573
|
+
const output = src.output_tokens;
|
|
1574
|
+
if (input === void 0 && output === void 0) return null;
|
|
1575
|
+
return {
|
|
1576
|
+
input: num4(input),
|
|
1577
|
+
cached: num4(src.cached_input_tokens),
|
|
1578
|
+
output: num4(output),
|
|
1579
|
+
reasoning: num4(src.reasoning_output_tokens)
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function parseCodexRollout(lines) {
|
|
1583
|
+
let model = "unknown";
|
|
1584
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
1585
|
+
let lastSeenDate = null;
|
|
1586
|
+
for (const raw of lines) {
|
|
1587
|
+
const trimmed = raw.trim();
|
|
1588
|
+
if (!trimmed) continue;
|
|
1589
|
+
let obj;
|
|
1590
|
+
try {
|
|
1591
|
+
obj = JSON.parse(trimmed);
|
|
1592
|
+
} catch {
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const payload = obj.payload;
|
|
1596
|
+
if (!payload || typeof payload !== "object") continue;
|
|
1597
|
+
const kind = obj.type;
|
|
1598
|
+
if (kind === "session_meta" || kind === "turn_context") {
|
|
1599
|
+
if (typeof payload.model === "string" && payload.model) model = payload.model;
|
|
1600
|
+
}
|
|
1601
|
+
if (payload.type === "token_count") {
|
|
1602
|
+
const fields = readTokenFields(payload);
|
|
1603
|
+
if (!fields) continue;
|
|
1604
|
+
const parsed = localDate2(String(obj.timestamp ?? ""));
|
|
1605
|
+
const day = parsed ?? lastSeenDate;
|
|
1606
|
+
if (!day) continue;
|
|
1607
|
+
lastSeenDate = day;
|
|
1608
|
+
const e = perDay.get(day);
|
|
1609
|
+
if (e) {
|
|
1610
|
+
e.cum = fields;
|
|
1611
|
+
e.turns += 1;
|
|
1612
|
+
} else {
|
|
1613
|
+
perDay.set(day, { cum: fields, turns: 1 });
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (perDay.size === 0) return [];
|
|
1618
|
+
const dates = [...perDay.keys()].sort();
|
|
1619
|
+
const out = [];
|
|
1620
|
+
let prev = { input: 0, cached: 0, output: 0, reasoning: 0 };
|
|
1621
|
+
for (const date of dates) {
|
|
1622
|
+
const { cum, turns } = perDay.get(date);
|
|
1623
|
+
const dInput = Math.max(0, cum.input - prev.input);
|
|
1624
|
+
const dCached = Math.max(0, cum.cached - prev.cached);
|
|
1625
|
+
const dOutput = Math.max(0, cum.output - prev.output);
|
|
1626
|
+
const dReasoning = Math.max(0, cum.reasoning - prev.reasoning);
|
|
1627
|
+
prev = cum;
|
|
1628
|
+
const cacheReadTokens = dCached;
|
|
1629
|
+
const inputTokens = Math.max(0, dInput - dCached);
|
|
1630
|
+
const outputTokens = dOutput + dReasoning;
|
|
1631
|
+
if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
|
|
1632
|
+
out.push({
|
|
1633
|
+
date,
|
|
1634
|
+
model,
|
|
1635
|
+
inputTokens,
|
|
1636
|
+
outputTokens,
|
|
1637
|
+
cacheCreationTokens: 0,
|
|
1638
|
+
cacheReadTokens,
|
|
1639
|
+
turnCount: turns
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
return out;
|
|
1643
|
+
}
|
|
1644
|
+
function accumulateCodexSession(acc, lines) {
|
|
1645
|
+
for (const s of parseCodexRollout(lines)) {
|
|
1646
|
+
const k = `${s.date}|${s.model}`;
|
|
1647
|
+
let b = acc.get(k);
|
|
1648
|
+
if (!b) {
|
|
1649
|
+
b = {
|
|
1650
|
+
date: s.date,
|
|
1651
|
+
model: s.model,
|
|
1652
|
+
inputTokens: 0,
|
|
1653
|
+
outputTokens: 0,
|
|
1654
|
+
cacheCreationTokens: 0,
|
|
1655
|
+
cacheReadTokens: 0,
|
|
1656
|
+
requestCount: 0
|
|
1657
|
+
};
|
|
1658
|
+
acc.set(k, b);
|
|
1659
|
+
}
|
|
1660
|
+
b.inputTokens += s.inputTokens;
|
|
1661
|
+
b.outputTokens += s.outputTokens;
|
|
1662
|
+
b.cacheCreationTokens += s.cacheCreationTokens;
|
|
1663
|
+
b.cacheReadTokens += s.cacheReadTokens;
|
|
1664
|
+
b.requestCount += s.turnCount;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function finalizeCodexEntries(acc) {
|
|
1668
|
+
const entries = [];
|
|
1669
|
+
for (const b of acc.values()) {
|
|
1670
|
+
entries.push({
|
|
1671
|
+
date: b.date,
|
|
1672
|
+
tool: "codex",
|
|
1673
|
+
model: b.model,
|
|
1674
|
+
inputTokens: b.inputTokens,
|
|
1675
|
+
outputTokens: b.outputTokens,
|
|
1676
|
+
cacheCreationTokens: b.cacheCreationTokens,
|
|
1677
|
+
cacheReadTokens: b.cacheReadTokens,
|
|
1678
|
+
costUSD: estimateCostUSD(b.model, b),
|
|
1679
|
+
origin: "cli",
|
|
1680
|
+
verified: false,
|
|
1681
|
+
requestCount: b.requestCount
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
return entries;
|
|
1685
|
+
}
|
|
1686
|
+
function resolveCodexSessionsDir(env = process.env) {
|
|
1687
|
+
const home = env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join7(homedir6(), ".codex");
|
|
1688
|
+
return join7(home, "sessions");
|
|
1689
|
+
}
|
|
1690
|
+
async function listJsonl2(dir) {
|
|
1691
|
+
let dirents;
|
|
1692
|
+
try {
|
|
1693
|
+
dirents = await readdir2(dir, { withFileTypes: true });
|
|
1694
|
+
} catch {
|
|
1695
|
+
return [];
|
|
1696
|
+
}
|
|
1697
|
+
const out = [];
|
|
1698
|
+
for (const d of dirents) {
|
|
1699
|
+
const full = join7(dir, d.name);
|
|
1700
|
+
if (d.isDirectory()) out.push(...await listJsonl2(full));
|
|
1701
|
+
else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
|
|
1702
|
+
}
|
|
1703
|
+
return out;
|
|
1704
|
+
}
|
|
1705
|
+
function* splitLines2(content) {
|
|
1706
|
+
let start = 0;
|
|
1707
|
+
for (let i = 0; i < content.length; i++) {
|
|
1708
|
+
if (content[i] === "\n") {
|
|
1709
|
+
yield content.slice(start, i);
|
|
1710
|
+
start = i + 1;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (start < content.length) yield content.slice(start);
|
|
1714
|
+
}
|
|
1715
|
+
var NATIVE_READ_BUDGET_MS2 = 2e4;
|
|
1716
|
+
async function collectCodexNative(env = process.env, opts = {}) {
|
|
1717
|
+
const dir = resolveCodexSessionsDir(env);
|
|
1718
|
+
const files = await listJsonl2(dir);
|
|
1719
|
+
if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
|
|
1720
|
+
const now = opts.now ?? Date.now;
|
|
1721
|
+
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2);
|
|
1722
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1723
|
+
let scanned = 0;
|
|
1724
|
+
for (const f of files) {
|
|
1725
|
+
if (now() > deadline) {
|
|
1726
|
+
return { entries: [], found: false, filesScanned: scanned, timedOut: true };
|
|
1727
|
+
}
|
|
1728
|
+
let content;
|
|
1729
|
+
try {
|
|
1730
|
+
content = await readFile2(f, "utf8");
|
|
1731
|
+
} catch {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
accumulateCodexSession(acc, splitLines2(content));
|
|
1735
|
+
scanned += 1;
|
|
1736
|
+
}
|
|
1737
|
+
return { entries: finalizeCodexEntries(acc), found: true, filesScanned: scanned };
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1275
1740
|
// src/collect.ts
|
|
1276
1741
|
var execFileAsync = promisify(execFile);
|
|
1277
1742
|
var SOURCES = [
|
|
@@ -1340,9 +1805,12 @@ function mapCcusageDaily(tool, json) {
|
|
|
1340
1805
|
};
|
|
1341
1806
|
});
|
|
1342
1807
|
} else {
|
|
1808
|
+
const modelsUsed = Array.isArray(day.modelsUsed) ? day.modelsUsed.filter(
|
|
1809
|
+
(m) => typeof m === "string" && m.length > 0
|
|
1810
|
+
) : [];
|
|
1343
1811
|
candidates = [
|
|
1344
1812
|
{
|
|
1345
|
-
model: "unknown",
|
|
1813
|
+
model: modelsUsed.length === 1 ? modelsUsed[0] : "unknown",
|
|
1346
1814
|
inputTokens: norm(day.inputTokens),
|
|
1347
1815
|
outputTokens: norm(day.outputTokens),
|
|
1348
1816
|
cacheCreationTokens: norm(day.cacheCreationTokens),
|
|
@@ -1409,7 +1877,7 @@ function resolveCcusageBin() {
|
|
|
1409
1877
|
const pkgPath = require3.resolve("ccusage/package.json");
|
|
1410
1878
|
const pkg = require3("ccusage/package.json");
|
|
1411
1879
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
|
|
1412
|
-
const binPath =
|
|
1880
|
+
const binPath = join8(dirname3(pkgPath), rel);
|
|
1413
1881
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
1414
1882
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
1415
1883
|
}
|
|
@@ -1430,6 +1898,9 @@ function dedupeDaily(entries) {
|
|
|
1430
1898
|
prev.cacheReadTokens += e.cacheReadTokens;
|
|
1431
1899
|
prev.costUSD = Number((prev.costUSD + e.costUSD).toFixed(6));
|
|
1432
1900
|
prev.verified = prev.verified && e.verified;
|
|
1901
|
+
if (e.requestCount !== void 0 || prev.requestCount !== void 0) {
|
|
1902
|
+
prev.requestCount = (prev.requestCount ?? 0) + (e.requestCount ?? 0);
|
|
1903
|
+
}
|
|
1433
1904
|
}
|
|
1434
1905
|
return [...byKey.values()];
|
|
1435
1906
|
}
|
|
@@ -1462,7 +1933,18 @@ function dedupeBlocks(blocks) {
|
|
|
1462
1933
|
}
|
|
1463
1934
|
return [...byStart.values()];
|
|
1464
1935
|
}
|
|
1465
|
-
|
|
1936
|
+
function selectSourceEntries(source, ccusageEntries, native) {
|
|
1937
|
+
if (source === "claude" && native.claude.found && native.claude.entries.length > 0)
|
|
1938
|
+
return native.claude.entries;
|
|
1939
|
+
if (source === "codex" && native.codex.found && native.codex.entries.length > 0)
|
|
1940
|
+
return native.codex.entries;
|
|
1941
|
+
return ccusageEntries;
|
|
1942
|
+
}
|
|
1943
|
+
function ccusageClaudeEnv(env = process.env) {
|
|
1944
|
+
if (env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.trim()) return env;
|
|
1945
|
+
return { ...env, CLAUDE_CONFIG_DIR: resolveClaudeConfigRoots(env).join(",") };
|
|
1946
|
+
}
|
|
1947
|
+
async function runCcusageOnce(cmd, args, env) {
|
|
1466
1948
|
try {
|
|
1467
1949
|
const { stdout } = await execFileAsync(cmd, args, {
|
|
1468
1950
|
encoding: "utf8",
|
|
@@ -1470,7 +1952,8 @@ async function runCcusageOnce(cmd, args) {
|
|
|
1470
1952
|
// A single source shouldn't be able to hang the whole run. 25s is plenty
|
|
1471
1953
|
// for a healthy local read; a hung source gets killed and (if transient)
|
|
1472
1954
|
// retried once below rather than stalling everything for minutes.
|
|
1473
|
-
timeout: 25e3
|
|
1955
|
+
timeout: 25e3,
|
|
1956
|
+
...env ? { env } : {}
|
|
1474
1957
|
});
|
|
1475
1958
|
if (!stdout) return { json: null, transient: false };
|
|
1476
1959
|
try {
|
|
@@ -1484,18 +1967,29 @@ async function runCcusageOnce(cmd, args) {
|
|
|
1484
1967
|
return { json: null, transient };
|
|
1485
1968
|
}
|
|
1486
1969
|
}
|
|
1487
|
-
async function runCcusage(cmd, args) {
|
|
1488
|
-
const first = await runCcusageOnce(cmd, args);
|
|
1970
|
+
async function runCcusage(cmd, args, env) {
|
|
1971
|
+
const first = await runCcusageOnce(cmd, args, env);
|
|
1489
1972
|
if (first.json !== null || !first.transient) return first.json;
|
|
1490
|
-
return (await runCcusageOnce(cmd, args)).json;
|
|
1973
|
+
return (await runCcusageOnce(cmd, args, env)).json;
|
|
1491
1974
|
}
|
|
1492
1975
|
var COLLECT_STAGES = SOURCES.length + 4;
|
|
1493
1976
|
async function collectAll(onProgress) {
|
|
1494
1977
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
1495
1978
|
let done = 0;
|
|
1496
1979
|
const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
|
|
1980
|
+
const nativeClaudeTask = collectClaudeNative().catch(
|
|
1981
|
+
() => ({ entries: [], found: false, filesScanned: 0 })
|
|
1982
|
+
);
|
|
1983
|
+
const nativeCodexTask = collectCodexNative().catch(
|
|
1984
|
+
() => ({ entries: [], found: false, filesScanned: 0 })
|
|
1985
|
+
);
|
|
1497
1986
|
const sourceTasks = SOURCES.map(async (source) => {
|
|
1498
|
-
const
|
|
1987
|
+
const env = source === "claude" ? ccusageClaudeEnv() : void 0;
|
|
1988
|
+
const json = await runCcusage(
|
|
1989
|
+
cmd,
|
|
1990
|
+
[...prefixArgs, source, "daily", "--json", "--offline"],
|
|
1991
|
+
env
|
|
1992
|
+
);
|
|
1499
1993
|
tick();
|
|
1500
1994
|
return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
|
|
1501
1995
|
});
|
|
@@ -1519,18 +2013,22 @@ async function collectAll(onProgress) {
|
|
|
1519
2013
|
tick();
|
|
1520
2014
|
return a;
|
|
1521
2015
|
});
|
|
1522
|
-
const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
|
|
2016
|
+
const [sourceResults, sessions, blocks, cursor, attribution, nativeClaude, nativeCodex] = await Promise.all([
|
|
1523
2017
|
Promise.all(sourceTasks),
|
|
1524
2018
|
sessionTask,
|
|
1525
2019
|
blockTask,
|
|
1526
2020
|
cursorTask,
|
|
1527
|
-
attributionTask
|
|
2021
|
+
attributionTask,
|
|
2022
|
+
nativeClaudeTask,
|
|
2023
|
+
nativeCodexTask
|
|
1528
2024
|
]);
|
|
2025
|
+
const native = { claude: nativeClaude, codex: nativeCodex };
|
|
1529
2026
|
const entries = [];
|
|
1530
2027
|
const toolsFound = [];
|
|
1531
2028
|
for (const { source, mapped } of sourceResults) {
|
|
1532
|
-
|
|
1533
|
-
|
|
2029
|
+
const chosen = selectSourceEntries(source, mapped, native);
|
|
2030
|
+
if (chosen.length > 0) {
|
|
2031
|
+
entries.push(...chosen);
|
|
1534
2032
|
toolsFound.push(source);
|
|
1535
2033
|
}
|
|
1536
2034
|
}
|
|
@@ -1539,14 +2037,12 @@ async function collectAll(onProgress) {
|
|
|
1539
2037
|
blocks.push(...cursor.blocks);
|
|
1540
2038
|
toolsFound.push("cursor");
|
|
1541
2039
|
}
|
|
1542
|
-
const { tools, skills,
|
|
2040
|
+
const { tools, skills, agent, sessionMessages, complete } = attribution;
|
|
1543
2041
|
onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
|
|
1544
2042
|
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
1545
|
-
const title = titles.get(s.sessionId);
|
|
1546
2043
|
const messageCount = sessionMessages.get(s.sessionId);
|
|
1547
2044
|
return {
|
|
1548
2045
|
...s,
|
|
1549
|
-
...title ? { title } : {},
|
|
1550
2046
|
...messageCount ? { messageCount } : {}
|
|
1551
2047
|
};
|
|
1552
2048
|
});
|
|
@@ -1555,14 +2051,13 @@ async function collectAll(onProgress) {
|
|
|
1555
2051
|
// schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
|
|
1556
2052
|
// highest-token rows. Without this, a power user with >10000 distinct sessions
|
|
1557
2053
|
// would have their ENTIRE submit rejected with a 400 instead of a capped one.
|
|
1558
|
-
// tools/skills
|
|
2054
|
+
// tools/skills are already bounded upstream (attribution caps).
|
|
1559
2055
|
entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
|
|
1560
2056
|
sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
|
|
1561
2057
|
blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
|
|
1562
2058
|
toolsFound,
|
|
1563
2059
|
tools,
|
|
1564
2060
|
skills,
|
|
1565
|
-
projects,
|
|
1566
2061
|
agent,
|
|
1567
2062
|
attributionComplete: complete
|
|
1568
2063
|
};
|
|
@@ -5747,7 +6242,18 @@ var DailyUsageEntry = external_exports.object({
|
|
|
5747
6242
|
/** Where this entry came from. Defaults to the local CLI for back-compat. */
|
|
5748
6243
|
origin: UsageOrigin.default("cli"),
|
|
5749
6244
|
/** True when the numbers come from a provider's authoritative usage API. */
|
|
5750
|
-
verified: external_exports.boolean().default(false)
|
|
6245
|
+
verified: external_exports.boolean().default(false),
|
|
6246
|
+
/**
|
|
6247
|
+
* Structural fingerprint: the number of DISTINCT provider requests
|
|
6248
|
+
* (unique message-id + request-id pairs) that summed into this entry. A real
|
|
6249
|
+
* heavy day is the product of thousands of distinct API requests; a hand-typed
|
|
6250
|
+
* fabrication has none. The server uses this as an anti-fraud signal — a
|
|
6251
|
+
* billion-token day backed by ~zero real requests is the forgery signature.
|
|
6252
|
+
* Optional + back-compat: older CLIs and the ccusage fallback path (which
|
|
6253
|
+
* cannot see request ids) omit it, and an omitted fingerprint is never
|
|
6254
|
+
* penalized.
|
|
6255
|
+
*/
|
|
6256
|
+
requestCount: external_exports.number().int().nonnegative().optional()
|
|
5751
6257
|
});
|
|
5752
6258
|
var Timestamp = external_exports.string().min(1).max(40);
|
|
5753
6259
|
var SessionEntry = external_exports.object({
|
|
@@ -5760,8 +6266,6 @@ var SessionEntry = external_exports.object({
|
|
|
5760
6266
|
cacheReadTokens: tokenCount,
|
|
5761
6267
|
costUSD: external_exports.number().nonnegative(),
|
|
5762
6268
|
lastActivity: Timestamp,
|
|
5763
|
-
/** Human-readable AI-generated session title (from transcripts). Optional. */
|
|
5764
|
-
title: external_exports.string().max(200).optional(),
|
|
5765
6269
|
/** Number of assistant messages in this session (from transcripts). Optional. */
|
|
5766
6270
|
messageCount: external_exports.number().int().nonnegative().optional()
|
|
5767
6271
|
});
|
|
@@ -5778,11 +6282,6 @@ var ToolStat = external_exports.object({
|
|
|
5778
6282
|
/** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
|
|
5779
6283
|
tokens: external_exports.number().int().nonnegative().optional()
|
|
5780
6284
|
});
|
|
5781
|
-
var ProjectStat = external_exports.object({
|
|
5782
|
-
name: external_exports.string().min(1).max(128),
|
|
5783
|
-
tokens: external_exports.number().int().nonnegative(),
|
|
5784
|
-
costUSD: external_exports.number().nonnegative()
|
|
5785
|
-
});
|
|
5786
6285
|
var AgentStat = external_exports.object({
|
|
5787
6286
|
/** Total assistant messages across transcripts. */
|
|
5788
6287
|
messageCount: external_exports.number().int().nonnegative(),
|
|
@@ -5816,15 +6315,13 @@ var SubmitPayload = external_exports.object({
|
|
|
5816
6315
|
tools: external_exports.array(ToolStat).max(300).optional(),
|
|
5817
6316
|
/** Optional skill-usage frequencies parsed from local transcripts. */
|
|
5818
6317
|
skills: external_exports.array(SkillStat).max(300).optional(),
|
|
5819
|
-
/** Optional per-project usage totals parsed from local transcripts. */
|
|
5820
|
-
projects: external_exports.array(ProjectStat).max(500).optional(),
|
|
5821
6318
|
/** Optional subagent-vs-main rollup parsed from local transcripts. */
|
|
5822
6319
|
agent: AgentStat.optional(),
|
|
5823
6320
|
/**
|
|
5824
6321
|
* Set when the transcript scan completed within its time budget, i.e. the
|
|
5825
|
-
* tool/skill/
|
|
5826
|
-
*
|
|
5827
|
-
*
|
|
6322
|
+
* tool/skill/agent rollups are a FULL snapshot. The server refreshes the
|
|
6323
|
+
* dashboard breakdowns unconditionally for a full snapshot; for a partial one
|
|
6324
|
+
* (flag absent/false) it keeps its no-shrink guard. Back-compat: omittable.
|
|
5828
6325
|
*/
|
|
5829
6326
|
attributionComplete: external_exports.boolean().optional(),
|
|
5830
6327
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
@@ -5837,12 +6334,47 @@ var SubmitPayload = external_exports.object({
|
|
|
5837
6334
|
* never needs it, and a wrong/missing code only skips the org attach (the
|
|
5838
6335
|
* personal submit still succeeds).
|
|
5839
6336
|
*/
|
|
5840
|
-
orgCode: external_exports.string().min(1).max(64).optional()
|
|
6337
|
+
orgCode: external_exports.string().min(1).max(64).optional(),
|
|
6338
|
+
/**
|
|
6339
|
+
* The submitter's local UTC offset in minutes EAST of UTC (IST = +330,
|
|
6340
|
+
* US-Pacific DST = -420; i.e. `-new Date().getTimezoneOffset()`). ccusage dates
|
|
6341
|
+
* usage in this local zone, so the server uses the offset to compute each
|
|
6342
|
+
* member's daily/weekly leaderboard window in THEIR calendar day rather than a
|
|
6343
|
+
* UTC one. Back-compat: omittable — older CLIs don't send it and the board falls
|
|
6344
|
+
* back to its default offset. Range clamps to real zones (UTC-14..+14).
|
|
6345
|
+
*/
|
|
6346
|
+
tzOffsetMinutes: external_exports.number().int().min(-840).max(840).optional()
|
|
5841
6347
|
});
|
|
5842
6348
|
var AnonSubmitPayload = SubmitPayload.extend({
|
|
5843
6349
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
5844
6350
|
anonKey: external_exports.string().min(16).max(128)
|
|
5845
6351
|
});
|
|
6352
|
+
var VerifyRequestRecord = external_exports.object({
|
|
6353
|
+
/** Local calendar date (YYYY-MM-DD) this request is bucketed under. */
|
|
6354
|
+
date: DateString,
|
|
6355
|
+
/** Epoch milliseconds of the request's final (max-token) transcript line. */
|
|
6356
|
+
ts: external_exports.number().int().nonnegative(),
|
|
6357
|
+
tool: external_exports.string().min(1).max(64),
|
|
6358
|
+
model: external_exports.string().min(1).max(128),
|
|
6359
|
+
inputTokens: tokenCount,
|
|
6360
|
+
outputTokens: tokenCount,
|
|
6361
|
+
cacheCreationTokens: tokenCount,
|
|
6362
|
+
cacheReadTokens: tokenCount,
|
|
6363
|
+
/** sha256(msgId|reqId), truncated — preserves uniqueness without revealing the id. */
|
|
6364
|
+
reqHash: external_exports.string().min(1).max(64)
|
|
6365
|
+
});
|
|
6366
|
+
var VerifyPayload = external_exports.object({
|
|
6367
|
+
cliVersion: external_exports.string().min(1).max(32),
|
|
6368
|
+
/** The per-request skeleton (bounded/sampled — see `truncated`). */
|
|
6369
|
+
requests: external_exports.array(VerifyRequestRecord).min(1).max(1e5),
|
|
6370
|
+
/**
|
|
6371
|
+
* True when the local corpus exceeded the client upload cap and was sampled to
|
|
6372
|
+
* the most recent N requests. The server treats a truncated upload conservatively
|
|
6373
|
+
* (it never AUTO-passes a truncated one — it can still auto-FAIL on a physical
|
|
6374
|
+
* impossibility, or route to a human).
|
|
6375
|
+
*/
|
|
6376
|
+
truncated: external_exports.boolean().optional()
|
|
6377
|
+
});
|
|
5846
6378
|
function entryTotalTokens(e) {
|
|
5847
6379
|
return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
|
|
5848
6380
|
}
|
|
@@ -5923,7 +6455,16 @@ var OrgProvisionInput = external_exports.object({
|
|
|
5923
6455
|
ownerEmail: external_exports.string().email().max(254).optional(),
|
|
5924
6456
|
description: external_exports.string().max(2e3).optional(),
|
|
5925
6457
|
boardVisibility: OrgBoardVisibility.optional(),
|
|
5926
|
-
window: OrgWindow.optional()
|
|
6458
|
+
window: OrgWindow.optional(),
|
|
6459
|
+
/** Brand accent applied to the org homepage theming. */
|
|
6460
|
+
accentColor: HexColor.optional(),
|
|
6461
|
+
/**
|
|
6462
|
+
* Create the org UNOWNED and mint a one-time owner-claim ("admin sign-in")
|
|
6463
|
+
* token in the same call. The first person to open the link + sign in becomes
|
|
6464
|
+
* the owner; the token is then consumed. When set, an owner email/handle is
|
|
6465
|
+
* NOT required — the link is how the first admin is assigned.
|
|
6466
|
+
*/
|
|
6467
|
+
issueClaimLink: external_exports.boolean().optional()
|
|
5927
6468
|
});
|
|
5928
6469
|
var OrgSettingsInput = external_exports.object({
|
|
5929
6470
|
name: external_exports.string().min(1).max(120).optional(),
|
|
@@ -5949,26 +6490,49 @@ function formatTokens(n) {
|
|
|
5949
6490
|
function formatUSD(n) {
|
|
5950
6491
|
return `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
5951
6492
|
}
|
|
6493
|
+
function sanitizeServerText(s) {
|
|
6494
|
+
return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
6495
|
+
}
|
|
5952
6496
|
function submitNextStepLines(result) {
|
|
5953
6497
|
if (result.boardUrl) {
|
|
5954
6498
|
const code = result.boardCode ?? result.boardUrl.split("/").filter(Boolean).pop() ?? "";
|
|
5955
6499
|
return [
|
|
5956
|
-
` \u{1F91D} You're on the board: ${result.boardUrl}`,
|
|
6500
|
+
` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
|
|
5957
6501
|
" \u2192 Open it to see who burned more.",
|
|
5958
|
-
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${code}`,
|
|
6502
|
+
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`,
|
|
5959
6503
|
" \u2192 Sign in on the page and add your X to claim your spot and own your rank."
|
|
5960
6504
|
];
|
|
5961
6505
|
}
|
|
5962
6506
|
return [
|
|
5963
|
-
` Your dashboard: ${result.dashboardUrl}`,
|
|
6507
|
+
` Your dashboard: ${sanitizeServerText(result.dashboardUrl)}`,
|
|
5964
6508
|
" \u2192 Sign in and add your X on the page to get on the leaderboard and claim your rank.",
|
|
5965
6509
|
" Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
|
|
5966
6510
|
];
|
|
5967
6511
|
}
|
|
6512
|
+
function signedInNextStepLines(result) {
|
|
6513
|
+
if (result.orgBoardUrl) {
|
|
6514
|
+
return [
|
|
6515
|
+
` \u{1F3E2} You're on your team board: ${sanitizeServerText(result.orgBoardUrl)}`,
|
|
6516
|
+
" \u2192 Open it to see who burned more."
|
|
6517
|
+
];
|
|
6518
|
+
}
|
|
6519
|
+
if (result.boardUrl) {
|
|
6520
|
+
const code = result.boardUrl.split("/").filter(Boolean).pop() ?? "";
|
|
6521
|
+
return [
|
|
6522
|
+
` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
|
|
6523
|
+
" \u2192 Open it to see who burned more.",
|
|
6524
|
+
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`
|
|
6525
|
+
];
|
|
6526
|
+
}
|
|
6527
|
+
return [
|
|
6528
|
+
` Your dashboard: ${sanitizeServerText(result.profileUrl)}`,
|
|
6529
|
+
" \u2192 Open it to see your rank and share your profile."
|
|
6530
|
+
];
|
|
6531
|
+
}
|
|
5968
6532
|
|
|
5969
6533
|
// src/local-dashboard.ts
|
|
5970
6534
|
function esc(s) {
|
|
5971
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6535
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5972
6536
|
}
|
|
5973
6537
|
function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
|
|
5974
6538
|
const today = generatedAt.toISOString().slice(0, 10);
|
|
@@ -6181,8 +6745,12 @@ async function publishLocal(payload, deps) {
|
|
|
6181
6745
|
}
|
|
6182
6746
|
const key = deps.ensureAnonKey();
|
|
6183
6747
|
const res = await deps.anonSubmit(key, payload);
|
|
6184
|
-
deps.log(` Published \u2014 you're on the board: ${res.dashboardUrl}`);
|
|
6185
|
-
|
|
6748
|
+
deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.dashboardUrl)}`);
|
|
6749
|
+
if (isTrustedWebUrl(res.dashboardUrl)) {
|
|
6750
|
+
deps.openBrowser(claimUrl(res.dashboardUrl, key));
|
|
6751
|
+
} else {
|
|
6752
|
+
deps.log(" (The server returned an unexpected address, so it was not auto-opened.)");
|
|
6753
|
+
}
|
|
6186
6754
|
deps.log(
|
|
6187
6755
|
" Sign in there to claim it, or `npx whoburnedmore private` to hide it again."
|
|
6188
6756
|
);
|
|
@@ -6264,7 +6832,7 @@ async function confirm(question) {
|
|
|
6264
6832
|
function showLocalDashboard(payload) {
|
|
6265
6833
|
const dir = defaultConfigDir();
|
|
6266
6834
|
mkdirSync3(dir, { recursive: true });
|
|
6267
|
-
const file =
|
|
6835
|
+
const file = join9(dir, "dashboard.html");
|
|
6268
6836
|
writeFileSync3(
|
|
6269
6837
|
file,
|
|
6270
6838
|
renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
|
|
@@ -6299,7 +6867,7 @@ async function run(flags) {
|
|
|
6299
6867
|
} finally {
|
|
6300
6868
|
progress.stop();
|
|
6301
6869
|
}
|
|
6302
|
-
const { entries, sessions, blocks, tools, skills,
|
|
6870
|
+
const { entries, sessions, blocks, tools, skills, agent, attributionComplete } = collected;
|
|
6303
6871
|
if (entries.length === 0) {
|
|
6304
6872
|
console.log();
|
|
6305
6873
|
console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
|
|
@@ -6307,13 +6875,13 @@ async function run(flags) {
|
|
|
6307
6875
|
return;
|
|
6308
6876
|
}
|
|
6309
6877
|
const payload = { cliVersion: VERSION, entries };
|
|
6878
|
+
payload.tzOffsetMinutes = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
6310
6879
|
if (sessions.length > 0) payload.sessions = sessions;
|
|
6311
6880
|
if (blocks.length > 0) payload.blocks = blocks;
|
|
6312
6881
|
if (tools.length > 0) payload.tools = tools;
|
|
6313
6882
|
if (skills.length > 0) payload.skills = skills;
|
|
6314
|
-
if (projects.length > 0) payload.projects = projects;
|
|
6315
6883
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
6316
|
-
if (attributionComplete && (tools.length > 0 || skills.length > 0
|
|
6884
|
+
if (attributionComplete && (tools.length > 0 || skills.length > 0))
|
|
6317
6885
|
payload.attributionComplete = true;
|
|
6318
6886
|
applyScope(payload, flags);
|
|
6319
6887
|
if (flags.dryRun) {
|
|
@@ -6338,7 +6906,132 @@ async function run(flags) {
|
|
|
6338
6906
|
console.log(pc2.dim(" --no-submit: skipped the dashboard."));
|
|
6339
6907
|
return;
|
|
6340
6908
|
}
|
|
6341
|
-
const
|
|
6909
|
+
const cfg = loadConfig();
|
|
6910
|
+
const canSignIn = !flags.quiet && Boolean(process.stdout.isTTY);
|
|
6911
|
+
if (cfg?.cliToken) {
|
|
6912
|
+
await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
|
|
6913
|
+
} else if (cfg?.anonKey) {
|
|
6914
|
+
await submitWithDeviceKey(cfg.anonKey, payload, flags);
|
|
6915
|
+
} else if (canSignIn) {
|
|
6916
|
+
const auth = await ensureSignedIn();
|
|
6917
|
+
if (!auth) return;
|
|
6918
|
+
await submitSignedIn(auth.token, payload, flags, canSignIn);
|
|
6919
|
+
} else if (!flags.quiet) {
|
|
6920
|
+
console.log(pc2.yellow(" Sign in to put your usage on the leaderboard."));
|
|
6921
|
+
console.log(
|
|
6922
|
+
pc2.dim(
|
|
6923
|
+
" Run `npx whoburnedmore` in an interactive terminal to sign in, or `npx whoburnedmore link --token=\u2026` (from your signed-in profile) for servers/CI."
|
|
6924
|
+
)
|
|
6925
|
+
);
|
|
6926
|
+
} else {
|
|
6927
|
+
return;
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
function sleep(ms) {
|
|
6931
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6932
|
+
}
|
|
6933
|
+
async function ensureSignedIn() {
|
|
6934
|
+
const cfg = loadConfig();
|
|
6935
|
+
if (cfg?.cliToken) return { token: cfg.cliToken, handle: cfg.handle ?? "" };
|
|
6936
|
+
let dev;
|
|
6937
|
+
try {
|
|
6938
|
+
dev = await deviceStart();
|
|
6939
|
+
} catch (err) {
|
|
6940
|
+
console.log(pc2.yellow(` Couldn't start sign-in: ${err.message}`));
|
|
6941
|
+
return null;
|
|
6942
|
+
}
|
|
6943
|
+
console.log();
|
|
6944
|
+
console.log(pc2.bold(" Sign in to whoburnedmore to put your usage on the board."));
|
|
6945
|
+
console.log(` 1. Opening ${pc2.cyan(sanitizeServerText(dev.verifyUrl))} \u2014 or go there yourself.`);
|
|
6946
|
+
console.log(` 2. Approve this code: ${pc2.bold(sanitizeServerText(dev.userCode))}`);
|
|
6947
|
+
if (isTrustedWebUrl(dev.verifyUrl)) openBrowser(dev.verifyUrl);
|
|
6948
|
+
console.log(pc2.dim(" Waiting for you to approve in the browser\u2026"));
|
|
6949
|
+
const deadline = Date.now() + dev.expiresInSeconds * 1e3;
|
|
6950
|
+
const intervalMs = Math.max(1, dev.pollIntervalSeconds) * 1e3;
|
|
6951
|
+
while (Date.now() < deadline) {
|
|
6952
|
+
await sleep(intervalMs);
|
|
6953
|
+
let res;
|
|
6954
|
+
try {
|
|
6955
|
+
res = await devicePoll(dev.deviceCode);
|
|
6956
|
+
} catch {
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
if (res.status === "ok") {
|
|
6960
|
+
saveAuth(void 0, { cliToken: res.token, handle: res.handle });
|
|
6961
|
+
console.log(pc2.green(` \u2713 Signed in as @${sanitizeServerText(res.handle)}.`));
|
|
6962
|
+
return { token: res.token, handle: res.handle };
|
|
6963
|
+
}
|
|
6964
|
+
if (res.status === "expired") break;
|
|
6965
|
+
}
|
|
6966
|
+
console.log(
|
|
6967
|
+
pc2.yellow(" Sign-in timed out. Run `npx whoburnedmore` again to retry.")
|
|
6968
|
+
);
|
|
6969
|
+
return null;
|
|
6970
|
+
}
|
|
6971
|
+
async function submitSignedIn(token, payload, flags, interactive) {
|
|
6972
|
+
let result;
|
|
6973
|
+
try {
|
|
6974
|
+
result = await submit(token, payload);
|
|
6975
|
+
} catch (err) {
|
|
6976
|
+
if (err instanceof UnauthorizedError) {
|
|
6977
|
+
clearAuth();
|
|
6978
|
+
if (!interactive) return;
|
|
6979
|
+
const auth = await ensureSignedIn();
|
|
6980
|
+
if (!auth) return;
|
|
6981
|
+
result = await submit(auth.token, payload);
|
|
6982
|
+
} else {
|
|
6983
|
+
throw err;
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
try {
|
|
6987
|
+
recordSync();
|
|
6988
|
+
} catch {
|
|
6989
|
+
}
|
|
6990
|
+
const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.profileUrl;
|
|
6991
|
+
if (!flags.quiet) {
|
|
6992
|
+
console.log(
|
|
6993
|
+
pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
|
|
6994
|
+
);
|
|
6995
|
+
if (result.suppressed) {
|
|
6996
|
+
console.log();
|
|
6997
|
+
console.log(
|
|
6998
|
+
pc2.yellow(
|
|
6999
|
+
" \u26A0 You're currently OFF the leaderboard \u2014 we couldn't verify these numbers."
|
|
7000
|
+
)
|
|
7001
|
+
);
|
|
7002
|
+
console.log(
|
|
7003
|
+
pc2.dim(
|
|
7004
|
+
" Verify your usage to get back on \u2014 this sends a detailed per-request breakdown (timestamps + token counts, never your prompts or code)."
|
|
7005
|
+
)
|
|
7006
|
+
);
|
|
7007
|
+
if (process.stdin.isTTY && await confirm(" Verify now?")) {
|
|
7008
|
+
await runVerify();
|
|
7009
|
+
afterSubmitChores(flags);
|
|
7010
|
+
return;
|
|
7011
|
+
}
|
|
7012
|
+
console.log(
|
|
7013
|
+
pc2.dim(
|
|
7014
|
+
" Run `npx whoburnedmore verify` anytime, or appeal at whoburnedmore.com/appeal."
|
|
7015
|
+
)
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
if (isTrustedWebUrl(baseUrl)) {
|
|
7019
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
7020
|
+
openBrowser(baseUrl);
|
|
7021
|
+
} else {
|
|
7022
|
+
console.log(
|
|
7023
|
+
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
7024
|
+
);
|
|
7025
|
+
console.log(` ${sanitizeServerText(baseUrl)}`);
|
|
7026
|
+
}
|
|
7027
|
+
for (const line of signedInNextStepLines(result)) {
|
|
7028
|
+
if (line.includes("\u2192")) console.log(pc2.bold(line));
|
|
7029
|
+
else console.log(line);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
afterSubmitChores(flags);
|
|
7033
|
+
}
|
|
7034
|
+
async function submitWithDeviceKey(anonKey, payload, flags) {
|
|
6342
7035
|
const result = await anonSubmit(anonKey, payload);
|
|
6343
7036
|
try {
|
|
6344
7037
|
recordSync();
|
|
@@ -6366,28 +7059,26 @@ async function run(flags) {
|
|
|
6366
7059
|
console.log(
|
|
6367
7060
|
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
6368
7061
|
);
|
|
6369
|
-
console.log(` ${baseUrl}`);
|
|
7062
|
+
console.log(` ${sanitizeServerText(baseUrl)}`);
|
|
6370
7063
|
}
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
else if (line.startsWith(" Private until you do")) {
|
|
6376
|
-
if (!flags.quiet) console.log(pc2.dim(line));
|
|
6377
|
-
} else console.log(line);
|
|
6378
|
-
}
|
|
6379
|
-
if (!flags.quiet) {
|
|
6380
|
-
try {
|
|
6381
|
-
reconcileAutoSync();
|
|
6382
|
-
} catch {
|
|
7064
|
+
for (const line of submitNextStepLines(result)) {
|
|
7065
|
+
if (line.includes("\u2192")) console.log(pc2.bold(line));
|
|
7066
|
+
else if (line.startsWith(" Private until you do")) console.log(pc2.dim(line));
|
|
7067
|
+
else console.log(line);
|
|
6383
7068
|
}
|
|
6384
7069
|
}
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
7070
|
+
afterSubmitChores(flags);
|
|
7071
|
+
}
|
|
7072
|
+
function afterSubmitChores(flags) {
|
|
7073
|
+
if (flags.quiet) return;
|
|
7074
|
+
try {
|
|
7075
|
+
reconcileAutoSync();
|
|
7076
|
+
} catch {
|
|
6390
7077
|
}
|
|
7078
|
+
console.log();
|
|
7079
|
+
console.log(
|
|
7080
|
+
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
|
|
7081
|
+
);
|
|
6391
7082
|
}
|
|
6392
7083
|
async function linkServerInstall(token) {
|
|
6393
7084
|
if (!token) {
|
|
@@ -6395,8 +7086,9 @@ async function linkServerInstall(token) {
|
|
|
6395
7086
|
}
|
|
6396
7087
|
const anonKey = ensureAnonKey();
|
|
6397
7088
|
const linked = await redeemServerInstall(token, anonKey);
|
|
7089
|
+
const handle = sanitizeServerText(linked.handle);
|
|
6398
7090
|
console.log(
|
|
6399
|
-
linked.alreadyLinked ? ` This machine is already linked to @${
|
|
7091
|
+
linked.alreadyLinked ? ` This machine is already linked to @${handle}.` : ` Linked this machine to @${handle}.`
|
|
6400
7092
|
);
|
|
6401
7093
|
if (linked.mergedDays > 0) {
|
|
6402
7094
|
console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
|
|
@@ -6419,7 +7111,7 @@ async function linkServerInstall(token) {
|
|
|
6419
7111
|
} catch {
|
|
6420
7112
|
console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
|
|
6421
7113
|
}
|
|
6422
|
-
console.log(` Profile: ${linked.profileUrl}`);
|
|
7114
|
+
console.log(` Profile: ${sanitizeServerText(linked.profileUrl)}`);
|
|
6423
7115
|
}
|
|
6424
7116
|
function waitOrAbort(ms, signal) {
|
|
6425
7117
|
if (signal.aborted) return Promise.resolve();
|
|
@@ -6468,6 +7160,102 @@ async function runDaemon() {
|
|
|
6468
7160
|
console.log();
|
|
6469
7161
|
console.log(pc2.dim(` Daemon stopped after ${cycles} sync cycle${cycles === 1 ? "" : "s"}.`));
|
|
6470
7162
|
}
|
|
7163
|
+
async function runVerify() {
|
|
7164
|
+
printBanner();
|
|
7165
|
+
console.log();
|
|
7166
|
+
console.log(pc2.bold(" Verify your usage to get back on the leaderboard."));
|
|
7167
|
+
console.log(
|
|
7168
|
+
pc2.dim(
|
|
7169
|
+
" This sends a DETAILED per-request breakdown \u2014 timestamps and token counts, still never your prompts, code, or file names \u2014 so we can confirm your usage is real. It's deleted after review."
|
|
7170
|
+
)
|
|
7171
|
+
);
|
|
7172
|
+
const cfg = loadConfig();
|
|
7173
|
+
let token = cfg?.cliToken;
|
|
7174
|
+
if (!token) {
|
|
7175
|
+
if (!process.stdout.isTTY) {
|
|
7176
|
+
console.log(
|
|
7177
|
+
pc2.yellow(
|
|
7178
|
+
" Sign in first \u2014 run `npx whoburnedmore verify` in an interactive terminal."
|
|
7179
|
+
)
|
|
7180
|
+
);
|
|
7181
|
+
return;
|
|
7182
|
+
}
|
|
7183
|
+
const auth = await ensureSignedIn();
|
|
7184
|
+
if (!auth) return;
|
|
7185
|
+
token = auth.token;
|
|
7186
|
+
}
|
|
7187
|
+
if (process.stdin.isTTY) {
|
|
7188
|
+
const ok = await confirm("\n Send this breakdown to verify your usage?");
|
|
7189
|
+
if (!ok) {
|
|
7190
|
+
console.log(pc2.dim(" Cancelled \u2014 nothing was sent."));
|
|
7191
|
+
return;
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
console.log(pc2.dim(" Reading your local Claude Code logs\u2026"));
|
|
7195
|
+
const { requests, found } = await collectClaudeRequests();
|
|
7196
|
+
if (!found || requests.length === 0) {
|
|
7197
|
+
console.log(
|
|
7198
|
+
pc2.yellow(" No local Claude Code logs found on this machine to verify.")
|
|
7199
|
+
);
|
|
7200
|
+
console.log(
|
|
7201
|
+
pc2.dim(
|
|
7202
|
+
" Run this on the machine where you actually use Claude Code, or file a written appeal at whoburnedmore.com/appeal."
|
|
7203
|
+
)
|
|
7204
|
+
);
|
|
7205
|
+
return;
|
|
7206
|
+
}
|
|
7207
|
+
const CAP = 5e4;
|
|
7208
|
+
const sorted = requests.slice().sort((a, b) => b.ts - a.ts);
|
|
7209
|
+
const truncated = sorted.length > CAP;
|
|
7210
|
+
const capped = truncated ? sorted.slice(0, CAP) : sorted;
|
|
7211
|
+
const records = capped.map((r) => ({
|
|
7212
|
+
date: r.date,
|
|
7213
|
+
ts: r.ts,
|
|
7214
|
+
tool: "claude",
|
|
7215
|
+
model: r.model,
|
|
7216
|
+
inputTokens: r.inputTokens,
|
|
7217
|
+
outputTokens: r.outputTokens,
|
|
7218
|
+
cacheCreationTokens: r.cacheCreationTokens,
|
|
7219
|
+
cacheReadTokens: r.cacheReadTokens,
|
|
7220
|
+
reqHash: createHash("sha256").update(r.key).digest("hex").slice(0, 32)
|
|
7221
|
+
}));
|
|
7222
|
+
console.log(
|
|
7223
|
+
pc2.dim(
|
|
7224
|
+
` Sending ${records.length} request records for analysis${truncated ? " (most recent, sampled)" : ""}\u2026`
|
|
7225
|
+
)
|
|
7226
|
+
);
|
|
7227
|
+
let result;
|
|
7228
|
+
try {
|
|
7229
|
+
result = await verifyUsage(token, {
|
|
7230
|
+
cliVersion: VERSION,
|
|
7231
|
+
requests: records,
|
|
7232
|
+
...truncated ? { truncated: true } : {}
|
|
7233
|
+
});
|
|
7234
|
+
} catch (err) {
|
|
7235
|
+
if (err instanceof UnauthorizedError) {
|
|
7236
|
+
clearAuth();
|
|
7237
|
+
console.log(
|
|
7238
|
+
pc2.yellow(
|
|
7239
|
+
" Your sign-in expired \u2014 run `npx whoburnedmore verify` again to retry."
|
|
7240
|
+
)
|
|
7241
|
+
);
|
|
7242
|
+
return;
|
|
7243
|
+
}
|
|
7244
|
+
throw err;
|
|
7245
|
+
}
|
|
7246
|
+
console.log();
|
|
7247
|
+
const line = ` ${sanitizeServerText(result.message)}`;
|
|
7248
|
+
if (result.relisted) {
|
|
7249
|
+
console.log(pc2.green(` \u2713${line.slice(1)}`));
|
|
7250
|
+
} else {
|
|
7251
|
+
console.log(result.verdict === "fail" ? pc2.yellow(line) : line);
|
|
7252
|
+
console.log(
|
|
7253
|
+
pc2.dim(
|
|
7254
|
+
" We'll relist you automatically if it checks out \u2014 re-run `npx whoburnedmore` anytime to check."
|
|
7255
|
+
)
|
|
7256
|
+
);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
6471
7259
|
async function main() {
|
|
6472
7260
|
const major = Number(process.versions.node.split(".")[0]);
|
|
6473
7261
|
if (major < 20) {
|
|
@@ -6503,6 +7291,9 @@ async function main() {
|
|
|
6503
7291
|
case "daemon":
|
|
6504
7292
|
await runDaemon();
|
|
6505
7293
|
break;
|
|
7294
|
+
case "verify":
|
|
7295
|
+
await runVerify();
|
|
7296
|
+
break;
|
|
6506
7297
|
case "status":
|
|
6507
7298
|
case "doctor": {
|
|
6508
7299
|
for (const line of agentStatusReport()) console.log(line);
|
|
@@ -6554,8 +7345,8 @@ function printHelp() {
|
|
|
6554
7345
|
${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
|
|
6555
7346
|
|
|
6556
7347
|
${pc2.bold("usage")}
|
|
6557
|
-
npx whoburnedmore burn + land on the public leaderboard, open your dashboard
|
|
6558
|
-
npx whoburnedmore --board=CODE compare with friends \u2014 join their board
|
|
7348
|
+
npx whoburnedmore sign in, burn + land on the public leaderboard, open your dashboard
|
|
7349
|
+
npx whoburnedmore --board=CODE compare with friends \u2014 sign in and join their board
|
|
6559
7350
|
npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
|
|
6560
7351
|
npx whoburnedmore --local build the dashboard on your machine and open it (offline)
|
|
6561
7352
|
npx whoburnedmore --dry-run print exactly what would be sent, send nothing
|
|
@@ -6565,14 +7356,15 @@ function printHelp() {
|
|
|
6565
7356
|
npx whoburnedmore private hide your dashboard from the leaderboard
|
|
6566
7357
|
npx whoburnedmore public put it back on the leaderboard
|
|
6567
7358
|
npx whoburnedmore remove delete your dashboard and its data
|
|
7359
|
+
npx whoburnedmore verify delisted? re-verify your usage to get back on (sends a detailed breakdown)
|
|
6568
7360
|
npx whoburnedmore status check background-sync health (last sync, staleness)
|
|
6569
7361
|
npx whoburnedmore uninstall-sync turn off the background sync
|
|
6570
7362
|
npx whoburnedmore install-sync turn it back on after uninstalling
|
|
6571
7363
|
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
7364
|
+
Your first run signs you in (we open a page, you approve a short code) and binds
|
|
7365
|
+
this machine to your account \u2014 your usage lands on the leaderboard under your
|
|
7366
|
+
handle. Background sync is on by default: your page then refreshes automatically
|
|
7367
|
+
every 15 min (\`uninstall-sync\` to stop); run \`private\`/\`remove\` to pull it. Only
|
|
6576
7368
|
daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
|
|
6577
7369
|
your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
|
|
6578
7370
|
your machine at all.
|