tokenmaxxing 1.34.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +3 -5
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +23 -40
- package/src/cli/auth.ts +38 -52
- package/src/cli/check.ts +2 -1
- package/src/cli/doctor.ts +13 -13
- package/src/cli/init.ts +29 -126
- package/src/cli/rename.ts +11 -45
- package/src/cli/render.ts +0 -7
- package/src/cli/rm.ts +24 -39
- package/src/cli/setuptoken.ts +16 -16
- package/src/cli/status.ts +95 -238
- package/src/cli/switch.ts +54 -46
- package/src/entries/codexstophook.ts +22 -16
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +3 -2
- package/src/entries/statusline.ts +15 -13
- package/src/entries/stopfailurehook.ts +7 -5
- package/src/entries/stophook.ts +3 -2
- package/src/lib/claude.ts +320 -0
- package/src/lib/codex.ts +353 -0
- package/src/lib/codexauth.ts +6 -0
- package/src/lib/codexpresence.ts +0 -10
- package/src/lib/codexusage.ts +14 -32
- package/src/lib/decide.ts +86 -174
- package/src/lib/oauth.ts +7 -0
- package/src/lib/paths.ts +14 -6
- package/src/lib/picker.ts +56 -37
- package/src/lib/provider.ts +34 -0
- package/src/lib/sample.ts +33 -33
- package/src/lib/state.ts +49 -16
- package/src/lib/swap.ts +41 -38
- package/src/lib/types.ts +17 -51
- package/src/lib/usage.ts +17 -4
- package/src/main.ts +20 -20
- package/src/cli/codexadd.ts +0 -89
- package/src/cli/codexinit.ts +0 -120
- package/src/cli/codexrm.ts +0 -36
- package/src/cli/codexswitch.ts +0 -113
- package/src/cli/onboard.ts +0 -92
- package/src/lib/codexdecide.ts +0 -157
- package/src/lib/codexpick.ts +0 -80
- package/src/lib/codexsample.ts +0 -48
- package/src/lib/codexstate.ts +0 -22
- package/src/lib/codexswap.ts +0 -70
package/src/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";
|
|
@@ -31,7 +29,9 @@ import { c, emitError } from "./cli/render.ts";
|
|
|
31
29
|
|
|
32
30
|
const JSON_FLAG = "--json";
|
|
33
31
|
const CACHED_FLAG = "--cached";
|
|
32
|
+
const CODEX_FLAG = "--codex";
|
|
34
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)
|
|
@@ -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,6 +89,11 @@ async function main(): Promise<number> {
|
|
|
88
89
|
return 2;
|
|
89
90
|
}
|
|
90
91
|
|
|
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
|
+
|
|
91
97
|
if (json && sub != null && !JSON_COMMANDS.has(sub)) {
|
|
92
98
|
emitError({ json, message: `${sub} has no ${JSON_FLAG} form (${JSON_FLAG} applies to ${[...JSON_COMMANDS].join(", ")})` });
|
|
93
99
|
return 2;
|
|
@@ -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,15 +135,12 @@ 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 "init": return cmdInit(provider);
|
|
139
|
+
case "add": return cmdAdd(provider);
|
|
140
|
+
case "auth": return cmdAuth(provider, args.slice(1));
|
|
138
141
|
case "doctor": return cmdDoctor();
|
|
139
|
-
case "rm":
|
|
140
|
-
|
|
141
|
-
return args.includes("--codex") ? cmdCodexRm(rest[0]) : cmdRm(rest[0]);
|
|
142
|
-
}
|
|
143
|
-
case "rename": return cmdRename(args.slice(1));
|
|
142
|
+
case "rm": return cmdRm(provider, args[1]);
|
|
143
|
+
case "rename": return cmdRename(provider, args.slice(1));
|
|
144
144
|
case "setup-token": return cmdSetupToken(args.slice(1));
|
|
145
145
|
case "cursor": {
|
|
146
146
|
if (args[1] === "init") return cmdCursorInit(args.slice(2));
|
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,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
|
-
}
|
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
|
-
}
|