@christiandoxa/prodex 0.213.0 → 0.215.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/README.md CHANGED
@@ -127,7 +127,7 @@ Runtime proxy design contract:
127
127
  npm install -g @christiandoxa/prodex
128
128
  ```
129
129
 
130
- The npm package uses its bundled `@openai/codex@latest` dependency by default. To deliberately use a separate Codex CLI from your machine, set `PRODEX_CODEX_BIN=/path/to/codex` or `PRODEX_CODEX_RESOLUTION=external`.
130
+ The npm package uses its bundled `@openai/codex@latest` dependency by default. If the bundled native Codex optional package is missing or not executable, Prodex falls back to an executable external `codex` on `PATH` and prints a notice before launch. This includes nvm-managed global Codex installs when the active nvm version's `bin` directory is on `PATH`. If neither bundled nor external Codex is usable, set `PRODEX_CODEX_AUTO_INSTALL=1` to let Prodex run `npm install -g @openai/codex@latest` once before retrying PATH resolution. To deliberately pin a separate Codex CLI from your machine, set `PRODEX_CODEX_BIN=/path/to/codex` or `PRODEX_CODEX_RESOLUTION=external`.
131
131
 
132
132
  </details>
133
133
 
@@ -898,6 +898,7 @@ prodex run --rollout-budget-tokens 100000 --rollout-budget-reminders 75000,50000
898
898
 
899
899
  prodex run --current-time-reminder
900
900
  prodex run --current-time-reminder --current-time-reminder-interval 2
901
+ prodex run --respect-system-proxy
901
902
  ```
902
903
 
903
904
  `--web-search` maps to Codex's top-level `web_search = "disabled" | "cached" | "indexed" | "live"` setting. In Super provider mode, an explicit `--web-search` is appended after the provider default, so it overrides the default bridge choice.
@@ -906,6 +907,8 @@ prodex run --current-time-reminder --current-time-reminder-interval 2
906
907
 
907
908
  `--current-time-reminder` enables Codex's `[features.current_time_reminder]` config. The default system clock source is owned by Codex. `--current-time-clock-source external` is intended for Codex app-server clients that implement the upstream `currentTime/read` request.
908
909
 
910
+ `--respect-system-proxy` enables Codex's `[features.respect_system_proxy]` config when the bundled/upstream Codex supports it. Codex 0.142.1 uses this opt-in feature for Windows auth clients so PAC, WPAD, static system proxy, and bypass decisions can be honored. `--no-respect-system-proxy` renders an explicit false override for sessions that need the upstream default direct/env-proxy behavior.
911
+
909
912
  Codex `multiAgentMode` is an app-server/thread setting, not a normal TUI `config.toml` launch override. Prodex therefore does not invent a competing CLI config flag. Launch `prodex app-server` or `prodex run app-server` and pass upstream `multiAgentMode` values (`none`, `explicitRequestOnly`, or `proactive`) through the Codex app-server API.
910
913
 
911
914
  Codex plugin catalog commands are managed passthrough by default:
@@ -972,7 +975,7 @@ prodex s
972
975
  prodex s exec "review this repo"
973
976
  ANTHROPIC_API_KEY=... prodex s --provider anthropic --model claude-sonnet-4-6
974
977
  prodex profile import copilot
975
- prodex s --provider copilot --model gpt-5.1-codex
978
+ prodex s --provider copilot --model gpt-5.3-codex
976
979
  DEEPSEEK_API_KEY=... prodex s deepseek --model deepseek-v4-pro
977
980
  prodex s gemini
978
981
  prodex super
@@ -1004,7 +1007,7 @@ Use `--provider copilot` when you want the Codex/Super front end with GitHub Cop
1004
1007
 
1005
1008
  ```bash
1006
1009
  prodex profile import copilot
1007
- prodex s --provider copilot --model gpt-5.1-codex
1010
+ prodex s --provider copilot --model gpt-5.3-codex
1008
1011
  ```
1009
1012
 
1010
1013
  Without `--api-key`, Prodex uses imported Copilot CLI profiles, refreshes Copilot runtime API tokens from GitHub before launch, can rotate fresh native Responses requests across multiple eligible profiles, and binds streaming response IDs back to the owning profile for continuations. `GITHUB_COPILOT_API_KEY`, `GITHUB_COPILOT_API_KEYS`, or `--api-key` can be used when you already have Copilot runtime API token(s); plural keys may be comma-, semicolon-, or newline-separated and can rotate before commit on auth/quota/rate/temporary failures.
@@ -3,7 +3,7 @@
3
3
 
4
4
  const fs = require("node:fs");
5
5
  const path = require("node:path");
6
- const { spawn } = require("node:child_process");
6
+ const { spawn, spawnSync } = require("node:child_process");
7
7
  const { createRequire } = require("node:module");
8
8
 
9
9
  const requireFromHere = createRequire(__filename);
@@ -47,147 +47,370 @@ const PLATFORM_TARGETS = {
47
47
  },
48
48
  };
49
49
 
50
- function currentPlatformTarget() {
51
- return PLATFORM_TARGETS[process.platform]?.[process.arch] ?? null;
50
+ function platformSpec() {
51
+ return PLATFORM_TARGETS[process.platform]?.[process.arch] || null;
52
52
  }
53
53
 
54
- function resolveOpenAiCodexPackageRoot() {
55
- let packageJsonPath;
54
+ function isExecutableFile(filePath) {
56
55
  try {
57
- packageJsonPath = requireFromHere.resolve("@openai/codex/package.json");
56
+ const stats = fs.statSync(filePath);
57
+ if (!stats.isFile()) {
58
+ return false;
59
+ }
60
+ fs.accessSync(filePath, fs.constants.X_OK);
61
+ return true;
58
62
  } catch {
59
- return null;
63
+ return false;
60
64
  }
61
- return path.dirname(packageJsonPath);
62
65
  }
63
66
 
64
- function codexManagedEnv() {
65
- const packageRoot = resolveOpenAiCodexPackageRoot();
66
- if (!packageRoot) {
67
- return process.env;
67
+ function tryMakeExecutable(filePath) {
68
+ if (process.platform === "win32") {
69
+ return;
70
+ }
71
+ try {
72
+ const stats = fs.statSync(filePath);
73
+ if (stats.isFile()) {
74
+ fs.chmodSync(filePath, (stats.mode & 0o777) | 0o755);
75
+ }
76
+ } catch {
77
+ // The explicit executable check below reports the actionable failure.
68
78
  }
69
- return {
70
- ...process.env,
71
- CODEX_MANAGED_BY_NPM: "1",
72
- CODEX_MANAGED_PACKAGE_ROOT: fs.realpathSync(packageRoot),
73
- };
74
79
  }
75
80
 
76
- function ensureNativeCodexExecutable(nativeBinaryPath) {
81
+ function managedCodexPackageRoot(fallbackRoot) {
77
82
  try {
78
- fs.accessSync(nativeBinaryPath, fs.constants.X_OK);
79
- return;
83
+ return fs.realpathSync(path.dirname(requireFromHere.resolve("@openai/codex/package.json")));
80
84
  } catch {
81
- repairNativeCodexExecutablePermissionBestEffort(nativeBinaryPath);
85
+ return fs.realpathSync(fallbackRoot);
82
86
  }
87
+ }
83
88
 
89
+ function nativePackagePaths(spec) {
90
+ const packageJsonPath = requireFromHere.resolve(`${spec.packageName}/package.json`);
91
+ const packageRoot = path.dirname(packageJsonPath);
92
+ return {
93
+ packageRoot,
94
+ managedPackageRoot: managedCodexPackageRoot(packageRoot),
95
+ binaryPath: path.join(
96
+ packageRoot,
97
+ "vendor",
98
+ spec.targetTriple,
99
+ "bin",
100
+ spec.binaryFileName,
101
+ ),
102
+ };
103
+ }
104
+
105
+ function resolveNativeCodexCommand() {
106
+ const spec = platformSpec();
107
+ if (!spec) {
108
+ return {
109
+ ok: false,
110
+ reason: `Unsupported Codex platform ${process.platform}/${process.arch}.`,
111
+ };
112
+ }
113
+
114
+ let binaryPath;
115
+ let packageRoot;
116
+ let managedPackageRoot;
84
117
  try {
85
- fs.accessSync(nativeBinaryPath, fs.constants.X_OK);
86
- return;
87
- } catch {
88
- process.stderr.write(
89
- [
90
- `Bundled Codex native binary is not executable: ${nativeBinaryPath}`,
91
- "Reinstall @christiandoxa/prodex with optional dependencies enabled, or set PRODEX_CODEX_BIN to an existing Codex CLI.",
92
- "",
118
+ ({ binaryPath, packageRoot, managedPackageRoot } = nativePackagePaths(spec));
119
+ } catch (error) {
120
+ return {
121
+ ok: false,
122
+ reason: [
123
+ "Unable to locate bundled Codex native package for this platform.",
124
+ `Missing bundled Codex native package ${spec.packageName}.`,
125
+ `Original error: ${error && error.message ? error.message : String(error)}`,
93
126
  ].join("\n"),
94
- );
95
- process.exit(126);
127
+ };
96
128
  }
129
+
130
+ tryMakeExecutable(binaryPath);
131
+
132
+ if (!isExecutableFile(binaryPath)) {
133
+ return {
134
+ ok: false,
135
+ reason: `Bundled Codex native binary is not executable: ${binaryPath}`,
136
+ binaryPath,
137
+ };
138
+ }
139
+
140
+ return {
141
+ ok: true,
142
+ command: binaryPath,
143
+ args: process.argv.slice(2),
144
+ env: {
145
+ CODEX_MANAGED_BY_NPM: "1",
146
+ CODEX_MANAGED_PACKAGE_ROOT: managedPackageRoot,
147
+ },
148
+ };
97
149
  }
98
150
 
99
- function repairNativeCodexExecutablePermissionBestEffort(nativeBinaryPath) {
100
- if (process.platform === "win32") {
101
- return;
151
+ function pathEntries() {
152
+ return (process.env.PATH || "")
153
+ .split(path.delimiter)
154
+ .filter((entry) => entry.length > 0);
155
+ }
156
+
157
+ function commandNames(command) {
158
+ if (process.platform !== "win32") {
159
+ return [command];
160
+ }
161
+ const pathext = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
162
+ .split(";")
163
+ .filter(Boolean);
164
+ const lower = command.toLowerCase();
165
+ if (pathext.some((ext) => lower.endsWith(ext.toLowerCase()))) {
166
+ return [command];
102
167
  }
168
+ return [command, ...pathext.map((ext) => `${command}${ext}`)];
169
+ }
170
+
171
+ function isSelfShim(candidate) {
103
172
  try {
104
- const stats = fs.statSync(nativeBinaryPath);
105
- if (!stats.isFile()) {
106
- return;
173
+ if (fs.realpathSync(candidate) === fs.realpathSync(__filename)) {
174
+ return true;
107
175
  }
108
- fs.chmodSync(nativeBinaryPath, (stats.mode & 0o777) | 0o755);
109
176
  } catch {
110
- // The explicit executable check below reports the actionable failure.
177
+ // Continue with the lightweight content check.
111
178
  }
112
- }
113
-
114
- function resolveNativeCodexCommand() {
115
- const platformTarget = currentPlatformTarget();
116
- if (!platformTarget) {
117
- return null;
179
+ try {
180
+ const prefix = fs.readFileSync(candidate, "utf8").slice(0, 4096);
181
+ return prefix.includes(__filename);
182
+ } catch {
183
+ return false;
118
184
  }
185
+ }
119
186
 
120
- let platformPackageJsonPath;
187
+ function candidateKey(candidate) {
121
188
  try {
122
- platformPackageJsonPath = requireFromHere.resolve(`${platformTarget.packageName}/package.json`);
189
+ return fs.realpathSync(candidate).toLowerCase();
123
190
  } catch {
124
- return null;
191
+ return path.resolve(candidate).toLowerCase();
125
192
  }
193
+ }
126
194
 
127
- const nativeBinaryPath = path.join(
128
- path.dirname(platformPackageJsonPath),
129
- "vendor",
130
- platformTarget.targetTriple,
131
- "bin",
132
- platformTarget.binaryFileName,
133
- );
134
- if (!fs.existsSync(nativeBinaryPath)) {
135
- process.stderr.write(
136
- [
137
- `Missing bundled Codex native binary at ${nativeBinaryPath}`,
138
- "Reinstall @christiandoxa/prodex with optional dependencies enabled, or set PRODEX_CODEX_BIN to an existing Codex CLI.",
139
- "",
140
- ].join("\n"),
141
- );
142
- process.exit(1);
195
+ function externalCodexCandidates() {
196
+ const seen = new Set();
197
+ const candidates = [];
198
+ for (const dir of pathEntries()) {
199
+ for (const name of commandNames("codex")) {
200
+ const candidate = path.join(dir, name);
201
+ const key = candidateKey(candidate);
202
+ if (seen.has(key)) {
203
+ continue;
204
+ }
205
+ seen.add(key);
206
+ candidates.push(candidate);
207
+ }
208
+ }
209
+ return candidates;
210
+ }
211
+
212
+ function summarizeSkippedExternalCandidates(skipped) {
213
+ if (skipped.length === 0) {
214
+ return "No codex candidates were found on PATH.";
215
+ }
216
+ const shown = skipped.slice(0, 8).map(({ candidate, reason }) => `- ${candidate} (${reason})`);
217
+ if (skipped.length > shown.length) {
218
+ shown.push(`- ... ${skipped.length - shown.length} more skipped candidates`);
143
219
  }
144
- ensureNativeCodexExecutable(nativeBinaryPath);
220
+ return ["Searched PATH for external codex candidates:", ...shown].join("\n");
221
+ }
145
222
 
223
+ function resolveExternalCodexCommand() {
224
+ const skipped = [];
225
+ for (const candidate of externalCodexCandidates()) {
226
+ if (isSelfShim(candidate)) {
227
+ skipped.push({ candidate, reason: "Prodex Codex shim" });
228
+ continue;
229
+ }
230
+ if (!isExecutableFile(candidate)) {
231
+ skipped.push({ candidate, reason: "not executable" });
232
+ continue;
233
+ }
234
+ return {
235
+ ok: true,
236
+ command: candidate,
237
+ args: process.argv.slice(2),
238
+ searched: skipped,
239
+ };
240
+ }
146
241
  return {
147
- command: nativeBinaryPath,
148
- args: process.argv.slice(2),
149
- env: codexManagedEnv(),
242
+ ok: false,
243
+ reason: [
244
+ "No executable external codex was found on PATH.",
245
+ summarizeSkippedExternalCandidates(skipped),
246
+ ].join("\n"),
247
+ searched: skipped,
150
248
  };
151
249
  }
152
250
 
251
+
252
+ function autoInstallCodexEnabled() {
253
+ return ["1", "true", "yes"].includes(
254
+ (process.env.PRODEX_CODEX_AUTO_INSTALL || "").toLowerCase(),
255
+ );
256
+ }
257
+
258
+ function npmCommandNames() {
259
+ return commandNames("npm");
260
+ }
261
+
262
+ function resolveNpmCommand() {
263
+ const skipped = [];
264
+ for (const dir of pathEntries()) {
265
+ for (const name of npmCommandNames()) {
266
+ const candidate = path.join(dir, name);
267
+ if (!isExecutableFile(candidate)) {
268
+ skipped.push({ candidate, reason: "not executable" });
269
+ continue;
270
+ }
271
+ return { ok: true, command: candidate, searched: skipped };
272
+ }
273
+ }
274
+ return { ok: false, reason: "No executable npm was found on PATH.", searched: skipped };
275
+ }
276
+
277
+ function installExternalCodexWithNpm() {
278
+ if (!autoInstallCodexEnabled()) {
279
+ return { ok: false, reason: "PRODEX_CODEX_AUTO_INSTALL is not enabled." };
280
+ }
281
+
282
+ const npmCommand = resolveNpmCommand();
283
+ if (!npmCommand.ok) {
284
+ return npmCommand;
285
+ }
286
+
287
+ process.stderr.write(
288
+ [
289
+ "No executable external codex was found; attempting npm install because PRODEX_CODEX_AUTO_INSTALL=1.",
290
+ "Running: npm install -g @openai/codex@latest",
291
+ "",
292
+ ].join("\n"),
293
+ );
294
+
295
+ const result = spawnSync(npmCommand.command, ["install", "-g", "@openai/codex@latest"], {
296
+ stdio: "inherit",
297
+ env: process.env,
298
+ });
299
+
300
+ if (result.error) {
301
+ return {
302
+ ok: false,
303
+ reason: `npm install failed to start: ${result.error.message}`,
304
+ };
305
+ }
306
+ if (result.status !== 0) {
307
+ return {
308
+ ok: false,
309
+ reason: `npm install exited with status ${result.status}`,
310
+ };
311
+ }
312
+
313
+ return resolveExternalCodexCommand();
314
+ }
315
+
153
316
  function resolveCodexCommand() {
154
317
  const nativeCommand = resolveNativeCodexCommand();
155
- if (nativeCommand) {
318
+ if (nativeCommand.ok) {
156
319
  return nativeCommand;
157
320
  }
158
321
 
322
+ let externalCommand = resolveExternalCodexCommand();
323
+ if (!externalCommand.ok && autoInstallCodexEnabled()) {
324
+ const installCommand = installExternalCodexWithNpm();
325
+ if (installCommand.ok) {
326
+ externalCommand = installCommand;
327
+ } else {
328
+ externalCommand = {
329
+ ...externalCommand,
330
+ reason: [externalCommand.reason, installCommand.reason].filter(Boolean).join("\n"),
331
+ };
332
+ }
333
+ }
334
+
335
+ if (externalCommand.ok) {
336
+ process.stderr.write(
337
+ [
338
+ "Bundled Codex is unavailable; falling back to external codex from PATH.",
339
+ nativeCommand.reason,
340
+ summarizeSkippedExternalCandidates(externalCommand.searched || []),
341
+ `External codex: ${externalCommand.command}`,
342
+ "Set PRODEX_CODEX_BIN to pin a specific Codex executable.",
343
+ "",
344
+ ].join("\n"),
345
+ );
346
+ return externalCommand;
347
+ }
348
+
159
349
  process.stderr.write(
160
350
  [
161
- "Unable to locate bundled Codex native package for this platform.",
162
- "Reinstall @christiandoxa/prodex with optional dependencies enabled, or set PRODEX_CODEX_BIN to an existing Codex CLI.",
351
+ nativeCommand.reason || "Unable to locate bundled Codex native package for this platform.",
352
+ "Reinstall @christiandoxa/prodex with optional dependencies enabled, set PRODEX_CODEX_BIN to an existing Codex CLI, or set PRODEX_CODEX_AUTO_INSTALL=1 to let Prodex run npm install -g @openai/codex@latest.",
163
353
  "Prodex does not fall back to @openai/codex/bin/codex.js because npm/Node version skew can make Codex load the wrong optional native package.",
354
+ externalCommand.reason,
164
355
  "",
165
356
  ].join("\n"),
166
357
  );
167
358
  process.exit(1);
168
359
  }
169
360
 
170
- const codexCommand = resolveCodexCommand();
171
- const child = spawn(codexCommand.command, codexCommand.args, {
172
- env: codexCommand.env,
173
- stdio: "inherit",
174
- });
361
+ function spawnCodex(codexCommand, retriedExternalFallback = false) {
362
+ const child = spawn(codexCommand.command, codexCommand.args, {
363
+ stdio: "inherit",
364
+ env: {
365
+ ...process.env,
366
+ ...(codexCommand.env || {}),
367
+ },
368
+ });
175
369
 
176
- child.on("error", (error) => {
177
- if (error && error.code === "EACCES") {
178
- process.stderr.write(
179
- `${error.message}\nSet PRODEX_CODEX_BIN to an existing Codex CLI or reinstall @christiandoxa/prodex.\n`,
180
- );
181
- } else {
182
- process.stderr.write(`${error.message}\n`);
183
- }
184
- process.exit(1);
185
- });
370
+ child.on("error", (error) => {
371
+ if (error && error.code === "EACCES" && !retriedExternalFallback) {
372
+ let externalCommand = resolveExternalCodexCommand();
373
+ if (!externalCommand.ok && autoInstallCodexEnabled()) {
374
+ const installCommand = installExternalCodexWithNpm();
375
+ if (installCommand.ok) {
376
+ externalCommand = installCommand;
377
+ }
378
+ }
379
+ if (externalCommand.ok && externalCommand.command !== codexCommand.command) {
380
+ process.stderr.write(
381
+ [
382
+ `${error.message}`,
383
+ "Bundled Codex could not be executed; falling back to external codex from PATH.",
384
+ summarizeSkippedExternalCandidates(externalCommand.searched || []),
385
+ `External codex: ${externalCommand.command}`,
386
+ "Set PRODEX_CODEX_BIN to pin a specific Codex executable.",
387
+ "",
388
+ ].join("\n"),
389
+ );
390
+ spawnCodex(externalCommand, true);
391
+ return;
392
+ }
393
+ }
186
394
 
187
- child.on("exit", (code, signal) => {
188
- if (signal) {
189
- process.kill(process.pid, signal);
190
- return;
191
- }
192
- process.exit(code ?? 1);
193
- });
395
+ if (error && error.code === "EACCES") {
396
+ process.stderr.write(
397
+ `${error.message}\nSet PRODEX_CODEX_BIN to an existing Codex CLI, set PRODEX_CODEX_AUTO_INSTALL=1 to let Prodex run npm install -g @openai/codex@latest, or reinstall @christiandoxa/prodex.\n`,
398
+ );
399
+ process.exit(126);
400
+ return;
401
+ }
402
+
403
+ process.stderr.write(`${error && error.message ? error.message : String(error)}\n`);
404
+ process.exit(1);
405
+ });
406
+
407
+ child.on("exit", (code, signal) => {
408
+ if (signal) {
409
+ process.kill(process.pid, signal);
410
+ return;
411
+ }
412
+ process.exit(code ?? 1);
413
+ });
414
+ }
415
+
416
+ spawnCodex(resolveCodexCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christiandoxa/prodex",
3
- "version": "0.213.0",
3
+ "version": "0.215.0",
4
4
  "description": "Multi-provider Codex wrapper with OpenAI quota-aware routing and Claude Code support",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -16,12 +16,12 @@
16
16
  "@openai/codex": "latest"
17
17
  },
18
18
  "optionalDependencies": {
19
- "@christiandoxa/prodex-linux-x64": "0.213.0",
20
- "@christiandoxa/prodex-linux-arm64": "0.213.0",
21
- "@christiandoxa/prodex-darwin-x64": "0.213.0",
22
- "@christiandoxa/prodex-darwin-arm64": "0.213.0",
23
- "@christiandoxa/prodex-win32-x64": "0.213.0",
24
- "@christiandoxa/prodex-win32-arm64": "0.213.0",
19
+ "@christiandoxa/prodex-linux-x64": "0.215.0",
20
+ "@christiandoxa/prodex-linux-arm64": "0.215.0",
21
+ "@christiandoxa/prodex-darwin-x64": "0.215.0",
22
+ "@christiandoxa/prodex-darwin-arm64": "0.215.0",
23
+ "@christiandoxa/prodex-win32-x64": "0.215.0",
24
+ "@christiandoxa/prodex-win32-arm64": "0.215.0",
25
25
  "@openai/codex-linux-x64": "npm:@openai/codex@linux-x64",
26
26
  "@openai/codex-linux-arm64": "npm:@openai/codex@linux-arm64",
27
27
  "@openai/codex-darwin-x64": "npm:@openai/codex@darwin-x64",