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.
- package/DESIGN.md +3 -5
- package/README.md +2 -2
- 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/cloudrun.ts +5 -15
- package/src/cli/cursorinit.ts +4 -8
- package/src/cli/doctor.ts +22 -36
- package/src/cli/init.ts +29 -126
- package/src/cli/rename.ts +14 -50
- package/src/cli/render.ts +1 -8
- package/src/cli/rm.ts +27 -44
- package/src/cli/setuptoken.ts +25 -38
- 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 +33 -37
- package/src/cli/codexadd.ts +0 -89
- package/src/cli/codexinit.ts +0 -120
- package/src/cli/codexrm.ts +0 -37
- 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/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
|
|
28
|
+
import { c, emitError } from "./cli/render.ts";
|
|
31
29
|
|
|
32
30
|
const JSON_FLAG = "--json";
|
|
33
31
|
const CACHED_FLAG = "--cached";
|
|
34
|
-
const
|
|
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,
|
|
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
|
|
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 (
|
|
92
|
-
emitError({ json, message: `${
|
|
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
|
|
136
|
-
case "add": return
|
|
137
|
-
case "auth": return cmdAuth(args.slice(1));
|
|
138
|
-
case "doctor": return cmdDoctor(
|
|
139
|
-
case "rm":
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
147
|
-
emitError({
|
|
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)
|
|
152
|
-
emitError({
|
|
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({
|
|
180
|
-
|
|
175
|
+
emitError({ message: `unknown command: ${sub}` });
|
|
176
|
+
printHelp();
|
|
181
177
|
return 2;
|
|
182
178
|
}
|
|
183
179
|
}
|
package/src/cli/codexadd.ts
DELETED
|
@@ -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
|
-
}
|
package/src/cli/codexinit.ts
DELETED
|
@@ -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
|
-
}
|
package/src/cli/codexrm.ts
DELETED
|
@@ -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
|
-
}
|
package/src/cli/codexswitch.ts
DELETED
|
@@ -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
|
-
}
|
package/src/cli/onboard.ts
DELETED
|
@@ -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
|
-
}
|