whoburnedmore 0.8.0 → 0.8.1
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 +115 -68
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -258,29 +258,27 @@ function autoSyncInstalled() {
|
|
|
258
258
|
|
|
259
259
|
// src/banner.ts
|
|
260
260
|
import pc from "picocolors";
|
|
261
|
-
var
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
"
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
var SHADES = [226, 220, 214, 208, 202];
|
|
261
|
+
var WORD = "whoburnedmore";
|
|
262
|
+
var SHADES = [226, 220, 214, 208, 202, 196];
|
|
263
|
+
function paintWordmark() {
|
|
264
|
+
if (!pc.isColorSupported) return `${WORD}?`;
|
|
265
|
+
const letters = [...WORD].map((ch, i) => `\x1B[1;38;5;${SHADES[i % SHADES.length]}m${ch}`).join("");
|
|
266
|
+
return `${letters}\x1B[1;38;5;208m?\x1B[0m`;
|
|
267
|
+
}
|
|
269
268
|
function printBanner() {
|
|
270
|
-
const
|
|
269
|
+
const rule = pc.dim("\u2500".repeat(30));
|
|
271
270
|
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?")}`);
|
|
271
|
+
console.log(` \u{1F525} ${paintWordmark()}`);
|
|
272
|
+
console.log(` ${rule}`);
|
|
273
|
+
console.log(` ${pc.dim("who burned more \u2014 you, or them?")}`);
|
|
277
274
|
console.log();
|
|
278
275
|
}
|
|
279
276
|
|
|
280
277
|
// src/collect.ts
|
|
281
|
-
import {
|
|
278
|
+
import { execFile } from "node:child_process";
|
|
282
279
|
import { createRequire as createRequire3 } from "node:module";
|
|
283
280
|
import { dirname as dirname2, join as join6 } from "node:path";
|
|
281
|
+
import { promisify } from "node:util";
|
|
284
282
|
|
|
285
283
|
// src/attribution.ts
|
|
286
284
|
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
@@ -311,7 +309,7 @@ var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
|
311
309
|
var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
|
|
312
310
|
var MAX_FILES = 5e3;
|
|
313
311
|
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
314
|
-
var TIME_BUDGET_MS =
|
|
312
|
+
var TIME_BUDGET_MS = 12e3;
|
|
315
313
|
var MAX_STATS = 300;
|
|
316
314
|
var MAX_PROJECTS = 500;
|
|
317
315
|
function createAccumulator() {
|
|
@@ -563,10 +561,17 @@ function listTranscripts(dir) {
|
|
|
563
561
|
walk(dir);
|
|
564
562
|
return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
|
|
565
563
|
}
|
|
566
|
-
function collectAttribution() {
|
|
564
|
+
async function collectAttribution() {
|
|
567
565
|
const acc = createAccumulator();
|
|
568
566
|
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
569
567
|
let complete = true;
|
|
568
|
+
let sinceYield = 0;
|
|
569
|
+
const breathe = async () => {
|
|
570
|
+
if (++sinceYield >= 8) {
|
|
571
|
+
sinceYield = 0;
|
|
572
|
+
await new Promise((r) => setImmediate(r));
|
|
573
|
+
}
|
|
574
|
+
};
|
|
570
575
|
try {
|
|
571
576
|
for (const dir of claudeProjectDirs()) {
|
|
572
577
|
for (const file of listTranscripts(dir)) {
|
|
@@ -582,6 +587,7 @@ function collectAttribution() {
|
|
|
582
587
|
} catch {
|
|
583
588
|
}
|
|
584
589
|
}
|
|
590
|
+
await breathe();
|
|
585
591
|
}
|
|
586
592
|
}
|
|
587
593
|
for (const file of listTranscripts(CODEX_SESSIONS)) {
|
|
@@ -597,6 +603,7 @@ function collectAttribution() {
|
|
|
597
603
|
} catch {
|
|
598
604
|
}
|
|
599
605
|
}
|
|
606
|
+
await breathe();
|
|
600
607
|
}
|
|
601
608
|
} catch {
|
|
602
609
|
complete = false;
|
|
@@ -855,6 +862,7 @@ async function collectCursor() {
|
|
|
855
862
|
}
|
|
856
863
|
|
|
857
864
|
// src/collect.ts
|
|
865
|
+
var execFileAsync = promisify(execFile);
|
|
858
866
|
var SOURCES = [
|
|
859
867
|
"claude",
|
|
860
868
|
"codex",
|
|
@@ -1036,64 +1044,85 @@ function dedupeBlocks(blocks) {
|
|
|
1036
1044
|
}
|
|
1037
1045
|
return [...byStart.values()];
|
|
1038
1046
|
}
|
|
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 };
|
|
1047
|
+
async function runCcusageOnce(cmd, args) {
|
|
1047
1048
|
try {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1049
|
+
const { stdout } = await execFileAsync(cmd, args, {
|
|
1050
|
+
encoding: "utf8",
|
|
1051
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1052
|
+
// A single source shouldn't be able to hang the whole run. 25s is plenty
|
|
1053
|
+
// for a healthy local read; a hung source gets killed and (if transient)
|
|
1054
|
+
// retried once below rather than stalling everything for minutes.
|
|
1055
|
+
timeout: 25e3
|
|
1056
|
+
});
|
|
1057
|
+
if (!stdout) return { json: null, transient: false };
|
|
1058
|
+
try {
|
|
1059
|
+
return { json: JSON.parse(stdout), transient: false };
|
|
1060
|
+
} catch {
|
|
1061
|
+
return { json: null, transient: false };
|
|
1062
|
+
}
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
const e = err;
|
|
1065
|
+
const transient = e.killed === true || e.signal != null || typeof e.code === "string";
|
|
1066
|
+
return { json: null, transient };
|
|
1051
1067
|
}
|
|
1052
1068
|
}
|
|
1053
|
-
function runCcusage(cmd, args) {
|
|
1054
|
-
const first = runCcusageOnce(cmd, args);
|
|
1069
|
+
async function runCcusage(cmd, args) {
|
|
1070
|
+
const first = await runCcusageOnce(cmd, args);
|
|
1055
1071
|
if (first.json !== null || !first.transient) return first.json;
|
|
1056
|
-
return runCcusageOnce(cmd, args).json;
|
|
1072
|
+
return (await runCcusageOnce(cmd, args)).json;
|
|
1057
1073
|
}
|
|
1058
1074
|
var COLLECT_STAGES = SOURCES.length + 4;
|
|
1059
1075
|
async function collectAll(onProgress) {
|
|
1060
1076
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
1077
|
+
let done = 0;
|
|
1078
|
+
const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
|
|
1079
|
+
const sourceTasks = SOURCES.map(async (source) => {
|
|
1080
|
+
const json = await runCcusage(cmd, [...prefixArgs, source, "daily", "--json", "--offline"]);
|
|
1081
|
+
tick();
|
|
1082
|
+
return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
|
|
1083
|
+
});
|
|
1084
|
+
const sessionTask = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]).then(
|
|
1085
|
+
(json) => {
|
|
1086
|
+
tick();
|
|
1087
|
+
return json ? mapCcusageSessions(json) : [];
|
|
1088
|
+
}
|
|
1089
|
+
);
|
|
1090
|
+
const blockTask = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]).then(
|
|
1091
|
+
(json) => {
|
|
1092
|
+
tick();
|
|
1093
|
+
return json ? mapCcusageBlocks(json) : [];
|
|
1094
|
+
}
|
|
1095
|
+
);
|
|
1096
|
+
const cursorTask = collectCursor().then((c) => {
|
|
1097
|
+
tick();
|
|
1098
|
+
return c;
|
|
1099
|
+
});
|
|
1100
|
+
const attributionTask = collectAttribution().then((a) => {
|
|
1101
|
+
tick();
|
|
1102
|
+
return a;
|
|
1103
|
+
});
|
|
1104
|
+
const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
|
|
1105
|
+
Promise.all(sourceTasks),
|
|
1106
|
+
sessionTask,
|
|
1107
|
+
blockTask,
|
|
1108
|
+
cursorTask,
|
|
1109
|
+
attributionTask
|
|
1110
|
+
]);
|
|
1061
1111
|
const entries = [];
|
|
1062
1112
|
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);
|
|
1113
|
+
for (const { source, mapped } of sourceResults) {
|
|
1076
1114
|
if (mapped.length > 0) {
|
|
1077
1115
|
entries.push(...mapped);
|
|
1078
1116
|
toolsFound.push(source);
|
|
1079
1117
|
}
|
|
1080
1118
|
}
|
|
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
1119
|
if (cursor.found) {
|
|
1090
1120
|
entries.push(...cursor.entries);
|
|
1091
1121
|
blocks.push(...cursor.blocks);
|
|
1092
1122
|
toolsFound.push("cursor");
|
|
1093
1123
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
onProgress?.(done, COLLECT_STAGES, "done");
|
|
1124
|
+
const { tools, skills, projects, agent, titles, sessionMessages, complete } = attribution;
|
|
1125
|
+
onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
|
|
1097
1126
|
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
1098
1127
|
const title = titles.get(s.sessionId);
|
|
1099
1128
|
const messageCount = sessionMessages.get(s.sessionId);
|
|
@@ -5551,6 +5580,16 @@ async function publishLocal(payload, deps) {
|
|
|
5551
5580
|
// src/index.ts
|
|
5552
5581
|
var require2 = createRequire4(import.meta.url);
|
|
5553
5582
|
var VERSION = require2("../package.json").version;
|
|
5583
|
+
var LOADING_VIBES = [
|
|
5584
|
+
"counting up your token usage, right here on your machine\u2026",
|
|
5585
|
+
"tallying tokens across every coding agent you use\u2026",
|
|
5586
|
+
"adding up cache reads, writes & all the burn\u2026",
|
|
5587
|
+
"tokens & totals only \u2014 never your prompts or code\u2026",
|
|
5588
|
+
"summing up your usage, model by model\u2026",
|
|
5589
|
+
"working out what all that burn cost you \u{1F525}",
|
|
5590
|
+
"crunching your daily token totals\u2026",
|
|
5591
|
+
"almost there \u2014 adding it all up\u2026"
|
|
5592
|
+
];
|
|
5554
5593
|
function startProgress() {
|
|
5555
5594
|
if (!process.stdout.isTTY) {
|
|
5556
5595
|
let lastLogged = -1;
|
|
@@ -5559,7 +5598,7 @@ function startProgress() {
|
|
|
5559
5598
|
const pct = Math.round(done / total * 100);
|
|
5560
5599
|
if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
|
|
5561
5600
|
lastLogged = pct;
|
|
5562
|
-
console.log(pc3.dim(`
|
|
5601
|
+
console.log(pc3.dim(` counting local token usage\u2026 ${pct}%`));
|
|
5563
5602
|
}
|
|
5564
5603
|
},
|
|
5565
5604
|
stop: () => {
|
|
@@ -5569,22 +5608,29 @@ function startProgress() {
|
|
|
5569
5608
|
const width = 24;
|
|
5570
5609
|
let target = 0;
|
|
5571
5610
|
let shown = 0;
|
|
5572
|
-
let
|
|
5611
|
+
let ticks = 0;
|
|
5573
5612
|
process.stdout.write("\x1B[?25l");
|
|
5613
|
+
const vibe = () => {
|
|
5614
|
+
if (shown >= 0.9) return LOADING_VIBES[LOADING_VIBES.length - 1];
|
|
5615
|
+
const i = Math.floor(ticks / 18) % (LOADING_VIBES.length - 1);
|
|
5616
|
+
return LOADING_VIBES[i];
|
|
5617
|
+
};
|
|
5574
5618
|
const render = () => {
|
|
5619
|
+
ticks++;
|
|
5575
5620
|
shown += (target - shown) * 0.3;
|
|
5576
5621
|
if (target - shown < 4e-3) shown = target;
|
|
5577
5622
|
const filled = Math.round(shown * width);
|
|
5578
5623
|
const bar = pc3.yellow("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
|
|
5579
5624
|
const pct = String(Math.round(shown * 100)).padStart(3);
|
|
5580
|
-
process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(
|
|
5625
|
+
process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(vibe())}\x1B[K`);
|
|
5581
5626
|
};
|
|
5582
5627
|
render();
|
|
5583
5628
|
const timer = setInterval(render, 60);
|
|
5584
5629
|
return {
|
|
5585
|
-
|
|
5630
|
+
// Stage labels are no longer surfaced (the narration is friendlier); we only
|
|
5631
|
+
// consume the done/total fraction to drive the bar.
|
|
5632
|
+
onProgress: (done, total) => {
|
|
5586
5633
|
target = total > 0 ? done / total : 0;
|
|
5587
|
-
label = l;
|
|
5588
5634
|
},
|
|
5589
5635
|
stop: () => {
|
|
5590
5636
|
clearInterval(timer);
|
|
@@ -5623,15 +5669,16 @@ function showLocalDashboard(payload) {
|
|
|
5623
5669
|
async function run(flags) {
|
|
5624
5670
|
if (!flags.quiet) {
|
|
5625
5671
|
printBanner();
|
|
5626
|
-
console.log(pc3.dim(`whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
console.log(
|
|
5632
|
-
|
|
5633
|
-
|
|
5672
|
+
console.log(pc3.dim(` whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
|
|
5673
|
+
if (!flags.dryRun && !flags.noSubmit && !flags.local) {
|
|
5674
|
+
console.log(
|
|
5675
|
+
pc3.dim(" Counting your token usage and posting your rank \u2014 only daily totals leave your machine, never your prompts or code.")
|
|
5676
|
+
);
|
|
5677
|
+
console.log(
|
|
5678
|
+
pc3.dim(" (`--local` keeps it fully offline \xB7 `private`/`remove` pull it anytime \xB7 details: whoburnedmore.com/trust)")
|
|
5679
|
+
);
|
|
5634
5680
|
}
|
|
5681
|
+
console.log();
|
|
5635
5682
|
}
|
|
5636
5683
|
const progress = flags.quiet ? { onProgress: void 0, stop: () => {
|
|
5637
5684
|
} } : startProgress();
|