tokenmaxxing 1.25.0 → 1.27.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/DESIGN.md CHANGED
@@ -54,7 +54,7 @@ The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limi
54
54
  3. Done - the running session adopts the new credential on its own within a request or two. Only when the pool is depleted (the decision returned a `waitUntil`: pre-parked on the soonest-recovering account, or staying on the current one when it recovers first) does the hook write `respawn/<session_id>` (atomic temp+rename).
55
55
 
56
56
  ### 3.3 Depleted-pool pause (supervisor)
57
- The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, shows an interruptible countdown to the reset, and relaunches `claude --resume <sid>` when it passes. The resumed process reads the keychain cold → runs on the recovered account, same conversation. The `SessionStart` hook (source `resume`) re-checks the account before the first turn as a backstop.
57
+ The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, shows an interruptible countdown to the reset, and relaunches `claude --resume <sid>` when it passes. The resumed process reads the keychain cold → runs on the recovered account, same conversation. The `SessionStart` hook (source `resume`) re-checks the account before the first turn as a backstop. A marker that fails to parse is never deleted and treated as absent. The supervisor stops the child, restores the terminal, and exits naming the marker path.
58
58
 
59
59
  ### 3.4 Swap sequence (under the lock)
60
60
  1. **Harvest the live credential into its TRUE owner's backup** - read the current `Claude Code-credentials` blob and resolve which account it actually belongs to via the profile endpoint (`GET /api/oauth/profile`, matched on `account.uuid`), NOT the `accounts.json` active label and NOT the organization (team seats share one). The label drifts from the live blob (a kill mid-swap, a manual `/login`), and harvesting by label once overwrote another account's backup and destroyed its only credential. Mandatory anyway: Claude rotates the refresh token in place, so older backups are dead. Refuse the swap if the live credential belongs to no pooled account.
package/README.md CHANGED
@@ -70,8 +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 --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
- | `tokenmaxxing watch [seconds]` | live status: re-render every N seconds (default 120, floor 30; never pings) |
73
+ | `tokenmaxxing watch [seconds]` | live status: re-render every N seconds (default 120, floor 30) |
75
74
  | `tokenmaxxing config` | effective config with sources; `get`/`set`/`unset` dotted keys, `tidy` prunes unknown keys |
76
75
  | `tokenmaxxing doctor` | verify the supervisor + settings entries survived |
77
76
  | `tokenmaxxing rename [--codex] <sel> <label>` / `rm [--codex] <sel>` | manage the pool (`--codex` targets the codex pool: one email can hold both a claude and a codex account) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.25.0",
3
+ "version": "1.27.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,6 @@ 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 --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
40
  console.log(` ${c.cyan("xx add")} log in and pool another account`);
42
41
  console.log(` ${c.cyan("xx switch")} hop to the best account right now (the automatic switching needs no command)`);
43
42
  console.log(` ${c.cyan("xx help")} everything else`);
package/src/cli/status.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { chunk, sampleSize, sortBy } from "es-toolkit";
1
+ import { chunk, 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";
@@ -32,9 +32,6 @@ const ClaudeStatusAccountSchema = z.object({
32
32
  perModel: z.record(z.string(), UsageWindowSchema),
33
33
  usageAt: z.number().nullable(),
34
34
  sample: SampleReportSchema,
35
- pingError: z.string().nullable(),
36
- pingRejected: z.boolean(),
37
- pinged: z.boolean(),
38
35
  });
39
36
  type ClaudeStatusAccount = z.infer<typeof ClaudeStatusAccountSchema>;
40
37
 
@@ -86,31 +83,14 @@ function currentCodexWindow(w: CodexWindow, now: number): CodexWindow {
86
83
  return { ...currentWindow(w, !isSessionWindow({ window: w }), now), windowSeconds: w.windowSeconds };
87
84
  }
88
85
 
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;
86
+ async function collectClaude(input: { cfg: Config; now: number }): Promise<StatusReport["claude"]> {
87
+ const { cfg, now } = input;
105
88
  let idx = loadAccounts();
106
89
  const samples = new Map<string, { outcome: SampleOutcome; viaTee: boolean }>();
107
- let pings = new Set<string>();
108
90
  if (idx.accounts.length > 0) {
109
91
  await withLock(paths.lockFile, async () => {
110
92
  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 })));
93
+ console.error(c.dim("sampling live usage..."));
114
94
  const tee = loadUsageSnapshot();
115
95
  const live = tee?.state ?? null;
116
96
  const teeAt = tee?.at ?? null;
@@ -129,27 +109,12 @@ async function collectClaude(input: { cfg: Config; ping: boolean; pingCount: num
129
109
  perModel: modelUsage && modelUsage.account === a.accountUuid ? modelUsage.perModel : {},
130
110
  }
131
111
  : null;
132
- let viaTee = false;
133
- let outcome: SampleOutcome;
134
- if (pings.has(a.accountUuid)) {
135
- outcome = isActive ? await probeActiveUsage(a, { ping: true }) : await probeParkedUsage(a, { ping: true });
136
- if (!outcome.ok && fromStatusLine && outcome.reason.includes("no limit data")) {
137
- const failed = outcome;
138
- outcome = { ok: true, usage: fromStatusLine };
139
- if (failed.pingError != null) {
140
- outcome.pingError = failed.pingError;
141
- outcome.pingRejected = failed.pingRejected;
142
- }
143
- viaTee = true;
144
- }
145
- } else {
146
- viaTee = fromStatusLine != null;
147
- outcome = fromStatusLine
148
- ? { ok: true, usage: fromStatusLine }
149
- : isActive
150
- ? await probeActiveUsage(a)
151
- : await probeParkedUsage(a);
152
- }
112
+ const viaTee = fromStatusLine != null;
113
+ const outcome: SampleOutcome = fromStatusLine
114
+ ? { ok: true, usage: fromStatusLine }
115
+ : isActive
116
+ ? await probeActiveUsage(a)
117
+ : await probeParkedUsage(a);
153
118
  samples.set(a.accountUuid, { outcome, viaTee });
154
119
  if (!outcome.ok) return;
155
120
  a.lastUsage = { fiveHour: outcome.usage.session, sevenDay: outcome.usage.weekAll };
@@ -192,9 +157,6 @@ async function collectClaude(input: { cfg: Config; ping: boolean; pingCount: num
192
157
  perModel: Object.fromEntries(Object.entries(perModel).map(([name, w]) => [name, currentWindow(w, true, now)])),
193
158
  usageAt: a.lastUsageAt ?? null,
194
159
  sample: sampled.outcome.ok ? { ok: true, source: sampled.viaTee ? "statusline" : "probe" } : { ok: false, reason: sampled.outcome.reason },
195
- pingError: sampled.outcome.pingError ?? null,
196
- pingRejected: sampled.outcome.pingRejected === true,
197
- pinged: pings.has(a.accountUuid) && sampled.outcome.ok && sampled.outcome.pingError == null && aggregate != null && aggregate.fiveHour.resetsAt == null,
198
160
  };
199
161
  });
200
162
  return {
@@ -383,18 +345,6 @@ function claudeCard(a: ClaudeStatusAccount, now: number, staleAfterMs: number):
383
345
  if (!a.sample.ok) {
384
346
  notes.push(...sampleFailedNotes({ cached: a.usage != null || Object.keys(a.perModel).length > 0, usageAt: a.usageAt, reason: a.sample.reason, now }));
385
347
  }
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
348
  return { lines, notes };
399
349
  }
400
350
 
@@ -415,11 +365,11 @@ function renderClaude(input: { claude: StatusReport["claude"]; codexPooled: bool
415
365
  renderGrid(claude.accounts.map((a) => claudeCard(a, now, staleAfterMs)));
416
366
  }
417
367
 
418
- export async function cmdStatus(opts: { ping?: boolean; pingCount?: number; json?: boolean; preRender?: () => void } = {}): Promise<number> {
419
- const { ping = false, pingCount, json = false } = opts;
368
+ export async function cmdStatus(opts: { json?: boolean; preRender?: () => void } = {}): Promise<number> {
369
+ const { json = false } = opts;
420
370
  const cfg = loadConfig();
421
371
  const now = Date.now();
422
- const claude = await collectClaude({ cfg, ping, pingCount, now });
372
+ const claude = await collectClaude({ cfg, now });
423
373
  if (!json) {
424
374
  opts.preRender?.();
425
375
  const at = Date.now();
@@ -21,13 +21,14 @@ const NONINTERACTIVE_SUBCMDS = new Set([
21
21
 
22
22
  const PASSTHROUGH_FLAGS = new Set(["--version", "-V", "--help", "-h"]);
23
23
 
24
- function consumableCodexMarker(marker: string): z.infer<typeof CodexRespawnMarkerSchema> | null {
24
+ function readCodexMarker(marker: string): z.infer<typeof CodexRespawnMarkerSchema> {
25
25
  try {
26
26
  return CodexRespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
27
27
  } catch (e) {
28
- rmSync(marker, { force: true });
29
28
  log("codexsupervisor.marker_invalid", { err: e instanceof Error ? e.message : String(e) });
30
- return null;
29
+ throw new Error(
30
+ `${marker} is corrupt (unparsable JSON or off-schema) - auth.json may already hold another account, so the codex session was stopped instead of resumed on stale credentials; inspect and remove the marker, then run \`codex resume\``,
31
+ );
31
32
  }
32
33
  }
33
34
 
@@ -126,7 +127,10 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
126
127
  let done = false;
127
128
  const markerWatch = (async () => {
128
129
  while (!done) {
129
- if (existsSync(marker) && consumableCodexMarker(marker) != null) return true;
130
+ if (existsSync(marker)) {
131
+ readCodexMarker(marker);
132
+ return true;
133
+ }
130
134
  await Bun.sleep(150);
131
135
  }
132
136
  return false;
@@ -135,17 +139,20 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
135
139
  done = true;
136
140
  return "exit";
137
141
  });
138
- const winner = await Promise.race([exited, markerWatch.then((found) => (found ? "marker" : "exit"))]);
139
-
140
- if (winner === "marker") {
142
+ try {
143
+ const winner = await Promise.race([exited, markerWatch.then((found) => (found ? "marker" : "exit"))]);
144
+ if (winner === "marker") child.kill();
145
+ } catch (e) {
141
146
  child.kill();
147
+ throw e;
148
+ } finally {
149
+ await child.exited;
150
+ done = true;
151
+ await markerWatch.catch(() => false);
152
+ restoreTermios(savedTermios);
142
153
  }
143
- await child.exited;
144
- done = true;
145
- await markerWatch.catch(() => false);
146
- restoreTermios(savedTermios);
147
154
 
148
- const payload = existsSync(marker) ? consumableCodexMarker(marker) : null;
155
+ const payload = existsSync(marker) ? readCodexMarker(marker) : null;
149
156
  if (payload) {
150
157
  rmSync(marker, { force: true });
151
158
  respawns++;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, readdirSync, statSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { basename, join } from "node:path";
3
3
  import { maxBy } from "es-toolkit";
4
4
  import { z } from "zod";
5
5
  import { paths } from "../lib/paths.ts";
@@ -152,9 +152,10 @@ function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof Resp
152
152
  try {
153
153
  m = RespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
154
154
  } catch (e) {
155
- rmSync(marker, { force: true });
156
155
  log("supervisor.marker_invalid", { err: e instanceof Error ? e.message : String(e) });
157
- return null;
156
+ throw new Error(
157
+ `${marker} is corrupt (unparsable JSON or off-schema) - the session was stopped instead of guessing whether the pool is depleted; inspect the marker, then run \`claude --resume ${basename(marker)}\` (a fresh launch clears it)`,
158
+ );
158
159
  }
159
160
  if (m.launchedAt !== undefined && m.launchedAt !== gate.launchedAt) {
160
161
  rmSync(marker, { force: true });
@@ -270,15 +271,18 @@ export async function runSupervisor(argv: string[]): Promise<number> {
270
271
  return false;
271
272
  })();
272
273
  const exited = child.exited.then(() => { done = true; return "exit" as const; });
273
- const winner = await Promise.race([exited, markerWatch.then((m) => (m ? "marker" : "exit"))]);
274
-
275
- if (winner === "marker") {
274
+ try {
275
+ const winner = await Promise.race([exited, markerWatch.then((m) => (m ? "marker" : "exit"))]);
276
+ if (winner === "marker") child.kill();
277
+ } catch (e) {
276
278
  child.kill();
279
+ throw e;
280
+ } finally {
281
+ await child.exited;
282
+ done = true;
283
+ await markerWatch.catch(() => {});
284
+ restoreTermios(savedTermios);
277
285
  }
278
- await child.exited;
279
- done = true;
280
- await markerWatch.catch(() => {});
281
- restoreTermios(savedTermios);
282
286
 
283
287
  const m = existsSync(marker) ? consumableMarker(marker, gate) : null;
284
288
  if (m) {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { http, safeErrorDetail } from "./http.ts";
2
+ import { http, oauthErrorCode, safeErrorDetail } from "./http.ts";
3
3
  import { CodexAuthJsonSchema, type CodexAuthJson } from "./types.ts";
4
4
 
5
5
  const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
@@ -26,12 +26,12 @@ const CodexRefreshResponseSchema = z.looseObject({
26
26
  refresh_token: z.string().optional(),
27
27
  });
28
28
 
29
- const DEAD_GRANT_MARKERS = [
29
+ const DEAD_GRANT_CODES = new Set([
30
30
  "invalid_grant",
31
31
  "refresh_token_reused",
32
32
  "refresh_token_expired",
33
33
  "refresh_token_invalidated",
34
- ];
34
+ ]);
35
35
 
36
36
  export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: number }): Promise<CodexAuthJson> {
37
37
  const { auth, now = Date.now() } = input;
@@ -53,7 +53,8 @@ export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: numbe
53
53
  const text = await res.text();
54
54
  if (!res.ok) {
55
55
  const detail = safeErrorDetail({ text });
56
- if (DEAD_GRANT_MARKERS.some((marker) => text.includes(marker))) {
56
+ const code = oauthErrorCode({ text })?.toLowerCase();
57
+ if (code != null && DEAD_GRANT_CODES.has(code)) {
57
58
  throw new CodexInvalidGrantError(detail);
58
59
  }
59
60
  throw new CodexRefreshFailedError(`HTTP ${res.status}: ${detail}`);
package/src/lib/http.ts CHANGED
@@ -7,7 +7,7 @@ const OAuthErrorBodySchema = z.looseObject({
7
7
  });
8
8
 
9
9
  const NestedErrorBodySchema = z.looseObject({
10
- error: z.looseObject({ type: z.string().optional(), message: z.string().optional() }),
10
+ error: z.looseObject({ code: z.string().nullish(), type: z.string().optional(), message: z.string().optional() }),
11
11
  request_id: z.string().nullish(),
12
12
  });
13
13
 
@@ -31,7 +31,8 @@ function parseErrorBody(text: string): ErrorDetail | null {
31
31
  const nested = NestedErrorBodySchema.safeParse(json);
32
32
  if (nested.success) {
33
33
  const requestId = nested.data.request_id ? ` (${nested.data.request_id})` : "";
34
- return { code: nested.data.error.type ?? null, fields: [nested.data.error.type ?? "", (nested.data.error.message ?? "") + requestId] };
34
+ const code = nested.data.error.code ?? nested.data.error.type ?? null;
35
+ return { code, fields: [code ?? "", (nested.data.error.message ?? "") + requestId] };
35
36
  }
36
37
  const plain = PlainErrorBodySchema.safeParse(json);
37
38
  if (plain.success) return { code: null, fields: [plain.data.detail ?? "", plain.data.message ?? ""] };
package/src/lib/sample.ts CHANGED
@@ -5,15 +5,15 @@ import { readItem, writeItem, deleteItem, liveTarget, parkedTarget, isolatedTarg
5
5
  import { credItemFor, paths } from "./paths.ts";
6
6
  import { withClaudeRefreshLock } from "./claudelock.ts";
7
7
  import { refreshCredential, isAccessTokenExpiring, isDeadCredential, fetchTokenIdentity, describeIdentity, IdentityUnavailableError, InvalidGrantError } from "./oauth.ts";
8
- import { FullUsageSchema, pingSession, probeUsage } from "./usage.ts";
8
+ import { FullUsageSchema, probeUsage } from "./usage.ts";
9
9
  import { loadAccounts } from "./state.ts";
10
10
  import { keepRotatedPair } from "./swap.ts";
11
11
  import { log } from "./log.ts";
12
12
  import { CredentialBlobSchema, TokenIdentitySchema, type Account, type OAuthCreds, type TokenIdentity } from "./types.ts";
13
13
 
14
14
  const SampleOutcomeSchema = z.discriminatedUnion("ok", [
15
- z.object({ ok: z.literal(true), usage: FullUsageSchema, pingError: z.string().optional(), pingRejected: z.boolean().optional() }),
16
- z.object({ ok: z.literal(false), reason: z.string(), pingError: z.string().optional(), pingRejected: z.boolean().optional() }),
15
+ z.object({ ok: z.literal(true), usage: FullUsageSchema }),
16
+ z.object({ ok: z.literal(false), reason: z.string() }),
17
17
  ]);
18
18
  export type SampleOutcome = z.infer<typeof SampleOutcomeSchema>;
19
19
 
@@ -46,7 +46,7 @@ function refreshPlanFields(account: Account, creds: OAuthCreds): void {
46
46
 
47
47
  export async function probeParkedUsage(
48
48
  account: Account,
49
- opts: { ping?: boolean; retries?: number; refreshParked?: boolean; killMs?: number; signal?: AbortSignal } = {},
49
+ opts: { retries?: number; refreshParked?: boolean; killMs?: number; signal?: AbortSignal } = {},
50
50
  ): Promise<SampleOutcome> {
51
51
  const backup = parkedTarget(account.keychainItem);
52
52
  const parkedRaw = await readItem(backup);
@@ -165,16 +165,8 @@ export async function probeParkedUsage(
165
165
  try {
166
166
  await writeItem(isoTarget, installed);
167
167
  writeFileSync(join(dir, ".claude.json"), JSON.stringify({ oauthAccount: account.oauthAccount, hasCompletedOnboarding: true }));
168
- const ping = opts.ping ? await pingSession(dir) : null;
169
168
  const usage = await probeUsage(dir, Date.now(), opts);
170
- const outcome: SampleOutcome = usage
171
- ? { ok: true, usage }
172
- : { ok: false, reason: "`/usage` returned no limit data (see log)" };
173
- if (ping != null) {
174
- outcome.pingError = ping.reason;
175
- outcome.pingRejected = ping.rejected;
176
- }
177
- return outcome;
169
+ return usage ? { ok: true, usage } : { ok: false, reason: "`/usage` returned no limit data (see log)" };
178
170
  } finally {
179
171
  try {
180
172
  if (opts.refreshParked !== false) {
@@ -211,7 +203,7 @@ export async function ensureLiveTokenFresh(): Promise<void> {
211
203
  });
212
204
  }
213
205
 
214
- export async function probeActiveUsage(account: Account, opts: { ping?: boolean } = {}): Promise<SampleOutcome> {
206
+ export async function probeActiveUsage(account: Account): Promise<SampleOutcome> {
215
207
  try {
216
208
  await ensureLiveTokenFresh();
217
209
  } catch (e) {
@@ -232,12 +224,6 @@ export async function probeActiveUsage(account: Account, opts: { ping?: boolean
232
224
  if (identity.status === "unavailable") return { ok: false, reason: identity.reason };
233
225
  refreshPlanFields(account, creds);
234
226
 
235
- const ping = opts.ping ? await pingSession() : null;
236
227
  const usage = await probeUsage();
237
- const outcome: SampleOutcome = usage ? { ok: true, usage } : { ok: false, reason: "`/usage` returned no limit data (see log)" };
238
- if (ping != null) {
239
- outcome.pingError = ping.reason;
240
- outcome.pingRejected = ping.rejected;
241
- }
242
- return outcome;
228
+ return usage ? { ok: true, usage } : { ok: false, reason: "`/usage` returned no limit data (see log)" };
243
229
  }
package/src/lib/usage.ts CHANGED
@@ -1,10 +1,8 @@
1
- import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
3
2
  import { delay } from "es-toolkit";
4
3
  import { z } from "zod";
5
4
  import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealClaude } from "./claudebin.ts";
6
5
  import { log } from "./log.ts";
7
- import { paths } from "./paths.ts";
8
6
  import { RateLimitsStdinSchema, UsageWindowSchema, type ModelInfo, type UsageWindow, type UsageWindows } from "./types.ts";
9
7
 
10
8
  export function normalizeResetsAt(v: unknown): number | null {
@@ -285,12 +283,11 @@ type SpawnResult = z.infer<typeof SpawnResultSchema>;
285
283
  async function spawnClaudeBounded(
286
284
  cmd: string[],
287
285
  env: Record<string, string>,
288
- cwd?: string,
289
286
  killMs = PROBE_KILL_MS,
290
287
  signal?: AbortSignal,
291
288
  ): Promise<SpawnResult | null> {
292
289
  signal?.throwIfAborted();
293
- const p = Bun.spawn(cmd, { env, cwd, stdout: "pipe", stderr: "pipe" });
290
+ const p = Bun.spawn(cmd, { env, stdout: "pipe", stderr: "pipe" });
294
291
  const abort = () => p.kill("SIGKILL");
295
292
  signal?.addEventListener("abort", abort, { once: true });
296
293
  const killer = setTimeout(() => p.kill("SIGKILL"), killMs);
@@ -313,7 +310,7 @@ async function spawnClaudeBounded(
313
310
  async function probeUsageOnce(env: Record<string, string>, now: number, killMs: number, signal?: AbortSignal): Promise<FullUsage | null> {
314
311
  let out: string;
315
312
  try {
316
- const r = await spawnClaudeBounded([resolveRealClaude(), "-p", "/usage", "--output-format", "json"], env, undefined, killMs, signal);
313
+ const r = await spawnClaudeBounded([resolveRealClaude(), "-p", "/usage", "--output-format", "json"], env, killMs, signal);
317
314
  if (r === null) {
318
315
  log("usage.probe_failed", { err: "output pipes still open after child exit (leaked descendant)" });
319
316
  return null;
@@ -364,68 +361,3 @@ export async function probeUsage(configDir?: string, now = Date.now(), opts: { r
364
361
  await delay(PROBE_RETRY_DELAYS_MS[Math.min(attempt, PROBE_RETRY_DELAYS_MS.length - 1)]!);
365
362
  }
366
363
  }
367
-
368
- const PING_ARGS = [
369
- "-p", "Reply with exactly: ok",
370
- "--model", "haiku",
371
- "--settings", '{"disableAllHooks":true}',
372
- "--output-format", "json",
373
- ];
374
-
375
- const PingResultSchema = z.looseObject({ is_error: z.boolean().optional(), result: z.string().optional(), session_id: z.string().optional() });
376
-
377
- export const PingFailureSchema = z.object({ reason: z.string(), rejected: z.boolean() });
378
- export type PingFailure = z.infer<typeof PingFailureSchema>;
379
-
380
- export function transcriptSlug(cwd: string): string {
381
- return cwd.replace(/[^a-zA-Z0-9]/g, "-");
382
- }
383
-
384
- export function pingTranscriptPath(input: { configDir: string; cwd: string; sessionId: string }): string {
385
- return join(input.configDir, "projects", transcriptSlug(input.cwd), `${input.sessionId}.jsonl`);
386
- }
387
-
388
- const LIMIT_LABELS: Record<string, string> = { five_hour: "5h", seven_day: "weekly" };
389
-
390
- export function pingRejection(rows: TranscriptRow[]): string | null {
391
- const row = rows.findLast((r) => r.isApiErrorMessage === true);
392
- if (!row || row.error !== "rate_limit" || row.apiErrorIsTransient === true) return null;
393
- const type = row.quotaLimits?.rateLimitType ?? "";
394
- if (type === "" && parseErrorBody(row.errorDetails)?.error?.type !== "rate_limit_error") return null;
395
- const label = LIMIT_LABELS[type] ?? (type !== "" ? type.replace(/_/g, " ") : "usage");
396
- const text = transcriptRowText(row);
397
- return `${label} limit${text ? `: ${text}` : ""}`;
398
- }
399
-
400
- export async function pingSession(configDir?: string): Promise<PingFailure | null> {
401
- const cwd = join(paths.sampleDir, "ping-cwd");
402
- const fail = (reason: string, rejected = false): PingFailure => {
403
- log("usage.ping_failed", { dir: configDir ?? "live", rejected, reason: reason.slice(0, 200) });
404
- return { reason, rejected };
405
- };
406
- let r: SpawnResult | null;
407
- try {
408
- mkdirSync(cwd, { recursive: true });
409
- r = await spawnClaudeBounded([resolveRealClaude(), ...PING_ARGS], probeEnv(configDir), cwd);
410
- } catch (e) {
411
- return fail(e instanceof Error ? e.message : String(e));
412
- }
413
- if (r === null) return fail("output pipes still open after child exit (leaked descendant)");
414
- const parsed = PingResultSchema.safeParse((() => { try { return JSON.parse(r.stdout); } catch { return null; } })());
415
- const result = parsed.success ? parsed.data : null;
416
- if (r.exitCode === 0 && result?.is_error === false) {
417
- log("usage.ping_ok", { dir: configDir ?? "live" });
418
- return null;
419
- }
420
- if (result?.session_id) {
421
- const transcript = pingTranscriptPath({ configDir: configDir ?? paths.claudeDir, cwd, sessionId: result.session_id });
422
- const rejection = existsSync(transcript) ? pingRejection(readTranscriptTail(transcript)) : null;
423
- if (rejection) return fail(`rejected at the ${rejection}`.slice(0, 200), true);
424
- }
425
- if (r.exitCode !== 0) {
426
- const detail = result?.result?.trim() || r.stderr.trim() || r.stdout.trim();
427
- return fail(`claude exited ${r.exitCode ?? "on signal"}: ${detail.slice(0, 160)}`);
428
- }
429
- if (result?.is_error === true) return fail((result.result?.trim() || "request errored").slice(0, 160));
430
- return fail(`unrecognized ping output: ${r.stdout.trim().slice(0, 120)}`);
431
- }
package/src/main.ts CHANGED
@@ -45,8 +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 --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
- ${c.cyan("tokenmaxxing watch")} [seconds] live status: re-render every N seconds (default 120, never pings)
48
+ ${c.cyan("tokenmaxxing watch")} [seconds] live status: re-render every N seconds (default 120)
50
49
  ${c.cyan("tokenmaxxing config")} [get|set|unset|tidy] inspect and edit config.json (bare = effective config with sources)
51
50
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
52
51
  ${c.cyan("tokenmaxxing rename")} [--codex] <sel> <label>
@@ -60,28 +59,6 @@ function printHelp(): void {
60
59
 
61
60
  let jsonMode = false;
62
61
 
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
-
85
62
  async function main(): Promise<number> {
86
63
  if (process.platform !== "darwin" && process.platform !== "linux") {
87
64
  console.error(`tokenmaxxing supports macOS and Linux only (this is ${process.platform})`);
@@ -126,15 +103,13 @@ async function main(): Promise<number> {
126
103
  case "__session-start": return runSessionStart();
127
104
  case "__codex-stop-hook": return runCodexStopHook();
128
105
  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 });
106
+ case "status": {
107
+ const extra = args[1];
108
+ if (extra != null) {
109
+ emitError({ json, message: `unknown status option: ${extra} (status takes no options)` });
135
110
  return 2;
136
111
  }
137
- return cmdStatus({ ...flags, json });
112
+ return cmdStatus({ json });
138
113
  }
139
114
  case "switch": {
140
115
  const rest = args.slice(1).filter((a) => a !== "--codex");