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 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
- const data = JSON.parse(raw);
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 whichOutput = (0, child_process_1.execSync)("which -a codex", { encoding: "utf8" })
57
- .split("\n")
58
- .map((line) => line.trim())
59
- .filter(Boolean);
60
- for (const line of whichOutput) {
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
- const fallback = (0, child_process_1.execSync)("command -v codex", { encoding: "utf8" }).trim();
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(process.env.HOME ?? "", ".codex-account-orchestrator", "gateway.json");
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
- return JSON.parse(raw);
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
  }
@@ -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 { default_account: null, accounts: [] };
17
+ return { ...EMPTY_REGISTRY };
14
18
  }
15
19
  const raw = fs_1.default.readFileSync(registryPath, "utf8");
16
- const parsed = JSON.parse(raw);
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: parsed.default_account ?? null,
19
- accounts: Array.isArray(parsed.accounts) ? parsed.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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-account-orchestrator",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Codex OAuth account fallback orchestrator with seamless gateway mode",
5
5
  "main": "dist/cli_main.js",
6
6
  "types": "dist/cli_main.d.ts",