tokenmaxxing-cli 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.
@@ -0,0 +1 @@
1
+ {"openai":{"models":{"gpt-5.4":{"cost":{"input":2.5,"output":15,"cache_read":0.25,"tiers":[{"input":5,"output":22.5,"cache_read":0.5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":5,"output":22.5,"cache_read":0.5}},"name":"GPT-5.4"},"gpt-5.4-pro":{"cost":{"input":30,"output":180,"tiers":[{"input":60,"output":270,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":60,"output":270}},"name":"GPT-5.4 Pro"},"gpt-3.5-turbo":{"cost":{"input":0.5,"output":1.5,"cache_read":0},"name":"GPT-3.5-turbo"},"gpt-5.5-pro":{"cost":{"input":30,"output":180,"tiers":[{"input":60,"output":270,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":60,"output":270}},"name":"GPT-5.5 Pro"},"text-embedding-3-small":{"cost":{"input":0.02,"output":0},"name":"text-embedding-3-small"},"gpt-5.4-nano":{"cost":{"input":0.2,"output":1.25,"cache_read":0.02},"name":"GPT-5.4 nano"},"gpt-realtime-2.1":{"cost":{"input":4,"output":24,"cache_read":0.4,"input_audio":32,"output_audio":64},"name":"GPT-Realtime-2.1"},"gpt-4o-2024-05-13":{"cost":{"input":5,"output":15},"name":"GPT-4o (2024-05-13)"},"gpt-4o":{"cost":{"input":2.5,"output":10,"cache_read":1.25},"name":"GPT-4o"},"gpt-5-mini":{"cost":{"input":0.25,"output":2,"cache_read":0.025},"name":"GPT-5 Mini"},"gpt-image-2":{"cost":{"input":5,"output":30,"cache_read":1.25},"name":"gpt-image-2"},"gpt-5.2-pro":{"cost":{"input":21,"output":168},"name":"GPT-5.2 Pro"},"o4-mini":{"cost":{"input":1.1,"output":4.4,"cache_read":0.275},"name":"o4-mini"},"o3-mini":{"cost":{"input":1.1,"output":4.4,"cache_read":0.55},"name":"o3-mini"},"text-embedding-ada-002":{"cost":{"input":0.1,"output":0},"name":"text-embedding-ada-002"},"gpt-4":{"cost":{"input":30,"output":60},"name":"GPT-4"},"gpt-5.3-codex":{"cost":{"input":1.75,"output":14,"cache_read":0.175},"name":"GPT-5.3 Codex"},"gpt-4.1-nano":{"cost":{"input":0.1,"output":0.4,"cache_read":0.025},"name":"GPT-4.1 nano"},"gpt-5-nano":{"cost":{"input":0.05,"output":0.4,"cache_read":0.005},"name":"GPT-5 Nano"},"gpt-5.6":{"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}},"name":"GPT-5.6"},"gpt-5.2-chat-latest":{"cost":{"input":1.75,"output":14,"cache_read":0.175},"name":"GPT-5.2 Chat"},"o1":{"cost":{"input":15,"output":60,"cache_read":7.5},"name":"o1"},"gpt-5-pro":{"cost":{"input":15,"output":120},"name":"GPT-5 Pro"},"gpt-5.3-codex-spark":{"cost":{"input":1.75,"output":14,"cache_read":0.175},"name":"GPT-5.3 Codex Spark"},"text-embedding-3-large":{"cost":{"input":0.13,"output":0},"name":"text-embedding-3-large"},"gpt-4o-2024-08-06":{"cost":{"input":2.5,"output":10,"cache_read":1.25},"name":"GPT-4o (2024-08-06)"},"gpt-6-astra":{"cost":{"input":10,"output":50,"cache_read":1,"cache_write":12.5,"tiers":[{"input":20,"output":75,"cache_read":2,"cache_write":25,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":20,"output":75,"cache_read":2,"cache_write":25}},"name":"GPT-6 Astra"},"gpt-5.1":{"cost":{"input":1.25,"output":10,"cache_read":0.125},"name":"GPT-5.1"},"gpt-4o-mini":{"cost":{"input":0.15,"output":0.6,"cache_read":0.075},"name":"GPT-4o mini"},"o3-pro":{"cost":{"input":20,"output":80},"name":"o3-pro"},"gpt-5.4-mini":{"cost":{"input":0.75,"output":4.5,"cache_read":0.075},"name":"GPT-5.4 mini"},"gpt-5.6-luna":{"cost":{"input":0.2,"output":1.2,"cache_read":0.02,"cache_write":0.25,"tiers":[{"input":0.4,"output":1.8,"cache_read":0.04,"cache_write":0.5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":0.4,"output":1.8,"cache_read":0.04,"cache_write":0.5}},"name":"GPT-5.6 Luna"},"gpt-5.2":{"cost":{"input":1.75,"output":14,"cache_read":0.175},"name":"GPT-5.2"},"gpt-5.5":{"cost":{"input":5,"output":30,"cache_read":0.5,"tiers":[{"input":10,"output":45,"cache_read":1,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":10,"output":45,"cache_read":1}},"name":"GPT-5.5"},"gpt-4.1":{"cost":{"input":2,"output":8,"cache_read":0.5},"name":"GPT-4.1"},"gpt-4o-2024-11-20":{"cost":{"input":2.5,"output":10,"cache_read":1.25},"name":"GPT-4o (2024-11-20)"},"gpt-4.1-mini":{"cost":{"input":0.4,"output":1.6,"cache_read":0.1},"name":"GPT-4.1 mini"},"gpt-6-luna":{"cost":{"input":0.1,"output":0.5,"cache_read":0.01,"cache_write":0.125,"tiers":[{"input":0.2,"output":0.75,"cache_read":0.02,"cache_write":0.25,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":0.2,"output":0.75,"cache_read":0.02,"cache_write":0.25}},"name":"GPT-6 Luna"},"gpt-5.3-chat-latest":{"cost":{"input":1.75,"output":14,"cache_read":0.175},"name":"GPT-5.3 Chat (latest)"},"gpt-5.6-terra":{"cost":{"input":2,"output":12,"cache_read":0.2,"cache_write":2.5,"tiers":[{"input":4,"output":18,"cache_read":0.4,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4,"cache_write":5}},"name":"GPT-5.6 Terra"},"gpt-4-turbo":{"cost":{"input":10,"output":30},"name":"GPT-4 Turbo"},"o3":{"cost":{"input":2,"output":8,"cache_read":0.5},"name":"o3"},"gpt-5":{"cost":{"input":1.25,"output":10,"cache_read":0.125},"name":"GPT-5"},"gpt-5.6-sol":{"cost":{"input":4,"output":20,"cache_read":0.4,"cache_write":5,"tiers":[{"input":8,"output":30,"cache_read":0.8,"cache_write":10,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":8,"output":30,"cache_read":0.8,"cache_write":10}},"name":"GPT-5.6 Sol"},"gpt-6-sol":{"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5,"tiers":[{"input":4,"output":15,"cache_read":0.4,"cache_write":5,"tier":{"type":"context","size":272000}}],"context_over_200k":{"input":4,"output":15,"cache_read":0.4,"cache_write":5}},"name":"GPT-6 Sol"},"o1-pro":{"cost":{"input":150,"output":600},"name":"o1-pro"}}},"anthropic":{"models":{"claude-haiku-4-5":{"cost":{"input":1,"output":5,"cache_read":0.1,"cache_write":1.25},"name":"Claude Haiku 4.5 (latest)"},"claude-opus-4-5":{"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25},"name":"Claude Opus 4.5 (latest)"},"claude-sonnet-4-5":{"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75},"name":"Claude Sonnet 4.5 (latest)"},"claude-opus-5-5":{"cost":{"input":4,"output":20,"cache_read":0.2,"cache_write":5},"name":"Claude Opus 5.5"},"claude-fable-5-1":{"cost":{"input":10,"output":50,"cache_read":0.25,"cache_write":12.5},"name":"Claude Fable 5.1"},"claude-opus-4-5-20251101":{"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25},"name":"Claude Opus 4.5"},"claude-opus-5":{"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25},"name":"Claude Opus 5"},"claude-fable-5":{"cost":{"input":10,"output":50,"cache_read":1,"cache_write":12.5},"name":"Claude Fable 5"},"claude-opus-4-8":{"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25},"name":"Claude Opus 4.8"},"claude-sonnet-4-5-20250929":{"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75},"name":"Claude Sonnet 4.5"},"claude-sonnet-5":{"cost":{"input":2,"output":10,"cache_read":0.2,"cache_write":2.5},"name":"Claude Sonnet 5"},"claude-opus-4-6":{"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25},"name":"Claude Opus 4.6"},"claude-haiku-4-5-20251001":{"cost":{"input":1,"output":5,"cache_read":0.1,"cache_write":1.25},"name":"Claude Haiku 4.5"},"claude-sonnet-4-6":{"cost":{"input":3,"output":15,"cache_read":0.3,"cache_write":3.75},"name":"Claude Sonnet 4.6"},"claude-opus-4-7":{"cost":{"input":5,"output":25,"cache_read":0.5,"cache_write":6.25},"name":"Claude Opus 4.7"}}},"google":{"models":{"gemini-flash-latest":{"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75},"name":"Gemini Flash Latest"},"gemini-omni-flash-preview":{"cost":{"input":1.5,"output":17.5},"name":"Gemini Omni Flash Preview"},"gemini-3.1-flash-image-preview":{"cost":{"input":0.5,"output":60},"name":"Nano Banana 2"},"lyria-3-pro-preview":{"cost":{"input":0,"output":0},"name":"Lyria 3 Pro Preview"},"gemini-3.1-flash-tts-preview":{"cost":{"input":1,"output":20},"name":"Gemini 3.1 Flash TTS Preview"},"gemini-2.5-flash-image":{"cost":{"input":0.3,"output":30,"cache_read":0.075},"name":"Nano Banana"},"gemini-flash-lite-latest":{"cost":{"input":0.3,"output":2.5,"cache_read":0.03},"name":"Gemini Flash-Lite Latest"},"gemini-embedding-2":{"cost":{"input":0.2,"output":0,"input_audio":6.5},"name":"Gemini Embedding 2"},"gemini-3.6-flash":{"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75},"name":"Gemini 3.6 Flash"},"gemini-3-pro-image-preview":{"cost":{"input":2,"output":120},"name":"Nano Banana Pro"},"gemini-2.5-pro-preview-tts":{"cost":{"input":1,"output":20},"name":"Gemini 2.5 Pro Preview TTS"},"gemini-3.5-flash-lite":{"cost":{"input":0.3,"output":2.5,"cache_read":0.03},"name":"Gemini 3.5 Flash Lite"},"gemini-3.1-flash-image":{"cost":{"input":0.5,"output":60},"name":"Nano Banana 2"},"lyria-3-clip-preview":{"cost":{"input":0,"output":0},"name":"Lyria 3 Clip Preview"},"gemini-3.1-pro-preview":{"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}},"name":"Gemini 3.1 Pro Preview"},"gemini-2.5-computer-use-preview-10-2025":{"cost":{"input":1.25,"output":10,"tiers":[{"input":2.5,"output":15,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":15}},"name":"Gemini 2.5 Computer Use Preview 10-2025"},"gemini-3.5-flash":{"cost":{"input":1.5,"output":9,"cache_read":0.15,"input_audio":1.5},"name":"Gemini 3.5 Flash"},"gemini-2.5-pro":{"cost":{"input":1.25,"output":10,"cache_read":0.125,"tiers":[{"input":2.5,"output":15,"cache_read":0.25,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":2.5,"output":15,"cache_read":0.25}},"name":"Gemini 2.5 Pro"},"gemini-3-pro-image":{"cost":{"input":2,"output":120},"name":"Nano Banana Pro"},"gemini-2.5-flash":{"cost":{"input":0.3,"output":2.5,"cache_read":0.03,"input_audio":1},"name":"Gemini 2.5 Flash"},"gemini-2.5-flash-preview-tts":{"cost":{"input":0.5,"output":10},"name":"Gemini 2.5 Flash Preview TTS"},"gemini-3.1-flash-lite-image":{"cost":{"input":0.25,"output":30},"name":"Nano Banana 2 Lite"},"deep-research-preview-04-2026":{"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}},"name":"Deep Research Preview (Apr-21-2026)"},"deep-research-max-preview-04-2026":{"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}},"name":"Deep Research Max Preview (Apr-21-2026)"},"gemini-3.7-flash":{"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75},"name":"Gemini 3.7 Flash"},"gemini-3.1-pro-preview-customtools":{"cost":{"input":2,"output":12,"cache_read":0.2,"tiers":[{"input":4,"output":18,"cache_read":0.4,"tier":{"type":"context","size":200000}}],"context_over_200k":{"input":4,"output":18,"cache_read":0.4}},"name":"Gemini 3.1 Pro Preview Custom Tools"},"gemini-3-flash-preview":{"cost":{"input":0.5,"output":3,"cache_read":0.05,"input_audio":1},"name":"Gemini 3 Flash Preview"},"gemini-3.8-flash":{"cost":{"input":0.75,"output":3.75,"cache_read":0.075,"input_audio":0.75},"name":"Gemini 3.8 Flash"},"gemini-3.5-live-translate-preview":{"cost":{"input":3.5,"output":21,"input_audio":3.5,"output_audio":21},"name":"Gemini 3.5 Live Translate Preview"},"gemini-2.5-flash-lite":{"cost":{"input":0.1,"output":0.4,"cache_read":0.01,"input_audio":0.3},"name":"Gemini 2.5 Flash-Lite"},"gemini-embedding-001":{"cost":{"input":0.15,"output":0},"name":"Gemini Embedding 001"},"gemini-3.1-flash-lite-preview":{"cost":{"input":0.25,"output":1.5,"cache_read":0.025,"input_audio":0.5},"name":"Gemini 3.1 Flash Lite Preview"},"gemini-3.1-flash-live-preview":{"cost":{"input":0.75,"output":4.5,"input_audio":3,"output_audio":12},"name":"Gemini 3.1 Flash Live Preview"},"gemini-3.1-flash-lite":{"cost":{"input":0.25,"output":1.5,"cache_read":0.025,"input_audio":0.5},"name":"Gemini 3.1 Flash Lite"}}}}
@@ -0,0 +1,4 @@
1
+ {
2
+ "codex-auto-review": { "unpriced": "bundled reviewer, no list price" },
3
+ "gpt-reserve": { "unpriced": "bundled with Codex, no list price" }
4
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,61 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ /** Ask a yes/no question on the TTY. Non-interactive stdin answers "no". */
3
+ export async function confirm(question) {
4
+ if (!process.stdin.isTTY)
5
+ return false;
6
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
7
+ try {
8
+ const a = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
9
+ return a === 'y' || a === 'yes';
10
+ }
11
+ finally {
12
+ rl.close();
13
+ }
14
+ }
15
+ /** Minimal line diff (LCS) for showing exactly what an edit changes. */
16
+ export function lineDiff(before, after) {
17
+ const a = before === '' ? [] : before.replace(/\n$/, '').split('\n');
18
+ const b = after === '' ? [] : after.replace(/\n$/, '').split('\n');
19
+ const n = a.length;
20
+ const m = b.length;
21
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
22
+ for (let i = n - 1; i >= 0; i--)
23
+ for (let j = m - 1; j >= 0; j--)
24
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
25
+ const out = [];
26
+ let i = 0;
27
+ let j = 0;
28
+ while (i < n || j < m) {
29
+ if (i < n && j < m && a[i] === b[j]) {
30
+ out.push(` ${a[i]}`);
31
+ i++;
32
+ j++;
33
+ }
34
+ else if (i < n && (j >= m || dp[i + 1][j] >= dp[i][j + 1])) {
35
+ out.push(`- ${a[i++]}`);
36
+ }
37
+ else {
38
+ out.push(`+ ${b[j++]}`);
39
+ }
40
+ }
41
+ // Keep 3 lines of context around changes.
42
+ const keep = new Set();
43
+ out.forEach((l, k) => {
44
+ if (l[0] !== ' ')
45
+ for (let d = -3; d <= 3; d++)
46
+ keep.add(k + d);
47
+ });
48
+ const res = [];
49
+ let skipped = false;
50
+ out.forEach((l, k) => {
51
+ if (keep.has(k)) {
52
+ res.push(l);
53
+ skipped = false;
54
+ }
55
+ else if (!skipped) {
56
+ res.push(' …');
57
+ skipped = true;
58
+ }
59
+ });
60
+ return res.join('\n');
61
+ }
package/dist/report.js ADDED
@@ -0,0 +1,186 @@
1
+ import { fmtPrice, fmtTokens, fmtUsd, localDate, table } from './format.js';
2
+ import { inPeriod } from './period.js';
3
+ import { CUSTOM, describeLines, providerMonthly, PROVIDERS, roi, sourceFor, unitPrice } from './plans.js';
4
+ import { cost, resolve, SNAPSHOT_DATE, unpricedNote } from './pricing/index.js';
5
+ import { COUNT_FIELDS } from './types.js';
6
+ const tokensOf = (b) => b.input + b.cache_read + b.cache_write_5m + b.cache_write_1h + b.output;
7
+ function zeroRow() {
8
+ return {
9
+ input: 0, cache_read: 0, cache_write_5m: 0, cache_write_1h: 0, output: 0, reasoning: 0,
10
+ requests: 0, conversations: 0, cost: null, unpriced_tokens: 0,
11
+ };
12
+ }
13
+ export function buildReport(buckets, period, cfg, by = 'source-model') {
14
+ const groups = new Map();
15
+ const unpriced = new Map();
16
+ const costBySource = new Map();
17
+ const totals = { ...zeroRow(), cost: 0 };
18
+ for (const b of buckets) {
19
+ if (b.source === 'cursor' || !inPeriod(b.ts, period))
20
+ continue;
21
+ const day = localDate(Date.parse(b.ts));
22
+ const key = by === 'model' ? b.model : by === 'source' ? b.source : by === 'day' ? day : `${b.source}|${b.model}`;
23
+ let row = groups.get(key);
24
+ if (!row) {
25
+ row = zeroRow();
26
+ if (by === 'model')
27
+ row.model = b.model;
28
+ else if (by === 'source')
29
+ row.source = b.source;
30
+ else if (by === 'day')
31
+ row.day = day;
32
+ else {
33
+ row.source = b.source;
34
+ row.model = b.model;
35
+ }
36
+ groups.set(key, row);
37
+ }
38
+ for (const f of COUNT_FIELDS) {
39
+ row[f] += b[f];
40
+ totals[f] += b[f];
41
+ }
42
+ const rates = resolve(b.model);
43
+ if (rates) {
44
+ const c = cost(b, rates);
45
+ row.cost = (row.cost ?? 0) + c;
46
+ totals.cost += c;
47
+ costBySource.set(b.source, (costBySource.get(b.source) ?? 0) + c);
48
+ }
49
+ else {
50
+ const t = tokensOf(b);
51
+ row.unpriced_tokens += t;
52
+ totals.unpriced_tokens += t;
53
+ const uk = `${b.source}|${b.model}`;
54
+ let u = unpriced.get(uk);
55
+ if (!u) {
56
+ const note = unpricedNote(b.model);
57
+ u = { source: b.source, model: b.model, tokens: 0, ...(note ? { note } : {}) };
58
+ }
59
+ u.tokens += t;
60
+ unpriced.set(uk, u);
61
+ }
62
+ }
63
+ const rows = [...groups.values()].sort((a, b) => {
64
+ if (by === 'day')
65
+ return (a.day ?? '') < (b.day ?? '') ? -1 : 1;
66
+ if (a.source !== b.source)
67
+ return (a.source ?? '') < (b.source ?? '') ? -1 : 1;
68
+ return (b.cost ?? -1) - (a.cost ?? -1) || tokensOf(b) - tokensOf(a);
69
+ });
70
+ const plans = [];
71
+ for (const provider of PROVIDERS) {
72
+ const lines = cfg.plans[provider];
73
+ if (!lines?.length)
74
+ continue;
75
+ const source = sourceFor(provider);
76
+ const monthly = providerMonthly(provider, lines);
77
+ const c = costBySource.get(source) ?? 0;
78
+ plans.push({
79
+ provider,
80
+ source,
81
+ lines: lines.map((l) => ({
82
+ plan: l.plan,
83
+ qty: l.qty,
84
+ monthly: unitPrice(provider, l),
85
+ ...(l.plan === CUSTOM ? { label: l.label } : {}),
86
+ })),
87
+ monthly,
88
+ api_equiv: c,
89
+ roi: monthly > 0 ? roi(c, monthly, period.days) : null,
90
+ });
91
+ }
92
+ const excluded = [];
93
+ if (cfg.sources.cursor?.enabled) {
94
+ excluded.push({ source: 'cursor', reason: 'token counts not available locally; Cursor is excluded from totals' });
95
+ }
96
+ return {
97
+ period: {
98
+ label: period.label,
99
+ since: period.sinceDate,
100
+ until: period.untilDate,
101
+ days: period.days,
102
+ start: period.start.toISOString(),
103
+ end: period.end.toISOString(),
104
+ },
105
+ pricing_snapshot: SNAPSHOT_DATE,
106
+ by,
107
+ rows,
108
+ totals,
109
+ plans,
110
+ unpriced: [...unpriced.values()].sort((a, b) => b.tokens - a.tokens),
111
+ excluded,
112
+ };
113
+ }
114
+ function costCell(r) {
115
+ if (r.cost === null)
116
+ return 'unpriced';
117
+ return fmtUsd(r.cost) + (r.unpriced_tokens > 0 ? '*' : '');
118
+ }
119
+ export function renderReport(rep) {
120
+ const out = [];
121
+ out.push(`tokenmaxxing · ${rep.period.label} · ${rep.period.since} → ${rep.period.until} · pricing snapshot ${rep.pricing_snapshot}`, '');
122
+ const lead = rep.by === 'model' ? ['MODEL'] : rep.by === 'source' ? ['SOURCE'] : rep.by === 'day' ? ['DAY'] : ['SOURCE', 'MODEL'];
123
+ const header = [...lead, 'INPUT', 'CACHE R', 'CACHE W', 'OUTPUT', 'API-EQUIV'];
124
+ const align = [...lead.map(() => 'l'), 'r', 'r', 'r', 'r', 'r'];
125
+ const body = rep.rows.map((r) => [
126
+ ...(rep.by === 'model' ? [r.model] : rep.by === 'source' ? [r.source] : rep.by === 'day' ? [r.day] : [r.source, r.model]),
127
+ fmtTokens(r.input),
128
+ fmtTokens(r.cache_read),
129
+ fmtTokens(r.cache_write_5m + r.cache_write_1h),
130
+ fmtTokens(r.output),
131
+ costCell(r),
132
+ ]);
133
+ if (!body.length) {
134
+ out.push('No usage in this period.');
135
+ }
136
+ else {
137
+ const lines = table(header, body, align);
138
+ const width = Math.max(...lines.map((l) => l.length));
139
+ out.push(...lines);
140
+ out.push('─'.repeat(width));
141
+ const t = rep.totals;
142
+ const totalCells = [
143
+ 'TOTAL',
144
+ ...lead.slice(1).map(() => ''),
145
+ fmtTokens(t.input),
146
+ fmtTokens(t.cache_read),
147
+ fmtTokens(t.cache_write_5m + t.cache_write_1h),
148
+ fmtTokens(t.output),
149
+ fmtUsd(t.cost),
150
+ ];
151
+ // Align the TOTAL row with the table's columns.
152
+ out.push(table(header, [...body, totalCells], align).at(-1));
153
+ }
154
+ out.push('');
155
+ if (!rep.plans.length) {
156
+ out.push('Plans: none set (tokenmaxxing plan set <provider> <plan> to see ROI)');
157
+ }
158
+ else {
159
+ const planRows = rep.plans.map((p) => [
160
+ p.provider,
161
+ describeLines(p.lines),
162
+ `= ${fmtPrice(p.monthly)}/mo`,
163
+ `→ API-equivalent ${fmtUsd(p.api_equiv)}`,
164
+ p.roi === null ? '' : `→ ROI ${p.roi.toFixed(1)}×`,
165
+ ]);
166
+ if (rep.plans.length > 1) {
167
+ const monthly = Math.round(rep.plans.reduce((s, p) => s + p.monthly, 0) * 100) / 100;
168
+ const c = rep.plans.reduce((s, p) => s + p.api_equiv, 0);
169
+ planRows.push(['total', '', `= ${fmtPrice(monthly)}/mo`, `→ API-equivalent ${fmtUsd(c)}`, `→ ROI ${roi(c, monthly, rep.period.days).toFixed(1)}×`]);
170
+ }
171
+ table(['', '', '', '', ''], planRows, ['l', 'l', 'l', 'l', 'l'], 2)
172
+ .slice(1)
173
+ .forEach((l, i) => out.push((i === 0 ? 'Plans: ' : ' ') + l));
174
+ }
175
+ const up = rep.unpriced.length
176
+ ? rep.unpriced.map((u) => `${u.source}/${u.model} (${fmtTokens(u.tokens)} tokens${u.note ? ` — ${u.note}` : ''})`).join(', ')
177
+ : 'none';
178
+ out.push(`Unpriced models (0 tokens counted toward $): ${up}`);
179
+ for (const e of rep.excluded)
180
+ out.push(`${cap(e.source)}: detected — ${e.reason}`);
181
+ return out.join('\n');
182
+ }
183
+ const cap = (s) => s[0].toUpperCase() + s.slice(1);
184
+ export function reportJson(rep) {
185
+ return JSON.stringify(rep, null, 2);
186
+ }
@@ -0,0 +1,160 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { isFile } from './fsutil.js';
5
+ import { home, tmxHome } from './paths.js';
6
+ import { confirm, lineDiff } from './prompt.js';
7
+ export const LABEL = 'fyi.tokenmaxxing.sync';
8
+ const CRON_MARK = '# tokenmaxxing sync';
9
+ const INTERVAL_S = 30 * 60;
10
+ function nodeBinDir() {
11
+ return dirname(process.execPath);
12
+ }
13
+ function npxPath() {
14
+ const p = join(nodeBinDir(), 'npx');
15
+ return isFile(p) ? p : 'npx';
16
+ }
17
+ export function plistPath() {
18
+ return join(home(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
19
+ }
20
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
21
+ export function launchdPlist(npx, binDir, tmx, envHome) {
22
+ const args = [npx, '-y', 'tokenmaxxing-cli', 'sync', '--quiet'];
23
+ const env = { PATH: `${binDir}:/usr/bin:/bin:/usr/sbin:/sbin` };
24
+ if (envHome)
25
+ env.TOKENMAXXING_HOME = envHome;
26
+ return `<?xml version="1.0" encoding="UTF-8"?>
27
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28
+ <plist version="1.0">
29
+ <dict>
30
+ <key>Label</key>
31
+ <string>${LABEL}</string>
32
+ <key>ProgramArguments</key>
33
+ <array>
34
+ ${args.map((a) => ` <string>${esc(a)}</string>`).join('\n')}
35
+ </array>
36
+ <key>EnvironmentVariables</key>
37
+ <dict>
38
+ ${Object.entries(env)
39
+ .map(([k, v]) => ` <key>${esc(k)}</key>\n <string>${esc(v)}</string>`)
40
+ .join('\n')}
41
+ </dict>
42
+ <key>StartInterval</key>
43
+ <integer>${INTERVAL_S}</integer>
44
+ <key>RunAtLoad</key>
45
+ <true/>
46
+ <key>StandardOutPath</key>
47
+ <string>${esc(join(tmx, 'schedule.log'))}</string>
48
+ <key>StandardErrorPath</key>
49
+ <string>${esc(join(tmx, 'schedule.log'))}</string>
50
+ </dict>
51
+ </plist>
52
+ `;
53
+ }
54
+ export function cronLine(npx, binDir, envHome) {
55
+ const envs = [`PATH=${binDir}:/usr/bin:/bin`, ...(envHome ? [`TOKENMAXXING_HOME=${envHome}`] : [])].join(' ');
56
+ return `*/30 * * * * ${envs} ${npx} -y tokenmaxxing-cli sync --quiet >/dev/null 2>&1 ${CRON_MARK}`;
57
+ }
58
+ function readCrontab() {
59
+ const r = spawnSync('crontab', ['-l'], { encoding: 'utf8' });
60
+ return r.status === 0 ? r.stdout : '';
61
+ }
62
+ function writeCrontab(text) {
63
+ execFileSync('crontab', ['-'], { input: text });
64
+ }
65
+ export async function scheduleCommand(sub, opts, log = console.log) {
66
+ const envHome = process.env.TOKENMAXXING_HOME;
67
+ if (process.platform === 'darwin') {
68
+ const path = plistPath();
69
+ const exists = isFile(path);
70
+ if (sub === 'status' || sub === undefined) {
71
+ log(`launchd agent ${LABEL}: ${exists ? 'installed' : 'not installed'} (${path})`);
72
+ if (sub === undefined)
73
+ log('\nUsage: tokenmaxxing schedule install|uninstall [--yes]');
74
+ return 0;
75
+ }
76
+ const uid = process.getuid?.() ?? 0;
77
+ if (sub === 'install') {
78
+ const body = launchdPlist(npxPath(), nodeBinDir(), tmxHome(), envHome);
79
+ const before = exists ? readFileSync(path, 'utf8') : '';
80
+ if (before === body) {
81
+ log(`Already installed: ${path}`);
82
+ return 0;
83
+ }
84
+ log(`Will write ${path} (runs \`tokenmaxxing sync\` every 30 min) and load it with launchctl:\n`);
85
+ log(lineDiff(before, body));
86
+ if (!(opts.yes || (await confirm('Install?')))) {
87
+ log('Nothing was changed. Re-run with --yes to apply.');
88
+ return 1;
89
+ }
90
+ mkdirSync(dirname(path), { recursive: true });
91
+ mkdirSync(tmxHome(), { recursive: true });
92
+ if (exists)
93
+ spawnSync('launchctl', ['bootout', `gui/${uid}`, path], { stdio: 'ignore' });
94
+ writeFileSync(path, body);
95
+ const r = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, path], { encoding: 'utf8' });
96
+ if (r.status !== 0)
97
+ log(`launchctl bootstrap failed (${(r.stderr || '').trim()}); the agent will load at next login.`);
98
+ log(`Installed ${path}`);
99
+ return 0;
100
+ }
101
+ if (sub === 'uninstall') {
102
+ if (!exists) {
103
+ log('Not installed.');
104
+ return 0;
105
+ }
106
+ if (!(opts.yes || (await confirm(`Unload and delete ${path}?`)))) {
107
+ log('Nothing was changed. Re-run with --yes to apply.');
108
+ return 1;
109
+ }
110
+ spawnSync('launchctl', ['bootout', `gui/${uid}`, path], { stdio: 'ignore' });
111
+ rmSync(path, { force: true });
112
+ log(`Removed ${path}`);
113
+ return 0;
114
+ }
115
+ }
116
+ else if (process.platform === 'linux') {
117
+ const current = readCrontab();
118
+ const has = current.split('\n').some((l) => l.includes(CRON_MARK));
119
+ if (sub === 'status' || sub === undefined) {
120
+ log(`cron entry: ${has ? 'installed' : 'not installed'}`);
121
+ return 0;
122
+ }
123
+ let next;
124
+ if (sub === 'install') {
125
+ if (has) {
126
+ log('Already installed.');
127
+ return 0;
128
+ }
129
+ next = (current && !current.endsWith('\n') ? current + '\n' : current) + cronLine(npxPath(), nodeBinDir(), envHome) + '\n';
130
+ }
131
+ else if (sub === 'uninstall') {
132
+ if (!has) {
133
+ log('Not installed.');
134
+ return 0;
135
+ }
136
+ next = current
137
+ .split('\n')
138
+ .filter((l) => !l.includes(CRON_MARK))
139
+ .join('\n');
140
+ }
141
+ else {
142
+ log('Usage: tokenmaxxing schedule install|uninstall [--yes]');
143
+ return 1;
144
+ }
145
+ log('crontab change:\n' + lineDiff(current, next));
146
+ if (!(opts.yes || (await confirm('Apply?')))) {
147
+ log('Nothing was changed. Re-run with --yes to apply.');
148
+ return 1;
149
+ }
150
+ writeCrontab(next);
151
+ log(sub === 'install' ? 'Installed cron entry.' : 'Removed cron entry.');
152
+ return 0;
153
+ }
154
+ else {
155
+ log(`schedule is supported on macOS (launchd) and Linux (cron); on ${process.platform} use your OS scheduler to run \`npx -y tokenmaxxing-cli sync --quiet\` every 30 minutes.`);
156
+ return 1;
157
+ }
158
+ log('Usage: tokenmaxxing schedule install|uninstall [--yes]');
159
+ return 1;
160
+ }
package/dist/site.js ADDED
@@ -0,0 +1,196 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { saveConfig, siteUrl } from './config.js';
3
+ import { cleanRow, loadBuckets, sortRows } from './store.js';
4
+ export const PUSH_BATCH = 5000;
5
+ const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no I, O, 0, 1
6
+ export function linkCode() {
7
+ const bytes = randomBytes(8);
8
+ let s = '';
9
+ for (const b of bytes)
10
+ s += ALPHABET[b % ALPHABET.length];
11
+ return s;
12
+ }
13
+ function describeNetError(e, url) {
14
+ const err = e;
15
+ const code = err?.cause?.code ?? err?.code;
16
+ if (code === 'ECONNREFUSED')
17
+ return `could not connect to ${url} (connection refused). Is the site running? Use --site to point elsewhere.`;
18
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN')
19
+ return `could not resolve ${new URL(url).host}. Check your network or use --site.`;
20
+ if (err?.name === 'TimeoutError' || err?.name === 'AbortError')
21
+ return `request to ${url} timed out.`;
22
+ return `request to ${url} failed: ${err?.cause?.message ?? err?.message ?? String(e)}`;
23
+ }
24
+ export class SiteError extends Error {
25
+ }
26
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
27
+ /** Print the sign-in URL, poll the site until it hands back { token, handle }, store both. */
28
+ export async function link(cfg, opts = {}) {
29
+ const log = opts.log ?? console.log;
30
+ const site = siteUrl(cfg, opts.site);
31
+ const interval = opts.intervalMs ?? Number(process.env.TOKENMAXXING_POLL_MS ?? 3000);
32
+ const timeout = opts.timeoutMs ?? 5 * 60 * 1000;
33
+ const code = linkCode();
34
+ log(`Open ${site}/link?code=${code} and sign in`);
35
+ log(`Waiting for the site to confirm (polling ${site}/api/v1/link/${code} every ${Math.round(interval / 1000)}s, up to ${Math.round(timeout / 60000)} min)…`);
36
+ const deadline = Date.now() + timeout;
37
+ const url = `${site}/api/v1/link/${code}`;
38
+ while (Date.now() < deadline) {
39
+ let res;
40
+ try {
41
+ res = await fetch(url, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(15000) });
42
+ }
43
+ catch (e) {
44
+ throw new SiteError(describeNetError(e, url));
45
+ }
46
+ if (res.status === 404) {
47
+ throw new SiteError(`${site} has no link endpoint yet (GET /api/v1/link/{code} returned 404). The site may not be live; nothing was stored.`);
48
+ }
49
+ if (res.ok && res.status !== 202 && res.status !== 204) {
50
+ let body = null;
51
+ try {
52
+ body = await res.json();
53
+ }
54
+ catch {
55
+ /* not json yet */
56
+ }
57
+ if (body && typeof body.token === 'string' && body.token) {
58
+ cfg.site = { ...cfg.site, url: site, token: body.token, handle: typeof body.handle === 'string' ? body.handle : undefined };
59
+ saveConfig(cfg);
60
+ log(`Linked${cfg.site.handle ? ` as @${cfg.site.handle}` : ''}. Run \`tokenmaxxing push\` to publish your buckets.`);
61
+ return { handle: cfg.site.handle };
62
+ }
63
+ }
64
+ else if (!res.ok) {
65
+ throw new SiteError(`${url} returned HTTP ${res.status}.`);
66
+ }
67
+ await sleep(interval);
68
+ }
69
+ throw new SiteError('Timed out waiting for sign-in (5 min). Run `tokenmaxxing link` again.');
70
+ }
71
+ /** Start of the period containing an hourly ts: the hour itself, 00:00 UTC, or Monday 00:00 UTC. */
72
+ export function periodStart(ts, g) {
73
+ if (g === 'hour')
74
+ return ts;
75
+ const d = new Date(ts);
76
+ d.setUTCHours(0, 0, 0, 0);
77
+ if (g === 'week')
78
+ d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7));
79
+ return d.toISOString().replace('.000Z', 'Z');
80
+ }
81
+ /** Sum hourly rows into one row per (period, source, model). Hour is the identity. */
82
+ export function foldRows(rows, g) {
83
+ if (g === 'hour')
84
+ return rows;
85
+ const out = new Map();
86
+ for (const r of rows) {
87
+ const ts = periodStart(r.ts, g);
88
+ const k = `${ts}|${r.source}|${r.model}`;
89
+ const cur = out.get(k);
90
+ if (!cur) {
91
+ out.set(k, { ...r, ts });
92
+ continue;
93
+ }
94
+ for (const f of ['input', 'cache_read', 'cache_write_5m', 'cache_write_1h', 'output', 'reasoning', 'requests', 'conversations']) {
95
+ cur[f] += r[f];
96
+ }
97
+ }
98
+ return sortRows([...out.values()]);
99
+ }
100
+ /**
101
+ * Rows to send: the current (last-per-key) row for every hour at or after lastPushedTs, folded to
102
+ * the chosen granularity. The whole period containing lastPushedTs is resent so a partial day or
103
+ * week is replaced by its complete total on the next push.
104
+ */
105
+ export function planPush(all, lastPushedTs, granularity = 'hour') {
106
+ const floor = lastPushedTs ? periodStart(lastPushedTs, granularity) : undefined;
107
+ const hourly = sortRows([...all].filter((r) => r.source !== 'cursor' && (!floor || r.ts >= floor))).map(cleanRow);
108
+ const rows = foldRows(hourly, granularity);
109
+ const batches = [];
110
+ for (let i = 0; i < rows.length; i += PUSH_BATCH)
111
+ batches.push(rows.slice(i, i + PUSH_BATCH));
112
+ return { rows, batches, from: rows[0]?.ts, to: rows.at(-1)?.ts, maxHourlyTs: hourly.at(-1)?.ts, granularity };
113
+ }
114
+ export async function push(cfg, opts = {}) {
115
+ const log = opts.log ?? console.log;
116
+ const site = siteUrl(cfg, opts.site);
117
+ if (!cfg.site.token && !opts.dryRun) {
118
+ throw new SiteError('Not linked. Run `tokenmaxxing link` first.');
119
+ }
120
+ const { rows } = await loadBuckets();
121
+ const granularity = opts.granularity ?? cfg.site.granularity ?? 'hour';
122
+ if (opts.granularity && opts.granularity !== cfg.site.granularity) {
123
+ cfg.site.granularity = opts.granularity;
124
+ saveConfig(cfg);
125
+ }
126
+ // Changing granularity (or --all) replaces everything the site holds for this device.
127
+ const replace = opts.all || (cfg.site.lastPushedGranularity ?? 'hour') !== granularity;
128
+ const plan = planPush(rows.values(), replace ? undefined : cfg.site.lastPushedTs, granularity);
129
+ let earliestRejected;
130
+ if (!plan.rows.length) {
131
+ log('Nothing to push.');
132
+ return 0;
133
+ }
134
+ const url = `${site}/api/v1/push`;
135
+ log(`${opts.dryRun ? 'Would send' : 'Sending'} ${plan.rows.length} bucket row${plan.rows.length === 1 ? '' : 's'} (${plan.from} → ${plan.to}) in ${plan.batches.length} request${plan.batches.length === 1 ? '' : 's'} to POST ${url}`);
136
+ log(`Granularity: ${granularity}${granularity === 'hour' ? '' : ' (rows are ' + granularity + 'ly totals; the site never sees your hours)'}${replace ? ' — replacing every row the site holds for this device' : ''}.`);
137
+ log(`Each request body is exactly: { "v": 1, "deviceId": "${cfg.deviceId}", "granularity": "${granularity}"${replace ? ', "replaceDevice": true (first request only)' : ''}, "rows": [ …up to ${PUSH_BATCH} rows… ] }`);
138
+ log(`Row fields: v, ts, source, model, input, cache_read, cache_write_5m, cache_write_1h, output, reasoning, requests, conversations. No paths, projects or prompts.`);
139
+ log(`First row: ${JSON.stringify(plan.rows[0])}`);
140
+ if (opts.dryRun) {
141
+ plan.batches.forEach((b, i) => log(JSON.stringify({ v: 1, deviceId: cfg.deviceId, granularity, ...(replace && i === 0 ? { replaceDevice: true } : {}), rows: b })));
142
+ return 0;
143
+ }
144
+ log('(run `tokenmaxxing push --dry-run` to print every row without sending)');
145
+ let sent = 0;
146
+ for (const [bi, batch] of plan.batches.entries()) {
147
+ let res;
148
+ try {
149
+ res = await fetch(url, {
150
+ method: 'POST',
151
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.site.token}` },
152
+ body: JSON.stringify({ v: 1, deviceId: cfg.deviceId, granularity, ...(replace && bi === 0 ? { replaceDevice: true } : {}), rows: batch }),
153
+ signal: AbortSignal.timeout(60000),
154
+ });
155
+ }
156
+ catch (e) {
157
+ throw new SiteError(describeNetError(e, url));
158
+ }
159
+ if (res.status === 404)
160
+ throw new SiteError(`${site} has no push endpoint yet (POST /api/v1/push returned 404). The site may not be live.`);
161
+ if (res.status === 401 || res.status === 403)
162
+ throw new SiteError(`${site} rejected the token (HTTP ${res.status}). Run \`tokenmaxxing link\` again.`);
163
+ if (!res.ok)
164
+ throw new SiteError(`${url} returned HTTP ${res.status}.`);
165
+ let body = null;
166
+ try {
167
+ body = await res.json();
168
+ }
169
+ catch {
170
+ /* no body */
171
+ }
172
+ const rejected = Array.isArray(body?.rejected) ? body.rejected : [];
173
+ if (rejected.length) {
174
+ log(`Site rejected ${rejected.length} of ${batch.length} rows in this batch (kept the rest):`);
175
+ for (const r of rejected.slice(0, 5))
176
+ log(` ${JSON.stringify(r)}`);
177
+ if (rejected.length > 5)
178
+ log(` …and ${rejected.length - 5} more`);
179
+ for (const r of rejected) {
180
+ const row = typeof r.index === 'number' ? batch[r.index] : undefined;
181
+ if (row && (!earliestRejected || row.ts < earliestRejected))
182
+ earliestRejected = row.ts;
183
+ }
184
+ }
185
+ sent += batch.length - rejected.length;
186
+ // Never advance the high-water mark past a rejected period, so the next push retries it.
187
+ const mark = bi === plan.batches.length - 1 ? (plan.maxHourlyTs ?? batch.at(-1).ts) : batch.at(-1).ts;
188
+ cfg.site.lastPushedTs = earliestRejected && earliestRejected < mark ? earliestRejected : mark;
189
+ cfg.site.lastPushedGranularity = granularity;
190
+ saveConfig(cfg);
191
+ }
192
+ if (earliestRejected)
193
+ log(`Some rows were rejected; the next push retries from ${earliestRejected}. Run \`tokenmaxxing push --all\` to resend everything.`);
194
+ log(`Pushed ${sent} rows.`);
195
+ return sent;
196
+ }