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
@@ -1,89 +0,0 @@
1
- import { mkdirSync, rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "../lib/claudebin.ts";
4
- import { resolveRealCodex } from "../lib/codexbin.ts";
5
- import { codexIdentityOf, readCodexAuthAt, writeParkedCodexAuth } from "../lib/codexauth.ts";
6
- import { CodexUsageReadError, fetchCodexUsage } from "../lib/codexusage.ts";
7
- import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
8
- import { saveTermios, restoreTermios } from "../lib/tty.ts";
9
- import { withLock } from "../lib/lock.ts";
10
- import { codexCredItemFor, codexPaths } from "../lib/paths.ts";
11
- import { writeFileAtomic } from "../lib/atomic.ts";
12
- import type { CodexAccount, CodexUsage } from "../lib/types.ts";
13
- import { c, count } from "./render.ts";
14
-
15
- export async function cmdCodexAdd(): Promise<number> {
16
- const real = resolveRealCodex();
17
- const onboardDir = codexPaths.onboardDir;
18
- rmSync(onboardDir, { recursive: true, force: true });
19
- mkdirSync(onboardDir, { recursive: true });
20
- writeFileAtomic(join(onboardDir, "config.toml"), 'cli_auth_credentials_store = "file"\n');
21
-
22
- console.log(c.cyan("Opening an isolated codex login - your primary login is untouched."));
23
- console.log(c.dim("Open the URL codex prints, enter the code, and sign in with the account to add; the command exits once you're in."));
24
- console.log();
25
-
26
- const savedTermios = saveTermios();
27
- const p = Bun.spawn([real, "login", "--device-auth"], {
28
- stdin: "inherit",
29
- stdout: "inherit",
30
- stderr: "inherit",
31
- env: {
32
- ...process.env,
33
- CODEX_HOME: onboardDir,
34
- TOKENMAXXING_PROBE: "1",
35
- [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH),
36
- },
37
- });
38
- await p.exited;
39
- restoreTermios(savedTermios);
40
-
41
- let account: CodexAccount;
42
- let poolSize: number;
43
- try {
44
- const auth = readCodexAuthAt({ path: join(onboardDir, "auth.json") });
45
- if (p.exitCode !== 0 || !auth) {
46
- console.error(c.red("no codex login landed in the isolated home - nothing added."));
47
- return 1;
48
- }
49
-
50
- const identity = codexIdentityOf({ auth });
51
- console.log(c.dim("sampling usage..."));
52
- let usage: CodexUsage | null = null;
53
- try {
54
- usage = await fetchCodexUsage({ auth });
55
- } catch (e) {
56
- if (!(e instanceof CodexUsageReadError)) throw e;
57
- console.log(c.yellow("could not sample usage now - it will fill in on first use."));
58
- }
59
-
60
- const credFile = codexCredItemFor(identity.accountId);
61
- writeParkedCodexAuth({ credFile, auth });
62
-
63
- ({ account, poolSize } = await withLock(codexPaths.lockFile, () => {
64
- const index = loadCodexAccounts();
65
- const existing = index.accounts.find((entry) => entry.accountId === identity.accountId);
66
- const fresh: CodexAccount = {
67
- accountId: identity.accountId,
68
- email: usage?.email ?? identity.email,
69
- label: existing?.label ?? usage?.email ?? identity.email ?? identity.accountId.slice(0, 8),
70
- planType: usage?.planType ?? identity.planType,
71
- credFile,
72
- addedAt: existing?.addedAt ?? new Date().toISOString(),
73
- needsReauth: false,
74
- lastUsage: usage ? { aggregate: usage.aggregate, perLimit: usage.perLimit } : existing?.lastUsage,
75
- lastUsageAt: usage ? Date.now() : existing?.lastUsageAt,
76
- };
77
- if (existing) Object.assign(existing, fresh);
78
- else index.accounts.push(fresh);
79
- saveCodexAccounts({ index });
80
- return { account: fresh, poolSize: index.accounts.length };
81
- }));
82
- } finally {
83
- rmSync(onboardDir, { recursive: true, force: true });
84
- }
85
-
86
- console.log();
87
- console.log(`${c.green("✓")} added codex account ${c.bold(account.label)} (${account.planType ?? "?"}) - codex pool now has ${count({ n: poolSize, noun: "account" })}`);
88
- return 0;
89
- }
@@ -1,120 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolveRealCodex, verifyRealCodex } from "../lib/codexbin.ts";
3
- import { codexIdentityOf, readLiveCodexAuth, writeParkedCodexAuth } from "../lib/codexauth.ts";
4
- import { CodexUsageReadError, fetchCodexUsage } from "../lib/codexusage.ts";
5
- import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
6
- import { loadConfig, pinBinOverride } from "../lib/state.ts";
7
- import { installCodexSupervisor, codexSupervisorLink, ensurePathInRc, managedShellRcSkipLines, shellRcPath } from "../lib/install.ts";
8
- import { withLock } from "../lib/lock.ts";
9
- import { presentCodexAccountIds } from "../lib/codexpresence.ts";
10
- import { codexCredItemFor, codexPaths } from "../lib/paths.ts";
11
- import type { CodexAccount, CodexUsage } from "../lib/types.ts";
12
- import { c } from "./render.ts";
13
-
14
- function storePinnedAwayFromFile(): boolean {
15
- const configToml = `${codexPaths.home}/config.toml`;
16
- if (!existsSync(configToml)) return false;
17
- for (const rawLine of readFileSync(configToml, "utf8").split("\n")) {
18
- const line = rawLine.trim();
19
- if (!line.startsWith("cli_auth_credentials_store")) continue;
20
- const rest = line.slice("cli_auth_credentials_store".length).trimStart();
21
- if (!rest.startsWith("=")) continue;
22
- const beforeComment = rest.slice(1).split("#", 1)[0]!;
23
- const value = beforeComment.replaceAll('"', "").replaceAll("'", "").trim();
24
- return value !== "file";
25
- }
26
- return false;
27
- }
28
-
29
- export async function cmdCodexInit(): Promise<number> {
30
- loadConfig();
31
- const real = resolveRealCodex();
32
- const fail = verifyRealCodex({ bin: real });
33
- if (fail !== null) {
34
- console.error(c.red(`codex binary failed verification: ${real}: ${fail}`));
35
- return 1;
36
- }
37
- pinBinOverride({ key: "codexBin", bin: real });
38
-
39
- if (storePinnedAwayFromFile()) {
40
- console.error(c.red("codex config.toml pins cli_auth_credentials_store away from the plain auth.json file tokenmaxxing swaps."));
41
- console.error(c.dim('recovery: set cli_auth_credentials_store = "file" in ~/.codex/config.toml, run `codex login`, then re-run this.'));
42
- return 1;
43
- }
44
-
45
- const live = readLiveCodexAuth();
46
- if (!live) {
47
- console.error(c.red(`no codex login found at ${codexPaths.authJson} - run \`codex login\` first, then re-run this.`));
48
- return 1;
49
- }
50
- const identity = codexIdentityOf({ auth: live });
51
-
52
- console.log(c.dim("sampling usage..."));
53
- let usage: CodexUsage | null = null;
54
- try {
55
- usage = await fetchCodexUsage({ auth: live });
56
- } catch (e) {
57
- if (!(e instanceof CodexUsageReadError)) throw e;
58
- console.log(c.yellow("could not sample usage now - it will fill in on first use."));
59
- }
60
-
61
- const credFile = codexCredItemFor(identity.accountId);
62
-
63
- if (presentCodexAccountIds().has(identity.accountId)) {
64
- 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."));
65
- console.error(c.dim("close that codex session (or let it exit) and re-run `tokenmaxxing init --codex`."));
66
- return 1;
67
- }
68
-
69
- const account = await withLock(codexPaths.lockFile, () => {
70
- if (presentCodexAccountIds().has(identity.accountId)) {
71
- throw new Error("a live supervised codex session started running this account mid-init - close it and re-run `tokenmaxxing init --codex`");
72
- }
73
- const fresh2 = readLiveCodexAuth();
74
- if (!fresh2 || codexIdentityOf({ auth: fresh2 }).accountId !== identity.accountId) {
75
- throw new Error("the live codex login changed while init was running (a concurrent swap?) - re-run `tokenmaxxing init --codex`");
76
- }
77
- writeParkedCodexAuth({ credFile, auth: fresh2 });
78
- const index = loadCodexAccounts();
79
- const existing = index.accounts.find((entry) => entry.accountId === identity.accountId);
80
- const fresh: CodexAccount = {
81
- accountId: identity.accountId,
82
- email: usage?.email ?? identity.email,
83
- label: existing?.label ?? usage?.email ?? identity.email ?? identity.accountId.slice(0, 8),
84
- planType: usage?.planType ?? identity.planType,
85
- credFile,
86
- addedAt: existing?.addedAt ?? new Date().toISOString(),
87
- needsReauth: false,
88
- lastUsage: usage ? { aggregate: usage.aggregate, perLimit: usage.perLimit } : existing?.lastUsage,
89
- lastUsageAt: usage ? Date.now() : existing?.lastUsageAt,
90
- };
91
- if (existing) Object.assign(existing, fresh);
92
- else index.accounts.push(fresh);
93
- index.activeAccountId = identity.accountId;
94
- saveCodexAccounts({ index });
95
- return fresh;
96
- });
97
-
98
- installCodexSupervisor();
99
- const rc = shellRcPath();
100
- if (rc) {
101
- const pathOutcome = ensurePathInRc(rc);
102
- if (pathOutcome === "skipped") {
103
- const hint = managedShellRcSkipLines();
104
- console.log();
105
- console.log(c.yellow(`⚠ ${hint.headline}`));
106
- console.log(c.yellow(` ${hint.detail}`));
107
- console.log(c.yellow(` ${hint.exportLine}`));
108
- }
109
- }
110
-
111
- console.log();
112
- console.log(`${c.green("✓")} imported codex account ${c.bold(account.label)} (${account.planType ?? "?"})`);
113
- console.log(`${c.green("✓")} codex supervisor installed at ${codexSupervisorLink()}`);
114
- console.log(`${c.green("✓")} Stop hook declared in ${codexPaths.hooksJson}`);
115
- console.log();
116
- console.log(c.bold(c.yellow("one manual step: codex skips untrusted hooks.")));
117
- console.log(c.yellow("open codex, run /hooks, and trust the tokenmaxxing Stop hook - auto-switching is inert until then."));
118
- console.log(c.dim("then add more accounts with `tokenmaxxing add --codex`."));
119
- return 0;
120
- }
@@ -1,36 +0,0 @@
1
- import { withLock } from "../lib/lock.ts";
2
- import { codexPaths } from "../lib/paths.ts";
3
- import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
4
- import { deleteParkedCodexAuth } from "../lib/codexauth.ts";
5
- import { liveCodexAccountId } from "../lib/codexsample.ts";
6
- import { presentCodexAccountIds } from "../lib/codexpresence.ts";
7
- import { findCodexAccount } from "./rename.ts";
8
- import { c, emitError, plain } from "./render.ts";
9
-
10
- export async function cmdCodexRm(selector?: string): Promise<number> {
11
- if (!selector) {
12
- emitError({ message: "usage: tokenmaxxing rm --codex <email|label|id>", paint: plain });
13
- return 2;
14
- }
15
- return withLock(codexPaths.lockFile, async () => {
16
- const index = loadCodexAccounts();
17
- const account = findCodexAccount(index.accounts, selector);
18
- if (!account) {
19
- emitError({ message: `no codex account matches "${selector}"` });
20
- return 1;
21
- }
22
- if (liveCodexAccountId() === account.accountId) {
23
- emitError({ message: `${account.label} is the LIVE codex account - run \`tokenmaxxing switch --codex\` to move off it first.` });
24
- return 1;
25
- }
26
- if (presentCodexAccountIds().has(account.accountId)) {
27
- emitError({ message: `${account.label} is running in a live codex session - close that session before removing it.` });
28
- return 1;
29
- }
30
- deleteParkedCodexAuth({ credFile: account.credFile });
31
- index.accounts = index.accounts.filter((x) => x.accountId !== account.accountId);
32
- saveCodexAccounts({ index });
33
- console.log(`removed codex account ${c.bold(account.label)} from the pool (${index.accounts.length} left)`);
34
- return 0;
35
- });
36
- }
@@ -1,113 +0,0 @@
1
- import { withLock } from "../lib/lock.ts";
2
- import { codexPaths } from "../lib/paths.ts";
3
- import { loadConfig } from "../lib/state.ts";
4
- import { loadCodexAccounts } from "../lib/codexstate.ts";
5
- import { codexCurrentWins, pickBestCodex } from "../lib/codexpick.ts";
6
- import { performCodexSwap } from "../lib/codexswap.ts";
7
- import { CodexInvalidGrantError } from "../lib/codexoauth.ts";
8
- import { liveCodexAccountId } from "../lib/codexsample.ts";
9
- import { presentCodexAccountIds, targetableCodexAccounts } from "../lib/codexpresence.ts";
10
- import { thresholdBars } from "../lib/picker.ts";
11
- import { c, emitError, emitJson } from "./render.ts";
12
-
13
- export async function cmdCodexSwitch(sel?: string, json = false): Promise<number> {
14
- const deadGrants: string[] = [];
15
- const withDeadGrants = (report: Record<string, unknown>) => (deadGrants.length > 0 ? { ...report, deadGrants } : report);
16
- const emit = (text: string, report: Record<string, unknown>): void => {
17
- if (json) emitJson({ ok: true, ...withDeadGrants(report) });
18
- else console.log(text);
19
- };
20
- const fail = (message: string, opts: { paint?: (s: string) => string; notes?: string[]; extra?: Record<string, unknown> } = {}): number => {
21
- emitError({ json, message, paint: opts.paint, notes: opts.notes, extra: withDeadGrants(opts.extra ?? {}) });
22
- return 1;
23
- };
24
- const deadGrantMessage = (label: string) => `${label}'s refresh token is dead - re-add it with \`tokenmaxxing add --codex\``;
25
- const cfg = loadConfig();
26
- const bars = thresholdBars(cfg);
27
- const now = Date.now();
28
-
29
- return withLock(codexPaths.lockFile, async () => {
30
- const index = loadCodexAccounts();
31
- if (index.accounts.length === 0) {
32
- if (json) emitJson({ ok: false, error: "no codex accounts yet - run `tokenmaxxing init --codex`" });
33
- else console.log(c.dim("no codex accounts yet - run `tokenmaxxing init --codex`"));
34
- return 1;
35
- }
36
- const currentId = liveCodexAccountId();
37
-
38
- if (sel) {
39
- const target = index.accounts.find(
40
- (account) => account.label === sel || account.email === sel || account.accountId.startsWith(sel),
41
- );
42
- if (!target) {
43
- return fail(`no codex account matches "${sel}"`, {
44
- notes: index.accounts.map((account) => ` ${account.label} (${account.accountId.slice(0, 8)})`),
45
- extra: { accounts: index.accounts.map((account) => account.label) },
46
- });
47
- }
48
- if (target.accountId === currentId) {
49
- emit(`already on ${c.bold(target.label)}`, { switched: false, account: target.label, reason: "already-on" });
50
- return 0;
51
- }
52
- if (presentCodexAccountIds().has(target.accountId)) {
53
- return fail(`${target.label} is running in a live codex session - swapping onto it would break that session's credential`);
54
- }
55
- if (target.needsReauth) {
56
- return fail(`${target.label} needs re-auth - run \`codex login\` in an isolated home and \`tokenmaxxing add --codex\``);
57
- }
58
- try {
59
- await performCodexSwap({ target });
60
- } catch (e) {
61
- if (e instanceof CodexInvalidGrantError) {
62
- deadGrants.push(target.label);
63
- return fail(deadGrantMessage(target.label));
64
- }
65
- throw e;
66
- }
67
- emit(`${c.green("✓")} switched codex to ${c.bold(target.label)} (takes effect on the next codex start)`, {
68
- switched: true,
69
- account: target.label,
70
- reason: "selected",
71
- });
72
- return 0;
73
- }
74
-
75
- while (true) {
76
- const current = loadCodexAccounts();
77
- const candidates = targetableCodexAccounts({ accounts: current.accounts, activeAccountId: currentId });
78
- const active = candidates.find((account) => account.accountId === currentId) ?? null;
79
- if (codexCurrentWins({ active, accounts: candidates, thresholds: bars, now })) {
80
- emit(`already on the best codex account: ${c.bold(active?.label ?? "?")}`, {
81
- switched: false,
82
- account: active?.label ?? null,
83
- reason: "current-wins",
84
- });
85
- return 0;
86
- }
87
- const best = pickBestCodex({ accounts: candidates, thresholds: bars, now, currentAccountId: currentId });
88
- if (!best) {
89
- const message = "no usable codex switch target (all at their bars, unmeasured, or needing reauth)";
90
- const reauthNeeded = current.accounts.filter((account) => account.needsReauth).map((account) => account.label);
91
- if (json) return fail(message, { extra: { reauthNeeded } });
92
- console.log(c.yellow(message));
93
- return 1;
94
- }
95
- try {
96
- await performCodexSwap({ target: best });
97
- } catch (e) {
98
- if (e instanceof CodexInvalidGrantError) {
99
- deadGrants.push(best.label);
100
- if (!json) console.error(c.red(deadGrantMessage(best.label)));
101
- continue;
102
- }
103
- throw e;
104
- }
105
- emit(`${c.green("✓")} switched codex to ${c.bold(best.label)} (takes effect on the next codex start)`, {
106
- switched: true,
107
- account: best.label,
108
- reason: "best",
109
- });
110
- return 0;
111
- }
112
- });
113
- }
@@ -1,92 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { z } from "zod";
4
- import { readItem, deleteItem, isolatedTarget } from "../lib/credstore.ts";
5
- import { resolveRealClaude } from "../lib/claudebin.ts";
6
- import { CRED_ENV_OVERRIDES, probeUsage } from "../lib/usage.ts";
7
- import { saveTermios, restoreTermios } from "../lib/tty.ts";
8
- import { paths } from "../lib/paths.ts";
9
- import { CredentialBlobSchema, OAuthAccountSchema, UsageWindowsSchema } from "../lib/types.ts";
10
- import { c } from "./render.ts";
11
-
12
- const HarvestedLoginSchema = z.object({
13
- blobRaw: z.string(),
14
- blob: CredentialBlobSchema,
15
- oauthAccount: OAuthAccountSchema,
16
- sampled: UsageWindowsSchema.nullable(),
17
- });
18
- export type HarvestedLogin = z.infer<typeof HarvestedLoginSchema>;
19
-
20
- function identityReady(cjPath: string): boolean {
21
- if (!existsSync(cjPath)) return false;
22
- try {
23
- const oauthAccount = JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount;
24
- return z.object({ accountUuid: z.string().min(1) }).safeParse(oauthAccount).success;
25
- } catch {
26
- return false;
27
- }
28
- }
29
-
30
- export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
31
- const onboardDir = paths.onboardDir;
32
- rmSync(onboardDir, { recursive: true, force: true });
33
- mkdirSync(onboardDir, { recursive: true });
34
- const iso = isolatedTarget(onboardDir);
35
- await deleteItem(iso);
36
- const cjPath = join(onboardDir, ".claude.json");
37
- const real = resolveRealClaude();
38
-
39
- const savedTermios = saveTermios();
40
- const env: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
41
- for (const key of CRED_ENV_OVERRIDES) delete env[key];
42
- const p = Bun.spawn([real], {
43
- stdin: "inherit",
44
- stdout: "inherit",
45
- stderr: "inherit",
46
- env,
47
- });
48
-
49
- try {
50
- let exited = false;
51
- const onExit = p.exited.then(() => { exited = true; });
52
- while (!exited) {
53
- await Bun.sleep(400);
54
- if (identityReady(cjPath) && (await readItem(iso))) {
55
- p.kill();
56
- break;
57
- }
58
- }
59
- await p.exited;
60
- await onExit;
61
- restoreTermios(savedTermios);
62
-
63
- const blobRaw = await readItem(iso);
64
- if (!blobRaw || !identityReady(cjPath)) {
65
- console.error(c.red("no login detected in the isolated session - nothing changed."));
66
- return null;
67
- }
68
-
69
- let blob, oauthAccount;
70
- try {
71
- blob = CredentialBlobSchema.parse(JSON.parse(blobRaw));
72
- oauthAccount = OAuthAccountSchema.parse(JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount);
73
- } catch {
74
- console.error(c.red("could not parse the onboarded account's credential/identity."));
75
- return null;
76
- }
77
-
78
- console.log(c.dim("sampling usage..."));
79
- const sampled = await probeUsage(onboardDir);
80
- if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
81
-
82
- return { blobRaw, blob, oauthAccount, sampled };
83
- } finally {
84
- if (p.exitCode === null) {
85
- p.kill();
86
- await p.exited;
87
- }
88
- restoreTermios(savedTermios);
89
- await deleteItem(iso);
90
- rmSync(onboardDir, { recursive: true, force: true });
91
- }
92
- }
@@ -1,157 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { z } from "zod";
4
- import { withLock } from "./lock.ts";
5
- import { writeFileAtomic } from "./atomic.ts";
6
- import { codexPaths } from "./paths.ts";
7
- import { loadConfig } from "./state.ts";
8
- import { loadCodexAccounts, loadCodexLastSwapAt, saveCodexAccounts } from "./codexstate.ts";
9
- import { isCodexExhausted, pickBestCodex } from "./codexpick.ts";
10
- import { performCodexSwap } from "./codexswap.ts";
11
- import { CodexInvalidGrantError, refreshCodexAuth } from "./codexoauth.ts";
12
- import { fetchCodexUsage } from "./codexusage.ts";
13
- import { codexIdentityOf, isCodexAccessExpiring, readLiveCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
14
- import { liveCodexAccountId } from "./codexsample.ts";
15
- import { livingCodexPresences, presentCodexAccountIds, targetableCodexAccounts } from "./codexpresence.ts";
16
- import { thresholdBars } from "./picker.ts";
17
- import { log } from "./log.ts";
18
- import { CodexAccountSchema, CodexReconcileMarkerSchema, type CodexAccount } from "./types.ts";
19
-
20
- const CodexSwapDecisionSchema = z.object({
21
- swapped: z.boolean(),
22
- account: CodexAccountSchema.nullable(),
23
- reason: z.string(),
24
- });
25
- export type CodexSwapDecision = z.infer<typeof CodexSwapDecisionSchema>;
26
-
27
- const POST_SWAP_COOLDOWN_MS = 45_000;
28
-
29
- async function sampleLiveOntoOwner(input: { now: number }): Promise<string | null> {
30
- const { now } = input;
31
- let live = readLiveCodexAuth();
32
- if (!live) return null;
33
- const index = loadCodexAccounts();
34
- const identity = codexIdentityOf({ auth: live });
35
- const owner = index.accounts.find((account) => account.accountId === identity.accountId);
36
- if (!owner) return null;
37
-
38
- if (isCodexAccessExpiring({ auth: live, now }) && !presentCodexAccountIds().has(identity.accountId)) {
39
- try {
40
- live = await refreshCodexAuth({ auth: live, now });
41
- } catch (e) {
42
- if (e instanceof CodexInvalidGrantError) {
43
- owner.needsReauth = true;
44
- saveCodexAccounts({ index });
45
- log("codexdecide.live_invalid_grant", { account: owner.accountId.slice(0, 8) });
46
- return owner.accountId;
47
- }
48
- throw e;
49
- }
50
- writeLiveCodexAuth({ auth: live });
51
- writeParkedCodexAuth({ credFile: owner.credFile, auth: live });
52
- }
53
- const usage = await fetchCodexUsage({ auth: live });
54
- owner.lastUsage = { aggregate: usage.aggregate, perLimit: usage.perLimit };
55
- owner.lastUsageAt = now;
56
- if (usage.email != null) owner.email = usage.email;
57
- if (usage.planType != null) owner.planType = usage.planType;
58
- saveCodexAccounts({ index });
59
- return owner.accountId;
60
- }
61
-
62
- function reconcileNonLiveSiblings(input: {
63
- index: { accounts: CodexAccount[] };
64
- liveAccountId: string;
65
- bars: { session: number; weekly: number };
66
- now: number;
67
- }): void {
68
- const { index, liveAccountId, bars, now } = input;
69
- const unusable = (account: CodexAccount): boolean =>
70
- account.needsReauth === true || isCodexExhausted({ account, thresholds: bars, now });
71
- const liveAccount = index.accounts.find((account) => account.accountId === liveAccountId);
72
- if (!liveAccount || unusable(liveAccount)) return;
73
- const living = livingCodexPresences();
74
- if (existsSync(codexPaths.reconcileDir)) {
75
- const alive = new Set(living.map((presence) => presence.supervisorId));
76
- for (const name of readdirSync(codexPaths.reconcileDir)) {
77
- if (!alive.has(name)) rmSync(join(codexPaths.reconcileDir, name), { force: true });
78
- }
79
- }
80
- for (const presence of living) {
81
- if (presence.accountId === liveAccountId) continue;
82
- const seated = index.accounts.find((account) => account.accountId === presence.accountId);
83
- if (!seated) continue;
84
- const markerPath = join(codexPaths.reconcileDir, presence.supervisorId);
85
- if (existsSync(markerPath)) continue;
86
- mkdirSync(codexPaths.reconcileDir, { recursive: true });
87
- writeFileAtomic(markerPath, JSON.stringify(CodexReconcileMarkerSchema.parse({ accountId: presence.accountId, ts: now })));
88
- log("codexdecide.reconcile_signal", { supervisorId: presence.supervisorId.slice(0, 8), account: presence.accountId.slice(0, 8) });
89
- }
90
- }
91
-
92
- function postSwapResweep(input: { liveAccountId: string; bars: { session: number; weekly: number }; now: number }): void {
93
- try {
94
- reconcileNonLiveSiblings({ index: loadCodexAccounts(), liveAccountId: input.liveAccountId, bars: input.bars, now: input.now });
95
- } catch (e) {
96
- log("codexdecide.resweep_failed", { err: e instanceof Error ? e.message : String(e) });
97
- }
98
- }
99
-
100
- export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promise<CodexSwapDecision> {
101
- const now = input.now ?? Date.now();
102
- const cfg = loadConfig();
103
- const bars = thresholdBars(cfg);
104
-
105
- return withLock(codexPaths.lockFile, async () => {
106
- let index = loadCodexAccounts();
107
- if (index.accounts.length === 0) return { swapped: false, account: null, reason: "no-pool" };
108
-
109
- const activeId = liveCodexAccountId();
110
- if (activeId == null || !index.accounts.some((account) => account.accountId === activeId)) {
111
- return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
112
- }
113
-
114
- reconcileNonLiveSiblings({ index, liveAccountId: activeId, bars, now });
115
-
116
- const lastSwapAt = loadCodexLastSwapAt();
117
- if (lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
118
- return { swapped: false, account: null, reason: "post-swap-cooldown" };
119
- }
120
-
121
- const activeEntry = index.accounts.find((account) => account.accountId === activeId);
122
- const stale = activeEntry?.lastUsageAt == null || now - activeEntry.lastUsageAt > cfg.policy.usagePollTtlMs;
123
- if (stale) {
124
- const sampledId = await sampleLiveOntoOwner({ now });
125
- if (sampledId == null) {
126
- return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
127
- }
128
- index = loadCodexAccounts();
129
- }
130
-
131
- const active = index.accounts.find((account) => account.accountId === activeId) ?? null;
132
- if (!active) return { swapped: false, account: null, reason: "no-active-account" };
133
-
134
- const engaged = active.needsReauth === true || isCodexExhausted({ account: active, thresholds: bars, now });
135
- if (!engaged) return { swapped: false, account: null, reason: "under-threshold-or-stale" };
136
-
137
- const tried = new Set<string>();
138
- while (true) {
139
- const current = loadCodexAccounts();
140
- const candidates = targetableCodexAccounts({ accounts: current.accounts, activeAccountId: activeId }).filter(
141
- (account) => !tried.has(account.accountId),
142
- );
143
- const best = pickBestCodex({ accounts: candidates, thresholds: bars, now, currentAccountId: activeId });
144
- if (!best) return { swapped: false, account: null, reason: "all-depleted" };
145
- tried.add(best.accountId);
146
- try {
147
- await performCodexSwap({ target: best });
148
- } catch (e) {
149
- if (e instanceof CodexInvalidGrantError) continue;
150
- throw e;
151
- }
152
- log("codexdecide.swap", { account: best.accountId.slice(0, 8) });
153
- postSwapResweep({ liveAccountId: best.accountId, bars, now });
154
- return { swapped: true, account: best, reason: "swapped" };
155
- }
156
- });
157
- }