whoburnedmore 0.8.0 → 0.8.3
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 +136 -73
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,6 +24,15 @@ function parseBoard(args) {
|
|
|
24
24
|
}
|
|
25
25
|
return void 0;
|
|
26
26
|
}
|
|
27
|
+
function resolveCommand(args) {
|
|
28
|
+
if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
|
|
29
|
+
return "help";
|
|
30
|
+
}
|
|
31
|
+
if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
|
|
32
|
+
return "version";
|
|
33
|
+
}
|
|
34
|
+
return args.find((a) => !a.startsWith("-")) ?? "run";
|
|
35
|
+
}
|
|
27
36
|
|
|
28
37
|
// src/api.ts
|
|
29
38
|
function apiBase() {
|
|
@@ -101,7 +110,13 @@ import { fileURLToPath } from "node:url";
|
|
|
101
110
|
|
|
102
111
|
// src/config.ts
|
|
103
112
|
import { randomBytes } from "node:crypto";
|
|
104
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
chmodSync,
|
|
115
|
+
existsSync,
|
|
116
|
+
mkdirSync,
|
|
117
|
+
readFileSync,
|
|
118
|
+
writeFileSync
|
|
119
|
+
} from "node:fs";
|
|
105
120
|
import { homedir } from "node:os";
|
|
106
121
|
import { join } from "node:path";
|
|
107
122
|
function defaultConfigDir() {
|
|
@@ -123,6 +138,10 @@ function saveConfig(dir = defaultConfigDir(), config = {}) {
|
|
|
123
138
|
mkdirSync(dir, { recursive: true });
|
|
124
139
|
const file = join(dir, "config.json");
|
|
125
140
|
writeFileSync(file, JSON.stringify(config, null, 2), { mode: 384 });
|
|
141
|
+
try {
|
|
142
|
+
chmodSync(file, 384);
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
126
145
|
}
|
|
127
146
|
function ensureAnonKey(dir = defaultConfigDir()) {
|
|
128
147
|
const config = loadConfig(dir) ?? {};
|
|
@@ -258,29 +277,27 @@ function autoSyncInstalled() {
|
|
|
258
277
|
|
|
259
278
|
// src/banner.ts
|
|
260
279
|
import pc from "picocolors";
|
|
261
|
-
var
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
"
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
var SHADES = [226, 220, 214, 208, 202];
|
|
280
|
+
var WORD = "whoburnedmore";
|
|
281
|
+
var SHADES = [226, 220, 214, 208, 202, 196];
|
|
282
|
+
function paintWordmark() {
|
|
283
|
+
if (!pc.isColorSupported) return `${WORD}?`;
|
|
284
|
+
const letters = [...WORD].map((ch, i) => `\x1B[1;38;5;${SHADES[i % SHADES.length]}m${ch}`).join("");
|
|
285
|
+
return `${letters}\x1B[1;38;5;208m?\x1B[0m`;
|
|
286
|
+
}
|
|
269
287
|
function printBanner() {
|
|
270
|
-
const
|
|
288
|
+
const rule = pc.dim("\u2500".repeat(30));
|
|
271
289
|
console.log();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
275
|
-
const mark = color ? `\x1B[1;38;5;208m\u{1F525} whoburnedmore\x1B[0m` : "\u{1F525} whoburnedmore";
|
|
276
|
-
console.log(` ${mark} ${pc.dim("\xB7 who burned more?")}`);
|
|
290
|
+
console.log(` \u{1F525} ${paintWordmark()}`);
|
|
291
|
+
console.log(` ${rule}`);
|
|
292
|
+
console.log(` ${pc.dim("who burned more \u2014 you, or them?")}`);
|
|
277
293
|
console.log();
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
// src/collect.ts
|
|
281
|
-
import {
|
|
297
|
+
import { execFile } from "node:child_process";
|
|
282
298
|
import { createRequire as createRequire3 } from "node:module";
|
|
283
299
|
import { dirname as dirname2, join as join6 } from "node:path";
|
|
300
|
+
import { promisify } from "node:util";
|
|
284
301
|
|
|
285
302
|
// src/attribution.ts
|
|
286
303
|
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
@@ -311,7 +328,7 @@ var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
|
311
328
|
var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
|
|
312
329
|
var MAX_FILES = 5e3;
|
|
313
330
|
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
314
|
-
var TIME_BUDGET_MS =
|
|
331
|
+
var TIME_BUDGET_MS = 12e3;
|
|
315
332
|
var MAX_STATS = 300;
|
|
316
333
|
var MAX_PROJECTS = 500;
|
|
317
334
|
function createAccumulator() {
|
|
@@ -563,10 +580,17 @@ function listTranscripts(dir) {
|
|
|
563
580
|
walk(dir);
|
|
564
581
|
return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
|
|
565
582
|
}
|
|
566
|
-
function collectAttribution() {
|
|
583
|
+
async function collectAttribution() {
|
|
567
584
|
const acc = createAccumulator();
|
|
568
585
|
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
569
586
|
let complete = true;
|
|
587
|
+
let sinceYield = 0;
|
|
588
|
+
const breathe = async () => {
|
|
589
|
+
if (++sinceYield >= 8) {
|
|
590
|
+
sinceYield = 0;
|
|
591
|
+
await new Promise((r) => setImmediate(r));
|
|
592
|
+
}
|
|
593
|
+
};
|
|
570
594
|
try {
|
|
571
595
|
for (const dir of claudeProjectDirs()) {
|
|
572
596
|
for (const file of listTranscripts(dir)) {
|
|
@@ -582,6 +606,7 @@ function collectAttribution() {
|
|
|
582
606
|
} catch {
|
|
583
607
|
}
|
|
584
608
|
}
|
|
609
|
+
await breathe();
|
|
585
610
|
}
|
|
586
611
|
}
|
|
587
612
|
for (const file of listTranscripts(CODEX_SESSIONS)) {
|
|
@@ -597,6 +622,7 @@ function collectAttribution() {
|
|
|
597
622
|
} catch {
|
|
598
623
|
}
|
|
599
624
|
}
|
|
625
|
+
await breathe();
|
|
600
626
|
}
|
|
601
627
|
} catch {
|
|
602
628
|
complete = false;
|
|
@@ -855,6 +881,7 @@ async function collectCursor() {
|
|
|
855
881
|
}
|
|
856
882
|
|
|
857
883
|
// src/collect.ts
|
|
884
|
+
var execFileAsync = promisify(execFile);
|
|
858
885
|
var SOURCES = [
|
|
859
886
|
"claude",
|
|
860
887
|
"codex",
|
|
@@ -1036,64 +1063,85 @@ function dedupeBlocks(blocks) {
|
|
|
1036
1063
|
}
|
|
1037
1064
|
return [...byStart.values()];
|
|
1038
1065
|
}
|
|
1039
|
-
function runCcusageOnce(cmd, args) {
|
|
1040
|
-
const res = spawnSync4(cmd, args, {
|
|
1041
|
-
encoding: "utf8",
|
|
1042
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
1043
|
-
timeout: 12e4
|
|
1044
|
-
});
|
|
1045
|
-
const transient = res.signal != null || res.error != null;
|
|
1046
|
-
if (res.status !== 0 || !res.stdout) return { json: null, transient };
|
|
1066
|
+
async function runCcusageOnce(cmd, args) {
|
|
1047
1067
|
try {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1068
|
+
const { stdout } = await execFileAsync(cmd, args, {
|
|
1069
|
+
encoding: "utf8",
|
|
1070
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1071
|
+
// A single source shouldn't be able to hang the whole run. 25s is plenty
|
|
1072
|
+
// for a healthy local read; a hung source gets killed and (if transient)
|
|
1073
|
+
// retried once below rather than stalling everything for minutes.
|
|
1074
|
+
timeout: 25e3
|
|
1075
|
+
});
|
|
1076
|
+
if (!stdout) return { json: null, transient: false };
|
|
1077
|
+
try {
|
|
1078
|
+
return { json: JSON.parse(stdout), transient: false };
|
|
1079
|
+
} catch {
|
|
1080
|
+
return { json: null, transient: false };
|
|
1081
|
+
}
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
const e = err;
|
|
1084
|
+
const transient = e.killed === true || e.signal != null || typeof e.code === "string";
|
|
1085
|
+
return { json: null, transient };
|
|
1051
1086
|
}
|
|
1052
1087
|
}
|
|
1053
|
-
function runCcusage(cmd, args) {
|
|
1054
|
-
const first = runCcusageOnce(cmd, args);
|
|
1088
|
+
async function runCcusage(cmd, args) {
|
|
1089
|
+
const first = await runCcusageOnce(cmd, args);
|
|
1055
1090
|
if (first.json !== null || !first.transient) return first.json;
|
|
1056
|
-
return runCcusageOnce(cmd, args).json;
|
|
1091
|
+
return (await runCcusageOnce(cmd, args)).json;
|
|
1057
1092
|
}
|
|
1058
1093
|
var COLLECT_STAGES = SOURCES.length + 4;
|
|
1059
1094
|
async function collectAll(onProgress) {
|
|
1060
1095
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
1096
|
+
let done = 0;
|
|
1097
|
+
const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
|
|
1098
|
+
const sourceTasks = SOURCES.map(async (source) => {
|
|
1099
|
+
const json = await runCcusage(cmd, [...prefixArgs, source, "daily", "--json", "--offline"]);
|
|
1100
|
+
tick();
|
|
1101
|
+
return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
|
|
1102
|
+
});
|
|
1103
|
+
const sessionTask = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]).then(
|
|
1104
|
+
(json) => {
|
|
1105
|
+
tick();
|
|
1106
|
+
return json ? mapCcusageSessions(json) : [];
|
|
1107
|
+
}
|
|
1108
|
+
);
|
|
1109
|
+
const blockTask = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]).then(
|
|
1110
|
+
(json) => {
|
|
1111
|
+
tick();
|
|
1112
|
+
return json ? mapCcusageBlocks(json) : [];
|
|
1113
|
+
}
|
|
1114
|
+
);
|
|
1115
|
+
const cursorTask = collectCursor().then((c) => {
|
|
1116
|
+
tick();
|
|
1117
|
+
return c;
|
|
1118
|
+
});
|
|
1119
|
+
const attributionTask = collectAttribution().then((a) => {
|
|
1120
|
+
tick();
|
|
1121
|
+
return a;
|
|
1122
|
+
});
|
|
1123
|
+
const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
|
|
1124
|
+
Promise.all(sourceTasks),
|
|
1125
|
+
sessionTask,
|
|
1126
|
+
blockTask,
|
|
1127
|
+
cursorTask,
|
|
1128
|
+
attributionTask
|
|
1129
|
+
]);
|
|
1061
1130
|
const entries = [];
|
|
1062
1131
|
const toolsFound = [];
|
|
1063
|
-
|
|
1064
|
-
const tick = (label) => onProgress?.(done++, COLLECT_STAGES, label);
|
|
1065
|
-
for (const source of SOURCES) {
|
|
1066
|
-
tick(`reading ${source}`);
|
|
1067
|
-
const json = runCcusage(cmd, [
|
|
1068
|
-
...prefixArgs,
|
|
1069
|
-
source,
|
|
1070
|
-
"daily",
|
|
1071
|
-
"--json",
|
|
1072
|
-
"--offline"
|
|
1073
|
-
]);
|
|
1074
|
-
if (!json) continue;
|
|
1075
|
-
const mapped = mapCcusageDaily(source, json);
|
|
1132
|
+
for (const { source, mapped } of sourceResults) {
|
|
1076
1133
|
if (mapped.length > 0) {
|
|
1077
1134
|
entries.push(...mapped);
|
|
1078
1135
|
toolsFound.push(source);
|
|
1079
1136
|
}
|
|
1080
1137
|
}
|
|
1081
|
-
tick("reading conversations");
|
|
1082
|
-
const sessionJson = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]);
|
|
1083
|
-
tick("reading active blocks");
|
|
1084
|
-
const blockJson = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]);
|
|
1085
|
-
const sessions = sessionJson ? mapCcusageSessions(sessionJson) : [];
|
|
1086
|
-
const blocks = blockJson ? mapCcusageBlocks(blockJson) : [];
|
|
1087
|
-
tick("reading cursor");
|
|
1088
|
-
const cursor = await collectCursor();
|
|
1089
1138
|
if (cursor.found) {
|
|
1090
1139
|
entries.push(...cursor.entries);
|
|
1091
1140
|
blocks.push(...cursor.blocks);
|
|
1092
1141
|
toolsFound.push("cursor");
|
|
1093
1142
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
onProgress?.(done, COLLECT_STAGES, "done");
|
|
1143
|
+
const { tools, skills, projects, agent, titles, sessionMessages, complete } = attribution;
|
|
1144
|
+
onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
|
|
1097
1145
|
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
1098
1146
|
const title = titles.get(s.sessionId);
|
|
1099
1147
|
const messageCount = sessionMessages.get(s.sessionId);
|
|
@@ -5551,6 +5599,16 @@ async function publishLocal(payload, deps) {
|
|
|
5551
5599
|
// src/index.ts
|
|
5552
5600
|
var require2 = createRequire4(import.meta.url);
|
|
5553
5601
|
var VERSION = require2("../package.json").version;
|
|
5602
|
+
var LOADING_VIBES = [
|
|
5603
|
+
"counting up your token usage, right here on your machine\u2026",
|
|
5604
|
+
"tallying tokens across every coding agent you use\u2026",
|
|
5605
|
+
"adding up cache reads, writes & all the burn\u2026",
|
|
5606
|
+
"tokens & totals only \u2014 never your prompts or code\u2026",
|
|
5607
|
+
"summing up your usage, model by model\u2026",
|
|
5608
|
+
"working out what all that burn cost you \u{1F525}",
|
|
5609
|
+
"crunching your daily token totals\u2026",
|
|
5610
|
+
"almost there \u2014 adding it all up\u2026"
|
|
5611
|
+
];
|
|
5554
5612
|
function startProgress() {
|
|
5555
5613
|
if (!process.stdout.isTTY) {
|
|
5556
5614
|
let lastLogged = -1;
|
|
@@ -5559,7 +5617,7 @@ function startProgress() {
|
|
|
5559
5617
|
const pct = Math.round(done / total * 100);
|
|
5560
5618
|
if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
|
|
5561
5619
|
lastLogged = pct;
|
|
5562
|
-
console.log(pc3.dim(`
|
|
5620
|
+
console.log(pc3.dim(` counting local token usage\u2026 ${pct}%`));
|
|
5563
5621
|
}
|
|
5564
5622
|
},
|
|
5565
5623
|
stop: () => {
|
|
@@ -5569,22 +5627,29 @@ function startProgress() {
|
|
|
5569
5627
|
const width = 24;
|
|
5570
5628
|
let target = 0;
|
|
5571
5629
|
let shown = 0;
|
|
5572
|
-
let
|
|
5630
|
+
let ticks = 0;
|
|
5573
5631
|
process.stdout.write("\x1B[?25l");
|
|
5632
|
+
const vibe = () => {
|
|
5633
|
+
if (shown >= 0.9) return LOADING_VIBES[LOADING_VIBES.length - 1];
|
|
5634
|
+
const i = Math.floor(ticks / 18) % (LOADING_VIBES.length - 1);
|
|
5635
|
+
return LOADING_VIBES[i];
|
|
5636
|
+
};
|
|
5574
5637
|
const render = () => {
|
|
5638
|
+
ticks++;
|
|
5575
5639
|
shown += (target - shown) * 0.3;
|
|
5576
5640
|
if (target - shown < 4e-3) shown = target;
|
|
5577
5641
|
const filled = Math.round(shown * width);
|
|
5578
5642
|
const bar = pc3.yellow("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
|
|
5579
5643
|
const pct = String(Math.round(shown * 100)).padStart(3);
|
|
5580
|
-
process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(
|
|
5644
|
+
process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(vibe())}\x1B[K`);
|
|
5581
5645
|
};
|
|
5582
5646
|
render();
|
|
5583
5647
|
const timer = setInterval(render, 60);
|
|
5584
5648
|
return {
|
|
5585
|
-
|
|
5649
|
+
// Stage labels are no longer surfaced (the narration is friendlier); we only
|
|
5650
|
+
// consume the done/total fraction to drive the bar.
|
|
5651
|
+
onProgress: (done, total) => {
|
|
5586
5652
|
target = total > 0 ? done / total : 0;
|
|
5587
|
-
label = l;
|
|
5588
5653
|
},
|
|
5589
5654
|
stop: () => {
|
|
5590
5655
|
clearInterval(timer);
|
|
@@ -5623,15 +5688,16 @@ function showLocalDashboard(payload) {
|
|
|
5623
5688
|
async function run(flags) {
|
|
5624
5689
|
if (!flags.quiet) {
|
|
5625
5690
|
printBanner();
|
|
5626
|
-
console.log(pc3.dim(`whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
console.log(
|
|
5632
|
-
|
|
5633
|
-
|
|
5691
|
+
console.log(pc3.dim(` whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
|
|
5692
|
+
if (!flags.dryRun && !flags.noSubmit && !flags.local) {
|
|
5693
|
+
console.log(
|
|
5694
|
+
pc3.dim(" Counting your token usage and posting your rank \u2014 only daily totals leave your machine, never your prompts or code.")
|
|
5695
|
+
);
|
|
5696
|
+
console.log(
|
|
5697
|
+
pc3.dim(" (`--local` keeps it fully offline \xB7 `private`/`remove` pull it anytime \xB7 details: whoburnedmore.com/trust)")
|
|
5698
|
+
);
|
|
5634
5699
|
}
|
|
5700
|
+
console.log();
|
|
5635
5701
|
}
|
|
5636
5702
|
const progress = flags.quiet ? { onProgress: void 0, stop: () => {
|
|
5637
5703
|
} } : startProgress();
|
|
@@ -5732,7 +5798,7 @@ async function main() {
|
|
|
5732
5798
|
return;
|
|
5733
5799
|
}
|
|
5734
5800
|
const args = process.argv.slice(2);
|
|
5735
|
-
const command =
|
|
5801
|
+
const command = resolveCommand(args);
|
|
5736
5802
|
const flags = {
|
|
5737
5803
|
dryRun: args.includes("--dry-run"),
|
|
5738
5804
|
noSubmit: args.includes("--no-submit"),
|
|
@@ -5779,12 +5845,9 @@ async function main() {
|
|
|
5779
5845
|
console.log(` ${uninstallAutoSync()}`);
|
|
5780
5846
|
break;
|
|
5781
5847
|
case "help":
|
|
5782
|
-
case "--help":
|
|
5783
5848
|
printHelp();
|
|
5784
5849
|
break;
|
|
5785
5850
|
case "version":
|
|
5786
|
-
case "--version":
|
|
5787
|
-
case "-v":
|
|
5788
5851
|
console.log(VERSION);
|
|
5789
5852
|
break;
|
|
5790
5853
|
default:
|