codex-account-orchestrator 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2025-01-27
9
+
10
+ ### Added
11
+
12
+ - Account management with separate CODEX_HOME directories per account
13
+ - OAuth login support with browser and device auth flows
14
+ - Automatic fallback to next account on quota exhaustion
15
+ - Gateway mode for seamless account switching without session drops
16
+ - Codex shim installation for transparent traffic routing
17
+ - Token refresh and session management
18
+ - Configurable cooldown and retry settings
19
+ - Debug logging with header sanitization
20
+
21
+ ### Commands
22
+
23
+ - `cao add <name>` - Register a new account
24
+ - `cao list` - List registered accounts
25
+ - `cao use <name>` - Set default account
26
+ - `cao remove <name>` - Remove an account
27
+ - `cao run` - Run codex with automatic fallback
28
+ - `cao gateway start` - Start local gateway server
29
+ - `cao gateway enable` - Install codex routing shim
30
+ - `cao gateway disable` - Remove shim and restore config
31
+ - `cao gateway status` - Show gateway and account status
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 DAWNCR0W
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # codex-account-orchestrator
2
+
3
+ Codex OAuth account fallback orchestrator. It keeps **separate `CODEX_HOME` directories per account** and automatically falls back to the next account when quota is exhausted.
4
+
5
+ ## Install (local dev)
6
+
7
+ ```bash
8
+ npm install
9
+ npm run build
10
+ npm link # Makes 'cao' command available globally
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Add accounts
16
+
17
+ ```bash
18
+ cao add accountA
19
+ cao add accountB
20
+ ```
21
+
22
+ `cao add` starts OAuth login by default and should open a browser window. Use `--no-login` to skip:
23
+
24
+ ```bash
25
+ cao add accountA --no-login
26
+ ```
27
+
28
+ If you prefer device auth:
29
+
30
+ ```bash
31
+ cao add accountA --device-auth
32
+ ```
33
+
34
+ ### Set default account
35
+
36
+ ```bash
37
+ cao use accountA
38
+ ```
39
+
40
+ ### List accounts
41
+
42
+ ```bash
43
+ cao list
44
+ ```
45
+
46
+ ### Remove an account
47
+
48
+ ```bash
49
+ cao remove accountB
50
+ ```
51
+
52
+ To keep files on disk:
53
+
54
+ ```bash
55
+ cao remove accountB --keep-files
56
+ ```
57
+
58
+ ### Run with fallback
59
+
60
+ ```bash
61
+ cao run
62
+ ```
63
+
64
+ To pass arguments to Codex, put them after `--`:
65
+
66
+ ```bash
67
+ cao run -- exec "summarize README"
68
+ ```
69
+
70
+ To recheck all accounts when everyone is quota-limited, use multiple passes:
71
+
72
+ ```bash
73
+ cao run --max-passes 2 --retry-delay 5
74
+ ```
75
+
76
+ ### Run a specific account (no fallback)
77
+
78
+ ```bash
79
+ cao run --no-fallback --account accountB -- codex
80
+ ```
81
+
82
+ ### Custom data directory
83
+
84
+ ```bash
85
+ cao --data-dir /path/to/data run -- codex
86
+ ```
87
+
88
+ ## How it works
89
+
90
+ - Each account is stored in its own `CODEX_HOME` directory under:
91
+ - `~/.codex-account-orchestrator/<account>/`
92
+ - A `config.toml` is created with:
93
+ - `cli_auth_credentials_store = "file"`
94
+ - `forced_login_method = "chatgpt"`
95
+ - On quota errors (detected via output keywords), the CLI re-runs Codex with the next account and can recheck all accounts for a configurable number of passes.
96
+
97
+ ## Gateway mode (no session drop)
98
+
99
+ Gateway mode keeps the Codex session open while switching accounts on quota errors. It requires routing Codex traffic through the local gateway.
100
+
101
+ ### Start the gateway
102
+
103
+ ```bash
104
+ cao gateway start
105
+ ```
106
+
107
+ For troubleshooting, you can pass through the current Codex auth without overriding it:
108
+
109
+ ```bash
110
+ cao gateway start --passthrough-auth
111
+ ```
112
+
113
+ ### Enable routing for Codex
114
+
115
+ ```bash
116
+ cao gateway enable
117
+ ```
118
+
119
+ This installs a small `codex` shim (in `~/.local/bin`) that sets `OPENAI_BASE_URL` to the gateway. You can revert with:
120
+
121
+ ```bash
122
+ cao gateway disable
123
+ ```
124
+
125
+ If `~/.local/bin` is not in your PATH, add it so the shim is used.
126
+
127
+ ### Check gateway status
128
+
129
+ ```bash
130
+ cao gateway status
131
+ ```
132
+
133
+ ### Debug logging
134
+
135
+ Set `CAO_DEBUG_HEADERS=1` to log sanitized request/response headers in the gateway process. To capture the last request body for inspection, also set `CAO_CAPTURE_BODY=1` (saved to `/tmp/cao-last-body.json` by default).
136
+
137
+ ## Notes
138
+
139
+ - Fallback requires capturing output; this may make Codex detect a non-TTY stdout. If you want a pure TTY session, use `--no-fallback`.
140
+ - The quota detector is keyword-based and can be extended in `src/constants.ts`.
@@ -0,0 +1,9 @@
1
+ import { Registry } from "./registry_store";
2
+ export declare function validateAccountName(name: string): string;
3
+ export declare function ensureBaseDir(baseDir: string): void;
4
+ export declare function ensureAccountDir(baseDir: string, accountName: string): string;
5
+ export declare function ensureAccountConfig(accountDir: string): void;
6
+ export declare function addAccount(baseDir: string, accountName: string): Registry;
7
+ export declare function setDefaultAccount(baseDir: string, accountName: string): Registry;
8
+ export declare function removeAccount(baseDir: string, accountName: string, removeFiles: boolean): Registry;
9
+ export declare function getAccountOrder(registry: Registry): string[];
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validateAccountName = validateAccountName;
7
+ exports.ensureBaseDir = ensureBaseDir;
8
+ exports.ensureAccountDir = ensureAccountDir;
9
+ exports.ensureAccountConfig = ensureAccountConfig;
10
+ exports.addAccount = addAccount;
11
+ exports.setDefaultAccount = setDefaultAccount;
12
+ exports.removeAccount = removeAccount;
13
+ exports.getAccountOrder = getAccountOrder;
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const constants_1 = require("./constants");
17
+ const paths_1 = require("./paths");
18
+ const registry_store_1 = require("./registry_store");
19
+ function validateAccountName(name) {
20
+ const trimmed = name.trim();
21
+ if (trimmed.length === 0) {
22
+ throw new Error("Account name cannot be empty.");
23
+ }
24
+ if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
25
+ throw new Error("Account name must use letters, numbers, underscores, or hyphens only.");
26
+ }
27
+ return trimmed;
28
+ }
29
+ function ensureBaseDir(baseDir) {
30
+ if (!fs_1.default.existsSync(baseDir)) {
31
+ fs_1.default.mkdirSync(baseDir, { recursive: true });
32
+ }
33
+ }
34
+ function ensureAccountDir(baseDir, accountName) {
35
+ const accountDir = (0, paths_1.getAccountDir)(baseDir, accountName);
36
+ if (!fs_1.default.existsSync(accountDir)) {
37
+ fs_1.default.mkdirSync(accountDir, { recursive: true });
38
+ }
39
+ return accountDir;
40
+ }
41
+ function ensureAccountConfig(accountDir) {
42
+ const configPath = path_1.default.join(accountDir, "config.toml");
43
+ if (!fs_1.default.existsSync(configPath)) {
44
+ fs_1.default.writeFileSync(configPath, constants_1.DEFAULT_CONFIG_TOML, "utf8");
45
+ }
46
+ }
47
+ function addAccount(baseDir, accountName) {
48
+ ensureBaseDir(baseDir);
49
+ const registry = (0, registry_store_1.loadRegistry)(baseDir);
50
+ const normalizedName = validateAccountName(accountName);
51
+ ensureAccountConfig(ensureAccountDir(baseDir, normalizedName));
52
+ if (!registry.accounts.includes(normalizedName)) {
53
+ registry.accounts.push(normalizedName);
54
+ }
55
+ if (!registry.default_account) {
56
+ registry.default_account = normalizedName;
57
+ }
58
+ (0, registry_store_1.saveRegistry)(baseDir, registry);
59
+ return registry;
60
+ }
61
+ function setDefaultAccount(baseDir, accountName) {
62
+ ensureBaseDir(baseDir);
63
+ const registry = (0, registry_store_1.loadRegistry)(baseDir);
64
+ const normalizedName = validateAccountName(accountName);
65
+ if (!registry.accounts.includes(normalizedName)) {
66
+ throw new Error(`Account not found: ${normalizedName}`);
67
+ }
68
+ registry.default_account = normalizedName;
69
+ (0, registry_store_1.saveRegistry)(baseDir, registry);
70
+ return registry;
71
+ }
72
+ function removeAccount(baseDir, accountName, removeFiles) {
73
+ ensureBaseDir(baseDir);
74
+ const registry = (0, registry_store_1.loadRegistry)(baseDir);
75
+ const normalizedName = validateAccountName(accountName);
76
+ const index = registry.accounts.indexOf(normalizedName);
77
+ if (index === -1) {
78
+ throw new Error(`Account not found: ${normalizedName}`);
79
+ }
80
+ registry.accounts.splice(index, 1);
81
+ if (registry.default_account === normalizedName) {
82
+ registry.default_account = registry.accounts.length > 0 ? registry.accounts[0] : null;
83
+ }
84
+ if (removeFiles) {
85
+ const accountDir = (0, paths_1.getAccountDir)(baseDir, normalizedName);
86
+ if (fs_1.default.existsSync(accountDir)) {
87
+ fs_1.default.rmSync(accountDir, { recursive: true, force: true });
88
+ }
89
+ }
90
+ (0, registry_store_1.saveRegistry)(baseDir, registry);
91
+ return registry;
92
+ }
93
+ function getAccountOrder(registry) {
94
+ if (registry.accounts.length === 0) {
95
+ return [];
96
+ }
97
+ const defaultName = registry.default_account;
98
+ if (!defaultName) {
99
+ return [...registry.accounts];
100
+ }
101
+ const rest = registry.accounts.filter((name) => name !== defaultName);
102
+ return [defaultName, ...rest];
103
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const account_manager_1 = require("./account_manager");
11
+ const codex_auth_1 = require("./codex_auth");
12
+ const constants_1 = require("./constants");
13
+ const codex_config_1 = require("./gateway/codex_config");
14
+ const account_pool_1 = require("./gateway/account_pool");
15
+ const gateway_config_1 = require("./gateway/gateway_config");
16
+ const server_1 = require("./gateway/server");
17
+ const codex_shim_1 = require("./gateway/codex_shim");
18
+ const paths_1 = require("./paths");
19
+ const registry_store_1 = require("./registry_store");
20
+ const process_runner_1 = require("./process_runner");
21
+ const program = new commander_1.Command();
22
+ program
23
+ .name("codex-account-orchestrator")
24
+ .description("Codex OAuth account fallback orchestrator")
25
+ .option("--data-dir <path>", "Custom data directory");
26
+ program
27
+ .command("add")
28
+ .argument("<name>", "Account name")
29
+ .description("Register a new account and create its config")
30
+ .option("--codex <path>", "Path to the codex binary", "codex")
31
+ .option("--no-login", "Skip OAuth login")
32
+ .option("--device-auth", "Use device auth flow")
33
+ .action(async (name, options) => {
34
+ const baseDir = (0, paths_1.getBaseDir)(program.opts().dataDir);
35
+ const registry = (0, account_manager_1.addAccount)(baseDir, name);
36
+ const normalizedName = (0, account_manager_1.validateAccountName)(name);
37
+ const accountDir = (0, paths_1.getAccountDir)(baseDir, normalizedName);
38
+ process.stdout.write(`Added account: ${normalizedName}\n`);
39
+ process.stdout.write(`Account directory: ${accountDir}\n`);
40
+ process.stdout.write(`Default account: ${registry.default_account ?? "(none)"}\n`);
41
+ process.stdout.write("Run `cao run` to start with fallback.\n");
42
+ if (!options.login) {
43
+ return;
44
+ }
45
+ const alreadyLoggedIn = await (0, codex_auth_1.isCodexLoggedIn)(options.codex, accountDir);
46
+ if (alreadyLoggedIn) {
47
+ process.stdout.write("OAuth already configured for this account.\n");
48
+ return;
49
+ }
50
+ process.stdout.write("Starting OAuth login...\n");
51
+ const exitCode = await (0, codex_auth_1.runCodexLogin)(options.codex, accountDir, options.deviceAuth);
52
+ if (exitCode !== 0) {
53
+ process.stderr.write("OAuth login failed.\n");
54
+ process.exit(exitCode);
55
+ }
56
+ const authPath = getAuthFilePath(accountDir);
57
+ if (!fs_1.default.existsSync(authPath)) {
58
+ process.stderr.write("Warning: OAuth login completed but auth.json was not found. Check your Codex auth store.\n");
59
+ }
60
+ });
61
+ program
62
+ .command("list")
63
+ .description("List registered accounts")
64
+ .action(() => {
65
+ const baseDir = (0, paths_1.getBaseDir)(program.opts().dataDir);
66
+ (0, account_manager_1.ensureBaseDir)(baseDir);
67
+ const registry = (0, registry_store_1.loadRegistry)(baseDir);
68
+ if (registry.accounts.length === 0) {
69
+ process.stdout.write("No accounts registered. Use `cao add <name>` first.\n");
70
+ return;
71
+ }
72
+ for (const name of registry.accounts) {
73
+ const marker = registry.default_account === name ? "*" : " ";
74
+ const accountDir = (0, paths_1.getAccountDir)(baseDir, name);
75
+ const loggedIn = fs_1.default.existsSync(getAuthFilePath(accountDir));
76
+ const status = loggedIn ? "logged-in" : "not-logged-in";
77
+ process.stdout.write(`${marker} ${name} (${status})\n`);
78
+ }
79
+ });
80
+ program
81
+ .command("use")
82
+ .argument("<name>", "Account name")
83
+ .description("Set the default account")
84
+ .action((name) => {
85
+ const baseDir = (0, paths_1.getBaseDir)(program.opts().dataDir);
86
+ const registry = (0, account_manager_1.setDefaultAccount)(baseDir, name);
87
+ process.stdout.write(`Default account set to: ${registry.default_account}\n`);
88
+ });
89
+ program
90
+ .command("remove")
91
+ .argument("<name>", "Account name")
92
+ .option("--keep-files", "Keep account files on disk")
93
+ .description("Remove an account from fallback rotation")
94
+ .action((name, options) => {
95
+ const baseDir = (0, paths_1.getBaseDir)(program.opts().dataDir);
96
+ const registry = (0, account_manager_1.removeAccount)(baseDir, name, !options.keepFiles);
97
+ process.stdout.write(`Removed account: ${name}\n`);
98
+ process.stdout.write(`Default account: ${registry.default_account ?? "(none)"}\n`);
99
+ });
100
+ const gateway = program.command("gateway").description("Manage CAO gateway");
101
+ gateway
102
+ .command("start")
103
+ .description("Start the local gateway for seamless account switching")
104
+ .option("--bind <address>", "Bind address", "127.0.0.1")
105
+ .option("--port <port>", "Port", "4319")
106
+ .option("--base-url <url>", "Upstream OpenAI base URL", "https://chatgpt.com/backend-api/codex")
107
+ .option("--cooldown-seconds <seconds>", "Cooldown for quota-hit accounts", "900")
108
+ .option("--max-retry-passes <count>", "Max retry passes per request", "1")
109
+ .option("--timeout-ms <ms>", "Request timeout in milliseconds", "120000")
110
+ .option("--passthrough-auth", "Do not override Authorization header")
111
+ .option("--save", "Persist options to gateway.json")
112
+ .action((options) => {
113
+ const baseDir = (0, paths_1.getBaseDir)(program.opts().dataDir);
114
+ (0, account_manager_1.ensureBaseDir)(baseDir);
115
+ const overrides = buildGatewayOverrides(options);
116
+ const merged = (0, gateway_config_1.resolveGatewayConfig)({
117
+ ...(0, gateway_config_1.loadGatewayConfig)(),
118
+ ...overrides
119
+ });
120
+ if (options.save) {
121
+ (0, gateway_config_1.saveGatewayConfig)({
122
+ bindAddress: merged.bindAddress,
123
+ port: merged.port,
124
+ baseUrl: merged.baseUrl,
125
+ cooldownSeconds: merged.cooldownSeconds,
126
+ maxRetryPasses: merged.maxRetryPasses,
127
+ requestTimeoutMs: merged.requestTimeoutMs,
128
+ overrideAuth: merged.overrideAuth
129
+ });
130
+ }
131
+ const pool = account_pool_1.AccountPool.loadFromRegistry(baseDir);
132
+ if (pool.getAccounts().length === 0) {
133
+ process.stderr.write("No accounts with auth.json were found. Run `cao add` first.\n");
134
+ process.exit(1);
135
+ return;
136
+ }
137
+ const server = (0, server_1.startGatewayServer)(pool, merged);
138
+ process.stdout.write(`Gateway started on http://${merged.bindAddress}:${merged.port} (upstream ${merged.baseUrl})\n`);
139
+ process.on("SIGINT", () => {
140
+ server.close(() => {
141
+ process.stdout.write("Gateway stopped.\n");
142
+ process.exit(0);
143
+ });
144
+ });
145
+ });
146
+ gateway
147
+ .command("status")
148
+ .description("Show gateway config and account readiness")
149
+ .action(() => {
150
+ const baseDir = (0, paths_1.getBaseDir)(program.opts().dataDir);
151
+ const config = (0, gateway_config_1.resolveGatewayConfig)((0, gateway_config_1.loadGatewayConfig)());
152
+ const pool = account_pool_1.AccountPool.loadFromRegistry(baseDir);
153
+ process.stdout.write(`Bind: ${config.bindAddress}:${config.port}\n`);
154
+ process.stdout.write(`Upstream: ${config.baseUrl}\n`);
155
+ process.stdout.write(`Override auth: ${config.overrideAuth ? "yes" : "no"}\n`);
156
+ process.stdout.write(`Accounts: ${pool.getAccounts().length}\n`);
157
+ for (const account of pool.getAccounts()) {
158
+ const cooldown = account.cooldownUntilMs > Date.now()
159
+ ? `cooldown ${Math.ceil((account.cooldownUntilMs - Date.now()) / 1000)}s`
160
+ : "ready";
161
+ process.stdout.write(`- ${account.name} (${cooldown})\n`);
162
+ }
163
+ });
164
+ gateway
165
+ .command("enable")
166
+ .description("Install a codex shim that routes traffic through the gateway")
167
+ .option("--base-url <url>", "Gateway base URL", "http://127.0.0.1:4319")
168
+ .action((options) => {
169
+ (0, codex_config_1.disableGatewayConfig)();
170
+ const result = (0, codex_shim_1.enableGatewayShim)(options.baseUrl);
171
+ process.stdout.write(`Codex shim installed: ${result.shimPath}\n`);
172
+ process.stdout.write(`Real codex: ${result.realCodexPath}\n`);
173
+ if (!result.inPath) {
174
+ process.stdout.write(`Warning: ${path_1.default.dirname(result.shimPath)} is not in PATH. Update PATH to use the shim.\n`);
175
+ }
176
+ });
177
+ gateway
178
+ .command("disable")
179
+ .description("Remove the codex shim and restore config backup if present")
180
+ .action(() => {
181
+ const removed = (0, codex_shim_1.disableGatewayShim)();
182
+ (0, codex_config_1.disableGatewayConfig)();
183
+ process.stdout.write(`Codex config restored: ${(0, codex_config_1.getCodexConfigPath)()}\n`);
184
+ process.stdout.write(removed ? "Codex shim removed.\n" : "No codex shim found.\n");
185
+ });
186
+ program
187
+ .command("run")
188
+ .option("--account <name>", "Run with a specific account")
189
+ .option("--codex <path>", "Path to the codex binary", "codex")
190
+ .option("--no-fallback", "Disable automatic fallback")
191
+ .option("--max-passes <count>", "Retry passes when all accounts hit quota", "2")
192
+ .option("--retry-delay <seconds>", "Delay between retry passes in seconds", "0")
193
+ .description("Run codex with OAuth fallback across accounts")
194
+ .allowUnknownOption(true)
195
+ .allowExcessArguments(true)
196
+ .action(async (options) => {
197
+ const baseDir = (0, paths_1.getBaseDir)(program.opts().dataDir);
198
+ (0, account_manager_1.ensureBaseDir)(baseDir);
199
+ const registry = (0, registry_store_1.loadRegistry)(baseDir);
200
+ const orderedAccounts = (0, account_manager_1.getAccountOrder)(registry);
201
+ const codexArgs = normalizeCodexArgs(getCodexArgs(process.argv), options.codex);
202
+ if (orderedAccounts.length === 0) {
203
+ process.stderr.write("No accounts registered. Use `cao add <name>` first.\n");
204
+ process.exit(1);
205
+ return;
206
+ }
207
+ const resolvedAccounts = resolveAccounts(orderedAccounts, options.account);
208
+ if (resolvedAccounts.length === 0) {
209
+ process.stderr.write("No matching accounts found.\n");
210
+ process.exit(1);
211
+ return;
212
+ }
213
+ await runWithFallback(options, baseDir, resolvedAccounts, codexArgs);
214
+ });
215
+ program.parse(process.argv);
216
+ function resolveAccounts(ordered, requested) {
217
+ if (!requested) {
218
+ return ordered;
219
+ }
220
+ const normalized = (0, account_manager_1.validateAccountName)(requested);
221
+ if (ordered.includes(normalized)) {
222
+ return [normalized];
223
+ }
224
+ return [];
225
+ }
226
+ function getCodexArgs(argv) {
227
+ const separatorIndex = argv.indexOf("--");
228
+ if (separatorIndex === -1) {
229
+ return [];
230
+ }
231
+ return argv.slice(separatorIndex + 1);
232
+ }
233
+ function normalizeCodexArgs(args, codexBin) {
234
+ if (args.length === 0) {
235
+ return args;
236
+ }
237
+ if (codexBin === "codex" && args[0] === "codex") {
238
+ return args.slice(1);
239
+ }
240
+ return args;
241
+ }
242
+ function getAuthFilePath(accountDir) {
243
+ return path_1.default.join(accountDir, constants_1.AUTH_FILE_NAME);
244
+ }
245
+ function buildGatewayOverrides(options) {
246
+ const overrides = {};
247
+ overrides.bindAddress = options.bind;
248
+ overrides.baseUrl = options.baseUrl;
249
+ overrides.overrideAuth = !options.passthroughAuth;
250
+ const port = Number.parseInt(options.port, 10);
251
+ if (!Number.isNaN(port)) {
252
+ overrides.port = port;
253
+ }
254
+ const cooldown = Number.parseInt(options.cooldownSeconds, 10);
255
+ if (!Number.isNaN(cooldown)) {
256
+ overrides.cooldownSeconds = cooldown;
257
+ }
258
+ const maxRetries = Number.parseInt(options.maxRetryPasses, 10);
259
+ if (!Number.isNaN(maxRetries)) {
260
+ overrides.maxRetryPasses = maxRetries;
261
+ }
262
+ const timeout = Number.parseInt(options.timeoutMs, 10);
263
+ if (!Number.isNaN(timeout)) {
264
+ overrides.requestTimeoutMs = timeout;
265
+ }
266
+ return overrides;
267
+ }
268
+ async function runWithFallback(options, baseDir, accounts, codexArgs) {
269
+ const codexBin = options.codex;
270
+ const maxPasses = normalizeMaxPasses(options.maxPasses);
271
+ const retryDelayMs = normalizeDelay(options.retryDelay);
272
+ for (let passIndex = 0; passIndex < maxPasses; passIndex += 1) {
273
+ let quotaFailures = 0;
274
+ let lastExitCode = 1;
275
+ for (let index = 0; index < accounts.length; index += 1) {
276
+ const name = accounts[index];
277
+ const accountDir = (0, account_manager_1.ensureAccountDir)(baseDir, name);
278
+ (0, account_manager_1.ensureAccountConfig)(accountDir);
279
+ process.stderr.write(`Using account: ${name}\n`);
280
+ const result = await (0, process_runner_1.runCodexOnce)(codexBin, codexArgs, accountDir, options.fallback);
281
+ lastExitCode = result.exitCode;
282
+ if (result.exitCode === 0) {
283
+ process.exit(0);
284
+ return;
285
+ }
286
+ if (!options.fallback) {
287
+ process.exit(result.exitCode);
288
+ return;
289
+ }
290
+ if (!result.quotaError) {
291
+ process.exit(result.exitCode);
292
+ return;
293
+ }
294
+ quotaFailures += 1;
295
+ const nextName = accounts[index + 1];
296
+ if (nextName) {
297
+ process.stderr.write(`Quota exhausted. Falling back to: ${nextName}\n`);
298
+ }
299
+ }
300
+ if (!options.fallback) {
301
+ process.exit(lastExitCode);
302
+ return;
303
+ }
304
+ if (quotaFailures === accounts.length) {
305
+ if (passIndex < maxPasses - 1) {
306
+ process.stderr.write("All accounts hit quota. Rechecking...\n");
307
+ if (retryDelayMs > 0) {
308
+ await delay(retryDelayMs);
309
+ }
310
+ continue;
311
+ }
312
+ process.stderr.write("All accounts exhausted due to quota.\n");
313
+ process.exit(lastExitCode);
314
+ return;
315
+ }
316
+ }
317
+ }
318
+ function normalizeMaxPasses(value) {
319
+ const parsed = Number.parseInt(value, 10);
320
+ if (Number.isNaN(parsed) || parsed < 1) {
321
+ return 1;
322
+ }
323
+ return parsed;
324
+ }
325
+ function normalizeDelay(value) {
326
+ const parsed = Number.parseFloat(value);
327
+ if (Number.isNaN(parsed) || parsed <= 0) {
328
+ return 0;
329
+ }
330
+ return Math.floor(parsed * 1000);
331
+ }
332
+ function delay(ms) {
333
+ return new Promise((resolve) => setTimeout(resolve, ms));
334
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isCodexLoggedIn(codexBin: string, accountDir: string): Promise<boolean>;
2
+ export declare function runCodexLogin(codexBin: string, accountDir: string, useDeviceAuth: boolean): Promise<number>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCodexLoggedIn = isCodexLoggedIn;
4
+ exports.runCodexLogin = runCodexLogin;
5
+ const child_process_1 = require("child_process");
6
+ function runCodexCommand(codexBin, args, accountDir, stdio) {
7
+ return new Promise((resolve) => {
8
+ const child = (0, child_process_1.spawn)(codexBin, args, {
9
+ env: { ...process.env, CODEX_HOME: accountDir },
10
+ stdio
11
+ });
12
+ child.on("close", (code) => {
13
+ resolve({ exitCode: code ?? 1 });
14
+ });
15
+ child.on("error", (error) => {
16
+ process.stderr.write(`Failed to start codex: ${error.message}\n`);
17
+ resolve({ exitCode: 1 });
18
+ });
19
+ });
20
+ }
21
+ async function isCodexLoggedIn(codexBin, accountDir) {
22
+ const result = await runCodexCommand(codexBin, ["login", "status"], accountDir, "ignore");
23
+ return result.exitCode === 0;
24
+ }
25
+ async function runCodexLogin(codexBin, accountDir, useDeviceAuth) {
26
+ const args = useDeviceAuth ? ["login", "--device-auth"] : ["login"];
27
+ const result = await runCodexCommand(codexBin, args, accountDir, "inherit");
28
+ return result.exitCode;
29
+ }
@@ -0,0 +1,5 @@
1
+ export declare const REGISTRY_FILE_NAME = "registry.json";
2
+ export declare const AUTH_FILE_NAME = "auth.json";
3
+ export declare const DEFAULT_CONFIG_TOML: string;
4
+ export declare const QUOTA_ERROR_PATTERNS: RegExp[];
5
+ export declare const MAX_CAPTURED_LINES = 200;