@christiandoxa/prodex 0.212.0 → 0.214.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:
@@ -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,169 +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
+ }
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
+ }
83
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
+ }
143
208
  }
144
- ensureNativeCodexExecutable(nativeBinaryPath);
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`);
219
+ }
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
 
153
- function resolveCodexJsCommand() {
154
- const packageRoot = resolveOpenAiCodexPackageRoot();
155
- if (!packageRoot) {
156
- process.stderr.write(
157
- "Unable to locate @openai/codex. Reinstall @christiandoxa/prodex so its runtime dependency is present.\n",
158
- );
159
- process.exit(1);
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." };
160
280
  }
161
- const packageJsonPath = path.join(packageRoot, "package.json");
162
281
 
163
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
164
- if (typeof packageJson.bin === "string") {
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) {
165
301
  return {
166
- command: process.execPath,
167
- args: [path.resolve(packageRoot, packageJson.bin), ...process.argv.slice(2)],
168
- env: process.env,
302
+ ok: false,
303
+ reason: `npm install failed to start: ${result.error.message}`,
169
304
  };
170
305
  }
171
- if (packageJson.bin && typeof packageJson.bin === "object") {
172
- const candidate = packageJson.bin.codex ?? Object.values(packageJson.bin)[0];
173
- if (typeof candidate === "string") {
174
- return {
175
- command: process.execPath,
176
- args: [path.resolve(packageRoot, candidate), ...process.argv.slice(2)],
177
- env: process.env,
178
- };
179
- }
306
+ if (result.status !== 0) {
307
+ return {
308
+ ok: false,
309
+ reason: `npm install exited with status ${result.status}`,
310
+ };
180
311
  }
181
- return {
182
- command: process.execPath,
183
- args: [path.resolve(packageRoot, "bin", "codex.js"), ...process.argv.slice(2)],
184
- env: process.env,
185
- };
312
+
313
+ return resolveExternalCodexCommand();
186
314
  }
187
315
 
188
316
  function resolveCodexCommand() {
189
- return resolveNativeCodexCommand() ?? resolveCodexJsCommand();
190
- }
317
+ const nativeCommand = resolveNativeCodexCommand();
318
+ if (nativeCommand.ok) {
319
+ return nativeCommand;
320
+ }
191
321
 
192
- const codexCommand = resolveCodexCommand();
193
- const child = spawn(codexCommand.command, codexCommand.args, {
194
- env: codexCommand.env,
195
- stdio: "inherit",
196
- });
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
+ }
197
334
 
198
- child.on("error", (error) => {
199
- if (error && error.code === "EACCES") {
335
+ if (externalCommand.ok) {
200
336
  process.stderr.write(
201
- `${error.message}\nSet PRODEX_CODEX_BIN to an existing Codex CLI or reinstall @christiandoxa/prodex.\n`,
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"),
202
345
  );
203
- } else {
204
- process.stderr.write(`${error.message}\n`);
346
+ return externalCommand;
205
347
  }
348
+
349
+ process.stderr.write(
350
+ [
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.",
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,
355
+ "",
356
+ ].join("\n"),
357
+ );
206
358
  process.exit(1);
207
- });
359
+ }
208
360
 
209
- child.on("exit", (code, signal) => {
210
- if (signal) {
211
- process.kill(process.pid, signal);
212
- return;
213
- }
214
- process.exit(code ?? 1);
215
- });
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
+ });
369
+
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
+ }
394
+
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.212.0",
3
+ "version": "0.214.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.212.0",
20
- "@christiandoxa/prodex-linux-arm64": "0.212.0",
21
- "@christiandoxa/prodex-darwin-x64": "0.212.0",
22
- "@christiandoxa/prodex-darwin-arm64": "0.212.0",
23
- "@christiandoxa/prodex-win32-x64": "0.212.0",
24
- "@christiandoxa/prodex-win32-arm64": "0.212.0",
19
+ "@christiandoxa/prodex-linux-x64": "0.214.0",
20
+ "@christiandoxa/prodex-linux-arm64": "0.214.0",
21
+ "@christiandoxa/prodex-darwin-x64": "0.214.0",
22
+ "@christiandoxa/prodex-darwin-arm64": "0.214.0",
23
+ "@christiandoxa/prodex-win32-x64": "0.214.0",
24
+ "@christiandoxa/prodex-win32-arm64": "0.214.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",