codex-multi-auth 1.2.5 → 1.2.7

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/codex-multi-auth.svg)](https://www.npmjs.com/package/codex-multi-auth)
4
4
  [![npm downloads](https://img.shields.io/npm/dw/codex-multi-auth.svg)](https://www.npmjs.com/package/codex-multi-auth)
5
5
 
6
- Codex CLI-first multi-account OAuth manager for the official `@openai/codex` CLI.
6
+ Codex CLI-first multi-account OAuth manager for the official Codex CLI, including the `@openai/codex` npm launcher and native `codex` installs.
7
7
 
8
8
  <img width="1270" height="729" alt="2026-02-28 12_54_58-prompt txt ‎- Notepads" src="https://github.com/user-attachments/assets/0cecb77e-a6d3-432a-ba48-3577db0c7093" />
9
9
 
@@ -11,6 +11,7 @@ Codex CLI-first multi-account OAuth manager for the official `@openai/codex` CLI
11
11
  > [!NOTE]
12
12
  > Legacy scoped prerelease package `@ndycode/codex-multi-auth` is migration-only.
13
13
  > Use `codex-multi-auth` for all new installs.
14
+
14
15
  ## What You Get
15
16
 
16
17
  - Canonical `codex auth ...` workflow for account login, switching, checks, and diagnostics
@@ -19,7 +20,7 @@ Codex CLI-first multi-account OAuth manager for the official `@openai/codex` CLI
19
20
  - Interactive dashboard for account actions and settings
20
21
  - Experimental settings tab for staged sync, backup, and refresh-guard controls
21
22
  - Forecast, report, fix, and doctor commands for operational safety
22
- - Runtime request budget, cooldown, and traffic observability in `codex auth status` / `codex auth report`
23
+ - Runtime counters, budget/cooldown state, and multi-auth probe visibility in `codex auth status` / `codex auth report`
23
24
  - Flagged account verification and restore flow
24
25
  - Session affinity and live account sync controls
25
26
  - Proactive refresh and preemptive quota deferral controls
@@ -71,6 +72,8 @@ codex-multi-auth --version
71
72
  codex auth status
72
73
  ```
73
74
 
75
+ Any official install path is fine as long as `codex` is on `PATH`: `npm i -g @openai/codex`, `brew install --cask codex`, or an official release binary.
76
+
74
77
  </details>
75
78
 
76
79
  <details>
@@ -105,6 +108,13 @@ npm i -g codex-multi-auth
105
108
  codex auth login
106
109
  ```
107
110
 
111
+ If you already installed the official native CLI via Homebrew or a release binary, you only need:
112
+
113
+ ```bash
114
+ npm i -g codex-multi-auth
115
+ codex auth login
116
+ ```
117
+
108
118
  Verify the wrapper and the new account:
109
119
 
110
120
  ```bash
@@ -163,7 +173,7 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
163
173
  - active requests use a bounded outbound request budget so one prompt cannot walk the full pool indefinitely
164
174
  - repeated cross-account 5xx bursts trigger a short cooldown instead of continuing aggressive rotation
165
175
  - proactive refresh is staggered to reduce background refresh bursts
166
- - `codex auth status` surfaces recent runtime request metrics in text output, and `codex auth report --json` exposes the machine-readable cooldown/runtime snapshot
176
+ - `codex auth status` surfaces recent runtime request metrics in text output, and `codex auth report --json` exposes the machine-readable cooldown/runtime snapshot
167
177
 
168
178
  ---
169
179
 
@@ -196,12 +206,12 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
196
206
  | File | Default path |
197
207
  | --- | --- |
198
208
  | Settings | `~/.codex/multi-auth/settings.json` |
199
- | Accounts | `~/.codex/multi-auth/openai-codex-accounts.json` |
200
- | Flagged accounts | `~/.codex/multi-auth/openai-codex-flagged-accounts.json` |
201
- | Quota cache | `~/.codex/multi-auth/quota-cache.json` |
202
- | Runtime observability | `~/.codex/multi-auth/runtime-observability.json` |
203
- | Logs | `~/.codex/multi-auth/logs/codex-plugin/` |
204
- | Per-project accounts | `~/.codex/multi-auth/projects/<project-key>/openai-codex-accounts.json` |
209
+ | Accounts | `~/.codex/multi-auth/openai-codex-accounts.json` |
210
+ | Flagged accounts | `~/.codex/multi-auth/openai-codex-flagged-accounts.json` |
211
+ | Quota cache | `~/.codex/multi-auth/quota-cache.json` |
212
+ | Runtime observability | `~/.codex/multi-auth/runtime-observability.json` |
213
+ | Logs | `~/.codex/multi-auth/logs/codex-plugin/` |
214
+ | Per-project accounts | `~/.codex/multi-auth/projects/<project-key>/openai-codex-accounts.json` |
205
215
 
206
216
  Override root with `CODEX_MULTI_AUTH_DIR=<path>`.
207
217
 
@@ -273,12 +283,12 @@ codex auth login
273
283
  <details>
274
284
  <summary><b>Common symptoms</b></summary>
275
285
 
276
- - `codex auth` unrecognized: run `where codex`, then follow `docs/troubleshooting.md` for routing fallback commands
286
+ - `codex auth` unrecognized: run `where codex` or `which codex`, then follow `docs/troubleshooting.md` for routing fallback commands
277
287
  - Switch succeeds but wrong account appears active: run `codex auth switch <index>`, then restart session
278
- - Requests fail fast with a pool cooldown message: wait for the cooldown window or inspect `codex auth status`
279
- - Requests fail fast after repeated upstream 5xx errors: inspect `codex auth report --json` for runtime traffic and cooldown details
280
- - Storage cleanup fails with `EBUSY` / `EPERM` (Windows): run `codex auth doctor --fix` to retry, or manually remove `~/.codex/multi-auth/<project-key>/` and re-login
281
- - OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
288
+ - Requests fail fast with a pool cooldown message: wait for the cooldown window or inspect `codex auth status`
289
+ - Requests fail fast after repeated upstream 5xx errors: inspect `codex auth report --json` for runtime traffic and cooldown details
290
+ - Storage cleanup fails with `EBUSY` / `EPERM` (Windows): run `codex auth doctor --fix` to retry, or manually remove `~/.codex/multi-auth/<project-key>/` and re-login
291
+ - OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
282
292
  - Browser launch is blocked or you are in a headless shell: re-run `codex auth login --manual` or set `CODEX_AUTH_NO_BROWSER=1`
283
293
  - `missing field id_token` / `token_expired` / `refresh_token_reused`: re-login affected account
284
294
 
@@ -321,9 +331,10 @@ codex auth doctor --json
321
331
 
322
332
  ## Release Notes
323
333
 
324
- - Current stable: [docs/releases/v1.2.4.md](docs/releases/v1.2.4.md)
325
- - Previous stable: [docs/releases/v1.2.2.md](docs/releases/v1.2.2.md)
326
- - Earlier stable: [docs/releases/v1.2.1.md](docs/releases/v1.2.1.md)
334
+ - Current stable: [docs/releases/v1.2.7.md](docs/releases/v1.2.7.md)
335
+ - Previous stable: [docs/releases/v1.2.6.md](docs/releases/v1.2.6.md)
336
+ - Earlier stable: [docs/releases/v1.2.5.md](docs/releases/v1.2.5.md)
337
+ - Full release archive: [docs/README.md#release-history](docs/README.md#release-history)
327
338
  - Archived prerelease: [docs/releases/v0.1.0-beta.0.md](docs/releases/v0.1.0-beta.0.md)
328
339
 
329
340
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Multi-account OAuth manager and codex auth wrapper for the official @openai/codex CLI, with switching, health checks, and recovery tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,10 +1,36 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, realpathSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
- import { dirname, join } from "node:path";
4
+ import { basename, delimiter, dirname, extname, join } from "node:path";
5
5
  import process from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
+ function isJavaScriptEntryPath(candidate) {
9
+ const extension = extname(candidate).toLowerCase();
10
+ return extension === ".js" || extension === ".mjs" || extension === ".cjs";
11
+ }
12
+
13
+ function createResolvedCodexBin(path) {
14
+ return {
15
+ path,
16
+ launchWithNode: isJavaScriptEntryPath(path),
17
+ };
18
+ }
19
+
20
+ function normalizeResolvedPath(candidatePath, realpathSyncImpl) {
21
+ try {
22
+ return realpathSyncImpl(candidatePath);
23
+ } catch {
24
+ return candidatePath;
25
+ }
26
+ }
27
+
28
+ function resolveWrapperScriptPath(moduleUrl, realpathSyncImpl) {
29
+ return normalizeResolvedPath(fileURLToPath(moduleUrl), realpathSyncImpl);
30
+ }
31
+
32
+ const DEFAULT_WRAPPER_MODULE_URL = new URL("./codex.js", import.meta.url).href;
33
+
8
34
  function defaultResolvePackageBin(moduleUrl) {
9
35
  try {
10
36
  const require = createRequire(moduleUrl);
@@ -26,26 +52,141 @@ function resolveWindowsCmdPath(env) {
26
52
  return "cmd.exe";
27
53
  }
28
54
 
55
+ export function splitPathEntries(pathValue) {
56
+ if (typeof pathValue !== "string" || pathValue.trim().length === 0) {
57
+ return [];
58
+ }
59
+ return pathValue
60
+ .split(delimiter)
61
+ .map((entry) => entry.trim())
62
+ .filter((entry) => entry.length > 0);
63
+ }
64
+
65
+ function resolvePathExecutableName(platform) {
66
+ return platform === "win32" ? "codex.exe" : "codex";
67
+ }
68
+
69
+ function resolveCandidateExecutableNames(platform) {
70
+ if (platform !== "win32") {
71
+ return [resolvePathExecutableName(platform)];
72
+ }
73
+ return ["codex.exe", "codex"];
74
+ }
75
+
76
+ function resolveCodexExecutableFromPath(
77
+ pathEntries,
78
+ platform,
79
+ existsSyncImpl,
80
+ selfScriptPath,
81
+ realpathSyncImpl,
82
+ ) {
83
+ for (const entry of pathEntries) {
84
+ for (const executableName of resolveCandidateExecutableNames(platform)) {
85
+ const candidate = join(entry, executableName);
86
+ if (!existsSyncImpl(candidate)) {
87
+ continue;
88
+ }
89
+ if (
90
+ selfScriptPath &&
91
+ normalizeResolvedPath(candidate, realpathSyncImpl) === selfScriptPath
92
+ ) {
93
+ continue;
94
+ }
95
+ return candidate;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function normalizeWhereOutput(stdout) {
102
+ return stdout
103
+ .split(/\r?\n/)
104
+ .map((line) => line.trim())
105
+ .filter((line) => line.length > 0);
106
+ }
107
+
108
+ function resolveCodexExecutableFromSystemPath(
109
+ env,
110
+ platform,
111
+ spawnSyncImpl,
112
+ existsSyncImpl,
113
+ selfScriptPath,
114
+ realpathSyncImpl,
115
+ ) {
116
+ const pathEntries = splitPathEntries(env.PATH ?? env.Path ?? "");
117
+ const fromEnvPath = resolveCodexExecutableFromPath(
118
+ pathEntries,
119
+ platform,
120
+ existsSyncImpl,
121
+ selfScriptPath,
122
+ realpathSyncImpl,
123
+ );
124
+ if (fromEnvPath) {
125
+ return fromEnvPath;
126
+ }
127
+
128
+ try {
129
+ const lookupResult =
130
+ platform === "win32"
131
+ ? spawnSyncImpl(resolveWindowsCmdPath(env), ["/d", "/s", "/c", "where codex"], {
132
+ encoding: "utf8",
133
+ env,
134
+ stdio: ["ignore", "pipe", "ignore"],
135
+ timeout: 5000,
136
+ windowsHide: true,
137
+ })
138
+ : spawnSyncImpl("which", ["codex"], {
139
+ encoding: "utf8",
140
+ env,
141
+ stdio: ["ignore", "pipe", "ignore"],
142
+ timeout: 5000,
143
+ });
144
+ if (lookupResult.status !== 0) {
145
+ return null;
146
+ }
147
+ for (const candidate of normalizeWhereOutput(lookupResult.stdout)) {
148
+ if (!existsSyncImpl(candidate)) {
149
+ continue;
150
+ }
151
+ if (
152
+ selfScriptPath &&
153
+ normalizeResolvedPath(candidate, realpathSyncImpl) === selfScriptPath
154
+ ) {
155
+ continue;
156
+ }
157
+ const fileName = basename(candidate).toLowerCase();
158
+ if (fileName === "codex" || fileName === "codex.exe") {
159
+ return candidate;
160
+ }
161
+ }
162
+ } catch {
163
+ // Ignore and fall through.
164
+ }
165
+
166
+ return null;
167
+ }
168
+
29
169
  export function resolveRealCodexBin(options = {}) {
30
170
  const {
31
171
  env = process.env,
32
172
  argv = process.argv,
33
173
  platform = process.platform,
34
- moduleUrl = import.meta.url,
174
+ moduleUrl = DEFAULT_WRAPPER_MODULE_URL,
35
175
  existsSyncImpl = existsSync,
176
+ realpathSyncImpl = realpathSync,
36
177
  spawnSyncImpl = spawnSync,
37
178
  resolvePackageBin = defaultResolvePackageBin,
38
179
  } = options;
39
180
 
40
181
  const override = (env.CODEX_MULTI_AUTH_REAL_CODEX_BIN ?? "").trim();
41
182
  if (override.length > 0) {
42
- if (existsSyncImpl(override)) return override;
183
+ if (existsSyncImpl(override)) return createResolvedCodexBin(override);
43
184
  return null;
44
185
  }
45
186
 
46
187
  const resolved = resolvePackageBin(moduleUrl);
47
188
  if (typeof resolved === "string" && resolved.length > 0 && existsSyncImpl(resolved)) {
48
- return resolved;
189
+ return createResolvedCodexBin(resolved);
49
190
  }
50
191
 
51
192
  const searchRoots = [];
@@ -65,7 +206,7 @@ export function resolveRealCodexBin(options = {}) {
65
206
 
66
207
  for (const root of searchRoots) {
67
208
  const candidate = join(root, "@openai", "codex", "bin", "codex.js");
68
- if (existsSyncImpl(candidate)) return candidate;
209
+ if (existsSyncImpl(candidate)) return createResolvedCodexBin(candidate);
69
210
  }
70
211
 
71
212
  try {
@@ -88,12 +229,26 @@ export function resolveRealCodexBin(options = {}) {
88
229
  const globalRoot = rootResult.stdout.trim();
89
230
  if (globalRoot.length > 0) {
90
231
  const globalBin = join(globalRoot, "@openai", "codex", "bin", "codex.js");
91
- if (existsSyncImpl(globalBin)) return globalBin;
232
+ if (existsSyncImpl(globalBin)) return createResolvedCodexBin(globalBin);
92
233
  }
93
234
  }
94
235
  } catch {
95
236
  // Ignore and fall through to null.
96
237
  }
97
238
 
239
+ const selfScriptPath = resolveWrapperScriptPath(moduleUrl, realpathSyncImpl);
240
+
241
+ const nativeCodexBin = resolveCodexExecutableFromSystemPath(
242
+ env,
243
+ platform,
244
+ spawnSyncImpl,
245
+ existsSyncImpl,
246
+ selfScriptPath,
247
+ realpathSyncImpl,
248
+ );
249
+ if (nativeCodexBin) {
250
+ return createResolvedCodexBin(nativeCodexBin);
251
+ }
252
+
98
253
  return null;
99
254
  }
package/scripts/codex.js CHANGED
@@ -18,7 +18,10 @@ import { homedir, tmpdir } from "node:os";
18
18
  import { basename, delimiter, dirname, join, resolve as resolvePath } from "node:path";
19
19
  import process from "node:process";
20
20
  import { fileURLToPath } from "node:url";
21
- import { resolveRealCodexBin as resolveRealCodexBinFromEnvironment } from "./codex-bin-resolver.js";
21
+ import {
22
+ resolveRealCodexBin as resolveRealCodexBinFromEnvironment,
23
+ splitPathEntries,
24
+ } from "./codex-bin-resolver.js";
22
25
  import { normalizeAuthAlias, shouldHandleMultiAuthAuth } from "./codex-routing.js";
23
26
 
24
27
  const RETRYABLE_SHADOW_HOME_CLEANUP_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
@@ -124,11 +127,17 @@ async function autoSyncManagerActiveSelectionIfEnabled() {
124
127
  function resolveRealCodexBin() {
125
128
  const override = (process.env.CODEX_MULTI_AUTH_REAL_CODEX_BIN ?? "").trim();
126
129
  if (override.length > 0) {
127
- if (existsSync(override)) return override;
128
- console.error(
129
- `CODEX_MULTI_AUTH_REAL_CODEX_BIN is set but missing: ${override}`,
130
- );
131
- return null;
130
+ if (!existsSync(override)) {
131
+ console.error(
132
+ `CODEX_MULTI_AUTH_REAL_CODEX_BIN is set but missing: ${override}`,
133
+ );
134
+ return null;
135
+ }
136
+ return resolveRealCodexBinFromEnvironment({
137
+ moduleUrl: import.meta.url,
138
+ env: process.env,
139
+ existsSyncImpl: existsSync,
140
+ });
132
141
  }
133
142
 
134
143
  return resolveRealCodexBinFromEnvironment({ moduleUrl: import.meta.url });
@@ -145,7 +154,9 @@ function forwardToRealCodex(codexBin, args, env = process.env, cleanup) {
145
154
  resolve(exitCode);
146
155
  };
147
156
 
148
- const child = spawn(process.execPath, [codexBin, ...args], {
157
+ const command = codexBin.launchWithNode ? process.execPath : codexBin.path;
158
+ const commandArgs = codexBin.launchWithNode ? [codexBin.path, ...args] : args;
159
+ const child = spawn(command, commandArgs, {
149
160
  stdio: "inherit",
150
161
  env,
151
162
  });
@@ -661,6 +672,189 @@ function resolveOriginalMultiAuthDir(env) {
661
672
  return undefined;
662
673
  }
663
674
 
675
+ async function loadRuntimeObservabilityModule() {
676
+ try {
677
+ const mod = await import("../dist/lib/runtime/runtime-observability.js");
678
+ if (
679
+ typeof mod.loadPersistedRuntimeObservabilitySnapshot !== "function" ||
680
+ typeof mod.mutateRuntimeObservabilitySnapshot !== "function"
681
+ ) {
682
+ return null;
683
+ }
684
+ return mod;
685
+ } catch (error) {
686
+ if (
687
+ error &&
688
+ typeof error === "object" &&
689
+ "code" in error &&
690
+ error.code === "ERR_MODULE_NOT_FOUND"
691
+ ) {
692
+ return null;
693
+ }
694
+ throw error;
695
+ }
696
+ }
697
+
698
+ function isPureHelpOrVersionArgs(rawArgs) {
699
+ if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
700
+ return false;
701
+ }
702
+ return rawArgs.every((arg) =>
703
+ typeof arg === "string" && ["--help", "-h", "--version", "-V"].includes(arg),
704
+ );
705
+ }
706
+
707
+ function consumesNextArg(arg) {
708
+ return new Set([
709
+ "-c",
710
+ "--config",
711
+ "--enable",
712
+ "--disable",
713
+ "--remote",
714
+ "--remote-auth-token-env",
715
+ "-i",
716
+ "--image",
717
+ "-m",
718
+ "--model",
719
+ "--local-provider",
720
+ "-p",
721
+ "--profile",
722
+ "-s",
723
+ "--sandbox",
724
+ "-a",
725
+ "--ask-for-approval",
726
+ "-C",
727
+ "--cd",
728
+ "--add-dir",
729
+ "--output-schema",
730
+ "--color",
731
+ "-o",
732
+ "--output-last-message",
733
+ ]).has(arg);
734
+ }
735
+
736
+ function shouldTrackForwardedRuntimeObservability(rawArgs) {
737
+ if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
738
+ return true;
739
+ }
740
+ if (isPureHelpOrVersionArgs(rawArgs)) {
741
+ return false;
742
+ }
743
+
744
+ const requestCommands = new Set(["exec", "review", "resume", "fork"]);
745
+ const nonRequestCommands = new Set([
746
+ "help",
747
+ "completion",
748
+ "login",
749
+ "logout",
750
+ "mcp",
751
+ "mcp-server",
752
+ "app-server",
753
+ "sandbox",
754
+ "debug",
755
+ "apply",
756
+ "cloud",
757
+ "features",
758
+ "auth",
759
+ ]);
760
+
761
+ for (let i = 0; i < rawArgs.length; i += 1) {
762
+ const arg = rawArgs[i];
763
+ if (typeof arg !== "string" || arg.length === 0) continue;
764
+ if (arg === "--") {
765
+ return i + 1 < rawArgs.length;
766
+ }
767
+ if (arg.startsWith("--config=")) {
768
+ continue;
769
+ }
770
+ if (arg.startsWith("--") || (arg.startsWith("-") && arg !== "-")) {
771
+ if (consumesNextArg(arg)) {
772
+ i += 1;
773
+ }
774
+ continue;
775
+ }
776
+ if (requestCommands.has(arg)) {
777
+ return true;
778
+ }
779
+ if (nonRequestCommands.has(arg)) {
780
+ return false;
781
+ }
782
+ return true;
783
+ }
784
+
785
+ return true;
786
+ }
787
+
788
+ function createRuntimeSnapshotChangeToken(snapshot) {
789
+ return JSON.stringify({
790
+ updatedAt: snapshot?.updatedAt ?? null,
791
+ responsesRequests: snapshot?.responsesRequests ?? null,
792
+ authRefreshRequests: snapshot?.authRefreshRequests ?? null,
793
+ diagnosticProbeRequests: snapshot?.diagnosticProbeRequests ?? null,
794
+ totalRequests: snapshot?.runtimeMetrics?.totalRequests ?? null,
795
+ successfulRequests: snapshot?.runtimeMetrics?.successfulRequests ?? null,
796
+ failedRequests: snapshot?.runtimeMetrics?.failedRequests ?? null,
797
+ lastRequestAt: snapshot?.runtimeMetrics?.lastRequestAt ?? null,
798
+ });
799
+ }
800
+
801
+ async function loadRuntimeSnapshotWithRetry(runtimeObservabilityModule) {
802
+ let snapshot = null;
803
+ for (let attempt = 0; attempt < 5; attempt += 1) {
804
+ snapshot =
805
+ (await runtimeObservabilityModule.loadPersistedRuntimeObservabilitySnapshot()) ??
806
+ null;
807
+ if (snapshot) {
808
+ return snapshot;
809
+ }
810
+ if (attempt < 4) {
811
+ await new Promise((resolve) => setTimeout(resolve, 20));
812
+ }
813
+ }
814
+ return snapshot;
815
+ }
816
+
817
+ async function withForwardedRuntimeObservability(rawArgs, runForwardedCommand) {
818
+ if (!shouldTrackForwardedRuntimeObservability(rawArgs)) {
819
+ return runForwardedCommand();
820
+ }
821
+
822
+ const runtimeObservabilityModule = await loadRuntimeObservabilityModule();
823
+ if (!runtimeObservabilityModule) {
824
+ return runForwardedCommand();
825
+ }
826
+
827
+ const beforeSnapshot = await loadRuntimeSnapshotWithRetry(runtimeObservabilityModule);
828
+ const beforeToken = createRuntimeSnapshotChangeToken(beforeSnapshot);
829
+ const startedAt = Date.now();
830
+ const exitCode = await runForwardedCommand();
831
+ const afterSnapshot = await loadRuntimeSnapshotWithRetry(runtimeObservabilityModule);
832
+ const afterToken = createRuntimeSnapshotChangeToken(afterSnapshot);
833
+ if (afterToken !== beforeToken) {
834
+ return exitCode;
835
+ }
836
+
837
+ runtimeObservabilityModule.mutateRuntimeObservabilitySnapshot((snapshot) => {
838
+ snapshot.currentRequestId = null;
839
+ snapshot.responsesRequests += 1;
840
+ snapshot.runtimeMetrics.totalRequests += 1;
841
+ snapshot.runtimeMetrics.responsesRequests += 1;
842
+ snapshot.runtimeMetrics.cumulativeLatencyMs += Math.max(0, Date.now() - startedAt);
843
+ snapshot.runtimeMetrics.lastRequestAt = Date.now();
844
+ if (!snapshot.runtimeMetrics.startedAt) {
845
+ snapshot.runtimeMetrics.startedAt = startedAt;
846
+ }
847
+ if (exitCode === 0) {
848
+ snapshot.runtimeMetrics.successfulRequests += 1;
849
+ snapshot.runtimeMetrics.lastError = null;
850
+ } else {
851
+ snapshot.runtimeMetrics.failedRequests += 1;
852
+ snapshot.runtimeMetrics.lastError = `forwarded-codex-exit:${exitCode}`;
853
+ }
854
+ });
855
+ return exitCode;
856
+ }
857
+
664
858
  function createCompatibilityCodexHome(
665
859
  processedArgs,
666
860
  requestedModel,
@@ -810,16 +1004,6 @@ function shouldInstallWindowsBatchShimGuard() {
810
1004
  return override !== "0";
811
1005
  }
812
1006
 
813
- function splitPathEntries(pathValue) {
814
- if (typeof pathValue !== "string" || pathValue.trim().length === 0) {
815
- return [];
816
- }
817
- return pathValue
818
- .split(delimiter)
819
- .map((entry) => entry.trim())
820
- .filter((entry) => entry.length > 0);
821
- }
822
-
823
1007
  function resolveWindowsShimDirectoryFromInvocation() {
824
1008
  const invokedScript = (process.argv[1] ?? "").trim();
825
1009
  if (invokedScript.length === 0) return null;
@@ -955,7 +1139,13 @@ function ensureWindowsShellShim(filePath, desiredContent, options = {}) {
955
1139
  }
956
1140
  const looksLikeStockOpenAiShim =
957
1141
  currentContent.includes("node_modules\\@openai\\codex\\bin\\codex.js") ||
958
- currentContent.includes("node_modules/@openai/codex/bin/codex.js");
1142
+ currentContent.includes("node_modules/@openai/codex/bin/codex.js") ||
1143
+ currentContent.includes("@openai\\codex-win32-") ||
1144
+ currentContent.includes("@openai/codex-win32-") ||
1145
+ currentContent.includes("vendor\\x86_64-pc-windows-msvc\\codex\\codex.exe") ||
1146
+ currentContent.includes("vendor\\aarch64-pc-windows-msvc\\codex\\codex.exe") ||
1147
+ currentContent.includes("vendor/x86_64-pc-windows-msvc/codex/codex.exe") ||
1148
+ currentContent.includes("vendor/aarch64-pc-windows-msvc/codex/codex.exe");
959
1149
  if (looksLikeStockOpenAiShim) {
960
1150
  try {
961
1151
  writeFileSync(filePath, desiredContent, { encoding: "utf8", mode: 0o755 });
@@ -1135,9 +1325,9 @@ async function main() {
1135
1325
  if (!realCodexBin) {
1136
1326
  console.error(
1137
1327
  [
1138
- "Could not locate the official Codex CLI binary (@openai/codex).",
1139
- "Install it globally: npm install -g @openai/codex",
1140
- "Or set CODEX_MULTI_AUTH_REAL_CODEX_BIN to a full bin/codex.js path.",
1328
+ "Could not locate the official Codex CLI.",
1329
+ "Install it with npm, Homebrew, or an official native release so `codex` is on PATH.",
1330
+ "Or set CODEX_MULTI_AUTH_REAL_CODEX_BIN to the full path of either codex or @openai/codex/bin/codex.js.",
1141
1331
  ].join("\n"),
1142
1332
  );
1143
1333
  return 1;
@@ -1149,11 +1339,13 @@ async function main() {
1149
1339
  forwardArgs,
1150
1340
  requestedModel,
1151
1341
  );
1152
- return forwardToRealCodex(
1153
- realCodexBin,
1154
- compatibility.args,
1155
- compatibility.env,
1156
- compatibility.cleanup,
1342
+ return withForwardedRuntimeObservability(rawArgs, () =>
1343
+ forwardToRealCodex(
1344
+ realCodexBin,
1345
+ compatibility.args,
1346
+ compatibility.env,
1347
+ compatibility.cleanup,
1348
+ ),
1157
1349
  );
1158
1350
  }
1159
1351