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 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.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ runCli().catch((error) => {
6
+ console.error(`ai-wrapped failed: ${error?.message || error}`);
7
+ process.exitCode = 1;
8
+ });
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
+ }