tokenmaxing 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 +50 -0
- package/bin/ai-wrapped.js +8 -0
- package/package.json +29 -0
- package/src/cli.js +91 -0
- package/src/metrics.js +75 -0
- package/src/pricing.js +68 -0
- package/src/report.js +974 -0
- package/src/scanners/claude.js +198 -0
- package/src/scanners/codex.js +103 -0
- package/src/utils.js +281 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# tokenmaxing
|
|
2
|
+
|
|
3
|
+
`tokenmaxing` is a local-only, read-only terminal summary for Claude Code and Codex usage — your AI usage, wrapped.
|
|
4
|
+
|
|
5
|
+
It scans:
|
|
6
|
+
|
|
7
|
+
- Claude Code: `~/.claude` (or `$CLAUDE_CONFIG_DIR`) for usage stats and transcripts, plus project `.claude` directories under your home directory.
|
|
8
|
+
- Codex: `~/.codex` (or `$CODEX_HOME`).
|
|
9
|
+
|
|
10
|
+
It renders, one section at a time:
|
|
11
|
+
|
|
12
|
+
1. Total messages by source
|
|
13
|
+
2. Per-month token usage
|
|
14
|
+
3. Burn — local spend cache when present, otherwise a clearly marked list-price estimate
|
|
15
|
+
4. All-time token usage
|
|
16
|
+
5. Token usage summary
|
|
17
|
+
6. Input/output token comparison
|
|
18
|
+
7. Model token comparison
|
|
19
|
+
8. Model standing by input + output tokens
|
|
20
|
+
9. Most active time of day (messages per hour)
|
|
21
|
+
10. Daily activity matrix (contribution-graph style)
|
|
22
|
+
|
|
23
|
+
Every count and token figure comes from locally recorded data. The only estimated values are in the burn panel, marked with ≈.
|
|
24
|
+
|
|
25
|
+
## Run
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx tokenmaxing
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or from a clone:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
In a terminal the report reveals one section at a time, with a `[NN/total]` progress counter. Each section decodes in with a brief scramble-resolve animation.
|
|
38
|
+
|
|
39
|
+
- `enter` / `space` / `→` — next section
|
|
40
|
+
- `←` / `b` — replay the previous section
|
|
41
|
+
- `a` — show all remaining sections
|
|
42
|
+
- `q` / `ctrl-c` — quit
|
|
43
|
+
|
|
44
|
+
Provider colors are categorical: Claude is orange, Codex is cyan, the combined total is amber — so any bar, model row, or table line maps to its source at a glance.
|
|
45
|
+
|
|
46
|
+
Piped or redirected output prints everything at once without colors or animation.
|
|
47
|
+
|
|
48
|
+
## Privacy
|
|
49
|
+
|
|
50
|
+
The CLI reads local metadata and transcripts to compute usage, but the report does not print prompt text, model responses, tool outputs, credentials, or file contents. Nothing leaves your machine — there is no network call anywhere in the tool.
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tokenmaxing",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal AI usage wrapped summary for Claude Code and Codex.",
|
|
5
|
+
"author": "Kartik Gupta",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tokenmaxing": "bin/ai-wrapped.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node ./bin/ai-wrapped.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"codex",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"usage",
|
|
17
|
+
"wrapped",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
]
|
|
29
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { scanClaude } from "./scanners/claude.js";
|
|
3
|
+
import { scanCodex } from "./scanners/codex.js";
|
|
4
|
+
import { combineReports } from "./metrics.js";
|
|
5
|
+
import { renderReportBlocks, animateReveal, isColorEnabled } from "./report.js";
|
|
6
|
+
|
|
7
|
+
export async function runCli() {
|
|
8
|
+
const reports = [
|
|
9
|
+
await scanClaude({
|
|
10
|
+
claudeHome: process.env.CLAUDE_CONFIG_DIR || `${os.homedir()}/.claude`,
|
|
11
|
+
scanRoot: os.homedir(),
|
|
12
|
+
maxScanDepth: 8,
|
|
13
|
+
}),
|
|
14
|
+
await scanCodex({
|
|
15
|
+
codexHome: process.env.CODEX_HOME || `${os.homedir()}/.codex`,
|
|
16
|
+
}),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const combined = combineReports(reports);
|
|
20
|
+
const blocks = renderReportBlocks({ generatedAt: new Date().toISOString(), reports, combined });
|
|
21
|
+
|
|
22
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY;
|
|
23
|
+
if (!interactive) {
|
|
24
|
+
console.log(blocks.join("\n"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const animated = isColorEnabled();
|
|
29
|
+
const reveal = async (text) => {
|
|
30
|
+
if (animated) await animateReveal(text);
|
|
31
|
+
else console.log(text);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let index = 0;
|
|
35
|
+
await reveal(blocks[0]);
|
|
36
|
+
|
|
37
|
+
while (true) {
|
|
38
|
+
const atEnd = index === blocks.length - 1;
|
|
39
|
+
const action = await waitForStep(index + 1, blocks.length, atEnd);
|
|
40
|
+
|
|
41
|
+
if (action === "quit") break;
|
|
42
|
+
if (action === "all") {
|
|
43
|
+
if (index < blocks.length - 1) console.log(blocks.slice(index + 1).join("\n"));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (action === "next") {
|
|
47
|
+
if (atEnd) break;
|
|
48
|
+
index += 1;
|
|
49
|
+
await reveal(blocks[index]);
|
|
50
|
+
} else if (action === "back" && index > 0) {
|
|
51
|
+
index -= 1;
|
|
52
|
+
console.log(`\x1b[2m ↑ replaying ${String(index + 1).padStart(2, "0")}/${blocks.length}\x1b[0m`);
|
|
53
|
+
await reveal(blocks[index]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function waitForStep(current, total, atEnd) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const stdin = process.stdin;
|
|
61
|
+
const progress = `\x1b[1m\x1b[38;2;255;138;0m[${String(current).padStart(2, "0")}/${total}]\x1b[0m`;
|
|
62
|
+
const forward = atEnd ? "↵ finish" : "↵ next";
|
|
63
|
+
const hints = `\x1b[2m${forward} ← back a all q quit\x1b[0m`;
|
|
64
|
+
process.stdout.write(` ${progress} \x1b[2m·\x1b[0m ${hints}`);
|
|
65
|
+
|
|
66
|
+
stdin.setRawMode(true);
|
|
67
|
+
stdin.resume();
|
|
68
|
+
|
|
69
|
+
const finish = (action) => {
|
|
70
|
+
stdin.setRawMode(false);
|
|
71
|
+
stdin.pause();
|
|
72
|
+
stdin.off("data", onData);
|
|
73
|
+
process.stdout.write("\r\x1b[2K");
|
|
74
|
+
resolve(action);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const onData = (buffer) => {
|
|
78
|
+
const sequence = buffer.toString();
|
|
79
|
+
if (sequence === "\x1b[C") return finish("next");
|
|
80
|
+
if (sequence === "\x1b[D") return finish("back");
|
|
81
|
+
for (const key of sequence) {
|
|
82
|
+
if (key === "\r" || key === "\n" || key === " ") return finish("next");
|
|
83
|
+
if (key === "q" || key === "Q" || key === "\x03") return finish("quit");
|
|
84
|
+
if (key === "a" || key === "A") return finish("all");
|
|
85
|
+
if (key === "b" || key === "B" || key === "p" || key === "P") return finish("back");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
stdin.on("data", onData);
|
|
90
|
+
});
|
|
91
|
+
}
|
package/src/metrics.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createEmptyReport, emptyTokens, addTokens, makeObjectMap } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
export function combineReports(reports) {
|
|
4
|
+
const combined = createEmptyReport("All AI", "all", "");
|
|
5
|
+
combined.sources = reports.map((report) => ({
|
|
6
|
+
key: report.key,
|
|
7
|
+
provider: report.provider,
|
|
8
|
+
root: report.root,
|
|
9
|
+
available: report.available,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
for (const report of reports) {
|
|
13
|
+
combined.available ||= report.available;
|
|
14
|
+
combined.sessionFiles += report.sessionFiles;
|
|
15
|
+
combined.historyEntries += report.historyEntries;
|
|
16
|
+
combined.projectConfigDirs += report.projectConfigDirs;
|
|
17
|
+
combined.totalSessions += report.totalSessions;
|
|
18
|
+
combined.totalMessages += report.totalMessages;
|
|
19
|
+
combined.knownSpendUSD += report.knownSpendUSD;
|
|
20
|
+
combined.unknownSpend ||= report.unknownSpend;
|
|
21
|
+
addTokens(combined.tokens, report.tokens);
|
|
22
|
+
|
|
23
|
+
if (report.firstSeen && (!combined.firstSeen || report.firstSeen < combined.firstSeen)) {
|
|
24
|
+
combined.firstSeen = report.firstSeen;
|
|
25
|
+
}
|
|
26
|
+
if (report.lastSeen && (!combined.lastSeen || report.lastSeen > combined.lastSeen)) {
|
|
27
|
+
combined.lastSeen = report.lastSeen;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [day, messages] of Object.entries(report.dailyActivity || {})) {
|
|
31
|
+
combined.dailyActivity[day] = (combined.dailyActivity[day] || 0) + messages;
|
|
32
|
+
}
|
|
33
|
+
for (const [month, spend] of Object.entries(report.monthlySpendUSD)) {
|
|
34
|
+
combined.monthlySpendUSD[month] = (combined.monthlySpendUSD[month] || 0) + spend;
|
|
35
|
+
}
|
|
36
|
+
for (const [month, tokens] of Object.entries(report.monthlyTokens)) {
|
|
37
|
+
combined.monthlyTokens[month] ||= emptyTokens();
|
|
38
|
+
addTokens(combined.monthlyTokens[month], tokens);
|
|
39
|
+
}
|
|
40
|
+
for (const [model, usage] of Object.entries(report.modelUsage)) {
|
|
41
|
+
const key = `${report.key}:${model}`;
|
|
42
|
+
combined.modelUsage[key] ||= emptyTokens();
|
|
43
|
+
combined.modelUsage[key].provider = report.provider;
|
|
44
|
+
combined.modelUsage[key].model = model;
|
|
45
|
+
combined.modelUsage[key].costUSD = (combined.modelUsage[key].costUSD || 0) + (usage.costUSD || 0);
|
|
46
|
+
addTokens(combined.modelUsage[key], usage);
|
|
47
|
+
}
|
|
48
|
+
for (let hour = 0; hour < 24; hour += 1) {
|
|
49
|
+
combined.hourlyActivity[hour] += report.hourlyActivity[hour] || 0;
|
|
50
|
+
}
|
|
51
|
+
combined.notes.push(...report.notes.map((note) => `${report.provider}: ${note}`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
finalizeReport(combined);
|
|
55
|
+
return combined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function finalizeReport(report) {
|
|
59
|
+
report.dailyActivity = makeObjectMap(report.dailyActivity || {});
|
|
60
|
+
report.monthlySpendUSD = makeObjectMap(report.monthlySpendUSD);
|
|
61
|
+
report.monthlyTokens = makeObjectMap(report.monthlyTokens);
|
|
62
|
+
report.modelUsage = makeObjectMap(report.modelUsage);
|
|
63
|
+
|
|
64
|
+
const favorite = Object.values(report.modelUsage)
|
|
65
|
+
.map((usage) => ({
|
|
66
|
+
provider: usage.provider || report.provider,
|
|
67
|
+
model: usage.model || "unknown",
|
|
68
|
+
score: (usage.input || 0) + (usage.output || 0),
|
|
69
|
+
total: usage.total || 0,
|
|
70
|
+
}))
|
|
71
|
+
.sort((a, b) => b.score - a.score)[0];
|
|
72
|
+
|
|
73
|
+
report.favoriteModel = favorite || null;
|
|
74
|
+
return report;
|
|
75
|
+
}
|
package/src/pricing.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Public list prices in USD per 1M tokens. cacheWrite is the 5m prompt-cache
|
|
2
|
+
// write rate where the provider charges one; OpenAI does not bill cache writes.
|
|
3
|
+
// Reasoning tokens are billed inside output tokens by both providers, so they
|
|
4
|
+
// are intentionally not priced separately.
|
|
5
|
+
const PRICING_TABLE = [
|
|
6
|
+
// anthropic
|
|
7
|
+
{ prefix: "claude-opus-4-5", rates: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 } },
|
|
8
|
+
{ prefix: "claude-opus-4-6", rates: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 } },
|
|
9
|
+
{ prefix: "claude-opus-4-7", rates: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 } },
|
|
10
|
+
{ prefix: "claude-opus-4-8", rates: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 } },
|
|
11
|
+
{ prefix: "claude-opus", rates: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 } },
|
|
12
|
+
{ prefix: "claude-sonnet", rates: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } },
|
|
13
|
+
{ prefix: "claude-haiku-4-5", rates: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 } },
|
|
14
|
+
{ prefix: "claude-haiku", rates: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 } },
|
|
15
|
+
// openai
|
|
16
|
+
{ prefix: "gpt-5-mini", rates: { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 } },
|
|
17
|
+
{ prefix: "gpt-5-nano", rates: { input: 0.05, output: 0.4, cacheRead: 0.005, cacheWrite: 0 } },
|
|
18
|
+
{ prefix: "gpt-5", rates: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 } },
|
|
19
|
+
{ prefix: "gpt-4.1-mini", rates: { input: 0.4, output: 1.6, cacheRead: 0.1, cacheWrite: 0 } },
|
|
20
|
+
{ prefix: "gpt-4.1", rates: { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 } },
|
|
21
|
+
{ prefix: "gpt-4o-mini", rates: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0 } },
|
|
22
|
+
{ prefix: "gpt-4o", rates: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 0 } },
|
|
23
|
+
{ prefix: "o4-mini", rates: { input: 1.1, output: 4.4, cacheRead: 0.275, cacheWrite: 0 } },
|
|
24
|
+
{ prefix: "o3", rates: { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 } },
|
|
25
|
+
{ prefix: "codex-mini", rates: { input: 1.5, output: 6, cacheRead: 0.375, cacheWrite: 0 } },
|
|
26
|
+
].sort((a, b) => b.prefix.length - a.prefix.length);
|
|
27
|
+
|
|
28
|
+
export function priceForModel(model) {
|
|
29
|
+
const normalized = String(model || "")
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/\./g, "-")
|
|
32
|
+
.trim();
|
|
33
|
+
for (const entry of PRICING_TABLE) {
|
|
34
|
+
if (normalized.startsWith(entry.prefix.replace(/\./g, "-"))) return entry.rates;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function estimateTokensCostUSD(tokens, rates) {
|
|
40
|
+
if (!rates) return null;
|
|
41
|
+
return (
|
|
42
|
+
((tokens.input || 0) * rates.input +
|
|
43
|
+
(tokens.output || 0) * rates.output +
|
|
44
|
+
(tokens.cacheRead || 0) * rates.cacheRead +
|
|
45
|
+
(tokens.cacheCreation || 0) * rates.cacheWrite) /
|
|
46
|
+
1_000_000
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function estimateModelCosts(modelUsage) {
|
|
51
|
+
const priced = [];
|
|
52
|
+
const unpriced = [];
|
|
53
|
+
let totalUSD = 0;
|
|
54
|
+
|
|
55
|
+
for (const usage of Object.values(modelUsage || {})) {
|
|
56
|
+
const rates = priceForModel(usage.model);
|
|
57
|
+
const costUSD = estimateTokensCostUSD(usage, rates);
|
|
58
|
+
if (costUSD === null) {
|
|
59
|
+
unpriced.push(usage.model || "unknown");
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
totalUSD += costUSD;
|
|
63
|
+
priced.push({ usage, rates, costUSD });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
priced.sort((a, b) => b.costUSD - a.costUSD);
|
|
67
|
+
return { priced, unpriced, totalUSD };
|
|
68
|
+
}
|