tokenmaxxing 1.76.0 → 1.78.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.76.0",
3
+ "version": "1.78.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/doctor.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { verifyRealClaude } from "../lib/claudebin.ts";
2
+ import { CLAUDE_BIN, verifyRealBin } from "../lib/claudebin.ts";
3
3
  import { checkSettings, installedBin } from "../lib/settings.ts";
4
4
  import { checkTimerHealthy, codexStoreHookTrust, findClaudeShadowers, hubServiceHealthy, isBinDirAhead, shellRcPath, skipImperativeHub, timerActivationHint } from "../lib/install.ts";
5
5
  import { errorMessage } from "../lib/log.ts";
@@ -95,7 +95,7 @@ export async function cmdDoctor(): Promise<number> {
95
95
  const cfg = loadConfig();
96
96
  check(!!cfg.claudeBin && existsSync(cfg.claudeBin), "real claude binary resolved", "set claudeBin in config.json");
97
97
  if (cfg.claudeBin && existsSync(cfg.claudeBin)) {
98
- const fail = verifyRealClaude(cfg.claudeBin);
98
+ const fail = verifyRealBin({ ...CLAUDE_BIN, bin: cfg.claudeBin });
99
99
  check(fail === null, "claudeBin launches the real claude", fail ?? undefined);
100
100
  }
101
101
 
@@ -2,9 +2,8 @@ import { join } from "node:path";
2
2
  import type { z } from "zod";
3
3
  import { codexPaths, optionalEnv } from "../lib/paths.ts";
4
4
  import { writeFileAtomic } from "../lib/atomic.ts";
5
- import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "../lib/claudebin.ts";
5
+ import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin } from "../lib/claudebin.ts";
6
6
  import { codex, codexPickCtx } from "../lib/codex.ts";
7
- import { resolveRealCodex } from "../lib/codexbin.ts";
8
7
  import { compactCodexThread } from "../lib/compact.ts";
9
8
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
10
9
  import { isExhausted } from "../lib/picker.ts";
@@ -26,7 +25,7 @@ async function compactBeforeMove(input: { sessionId: string | null; now: number
26
25
  const env: Record<string, string | undefined> = { ...process.env, TOKENMAXXING_PROBE: "1", [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) };
27
26
  delete env[CODEX_SUPERVISOR_ID_ENV];
28
27
  log("codexstop.compact_start", { thread: sessionId.slice(0, 8), account: live.id.slice(0, 8) });
29
- await compactCodexThread({ real: resolveRealCodex(), threadId: sessionId, env });
28
+ await compactCodexThread({ real: resolveRealBin(CODEX_BIN), threadId: sessionId, env });
30
29
  }
31
30
 
32
31
  async function handleCodexStop(rawStdin: string): Promise<void> {
@@ -3,8 +3,7 @@ import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { codexPaths, codexPool } from "../lib/paths.ts";
5
5
  import { withLock } from "../lib/lock.ts";
6
- import { UNMANAGED_ENV, WRAP_DEPTH_ENV, wrapDepth } from "../lib/claudebin.ts";
7
- import { resolveRealCodex } from "../lib/codexbin.ts";
6
+ import { CODEX_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
8
7
  import { codexStoreUsable, ensureCodexStoreHome } from "../lib/codexauth.ts";
9
8
  import { codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
10
9
  import { clearPresence, livingPresences } from "../lib/presence.ts";
@@ -74,7 +73,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
74
73
  const { argv } = input;
75
74
  if (loopGuardTripped("codex")) return 1;
76
75
 
77
- const real = resolveRealCodex();
76
+ const real = resolveRealBin(CODEX_BIN);
78
77
  const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
79
78
 
80
79
  if (!shouldManageCodex({ argv }) || process.env[UNMANAGED_ENV]) {
@@ -4,7 +4,7 @@ import type { FileSink, Subprocess } from "bun";
4
4
  import { maxBy } from "es-toolkit";
5
5
  import { z } from "zod";
6
6
  import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
7
- import { UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealClaude, wrapDepth } from "../lib/claudebin.ts";
7
+ import { CLAUDE_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
8
8
  import { claude, pickSeat } from "../lib/claude.ts";
9
9
  import { compactClaudeSession } from "../lib/compact.ts";
10
10
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
@@ -353,7 +353,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
353
353
  }
354
354
  let real: string;
355
355
  try {
356
- real = resolveRealClaude();
356
+ real = resolveRealBin(CLAUDE_BIN);
357
357
  } catch (e) {
358
358
  if (!earlyStream) throw e;
359
359
  process.stdout.write(systemLine(earlySid, `tokenmaxxing: ${errorMessage(e)}`));
package/src/lib/claude.ts CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
5
- import { resolveRealClaude, resolveVerifiedClaude } from "./claudebin.ts";
5
+ import { CLAUDE_BIN, resolveRealBin, resolveVerifiedClaude } from "./claudebin.ts";
6
6
  import { ensurePathInRc, hubActivationHint, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
7
7
  import { withLock } from "./lock.ts";
8
8
  import { errorMessage, log } from "./log.ts";
@@ -151,7 +151,7 @@ async function login(): Promise<Harvest | null> {
151
151
  const iso = isolatedTarget(onboardDir);
152
152
  await deleteItem(iso);
153
153
  const cjPath = join(onboardDir, ".claude.json");
154
- const real = resolveRealClaude();
154
+ const real = resolveRealBin(CLAUDE_BIN);
155
155
 
156
156
  const savedTermios = saveTermios();
157
157
  const env = scrubCredentialEnv({ ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" });
@@ -4,7 +4,7 @@ import { uniq } from "es-toolkit";
4
4
  import { z } from "zod";
5
5
  import { errorMessage } from "./log.ts";
6
6
  import { paths } from "./paths.ts";
7
- import { loadConfig } from "./state.ts";
7
+ import { loadConfig, type BinKey } from "./state.ts";
8
8
  import { writeFileAtomic } from "./atomic.ts";
9
9
 
10
10
  export const WRAP_DEPTH_ENV = "TOKENMAXXING_WRAP_DEPTH";
@@ -49,75 +49,70 @@ export function pointsBackAtUs(bin: string): boolean {
49
49
  return resolved != null && binDir != null && dirname(resolved) === binDir;
50
50
  }
51
51
 
52
- function scanPathForClaudeCandidates(): string[] {
52
+ function scanPathCandidates(name: string): string[] {
53
53
  const seen = new Set<string>();
54
54
  const out: string[] = [];
55
55
  for (const d of (process.env.PATH ?? "").split(":")) {
56
56
  if (!d) continue;
57
- const cand = join(d, "claude");
58
- try {
59
- if (existsSync(cand) && statSync(cand).isFile() && !pointsBackAtUs(cand)) {
60
- const key = realpathOrNull(cand) ?? cand;
61
- if (!seen.has(key)) {
62
- seen.add(key);
63
- out.push(cand);
64
- }
65
- }
66
- } catch { }
57
+ const cand = join(d, name);
58
+ if (!existsSync(cand) || !statSync(cand, { throwIfNoEntry: false })?.isFile() || pointsBackAtUs(cand)) continue;
59
+ const key = realpathOrNull(cand) ?? cand;
60
+ if (seen.has(key)) continue;
61
+ seen.add(key);
62
+ out.push(cand);
67
63
  }
68
64
  return out;
69
65
  }
70
66
 
71
- export function scanPathForClaude(): string | null {
72
- return scanPathForClaudeCandidates()[0] ?? null;
73
- }
74
-
75
- export function resolveRealClaude(): string {
76
- const cfg = loadConfig();
77
- if (cfg.claudeBin) {
78
- if (!existsSync(cfg.claudeBin)) {
79
- throw new Error(`configured claudeBin does not exist: ${cfg.claudeBin} - fix config.json`);
67
+ export function resolveRealBin(input: { name: string; key: BinKey }): string {
68
+ const configured = loadConfig()[input.key];
69
+ if (configured) {
70
+ if (!existsSync(configured)) {
71
+ throw new Error(`configured ${input.key} does not exist: ${configured} - fix config.json`);
80
72
  }
81
- if (pointsBackAtUs(cfg.claudeBin)) {
73
+ if (pointsBackAtUs(configured)) {
82
74
  throw new Error(
83
- `configured claudeBin (${cfg.claudeBin}) is tokenmaxxing's own wrapper - spawning it recurses. Point claudeBin at the real claude binary in ${paths.configJson}`,
75
+ `configured ${input.key} (${configured}) is tokenmaxxing's own wrapper - spawning it recurses. Point ${input.key} at the real ${input.name} binary in ${paths.configJson}`,
84
76
  );
85
77
  }
86
- return cfg.claudeBin;
78
+ return configured;
87
79
  }
88
- const scanned = scanPathForClaude();
80
+ const scanned = scanPathCandidates(input.name)[0];
89
81
  if (scanned) return scanned;
90
- throw new Error("could not locate the real `claude` binary (set claudeBin in config.json)");
82
+ throw new Error(`could not locate the real \`${input.name}\` binary (set ${input.key} in config.json)`);
91
83
  }
92
84
 
93
- export function verifyRealClaude(bin: string): string | null {
85
+ export function verifyRealBin(input: { bin: string; name: string; versionOk: (versionOutput: string) => boolean }): string | null {
94
86
  const env = { ...process.env, [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH), TOKENMAXXING_PROBE: "1" };
95
87
  let p;
96
88
  try {
97
- p = Bun.spawnSync([bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
89
+ p = Bun.spawnSync([input.bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
98
90
  } catch (e) {
99
91
  return errorMessage(e);
100
92
  }
101
93
  const outText = p.stdout.toString().trim();
102
94
  const err = p.stderr.toString().trim();
103
95
  if (p.exitCode === 0) {
104
- if (/claude/i.test(outText)) return null;
105
- return `--version output does not identify claude: "${outText.slice(0, 80)}"`;
96
+ if (input.versionOk(outText)) return null;
97
+ return `--version output does not identify ${input.name}: "${outText.slice(0, 80)}"`;
106
98
  }
107
99
  if (err.includes(LOOP_DIAGNOSIS)) return "it leads back into the tokenmaxxing wrapper (recursion)";
108
100
  return `--version exited ${p.exitCode ?? "on signal/timeout"}: ${(err || outText).slice(0, 160)}`;
109
101
  }
110
102
 
103
+ export const CLAUDE_BIN = { name: "claude", key: "claudeBin", versionOk: (out: string) => out.toLowerCase().includes("claude") } as const;
104
+ export const CODEX_BIN = { name: "codex", key: "codexBin", versionOk: (out: string) => out.toLowerCase().includes("codex") } as const;
105
+
111
106
  export function resolveVerifiedClaude(): string {
112
107
  const candidates: string[] = [];
113
108
  try {
114
- candidates.push(resolveRealClaude());
109
+ candidates.push(resolveRealBin(CLAUDE_BIN));
115
110
  } catch { }
116
- candidates.push(...scanPathForClaudeCandidates());
111
+ candidates.push(...scanPathCandidates(CLAUDE_BIN.name));
117
112
 
118
113
  const failures: string[] = [];
119
114
  for (const cand of uniq(candidates)) {
120
- const fail = verifyRealClaude(cand);
115
+ const fail = verifyRealBin({ ...CLAUDE_BIN, bin: cand });
121
116
  if (fail === null) return cand;
122
117
  failures.push(`${cand}: ${fail}`);
123
118
  }
package/src/lib/codex.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { existsSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { writeFileAtomic } from "./atomic.ts";
4
- import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "./claudebin.ts";
4
+ import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
5
5
  import { codexIdentityOf, codexStoreUsable, deleteCodexStoreAuth, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
6
- import { resolveRealCodex, verifyRealCodex } from "./codexbin.ts";
7
6
  import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
8
7
  import { seatCounts } from "./presence.ts";
9
8
  import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
@@ -113,7 +112,7 @@ async function prepareMove(target: Account): Promise<void> {
113
112
  }
114
113
 
115
114
  async function login(): Promise<Harvest | null> {
116
- const real = resolveRealCodex();
115
+ const real = resolveRealBin(CODEX_BIN);
117
116
  const onboardDir = codexPaths.onboardDir;
118
117
  rmSync(onboardDir, { recursive: true, force: true });
119
118
  writeFileAtomic(join(onboardDir, "config.toml"), 'cli_auth_credentials_store = "file"\n');
@@ -179,8 +178,8 @@ function storePinnedAwayFromFile(): boolean {
179
178
  }
180
179
 
181
180
  function preflight(): void {
182
- const real = resolveRealCodex();
183
- const fail = verifyRealCodex({ bin: real });
181
+ const real = resolveRealBin(CODEX_BIN);
182
+ const fail = verifyRealBin({ ...CODEX_BIN, bin: real });
184
183
  if (fail !== null) throw new Error(`codex binary failed verification: ${real}: ${fail}`);
185
184
  pinBinOverride({ key: "codexBin", bin: real });
186
185
  if (storePinnedAwayFromFile()) {
package/src/lib/grok.ts CHANGED
@@ -2,13 +2,13 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { writeFileAtomic } from "./atomic.ts";
5
- import { fetchGrokUsage, GrokUsageReadError } from "./grokusage.ts";
5
+ import { fetchGrokUsage, GrokUsageReadError, type GrokUsage } from "./grokusage.ts";
6
6
  import { errorMessage } from "./log.ts";
7
7
  import { grokAuthJsonFor, grokPaths, grokPool, grokSeatFromEnv, grokStoreDirFor } from "./paths.ts";
8
8
  import type { Provider, SampleReport } from "./provider.ts";
9
9
  import { loadAccounts, type Harvest } from "./state.ts";
10
10
  import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
11
- import type { Account } from "./types.ts";
11
+ import { InstantSchema, type Account } from "./types.ts";
12
12
  import { c } from "../cli/render.ts";
13
13
 
14
14
  const GrokAuthEntrySchema = z.looseObject({
@@ -69,8 +69,7 @@ function credsIn(path: string): GrokCred[] {
69
69
  const id = entry.user_id ?? entry.principal_id ?? key;
70
70
  const token = entry.key ?? "";
71
71
  if (id === "" || token === "") continue;
72
- const expiresAt = entry.expires_at != null ? Date.parse(entry.expires_at) : null;
73
- out.push({ id, token, expiresAt: expiresAt != null && Number.isFinite(expiresAt) ? expiresAt : null });
72
+ out.push({ id, token, expiresAt: InstantSchema.safeParse(entry.expires_at).data ?? null });
74
73
  }
75
74
  return out;
76
75
  }
@@ -91,27 +90,21 @@ function bearerFor(accountId: string, now: number): { token: string; from: "stor
91
90
  async function readGrokUsage(account: Account, now: number): Promise<{ ok: true; at: number } | { ok: false; reason: string }> {
92
91
  const first = bearerFor(account.id, now);
93
92
  if (first == null) return { ok: false, reason: "no usable credential in this account's store - run `tokenmaxxing auth --grok`" };
93
+ let usage: GrokUsage;
94
94
  try {
95
- const usage = await fetchGrokUsage({ token: first.token, at: now });
96
- account.windows = usage.windows;
97
- account.lastUsageAt = usage.at;
98
- return { ok: true, at: usage.at };
95
+ usage = await fetchGrokUsage({ token: first.token, at: now });
99
96
  } catch (e) {
100
- if (e instanceof GrokUsageReadError && e.status === 401 && first.from === "store") {
101
- const live = liveCred(account.id);
102
- if (live != null && live.token !== first.token) {
103
- try {
104
- const usage = await fetchGrokUsage({ token: live.token, at: now });
105
- account.windows = usage.windows;
106
- account.lastUsageAt = usage.at;
107
- return { ok: true, at: usage.at };
108
- } catch (retry) {
109
- return { ok: false, reason: retry instanceof GrokUsageReadError ? retry.message : errorMessage(retry) };
110
- }
111
- }
97
+ const live = e instanceof GrokUsageReadError && e.status === 401 && first.from === "store" ? liveCred(account.id) : null;
98
+ if (live == null || live.token === first.token) return { ok: false, reason: errorMessage(e) };
99
+ try {
100
+ usage = await fetchGrokUsage({ token: live.token, at: now });
101
+ } catch (retry) {
102
+ return { ok: false, reason: errorMessage(retry) };
112
103
  }
113
- return { ok: false, reason: e instanceof GrokUsageReadError ? e.message : errorMessage(e) };
114
104
  }
105
+ account.windows = usage.windows;
106
+ account.lastUsageAt = usage.at;
107
+ return { ok: true, at: usage.at };
115
108
  }
116
109
 
117
110
  async function samplePool(accounts: Account[], _liveId: string | null, now: number): Promise<Map<string, SampleReport>> {
@@ -140,7 +133,7 @@ const base = statusOnlyProvider({
140
133
  liveAuthPath: () => join(grokPaths.home, "auth.json"),
141
134
  readAuth,
142
135
  liveId: () => grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id)),
143
- versionOk: (out) => out.trim().toLowerCase().includes("grok"),
136
+ versionOk: (out) => out.toLowerCase().includes("grok"),
144
137
  importIntro: "Opening an isolated grok login for your first pooled account - the login you already have stays as it is.",
145
138
  foundLive: (count) => `found ${count} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`,
146
139
  installNotice: "grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts.",
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { http, safeErrorDetail } from "./http.ts";
3
3
  import { errorMessage } from "./log.ts";
4
4
  import { env } from "./paths.ts";
5
- import { JsonTextSchema, type Window } from "./types.ts";
5
+ import { InstantSchema, JsonTextSchema, type Window } from "./types.ts";
6
6
 
7
7
  export class GrokUsageReadError extends Error {
8
8
  status: number | null;
@@ -13,8 +13,6 @@ export class GrokUsageReadError extends Error {
13
13
  }
14
14
  }
15
15
 
16
- const InstantSchema = z.iso.datetime({ offset: true });
17
-
18
16
  const CreditsPeriodSchema = z.looseObject({
19
17
  start: InstantSchema,
20
18
  end: InstantSchema,
@@ -57,17 +55,15 @@ export async function fetchGrokUsage(input: { token: string; at: number }): Prom
57
55
  if (!parsed.success) {
58
56
  throw new GrokUsageReadError("endpoint returned an unexpected body shape (withheld)");
59
57
  }
60
- const period = parsed.data.config.currentPeriod;
61
- const start = Date.parse(period.start);
62
- const end = Date.parse(period.end);
58
+ const { start, end } = parsed.data.config.currentPeriod;
63
59
  return {
64
60
  at,
65
61
  windows: [
66
62
  {
67
63
  name: null,
68
64
  usedPercentage: parsed.data.config.creditUsagePercent,
69
- resetsAt: Number.isFinite(end) ? end : null,
70
- windowSeconds: Number.isFinite(start) && Number.isFinite(end) && end > start ? Math.round((end - start) / 1000) : null,
65
+ resetsAt: end,
66
+ windowSeconds: end > start ? Math.round((end - start) / 1000) : null,
71
67
  sampledAt: at,
72
68
  },
73
69
  ],
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
  import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.ts";
7
7
  import { writeFileAtomic } from "./atomic.ts";
8
8
  import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
9
- import { resolveRealClaude } from "./claudebin.ts";
9
+ import { CLAUDE_BIN, resolveRealBin } from "./claudebin.ts";
10
10
  import { loadConfig } from "./state.ts";
11
11
  import { ErrnoSchema } from "./types.ts";
12
12
 
@@ -17,7 +17,7 @@ export function isBinDirAhead(): boolean {
17
17
  const ourIdx = dirs.indexOf(paths.binDir);
18
18
  if (ourIdx < 0) return false;
19
19
  try {
20
- const realDir = dirname(resolveRealClaude());
20
+ const realDir = dirname(resolveRealBin(CLAUDE_BIN));
21
21
  const realIdx = dirs.indexOf(realDir);
22
22
  return realIdx < 0 || ourIdx < realIdx;
23
23
  } catch {
@@ -63,7 +63,7 @@ export const opencodeGo: Provider = statusOnlyProvider({
63
63
  liveAuthPath,
64
64
  readAuth,
65
65
  liveId: () => null,
66
- versionOk: (out) => out.trim() !== "",
66
+ versionOk: (out) => out !== "",
67
67
  importIntro: "Pooling your opencode-go API key - your existing opencode auth stays as it is.",
68
68
  foundLive: () => `found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`,
69
69
  installNotice: "opencode-go pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled keys.",
package/src/lib/state.ts CHANGED
@@ -39,7 +39,9 @@ export function loadConfig(): Config {
39
39
  return cfg;
40
40
  }
41
41
 
42
- export function pinBinOverride(input: { key: "claudeBin" | "codexBin" | "grokBin" | "opencodeBin"; bin: string }): void {
42
+ export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "opencodeBin";
43
+
44
+ export function pinBinOverride(input: { key: BinKey; bin: string }): void {
43
45
  let raw: Record<string, unknown> = {};
44
46
  if (existsSync(paths.configJson)) {
45
47
  raw = z.record(z.string(), z.unknown()).parse(JSON.parse(readFileSync(paths.configJson, "utf8")));
@@ -1,8 +1,9 @@
1
- import { existsSync, mkdirSync, rmSync } from "node:fs";
1
+ import { mkdirSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { resolveRealBin, verifyRealBin } from "./claudebin.ts";
3
4
  import type { PoolPaths } from "./paths.ts";
4
5
  import type { Provider, SampleReport } from "./provider.ts";
5
- import { loadConfig, pinBinOverride, type Harvest } from "./state.ts";
6
+ import { pinBinOverride, type Harvest } from "./state.ts";
6
7
  import type { Account } from "./types.ts";
7
8
  import { c } from "../cli/render.ts";
8
9
 
@@ -31,23 +32,7 @@ export type StatusOnlySpec = {
31
32
  };
32
33
 
33
34
  export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
34
- function resolveBin(): string {
35
- const configured = loadConfig()[spec.binKey];
36
- if (configured) {
37
- if (!existsSync(configured)) throw new Error(`configured ${spec.binKey} does not exist: ${configured} - fix config.json`);
38
- return configured;
39
- }
40
- for (const d of (process.env.PATH ?? "").split(":")) {
41
- if (!d) continue;
42
- const cand = join(d, spec.binName);
43
- try {
44
- if (existsSync(cand)) return cand;
45
- } catch {
46
- continue;
47
- }
48
- }
49
- throw new Error(`could not locate the real \`${spec.binName}\` binary (set ${spec.binKey} in config.json)`);
50
- }
35
+ const bin = { name: spec.binName, key: spec.binKey, versionOk: spec.versionOk };
51
36
 
52
37
  async function storeUsable(a: Account): Promise<boolean> {
53
38
  try {
@@ -66,7 +51,7 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
66
51
  }
67
52
 
68
53
  async function login(): Promise<Harvest | null> {
69
- const real = resolveBin();
54
+ const real = resolveRealBin(bin);
70
55
  const onboardDir = spec.onboardDir;
71
56
  rmSync(onboardDir, { recursive: true, force: true });
72
57
  mkdirSync(onboardDir, { recursive: true });
@@ -102,11 +87,9 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
102
87
  }
103
88
 
104
89
  function preflight(): void {
105
- const real = resolveBin();
106
- const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
107
- if (p.exitCode !== 0 || !spec.versionOk(p.stdout.toString())) {
108
- throw new Error(`${spec.binName} binary failed verification: ${real}`);
109
- }
90
+ const real = resolveRealBin(bin);
91
+ const fail = verifyRealBin({ ...bin, bin: real });
92
+ if (fail !== null) throw new Error(`${spec.binName} binary failed verification: ${real}: ${fail}`);
110
93
  pinBinOverride({ key: spec.binKey, bin: real });
111
94
  }
112
95
 
package/src/lib/types.ts CHANGED
@@ -152,6 +152,8 @@ export const RespawnMarkerSchema = z.object({
152
152
 
153
153
  export const EpochSecondsSchema = z.number().transform((seconds) => seconds * 1000);
154
154
 
155
+ export const InstantSchema = z.iso.datetime({ offset: true }).transform((iso) => Date.parse(iso));
156
+
155
157
  const StdinWindowSchema = z.looseObject({ used_percentage: z.number(), resets_at: EpochSecondsSchema.nullable().optional() });
156
158
 
157
159
  export const RateLimitsStdinSchema = z.looseObject({
package/src/lib/usage.ts CHANGED
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { http } from "./http.ts";
4
4
  import { errorMessage, log } from "./log.ts";
5
5
  import { env } from "./paths.ts";
6
- import { EpochSecondsSchema, JsonTextSchema, RateLimitsStdinSchema, type ModelInfo, type UsageWindow, type UsageWindows, type Window } from "./types.ts";
6
+ import { EpochSecondsSchema, InstantSchema, JsonTextSchema, RateLimitsStdinSchema, type ModelInfo, type UsageWindow, type UsageWindows, type Window } from "./types.ts";
7
7
 
8
8
  const win = (w: { used_percentage: number; resets_at?: number | null }): UsageWindow => ({
9
9
  usedPercentage: w.used_percentage,
@@ -185,7 +185,7 @@ export const OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
185
185
  const USAGE_URL = env("TOKENMAXXING_OAUTH_USAGE_URL", OAUTH_USAGE_URL);
186
186
  const USAGE_DEADLINE_MS = 10_000;
187
187
 
188
- const ResetsAtSchema = z.iso.datetime({ offset: true }).transform((iso) => Date.parse(iso)).nullish();
188
+ const ResetsAtSchema = InstantSchema.nullish();
189
189
  const UsageLimitSchema = z.looseObject({ utilization: z.number(), resets_at: ResetsAtSchema });
190
190
  const UsageScopedLimitSchema = z.looseObject({
191
191
  kind: z.string(),
@@ -1,55 +0,0 @@
1
- import { existsSync, statSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, pointsBackAtUs } from "./claudebin.ts";
4
- import { errorMessage } from "./log.ts";
5
- import { loadConfig } from "./state.ts";
6
- import { paths } from "./paths.ts";
7
-
8
- function scanPathForCodex(): string | null {
9
- for (const d of (process.env.PATH ?? "").split(":")) {
10
- if (!d) continue;
11
- const cand = join(d, "codex");
12
- try {
13
- if (existsSync(cand) && statSync(cand).isFile() && !pointsBackAtUs(cand)) return cand;
14
- } catch {
15
- continue;
16
- }
17
- }
18
- return null;
19
- }
20
-
21
- export function resolveRealCodex(): string {
22
- const cfg = loadConfig();
23
- if (cfg.codexBin) {
24
- if (!existsSync(cfg.codexBin)) {
25
- throw new Error(`configured codexBin does not exist: ${cfg.codexBin} - fix config.json`);
26
- }
27
- if (pointsBackAtUs(cfg.codexBin)) {
28
- throw new Error(
29
- `configured codexBin (${cfg.codexBin}) is tokenmaxxing's own wrapper - spawning it recurses. Point codexBin at the real codex binary in ${paths.configJson}`,
30
- );
31
- }
32
- return cfg.codexBin;
33
- }
34
- const scanned = scanPathForCodex();
35
- if (scanned) return scanned;
36
- throw new Error("could not locate the real `codex` binary (set codexBin in config.json)");
37
- }
38
-
39
- export function verifyRealCodex(input: { bin: string }): string | null {
40
- const env = { ...process.env, [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH), TOKENMAXXING_PROBE: "1" };
41
- let p;
42
- try {
43
- p = Bun.spawnSync([input.bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
44
- } catch (e) {
45
- return errorMessage(e);
46
- }
47
- const outText = p.stdout.toString().trim();
48
- const err = p.stderr.toString().trim();
49
- if (p.exitCode === 0) {
50
- if (outText.toLowerCase().includes("codex")) return null;
51
- return `--version output does not identify codex: "${outText.slice(0, 80)}"`;
52
- }
53
- if (err.includes(LOOP_DIAGNOSIS)) return "it leads back into the tokenmaxxing wrapper (recursion)";
54
- return `--version exited ${p.exitCode ?? "on signal/timeout"}: ${(err || outText).slice(0, 160)}`;
55
- }