codex-account-orchestrator 1.0.0 → 1.0.1
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 +12 -0
- package/README.md +19 -0
- package/dist/gateway/account_pool.js +8 -1
- package/dist/gateway/codex_shim.js +24 -10
- package/dist/gateway/gateway_config.js +11 -2
- package/dist/paths.js +1 -1
- package/dist/registry_store.js +47 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.1] - 2026-01-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Global npm install instructions and expanded debug options in README
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Hardened registry and gateway config loading with automatic corruption backups
|
|
17
|
+
- Improved gateway config path resolution via `os.homedir()`
|
|
18
|
+
- Made codex shim resolution more robust when `codex` is missing from PATH
|
|
19
|
+
|
|
8
20
|
## [1.0.0] - 2025-01-27
|
|
9
21
|
|
|
10
22
|
### Added
|
package/README.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18+
|
|
8
|
+
- Codex CLI installed and available on `PATH`
|
|
9
|
+
|
|
10
|
+
## Install (npm)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g codex-account-orchestrator
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The CLI is available as `cao` (alias) or `codex-account-orchestrator`.
|
|
17
|
+
|
|
5
18
|
## Install (local dev)
|
|
6
19
|
|
|
7
20
|
```bash
|
|
@@ -134,6 +147,12 @@ cao gateway status
|
|
|
134
147
|
|
|
135
148
|
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
149
|
|
|
150
|
+
Additional debug flags:
|
|
151
|
+
|
|
152
|
+
- `CAO_DEBUG_BODY=1` to log a sanitized body snippet.
|
|
153
|
+
- `CAO_CAPTURE_BODY_PATH=/path/to/file.json` to override the capture path.
|
|
154
|
+
- `CAO_FORCE_QUOTA_ACCOUNTS=accountA,accountB` to simulate quota failures for specific accounts.
|
|
155
|
+
|
|
137
156
|
## Notes
|
|
138
157
|
|
|
139
158
|
- Fallback requires capturing output; this may make Codex detect a non-TTY stdout. If you want a pure TTY session, use `--no-fallback`.
|
|
@@ -95,7 +95,14 @@ function loadTokens(accountDir) {
|
|
|
95
95
|
return undefined;
|
|
96
96
|
}
|
|
97
97
|
const raw = fs_1.default.readFileSync(authPath, "utf8");
|
|
98
|
-
|
|
98
|
+
let data;
|
|
99
|
+
try {
|
|
100
|
+
data = JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
process.stderr.write(`Warning: auth.json for ${accountDir} is invalid and will be skipped.\n`);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
99
106
|
const tokens = data.tokens ?? {};
|
|
100
107
|
if (!tokens.access_token || !tokens.refresh_token) {
|
|
101
108
|
return undefined;
|
|
@@ -53,11 +53,21 @@ function disableGatewayShim() {
|
|
|
53
53
|
}
|
|
54
54
|
function resolveCodexPath() {
|
|
55
55
|
const shimPath = path_1.default.join(resolveShimDir(), "codex");
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
|
|
56
|
+
const candidates = [];
|
|
57
|
+
const whichOutput = tryExec("which -a codex");
|
|
58
|
+
if (whichOutput) {
|
|
59
|
+
candidates.push(...whichOutput
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter(Boolean));
|
|
63
|
+
}
|
|
64
|
+
if (candidates.length === 0) {
|
|
65
|
+
const fallback = tryExec("command -v codex");
|
|
66
|
+
if (fallback) {
|
|
67
|
+
candidates.push(fallback.trim());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const line of candidates) {
|
|
61
71
|
if (line.includes("aliased to")) {
|
|
62
72
|
continue;
|
|
63
73
|
}
|
|
@@ -68,11 +78,7 @@ function resolveCodexPath() {
|
|
|
68
78
|
return line;
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
|
|
72
|
-
if (!fallback || fallback === shimPath) {
|
|
73
|
-
throw new Error("codex binary not found in PATH (or only the CAO shim is present). Disable the shim first.");
|
|
74
|
-
}
|
|
75
|
-
return fallback;
|
|
81
|
+
throw new Error("codex binary not found in PATH (or only the CAO shim is present). Disable the shim first.");
|
|
76
82
|
}
|
|
77
83
|
function resolveShimDir() {
|
|
78
84
|
return path_1.default.join(os_1.default.homedir(), ".local", "bin");
|
|
@@ -81,3 +87,11 @@ function isDirInPath(dir) {
|
|
|
81
87
|
const envPath = process.env.PATH ?? "";
|
|
82
88
|
return envPath.split(":").includes(dir);
|
|
83
89
|
}
|
|
90
|
+
function tryExec(command) {
|
|
91
|
+
try {
|
|
92
|
+
return (0, child_process_1.execSync)(command, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.getGatewayConfigPath = getGatewayConfigPath;
|
|
|
8
8
|
exports.loadGatewayConfig = loadGatewayConfig;
|
|
9
9
|
exports.saveGatewayConfig = saveGatewayConfig;
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
12
13
|
const DEFAULT_CONFIG = {
|
|
13
14
|
bindAddress: "127.0.0.1",
|
|
@@ -26,7 +27,7 @@ function resolveGatewayConfig(overrides) {
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
function getGatewayConfigPath() {
|
|
29
|
-
return path_1.default.join(
|
|
30
|
+
return path_1.default.join(os_1.default.homedir(), ".codex-account-orchestrator", "gateway.json");
|
|
30
31
|
}
|
|
31
32
|
function loadGatewayConfig() {
|
|
32
33
|
const configPath = getGatewayConfigPath();
|
|
@@ -34,7 +35,15 @@ function loadGatewayConfig() {
|
|
|
34
35
|
return {};
|
|
35
36
|
}
|
|
36
37
|
const raw = fs_1.default.readFileSync(configPath, "utf8");
|
|
37
|
-
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const backupPath = `${configPath}.corrupt-${Date.now()}`;
|
|
43
|
+
fs_1.default.writeFileSync(backupPath, raw, "utf8");
|
|
44
|
+
process.stderr.write(`Warning: gateway.json was invalid and has been backed up to ${backupPath}.\n`);
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
48
|
function saveGatewayConfig(config) {
|
|
40
49
|
const configPath = getGatewayConfigPath();
|
package/dist/paths.js
CHANGED
|
@@ -11,7 +11,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
11
11
|
const constants_1 = require("./constants");
|
|
12
12
|
function getBaseDir(dataDir) {
|
|
13
13
|
if (dataDir && dataDir.trim().length > 0) {
|
|
14
|
-
return dataDir;
|
|
14
|
+
return path_1.default.resolve(dataDir);
|
|
15
15
|
}
|
|
16
16
|
return path_1.default.join(os_1.default.homedir(), ".codex-account-orchestrator");
|
|
17
17
|
}
|
package/dist/registry_store.js
CHANGED
|
@@ -7,16 +7,34 @@ exports.loadRegistry = loadRegistry;
|
|
|
7
7
|
exports.saveRegistry = saveRegistry;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const paths_1 = require("./paths");
|
|
10
|
+
const EMPTY_REGISTRY = {
|
|
11
|
+
default_account: null,
|
|
12
|
+
accounts: []
|
|
13
|
+
};
|
|
10
14
|
function loadRegistry(baseDir) {
|
|
11
15
|
const registryPath = (0, paths_1.getRegistryPath)(baseDir);
|
|
12
16
|
if (!fs_1.default.existsSync(registryPath)) {
|
|
13
|
-
return {
|
|
17
|
+
return { ...EMPTY_REGISTRY };
|
|
14
18
|
}
|
|
15
19
|
const raw = fs_1.default.readFileSync(registryPath, "utf8");
|
|
16
|
-
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const backupPath = `${registryPath}.corrupt-${Date.now()}`;
|
|
26
|
+
fs_1.default.writeFileSync(backupPath, raw, "utf8");
|
|
27
|
+
process.stderr.write(`Warning: registry.json was invalid and has been backed up to ${backupPath}.\n`);
|
|
28
|
+
return { ...EMPTY_REGISTRY };
|
|
29
|
+
}
|
|
30
|
+
const accounts = normalizeAccounts(parsed.accounts);
|
|
31
|
+
const defaultAccount = typeof parsed.default_account === "string" ? parsed.default_account.trim() : null;
|
|
32
|
+
const normalizedDefault = defaultAccount && accounts.includes(defaultAccount)
|
|
33
|
+
? defaultAccount
|
|
34
|
+
: accounts[0] ?? null;
|
|
17
35
|
return {
|
|
18
|
-
default_account:
|
|
19
|
-
accounts
|
|
36
|
+
default_account: normalizedDefault,
|
|
37
|
+
accounts
|
|
20
38
|
};
|
|
21
39
|
}
|
|
22
40
|
function saveRegistry(baseDir, registry) {
|
|
@@ -24,3 +42,28 @@ function saveRegistry(baseDir, registry) {
|
|
|
24
42
|
const payload = JSON.stringify(registry, null, 2) + "\n";
|
|
25
43
|
fs_1.default.writeFileSync(registryPath, payload, "utf8");
|
|
26
44
|
}
|
|
45
|
+
function normalizeAccounts(accounts) {
|
|
46
|
+
if (!Array.isArray(accounts)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const normalized = [];
|
|
51
|
+
for (const entry of accounts) {
|
|
52
|
+
if (typeof entry !== "string") {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const trimmed = entry.trim();
|
|
56
|
+
if (trimmed.length === 0) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(trimmed)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
seen.add(trimmed);
|
|
66
|
+
normalized.push(trimmed);
|
|
67
|
+
}
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|