tokenmaxxing 1.15.0 → 1.16.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "tokenmaxxing",
4
- "version": "1.15.0",
4
+ "version": "1.16.0",
5
5
  "description": "Pool Claude Code and Codex logins, switch on pace pressure, and expose safe ops tools to agent clients.",
6
6
  "author": {
7
7
  "name": "anaclumos",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/status.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { sampleSize, sortBy } from "es-toolkit";
1
+ import { chunk, sampleSize, sortBy } from "es-toolkit";
2
2
  import { z } from "zod";
3
3
  import { loadAccounts, loadConfig, loadUsage, loadUsageSnapshot, loadModelUsage, saveAccounts } from "../lib/state.ts";
4
4
  import { readOAuthAccount } from "../lib/claudejson.ts";
@@ -10,7 +10,7 @@ import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
10
10
  import { liveCodexAccountId, sampleCodexAccount, type CodexSampleOutcome } from "../lib/codexsample.ts";
11
11
  import { isCodexExhausted } from "../lib/codexpick.ts";
12
12
  import { codexLimitLabel, isSessionWindow } from "../lib/codexusage.ts";
13
- import { bar, c, claudeTierLabel, count, emitJson, fmtAgo, fmtReset } from "./render.ts";
13
+ import { bar, c, claudeTierLabel, count, emitJson, fmtAgo } from "./render.ts";
14
14
  import { gatedFamilies, type FullUsage } from "../lib/usage.ts";
15
15
  import { ThresholdsSchema, UsageWindowSchema, type Account, type CodexWindow, type Config, type UsageWindow } from "../lib/types.ts";
16
16
 
@@ -268,48 +268,138 @@ async function collectCodex(input: { cfg: Config; now: number }): Promise<Status
268
268
  return { bars, accounts };
269
269
  }
270
270
 
271
- function renderCodex(input: { codex: StatusReport["codex"]; now: number; row: (name: string, w: UsageWindow) => void }): void {
272
- const { codex, now, row } = input;
273
- if (codex.accounts.length === 0) return;
274
- console.log(c.dim(`codex (${count({ n: codex.accounts.length, noun: "account" })})`));
275
- console.log();
276
- const windowLabel = (window: CodexWindow) =>
277
- isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
278
- for (const account of codex.accounts) {
279
- const marker = account.active ? c.green("●") : c.dim("○");
280
- const badges: string[] = [];
281
- if (account.active) badges.push(c.green("active"));
282
- if (account.needsReauth) badges.push(c.red("needs-reauth"));
283
- if (account.exhausted) badges.push(c.yellow("exhausted"));
284
- console.log(`${marker} ${c.bold(account.label)}${account.planType ? ` ${c.dim(account.planType)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
285
- if (account.usage) {
286
- for (const window of account.usage.aggregate) row(windowLabel(window), window);
287
- for (const [name, windows] of Object.entries(account.usage.perLimit)) {
288
- for (const window of windows) row(codexLimitLabel({ limitName: name }), window);
289
- }
271
+ const CARD_GAP = 3;
272
+ const NOTE_INDENT = " ";
273
+
274
+ type Note = { paint: (s: string) => string; text: string };
275
+ type Card = { lines: string[]; notes: Note[] };
276
+
277
+ function splitToWidth(token: string, width: number): string[] {
278
+ const parts: string[] = [];
279
+ let part = "";
280
+ for (const ch of token) {
281
+ if (part !== "" && Bun.stringWidth(part + ch) > width) {
282
+ parts.push(part);
283
+ part = "";
290
284
  }
291
- if (!account.sample.ok) {
292
- const cached = account.usage ? `cached${account.usageAt != null ? ` ${fmtAgo(account.usageAt, now)}` : ""}, ` : "";
293
- console.log(` ${c.yellow(`${cached}live sample failed`)}: ${c.dim(account.sample.reason)}`);
285
+ part += ch;
286
+ }
287
+ if (part !== "") parts.push(part);
288
+ return parts;
289
+ }
290
+
291
+ function wrapWords(text: string, width: number): string[] {
292
+ const out: string[] = [];
293
+ let line = "";
294
+ for (const word of text.split(" ").flatMap((token) => splitToWidth(token, width))) {
295
+ if (line !== "" && Bun.stringWidth(`${line} ${word}`) > width) {
296
+ out.push(line);
297
+ line = word;
298
+ } else {
299
+ line = line === "" ? word : `${line} ${word}`;
300
+ }
301
+ }
302
+ if (line !== "") out.push(line);
303
+ return out;
304
+ }
305
+
306
+ function renderGrid(cards: Card[]): void {
307
+ const bodyWidth = Math.max(...cards.flatMap((card) => card.lines.map((line) => Bun.stringWidth(line))));
308
+ const termWidth = process.stdout.isTTY ? process.stdout.columns : Number.POSITIVE_INFINITY;
309
+ const columns = process.stdout.isTTY ? Math.min(cards.length, Math.max(1, Math.floor((termWidth + CARD_GAP) / (bodyWidth + CARD_GAP)))) : 1;
310
+ const cellWidth = columns === 1 ? termWidth : Math.floor((termWidth + CARD_GAP) / columns) - CARD_GAP;
311
+ const blocks = cards.map((card) => [
312
+ ...card.lines,
313
+ ...card.notes.flatMap((note) => wrapWords(note.text, cellWidth - NOTE_INDENT.length).map((line) => `${NOTE_INDENT}${note.paint(line)}`)),
314
+ ]);
315
+ const width = Math.max(...blocks.flat().map((line) => Bun.stringWidth(line)));
316
+ for (const rowBlocks of chunk(blocks, columns)) {
317
+ const height = Math.max(...rowBlocks.map((block) => block.length));
318
+ for (let i = 0; i < height; i++) {
319
+ const cells = rowBlocks.map((block) => block[i] ?? "");
320
+ const padded = cells.map((cell, col) => (col === cells.length - 1 ? cell : cell + " ".repeat(Math.max(0, width + CARD_GAP - Bun.stringWidth(cell)))));
321
+ console.log(padded.join("").trimEnd());
294
322
  }
295
323
  console.log();
296
324
  }
297
325
  }
298
326
 
299
- function rowPrinter(now: number): (name: string, w: UsageWindow) => void {
300
- return (name, w) => {
301
- console.log(` ${name.padEnd(5)} ${bar(w.usedPercentage)} ${c.dim(fmtReset(w.resetsAt, now))}`);
302
- };
327
+ function usageRow(name: string, w: UsageWindow): string {
328
+ return `${NOTE_INDENT}${name.padEnd(5)} ${bar(w.usedPercentage)}`;
329
+ }
330
+
331
+ function sampleFailedNotes(input: { cached: boolean; usageAt: number | null; reason: string; now: number }): Note[] {
332
+ const cached = input.cached ? `cached${input.usageAt != null ? ` ${fmtAgo(input.usageAt, input.now)}` : ""}, ` : "";
333
+ return [
334
+ { paint: c.yellow, text: `${cached}live sample failed:` },
335
+ { paint: c.dim, text: input.reason },
336
+ ];
337
+ }
338
+
339
+ function headerLine(input: { active: boolean; needsReauth: boolean; exhausted: boolean; name: string; tier: string | null }): string {
340
+ const marker = input.active ? c.green("●") : c.dim("○");
341
+ const badges: string[] = [];
342
+ if (input.active) badges.push(c.green("active"));
343
+ if (input.needsReauth) badges.push(c.red("needs-reauth"));
344
+ if (input.exhausted) badges.push(c.yellow("exhausted"));
345
+ return `${marker} ${c.bold(input.name)}${input.tier ? ` ${c.dim(input.tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`;
346
+ }
347
+
348
+ function codexCard(account: CodexStatusAccount, now: number): Card {
349
+ const windowLabel = (window: CodexWindow) =>
350
+ isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
351
+ const lines = [headerLine({ ...account, name: account.label, tier: account.planType })];
352
+ if (account.usage) {
353
+ for (const window of account.usage.aggregate) lines.push(usageRow(windowLabel(window), window));
354
+ for (const [name, windows] of Object.entries(account.usage.perLimit)) {
355
+ for (const window of windows) lines.push(usageRow(codexLimitLabel({ limitName: name }), window));
356
+ }
357
+ }
358
+ const notes = account.sample.ok ? [] : sampleFailedNotes({ cached: account.usage != null, usageAt: account.usageAt, reason: account.sample.reason, now });
359
+ return { lines, notes };
360
+ }
361
+
362
+ function renderCodex(input: { codex: StatusReport["codex"]; now: number }): void {
363
+ const { codex, now } = input;
364
+ if (codex.accounts.length === 0) return;
365
+ console.log(c.dim(`codex (${count({ n: codex.accounts.length, noun: "account" })})`));
366
+ console.log();
367
+ renderGrid(codex.accounts.map((account) => codexCard(account, now)));
303
368
  }
304
369
 
305
- function renderClaude(input: {
306
- claude: StatusReport["claude"];
307
- codexPooled: boolean;
308
- now: number;
309
- staleAfterMs: number;
310
- row: (name: string, w: UsageWindow) => void;
311
- }): void {
312
- const { claude, codexPooled, now, staleAfterMs, row } = input;
370
+ function claudeCard(a: ClaudeStatusAccount, now: number, staleAfterMs: number): Card {
371
+ const lines = [headerLine({ ...a, name: a.label || a.email })];
372
+ if (a.usage) {
373
+ lines.push(usageRow("5h", a.usage.fiveHour));
374
+ lines.push(usageRow("week", a.usage.week));
375
+ }
376
+ for (const [name, w] of Object.entries(a.perModel)) lines.push(usageRow(name.toLowerCase(), w));
377
+ const notes: Note[] = [];
378
+ if (a.sample.ok && a.sample.source === "statusline") {
379
+ const stale = a.usageAt == null || now - a.usageAt > staleAfterMs;
380
+ const age = a.usageAt != null ? fmtAgo(a.usageAt, now) : "age unknown";
381
+ notes.push({ paint: stale ? c.yellow : c.dim, text: `statusline tee ${age}${stale ? " (stale)" : ""}` });
382
+ }
383
+ if (!a.sample.ok) {
384
+ notes.push(...sampleFailedNotes({ cached: a.usage != null || Object.keys(a.perModel).length > 0, usageAt: a.usageAt, reason: a.sample.reason, now }));
385
+ }
386
+ if (a.pingError != null && a.pingRejected) {
387
+ const at = a.pingError.indexOf(": ");
388
+ const head = at >= 0 ? a.pingError.slice(0, at) : a.pingError;
389
+ const tail = at >= 0 ? a.pingError.slice(at + 2) : "";
390
+ notes.push({ paint: c.yellow, text: `ping ${head}${tail ? ":" : ""}` });
391
+ if (tail) notes.push({ paint: c.dim, text: tail });
392
+ } else if (a.pingError != null) {
393
+ notes.push({ paint: c.yellow, text: "ping failed (5h timer may not have started):" }, { paint: c.dim, text: a.pingError });
394
+ }
395
+ if (a.pinged) {
396
+ notes.push({ paint: c.dim, text: "pinged - 5h timer started this run; the usage feed lags, re-run status shortly for the fresh window" });
397
+ }
398
+ return { lines, notes };
399
+ }
400
+
401
+ function renderClaude(input: { claude: StatusReport["claude"]; codexPooled: boolean; now: number; staleAfterMs: number }): void {
402
+ const { claude, codexPooled, now, staleAfterMs } = input;
313
403
  if (claude.accounts.length === 0) {
314
404
  if (!codexPooled) {
315
405
  console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`)"));
@@ -322,40 +412,7 @@ function renderClaude(input: {
322
412
 
323
413
  console.log(c.dim(`thresholds 5h ${claude.thresholds.session.join("/")}% (at ${claude.bars.session + claude.projectionMargin}%) weekly ${claude.thresholds.weekly}% (${count({ n: claude.accounts.length, noun: "claude account" })})`));
324
414
  console.log();
325
- for (const a of claude.accounts) {
326
- const marker = a.active ? c.green("●") : c.dim("○");
327
- const badges: string[] = [];
328
- if (a.active) badges.push(c.green("active"));
329
- if (a.needsReauth) badges.push(c.red("needs-reauth"));
330
- if (a.exhausted) badges.push(c.yellow("exhausted"));
331
- console.log(`${marker} ${c.bold(a.label || a.email)}${a.tier ? ` ${c.dim(a.tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
332
- if (a.usage) {
333
- row("5h", a.usage.fiveHour);
334
- row("week", a.usage.week);
335
- }
336
- for (const [name, w] of Object.entries(a.perModel)) row(name.toLowerCase(), w);
337
- if (a.sample.ok && a.sample.source === "statusline") {
338
- const stale = a.usageAt == null || now - a.usageAt > staleAfterMs;
339
- const age = a.usageAt != null ? fmtAgo(a.usageAt, now) : "age unknown";
340
- console.log(` ${(stale ? c.yellow : c.dim)(`statusline tee ${age}${stale ? " (stale)" : ""}`)}`);
341
- }
342
- if (!a.sample.ok) {
343
- const cached = a.usage || Object.keys(a.perModel).length > 0 ? `cached${a.usageAt != null ? ` ${fmtAgo(a.usageAt, now)}` : ""}, ` : "";
344
- console.log(` ${c.yellow(`${cached}live sample failed`)}: ${c.dim(a.sample.reason)}`);
345
- }
346
- if (a.pingError != null && a.pingRejected) {
347
- const at = a.pingError.indexOf(": ");
348
- const head = at >= 0 ? a.pingError.slice(0, at) : a.pingError;
349
- const tail = at >= 0 ? a.pingError.slice(at + 2) : "";
350
- console.log(` ${c.yellow(`ping ${head}`)}${tail ? `: ${c.dim(tail)}` : ""}`);
351
- } else if (a.pingError != null) {
352
- console.log(` ${c.yellow("ping failed (5h timer may not have started)")}: ${c.dim(a.pingError)}`);
353
- }
354
- if (a.pinged) {
355
- console.log(` ${c.dim("pinged - 5h timer started this run; the usage feed lags, re-run status shortly for the fresh window")}`);
356
- }
357
- console.log();
358
- }
415
+ renderGrid(claude.accounts.map((a) => claudeCard(a, now, staleAfterMs)));
359
416
  }
360
417
 
361
418
  export async function cmdStatus(opts: { ping?: boolean; pingCount?: number; json?: boolean; preRender?: () => void } = {}): Promise<number> {
@@ -366,7 +423,7 @@ export async function cmdStatus(opts: { ping?: boolean; pingCount?: number; json
366
423
  if (!json) {
367
424
  opts.preRender?.();
368
425
  const at = Date.now();
369
- renderClaude({ claude, codexPooled: loadCodexAccounts().accounts.length > 0, now: at, staleAfterMs: cfg.policy.usagePollTtlMs, row: rowPrinter(at) });
426
+ renderClaude({ claude, codexPooled: loadCodexAccounts().accounts.length > 0, now: at, staleAfterMs: cfg.policy.usagePollTtlMs });
370
427
  }
371
428
  const codex = await collectCodex({ cfg, now });
372
429
  if (json) {
@@ -374,7 +431,6 @@ export async function cmdStatus(opts: { ping?: boolean; pingCount?: number; json
374
431
  emitJson({ ok: true, ...report });
375
432
  return 0;
376
433
  }
377
- const at = Date.now();
378
- renderCodex({ codex, now: at, row: rowPrinter(at) });
434
+ renderCodex({ codex, now: Date.now() });
379
435
  return 0;
380
436
  }