tokenmaxxing 1.34.0 → 1.38.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.
Files changed (54) hide show
  1. package/DESIGN.md +3 -5
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/cli/add.ts +23 -40
  5. package/src/cli/auth.ts +38 -52
  6. package/src/cli/check.ts +2 -1
  7. package/src/cli/doctor.ts +13 -13
  8. package/src/cli/init.ts +29 -126
  9. package/src/cli/rename.ts +11 -45
  10. package/src/cli/render.ts +0 -7
  11. package/src/cli/rm.ts +24 -39
  12. package/src/cli/setuptoken.ts +16 -16
  13. package/src/cli/status.ts +95 -238
  14. package/src/cli/switch.ts +54 -46
  15. package/src/entries/codexstophook.ts +23 -23
  16. package/src/entries/codexsupervisor.ts +3 -3
  17. package/src/entries/sessionstart.ts +5 -3
  18. package/src/entries/statusline.ts +17 -19
  19. package/src/entries/stopfailurehook.ts +9 -7
  20. package/src/entries/stophook.ts +5 -4
  21. package/src/entries/subagentstatusline.ts +2 -7
  22. package/src/lib/atomic.ts +2 -8
  23. package/src/lib/claude.ts +320 -0
  24. package/src/lib/claudelock.ts +2 -3
  25. package/src/lib/codex.ts +345 -0
  26. package/src/lib/codexauth.ts +9 -7
  27. package/src/lib/codexoauth.ts +5 -11
  28. package/src/lib/codexpresence.ts +3 -19
  29. package/src/lib/codexusage.ts +17 -41
  30. package/src/lib/credstore.ts +3 -7
  31. package/src/lib/decide.ts +86 -174
  32. package/src/lib/http.ts +3 -6
  33. package/src/lib/install.ts +4 -7
  34. package/src/lib/oauth.ts +14 -12
  35. package/src/lib/paths.ts +15 -7
  36. package/src/lib/picker.ts +56 -37
  37. package/src/lib/proc.ts +4 -4
  38. package/src/lib/provider.ts +34 -0
  39. package/src/lib/sample.ts +33 -33
  40. package/src/lib/state.ts +51 -18
  41. package/src/lib/swap.ts +41 -38
  42. package/src/lib/types.ts +31 -51
  43. package/src/lib/usage.ts +28 -20
  44. package/src/main.ts +20 -20
  45. package/src/cli/codexadd.ts +0 -89
  46. package/src/cli/codexinit.ts +0 -120
  47. package/src/cli/codexrm.ts +0 -36
  48. package/src/cli/codexswitch.ts +0 -113
  49. package/src/cli/onboard.ts +0 -92
  50. package/src/lib/codexdecide.ts +0 -157
  51. package/src/lib/codexpick.ts +0 -80
  52. package/src/lib/codexsample.ts +0 -48
  53. package/src/lib/codexstate.ts +0 -22
  54. package/src/lib/codexswap.ts +0 -70
@@ -0,0 +1,320 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { readItem, writeItem, deleteItem, liveTarget, parkedTarget, isolatedTarget, claudeAiOauthOnly, mergeIntoLive } from "./credstore.ts";
5
+ import { resolveRealClaude, resolveVerifiedClaude } from "./claudebin.ts";
6
+ import { withClaudeRefreshLock } from "./claudelock.ts";
7
+ import { isApiKeyMode, readOAuthAccount } from "./claudejson.ts";
8
+ import { ensurePathInRc, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
9
+ import { withLock } from "./lock.ts";
10
+ import { log } from "./log.ts";
11
+ import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isAccessTokenExpiring, isDeadCredential, refreshCredential, InvalidGrantError } from "./oauth.ts";
12
+ import { claudePool, credItemFor, paths } from "./paths.ts";
13
+ import type { Observation, Provider, SampleReport } from "./provider.ts";
14
+ import { ensureLiveTokenFresh, probeActiveUsage, probeParkedUsage } from "./sample.ts";
15
+ import { loadAccounts, loadUsage, loadUsageSnapshot, pinBinOverride, saveAccounts, writeUsage, type Harvest } from "./state.ts";
16
+ import { isSkippableSwapError, performSwap } from "./swap.ts";
17
+ import { loadSetupTokens, saveSetupTokens } from "./setuptokens.ts";
18
+ import { saveTermios, restoreTermios } from "./tty.ts";
19
+ import { CRED_ENV_OVERRIDES, familyTokens, gatedFamilies, mergeWindows, probeUsage, windowsOf } from "./usage.ts";
20
+ import { CredentialBlobSchema, OAuthAccountSchema, type Account, type Config, type UsageState } from "./types.ts";
21
+ import { c } from "../cli/render.ts";
22
+
23
+ function teeObservation(account: Account, snap: { state: UsageState; at: number } | null): Observation | null {
24
+ if (!snap || snap.state.account !== account.id) return null;
25
+ if (account.lastUsageAt != null && snap.at < account.lastUsageAt) return { windows: account.windows, at: account.lastUsageAt };
26
+ const at = snap.state.sampledAt ?? snap.at;
27
+ const aggregate = windowsOf({ fiveHour: snap.state.fiveHour, sevenDay: snap.state.sevenDay, perModel: {} }, at);
28
+ return { windows: mergeWindows(aggregate, account.windows), at };
29
+ }
30
+
31
+ async function observeLive(account: Account, cfg: Config, now: number, opts: { probe: boolean }): Promise<Observation | null> {
32
+ let snap = loadUsageSnapshot();
33
+ const ttl = cfg.policy.usagePollTtlMs;
34
+ const probeAttempted = account.lastProbeAt != null && now - account.lastProbeAt <= ttl;
35
+ const fresh = snap != null && snap.state.account === account.id && now - snap.at <= ttl;
36
+ const needsPerModel = snap != null && gatedFamilies(snap.state.model, cfg.policy.switchModels).length > 0;
37
+ if (opts.probe && !probeAttempted && (!fresh || needsPerModel)) {
38
+ const startedAt = Date.now();
39
+ const full = await probeUsage();
40
+ const ts = Date.now();
41
+ if (readOAuthAccount()?.accountUuid === account.id) {
42
+ if (full) {
43
+ const teed = loadUsageSnapshot();
44
+ if (teed && teed.state.account === account.id && ts - teed.at <= ttl) {
45
+ snap = teed;
46
+ } else {
47
+ const state: UsageState = { fiveHour: full.fiveHour, sevenDay: full.sevenDay, account: account.id, ts, model: null };
48
+ writeUsage(state);
49
+ snap = { state, at: ts };
50
+ }
51
+ const expected = gatedFamilies(snap.state.model, cfg.policy.switchModels);
52
+ const rows = Object.keys(full.perModel);
53
+ if (expected.length > 0 && !expected.some((f) => rows.some((k) => familyTokens(k).includes(f)))) {
54
+ log("usage.no_permodel_row", { families: expected.join(","), rows: rows.join(",") });
55
+ }
56
+ }
57
+ await withLock(claudePool.lockFile, () => {
58
+ const idx = loadAccounts(claudePool);
59
+ const a = idx.accounts.find((x) => x.id === account.id);
60
+ if (!a) return;
61
+ a.lastProbeAt = startedAt;
62
+ if (full) {
63
+ const probed = windowsOf(full, startedAt);
64
+ const aggregate = a.windows.some((w) => w.name == null) ? a.windows.filter((w) => w.name == null) : probed.filter((w) => w.name == null);
65
+ if (a.lastUsageAt == null) a.lastUsageAt = startedAt;
66
+ a.windows = mergeWindows([...aggregate, ...probed.filter((w) => w.name != null)], a.windows);
67
+ }
68
+ saveAccounts(claudePool, idx);
69
+ });
70
+ }
71
+ }
72
+ const current = loadAccounts(claudePool).accounts.find((a) => a.id === account.id) ?? account;
73
+ return teeObservation(current, snap);
74
+ }
75
+
76
+ async function samplePool(accounts: Account[], liveId: string | null): Promise<Map<string, SampleReport>> {
77
+ const reports = new Map<string, SampleReport>();
78
+ const tee = loadUsageSnapshot();
79
+ const probeOne = async (a: Account): Promise<void> => {
80
+ const isActive = liveId != null && liveId === a.id;
81
+ const teeCurrent = tee != null && (a.lastUsageAt == null || tee.at >= a.lastUsageAt);
82
+ if (isActive && tee && teeCurrent && tee.state.account === a.id) {
83
+ const at = tee.state.sampledAt ?? tee.at;
84
+ a.lastUsageAt = at;
85
+ a.windows = mergeWindows(windowsOf({ fiveHour: tee.state.fiveHour, sevenDay: tee.state.sevenDay, perModel: {} }, at), a.windows);
86
+ reports.set(a.id, { ok: true, source: "statusline" });
87
+ return;
88
+ }
89
+ const outcome = isActive ? await probeActiveUsage(a) : await probeParkedUsage(a);
90
+ if (!outcome.ok) {
91
+ reports.set(a.id, { ok: false, reason: outcome.reason });
92
+ return;
93
+ }
94
+ const at = Date.now();
95
+ a.lastUsageAt = at;
96
+ a.windows = mergeWindows(windowsOf(outcome.usage, at), a.windows);
97
+ reports.set(a.id, { ok: true, source: "probe" });
98
+ };
99
+ const active = accounts.find((a) => liveId != null && liveId === a.id) ?? null;
100
+ if (active) await probeOne(active);
101
+ try {
102
+ await ensureLiveTokenFresh();
103
+ } catch {
104
+ }
105
+ await Promise.all(accounts.filter((a) => a !== active).map(probeOne));
106
+ return reports;
107
+ }
108
+
109
+ async function liveOwner(): Promise<string | null> {
110
+ const live = await readItem(liveTarget());
111
+ if (live == null) return null;
112
+ const creds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
113
+ return (await fetchTokenIdentity(creds.accessToken)).accountUuid;
114
+ }
115
+
116
+ async function removeCredentials(a: Account): Promise<void> {
117
+ const setupTokens = loadSetupTokens();
118
+ const item = credItemFor(a.id);
119
+ await deleteItem(parkedTarget(item));
120
+ for (const sampleDir of [join(paths.sampleDir, item), join(paths.sampleDir, `${item}-tick`)]) {
121
+ await deleteItem(isolatedTarget(sampleDir));
122
+ rmSync(sampleDir, { recursive: true, force: true });
123
+ }
124
+ if (setupTokens.tokens.some((t) => t.accountUuid === a.id)) {
125
+ saveSetupTokens({ ...setupTokens, tokens: setupTokens.tokens.filter((t) => t.accountUuid !== a.id) });
126
+ }
127
+ }
128
+
129
+ function identityReady(cjPath: string): boolean {
130
+ if (!existsSync(cjPath)) return false;
131
+ try {
132
+ const oauthAccount = JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount;
133
+ return z.object({ accountUuid: z.string().min(1) }).safeParse(oauthAccount).success;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ async function login(): Promise<Harvest | null> {
140
+ const onboardDir = paths.onboardDir;
141
+ rmSync(onboardDir, { recursive: true, force: true });
142
+ mkdirSync(onboardDir, { recursive: true });
143
+ const iso = isolatedTarget(onboardDir);
144
+ await deleteItem(iso);
145
+ const cjPath = join(onboardDir, ".claude.json");
146
+ const real = resolveRealClaude();
147
+
148
+ const savedTermios = saveTermios();
149
+ const env: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
150
+ for (const key of CRED_ENV_OVERRIDES) delete env[key];
151
+ const p = Bun.spawn([real], {
152
+ stdin: "inherit",
153
+ stdout: "inherit",
154
+ stderr: "inherit",
155
+ env,
156
+ });
157
+
158
+ try {
159
+ let exited = false;
160
+ const onExit = p.exited.then(() => { exited = true; });
161
+ while (!exited) {
162
+ await Bun.sleep(400);
163
+ if (identityReady(cjPath) && (await readItem(iso))) {
164
+ p.kill();
165
+ break;
166
+ }
167
+ }
168
+ await p.exited;
169
+ await onExit;
170
+ restoreTermios(savedTermios);
171
+
172
+ const blobRaw = await readItem(iso);
173
+ if (!blobRaw || !identityReady(cjPath)) {
174
+ console.error(c.red("no login detected in the isolated session - nothing changed."));
175
+ return null;
176
+ }
177
+
178
+ let blob, oauthAccount;
179
+ try {
180
+ blob = CredentialBlobSchema.parse(JSON.parse(blobRaw));
181
+ oauthAccount = OAuthAccountSchema.parse(JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount);
182
+ } catch {
183
+ console.error(c.red("could not parse the onboarded account's credential/identity."));
184
+ return null;
185
+ }
186
+
187
+ console.log(c.dim("sampling usage..."));
188
+ const sampled = await probeUsage(onboardDir);
189
+ if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
190
+ const at = Date.now();
191
+ const id = oauthAccount.accountUuid;
192
+ return {
193
+ id,
194
+ email: oauthAccount.emailAddress,
195
+ tier: claudeTierLabel(blob.claudeAiOauth),
196
+ oauthAccount,
197
+ sample: sampled ? { windows: windowsOf(sampled, at), at } : null,
198
+ park: () => writeItem(parkedTarget(credItemFor(id)), claudeAiOauthOnly(blobRaw)),
199
+ };
200
+ } finally {
201
+ if (p.exitCode === null) {
202
+ p.kill();
203
+ await p.exited;
204
+ }
205
+ restoreTermios(savedTermios);
206
+ await deleteItem(iso);
207
+ rmSync(onboardDir, { recursive: true, force: true });
208
+ }
209
+ }
210
+
211
+ async function importLive(): Promise<Harvest | null> {
212
+ if (isApiKeyMode()) {
213
+ console.error(c.yellow("tokenmaxxing pools subscription accounts, but you're authed via API key / apiKeyHelper."));
214
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} with a Pro/Max account first, then re-run ${c.cyan("tokenmaxxing init")}.`);
215
+ return null;
216
+ }
217
+
218
+ const oauthAccount = readOAuthAccount();
219
+ const liveRaw = await readItem(liveTarget());
220
+ if (!oauthAccount || !liveRaw) {
221
+ console.error(c.red("no active Claude subscription login found (missing oauthAccount or credential)."));
222
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
223
+ return null;
224
+ }
225
+
226
+ let blob;
227
+ try {
228
+ blob = CredentialBlobSchema.parse(JSON.parse(liveRaw));
229
+ } catch {
230
+ console.error(c.red("the live credential is not a recognizable Claude OAuth blob."));
231
+ return null;
232
+ }
233
+
234
+ let creds = blob.claudeAiOauth;
235
+ if (isDeadCredential(creds)) {
236
+ console.error(c.red("the live credential was cleared after a failed refresh."));
237
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
238
+ return null;
239
+ }
240
+ if (isAccessTokenExpiring(creds)) {
241
+ await withClaudeRefreshLock(async (lock) => {
242
+ const raw2 = await readItem(liveTarget());
243
+ if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
244
+ const current = CredentialBlobSchema.parse(JSON.parse(raw2)).claudeAiOauth;
245
+ creds = isAccessTokenExpiring(current) ? await refreshCredential(current) : current;
246
+ if (creds === current) return;
247
+ if (lock.compromised()) throw new Error("refresh lock compromised mid-refresh - discarding the live rewrite");
248
+ await writeItem(liveTarget(), mergeIntoLive(raw2, creds));
249
+ });
250
+ }
251
+ const identity = await fetchTokenIdentity(creds.accessToken);
252
+ if (identity.accountUuid !== oauthAccount.accountUuid) {
253
+ console.error(c.red(`the live credential belongs to ${describeIdentity(identity)}, but ~/.claude.json identifies ${oauthAccount.emailAddress} - identity drift.`));
254
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} to realign them, then re-run ${c.cyan("tokenmaxxing init")}.`);
255
+ return null;
256
+ }
257
+
258
+ const id = oauthAccount.accountUuid;
259
+ const parked = JSON.stringify({ claudeAiOauth: creds });
260
+ return {
261
+ id,
262
+ email: oauthAccount.emailAddress,
263
+ tier: claudeTierLabel(creds),
264
+ oauthAccount,
265
+ sample: null,
266
+ park: () => writeItem(parkedTarget(credItemFor(id)), parked),
267
+ };
268
+ }
269
+
270
+ function ensurePathAhead(): void {
271
+ const rc = shellRcPath();
272
+ if (!rc) {
273
+ console.log(c.yellow(`⚠ add to your shell rc: export PATH="${paths.binDir}:$PATH"`));
274
+ return;
275
+ }
276
+ const outcome = ensurePathInRc(rc);
277
+ if (outcome === "added") console.log(`${c.green("✓")} added ${paths.binDir} to PATH in ${rc} - restart your shell (or \`source ${rc}\`)`);
278
+ else if (outcome === "skipped") {
279
+ const hint = managedShellRcSkipLines();
280
+ console.log(c.yellow(`⚠ ${hint.headline}`));
281
+ console.log(c.yellow(` ${hint.detail}`));
282
+ console.log(c.yellow(` ${hint.exportLine}`));
283
+ } else console.log(c.yellow(`⚠ PATH line already in ${rc} - restart your shell to pick it up`));
284
+ }
285
+
286
+ function install(): void {
287
+ const out = installSupervisor();
288
+ console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/SessionStart hooks`);
289
+ if (out.timerLoaded) console.log(`${c.green("✓")} periodic check timer active (every ${out.checkIntervalS}s)`);
290
+ else console.log(c.yellow(`⚠ check timer written but not activated - run: ${timerActivationHint()}`));
291
+ if (!out.pathAhead) {
292
+ console.log();
293
+ ensurePathAhead();
294
+ }
295
+ }
296
+
297
+ export const claude: Provider = {
298
+ name: "claude",
299
+ flag: "",
300
+ pool: claudePool,
301
+ waitsWhenDepleted: true,
302
+ switchMargin: 1,
303
+ switchNote: "",
304
+ liveId: () => readOAuthAccount()?.accountUuid ?? null,
305
+ liveOwner,
306
+ presentIds: () => new Set(),
307
+ gatedFamilies: (cfg) => gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels),
308
+ observeLive,
309
+ samplePool,
310
+ mergeWindows,
311
+ swap: performSwap,
312
+ classifySwapError: (e) => (e instanceof InvalidGrantError ? "dead-grant" : isSkippableSwapError(e) ? "skip" : "fatal"),
313
+ removeCredentials,
314
+ login,
315
+ importLive,
316
+ preflight: () => pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() }),
317
+ install,
318
+ loginStep: (who) => `In the session that opens, run ${c.bold("/login")} with ${who}. It closes itself once you're in.`,
319
+ windowLabel: (name) => name.toLowerCase(),
320
+ };
@@ -1,9 +1,9 @@
1
1
  import { mkdirSync, realpathSync, rmdirSync, statSync, utimesSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { delay } from "es-toolkit";
4
- import { z } from "zod";
5
4
  import { credDir } from "./paths.ts";
6
5
  import { log } from "./log.ts";
6
+ import { ErrnoSchema } from "./types.ts";
7
7
 
8
8
  const STALE_MS = 60_000;
9
9
  const HEARTBEAT_MS = 5_000;
@@ -15,8 +15,7 @@ function tryAcquire(lockDir: string): boolean {
15
15
  mkdirSync(lockDir);
16
16
  return true;
17
17
  } catch (e) {
18
- const errno = z.object({ code: z.string() }).safeParse(e);
19
- if (!errno.success || errno.data.code !== "EEXIST") throw e;
18
+ if (ErrnoSchema.safeParse(e).data?.code !== "EEXIST") throw e;
20
19
  }
21
20
  try {
22
21
  if (Date.now() - statSync(lockDir).mtimeMs > STALE_MS) {
@@ -0,0 +1,345 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { writeFileAtomic } from "./atomic.ts";
4
+ import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "./claudebin.ts";
5
+ import { codexIdentityOf, deleteParkedCodexAuth, isCodexAccessExpiring, liveCodexAccountId, readCodexAuthAt, readLiveCodexAuth, readParkedCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
6
+ import { resolveRealCodex, verifyRealCodex } from "./codexbin.ts";
7
+ import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
8
+ import { livingCodexPresences, presentCodexAccountIds } from "./codexpresence.ts";
9
+ import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
10
+ import { codexSupervisorLink, ensurePathInRc, installCodexSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
11
+ import { withLock } from "./lock.ts";
12
+ import { log } from "./log.ts";
13
+ import { codexCredItemFor, codexPaths, codexPool } from "./paths.ts";
14
+ import { isExhausted, thresholdBars, type PickCtx } from "./picker.ts";
15
+ import type { Observation, Provider, SampleReport } from "./provider.ts";
16
+ import { loadAccounts, loadConfig, pinBinOverride, saveAccounts, saveLastSwapAt, type Harvest } from "./state.ts";
17
+ import { restoreTermios, saveTermios } from "./tty.ts";
18
+ import { CodexReconcileMarkerSchema, type Account, type CodexAuthJson, type CodexUsage, type Config } from "./types.ts";
19
+ import { c } from "../cli/render.ts";
20
+
21
+ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
22
+ account.windows = usage.windows;
23
+ account.lastUsageAt = at;
24
+ if (usage.email != null) account.email = usage.email;
25
+ if (usage.planType != null) account.tier = usage.planType;
26
+ }
27
+
28
+ async function sampleLiveOntoOwner(now: number): Promise<void> {
29
+ let live = readLiveCodexAuth();
30
+ if (!live) return;
31
+ const idx = loadAccounts(codexPool);
32
+ const identity = codexIdentityOf({ auth: live });
33
+ const owner = idx.accounts.find((a) => a.id === identity.accountId);
34
+ if (!owner) return;
35
+
36
+ if (isCodexAccessExpiring({ auth: live, now }) && !presentCodexAccountIds().has(identity.accountId)) {
37
+ try {
38
+ live = await refreshCodexAuth({ auth: live, now });
39
+ } catch (e) {
40
+ if (e instanceof CodexInvalidGrantError) {
41
+ owner.needsReauth = true;
42
+ saveAccounts(codexPool, idx);
43
+ log("codex.live_invalid_grant", { account: owner.id.slice(0, 8) });
44
+ return;
45
+ }
46
+ throw e;
47
+ }
48
+ writeLiveCodexAuth({ auth: live });
49
+ writeParkedCodexAuth({ credFile: codexCredItemFor(owner.id), auth: live });
50
+ }
51
+ applyUsage(owner, await fetchCodexUsage({ auth: live, at: now }), now);
52
+ saveAccounts(codexPool, idx);
53
+ }
54
+
55
+ async function observeLive(account: Account, cfg: Config, now: number, opts: { probe: boolean }): Promise<Observation | null> {
56
+ if (opts.probe && (account.lastUsageAt == null || now - account.lastUsageAt > cfg.policy.usagePollTtlMs)) {
57
+ await withLock(codexPool.lockFile, () => sampleLiveOntoOwner(now));
58
+ }
59
+ const fresh = loadAccounts(codexPool).accounts.find((a) => a.id === account.id);
60
+ return fresh?.lastUsageAt != null ? { windows: fresh.windows, at: fresh.lastUsageAt } : null;
61
+ }
62
+
63
+ type CodexSampleOutcome = { ok: true; usage: CodexUsage; at: number } | { ok: false; reason: string; deadGrant: boolean };
64
+
65
+ async function sampleAccount(account: Account, liveId: string | null, now: number): Promise<CodexSampleOutcome> {
66
+ const isLive = liveId != null && account.id === liveId;
67
+ const credFile = codexCredItemFor(account.id);
68
+ try {
69
+ let auth = isLive ? readLiveCodexAuth() : readParkedCodexAuth({ credFile });
70
+ if (!auth) return { ok: false, reason: isLive ? "live auth.json vanished" : "no parked credential", deadGrant: false };
71
+ if (isCodexAccessExpiring({ auth, now })) {
72
+ const running = presentCodexAccountIds().has(account.id);
73
+ if (running && !isLive) {
74
+ return { ok: false, reason: "running in a live codex session (parked token refresh unsafe)", deadGrant: false };
75
+ }
76
+ if (!running) {
77
+ auth = await refreshCodexAuth({ auth, now });
78
+ if (isLive) writeLiveCodexAuth({ auth });
79
+ writeParkedCodexAuth({ credFile, auth });
80
+ }
81
+ }
82
+ const at = Date.now();
83
+ return { ok: true, usage: await fetchCodexUsage({ auth, at }), at };
84
+ } catch (e) {
85
+ if (e instanceof CodexInvalidGrantError) return { ok: false, reason: e.message, deadGrant: true };
86
+ if (e instanceof CodexRefreshFailedError || e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false };
87
+ throw e;
88
+ }
89
+ }
90
+
91
+ async function samplePool(accounts: Account[], liveId: string | null, now: number): Promise<Map<string, SampleReport>> {
92
+ const reports = new Map<string, SampleReport>();
93
+ await Promise.all(
94
+ accounts.map(async (account) => {
95
+ const outcome = await sampleAccount(account, liveId, now);
96
+ if (outcome.ok) {
97
+ applyUsage(account, outcome.usage, outcome.at);
98
+ reports.set(account.id, { ok: true, source: "probe" });
99
+ } else {
100
+ if (outcome.deadGrant) account.needsReauth = true;
101
+ reports.set(account.id, { ok: false, reason: outcome.reason });
102
+ }
103
+ }),
104
+ );
105
+ return reports;
106
+ }
107
+
108
+ async function swap(target: Account): Promise<void> {
109
+ const idx = loadAccounts(codexPool);
110
+ const live = readLiveCodexAuth();
111
+ let liveOwner: Account | null = null;
112
+ if (live) {
113
+ const liveIdentity = codexIdentityOf({ auth: live });
114
+ liveOwner = idx.accounts.find((a) => a.id === liveIdentity.accountId) ?? null;
115
+ if (!liveOwner) {
116
+ throw new Error(
117
+ `live codex credential belongs to ${liveIdentity.email ?? liveIdentity.accountId.slice(0, 8)}, which is not in the pool - refusing to swap over it; import it first with \`tokenmaxxing add --codex\``,
118
+ );
119
+ }
120
+ if (liveOwner.id !== idx.activeId) {
121
+ log("codexswap.harvest_drift", { labeled: idx.activeId?.slice(0, 8) ?? null, actual: liveOwner.id.slice(0, 8) });
122
+ }
123
+ }
124
+ const commit = (): void => {
125
+ idx.activeId = target.id;
126
+ const entry = idx.accounts.find((a) => a.id === target.id);
127
+ if (entry) entry.needsReauth = false;
128
+ saveAccounts(codexPool, idx);
129
+ };
130
+
131
+ if (live && liveOwner && liveOwner.id === target.id) {
132
+ writeParkedCodexAuth({ credFile: codexCredItemFor(target.id), auth: live });
133
+ commit();
134
+ log("codexswap.reconciled", { account: target.id.slice(0, 8) });
135
+ return;
136
+ }
137
+
138
+ const parked = readParkedCodexAuth({ credFile: codexCredItemFor(target.id) });
139
+ if (!parked) throw new Error(`no parked codex credential for ${target.label}`);
140
+
141
+ let fresh: CodexAuthJson;
142
+ try {
143
+ fresh = await refreshCodexAuth({ auth: parked });
144
+ } catch (e) {
145
+ if (e instanceof CodexInvalidGrantError) {
146
+ const entry = idx.accounts.find((a) => a.id === target.id);
147
+ if (entry) {
148
+ entry.needsReauth = true;
149
+ saveAccounts(codexPool, idx);
150
+ }
151
+ log("codexswap.invalid_grant", { account: target.id.slice(0, 8) });
152
+ }
153
+ throw e;
154
+ }
155
+ writeParkedCodexAuth({ credFile: codexCredItemFor(target.id), auth: fresh });
156
+
157
+ if (live && liveOwner) {
158
+ const liveNow = readLiveCodexAuth();
159
+ if (!liveNow || codexIdentityOf({ auth: liveNow }).accountId !== liveOwner.id) {
160
+ throw new Error("live codex credential changed mid-swap - refusing to harvest under a stale identity; retry");
161
+ }
162
+ writeParkedCodexAuth({ credFile: codexCredItemFor(liveOwner.id), auth: liveNow });
163
+ log("codexswap.harvest", { account: liveOwner.id.slice(0, 8) });
164
+ }
165
+
166
+ writeLiveCodexAuth({ auth: fresh });
167
+ commit();
168
+ saveLastSwapAt(codexPool, Date.now());
169
+ log("codexswap.done", { account: target.id.slice(0, 8), label: target.label });
170
+ }
171
+
172
+ async function login(): Promise<Harvest | null> {
173
+ const real = resolveRealCodex();
174
+ const onboardDir = codexPaths.onboardDir;
175
+ rmSync(onboardDir, { recursive: true, force: true });
176
+ mkdirSync(onboardDir, { recursive: true });
177
+ writeFileAtomic(join(onboardDir, "config.toml"), 'cli_auth_credentials_store = "file"\n');
178
+
179
+ const savedTermios = saveTermios();
180
+ const p = Bun.spawn([real, "login", "--device-auth"], {
181
+ stdin: "inherit",
182
+ stdout: "inherit",
183
+ stderr: "inherit",
184
+ env: {
185
+ ...process.env,
186
+ CODEX_HOME: onboardDir,
187
+ TOKENMAXXING_PROBE: "1",
188
+ [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH),
189
+ },
190
+ });
191
+ await p.exited;
192
+ restoreTermios(savedTermios);
193
+
194
+ try {
195
+ const auth = readCodexAuthAt({ path: join(onboardDir, "auth.json") });
196
+ if (p.exitCode !== 0 || !auth) {
197
+ console.error(c.red("no codex login landed in the isolated home - nothing added."));
198
+ return null;
199
+ }
200
+ const identity = codexIdentityOf({ auth });
201
+ const usage = await sampleLogin(auth);
202
+ return {
203
+ id: identity.accountId,
204
+ email: usage?.usage.email ?? identity.email,
205
+ tier: usage?.usage.planType ?? identity.planType,
206
+ sample: usage ? { windows: usage.usage.windows, at: usage.at } : null,
207
+ park: async () => writeParkedCodexAuth({ credFile: codexCredItemFor(identity.accountId), auth }),
208
+ };
209
+ } finally {
210
+ rmSync(onboardDir, { recursive: true, force: true });
211
+ }
212
+ }
213
+
214
+ async function sampleLogin(auth: CodexAuthJson): Promise<{ usage: CodexUsage; at: number } | null> {
215
+ console.log(c.dim("sampling usage..."));
216
+ const at = Date.now();
217
+ try {
218
+ return { usage: await fetchCodexUsage({ auth, at }), at };
219
+ } catch (e) {
220
+ if (!(e instanceof CodexUsageReadError)) throw e;
221
+ console.log(c.yellow("could not sample usage now - it will fill in on first use."));
222
+ return null;
223
+ }
224
+ }
225
+
226
+ async function importLive(): Promise<Harvest | null> {
227
+ const live = readLiveCodexAuth();
228
+ if (!live) {
229
+ console.error(c.red(`no codex login found at ${codexPaths.authJson} - run \`codex login\` first, then re-run this.`));
230
+ return null;
231
+ }
232
+ const identity = codexIdentityOf({ auth: live });
233
+ const usage = await sampleLogin(live);
234
+
235
+ if (presentCodexAccountIds().has(identity.accountId)) {
236
+ console.error(c.red("a live supervised codex session is running this account - its token rotates under us, so parking a snapshot now could poison the backup."));
237
+ console.error(c.dim("close that codex session (or let it exit) and re-run `tokenmaxxing init --codex`."));
238
+ return null;
239
+ }
240
+
241
+ return {
242
+ id: identity.accountId,
243
+ email: usage?.usage.email ?? identity.email,
244
+ tier: usage?.usage.planType ?? identity.planType,
245
+ sample: usage ? { windows: usage.usage.windows, at: usage.at } : null,
246
+ park: async () => {
247
+ if (presentCodexAccountIds().has(identity.accountId)) {
248
+ throw new Error("a live supervised codex session started running this account mid-init - close it and re-run `tokenmaxxing init --codex`");
249
+ }
250
+ const fresh2 = readLiveCodexAuth();
251
+ if (!fresh2 || codexIdentityOf({ auth: fresh2 }).accountId !== identity.accountId) {
252
+ throw new Error("the live codex login changed while init was running (a concurrent swap?) - re-run `tokenmaxxing init --codex`");
253
+ }
254
+ writeParkedCodexAuth({ credFile: codexCredItemFor(identity.accountId), auth: fresh2 });
255
+ },
256
+ };
257
+ }
258
+
259
+ function storePinnedAwayFromFile(): boolean {
260
+ const configToml = `${codexPaths.home}/config.toml`;
261
+ if (!existsSync(configToml)) return false;
262
+ const config = Bun.TOML.parse(readFileSync(configToml, "utf8"));
263
+ return "cli_auth_credentials_store" in config && config.cli_auth_credentials_store !== "file";
264
+ }
265
+
266
+ function preflight(): void {
267
+ const real = resolveRealCodex();
268
+ const fail = verifyRealCodex({ bin: real });
269
+ if (fail !== null) throw new Error(`codex binary failed verification: ${real}: ${fail}`);
270
+ pinBinOverride({ key: "codexBin", bin: real });
271
+ if (storePinnedAwayFromFile()) {
272
+ throw new Error(
273
+ 'codex config.toml pins cli_auth_credentials_store away from the plain auth.json file tokenmaxxing swaps - set cli_auth_credentials_store = "file" in ~/.codex/config.toml, run `codex login`, then re-run this.',
274
+ );
275
+ }
276
+ }
277
+
278
+ function install(): void {
279
+ installCodexSupervisor();
280
+ const rc = shellRcPath();
281
+ if (rc && ensurePathInRc(rc) === "skipped") {
282
+ const hint = managedShellRcSkipLines();
283
+ console.log(c.yellow(`⚠ ${hint.headline}`));
284
+ console.log(c.yellow(` ${hint.detail}`));
285
+ console.log(c.yellow(` ${hint.exportLine}`));
286
+ }
287
+ console.log(`${c.green("✓")} codex supervisor installed at ${codexSupervisorLink()}`);
288
+ console.log(`${c.green("✓")} Stop hook declared in ${codexPaths.hooksJson}`);
289
+ console.log();
290
+ console.log(c.bold(c.yellow("one manual step: codex skips untrusted hooks.")));
291
+ console.log(c.yellow("open codex, run /hooks, and trust the tokenmaxxing Stop hook - auto-switching is inert until then."));
292
+ }
293
+
294
+ export function codexPickCtx(now: number, currentId: string | null): PickCtx {
295
+ return { now, thresholds: thresholdBars(loadConfig()), currentId, families: null };
296
+ }
297
+
298
+ export function reconcileSiblings(now: number): void {
299
+ const liveId = liveCodexAccountId();
300
+ if (liveId == null) return;
301
+ const idx = loadAccounts(codexPool);
302
+ const liveAccount = idx.accounts.find((a) => a.id === liveId);
303
+ if (!liveAccount || liveAccount.needsReauth === true || isExhausted(liveAccount, codexPickCtx(now, liveId))) return;
304
+ const living = livingCodexPresences();
305
+ if (existsSync(codexPaths.reconcileDir)) {
306
+ const alive = new Set(living.map((presence) => presence.supervisorId));
307
+ for (const name of readdirSync(codexPaths.reconcileDir)) {
308
+ if (!alive.has(name)) rmSync(join(codexPaths.reconcileDir, name), { force: true });
309
+ }
310
+ }
311
+ for (const presence of living) {
312
+ if (presence.accountId === liveId) continue;
313
+ if (!idx.accounts.some((a) => a.id === presence.accountId)) continue;
314
+ const markerPath = join(codexPaths.reconcileDir, presence.supervisorId);
315
+ if (existsSync(markerPath)) continue;
316
+ mkdirSync(codexPaths.reconcileDir, { recursive: true });
317
+ writeFileAtomic(markerPath, JSON.stringify(CodexReconcileMarkerSchema.parse({ accountId: presence.accountId, ts: now })));
318
+ log("codex.reconcile_signal", { supervisorId: presence.supervisorId.slice(0, 8), account: presence.accountId.slice(0, 8) });
319
+ }
320
+ }
321
+
322
+ export const codex: Provider = {
323
+ name: "codex",
324
+ flag: " --codex",
325
+ pool: codexPool,
326
+ waitsWhenDepleted: false,
327
+ switchMargin: 1.2,
328
+ switchNote: " (takes effect on the next codex start)",
329
+ liveId: liveCodexAccountId,
330
+ liveOwner: async () => liveCodexAccountId(),
331
+ presentIds: presentCodexAccountIds,
332
+ gatedFamilies: () => null,
333
+ observeLive,
334
+ samplePool,
335
+ mergeWindows: (next) => next,
336
+ swap,
337
+ classifySwapError: (e) => (e instanceof CodexInvalidGrantError ? "dead-grant" : "fatal"),
338
+ removeCredentials: async (a) => deleteParkedCodexAuth({ credFile: codexCredItemFor(a.id) }),
339
+ login,
340
+ importLive,
341
+ preflight,
342
+ install,
343
+ loginStep: (who) => `Open the URL codex prints, enter the code, and sign in with ${who}; the command exits once you're in.`,
344
+ windowLabel: codexLimitLabel,
345
+ };