Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to tokenmaxxing are documented here. Versions are anchored t

## Unreleased

### Added

- Added Amp, Codebuff, Droid, Goose, Grok Build CLI, Kilo Code, Kimi, OpenClaw, Qwen Code, and
Supercharge as supported sources. Supercharge is collected natively from its own session store
under the Supercharge data home (`SUPERCHARGE_HOME`, default `~/.supercharge`), independently of
ccusage.
- Carried custom agent data-directory environment variables (`GROK_HOME`, `SUPERCHARGE_HOME`,
`AMP_DATA_DIR`, `QWEN_DATA_DIR`, `KIMI_DATA_DIR`, `KILO_DATA_DIR`, `GOOSE_PATH_ROOT`,
`DROID_SESSIONS_DIR`, `CODEBUFF_DATA_DIR`, `OPENCLAW_DIR`) into scheduled syncs.

### Changed

- Required ccusage 20.0.20 or newer, which adds the Grok Build CLI adapter.

## 0.6.0 - 2026-08-05

### Added
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ installs automatic syncing, and opens your public profile.

## How it works

tokenmaxxing uses [ccusage](https://ccusage.com/) to read local coding-agent
usage, turn it into daily token and API-equivalent spend totals, and sync those
aggregates to your public profile. The leaderboard lets you compare spend or
tokenmaxxing uses [ccusage](https://ccusage.com/) to read local coding-agent usage
and collects Supercharge from its own local session store. It turns that usage into daily token
and API-equivalent spend totals, then syncs those aggregates to your public profile. The leaderboard lets you compare spend or
tokens over the last 7 days, 30 days, or all time.

Sync is idempotent and profiles aggregate across devices, so you can run
Expand All @@ -64,6 +64,16 @@ Sync is idempotent and profiles aggregate across devices, so you can run
- GitHub Copilot CLI
- Hermes
- Pi
- Supercharge
- Grok Build CLI
- Amp
- Qwen Code
- Kimi
- Kilo Code
- Goose
- Droid
- Codebuff
- OpenClaw

## Usage

Expand Down
5 changes: 3 additions & 2 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

CLI for [tokenmaxxing](https://tokenmaxxing.sh) — the social leaderboard
for LLM token usage. Parses your local agent usage (Claude Code, Codex,
OpenCode, Gemini CLI, Copilot CLI, Hermes, Pi) via
[ccusage](https://github.com/ryoppippi/ccusage) and pushes daily aggregates
OpenCode, Gemini CLI, Copilot CLI, Hermes, Pi, Grok Build CLI, Amp, Qwen Code, Kimi,
Kilo Code, Goose, Droid, Codebuff, OpenClaw) via [ccusage](https://github.com/ryoppippi/ccusage).
Supercharge is collected natively from its own local session store. The CLI pushes daily aggregates
to your public profile.

## Usage
Expand Down
26 changes: 13 additions & 13 deletions apps/cli/src/ccusage/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function ccusageErrorFor<A>(effect: Effect.Effect<A, CcusageRunError>) {
describe("ccusage commands", () => {
it("uses the minimum v20 release with the Codex replay fix", () => {
expect(dailyCcusageCommand(codex)).toEqual([
"ccusage@^20.0.19",
"ccusage@^20.0.20",
"codex",
"daily",
"--json",
Expand All @@ -42,7 +42,7 @@ describe("ccusage commands", () => {
"calculate",
]);
expect(sessionCcusageCommand(codex)).toEqual([
"ccusage@^20.0.19",
"ccusage@^20.0.20",
"codex",
"session",
"--json",
Expand All @@ -53,7 +53,7 @@ describe("ccusage commands", () => {

it("builds focused Pi daily and session commands", () => {
expect(dailyCcusageCommand(pi)).toEqual([
"ccusage@^20.0.19",
"ccusage@^20.0.20",
"pi",
"daily",
"--json",
Expand All @@ -62,7 +62,7 @@ describe("ccusage commands", () => {
"calculate",
]);
expect(sessionCcusageCommand(pi)).toEqual([
"ccusage@^20.0.19",
"ccusage@^20.0.20",
"pi",
"session",
"--json",
Expand All @@ -73,7 +73,7 @@ describe("ccusage commands", () => {

it("builds focused Hermes daily and session commands", () => {
expect(dailyCcusageCommand(hermes)).toEqual([
"ccusage@^20.0.19",
"ccusage@^20.0.20",
"hermes",
"daily",
"--json",
Expand All @@ -82,7 +82,7 @@ describe("ccusage commands", () => {
"calculate",
]);
expect(sessionCcusageCommand(hermes)).toEqual([
"ccusage@^20.0.19",
"ccusage@^20.0.20",
"hermes",
"session",
"--json",
Expand All @@ -95,15 +95,15 @@ describe("ccusage commands", () => {
describe("ccusageCommandInvocations", () => {
it("selects the Windows npm command shim", () => {
expect(ccusageCommandInvocations(["codex", "daily"], "win32")).toEqual([
{ args: ["x", "ccusage@^20.0.19", "codex", "daily"], command: "bun" },
{ args: ["-y", "ccusage@^20.0.19", "codex", "daily"], command: "npx.cmd" },
{ args: ["x", "ccusage@^20.0.20", "codex", "daily"], command: "bun" },
{ args: ["-y", "ccusage@^20.0.20", "codex", "daily"], command: "npx.cmd" },
]);
});

it("keeps the POSIX npm fallback", () => {
expect(ccusageCommandInvocations(["codex", "daily"], "linux")).toEqual([
{ args: ["x", "ccusage@^20.0.19", "codex", "daily"], command: "bun" },
{ args: ["-y", "ccusage@^20.0.19", "codex", "daily"], command: "npx" },
{ args: ["x", "ccusage@^20.0.20", "codex", "daily"], command: "bun" },
{ args: ["-y", "ccusage@^20.0.20", "codex", "daily"], command: "npx" },
]);
});
});
Expand All @@ -118,7 +118,7 @@ describe("execCcusage", () => {
),
).resolves.toBe('{"daily":[]}');
expect(run).toHaveBeenCalledOnce();
expect(run).toHaveBeenCalledWith("bun", ["x", "ccusage@^20.0.19", "codex", "daily"]);
expect(run).toHaveBeenCalledWith("bun", ["x", "ccusage@^20.0.20", "codex", "daily"]);
});

it("falls back to npx.cmd when Bun is missing on Windows", async () => {
Expand All @@ -138,8 +138,8 @@ describe("execCcusage", () => {
execCcusage(["codex", "daily"], "codex", "daily", { platform: "win32", run }),
),
).resolves.toBe('{"daily":[]}');
expect(run).toHaveBeenNthCalledWith(1, "bun", ["x", "ccusage@^20.0.19", "codex", "daily"]);
expect(run).toHaveBeenNthCalledWith(2, "npx.cmd", ["-y", "ccusage@^20.0.19", "codex", "daily"]);
expect(run).toHaveBeenNthCalledWith(1, "bun", ["x", "ccusage@^20.0.20", "codex", "daily"]);
expect(run).toHaveBeenNthCalledWith(2, "npx.cmd", ["-y", "ccusage@^20.0.20", "codex", "daily"]);
});

it("does not mask a Bun execution failure with the npm fallback", async () => {
Expand Down
8 changes: 4 additions & 4 deletions apps/cli/src/ccusage/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { decodeDailyReport, decodeSessionReport } from "./schema";
import type { CcusageSource } from "./sources";

/**
* Shells out to `bun x ccusage@^20.0.19 <source> daily --json --breakdown` (npx
* Shells out to `bun x ccusage@^20.0.20 <source> daily --json --breakdown` (npx
* fallback only when bun itself is missing). Runner and report failures stay
* typed so the sync layer can distinguish them from valid empty reports.
*/

const CCUSAGE_SPEC = "ccusage@^20.0.19";
const CCUSAGE_SPEC = "ccusage@^20.0.20";
const RUN_TIMEOUT_MS = 180_000;

class CcusageRunError extends Data.TaggedError("CcusageRunError")<{
Expand Down Expand Up @@ -124,7 +124,7 @@ function dailyCcusageArgs(source: CcusageSource, options: RunOptions = {}): stri
args.push("--since", options.since.replaceAll("-", ""));
}

return args;
return args.filter((arg): arg is string => arg !== undefined);
}

function sessionCcusageArgs(source: CcusageSource, options: RunOptions = {}): string[] {
Expand All @@ -133,7 +133,7 @@ function sessionCcusageArgs(source: CcusageSource, options: RunOptions = {}): st
args.push("--since", options.since.replaceAll("-", ""));
}

return args;
return args.filter((arg): arg is string => arg !== undefined);
}

function execCcusage(
Expand Down
15 changes: 15 additions & 0 deletions apps/cli/src/ccusage/sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ describe("resolveSources", () => {
});
});

it("resolves Grok to the focused ccusage subcommand", () => {
expect(resolveSources(["grok"])).toEqual({
invalid: [],
sources: [{ source: "grok", subcommand: "grok" }],
});
});

it("resolves Supercharge as a native (subcommand-less) source", () => {
const { invalid, sources } = resolveSources(["supercharge"]);

expect(invalid).toEqual([]);
expect(sources[0]?.source).toBe("supercharge");
expect(sources[0]?.subcommand).toBeUndefined();
});

it("rejects unknown sources", () => {
expect(resolveSources(["bogus"]).invalid).toEqual(["bogus"]);
});
Expand Down
14 changes: 12 additions & 2 deletions apps/cli/src/ccusage/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/

interface CcusageSource {
/** ccusage subcommand. */
subcommand: string;
/** ccusage subcommand; absent for sources collected natively (supercharge). */
subcommand?: string;
/** The source tag stored server-side and shown on profiles. */
source: string;
}
Expand All @@ -20,6 +20,16 @@ const CCUSAGE_SOURCES: readonly CcusageSource[] = [
{ source: "copilot", subcommand: "copilot" },
{ source: "hermes", subcommand: "hermes" },
{ source: "pi", subcommand: "pi" },
{ source: "supercharge" },
{ source: "grok", subcommand: "grok" },
{ source: "amp", subcommand: "amp" },
{ source: "qwen", subcommand: "qwen" },
{ source: "kimi", subcommand: "kimi" },
{ source: "kilo", subcommand: "kilo" },
{ source: "goose", subcommand: "goose" },
{ source: "droid", subcommand: "droid" },
{ source: "codebuff", subcommand: "codebuff" },
{ source: "openclaw", subcommand: "openclaw" },
];

const DEFAULT_SOURCE_NAMES = CCUSAGE_SOURCES.map((entry) => entry.source);
Expand Down
150 changes: 150 additions & 0 deletions apps/cli/src/ccusage/supercharge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Effect } from "effect";
import { describe, expect, it } from "vitest";

import { collectSuperchargeDays, superchargeDailyReport } from "./supercharge";

function turn(
sessionId: string,
eventId: string,
timestampSec: number,
model: string,
tokens: Record<string, number>,
): string {
return JSON.stringify({
timestamp: timestampSec,
method: "_x.ai/session/update",
params: {
sessionId,
update: {
sessionUpdate: "turn_completed",
usage: {
inputTokens: tokens.input ?? 0,
outputTokens: tokens.output ?? 0,
cachedReadTokens: tokens.cachedRead ?? 0,
cacheCreationTokens: tokens.cacheCreation ?? 0,
reasoningTokens: tokens.reasoning ?? 0,
costUsdTicks: Math.round((tokens.costUsd ?? 0) * 1e10),
modelUsage: {
[model]: {
inputTokens: tokens.input ?? 0,
outputTokens: tokens.output ?? 0,
totalTokens: (tokens.input ?? 0) + (tokens.output ?? 0),
cachedReadTokens: tokens.cachedRead ?? 0,
cacheCreationTokens: tokens.cacheCreation ?? 0,
reasoningTokens: tokens.reasoning ?? 0,
costUsdTicks: Math.round((tokens.costUsd ?? 0) * 1e10),
},
},
},
},
},
_meta: { eventId },
});
}

describe("collectSuperchargeDays", () => {
it("parses turn_completed records across sessions into per-model days", async () => {
const listSessionFiles = () =>
Effect.succeed([
"/root/sessions/proj-a/s1/updates.jsonl",
"/root/sessions/proj-b/s2/updates.jsonl",
]);
const readUpdates = (path: string) =>
Effect.succeed(
path.includes("proj-a/s1")
? [
turn("s1", "e1", 1750000000, "gpt-5.6-luna", {
input: 100,
output: 20,
costUsd: 0.01,
}),
turn("s1", "e2", 1750000000, "gpt-5.6-luna", {
input: 50,
output: 5,
costUsd: 0.005,
}),
].join("\n")
: turn("s2", "e3", 1750086400, "grok-4.6-build", {
input: 200,
output: 30,
cachedRead: 100,
costUsd: 0.02,
}),
);

const result = await Effect.runPromise(
collectSuperchargeDays({ listSessionFiles, readUpdates }),
);

expect(result.sessions).toBe(2);
expect(result.days).toHaveLength(3);
expect(result.days[0]?.model).toBe("gpt-5.6-luna");
expect(result.days[0]?.usage.costUsd).toBeCloseTo(0.01);
});

it("skips records with no usage and dedupes repeated turns", async () => {
const line = turn("s1", "e1", 1750000000, "gpt-5.6-luna", {
input: 100,
output: 20,
costUsd: 0.01,
});
const listSessionFiles = () => Effect.succeed(["/root/sessions/p/s1/updates.jsonl"]);
const readUpdates = () => Effect.succeed([line, line].join("\n"));

const result = await Effect.runPromise(
collectSuperchargeDays({ listSessionFiles, readUpdates }),
);
expect(result.days).toHaveLength(1);
});
});

describe("superchargeDailyReport", () => {
it("aggregates days by date+model", () => {
const report = superchargeDailyReport([
{
date: "2026-08-06",
model: "gpt-5.6-luna",
usage: {
inputTokens: 100,
outputTokens: 20,
cacheCreationTokens: 0,
cacheReadTokens: 0,
costUsd: 0.01,
reasoningTokens: 0,
},
},
{
date: "2026-08-06",
model: "gpt-5.6-luna",
usage: {
inputTokens: 50,
outputTokens: 5,
cacheCreationTokens: 0,
cacheReadTokens: 0,
costUsd: 0.005,
reasoningTokens: 0,
},
},
{
date: "2026-08-07",
model: "grok-4.6-build",
usage: {
inputTokens: 200,
outputTokens: 30,
cacheCreationTokens: 0,
cacheReadTokens: 100,
costUsd: 0.02,
reasoningTokens: 0,
},
},
]);

expect(report.daily).toHaveLength(2);
const luna = report.daily.find((d) => d.date === "2026-08-06");
expect(luna?.inputTokens).toBe(150);
expect(luna?.outputTokens).toBe(25);
expect(luna?.totalCost).toBeCloseTo(0.015);
expect(luna?.modelBreakdowns?.[0]?.modelName).toBe("gpt-5.6-luna");
expect(luna?.modelBreakdowns?.[0]?.cost).toBeCloseTo(0.015);
});
});
Loading