tokenmaxxing 1.33.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.
Files changed (48) hide show
  1. package/DESIGN.md +3 -5
  2. package/README.md +2 -2
  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/cloudrun.ts +5 -15
  8. package/src/cli/cursorinit.ts +4 -8
  9. package/src/cli/doctor.ts +22 -36
  10. package/src/cli/init.ts +29 -126
  11. package/src/cli/rename.ts +14 -50
  12. package/src/cli/render.ts +1 -8
  13. package/src/cli/rm.ts +27 -44
  14. package/src/cli/setuptoken.ts +25 -38
  15. package/src/cli/status.ts +95 -238
  16. package/src/cli/switch.ts +54 -46
  17. package/src/entries/codexstophook.ts +22 -16
  18. package/src/entries/codexsupervisor.ts +3 -3
  19. package/src/entries/sessionstart.ts +3 -2
  20. package/src/entries/statusline.ts +15 -13
  21. package/src/entries/stopfailurehook.ts +7 -5
  22. package/src/entries/stophook.ts +3 -2
  23. package/src/lib/claude.ts +320 -0
  24. package/src/lib/codex.ts +353 -0
  25. package/src/lib/codexauth.ts +6 -0
  26. package/src/lib/codexpresence.ts +0 -10
  27. package/src/lib/codexusage.ts +14 -32
  28. package/src/lib/decide.ts +86 -174
  29. package/src/lib/oauth.ts +7 -0
  30. package/src/lib/paths.ts +14 -6
  31. package/src/lib/picker.ts +56 -37
  32. package/src/lib/provider.ts +34 -0
  33. package/src/lib/sample.ts +33 -33
  34. package/src/lib/state.ts +49 -16
  35. package/src/lib/swap.ts +41 -38
  36. package/src/lib/types.ts +17 -51
  37. package/src/lib/usage.ts +17 -4
  38. package/src/main.ts +33 -37
  39. package/src/cli/codexadd.ts +0 -89
  40. package/src/cli/codexinit.ts +0 -120
  41. package/src/cli/codexrm.ts +0 -37
  42. package/src/cli/codexswitch.ts +0 -113
  43. package/src/cli/onboard.ts +0 -92
  44. package/src/lib/codexdecide.ts +0 -157
  45. package/src/lib/codexpick.ts +0 -80
  46. package/src/lib/codexsample.ts +0 -48
  47. package/src/lib/codexstate.ts +0 -22
  48. package/src/lib/codexswap.ts +0 -70
package/src/main.ts CHANGED
@@ -7,18 +7,16 @@ import { runSubagentStatusline } from "./entries/subagentstatusline.ts";
7
7
  import { runStopHook } from "./entries/stophook.ts";
8
8
  import { runStopFailureHook } from "./entries/stopfailurehook.ts";
9
9
  import { runSessionStart } from "./entries/sessionstart.ts";
10
+ import { runCodexSupervisor } from "./entries/codexsupervisor.ts";
11
+ import { runCodexStopHook } from "./entries/codexstophook.ts";
12
+ import { claude } from "./lib/claude.ts";
13
+ import { codex } from "./lib/codex.ts";
10
14
  import { cmdInit } from "./cli/init.ts";
11
15
  import { cmdAdd } from "./cli/add.ts";
12
16
  import { cmdAuth } from "./cli/auth.ts";
13
- import { cmdCodexAdd } from "./cli/codexadd.ts";
14
- import { cmdCodexInit } from "./cli/codexinit.ts";
15
- import { cmdCodexSwitch } from "./cli/codexswitch.ts";
16
- import { runCodexSupervisor } from "./entries/codexsupervisor.ts";
17
- import { runCodexStopHook } from "./entries/codexstophook.ts";
18
17
  import { cmdStatus } from "./cli/status.ts";
19
18
  import { cmdDoctor } from "./cli/doctor.ts";
20
19
  import { cmdRm } from "./cli/rm.ts";
21
- import { cmdCodexRm } from "./cli/codexrm.ts";
22
20
  import { cmdRename } from "./cli/rename.ts";
23
21
  import { cmdSwitch } from "./cli/switch.ts";
24
22
  import { cmdCheck } from "./cli/check.ts";
@@ -27,11 +25,13 @@ import { cmdSetupToken } from "./cli/setuptoken.ts";
27
25
  import { cmdCloudRun } from "./cli/cloudrun.ts";
28
26
  import { cmdCursorInit } from "./cli/cursorinit.ts";
29
27
  import { timerDeactivationHint, uninstallSupervisor } from "./lib/install.ts";
30
- import { c, emitError, emitJson } from "./cli/render.ts";
28
+ import { c, emitError } from "./cli/render.ts";
31
29
 
32
30
  const JSON_FLAG = "--json";
33
31
  const CACHED_FLAG = "--cached";
34
- const INTERACTIVE_COMMANDS = new Set(["init", "add", "auth"]);
32
+ const CODEX_FLAG = "--codex";
33
+ const JSON_COMMANDS = new Set(["status", "config", "check", "switch"]);
34
+ const CODEX_COMMANDS = new Set(["init", "add", "auth", "switch", "rm", "rename"]);
35
35
 
36
36
  function printHelp(): void {
37
37
  console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
@@ -43,7 +43,7 @@ function printHelp(): void {
43
43
  ${c.cyan("tokenmaxxing init --codex")} same for codex: import login, install codex supervisor + Stop hook (trust it via /hooks)
44
44
  ${c.cyan("tokenmaxxing add")} register an additional account (isolated login)
45
45
  ${c.cyan("tokenmaxxing add --codex")} register an additional codex account (isolated login)
46
- ${c.cyan("tokenmaxxing auth")} [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every needs-reauth account, one by one)
46
+ ${c.cyan("tokenmaxxing auth")} [--codex] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every needs-reauth account, one by one)
47
47
  ${c.cyan("tokenmaxxing switch --codex")} [sel] switch the codex pool (takes effect on next codex start)
48
48
  ${c.cyan("tokenmaxxing status")} [--cached] accounts with 5h / weekly / per-model usage bars (--cached: the stored figures, no sampling)
49
49
  ${c.cyan("tokenmaxxing config")} print the config path and the effective values (edit the file in an editor)
@@ -55,7 +55,7 @@ function printHelp(): void {
55
55
  ${c.cyan("tokenmaxxing cursor init")} [dir] write the Claude relay subagent (.cursor/agents/claude.md) and .cursor/environment.json into a repo
56
56
  ${c.cyan("tokenmaxxing cloud run")} [--session <id>] [--max-turns <n>] "<prompt>" on a Cursor Cloud VM: run claude -p on a setup token from TOKENMAXXING_TOKENS, rotate to the next token on a usage limit
57
57
 
58
- ${c.cyan("--json")} print one JSON document on stdout instead of text (status, config, doctor, check, switch, rename, rm, uninstall, setup-token --print, cursor init, cloud run); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
58
+ ${c.cyan("--json")} print one JSON document on stdout instead of text (status, config, check, switch); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
59
59
 
60
60
  ${c.dim("(aliased as")} ${c.cyan("xx")}${c.dim(")")} - then just run ${c.bold("claude")} as always; it switches accounts near quota automatically.`);
61
61
  }
@@ -80,7 +80,8 @@ async function main(): Promise<number> {
80
80
  jsonMode = argv.includes(JSON_FLAG);
81
81
  const json = jsonMode;
82
82
  const cached = argv.includes(CACHED_FLAG);
83
- const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG);
83
+ const provider = argv.includes(CODEX_FLAG) ? codex : claude;
84
+ const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== CODEX_FLAG);
84
85
  const sub = args[0];
85
86
 
86
87
  if (cached && sub != null && sub !== "status") {
@@ -88,8 +89,13 @@ async function main(): Promise<number> {
88
89
  return 2;
89
90
  }
90
91
 
91
- if (json && sub != null && INTERACTIVE_COMMANDS.has(sub)) {
92
- emitError({ json, message: `${sub} is interactive (it runs a login flow) and has no --json form` });
92
+ if (provider === codex && (sub == null || !CODEX_COMMANDS.has(sub))) {
93
+ emitError({ json, message: `${CODEX_FLAG} applies to ${[...CODEX_COMMANDS].join(", ")}, not ${sub ?? "status"}` });
94
+ return 2;
95
+ }
96
+
97
+ if (json && sub != null && !JSON_COMMANDS.has(sub)) {
98
+ emitError({ json, message: `${sub} has no ${JSON_FLAG} form (${JSON_FLAG} applies to ${[...JSON_COMMANDS].join(", ")})` });
93
99
  return 2;
94
100
  }
95
101
  if (!(sub != null && sub.startsWith("__")) && !process.env.TOKENMAXXING_PROBE) {
@@ -120,10 +126,7 @@ async function main(): Promise<number> {
120
126
  }
121
127
  return cmdStatus({ json, cached });
122
128
  }
123
- case "switch": {
124
- const rest = args.slice(1).filter((a) => a !== "--codex");
125
- return args.includes("--codex") ? cmdCodexSwitch(rest[0], json) : cmdSwitch(rest[0], json);
126
- }
129
+ case "switch": return cmdSwitch(provider, args[1], json);
127
130
  case "check": {
128
131
  if (args.length > 1) {
129
132
  emitError({ json, message: `unknown check option: ${args[1]} (check takes no options; the timer runs a plain check every tick)` });
@@ -132,24 +135,21 @@ async function main(): Promise<number> {
132
135
  return cmdCheck(json);
133
136
  }
134
137
  case "config": return cmdConfig(args.slice(1), json);
135
- case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
136
- case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
137
- case "auth": return cmdAuth(args.slice(1));
138
- case "doctor": return cmdDoctor(json);
139
- case "rm": {
140
- const rest = args.slice(1).filter((a) => a !== "--codex");
141
- return args.includes("--codex") ? cmdCodexRm(rest[0], json) : cmdRm(rest[0], json);
142
- }
143
- case "rename": return cmdRename(args.slice(1), json);
144
- case "setup-token": return cmdSetupToken(args.slice(1), json);
138
+ case "init": return cmdInit(provider);
139
+ case "add": return cmdAdd(provider);
140
+ case "auth": return cmdAuth(provider, args.slice(1));
141
+ case "doctor": return cmdDoctor();
142
+ case "rm": return cmdRm(provider, args[1]);
143
+ case "rename": return cmdRename(provider, args.slice(1));
144
+ case "setup-token": return cmdSetupToken(args.slice(1));
145
145
  case "cursor": {
146
- if (args[1] === "init") return cmdCursorInit(args.slice(2), json);
147
- emitError({ json, message: "usage: tokenmaxxing cursor init [dir]" });
146
+ if (args[1] === "init") return cmdCursorInit(args.slice(2));
147
+ emitError({ message: "usage: tokenmaxxing cursor init [dir]" });
148
148
  return 2;
149
149
  }
150
150
  case "cloud": {
151
- if (args[1] === "run") return cmdCloudRun(args.slice(2), json);
152
- emitError({ json, message: 'usage: tokenmaxxing cloud run [--session <id>] [--max-turns <n>] "<prompt>"' });
151
+ if (args[1] === "run") return cmdCloudRun(args.slice(2));
152
+ emitError({ message: 'usage: tokenmaxxing cloud run [--session <id>] [--max-turns <n>] "<prompt>"' });
153
153
  return 2;
154
154
  }
155
155
  case "uninstall": {
@@ -160,10 +160,6 @@ async function main(): Promise<number> {
160
160
  ...(out.timerDeactivated ? ["check timer"] : []),
161
161
  ...(out.pathLineRemoved ? ["rc PATH line"] : []),
162
162
  ];
163
- if (json) {
164
- emitJson({ ok: true, removed, timerDeactivated: out.timerDeactivated, pathLineRemoved: out.pathLineRemoved });
165
- return 0;
166
- }
167
163
  console.log(`removed ${removed.join(", ")}`);
168
164
  if (!out.timerDeactivated) console.log(c.yellow(`⚠ the check job may still be loaded - run: ${timerDeactivationHint()}`));
169
165
  if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
@@ -176,8 +172,8 @@ async function main(): Promise<number> {
176
172
  printHelp();
177
173
  return 0;
178
174
  default:
179
- emitError({ json, message: `unknown command: ${sub}` });
180
- if (!json) printHelp();
175
+ emitError({ message: `unknown command: ${sub}` });
176
+ printHelp();
181
177
  return 2;
182
178
  }
183
179
  }
@@ -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,37 +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, emitJson, plain } from "./render.ts";
9
-
10
- export async function cmdCodexRm(selector?: string, json = false): Promise<number> {
11
- if (!selector) {
12
- emitError({ json, 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({ json, message: `no codex account matches "${selector}"` });
20
- return 1;
21
- }
22
- if (liveCodexAccountId() === account.accountId) {
23
- emitError({ json, 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({ json, 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
- if (json) emitJson({ ok: true, pool: "codex", removed: account.label, remaining: index.accounts.length });
34
- else console.log(`removed codex account ${c.bold(account.label)} from the pool (${index.accounts.length} left)`);
35
- return 0;
36
- });
37
- }
@@ -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
- }