candobear-token-leaderboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # candobear-token-leaderboard
2
+
3
+ CLI uploader for the Candobear Token Economics leaderboard.
4
+
5
+ ## Install
6
+
7
+ Install CodexBar first:
8
+
9
+ - Website: https://codexbar.app
10
+ - Releases: https://github.com/steipete/CodexBar/releases
11
+ - CLI docs: https://github.com/steipete/CodexBar/blob/main/docs/cli.md
12
+
13
+ Then install the uploader:
14
+
15
+ ```bash
16
+ npm install -g candobear-token-leaderboard
17
+ ```
18
+
19
+ The installed command is `candobear-token`.
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ candobear-token configure --token <uploadToken>
25
+ candobear-token upload --date today --provider all --dry-run
26
+ candobear-token upload --date today --provider all
27
+ candobear-token upload --date yesterday --provider all
28
+ candobear-token backfill --from 2026-05-01 --to today --provider all
29
+ ```
30
+
31
+ The uploader sends only CodexBar daily summary fields. It does not upload raw JSONL, prompts, assistant messages, tool output, cwd, thread names, local paths, or source paths.
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { promisify } from "node:util";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ export const VERSION = "0.1.0";
13
+ export const DEFAULT_API_BASE = "https://ai.candobear.com/tokeneconomics/api";
14
+ export const CONFIG_PATH = path.join(homedir(), ".candobear", "token-uploader.json");
15
+ export const CODEXBAR_CANDIDATES = ["codexbar", "/opt/homebrew/bin/codexbar"];
16
+ export const UPLOAD_PROVIDERS = ["codex", "claude"];
17
+ export const QUERY_PROVIDERS = ["all", ...UPLOAD_PROVIDERS];
18
+
19
+ export const CODEXBAR_INSTALL_HELP = `Install CodexBar first:
20
+ Website: https://codexbar.app
21
+ Releases: https://github.com/steipete/CodexBar/releases
22
+ CLI docs: https://github.com/steipete/CodexBar/blob/main/docs/cli.md
23
+
24
+ On macOS, install CodexBar and run Preferences -> Advanced -> Install CLI.
25
+ Then verify with: codexbar --version`;
26
+
27
+ export function codexbarCandidates() {
28
+ const configured = process.env.CANDOBEAR_CODEXBAR_BIN?.trim();
29
+ return configured ? [configured] : CODEXBAR_CANDIDATES;
30
+ }
31
+
32
+ const usage = () => `candobear-token ${VERSION}
33
+
34
+ Usage:
35
+ candobear-token configure --token <uploadToken> [--api-base ${DEFAULT_API_BASE}]
36
+ candobear-token upload --date yesterday [--provider all|codex|claude] [--dry-run] [--token <uploadToken>] [--api-base <url>]
37
+ candobear-token backfill --from 2026-05-01 [--to today] [--provider all|codex|claude] [--dry-run] [--token <uploadToken>] [--api-base <url>]
38
+
39
+ Dates use UTC+8 YYYY-MM-DD. Supported shortcuts: today, yesterday.
40
+
41
+ Prerequisites:
42
+ 1. Install CodexBar: https://codexbar.app
43
+ 2. Install this uploader: npm install -g candobear-token-leaderboard`;
44
+
45
+ export function parseArgs(argv) {
46
+ const [command, ...rest] = argv;
47
+ const options = { command };
48
+ for (let i = 0; i < rest.length; i += 1) {
49
+ const arg = rest[i];
50
+ if (arg === "--dry-run") {
51
+ options.dryRun = true;
52
+ continue;
53
+ }
54
+ if (arg === "--help" || arg === "-h") {
55
+ options.help = true;
56
+ continue;
57
+ }
58
+ if (arg === "--version" || arg === "-V") {
59
+ options.version = true;
60
+ continue;
61
+ }
62
+ if (!arg.startsWith("--")) throw new Error(`Unexpected argument: ${arg}`);
63
+ const key = arg.slice(2).replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
64
+ const value = rest[i + 1];
65
+ if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}`);
66
+ options[key] = value;
67
+ i += 1;
68
+ }
69
+ return options;
70
+ }
71
+
72
+ export function resolveDateArg(value = "yesterday", now = new Date()) {
73
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
74
+ const shifted = new Date(now.getTime() + 8 * 60 * 60 * 1000);
75
+ if (value === "today") return shifted.toISOString().slice(0, 10);
76
+ if (value === "yesterday") {
77
+ shifted.setUTCDate(shifted.getUTCDate() - 1);
78
+ return shifted.toISOString().slice(0, 10);
79
+ }
80
+ throw new Error("Invalid --date. Use today, yesterday, or YYYY-MM-DD.");
81
+ }
82
+
83
+ export function resolveProviderArg(value = "all") {
84
+ const normalized = String(value || "all").trim().toLowerCase();
85
+ if (normalized === "claude-code" || normalized === "claude_code") return "claude";
86
+ if (!QUERY_PROVIDERS.includes(normalized)) throw new Error("Invalid --provider. Use all, codex, or claude.");
87
+ return normalized;
88
+ }
89
+
90
+ export function uploadProvidersFor(value = "all") {
91
+ const provider = resolveProviderArg(value);
92
+ return provider === "all" ? UPLOAD_PROVIDERS : [provider];
93
+ }
94
+
95
+ export function dateRange(from, to) {
96
+ const start = new Date(`${from}T00:00:00.000Z`);
97
+ const end = new Date(`${to}T00:00:00.000Z`);
98
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || start > end) {
99
+ throw new Error("--from must be earlier than or equal to --to.");
100
+ }
101
+ const dates = [];
102
+ for (const cursor = new Date(start); cursor <= end; cursor.setUTCDate(cursor.getUTCDate() + 1)) {
103
+ dates.push(cursor.toISOString().slice(0, 10));
104
+ }
105
+ return dates;
106
+ }
107
+
108
+ export function extractDailyReport(codexbarJson, date) {
109
+ const containers = Array.isArray(codexbarJson) ? codexbarJson : [codexbarJson];
110
+ const report = containers.find((item) => Array.isArray(item?.daily));
111
+ if (!report) throw new Error("CodexBar JSON does not contain a daily report.");
112
+ const row = report.daily.find((item) => item?.date === date);
113
+ if (!row) throw new Error(`No CodexBar daily row found for ${date}.`);
114
+ return row;
115
+ }
116
+
117
+ const normalizeCost = (value) => (typeof value === "number" ? Number(value.toFixed(6)) : value ?? null);
118
+
119
+ /**
120
+ * @param {Record<string, any>} row
121
+ * @param {{ provider?: string, codexbarVersion?: string | null, generatedAt?: Date }} [options]
122
+ */
123
+ export function buildUploadPayload(row, { provider = "codex", codexbarVersion, generatedAt = new Date() } = {}) {
124
+ if (!UPLOAD_PROVIDERS.includes(provider)) throw new Error("provider must be codex or claude.");
125
+ return {
126
+ date: row.date,
127
+ provider,
128
+ inputTokens: row.inputTokens,
129
+ outputTokens: row.outputTokens,
130
+ cacheReadTokens: row.cacheReadTokens ?? 0,
131
+ cacheCreationTokens: row.cacheCreationTokens ?? 0,
132
+ totalTokens: row.totalTokens,
133
+ totalCost: normalizeCost(row.totalCost),
134
+ modelsUsed: row.modelsUsed ?? [],
135
+ modelBreakdowns: (row.modelBreakdowns ?? []).map((item) => ({
136
+ modelName: item.modelName,
137
+ totalTokens: item.totalTokens,
138
+ cost: normalizeCost(item.cost),
139
+ })),
140
+ codexbarVersion: codexbarVersion ?? null,
141
+ generatedAt: generatedAt.toISOString(),
142
+ };
143
+ }
144
+
145
+ export function parseCodexbarVersion(stdout) {
146
+ const match = String(stdout || "").match(/(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/);
147
+ return match?.[1] ?? null;
148
+ }
149
+
150
+ async function readConfig() {
151
+ try {
152
+ return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
153
+ } catch (error) {
154
+ if (error.code === "ENOENT") return {};
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ async function writeConfig(config) {
160
+ await mkdir(path.dirname(CONFIG_PATH), { recursive: true });
161
+ await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
162
+ }
163
+
164
+ async function runCodexbar(args) {
165
+ const errors = [];
166
+ for (const bin of codexbarCandidates()) {
167
+ try {
168
+ return await execFileAsync(bin, args, { maxBuffer: 64 * 1024 * 1024 });
169
+ } catch (error) {
170
+ errors.push(`${bin}: ${error.code || error.message}`);
171
+ }
172
+ }
173
+ throw new Error(`CodexBar CLI is not available. Tried: ${errors.join("; ")}
174
+
175
+ ${CODEXBAR_INSTALL_HELP}`);
176
+ }
177
+
178
+ async function codexbarCostJson(provider) {
179
+ const { stdout } = await runCodexbar(["cost", "--provider", provider, "--format", "json"]);
180
+ return JSON.parse(stdout);
181
+ }
182
+
183
+ async function codexbarVersion() {
184
+ for (const bin of codexbarCandidates()) {
185
+ try {
186
+ const { stdout } = await execFileAsync(bin, ["--version"]);
187
+ const version = parseCodexbarVersion(stdout);
188
+ if (version) return version;
189
+ } catch {
190
+ /* try next candidate */
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+
196
+ async function configure(options) {
197
+ if (!options.token) throw new Error("configure requires --token <uploadToken>.");
198
+ await writeConfig({ apiBase: options.apiBase || DEFAULT_API_BASE, uploadToken: options.token });
199
+ console.log(`Saved config to ${CONFIG_PATH}`);
200
+ }
201
+
202
+ async function codexbarCostReports(providerArg) {
203
+ const reports = new Map();
204
+ for (const provider of uploadProvidersFor(providerArg)) {
205
+ reports.set(provider, await codexbarCostJson(provider));
206
+ }
207
+ return reports;
208
+ }
209
+
210
+ export function payloadsForDate(reports, date, codexbarVersion) {
211
+ const payloads = [];
212
+ const skipped = [];
213
+ for (const [provider, report] of reports.entries()) {
214
+ try {
215
+ payloads.push(buildUploadPayload(extractDailyReport(report, date), { provider, codexbarVersion }));
216
+ } catch (error) {
217
+ skipped.push({ provider, date, reason: error instanceof Error ? error.message : String(error) });
218
+ }
219
+ }
220
+ return { payloads, skipped };
221
+ }
222
+
223
+ async function postPayload(apiBase, token, payload) {
224
+ const response = await fetch(`${apiBase.replace(/\/$/, "")}/uploads`, {
225
+ method: "POST",
226
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
227
+ body: JSON.stringify(payload),
228
+ });
229
+ const body = await response.json().catch(() => ({}));
230
+ if (!response.ok) throw new Error(`Upload failed for ${payload.date}/${payload.provider}: ${body.message || body.error || response.status}`);
231
+ }
232
+
233
+ function printUploadResult({ payloads, skipped }) {
234
+ for (const payload of payloads) {
235
+ console.log(`Uploaded ${payload.date} ${payload.provider}: ${payload.totalTokens.toLocaleString()} tokens`);
236
+ }
237
+ for (const item of skipped) {
238
+ console.log(`Skipped ${item.date} ${item.provider}: ${item.reason}`);
239
+ }
240
+ if (!payloads.length) console.log("No payloads uploaded.");
241
+ }
242
+
243
+ async function upload(options) {
244
+ const config = await readConfig();
245
+ const apiBase = options.apiBase || config.apiBase || DEFAULT_API_BASE;
246
+ const token = options.token || config.uploadToken;
247
+ if (!token && !options.dryRun) throw new Error("Missing upload token. Run configure first.");
248
+ const date = resolveDateArg(options.date || "yesterday");
249
+ const provider = resolveProviderArg(options.provider || "all");
250
+ const [reports, version] = await Promise.all([codexbarCostReports(provider), codexbarVersion()]);
251
+ const result = payloadsForDate(reports, date, version);
252
+ if (options.dryRun) {
253
+ console.log(JSON.stringify({ apiBase, date, provider, ...result }, null, 2));
254
+ return;
255
+ }
256
+ for (const payload of result.payloads) await postPayload(apiBase, token, payload);
257
+ printUploadResult(result);
258
+ }
259
+
260
+ async function backfill(options) {
261
+ if (!options.from) throw new Error("backfill requires --from YYYY-MM-DD.");
262
+ const config = await readConfig();
263
+ const apiBase = options.apiBase || config.apiBase || DEFAULT_API_BASE;
264
+ const token = options.token || config.uploadToken;
265
+ if (!token && !options.dryRun) throw new Error("Missing upload token. Run configure first.");
266
+ const from = resolveDateArg(options.from);
267
+ const to = resolveDateArg(options.to || "today");
268
+ const provider = resolveProviderArg(options.provider || "all");
269
+ const [reports, version] = await Promise.all([codexbarCostReports(provider), codexbarVersion()]);
270
+ const results = dateRange(from, to).map((date) => ({ date, ...payloadsForDate(reports, date, version) }));
271
+ if (options.dryRun) {
272
+ console.log(JSON.stringify({ apiBase, from, to, provider, results }, null, 2));
273
+ return;
274
+ }
275
+ for (const result of results) {
276
+ for (const payload of result.payloads) await postPayload(apiBase, token, payload);
277
+ printUploadResult(result);
278
+ }
279
+ }
280
+
281
+ export async function main(argv = process.argv.slice(2)) {
282
+ const options = parseArgs(argv);
283
+ if (!options.command || options.command === "--help" || options.command === "-h" || options.help) {
284
+ console.log(usage());
285
+ return;
286
+ }
287
+ if (options.command === "--version" || options.command === "-V" || options.version) {
288
+ console.log(VERSION);
289
+ return;
290
+ }
291
+ if (options.command === "configure") return configure(options);
292
+ if (options.command === "upload") return upload(options);
293
+ if (options.command === "backfill") return backfill(options);
294
+ throw new Error(usage());
295
+ }
296
+
297
+ export function isCliEntrypoint(argvEntry = process.argv[1]) {
298
+ if (!argvEntry) return false;
299
+ try {
300
+ return realpathSync(argvEntry) === fileURLToPath(import.meta.url);
301
+ } catch {
302
+ return false;
303
+ }
304
+ }
305
+
306
+ if (isCliEntrypoint()) {
307
+ main().catch((error) => {
308
+ console.error(error.message);
309
+ process.exitCode = 1;
310
+ });
311
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "candobear-token-leaderboard",
3
+ "version": "0.1.0",
4
+ "description": "CLI uploader for the Candobear Token Economics leaderboard.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "homepage": "https://ai.candobear.com/tokeneconomics",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/howie-serious/token-economics.git",
11
+ "directory": "packages/candobear-token-leaderboard"
12
+ },
13
+ "bin": {
14
+ "candobear-token": "bin/candobear-token.mjs"
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ }
26
+ }