@axhub/genie 0.2.4 → 0.2.6

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.
Files changed (87) hide show
  1. package/dist/assets/App-CYTE30Cf.js +484 -0
  2. package/dist/assets/{ReviewApp-CsqTAlGU.js → ReviewApp-BEicSBzW.js} +1 -1
  3. package/dist/assets/{_basePickBy-CFRQvihx.js → _basePickBy-DkiHsp3X.js} +1 -1
  4. package/dist/assets/{_baseUniq-Dhh8nCvs.js → _baseUniq-7ElXb2sX.js} +1 -1
  5. package/dist/assets/{arc-DQ0v3dU4.js → arc-CEsS3MdK.js} +1 -1
  6. package/dist/assets/{architectureDiagram-2XIMDMQ5-DmUHdvQH.js → architectureDiagram-2XIMDMQ5-BubZ7T3U.js} +1 -1
  7. package/dist/assets/{blockDiagram-WCTKOSBZ-Bbxhj5KC.js → blockDiagram-WCTKOSBZ-Cza6M6Ht.js} +1 -1
  8. package/dist/assets/{c4Diagram-IC4MRINW-BOivDlQU.js → c4Diagram-IC4MRINW-jhjtOQ12.js} +1 -1
  9. package/dist/assets/channel-RmqTALN0.js +1 -0
  10. package/dist/assets/{chunk-4BX2VUAB-DlvtrM0q.js → chunk-4BX2VUAB--HkodwbY.js} +1 -1
  11. package/dist/assets/{chunk-55IACEB6-DJUSHyTa.js → chunk-55IACEB6-CyBuez4e.js} +1 -1
  12. package/dist/assets/{chunk-FMBD7UC4-C6Ch-htf.js → chunk-FMBD7UC4-CuzG4iAl.js} +1 -1
  13. package/dist/assets/{chunk-JSJVCQXG-DzQIht58.js → chunk-JSJVCQXG-BNi8S861.js} +1 -1
  14. package/dist/assets/{chunk-KX2RTZJC-C05jARMH.js → chunk-KX2RTZJC-D817O-GT.js} +1 -1
  15. package/dist/assets/{chunk-NQ4KR5QH-Ci-n7jfu.js → chunk-NQ4KR5QH-DyujyOvx.js} +1 -1
  16. package/dist/assets/{chunk-QZHKN3VN-jxti9HTX.js → chunk-QZHKN3VN-VMEn-zxh.js} +1 -1
  17. package/dist/assets/{chunk-WL4C6EOR-C559Mk71.js → chunk-WL4C6EOR-CQHHFLvx.js} +1 -1
  18. package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +1 -0
  19. package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +1 -0
  20. package/dist/assets/clone-oT5aWXpf.js +1 -0
  21. package/dist/assets/{cose-bilkent-S5V4N54A-DNO9ncXL.js → cose-bilkent-S5V4N54A-qykDd54p.js} +1 -1
  22. package/dist/assets/{dagre-KLK3FWXG-DJ3dNSYk.js → dagre-KLK3FWXG-Bqp7DjEa.js} +1 -1
  23. package/dist/assets/{diagram-E7M64L7V-Ba_LGLun.js → diagram-E7M64L7V-BKtx468K.js} +1 -1
  24. package/dist/assets/{diagram-IFDJBPK2-Da6K4aP-.js → diagram-IFDJBPK2--fHfW6V2.js} +1 -1
  25. package/dist/assets/{diagram-P4PSJMXO-vZZKB92A.js → diagram-P4PSJMXO-D1kQI5RB.js} +1 -1
  26. package/dist/assets/{erDiagram-INFDFZHY-Csb8dFdP.js → erDiagram-INFDFZHY-DT9YzdNw.js} +1 -1
  27. package/dist/assets/{flowDiagram-PKNHOUZH-DUV13pHi.js → flowDiagram-PKNHOUZH-DWeNr4yg.js} +1 -1
  28. package/dist/assets/{ganttDiagram-A5KZAMGK-B5Kv9Wfz.js → ganttDiagram-A5KZAMGK--IgwcUhI.js} +1 -1
  29. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js → gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js} +1 -1
  30. package/dist/assets/{graph-BbvHswRd.js → graph-Cw1rYoD9.js} +1 -1
  31. package/dist/assets/{highlighted-body-TPN3WLV5-DZJajMGm.js → highlighted-body-TPN3WLV5-BCxJHuqY.js} +1 -1
  32. package/dist/assets/{index-BiErUGrv.js → index-CBuAXA5S.js} +2 -2
  33. package/dist/assets/index-CyLWKyxy.css +1 -0
  34. package/dist/assets/{infoDiagram-LFFYTUFH-8auUIPKW.js → infoDiagram-LFFYTUFH-D2u70rhN.js} +1 -1
  35. package/dist/assets/{ishikawaDiagram-PHBUUO56-JmsNlo2I.js → ishikawaDiagram-PHBUUO56-Cl8yrezU.js} +1 -1
  36. package/dist/assets/{journeyDiagram-4ABVD52K-Cuudv7Vv.js → journeyDiagram-4ABVD52K-ddP0AMU9.js} +1 -1
  37. package/dist/assets/{kanban-definition-K7BYSVSG-Bappd2YO.js → kanban-definition-K7BYSVSG-DbVt0v29.js} +1 -1
  38. package/dist/assets/{layout-BmbfFZKy.js → layout-W_tRx4UV.js} +1 -1
  39. package/dist/assets/{linear-WZnF-PT6.js → linear-CcMb2ay-.js} +1 -1
  40. package/dist/assets/{mermaid-O7DHMXV3-D-2fQRvw.js → mermaid-O7DHMXV3-BBJqt8pT.js} +6 -6
  41. package/dist/assets/{mindmap-definition-YRQLILUH-BQHnzzud.js → mindmap-definition-YRQLILUH-BGhZa7Na.js} +1 -1
  42. package/dist/assets/{pieDiagram-SKSYHLDU-uxjlAy1t.js → pieDiagram-SKSYHLDU-CDyJaACv.js} +1 -1
  43. package/dist/assets/{quadrantDiagram-337W2JSQ-DpwZU-f_.js → quadrantDiagram-337W2JSQ-BSYuqf0Q.js} +1 -1
  44. package/dist/assets/{requirementDiagram-Z7DCOOCP-C_9ClOWm.js → requirementDiagram-Z7DCOOCP-Cfi9YX9H.js} +1 -1
  45. package/dist/assets/{sankeyDiagram-WA2Y5GQK-2-FHHM-R.js → sankeyDiagram-WA2Y5GQK-Di1ShaMF.js} +1 -1
  46. package/dist/assets/{sequenceDiagram-2WXFIKYE-egns-0XI.js → sequenceDiagram-2WXFIKYE-CYTTG38e.js} +1 -1
  47. package/dist/assets/{stateDiagram-RAJIS63D-DoW8U53H.js → stateDiagram-RAJIS63D-CVZYMqyW.js} +1 -1
  48. package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +1 -0
  49. package/dist/assets/{timeline-definition-YZTLITO2-chPa8ppH.js → timeline-definition-YZTLITO2-B1sdb5mK.js} +1 -1
  50. package/dist/assets/{treemap-KZPCXAKY-ajdAP-72.js → treemap-KZPCXAKY-CGG4gx3C.js} +1 -1
  51. package/dist/assets/{vennDiagram-LZ73GAT5-C9If0AT0.js → vennDiagram-LZ73GAT5-Dds37L2k.js} +1 -1
  52. package/dist/assets/{xychartDiagram-JWTSCODW-DD42U6Or.js → xychartDiagram-JWTSCODW-C8QKSyRR.js} +1 -1
  53. package/dist/favicon.png +0 -0
  54. package/dist/icons/icon-128x128.png +0 -0
  55. package/dist/icons/icon-144x144.png +0 -0
  56. package/dist/icons/icon-152x152.png +0 -0
  57. package/dist/icons/icon-192x192.png +0 -0
  58. package/dist/icons/icon-384x384.png +0 -0
  59. package/dist/icons/icon-512x512.png +0 -0
  60. package/dist/icons/icon-72x72.png +0 -0
  61. package/dist/icons/icon-96x96.png +0 -0
  62. package/dist/index.html +2 -3
  63. package/dist/logo-128.png +0 -0
  64. package/dist/logo-256.png +0 -0
  65. package/dist/logo-32.png +0 -0
  66. package/dist/logo-512.png +0 -0
  67. package/dist/logo-64.png +0 -0
  68. package/package.json +6 -3
  69. package/server/bin/codex-sdk-wrapper +68 -0
  70. package/server/bin/codex-sdk-wrapper.cmd +3 -0
  71. package/server/claude-sdk.js +79 -1
  72. package/server/cli.js +7 -3
  73. package/server/external-agent/service.js +25 -0
  74. package/server/index.js +24 -3
  75. package/server/openai-codex.js +13 -5
  76. package/server/routes/codex.js +5 -5
  77. package/server/routes/mcp.js +18 -34
  78. package/server/session-core/providerDiscovery.js +2 -2
  79. package/server/utils/codexPath.js +32 -7
  80. package/server/utils/spawnCommand.js +7 -0
  81. package/dist/assets/App-BxazfNJn.js +0 -484
  82. package/dist/assets/channel-Cj8xVD0X.js +0 -1
  83. package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +0 -1
  84. package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +0 -1
  85. package/dist/assets/clone-BEVqubrI.js +0 -1
  86. package/dist/assets/index-BFX9lxRB.css +0 -1
  87. package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +0 -1
package/server/cli.js CHANGED
@@ -817,6 +817,10 @@ function showVersion() {
817
817
  console.log(`${packageJson.version}`);
818
818
  }
819
819
 
820
+ function getManualUpdateCommand(packageName = packageJson.name || '@axhub/genie') {
821
+ return `npm install -g ${packageName}@latest`;
822
+ }
823
+
820
824
  // Compare semver versions, returns true if v1 > v2
821
825
  function isNewerVersion(v1, v2) {
822
826
  const parts1 = v1.split('.').map(Number);
@@ -838,7 +842,7 @@ async function checkForUpdates(silent = false) {
838
842
 
839
843
  if (isNewerVersion(latestVersion, currentVersion)) {
840
844
  console.log(`\n${c.warn('[UPDATE]')} New version available: ${c.bright(latestVersion)} (current: ${currentVersion})`);
841
- console.log(` Run ${c.bright('axhub-genie update')} to update\n`);
845
+ console.log(` Run ${c.bright(getManualUpdateCommand(packageName))} to update\n`);
842
846
  return { hasUpdate: true, latestVersion, currentVersion };
843
847
  } else if (!silent) {
844
848
  console.log(`${c.ok('[OK]')} You are on the latest version (${currentVersion})`);
@@ -867,11 +871,11 @@ async function updatePackage() {
867
871
  }
868
872
 
869
873
  console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
870
- execSync(`npm update -g ${packageName}`, { stdio: 'inherit' });
874
+ execSync(getManualUpdateCommand(packageName), { stdio: 'inherit' });
871
875
  console.log(`${c.ok('[OK]')} Update complete! Restart axhub-genie to use the new version.`);
872
876
  } catch (e) {
873
877
  console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
874
- console.log(`${c.tip('[TIP]')} Try running manually: npm update -g ${packageJson.name || '@axhub/genie'}`);
878
+ console.log(`${c.tip('[TIP]')} Try running manually: ${getManualUpdateCommand()}`);
875
879
  }
876
880
  }
877
881
 
@@ -5,6 +5,7 @@ import { queryClaudeSDK } from '../claude-sdk.js';
5
5
  import { queryCodex } from '../openai-codex.js';
6
6
  import { queryGemini } from '../gemini-cli.js';
7
7
  import { queryOpencode } from '../opencode-cli.js';
8
+ import { detectProviderInstallationStatus } from '../routes/cli-auth.js';
8
9
  import {
9
10
  buildAgentCallbackPayload,
10
11
  createAgentCallbackEventId,
@@ -38,6 +39,29 @@ function getDeprecatedGitHubFields(body) {
38
39
  return deprecatedFields.filter((field) => body[field] !== undefined);
39
40
  }
40
41
 
42
+ async function ensureProviderInstalled(provider) {
43
+ const status = await detectProviderInstallationStatus(provider);
44
+ if (status?.installed) {
45
+ return status;
46
+ }
47
+
48
+ const installHint = typeof status?.installHint === 'string' && status.installHint.trim()
49
+ ? status.installHint.trim()
50
+ : null;
51
+ const reason = typeof status?.reason === 'string' && status.reason.trim()
52
+ ? status.reason.trim()
53
+ : null;
54
+
55
+ const messageParts = [`Provider "${provider}" is not installed.`];
56
+ if (installHint) {
57
+ messageParts.push(installHint);
58
+ } else if (reason) {
59
+ messageParts.push(reason);
60
+ }
61
+
62
+ throw createRequestError(messageParts.join(' '), 400);
63
+ }
64
+
41
65
  export function buildSessionNavigation(sessionId) {
42
66
  const normalizedSessionId = typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : null;
43
67
  const sessionPath = normalizedSessionId ? `/session/${normalizedSessionId}` : null;
@@ -484,6 +508,7 @@ export async function runExternalAgentRequest({
484
508
  let callbackResult = null;
485
509
 
486
510
  try {
511
+ await ensureProviderInstalled(normalized.provider);
487
512
  await ensureProjectPathExists(finalProjectPath);
488
513
 
489
514
  try {
package/server/index.js CHANGED
@@ -83,7 +83,16 @@ import http from 'http';
83
83
  import cors from 'cors';
84
84
  import { promises as fsPromises } from 'fs';
85
85
  import { spawn } from 'child_process';
86
- import pty from 'node-pty';
86
+ // node-pty is lazy-loaded to avoid native module fd manipulation at startup.
87
+ // Eagerly importing it can corrupt the file descriptor table in npx contexts,
88
+ // causing subsequent spawn() calls (e.g. Claude SDK) to fail with EBADF.
89
+ let _pty = null;
90
+ async function getPty() {
91
+ if (!_pty) {
92
+ _pty = (await import('node-pty')).default;
93
+ }
94
+ return _pty;
95
+ }
87
96
  import fetch from 'node-fetch';
88
97
  import mime from 'mime-types';
89
98
 
@@ -258,6 +267,7 @@ async function setupProjectsWatcher() {
258
267
  const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
259
268
  const codexSessionsPath = path.join(os.homedir(), '.codex', 'sessions');
260
269
  const watchRoots = [claudeProjectsPath, codexSessionsPath];
270
+ const shouldUsePollingWatcher = process.platform === 'darwin';
261
271
 
262
272
  if (projectsWatcher) {
263
273
  projectsWatcher.close();
@@ -279,6 +289,11 @@ async function setupProjectsWatcher() {
279
289
  ignoreInitial: true, // Don't fire events for existing files on startup
280
290
  followSymlinks: false,
281
291
  depth: 10, // Reasonable depth limit
292
+ // On macOS, native watching on these session directories can corrupt
293
+ // later child_process.spawn calls and surface as EBADF across providers.
294
+ // Polling avoids the bad watcher backend while keeping session refreshes.
295
+ usePolling: shouldUsePollingWatcher,
296
+ interval: shouldUsePollingWatcher ? 500 : undefined,
282
297
  awaitWriteFinish: {
283
298
  stabilityThreshold: 100, // Wait 100ms for file to stabilize
284
299
  pollInterval: 50
@@ -551,7 +566,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
551
566
  console.log('Starting system update from directory:', projectRoot);
552
567
 
553
568
  const npmCommand = getNpmCommand();
554
- const updateArgs = ['update', '-g', UPDATE_PACKAGE_NAME];
569
+ const updateArgs = ['install', '-g', `${UPDATE_PACKAGE_NAME}@latest`];
555
570
 
556
571
  // Run npm global update command
557
572
  const child = spawn(npmCommand, updateArgs, {
@@ -1343,6 +1358,7 @@ function handleShellConnection(ws) {
1343
1358
  const termRows = data.rows || 24;
1344
1359
  console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
1345
1360
 
1361
+ const pty = await getPty();
1346
1362
  shellProcess = pty.spawn(shell, shellArgs, {
1347
1363
  name: 'xterm-256color',
1348
1364
  cols: termCols,
@@ -2103,13 +2119,18 @@ async function startServer() {
2103
2119
  // Check if running in production mode (dist folder exists)
2104
2120
  const distIndexPath = path.join(__dirname, '../dist/index.html');
2105
2121
  const isProduction = fs.existsSync(distIndexPath);
2122
+ const runtimeEnvironment = resolveRuntimeEnvironment(isProduction);
2123
+ const frontendMode = isProduction ? 'BUILT DIST' : 'VITE DEV SERVER';
2106
2124
 
2107
2125
  // Log Claude implementation mode
2108
2126
  console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
2109
- console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`);
2127
+ console.log(`${c.info('[INFO]')} Runtime environment: ${c.bright(String(runtimeEnvironment).toUpperCase())}`);
2128
+ console.log(`${c.info('[INFO]')} Frontend source: ${c.bright(frontendMode)}`);
2110
2129
 
2111
2130
  if (!isProduction) {
2112
2131
  console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`);
2132
+ } else {
2133
+ console.log(`${c.info('[INFO]')} Serving frontend assets from ${c.dim(path.join(__dirname, '../dist'))}`);
2113
2134
  }
2114
2135
 
2115
2136
  server.listen(PORT, '0.0.0.0', async () => {
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { Codex } from '@openai/codex-sdk';
17
17
  import { parseCodexTurnUsage } from './utils/codexTokenUsage.js';
18
- import { getCodexPathOverride } from './utils/codexPath.js';
18
+ import { getCodexPathOverride, getCodexProcessEnv } from './utils/codexPath.js';
19
19
  import { cleanupMaterializedImages, materializeImagesToTempFiles } from './utils/agentImages.js';
20
20
  import { resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
21
21
 
@@ -363,7 +363,8 @@ export async function queryCodex(command, options = {}, ws) {
363
363
  try {
364
364
  // Initialize Codex SDK
365
365
  codex = new Codex({
366
- codexPathOverride: getCodexPathOverride()
366
+ codexPathOverride: getCodexPathOverride(),
367
+ env: getCodexProcessEnv()
367
368
  });
368
369
 
369
370
  // Thread options with sandbox and approval settings
@@ -669,11 +670,18 @@ export function getActiveCodexSessions() {
669
670
  */
670
671
  function sendMessage(ws, data) {
671
672
  try {
672
- if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
673
- // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
673
+ const isStructuredWriter = !!(
674
+ ws?.isSSEStreamWriter ||
675
+ ws?.isWebSocketWriter ||
676
+ typeof ws?.setSessionId === 'function' ||
677
+ typeof ws?.getSessionId === 'function'
678
+ );
679
+
680
+ if (isStructuredWriter) {
681
+ // Internal writers expect plain objects and handle serialization themselves.
674
682
  ws.send(data);
675
683
  } else if (typeof ws.send === 'function') {
676
- // Raw WebSocket - stringify here
684
+ // Raw WebSocket clients expect a serialized payload.
677
685
  ws.send(JSON.stringify(data));
678
686
  }
679
687
  } catch (error) {
@@ -1,10 +1,10 @@
1
1
  import express from 'express';
2
- import { spawn } from 'child_process';
3
2
  import { promises as fs } from 'fs';
4
3
  import path from 'path';
5
4
  import os from 'os';
6
5
  import TOML from '@iarna/toml';
7
6
  import { getCodexSessions, getCodexSessionMessages, deleteCodexSession } from '../projects.js';
7
+ import { spawnCommand } from '../utils/spawnCommand.js';
8
8
 
9
9
  const router = express.Router();
10
10
 
@@ -100,7 +100,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
100
100
  router.get('/mcp/cli/list', async (req, res) => {
101
101
  try {
102
102
  const respond = createCliResponder(res);
103
- const proc = spawn('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] });
103
+ const proc = spawnCommand('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] });
104
104
 
105
105
  let stdout = '';
106
106
  let stderr = '';
@@ -151,7 +151,7 @@ router.post('/mcp/cli/add', async (req, res) => {
151
151
  }
152
152
 
153
153
  const respond = createCliResponder(res);
154
- const proc = spawn('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
154
+ const proc = spawnCommand('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
155
155
 
156
156
  let stdout = '';
157
157
  let stderr = '';
@@ -185,7 +185,7 @@ router.delete('/mcp/cli/remove/:name', async (req, res) => {
185
185
  const { name } = req.params;
186
186
 
187
187
  const respond = createCliResponder(res);
188
- const proc = spawn('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] });
188
+ const proc = spawnCommand('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] });
189
189
 
190
190
  let stdout = '';
191
191
  let stderr = '';
@@ -219,7 +219,7 @@ router.get('/mcp/cli/get/:name', async (req, res) => {
219
219
  const { name } = req.params;
220
220
 
221
221
  const respond = createCliResponder(res);
222
- const proc = spawn('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] });
222
+ const proc = spawnCommand('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] });
223
223
 
224
224
  let stdout = '';
225
225
  let stderr = '';
@@ -2,13 +2,9 @@ import express from 'express';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
- import { fileURLToPath } from 'url';
6
- import { dirname } from 'path';
7
- import { spawn } from 'child_process';
5
+ import { spawnCommand } from '../utils/spawnCommand.js';
8
6
 
9
7
  const router = express.Router();
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
8
 
13
9
  // Claude CLI command routes
14
10
 
@@ -16,12 +12,8 @@ const __dirname = dirname(__filename);
16
12
  router.get('/cli/list', async (req, res) => {
17
13
  try {
18
14
  console.log('📋 Listing MCP servers using Claude CLI');
19
-
20
- const { spawn } = await import('child_process');
21
- const { promisify } = await import('util');
22
- const exec = promisify(spawn);
23
-
24
- const process = spawn('claude', ['mcp', 'list'], {
15
+
16
+ const process = spawnCommand('claude', ['mcp', 'list'], {
25
17
  stdio: ['pipe', 'pipe', 'pipe']
26
18
  });
27
19
 
@@ -59,11 +51,9 @@ router.get('/cli/list', async (req, res) => {
59
51
  router.post('/cli/add', async (req, res) => {
60
52
  try {
61
53
  const { name, type = 'stdio', command, args = [], url, headers = {}, env = {}, scope = 'user', projectPath } = req.body;
62
-
54
+
63
55
  console.log(`➕ Adding MCP server using Claude CLI (${scope} scope):`, name);
64
-
65
- const { spawn } = await import('child_process');
66
-
56
+
67
57
  let cliArgs = ['mcp', 'add'];
68
58
 
69
59
  // Add scope flag
@@ -105,8 +95,8 @@ router.post('/cli/add', async (req, res) => {
105
95
  spawnOptions.cwd = projectPath;
106
96
  console.log('📁 Running in project directory:', projectPath);
107
97
  }
108
-
109
- const process = spawn('claude', cliArgs, spawnOptions);
98
+
99
+ const process = spawnCommand('claude', cliArgs, spawnOptions);
110
100
 
111
101
  let stdout = '';
112
102
  let stderr = '';
@@ -142,9 +132,9 @@ router.post('/cli/add', async (req, res) => {
142
132
  router.post('/cli/add-json', async (req, res) => {
143
133
  try {
144
134
  const { name, jsonConfig, scope = 'user', projectPath } = req.body;
145
-
135
+
146
136
  console.log('➕ Adding MCP server using JSON format:', name);
147
-
137
+
148
138
  // Validate and parse JSON config
149
139
  let parsedConfig;
150
140
  try {
@@ -178,8 +168,6 @@ router.post('/cli/add-json', async (req, res) => {
178
168
  });
179
169
  }
180
170
 
181
- const { spawn } = await import('child_process');
182
-
183
171
  // Build the command: claude mcp add-json --scope <scope> <name> '<json>'
184
172
  const cliArgs = ['mcp', 'add-json', '--scope', scope, name];
185
173
 
@@ -198,8 +186,8 @@ router.post('/cli/add-json', async (req, res) => {
198
186
  spawnOptions.cwd = projectPath;
199
187
  console.log('📁 Running in project directory:', projectPath);
200
188
  }
201
-
202
- const process = spawn('claude', cliArgs, spawnOptions);
189
+
190
+ const process = spawnCommand('claude', cliArgs, spawnOptions);
203
191
 
204
192
  let stdout = '';
205
193
  let stderr = '';
@@ -247,11 +235,9 @@ router.delete('/cli/remove/:name', async (req, res) => {
247
235
  actualName = serverName;
248
236
  actualScope = actualScope || prefix; // Use prefix as scope if not provided in query
249
237
  }
250
-
238
+
251
239
  console.log('🗑️ Removing MCP server using Claude CLI:', actualName, 'scope:', actualScope);
252
-
253
- const { spawn } = await import('child_process');
254
-
240
+
255
241
  // Build command args based on scope
256
242
  let cliArgs = ['mcp', 'remove'];
257
243
 
@@ -267,7 +253,7 @@ router.delete('/cli/remove/:name', async (req, res) => {
267
253
 
268
254
  console.log('🔧 Running Claude CLI command:', 'claude', cliArgs.join(' '));
269
255
 
270
- const process = spawn('claude', cliArgs, {
256
+ const process = spawnCommand('claude', cliArgs, {
271
257
  stdio: ['pipe', 'pipe', 'pipe']
272
258
  });
273
259
 
@@ -305,12 +291,10 @@ router.delete('/cli/remove/:name', async (req, res) => {
305
291
  router.get('/cli/get/:name', async (req, res) => {
306
292
  try {
307
293
  const { name } = req.params;
308
-
294
+
309
295
  console.log('📄 Getting MCP server details using Claude CLI:', name);
310
-
311
- const { spawn } = await import('child_process');
312
-
313
- const process = spawn('claude', ['mcp', 'get', name], {
296
+
297
+ const process = spawnCommand('claude', ['mcp', 'get', name], {
314
298
  stdio: ['pipe', 'pipe', 'pipe']
315
299
  });
316
300
 
@@ -549,4 +533,4 @@ function parseClaudeGetOutput(output) {
549
533
  }
550
534
  }
551
535
 
552
- export default router;
536
+ export default router;
@@ -1,4 +1,3 @@
1
- import { spawn } from 'child_process';
2
1
  import { promises as fs } from 'fs';
3
2
  import path from 'path';
4
3
  import os from 'os';
@@ -10,6 +9,7 @@ import {
10
9
  checkOpencodeCredentials
11
10
  } from '../routes/cli-auth.js';
12
11
  import { listOpencodeModels } from '../opencode-cli.js';
12
+ import { spawnCommand } from '../utils/spawnCommand.js';
13
13
  import {
14
14
  CLAUDE_MODELS,
15
15
  CODEX_MODELS, GEMINI_MODELS,
@@ -36,7 +36,7 @@ function runCommand(command, args = [], options = {}) {
36
36
  };
37
37
 
38
38
  try {
39
- child = spawn(command, args, {
39
+ child = spawnCommand(command, args, {
40
40
  cwd: options.cwd || process.cwd(),
41
41
  stdio: ['ignore', 'pipe', 'pipe'],
42
42
  env: { ...process.env, ...(options.env || {}) }
@@ -4,6 +4,16 @@ import { fileURLToPath } from 'url';
4
4
  const __filename = fileURLToPath(import.meta.url);
5
5
  const __dirname = path.dirname(__filename);
6
6
 
7
+ function getCodexSdkRoot() {
8
+ try {
9
+ const sdkEntryUrl = import.meta.resolve('@openai/codex-sdk');
10
+ const sdkEntryPath = fileURLToPath(sdkEntryUrl);
11
+ return path.resolve(path.dirname(sdkEntryPath), '..');
12
+ } catch {
13
+ return path.join(__dirname, '..', '..', 'node_modules', '@openai', 'codex-sdk');
14
+ }
15
+ }
16
+
7
17
  export function getBundledCodexPath() {
8
18
  const { platform, arch } = process;
9
19
  let targetTriple = null;
@@ -25,12 +35,7 @@ export function getBundledCodexPath() {
25
35
 
26
36
  const binaryName = platform === 'win32' ? 'codex.exe' : 'codex';
27
37
  return path.join(
28
- __dirname,
29
- '..',
30
- '..',
31
- 'node_modules',
32
- '@openai',
33
- 'codex-sdk',
38
+ getCodexSdkRoot(),
34
39
  'vendor',
35
40
  targetTriple,
36
41
  'codex',
@@ -40,8 +45,28 @@ export function getBundledCodexPath() {
40
45
 
41
46
  export function getCodexPathOverride() {
42
47
  if (process.platform === 'win32') {
43
- return path.join(__dirname, '..', 'bin', 'codex-sdk-wrapper.cmd');
48
+ // The SDK uses child_process.spawn() directly. Handing it a .cmd wrapper
49
+ // is brittle on Windows, so point it at the bundled executable instead.
50
+ return getBundledCodexPath();
44
51
  }
45
52
 
46
53
  return path.join(__dirname, '..', 'bin', 'codex-sdk-wrapper.js');
47
54
  }
55
+
56
+ export function getCodexProcessEnv(baseEnv = process.env) {
57
+ const env = {};
58
+
59
+ for (const [key, value] of Object.entries(baseEnv || {})) {
60
+ if (value !== undefined) {
61
+ env[key] = value;
62
+ }
63
+ }
64
+
65
+ // Hosted Node environments can leak IPC-related variables into children.
66
+ // If a Node-based helper inherits them, startup may fail with EBADF.
67
+ delete env.NODE_CHANNEL_FD;
68
+ delete env.NODE_UNIQUE_ID;
69
+ delete env.ELECTRON_RUN_AS_NODE;
70
+
71
+ return env;
72
+ }
@@ -0,0 +1,7 @@
1
+ import { spawn as nodeSpawn } from 'child_process';
2
+ import crossSpawn from 'cross-spawn';
3
+
4
+ export function spawnCommand(command, args = [], options = {}) {
5
+ const spawnImpl = process.platform === 'win32' ? crossSpawn : nodeSpawn;
6
+ return spawnImpl(command, args, options);
7
+ }