tokenmaxxing 1.14.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.
package/README.md CHANGED
@@ -70,7 +70,7 @@ claude # use claude as always
70
70
  | `tokenmaxxing switch --codex [sel]` | switch the codex pool (takes effect on the next codex start) |
71
71
  | `tokenmaxxing ls` | list pooled accounts |
72
72
  | `tokenmaxxing status` | accounts with 5h / weekly usage bars, active + exhausted-until-reset |
73
- | `tokenmaxxing status --force` | additionally ping every account (one tiny haiku request each) so all 5h session timers start now, then sample fresh |
73
+ | `tokenmaxxing status --ping [--count N]` | additionally ping every account (one tiny haiku request each) so all 5h session timers start now, then sample fresh; `--count N` pings N randomly picked accounts instead, to stagger the resets |
74
74
  | `tokenmaxxing watch [seconds]` | live status: re-render every N seconds (default 120, floor 30; never pings) |
75
75
  | `tokenmaxxing config` | effective config with sources; `get`/`set`/`unset` dotted keys, `tidy` prunes unknown keys |
76
76
  | `tokenmaxxing doctor` | verify the supervisor + settings entries survived |
@@ -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.14.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",
@@ -10,18 +10,18 @@ description: Read the Claude and Codex account pool safely (list, usage bars, wa
10
10
  - `pool_ls` for labels, active marker, needs-reauth
11
11
  - `pool_status` for 5h / weekly / per-model bars (free `/usage` path)
12
12
 
13
- If MCP is unavailable, run `tokenmaxxing ls --json` or `tokenmaxxing status --json` (alias `xx`) and parse the document. Never add `--force`.
13
+ If MCP is unavailable, run `tokenmaxxing ls --json` or `tokenmaxxing status --json` (alias `xx`) and parse the document. Never add `--ping`.
14
14
 
15
15
  ## Hard stops
16
16
 
17
- - Do not run `status --force` or bare `xx --force`. That pings every account with a real request and opens each 5h window.
17
+ - Do not run `status --ping` or bare `xx --ping`. That pings every account with a real request and opens each 5h window.
18
18
  - Do not print credential files, keychain blobs, or OAuth tokens. Labels and status only.
19
19
  - Do not kill sessions or supervisors to "free" an account.
20
20
 
21
21
  ## Notes
22
22
 
23
23
  - Active Claude usage often comes from the statusLine tee; parked accounts are probed in isolation.
24
- - `watch` re-renders status on an interval and never force-pings.
24
+ - `watch` re-renders status on an interval and never pings.
25
25
  - Hermetic agents: set `TOKENMAXXING_HOME` to a throwaway directory. That isolates state files only: `init`, `uninstall`, and the codex hook install still write settings.json, codex `hooks.json`, the shell rc, and the timer unit under `HOME`, so never run them from an agent.
26
26
 
27
27
  See [references/commands.md](references/commands.md).
@@ -1,9 +1,9 @@
1
1
  # Status and list commands
2
2
 
3
3
  - `tokenmaxxing` / `tokenmaxxing status`: sample and show bars; persists cached usage for the picker.
4
- - `tokenmaxxing status --force`: DENIED for agents without explicit user approval. Meters every account.
4
+ - `tokenmaxxing status --ping [--count N]`: DENIED for agents without explicit user approval. Meters every account, or N random ones under `--count`.
5
5
  - `tokenmaxxing ls`: compact list.
6
- - `tokenmaxxing watch [seconds]`: live re-render (default 120), never `--force`.
6
+ - `tokenmaxxing watch [seconds]`: live re-render (default 120), never `--ping`.
7
7
  - `--json` on any of these: one JSON document on stdout (`ok` mirrors the exit code); parse it instead of scraping the bars. `watch --json` prints one document per tick and never exits on its own.
8
8
 
9
9
  Docs: `docs/content/docs/commands.mdx`.
@@ -9,11 +9,11 @@ description: Safely change and ship tokenmaxxing (verification without a test su
9
9
 
10
10
  - The repo carries no test code and no comments (owner ruling 2026-09-02). Never add tests, a test script, or comments.
11
11
  - Verify with `bun run typecheck`, then hermetic CLI runs: `TOKENMAXXING_HOME=/tmp/xx-test bun run src/main.ts ...` (for decision-path changes, write accounts.json, usage.json, config.json, and a claude.json under that root and run `check`; point `TOKENMAXXING_KEYCHAIN_SERVICE` and `TOKENMAXXING_KEYCHAIN_ACCOUNT` at throwaway names and `TOKENMAXXING_CLAUDE_JSON` at that claude.json, because the CLI resolves claude.json from HOME, not from `TOKENMAXXING_HOME`).
12
- - Ask before any run that meters real quota (`status --force`, live-pool inference). Free `/usage` / plain status is fine.
12
+ - Ask before any run that meters real quota (`status --ping`, live-pool inference). Free `/usage` / plain status is fine.
13
13
 
14
14
  ## Ship
15
15
 
16
- - Work on a branch; bump `package.json` in the same PR; open PR; wait the full review window; handle every review; merge; `gh release create v<version>`; verify npm publish.
16
+ - Work on a branch; bump `package.json` and `agent-plugin/plugin.json` to the same version in the same PR; open PR; wait the full review window; handle every review; merge; `gh release create v<version>`; verify npm publish.
17
17
  - Never push directly to main. npm trusted publishing is bound to workflow filename `ci.yml`.
18
18
 
19
19
  ## Machine gotchas
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.14.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/init.ts CHANGED
@@ -37,7 +37,7 @@ export function printUsage(): void {
37
37
  console.log(` ${c.bold("how to use")} - ${c.cyan("xx")} is shorthand for ${c.cyan("tokenmaxxing")}:`);
38
38
  console.log(` ${c.cyan("claude")} use claude as always; it switches accounts near quota automatically`);
39
39
  console.log(` ${c.cyan("xx")} show the pool with usage bars (same as ${c.cyan("xx status")})`);
40
- console.log(` ${c.cyan("xx status --force")} ping every account (one tiny haiku request each) so all 5h timers start now, then sample fresh`);
40
+ console.log(` ${c.cyan("xx status --ping")} ping every account (one tiny haiku request each) so all 5h timers start now, then sample fresh; ${c.cyan("--count N")} pings N random accounts instead`);
41
41
  console.log(` ${c.cyan("xx add")} log in and pool another account`);
42
42
  console.log(` ${c.cyan("xx switch")} hop to the best account right now (the automatic switching needs no command)`);
43
43
  console.log(` ${c.cyan("xx help")} everything else`);
package/src/cli/status.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { 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
 
@@ -86,14 +86,31 @@ function currentCodexWindow(w: CodexWindow, now: number): CodexWindow {
86
86
  return { ...currentWindow(w, !isSessionWindow({ window: w }), now), windowSeconds: w.windowSeconds };
87
87
  }
88
88
 
89
- async function collectClaude(input: { cfg: Config; force: boolean; now: number }): Promise<StatusReport["claude"]> {
90
- const { cfg, force, now } = input;
89
+ function pickForPing(accounts: Account[], pingCount: number | undefined): Account[] {
90
+ if (pingCount == null) return accounts;
91
+ const usable = accounts.filter((a) => a.needsReauth !== true);
92
+ return sampleSize(usable, Math.min(pingCount, usable.length));
93
+ }
94
+
95
+ function progressNote(input: { ping: boolean; pingCount: number | undefined; picked: Account[]; total: number }): string {
96
+ const { ping, pingCount, picked, total } = input;
97
+ if (!ping) return "sampling live usage...";
98
+ if (pingCount == null) return "pinging every account (starts each 5h session timer) + sampling live usage...";
99
+ if (picked.length === 0) return "no account to ping (every account needs reauth), sampling live usage...";
100
+ return `pinging ${picked.length} of ${count({ n: total, noun: "account" })} (${picked.map((a) => a.label || a.email).join(", ")}) so their 5h session timers start now + sampling live usage...`;
101
+ }
102
+
103
+ async function collectClaude(input: { cfg: Config; ping: boolean; pingCount: number | undefined; now: number }): Promise<StatusReport["claude"]> {
104
+ const { cfg, ping, pingCount, now } = input;
91
105
  let idx = loadAccounts();
92
106
  const samples = new Map<string, { outcome: SampleOutcome; viaTee: boolean }>();
107
+ let pings = new Set<string>();
93
108
  if (idx.accounts.length > 0) {
94
- console.error(c.dim(force ? "pinging every account (starts each 5h session timer) + sampling live usage..." : "sampling live usage..."));
95
109
  await withLock(paths.lockFile, async () => {
96
110
  idx = loadAccounts();
111
+ const picked = ping ? pickForPing(idx.accounts, pingCount) : [];
112
+ pings = new Set(picked.map((a) => a.accountUuid));
113
+ console.error(c.dim(progressNote({ ping, pingCount, picked, total: idx.accounts.length })));
97
114
  const tee = loadUsageSnapshot();
98
115
  const live = tee?.state ?? null;
99
116
  const teeAt = tee?.at ?? null;
@@ -114,7 +131,7 @@ async function collectClaude(input: { cfg: Config; force: boolean; now: number }
114
131
  : null;
115
132
  let viaTee = false;
116
133
  let outcome: SampleOutcome;
117
- if (force) {
134
+ if (pings.has(a.accountUuid)) {
118
135
  outcome = isActive ? await probeActiveUsage(a, { ping: true }) : await probeParkedUsage(a, { ping: true });
119
136
  if (!outcome.ok && fromStatusLine && outcome.reason.includes("no limit data")) {
120
137
  const failed = outcome;
@@ -177,7 +194,7 @@ async function collectClaude(input: { cfg: Config; force: boolean; now: number }
177
194
  sample: sampled.outcome.ok ? { ok: true, source: sampled.viaTee ? "statusline" : "probe" } : { ok: false, reason: sampled.outcome.reason },
178
195
  pingError: sampled.outcome.pingError ?? null,
179
196
  pingRejected: sampled.outcome.pingRejected === true,
180
- pinged: force && sampled.outcome.ok && sampled.outcome.pingError == null && aggregate != null && aggregate.fiveHour.resetsAt == null,
197
+ pinged: pings.has(a.accountUuid) && sampled.outcome.ok && sampled.outcome.pingError == null && aggregate != null && aggregate.fiveHour.resetsAt == null,
181
198
  };
182
199
  });
183
200
  return {
@@ -251,48 +268,138 @@ async function collectCodex(input: { cfg: Config; now: number }): Promise<Status
251
268
  return { bars, accounts };
252
269
  }
253
270
 
254
- function renderCodex(input: { codex: StatusReport["codex"]; now: number; row: (name: string, w: UsageWindow) => void }): void {
255
- const { codex, now, row } = input;
256
- if (codex.accounts.length === 0) return;
257
- console.log(c.dim(`codex (${count({ n: codex.accounts.length, noun: "account" })})`));
258
- console.log();
259
- const windowLabel = (window: CodexWindow) =>
260
- isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
261
- for (const account of codex.accounts) {
262
- const marker = account.active ? c.green("●") : c.dim("○");
263
- const badges: string[] = [];
264
- if (account.active) badges.push(c.green("active"));
265
- if (account.needsReauth) badges.push(c.red("needs-reauth"));
266
- if (account.exhausted) badges.push(c.yellow("exhausted"));
267
- console.log(`${marker} ${c.bold(account.label)}${account.planType ? ` ${c.dim(account.planType)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
268
- if (account.usage) {
269
- for (const window of account.usage.aggregate) row(windowLabel(window), window);
270
- for (const [name, windows] of Object.entries(account.usage.perLimit)) {
271
- for (const window of windows) row(codexLimitLabel({ limitName: name }), window);
272
- }
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 = "";
273
284
  }
274
- if (!account.sample.ok) {
275
- const cached = account.usage ? `cached${account.usageAt != null ? ` ${fmtAgo(account.usageAt, now)}` : ""}, ` : "";
276
- 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());
277
322
  }
278
323
  console.log();
279
324
  }
280
325
  }
281
326
 
282
- function rowPrinter(now: number): (name: string, w: UsageWindow) => void {
283
- return (name, w) => {
284
- console.log(` ${name.padEnd(5)} ${bar(w.usedPercentage)} ${c.dim(fmtReset(w.resetsAt, now))}`);
285
- };
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(" ")}` : ""}`;
286
346
  }
287
347
 
288
- function renderClaude(input: {
289
- claude: StatusReport["claude"];
290
- codexPooled: boolean;
291
- now: number;
292
- staleAfterMs: number;
293
- row: (name: string, w: UsageWindow) => void;
294
- }): void {
295
- const { claude, codexPooled, now, staleAfterMs, row } = input;
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)));
368
+ }
369
+
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;
296
403
  if (claude.accounts.length === 0) {
297
404
  if (!codexPooled) {
298
405
  console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`)"));
@@ -305,51 +412,18 @@ function renderClaude(input: {
305
412
 
306
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" })})`));
307
414
  console.log();
308
- for (const a of claude.accounts) {
309
- const marker = a.active ? c.green("●") : c.dim("○");
310
- const badges: string[] = [];
311
- if (a.active) badges.push(c.green("active"));
312
- if (a.needsReauth) badges.push(c.red("needs-reauth"));
313
- if (a.exhausted) badges.push(c.yellow("exhausted"));
314
- console.log(`${marker} ${c.bold(a.label || a.email)}${a.tier ? ` ${c.dim(a.tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
315
- if (a.usage) {
316
- row("5h", a.usage.fiveHour);
317
- row("week", a.usage.week);
318
- }
319
- for (const [name, w] of Object.entries(a.perModel)) row(name.toLowerCase(), w);
320
- if (a.sample.ok && a.sample.source === "statusline") {
321
- const stale = a.usageAt == null || now - a.usageAt > staleAfterMs;
322
- const age = a.usageAt != null ? fmtAgo(a.usageAt, now) : "age unknown";
323
- console.log(` ${(stale ? c.yellow : c.dim)(`statusline tee ${age}${stale ? " (stale)" : ""}`)}`);
324
- }
325
- if (!a.sample.ok) {
326
- const cached = a.usage || Object.keys(a.perModel).length > 0 ? `cached${a.usageAt != null ? ` ${fmtAgo(a.usageAt, now)}` : ""}, ` : "";
327
- console.log(` ${c.yellow(`${cached}live sample failed`)}: ${c.dim(a.sample.reason)}`);
328
- }
329
- if (a.pingError != null && a.pingRejected) {
330
- const at = a.pingError.indexOf(": ");
331
- const head = at >= 0 ? a.pingError.slice(0, at) : a.pingError;
332
- const tail = at >= 0 ? a.pingError.slice(at + 2) : "";
333
- console.log(` ${c.yellow(`ping ${head}`)}${tail ? `: ${c.dim(tail)}` : ""}`);
334
- } else if (a.pingError != null) {
335
- console.log(` ${c.yellow("ping failed (5h timer may not have started)")}: ${c.dim(a.pingError)}`);
336
- }
337
- if (a.pinged) {
338
- console.log(` ${c.dim("pinged - 5h timer started this run; the usage feed lags, re-run status shortly for the fresh window")}`);
339
- }
340
- console.log();
341
- }
415
+ renderGrid(claude.accounts.map((a) => claudeCard(a, now, staleAfterMs)));
342
416
  }
343
417
 
344
- export async function cmdStatus(opts: { force?: boolean; json?: boolean; preRender?: () => void } = {}): Promise<number> {
345
- const { force = false, json = false } = opts;
418
+ export async function cmdStatus(opts: { ping?: boolean; pingCount?: number; json?: boolean; preRender?: () => void } = {}): Promise<number> {
419
+ const { ping = false, pingCount, json = false } = opts;
346
420
  const cfg = loadConfig();
347
421
  const now = Date.now();
348
- const claude = await collectClaude({ cfg, force, now });
422
+ const claude = await collectClaude({ cfg, ping, pingCount, now });
349
423
  if (!json) {
350
424
  opts.preRender?.();
351
425
  const at = Date.now();
352
- 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 });
353
427
  }
354
428
  const codex = await collectCodex({ cfg, now });
355
429
  if (json) {
@@ -357,7 +431,6 @@ export async function cmdStatus(opts: { force?: boolean; json?: boolean; preRend
357
431
  emitJson({ ok: true, ...report });
358
432
  return 0;
359
433
  }
360
- const at = Date.now();
361
- renderCodex({ codex, now: at, row: rowPrinter(at) });
434
+ renderCodex({ codex, now: Date.now() });
362
435
  return 0;
363
436
  }
@@ -97,7 +97,7 @@ const HELP_TEXT = `tokenmaxxing agent MCP
97
97
 
98
98
  Read tools (always available):
99
99
  pool_ls list pooled Claude and Codex accounts (labels/status only)
100
- pool_status sample usage bars (free /usage path; never --force)
100
+ pool_status sample usage bars (free /usage path; never --ping)
101
101
  doctor verify install health (labels/status only)
102
102
  config_get read effective config, or one key when provided
103
103
  help this catalog
@@ -109,7 +109,7 @@ Mutating tools (confirm=true AND ${MUTATIONS_ENV}=1):
109
109
  config_unset remove a config.json override
110
110
 
111
111
  Hard deny (no tools):
112
- status --force / metered pings
112
+ status --ping / metered pings
113
113
  init / add / auth / rm / uninstall
114
114
  credential blobs or token values
115
115
  killing sessions or supervisors
@@ -147,7 +147,7 @@ export function createTokenmaxxingMcpServer(): McpServer {
147
147
  server.registerTool(
148
148
  "pool_status",
149
149
  {
150
- description: "Show pool usage bars via the free /usage path. Never opens 5h windows. Do not request --force; that tool does not exist.",
150
+ description: "Show pool usage bars via the free /usage path. Never opens 5h windows. Do not request --ping; that tool does not exist.",
151
151
  inputSchema: {},
152
152
  },
153
153
  async () => {
package/src/main.ts CHANGED
@@ -45,7 +45,7 @@ function printHelp(): void {
45
45
  ${c.cyan("tokenmaxxing switch --codex")} [sel] switch the codex pool (takes effect on next codex start)
46
46
  ${c.cyan("tokenmaxxing ls")} list pooled accounts
47
47
  ${c.cyan("tokenmaxxing status")} accounts with 5h / weekly / per-model usage bars
48
- ${c.cyan("tokenmaxxing status --force")} ping every account (one tiny haiku request each) so all 5h session timers start now, then sample fresh; ${c.cyan("xx --force")} works too
48
+ ${c.cyan("tokenmaxxing status --ping")} [--count N] ping every account (one tiny haiku request each) so all 5h session timers start now, then sample fresh; ${c.cyan("--count N")} pings N randomly picked accounts instead, to stagger the resets; ${c.cyan("xx --ping")} works too
49
49
  ${c.cyan("tokenmaxxing watch")} [seconds] live status: re-render every N seconds (default 120, never pings)
50
50
  ${c.cyan("tokenmaxxing config")} [get|set|unset|tidy] inspect and edit config.json (bare = effective config with sources)
51
51
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
@@ -60,6 +60,28 @@ function printHelp(): void {
60
60
 
61
61
  let jsonMode = false;
62
62
 
63
+ function statusFlags(rest: string[]): { ping: boolean; pingCount: number | undefined } | { error: string } {
64
+ let ping = false;
65
+ let pingCount: number | undefined;
66
+ for (let i = 0; i < rest.length; i++) {
67
+ const arg = rest[i]!;
68
+ if (arg === "--ping") {
69
+ ping = true;
70
+ continue;
71
+ }
72
+ if (arg === "--count") {
73
+ if (pingCount != null) return { error: "--count may only be given once" };
74
+ const raw = rest[++i];
75
+ pingCount = Number(raw);
76
+ if (!Number.isInteger(pingCount) || pingCount < 1) return { error: `--count needs a positive whole number of accounts, got: ${raw ?? "nothing"}` };
77
+ continue;
78
+ }
79
+ return { error: `unknown status option: ${arg} (status takes --ping and --count N)` };
80
+ }
81
+ if (pingCount != null && !ping) return { error: "--count only applies together with --ping" };
82
+ return { ping, pingCount };
83
+ }
84
+
63
85
  async function main(): Promise<number> {
64
86
  if (process.platform !== "darwin" && process.platform !== "linux") {
65
87
  console.error(`tokenmaxxing supports macOS and Linux only (this is ${process.platform})`);
@@ -103,8 +125,17 @@ async function main(): Promise<number> {
103
125
  case "__stop-failure-hook": return runStopFailureHook();
104
126
  case "__session-start": return runSessionStart();
105
127
  case "__codex-stop-hook": return runCodexStopHook();
106
- case undefined: return cmdStatus({ json });
107
- case "--force": return cmdStatus({ force: true, json });
128
+ case undefined:
129
+ case "status":
130
+ case "--ping":
131
+ case "--count": {
132
+ const flags = statusFlags(sub === "status" ? args.slice(1) : args);
133
+ if ("error" in flags) {
134
+ emitError({ json, message: flags.error });
135
+ return 2;
136
+ }
137
+ return cmdStatus({ ...flags, json });
138
+ }
108
139
  case "switch": {
109
140
  const rest = args.slice(1).filter((a) => a !== "--codex");
110
141
  return args.includes("--codex") ? cmdCodexSwitch(rest[0], json) : cmdSwitch(rest[0], json);
@@ -115,7 +146,6 @@ async function main(): Promise<number> {
115
146
  case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
116
147
  case "auth": return cmdAuth(args.slice(1));
117
148
  case "ls": return cmdLs(json);
118
- case "status": return cmdStatus({ force: args.includes("--force"), json });
119
149
  case "watch": return cmdWatch(args[1], json);
120
150
  case "doctor": return cmdDoctor(json);
121
151
  case "rm": {