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/DESIGN.md +5 -4
- package/README.md +4 -5
- package/package.json +1 -1
- package/src/cli/auth.ts +2 -2
- package/src/cli/doctor.ts +33 -2
- package/src/cli/init.ts +6 -5
- package/src/cli/status.ts +17 -5
- package/src/entries/codexstophook.ts +7 -78
- package/src/entries/codexsupervisor.ts +39 -27
- package/src/lib/claude.ts +1 -0
- package/src/lib/codex.ts +86 -175
- package/src/lib/codexauth.ts +36 -27
- package/src/lib/grok.ts +188 -0
- package/src/lib/install.ts +35 -1
- package/src/lib/opencodego.ts +186 -0
- package/src/lib/paths.ts +69 -5
- package/src/lib/provider.ts +2 -1
- package/src/lib/state.ts +6 -2
- package/src/lib/types.ts +3 -6
- package/src/main.ts +28 -19
- package/src/cli/switch.ts +0 -116
package/src/lib/codex.ts
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
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,
|
|
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 {
|
|
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 {
|
|
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,
|
|
16
|
+
import { loadAccounts, loadConfig, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
|
|
17
17
|
import { restoreTermios, saveTermios } from "./tty.ts";
|
|
18
|
-
import {
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
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
|
|
109
|
-
|
|
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
|
-
|
|
111
|
+
auth = readCodexStoreAuth(target.id);
|
|
144
112
|
} catch (e) {
|
|
145
|
-
|
|
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
|
-
|
|
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 () =>
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
291
|
-
console.log(c.yellow("open codex, run /hooks, and trust the tokenmaxxing Stop hook - auto-switching
|
|
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
|
|
299
|
-
const
|
|
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
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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) =>
|
|
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
|
|
250
|
+
return readCodexStoreAuth(a.id) != null;
|
|
340
251
|
} catch {
|
|
341
252
|
return false;
|
|
342
253
|
}
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -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
|
|
23
|
-
return readCodexAuthAt({ path:
|
|
22
|
+
export function readCodexStoreAuth(accountId: string): CodexAuthJson | null {
|
|
23
|
+
return readCodexAuthAt({ path: codexAuthJsonFor(accountId) });
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
33
|
-
|
|
31
|
+
export function deleteCodexStoreAuth(accountId: string): void {
|
|
32
|
+
rmSync(codexStoreDirFor(accountId), { recursive: true, force: true });
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let
|
|
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
|
-
|
|
42
|
+
names = readdirSync(codexPaths.home);
|
|
44
43
|
} catch (e) {
|
|
45
|
-
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return
|
|
44
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return store;
|
|
46
45
|
throw e;
|
|
47
46
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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({
|
package/src/lib/grok.ts
ADDED
|
@@ -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
|
+
};
|