tokenmaxxing 1.43.0 → 1.45.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/src/lib/codex.ts CHANGED
@@ -1,23 +1,38 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { writeFileAtomic } from "./atomic.ts";
4
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";
5
+ import { codexIdentityOf, deleteCodexStoreAuth, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
6
6
  import { resolveRealCodex, verifyRealCodex } from "./codexbin.ts";
7
7
  import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
8
- import { livingPresences, seatCounts } from "./presence.ts";
8
+ import { seatCounts } from "./presence.ts";
9
9
  import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
10
10
  import { codexSupervisorLink, ensurePathInRc, installCodexSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
11
11
  import { withLock } from "./lock.ts";
12
12
  import { log } from "./log.ts";
13
- import { codexCredItemFor, codexPaths, codexPool } from "./paths.ts";
14
- import { isExhausted, thresholdBars, type PickCtx } from "./picker.ts";
13
+ import { codexPaths, codexPool, codexSeatFromEnv } from "./paths.ts";
14
+ import { isExhausted, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
15
15
  import type { Observation, Provider, SampleReport } from "./provider.ts";
16
- import { loadAccounts, loadConfig, pinBinOverride, saveAccounts, saveLastSwapAt, type Harvest } from "./state.ts";
16
+ import { loadAccounts, loadConfig, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
17
17
  import { restoreTermios, saveTermios } from "./tty.ts";
18
- import { CodexReconcileMarkerSchema, type Account, type CodexAuthJson, type CodexUsage, type Config } from "./types.ts";
18
+ import type { Account, CodexAuthJson, CodexUsage, Config } from "./types.ts";
19
19
  import { c } from "../cli/render.ts";
20
20
 
21
+ class StoreUnusableError extends Error {
22
+ constructor(message: string) {
23
+ super(message);
24
+ this.name = "StoreUnusableError";
25
+ }
26
+ }
27
+
28
+ function liveId(): string | null {
29
+ return codexSeatFromEnv(loadAccounts(codexPool).accounts.map((a) => a.id));
30
+ }
31
+
32
+ function presence(): Map<string, number> {
33
+ return seatCounts(codexPaths.presenceDir);
34
+ }
35
+
21
36
  function applyUsage(account: Account, usage: CodexUsage, at: number): void {
22
37
  account.windows = usage.windows;
23
38
  account.lastUsageAt = at;
@@ -25,59 +40,23 @@ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
25
40
  if (usage.planType != null) account.tier = usage.planType;
26
41
  }
27
42
 
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 }) && !seatCounts(codexPaths.presenceDir).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
- }
43
+ type CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | { ok: false; reason: string; deadGrant: boolean };
54
44
 
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));
45
+ async function readCodexUsage(account: Account, now: number): Promise<CodexReadOutcome> {
46
+ let auth: CodexAuthJson | null;
47
+ try {
48
+ auth = readCodexStoreAuth(account.id);
49
+ } catch (e) {
50
+ return { ok: false, reason: `store credential unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)})`, deadGrant: false };
58
51
  }
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);
52
+ if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
68
53
  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
54
  if (isCodexAccessExpiring({ auth, now })) {
72
- const running = seatCounts(codexPaths.presenceDir).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 });
55
+ if (seatCounts(codexPaths.presenceDir).has(account.id)) {
56
+ return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
80
57
  }
58
+ auth = await refreshCodexAuth({ auth, now });
59
+ writeCodexStoreAuth(account.id, auth);
81
60
  }
82
61
  const at = Date.now();
83
62
  return { ok: true, usage: await fetchCodexUsage({ auth, at }), at };
@@ -88,11 +67,32 @@ async function sampleAccount(account: Account, liveId: string | null, now: numbe
88
67
  }
89
68
  }
90
69
 
91
- async function samplePool(accounts: Account[], liveId: string | null, now: number): Promise<Map<string, SampleReport>> {
70
+ async function observeLive(account: Account, cfg: Config, now: number, opts: { probe: boolean }): Promise<Observation | null> {
71
+ if (opts.probe && (account.lastUsageAt == null || now - account.lastUsageAt > cfg.policy.usagePollTtlMs)) {
72
+ const outcome = await readCodexUsage(account, now);
73
+ await withLock(codexPool.lockFile, () => {
74
+ const idx = loadAccounts(codexPool);
75
+ const a = idx.accounts.find((x) => x.id === account.id);
76
+ if (!a) return;
77
+ if (outcome.ok) {
78
+ if (a.lastUsageAt == null || outcome.at > a.lastUsageAt) {
79
+ applyUsage(a, outcome.usage, outcome.at);
80
+ }
81
+ } else if (outcome.deadGrant) {
82
+ a.needsReauth = true;
83
+ }
84
+ saveAccounts(codexPool, idx);
85
+ });
86
+ }
87
+ const current = loadAccounts(codexPool).accounts.find((a) => a.id === account.id) ?? account;
88
+ return current.lastUsageAt != null ? { windows: current.windows, at: current.lastUsageAt } : null;
89
+ }
90
+
91
+ async function samplePool(accounts: Account[], _liveId: string | null, now: number): Promise<Map<string, SampleReport>> {
92
92
  const reports = new Map<string, SampleReport>();
93
93
  await Promise.all(
94
94
  accounts.map(async (account) => {
95
- const outcome = await sampleAccount(account, liveId, now);
95
+ const outcome = await readCodexUsage(account, now);
96
96
  if (outcome.ok) {
97
97
  applyUsage(account, outcome.usage, outcome.at);
98
98
  reports.set(account.id, { ok: true, source: "probe" });
@@ -105,68 +105,15 @@ async function samplePool(accounts: Account[], liveId: string | null, now: numbe
105
105
  return reports;
106
106
  }
107
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;
108
+ async function prepareMove(target: Account): Promise<void> {
109
+ let auth: CodexAuthJson | null;
142
110
  try {
143
- fresh = await refreshCodexAuth({ auth: parked });
111
+ auth = readCodexStoreAuth(target.id);
144
112
  } 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) });
113
+ throw new StoreUnusableError(`${target.label}'s store is unreadable (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
164
114
  }
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 });
115
+ if (!auth) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
116
+ log("move.prepared", { account: target.id.slice(0, 8), label: target.label });
170
117
  }
171
118
 
172
119
  async function login(): Promise<Harvest | null> {
@@ -204,7 +151,7 @@ async function login(): Promise<Harvest | null> {
204
151
  email: usage?.usage.email ?? identity.email,
205
152
  tier: usage?.usage.planType ?? identity.planType,
206
153
  sample: usage ? { windows: usage.usage.windows, at: usage.at } : null,
207
- park: async () => writeParkedCodexAuth({ credFile: codexCredItemFor(identity.accountId), auth }),
154
+ park: async () => writeCodexStoreAuth(identity.accountId, auth),
208
155
  };
209
156
  } finally {
210
157
  rmSync(onboardDir, { recursive: true, force: true });
@@ -224,36 +171,9 @@ async function sampleLogin(auth: CodexAuthJson): Promise<{ usage: CodexUsage; at
224
171
  }
225
172
 
226
173
  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 (seatCounts(codexPaths.presenceDir).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 (seatCounts(codexPaths.presenceDir).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
- };
174
+ console.log(c.cyan("Opening an isolated codex login for your first pooled account - the login you already have stays as it is for sessions started outside the supervisor."));
175
+ console.log();
176
+ return login();
257
177
  }
258
178
 
259
179
  function storePinnedAwayFromFile(): boolean {
@@ -287,36 +207,26 @@ function install(): void {
287
207
  console.log(`${c.green("✓")} codex supervisor installed at ${codexSupervisorLink()}`);
288
208
  console.log(`${c.green("✓")} Stop hook declared in ${codexPaths.hooksJson}`);
289
209
  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."));
210
+ console.log(c.bold(c.yellow("one manual step per seat: codex trusts hooks per store path.")));
211
+ console.log(c.yellow("open a supervised codex session on each pooled account, run /hooks, and trust the tokenmaxxing Stop hook - auto-switching stays inert on an untrusted seat."));
212
+ console.log(c.yellow("`tokenmaxxing doctor` lists seats still needing trust."));
292
213
  }
293
214
 
294
215
  export function codexPickCtx(now: number, currentId: string | null): PickCtx {
295
216
  return { now, thresholds: thresholdBars(loadConfig()), currentId, families: null, seats: null };
296
217
  }
297
218
 
298
- export function reconcileSiblings(now: number): void {
299
- const liveId = liveCodexAccountId();
300
- if (liveId == null) return;
219
+ export function pickCodexSeat(now: number, wantedId: string | null = null): Account | null {
220
+ const cfg = loadConfig();
301
221
  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 = livingPresences(codexPaths.presenceDir);
305
- if (existsSync(codexPaths.reconcileDir)) {
306
- const alive = new Set(living.map((presence) => presence.id));
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.id);
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.id.slice(0, 8), account: presence.accountId.slice(0, 8) });
222
+ const ctx: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: null, seats: null };
223
+ if (wantedId != null) {
224
+ const wanted = idx.accounts.find((a) => a.id === wantedId && a.needsReauth !== true && !isExhausted(a, { ...ctx, currentId: wantedId }));
225
+ if (wanted) return wanted;
319
226
  }
227
+ const present = seatCounts(codexPaths.presenceDir);
228
+ const usable = idx.accounts.filter((a) => a.needsReauth !== true && !isExhausted(a, ctx) && !present.has(a.id));
229
+ return pickBest(usable, ctx) ?? pickEarliestReset(idx.accounts.filter((a) => a.needsReauth !== true && !present.has(a.id)), ctx)?.account ?? null;
320
230
  }
321
231
 
322
232
  export const codex: Provider = {
@@ -325,18 +235,19 @@ export const codex: Provider = {
325
235
  pool: codexPool,
326
236
  seats: "live",
327
237
  waitsWhenDepleted: false,
328
- liveId: liveCodexAccountId,
329
- presence: () => seatCounts(codexPaths.presenceDir),
238
+ statusOnly: false,
239
+ liveId,
240
+ presence,
330
241
  gatedFamilies: () => null,
331
242
  observeLive,
332
243
  samplePool,
333
244
  mergeWindows: (next) => next,
334
- swap,
335
- classifySwapError: (e) => (e instanceof CodexInvalidGrantError ? "dead-grant" : "fatal"),
336
- removeCredentials: async (a) => deleteParkedCodexAuth({ credFile: codexCredItemFor(a.id) }),
245
+ swap: prepareMove,
246
+ classifySwapError: (e) => (e instanceof CodexInvalidGrantError ? "dead-grant" : e instanceof StoreUnusableError ? "skip" : "fatal"),
247
+ removeCredentials: async (a) => deleteCodexStoreAuth(a.id),
337
248
  storeUsable: async (a) => {
338
249
  try {
339
- return readParkedCodexAuth({ credFile: codexCredItemFor(a.id) }) != null;
250
+ return readCodexStoreAuth(a.id) != null;
340
251
  } catch {
341
252
  return false;
342
253
  }
@@ -1,8 +1,8 @@
1
- import { readFileSync, rmSync } from "node:fs";
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync } 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 { codexPaths } from "./paths.ts";
5
+ import { codexAuthJsonFor, codexPaths, codexStoreDirFor } from "./paths.ts";
6
6
  import { CodexAuthJsonSchema, ErrnoSchema, type CodexAuthJson } from "./types.ts";
7
7
 
8
8
  export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
@@ -19,41 +19,50 @@ export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
19
19
  return CodexAuthJsonSchema.parse(parsed);
20
20
  }
21
21
 
22
- export function readLiveCodexAuth(): CodexAuthJson | null {
23
- return readCodexAuthAt({ path: codexPaths.authJson });
22
+ export function readCodexStoreAuth(accountId: string): CodexAuthJson | null {
23
+ return readCodexAuthAt({ path: codexAuthJsonFor(accountId) });
24
24
  }
25
25
 
26
- export function liveCodexAccountId(): string | null {
27
- const live = readLiveCodexAuth();
28
- if (!live) return null;
29
- return codexIdentityOf({ auth: live }).accountId;
26
+ export function writeCodexStoreAuth(accountId: string, auth: CodexAuthJson): void {
27
+ mkdirSync(codexStoreDirFor(accountId), { recursive: true });
28
+ writeFileAtomic(codexAuthJsonFor(accountId), JSON.stringify(CodexAuthJsonSchema.parse(auth), null, 2), 0o600);
30
29
  }
31
30
 
32
- export function writeLiveCodexAuth(input: { auth: CodexAuthJson }): void {
33
- writeFileAtomic(codexPaths.authJson, JSON.stringify(CodexAuthJsonSchema.parse(input.auth), null, 2), 0o600);
31
+ export function deleteCodexStoreAuth(accountId: string): void {
32
+ rmSync(codexStoreDirFor(accountId), { recursive: true, force: true });
34
33
  }
35
34
 
36
- function parkedPath(input: { credFile: string }): string {
37
- return join(codexPaths.credsDir, `${input.credFile}.json`);
38
- }
39
-
40
- export function readParkedCodexAuth(input: { credFile: string }): CodexAuthJson | null {
41
- let raw: string;
35
+ export function ensureCodexStoreHome(accountId: string): string {
36
+ const store = codexStoreDirFor(accountId);
37
+ mkdirSync(store, { recursive: true });
38
+ mkdirSync(codexPaths.home, { recursive: true });
39
+ mkdirSync(join(codexPaths.home, "sessions"), { recursive: true });
40
+ let names: string[] = [];
42
41
  try {
43
- raw = readFileSync(parkedPath(input), "utf8");
42
+ names = readdirSync(codexPaths.home);
44
43
  } catch (e) {
45
- if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
44
+ if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return store;
46
45
  throw e;
47
46
  }
48
- return CodexAuthJsonSchema.parse(JSON.parse(raw));
49
- }
50
-
51
- export function writeParkedCodexAuth(input: { credFile: string; auth: CodexAuthJson }): void {
52
- writeFileAtomic(parkedPath(input), JSON.stringify(CodexAuthJsonSchema.parse(input.auth), null, 2), 0o600);
53
- }
54
-
55
- export function deleteParkedCodexAuth(input: { credFile: string }): void {
56
- rmSync(parkedPath(input), { force: true });
47
+ for (const name of names) {
48
+ if (name === "auth.json") continue;
49
+ const target = join(codexPaths.home, name);
50
+ const link = join(store, name);
51
+ if (existsSync(link)) {
52
+ try {
53
+ if (lstatSync(link).isSymbolicLink()) continue;
54
+ } catch {
55
+ continue;
56
+ }
57
+ continue;
58
+ }
59
+ try {
60
+ symlinkSync(target, link);
61
+ } catch (e) {
62
+ if (ErrnoSchema.safeParse(e).data?.code !== "EEXIST") throw e;
63
+ }
64
+ }
65
+ return store;
57
66
  }
58
67
 
59
68
  const IdClaimsSchema = z.looseObject({
@@ -0,0 +1,188 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { writeFileAtomic } from "./atomic.ts";
5
+ import { grokAuthJsonFor, grokPaths, grokPool, grokSeatFromEnv, grokStoreDirFor } from "./paths.ts";
6
+ import type { Observation, Provider, SampleReport } from "./provider.ts";
7
+ import { loadAccounts, loadConfig, pinBinOverride, type Harvest } from "./state.ts";
8
+ import type { Account } from "./types.ts";
9
+ import { c } from "../cli/render.ts";
10
+
11
+ const GrokAuthEntrySchema = z.looseObject({
12
+ user_id: z.string().optional(),
13
+ principal_id: z.string().optional(),
14
+ email: z.string().nullish(),
15
+ first_name: z.string().nullish(),
16
+ refresh_token: z.string().optional(),
17
+ expires_at: z.string().nullish(),
18
+ });
19
+ type GrokAuthEntry = z.infer<typeof GrokAuthEntrySchema>;
20
+
21
+ function parseGrokAuthFile(path: string): { key: string; entry: GrokAuthEntry }[] {
22
+ let raw: unknown;
23
+ try {
24
+ raw = JSON.parse(readFileSync(path, "utf8"));
25
+ } catch {
26
+ return [];
27
+ }
28
+ const map = z.record(z.string(), z.unknown()).safeParse(raw);
29
+ if (!map.success) return [];
30
+ const out: { key: string; entry: GrokAuthEntry }[] = [];
31
+ for (const [key, value] of Object.entries(map.data)) {
32
+ const parsed = GrokAuthEntrySchema.safeParse(value);
33
+ if (parsed.success) out.push({ key, entry: parsed.data });
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function identityOf(key: string, entry: GrokAuthEntry): { id: string; email: string | null } {
39
+ const id = entry.user_id ?? entry.principal_id ?? key;
40
+ return { id, email: entry.email ?? null };
41
+ }
42
+
43
+ function resolveRealGrok(): string {
44
+ const cfg = loadConfig();
45
+ if (cfg.grokBin) {
46
+ if (!existsSync(cfg.grokBin)) throw new Error(`configured grokBin does not exist: ${cfg.grokBin} - fix config.json`);
47
+ return cfg.grokBin;
48
+ }
49
+ for (const d of (process.env.PATH ?? "").split(":")) {
50
+ if (!d) continue;
51
+ const cand = join(d, "grok");
52
+ try {
53
+ if (existsSync(cand)) return cand;
54
+ } catch {
55
+ continue;
56
+ }
57
+ }
58
+ throw new Error("could not locate the real `grok` binary (set grokBin in config.json)");
59
+ }
60
+
61
+ function liveId(): string | null {
62
+ return grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id));
63
+ }
64
+
65
+ function presence(): Map<string, number> {
66
+ return new Map();
67
+ }
68
+
69
+ async function observeLive(account: Account): Promise<Observation | null> {
70
+ return account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null;
71
+ }
72
+
73
+ async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
74
+ const reports = new Map<string, SampleReport>();
75
+ for (const a of accounts) {
76
+ reports.set(a.id, (await storeUsable(a)) ? { ok: true, source: "probe" } : { ok: false, reason: "no usable credential in this account's store - run `tokenmaxxing auth --grok`" });
77
+ }
78
+ return reports;
79
+ }
80
+
81
+ async function storeUsable(a: Account): Promise<boolean> {
82
+ try {
83
+ const entries = parseGrokAuthFile(grokAuthJsonFor(a.id));
84
+ return entries.some((e) => identityOf(e.key, e.entry).id === a.id && (e.entry.refresh_token ?? "") !== "");
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ async function removeCredentials(a: Account): Promise<void> {
91
+ rmSync(grokStoreDirFor(a.id), { recursive: true, force: true });
92
+ }
93
+
94
+ function harvestOf(key: string, entry: GrokAuthEntry): Harvest | null {
95
+ const { id, email } = identityOf(key, entry);
96
+ if (!id) return null;
97
+ return {
98
+ id,
99
+ email,
100
+ tier: null,
101
+ sample: null,
102
+ park: async () => {
103
+ mkdirSync(grokStoreDirFor(id), { recursive: true });
104
+ writeFileAtomic(grokAuthJsonFor(id), JSON.stringify({ [key]: entry }, null, 2), 0o600);
105
+ },
106
+ };
107
+ }
108
+
109
+ async function login(): Promise<Harvest | null> {
110
+ const real = resolveRealGrok();
111
+ const onboardDir = grokPaths.onboardDir;
112
+ rmSync(onboardDir, { recursive: true, force: true });
113
+ mkdirSync(onboardDir, { recursive: true });
114
+ const p = Bun.spawn([real, "login"], {
115
+ stdin: "inherit",
116
+ stdout: "inherit",
117
+ stderr: "inherit",
118
+ env: { ...process.env, GROK_HOME: onboardDir, TOKENMAXXING_PROBE: "1" },
119
+ });
120
+ await p.exited;
121
+ try {
122
+ if (p.exitCode !== 0) {
123
+ console.error(c.red("no grok login landed in the isolated home - nothing added."));
124
+ return null;
125
+ }
126
+ const entries = parseGrokAuthFile(join(onboardDir, "auth.json"));
127
+ if (entries.length === 0) {
128
+ console.error(c.red("no grok login landed in the isolated home - nothing added."));
129
+ return null;
130
+ }
131
+ return harvestOf(entries[0]!.key, entries[0]!.entry);
132
+ } finally {
133
+ rmSync(onboardDir, { recursive: true, force: true });
134
+ }
135
+ }
136
+
137
+ async function importLive(): Promise<Harvest | null> {
138
+ console.log(c.cyan("Opening an isolated grok login for your first pooled account - the login you already have stays as it is."));
139
+ console.log();
140
+ const live = parseGrokAuthFile(join(grokPaths.home, "auth.json"));
141
+ if (live.length > 0) {
142
+ console.log(c.dim(`found ${live.length} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`));
143
+ const h = harvestOf(live[0]!.key, live[0]!.entry);
144
+ if (h) return h;
145
+ }
146
+ return login();
147
+ }
148
+
149
+ function preflight(): void {
150
+ const real = resolveRealGrok();
151
+ const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
152
+ const out = (p.stdout?.toString() ?? "").trim().toLowerCase();
153
+ if (p.exitCode !== 0 || !out.includes("grok")) {
154
+ throw new Error(`grok binary failed verification: ${real}`);
155
+ }
156
+ pinBinOverride({ key: "grokBin", bin: real });
157
+ }
158
+
159
+ function install(): void {
160
+ console.log(c.dim("grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts."));
161
+ }
162
+
163
+ export const grok: Provider = {
164
+ name: "grok",
165
+ flag: " --grok",
166
+ pool: grokPool,
167
+ seats: "live",
168
+ waitsWhenDepleted: false,
169
+ statusOnly: true,
170
+ liveId,
171
+ presence,
172
+ gatedFamilies: () => null,
173
+ observeLive,
174
+ samplePool,
175
+ mergeWindows: (next) => next,
176
+ swap: async () => {
177
+ throw new Error("grok moves are not supported yet (status-only pool)");
178
+ },
179
+ classifySwapError: () => "fatal",
180
+ removeCredentials,
181
+ storeUsable,
182
+ login,
183
+ importLive,
184
+ preflight,
185
+ install,
186
+ loginStep: () => `Sign in in the browser session that opens (or run ${c.bold("grok login --device-auth")} on headless hosts first).`,
187
+ windowLabel: (name) => name.toLowerCase(),
188
+ };