tokenmaxxing 1.34.0 → 1.35.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 +3 -5
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +23 -40
- package/src/cli/auth.ts +38 -52
- package/src/cli/check.ts +2 -1
- package/src/cli/doctor.ts +13 -13
- package/src/cli/init.ts +29 -126
- package/src/cli/rename.ts +11 -45
- package/src/cli/render.ts +0 -7
- package/src/cli/rm.ts +24 -39
- package/src/cli/setuptoken.ts +16 -16
- package/src/cli/status.ts +95 -238
- package/src/cli/switch.ts +54 -46
- package/src/entries/codexstophook.ts +22 -16
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +3 -2
- package/src/entries/statusline.ts +15 -13
- package/src/entries/stopfailurehook.ts +7 -5
- package/src/entries/stophook.ts +3 -2
- package/src/lib/claude.ts +320 -0
- package/src/lib/codex.ts +353 -0
- package/src/lib/codexauth.ts +6 -0
- package/src/lib/codexpresence.ts +0 -10
- package/src/lib/codexusage.ts +14 -32
- package/src/lib/decide.ts +86 -174
- package/src/lib/oauth.ts +7 -0
- package/src/lib/paths.ts +14 -6
- package/src/lib/picker.ts +56 -37
- package/src/lib/provider.ts +34 -0
- package/src/lib/sample.ts +33 -33
- package/src/lib/state.ts +49 -16
- package/src/lib/swap.ts +41 -38
- package/src/lib/types.ts +17 -51
- package/src/lib/usage.ts +17 -4
- package/src/main.ts +20 -20
- package/src/cli/codexadd.ts +0 -89
- package/src/cli/codexinit.ts +0 -120
- package/src/cli/codexrm.ts +0 -36
- package/src/cli/codexswitch.ts +0 -113
- package/src/cli/onboard.ts +0 -92
- package/src/lib/codexdecide.ts +0 -157
- package/src/lib/codexpick.ts +0 -80
- package/src/lib/codexsample.ts +0 -48
- package/src/lib/codexstate.ts +0 -22
- package/src/lib/codexswap.ts +0 -70
package/src/lib/codex.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
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
|
+
for (const rawLine of readFileSync(configToml, "utf8").split("\n")) {
|
|
263
|
+
const line = rawLine.trim();
|
|
264
|
+
if (!line.startsWith("cli_auth_credentials_store")) continue;
|
|
265
|
+
const rest = line.slice("cli_auth_credentials_store".length).trimStart();
|
|
266
|
+
if (!rest.startsWith("=")) continue;
|
|
267
|
+
const beforeComment = rest.slice(1).split("#", 1)[0]!;
|
|
268
|
+
const value = beforeComment.replaceAll('"', "").replaceAll("'", "").trim();
|
|
269
|
+
return value !== "file";
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function preflight(): void {
|
|
275
|
+
const real = resolveRealCodex();
|
|
276
|
+
const fail = verifyRealCodex({ bin: real });
|
|
277
|
+
if (fail !== null) throw new Error(`codex binary failed verification: ${real}: ${fail}`);
|
|
278
|
+
pinBinOverride({ key: "codexBin", bin: real });
|
|
279
|
+
if (storePinnedAwayFromFile()) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
'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.',
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function install(): void {
|
|
287
|
+
installCodexSupervisor();
|
|
288
|
+
const rc = shellRcPath();
|
|
289
|
+
if (rc && ensurePathInRc(rc) === "skipped") {
|
|
290
|
+
const hint = managedShellRcSkipLines();
|
|
291
|
+
console.log(c.yellow(`⚠ ${hint.headline}`));
|
|
292
|
+
console.log(c.yellow(` ${hint.detail}`));
|
|
293
|
+
console.log(c.yellow(` ${hint.exportLine}`));
|
|
294
|
+
}
|
|
295
|
+
console.log(`${c.green("✓")} codex supervisor installed at ${codexSupervisorLink()}`);
|
|
296
|
+
console.log(`${c.green("✓")} Stop hook declared in ${codexPaths.hooksJson}`);
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(c.bold(c.yellow("one manual step: codex skips untrusted hooks.")));
|
|
299
|
+
console.log(c.yellow("open codex, run /hooks, and trust the tokenmaxxing Stop hook - auto-switching is inert until then."));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function codexPickCtx(now: number, currentId: string | null): PickCtx {
|
|
303
|
+
return { now, thresholds: thresholdBars(loadConfig()), currentId, families: null };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function reconcileSiblings(now: number): void {
|
|
307
|
+
const liveId = liveCodexAccountId();
|
|
308
|
+
if (liveId == null) return;
|
|
309
|
+
const idx = loadAccounts(codexPool);
|
|
310
|
+
const liveAccount = idx.accounts.find((a) => a.id === liveId);
|
|
311
|
+
if (!liveAccount || liveAccount.needsReauth === true || isExhausted(liveAccount, codexPickCtx(now, liveId))) return;
|
|
312
|
+
const living = livingCodexPresences();
|
|
313
|
+
if (existsSync(codexPaths.reconcileDir)) {
|
|
314
|
+
const alive = new Set(living.map((presence) => presence.supervisorId));
|
|
315
|
+
for (const name of readdirSync(codexPaths.reconcileDir)) {
|
|
316
|
+
if (!alive.has(name)) rmSync(join(codexPaths.reconcileDir, name), { force: true });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const presence of living) {
|
|
320
|
+
if (presence.accountId === liveId) continue;
|
|
321
|
+
if (!idx.accounts.some((a) => a.id === presence.accountId)) continue;
|
|
322
|
+
const markerPath = join(codexPaths.reconcileDir, presence.supervisorId);
|
|
323
|
+
if (existsSync(markerPath)) continue;
|
|
324
|
+
mkdirSync(codexPaths.reconcileDir, { recursive: true });
|
|
325
|
+
writeFileAtomic(markerPath, JSON.stringify(CodexReconcileMarkerSchema.parse({ accountId: presence.accountId, ts: now })));
|
|
326
|
+
log("codex.reconcile_signal", { supervisorId: presence.supervisorId.slice(0, 8), account: presence.accountId.slice(0, 8) });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const codex: Provider = {
|
|
331
|
+
name: "codex",
|
|
332
|
+
flag: " --codex",
|
|
333
|
+
pool: codexPool,
|
|
334
|
+
waitsWhenDepleted: false,
|
|
335
|
+
switchMargin: 1.2,
|
|
336
|
+
switchNote: " (takes effect on the next codex start)",
|
|
337
|
+
liveId: liveCodexAccountId,
|
|
338
|
+
liveOwner: async () => liveCodexAccountId(),
|
|
339
|
+
presentIds: presentCodexAccountIds,
|
|
340
|
+
gatedFamilies: () => null,
|
|
341
|
+
observeLive,
|
|
342
|
+
samplePool,
|
|
343
|
+
mergeWindows: (next) => next,
|
|
344
|
+
swap,
|
|
345
|
+
classifySwapError: (e) => (e instanceof CodexInvalidGrantError ? "dead-grant" : "fatal"),
|
|
346
|
+
removeCredentials: async (a) => deleteParkedCodexAuth({ credFile: codexCredItemFor(a.id) }),
|
|
347
|
+
login,
|
|
348
|
+
importLive,
|
|
349
|
+
preflight,
|
|
350
|
+
install,
|
|
351
|
+
loginStep: (who) => `Open the URL codex prints, enter the code, and sign in with ${who}; the command exits once you're in.`,
|
|
352
|
+
windowLabel: codexLimitLabel,
|
|
353
|
+
};
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -27,6 +27,12 @@ export function readLiveCodexAuth(): CodexAuthJson | null {
|
|
|
27
27
|
return readCodexAuthAt({ path: codexPaths.authJson });
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function liveCodexAccountId(): string | null {
|
|
31
|
+
const live = readLiveCodexAuth();
|
|
32
|
+
if (!live) return null;
|
|
33
|
+
return codexIdentityOf({ auth: live }).accountId;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
export function writeLiveCodexAuth(input: { auth: CodexAuthJson }): void {
|
|
31
37
|
writeFileAtomic(codexPaths.authJson, JSON.stringify(CodexAuthJsonSchema.parse(input.auth), null, 2), 0o600);
|
|
32
38
|
}
|
package/src/lib/codexpresence.ts
CHANGED
|
@@ -68,13 +68,3 @@ export function livingCodexPresences(): LivingPresence[] {
|
|
|
68
68
|
export function presentCodexAccountIds(): Set<string> {
|
|
69
69
|
return new Set(livingCodexPresences().map((presence) => presence.accountId));
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
export function targetableCodexAccounts<T extends { accountId: string }>(input: {
|
|
73
|
-
accounts: T[];
|
|
74
|
-
activeAccountId: string | null;
|
|
75
|
-
}): T[] {
|
|
76
|
-
const present = presentCodexAccountIds();
|
|
77
|
-
return input.accounts.filter(
|
|
78
|
-
(account) => account.accountId === input.activeAccountId || !present.has(account.accountId),
|
|
79
|
-
);
|
|
80
|
-
}
|
package/src/lib/codexusage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, safeErrorDetail } from "./http.ts";
|
|
3
|
-
import { CodexUsageSchema, type CodexAuthJson, type CodexUsage, type
|
|
3
|
+
import { CodexUsageSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
|
|
4
4
|
import { codexIdentityOf } from "./codexauth.ts";
|
|
5
5
|
import { familyTokens } from "./usage.ts";
|
|
6
6
|
|
|
@@ -35,21 +35,23 @@ const WireUsageSchema = z.looseObject({
|
|
|
35
35
|
.nullish(),
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
function toWindows(rateLimit: z.infer<typeof WireRateLimitSchema> | null | undefined):
|
|
39
|
-
const out:
|
|
38
|
+
function toWindows(rateLimit: z.infer<typeof WireRateLimitSchema> | null | undefined, name: string | null, at: number): Window[] {
|
|
39
|
+
const out: Window[] = [];
|
|
40
40
|
for (const wire of [rateLimit?.primary_window, rateLimit?.secondary_window]) {
|
|
41
41
|
if (wire == null) continue;
|
|
42
42
|
out.push({
|
|
43
|
+
name,
|
|
43
44
|
usedPercentage: wire.used_percent,
|
|
44
45
|
resetsAt: wire.reset_at != null ? wire.reset_at * 1000 : null,
|
|
45
46
|
windowSeconds: wire.limit_window_seconds ?? null,
|
|
47
|
+
sampledAt: at,
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
return out;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<CodexUsage> {
|
|
52
|
-
const { auth } = input;
|
|
53
|
+
export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }): Promise<CodexUsage> {
|
|
54
|
+
const { auth, at } = input;
|
|
53
55
|
const identity = codexIdentityOf({ auth });
|
|
54
56
|
let res: Response;
|
|
55
57
|
try {
|
|
@@ -80,41 +82,21 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<C
|
|
|
80
82
|
}
|
|
81
83
|
const wire = parsed.data;
|
|
82
84
|
|
|
83
|
-
const perLimit: Record<string, CodexWindow[]> = {};
|
|
84
|
-
for (const row of wire.additional_rate_limits ?? []) {
|
|
85
|
-
const windows = toWindows(row.rate_limit);
|
|
86
|
-
if (windows.length > 0) perLimit[row.limit_name] = windows;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
85
|
return CodexUsageSchema.parse({
|
|
90
86
|
accountId: wire.account_id,
|
|
91
87
|
email: wire.email ?? null,
|
|
92
88
|
planType: wire.plan_type ?? null,
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
windows: [
|
|
90
|
+
...toWindows(wire.rate_limit, null, at),
|
|
91
|
+
...(wire.additional_rate_limits ?? []).flatMap((row) => toWindows(row.rate_limit, row.limit_name, at)),
|
|
92
|
+
],
|
|
95
93
|
});
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
const LIMIT_LABEL_ABBREVIATIONS = new Map([["reserve", "rsrv"]]);
|
|
99
97
|
|
|
100
|
-
export function codexLimitLabel(
|
|
101
|
-
const tokens = familyTokens(
|
|
102
|
-
const label = tokens.at(-1) ??
|
|
98
|
+
export function codexLimitLabel(limitName: string): string {
|
|
99
|
+
const tokens = familyTokens(limitName).filter((t) => Number.isNaN(Number(t)));
|
|
100
|
+
const label = tokens.at(-1) ?? limitName.trim().toLowerCase();
|
|
103
101
|
return LIMIT_LABEL_ABBREVIATIONS.get(label) ?? label;
|
|
104
102
|
}
|
|
105
|
-
|
|
106
|
-
const SESSION_WINDOW_MAX_S = 6 * 3600;
|
|
107
|
-
|
|
108
|
-
export function isSessionWindow(input: { window: CodexWindow }): boolean {
|
|
109
|
-
const seconds = input.window.windowSeconds;
|
|
110
|
-
return seconds != null && seconds <= SESSION_WINDOW_MAX_S;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function weeklyWindowOf(input: { aggregate: CodexWindow[] }): CodexWindow | null {
|
|
114
|
-
let best: CodexWindow | null = null;
|
|
115
|
-
for (const window of input.aggregate) {
|
|
116
|
-
if (isSessionWindow({ window })) continue;
|
|
117
|
-
if (best == null || (window.windowSeconds ?? 0) > (best.windowSeconds ?? 0)) best = window;
|
|
118
|
-
}
|
|
119
|
-
return best;
|
|
120
|
-
}
|