polydev-ai 1.2.14 → 1.4.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/lib/cliManager.js CHANGED
@@ -1,508 +1,715 @@
1
- "use strict";
2
- /**
3
- * CLI Manager with MCP Server Integration
4
- * Handles detection, authentication, and prompt sending for Claude Code, Codex CLI, and Gemini CLI
5
- * Uses MCP servers for database operations and status reporting
6
- */
7
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
- if (k2 === undefined) k2 = k;
9
- var desc = Object.getOwnPropertyDescriptor(m, k);
10
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
- desc = { enumerable: true, get: function() { return m[k]; } };
12
- }
13
- Object.defineProperty(o, k2, desc);
14
- }) : (function(o, m, k, k2) {
15
- if (k2 === undefined) k2 = k;
16
- o[k2] = m[k];
17
- }));
18
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
- Object.defineProperty(o, "default", { enumerable: true, value: v });
20
- }) : function(o, v) {
21
- o["default"] = v;
22
- });
23
- var __importStar = (this && this.__importStar) || (function () {
24
- var ownKeys = function(o) {
25
- ownKeys = Object.getOwnPropertyNames || function (o) {
26
- var ar = [];
27
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
- return ar;
29
- };
30
- return ownKeys(o);
31
- };
32
- return function (mod) {
33
- if (mod && mod.__esModule) return mod;
34
- var result = {};
35
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
- __setModuleDefault(result, mod);
37
- return result;
38
- };
39
- })();
40
- Object.defineProperty(exports, "__esModule", { value: true });
41
- exports.CLIManager = void 0;
42
- const child_process_1 = require("child_process");
43
- const util_1 = require("util");
44
- const fs = __importStar(require("fs"));
1
+ const { exec, spawn } = require('child_process');
2
+ const { promisify } = require('util');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
45
6
  const which = require('which');
46
- const shell = require('shelljs');
47
- const execAsync = (0, util_1.promisify)(child_process_1.exec);
7
+
8
+ const execAsync = promisify(exec);
9
+
48
10
  class CLIManager {
49
- constructor() {
50
- this.providers = new Map();
51
- this.statusCache = new Map();
52
- this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
53
- this.initializeProviders();
54
- }
55
- initializeProviders() {
56
- const providers = [
57
- {
58
- id: 'claude_code',
59
- name: 'Claude Code',
60
- executable: 'claude',
61
- versionCommand: 'claude --version',
62
- authCheckCommand: 'claude auth status',
63
- chatCommand: 'claude chat',
64
- supportsStdin: true,
65
- supportsArgs: true,
66
- installInstructions: 'Install via: npm install -g @anthropic-ai/claude-code',
67
- authInstructions: 'Authenticate with: claude auth login'
68
- },
69
- {
70
- id: 'codex_cli',
71
- name: 'Codex CLI',
72
- executable: 'codex',
73
- versionCommand: 'codex --version',
74
- authCheckCommand: 'codex login --check', // Changed to check login status
75
- chatCommand: 'codex exec', // Changed from 'codex chat' to 'codex exec'
76
- supportsStdin: false, // Codex exec doesn't use stdin
77
- supportsArgs: true,
78
- installInstructions: 'Install via: npm install -g codex-cli',
79
- authInstructions: 'Authenticate with: codex login'
80
- },
81
- {
82
- id: 'gemini_cli',
83
- name: 'Gemini CLI',
84
- executable: 'gemini',
85
- versionCommand: 'gemini --version',
86
- authCheckCommand: 'gemini auth status',
87
- chatCommand: 'gemini chat',
88
- supportsStdin: true,
89
- supportsArgs: true,
90
- installInstructions: 'Install Gemini CLI from Google',
91
- authInstructions: 'Authenticate with: gemini auth login'
92
- }
93
- ];
94
- providers.forEach(provider => {
95
- this.providers.set(provider.id, provider);
96
- });
97
- }
98
- /**
99
- * Force CLI detection for all providers or specific provider
100
- * Updates status cache and reports to MCP server via Supabase
101
- */
102
- async forceCliDetection(userId, providerId) {
103
- console.error(`[CLI Manager] Force detection started for ${providerId || 'all providers'}`);
104
- const results = {};
105
- const providersToCheck = providerId ? [providerId] : Array.from(this.providers.keys());
106
- for (const id of providersToCheck) {
107
- const provider = this.providers.get(id);
108
- if (!provider)
109
- continue;
110
- try {
111
- const status = await this.detectCLI(provider);
112
- this.statusCache.set(id, status);
113
- results[id] = status;
114
- // Update database via MCP Supabase if userId provided
115
- if (userId) {
116
- await this.updateCliStatusInDatabase(userId, id, status);
117
- }
118
- console.error(`[CLI Manager] ${provider.name}: ${status.available ? 'Available' : 'Not Available'}`);
119
- }
120
- catch (error) {
121
- console.error(`[CLI Manager] Error detecting ${provider.name}:`, error);
122
- const errorStatus = {
123
- available: false,
124
- authenticated: false,
125
- lastChecked: new Date(),
126
- error: error instanceof Error ? error.message : 'Unknown error'
127
- };
128
- this.statusCache.set(id, errorStatus);
129
- results[id] = errorStatus;
130
- }
131
- }
132
- return results;
133
- }
134
- /**
135
- * Get CLI status with cache support
136
- */
137
- async getCliStatus(providerId, userId) {
138
- const cached = this.statusCache.get(providerId);
139
- const now = new Date();
140
- // Return cached result if still valid
141
- if (cached && (now.getTime() - cached.lastChecked.getTime()) < this.cacheTimeout) {
142
- return cached;
143
- }
144
- // Force detection if cache is stale or missing
145
- const results = await this.forceCliDetection(userId, providerId);
146
- return results[providerId] || {
11
+ constructor() {
12
+ this.providers = new Map();
13
+ this.statusCache = new Map();
14
+ this.CACHE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
15
+ this.initializeProviders();
16
+ }
17
+
18
+ initializeProviders() {
19
+ const providers = [
20
+ {
21
+ id: 'claude_code',
22
+ name: 'Claude Code',
23
+ command: process.env.CLAUDE_CODE_PATH || 'claude',
24
+ subcommands: {
25
+ chat: [],
26
+ version: ['--version'],
27
+ auth_status: ['--print', 'test auth'], // Use --print to test auth
28
+ test_prompt: ['--print']
29
+ },
30
+ install_instructions: 'Install via: npm install -g @anthropic-ai/claude-code',
31
+ auth_instructions: 'Authenticate with Claude Code'
32
+ },
33
+ {
34
+ id: 'codex_cli',
35
+ name: 'Codex CLI',
36
+ command: process.env.CODEX_CLI_PATH || 'codex',
37
+ subcommands: {
38
+ chat: ['chat'],
39
+ version: ['--version'],
40
+ auth_status: ['login', 'status'], // Correct command: codex login status
41
+ test_prompt: ['exec'],
42
+ alternate_test_prompts: [
43
+ ['prompt'],
44
+ ['ask']
45
+ ]
46
+ },
47
+ install_instructions: 'Install Codex CLI from OpenAI',
48
+ auth_instructions: 'Authenticate with: codex login'
49
+ },
50
+ {
51
+ id: 'gemini_cli',
52
+ name: 'Gemini CLI',
53
+ command: process.env.GEMINI_CLI_PATH || 'gemini',
54
+ subcommands: {
55
+ chat: ['chat'],
56
+ version: ['--version'],
57
+ auth_status: ['auth-status'] // gemini-mcp auth-status command
58
+ },
59
+ install_instructions: 'Install Gemini CLI from Google',
60
+ auth_instructions: 'Authenticate with: gemini (then /auth login)'
61
+ }
62
+ ];
63
+
64
+ providers.forEach(provider => {
65
+ this.providers.set(provider.id, provider);
66
+ });
67
+ }
68
+
69
+ async forceCliDetection(specificProvider) {
70
+ const results = {};
71
+ const providersToCheck = specificProvider
72
+ ? [this.providers.get(specificProvider)].filter(Boolean)
73
+ : Array.from(this.providers.values());
74
+
75
+ // Log system environment for debugging
76
+ console.log(`[Polydev CLI] Detecting CLI providers - Node.js ${process.version}, Platform: ${process.platform}`);
77
+
78
+ for (const provider of providersToCheck) {
79
+ if (provider) {
80
+ try {
81
+ results[provider.id] = await this.detectCliProvider(provider);
82
+ this.statusCache.set(provider.id, results[provider.id]);
83
+
84
+ // Log compatibility issues for user awareness
85
+ if (results[provider.id].error && results[provider.id].error.includes('Compatibility Issue')) {
86
+ console.warn(`[Polydev CLI] ⚠️ ${provider.name} compatibility issue detected. See error details for solutions.`);
87
+ }
88
+ } catch (error) {
89
+ results[provider.id] = {
147
90
  available: false,
148
91
  authenticated: false,
149
- lastChecked: now,
150
- error: 'Provider not found'
151
- };
92
+ error: `Detection failed: ${error.message}`,
93
+ last_checked: new Date()
94
+ };
95
+ }
96
+ }
152
97
  }
153
- /**
154
- * Send prompt to CLI provider
155
- */
156
- async sendCliPrompt(providerId, prompt, mode = 'args', timeoutMs = 30000) {
157
- const provider = this.providers.get(providerId);
158
- if (!provider) {
159
- return {
160
- success: false,
161
- error: `Unknown CLI provider: ${providerId}`
162
- };
98
+
99
+ return results;
100
+ }
101
+
102
+ async getCliStatus(specificProvider) {
103
+ const results = {};
104
+ const providersToCheck = specificProvider
105
+ ? [this.providers.get(specificProvider)].filter(Boolean)
106
+ : Array.from(this.providers.values());
107
+
108
+ for (const provider of providersToCheck) {
109
+ if (provider) {
110
+ const cached = this.statusCache.get(provider.id);
111
+ if (cached && this.isCacheValid(cached)) {
112
+ results[provider.id] = cached;
113
+ } else {
114
+ const detection = await this.forceCliDetection(provider.id);
115
+ results[provider.id] = detection[provider.id];
163
116
  }
164
- // Check if CLI is available and authenticated
165
- const status = await this.getCliStatus(providerId);
166
- if (!status.available) {
167
- return {
168
- success: false,
169
- error: `${provider.name} is not available. ${provider.installInstructions}`
170
- };
117
+ }
118
+ }
119
+
120
+ return results;
121
+ }
122
+
123
+ isCacheValid(status) {
124
+ if (!status.last_checked) return false;
125
+ const now = new Date().getTime();
126
+ const checked = new Date(status.last_checked).getTime();
127
+ return (now - checked) < this.CACHE_TIMEOUT_MS;
128
+ }
129
+
130
+ async detectCliProvider(provider) {
131
+ try {
132
+ const cliPath = await this.findCliPath(provider.command);
133
+ if (!cliPath) {
134
+ return {
135
+ available: false,
136
+ authenticated: false,
137
+ error: `${provider.name} not found in PATH. ${provider.install_instructions}`,
138
+ last_checked: new Date()
139
+ };
140
+ }
141
+
142
+ let version;
143
+ try {
144
+ const versionResult = await this.executeCliCommand(
145
+ provider.command,
146
+ provider.subcommands.version,
147
+ 'args',
148
+ 5000
149
+ );
150
+ version = versionResult.stdout?.trim();
151
+ } catch (versionError) {
152
+ if (process.env.POLYDEV_CLI_DEBUG) {
153
+ console.log(`[CLI Debug] Version check failed for ${provider.id}:`, versionError);
171
154
  }
172
- if (!status.authenticated) {
173
- return {
174
- success: false,
175
- error: `${provider.name} is not authenticated. ${provider.authInstructions}`
176
- };
155
+ }
156
+
157
+ let authenticated = false;
158
+
159
+ // For Claude Code, skip command-based auth check and use file-based detection directly
160
+ // This avoids the recursion issue when running from within Claude Code
161
+ if (provider.id === 'claude_code') {
162
+ authenticated = await this.checkAuthenticationByFiles(provider.id);
163
+
164
+ if (process.env.POLYDEV_CLI_DEBUG) {
165
+ console.log(`[CLI Debug] File-based auth check for ${provider.id}:`, authenticated);
177
166
  }
178
- const startTime = Date.now();
167
+ } else {
168
+ // For other providers, try command-based auth check first
179
169
  try {
180
- let result;
181
- if (mode === 'stdin' && provider.supportsStdin) {
182
- result = await this.sendPromptViaStdin(provider, prompt, timeoutMs);
183
- }
184
- else if (mode === 'args' && provider.supportsArgs) {
185
- result = await this.sendPromptViaArgs(provider, prompt, timeoutMs);
186
- }
187
- else {
188
- return {
189
- success: false,
190
- error: `${provider.name} does not support ${mode} mode`
191
- };
192
- }
193
- const latencyMs = Date.now() - startTime;
194
- return {
195
- success: true,
196
- content: result,
197
- latencyMs
198
- };
170
+ const authResult = await this.executeCliCommand(
171
+ provider.command,
172
+ provider.subcommands.auth_status,
173
+ 'args',
174
+ 5000 // Reduced timeout to 5 seconds
175
+ );
176
+
177
+ // If command succeeds, check output for authentication indicators
178
+ const authOutput = (authResult.stdout + ' ' + authResult.stderr).toLowerCase();
179
+
180
+ if (process.env.POLYDEV_CLI_DEBUG) {
181
+ console.log(`[CLI Debug] Auth output for ${provider.id}: "${authOutput}"`);
182
+ }
183
+
184
+ authenticated = this.parseAuthenticationStatus(provider.id, authOutput);
185
+
186
+ } catch (authError) {
187
+ if (process.env.POLYDEV_CLI_DEBUG) {
188
+ console.log(`[CLI Debug] Auth check failed for ${provider.id}:`, authError);
189
+ }
190
+
191
+ // Fallback to file-based authentication detection
192
+ authenticated = await this.checkAuthenticationByFiles(provider.id);
193
+
194
+ if (process.env.POLYDEV_CLI_DEBUG) {
195
+ console.log(`[CLI Debug] File-based auth check for ${provider.id}:`, authenticated);
196
+ }
199
197
  }
200
- catch (error) {
201
- return {
202
- success: false,
203
- error: error instanceof Error ? error.message : 'CLI execution failed',
204
- latencyMs: Date.now() - startTime
205
- };
198
+ }
199
+
200
+ // Special handling for Gemini CLI Node.js compatibility issues
201
+ let errorMessage = undefined;
202
+ if (!authenticated) {
203
+ if (provider.id === 'gemini_cli') {
204
+ // Check if the issue is Node.js compatibility
205
+ try {
206
+ const authResult = await this.executeCliCommand(
207
+ provider.command,
208
+ ['--help'],
209
+ 'args',
210
+ 2000
211
+ );
212
+ const testOutput = (authResult.stdout + ' ' + authResult.stderr).toLowerCase();
213
+ if (testOutput.includes('referenceerror: file is not defined') ||
214
+ testOutput.includes('undici/lib/web/webidl')) {
215
+ errorMessage = `⚠️ Gemini CLI Compatibility Issue: Node.js v${process.version} doesn't support the 'File' global that Gemini CLI requires.
216
+
217
+ Solutions:
218
+ • Update to Node.js v20+ (recommended): nvm install 20 && nvm use 20
219
+ • Reinstall Gemini CLI: npm uninstall -g @google/gemini-cli && npm install -g @google/gemini-cli@latest
220
+ • Alternative: Use Google AI Studio directly or switch to Claude/OpenAI providers
221
+
222
+ This is a known issue with @google/gemini-cli@0.3.4 and older Node.js versions.`;
223
+ } else {
224
+ errorMessage = `Not authenticated. ${provider.auth_instructions}`;
225
+ }
226
+ } catch {
227
+ errorMessage = `Not authenticated. ${provider.auth_instructions}`;
228
+ }
229
+ } else {
230
+ errorMessage = `Not authenticated. ${provider.auth_instructions}`;
206
231
  }
232
+ }
233
+
234
+ return {
235
+ available: true,
236
+ authenticated,
237
+ version,
238
+ path: cliPath,
239
+ last_checked: new Date(),
240
+ error: errorMessage
241
+ };
242
+
243
+ } catch (error) {
244
+ return {
245
+ available: false,
246
+ authenticated: false,
247
+ error: `Detection failed: ${error.message}`,
248
+ last_checked: new Date()
249
+ };
207
250
  }
208
- /**
209
- * Detect CLI installation and authentication
210
- */
211
- async detectCLI(provider) {
212
- const customPath = process.env[`${provider.id.toUpperCase()}_PATH`];
213
- let executablePath;
214
- try {
215
- // Try custom path first, then system PATH
216
- if (customPath && fs.existsSync(customPath)) {
217
- executablePath = customPath;
218
- }
219
- else {
220
- executablePath = await which(provider.executable);
251
+ }
252
+
253
+ async findCliPath(command) {
254
+ try {
255
+ return await which(command);
256
+ } catch (error) {
257
+ return null;
258
+ }
259
+ }
260
+
261
+ async checkAuthenticationByFiles(providerId) {
262
+ const os = require('os');
263
+
264
+ try {
265
+ switch (providerId) {
266
+ case 'claude_code':
267
+ // Check for Claude Code session files
268
+ const claudeConfigPath = path.join(os.homedir(), '.claude.json');
269
+ if (fs.existsSync(claudeConfigPath)) {
270
+ const configContent = fs.readFileSync(claudeConfigPath, 'utf8');
271
+ // Look for session or auth tokens in the config
272
+ return configContent.length > 100 &&
273
+ (configContent.includes('session') ||
274
+ configContent.includes('token') ||
275
+ configContent.includes('auth'));
276
+ }
277
+ return false;
278
+
279
+ case 'codex_cli':
280
+ // Check for Codex auth files
281
+ const codexAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
282
+ if (fs.existsSync(codexAuthPath)) {
283
+ const authContent = fs.readFileSync(codexAuthPath, 'utf8');
284
+ try {
285
+ const authData = JSON.parse(authContent);
286
+ return authData && (authData.token || authData.access_token || authData.authenticated);
287
+ } catch {
288
+ return authContent.length > 10; // Has some auth content
221
289
  }
290
+ }
291
+ return false;
292
+
293
+ case 'gemini_cli':
294
+ // Check for Gemini CLI auth files (if any)
295
+ const geminiConfigPath = path.join(os.homedir(), '.config', 'gemini-cli', 'config.json');
296
+ if (fs.existsSync(geminiConfigPath)) {
297
+ const configContent = fs.readFileSync(geminiConfigPath, 'utf8');
298
+ return configContent.includes('auth') || configContent.includes('token');
299
+ }
300
+ return false;
301
+
302
+ default:
303
+ return false;
304
+ }
305
+ } catch (error) {
306
+ if (process.env.POLYDEV_CLI_DEBUG) {
307
+ console.log(`[CLI Debug] File-based auth check error for ${providerId}:`, error.message);
308
+ }
309
+ return false;
310
+ }
311
+ }
312
+
313
+ parseAuthenticationStatus(providerId, authOutput) {
314
+
315
+ switch (providerId) {
316
+ case 'claude_code':
317
+ // If --print "test auth" works without error, Claude Code is authenticated
318
+ // Look for actual response content (not authentication errors)
319
+ const claudeAuth = !authOutput.includes('not authenticated') &&
320
+ !authOutput.includes('please log in') &&
321
+ !authOutput.includes('authentication required') &&
322
+ !authOutput.includes('login required') &&
323
+ authOutput.length > 10; // Has actual content response
324
+
325
+ return claudeAuth;
326
+
327
+ case 'codex_cli':
328
+ // Look for specific codex login status responses
329
+ const hasLoggedIn = authOutput.includes('logged in using');
330
+ const hasAuthenticated = authOutput.includes('authenticated');
331
+ const hasChatGpt = authOutput.includes('chatgpt') && !authOutput.includes('not logged in');
332
+
333
+
334
+ return hasLoggedIn || hasAuthenticated || hasChatGpt;
335
+
336
+ case 'gemini_cli':
337
+ // Check for Node.js compatibility issues first
338
+ if (authOutput.includes('referenceerror: file is not defined') ||
339
+ authOutput.includes('undici/lib/web/webidl') ||
340
+ authOutput.includes('file is not defined')) {
341
+ return false; // CLI is broken due to Node.js compatibility
222
342
  }
223
- catch (error) {
224
- return {
225
- available: false,
226
- authenticated: false,
227
- lastChecked: new Date(),
228
- error: `${provider.name} not found in PATH. ${provider.installInstructions}`
229
- };
343
+
344
+ return !authOutput.includes('not authenticated') &&
345
+ !authOutput.includes('please login') &&
346
+ (authOutput.includes('authenticated') || authOutput.includes('logged in'));
347
+
348
+ default:
349
+ return authOutput.includes('authenticated') || authOutput.includes('logged in');
350
+ }
351
+ }
352
+
353
+ async sendCliPrompt(providerId, prompt, mode = 'args', timeoutMs = null) {
354
+ // Set provider-specific default timeouts
355
+ if (timeoutMs === null) {
356
+ timeoutMs = providerId === 'claude_code' ? 60000 : 30000; // 60s for Claude Code, 30s for others
357
+ }
358
+ if (providerId === 'codex_cli' && timeoutMs < 90000) {
359
+ timeoutMs = 90000;
360
+ }
361
+
362
+ // Ensure timeoutMs is valid (not undefined, null, Infinity, or negative)
363
+ if (!timeoutMs || timeoutMs === Infinity || timeoutMs < 1 || timeoutMs > 300000) {
364
+ timeoutMs = 30000 // Default to 30 seconds
365
+ }
366
+
367
+ const startTime = Date.now();
368
+
369
+ try {
370
+ const provider = this.providers.get(providerId);
371
+ if (!provider) {
372
+ return {
373
+ success: false,
374
+ error: `Unknown provider: ${providerId}`,
375
+ latency_ms: Date.now() - startTime,
376
+ timestamp: new Date()
377
+ };
378
+ }
379
+
380
+ const status = await this.getCliStatus(providerId);
381
+ const providerStatus = status[providerId];
382
+
383
+ if (!providerStatus?.available) {
384
+ return {
385
+ success: false,
386
+ error: `${provider.name} is not available. ${provider.install_instructions}`,
387
+ latency_ms: Date.now() - startTime,
388
+ timestamp: new Date()
389
+ };
390
+ }
391
+
392
+ if (!providerStatus.authenticated) {
393
+ return {
394
+ success: false,
395
+ error: `${provider.name} is not authenticated. ${provider.auth_instructions}`,
396
+ latency_ms: Date.now() - startTime,
397
+ timestamp: new Date()
398
+ };
399
+ }
400
+
401
+ const promptVariants = [
402
+ provider.subcommands?.test_prompt ? [...provider.subcommands.test_prompt] : []
403
+ ];
404
+
405
+ if (Array.isArray(provider?.subcommands?.alternate_test_prompts)) {
406
+ for (const altArgs of provider.subcommands.alternate_test_prompts) {
407
+ promptVariants.push(Array.isArray(altArgs) ? [...altArgs] : []);
230
408
  }
231
- // Check version
232
- let version;
409
+ }
410
+
411
+ if (providerId === 'codex_cli') {
412
+ const execArgs = promptVariants.find(args => args.includes('exec')) || promptVariants[0];
233
413
  try {
234
- const { stdout } = await execAsync(provider.versionCommand, { timeout: 10000 });
235
- version = stdout.trim();
414
+ const content = await this.executeCodexExec(provider.command, execArgs, prompt, timeoutMs);
415
+ return {
416
+ success: true,
417
+ content,
418
+ tokens_used: this.estimateTokens(prompt + content),
419
+ latency_ms: Date.now() - startTime,
420
+ provider: providerId,
421
+ mode: 'args',
422
+ timestamp: new Date()
423
+ };
424
+ } catch (error) {
425
+ return {
426
+ success: false,
427
+ error: `CLI execution failed: ${error instanceof Error ? error.message : String(error)}`,
428
+ latency_ms: Date.now() - startTime,
429
+ provider: providerId,
430
+ mode,
431
+ timestamp: new Date()
432
+ };
236
433
  }
237
- catch (error) {
434
+ }
435
+
436
+ let lastErrorMessage = null;
437
+
438
+ for (const promptArgs of promptVariants) {
439
+ const args = Array.isArray(promptArgs) ? [...promptArgs, prompt] : [prompt];
440
+ try {
441
+ const result = await this.executeCliCommand(
442
+ provider.command,
443
+ args,
444
+ 'args',
445
+ timeoutMs,
446
+ undefined
447
+ );
448
+
449
+ if (!result.error) {
450
+ const content = this.cleanCliResponse(result.stdout || '');
238
451
  return {
239
- available: false,
240
- authenticated: false,
241
- lastChecked: new Date(),
242
- path: executablePath,
243
- error: `Failed to get ${provider.name} version`
452
+ success: true,
453
+ content,
454
+ tokens_used: this.estimateTokens(prompt + content),
455
+ latency_ms: Date.now() - startTime,
456
+ provider: providerId,
457
+ mode: 'args',
458
+ timestamp: new Date()
244
459
  };
460
+ }
461
+
462
+ lastErrorMessage = result.error;
463
+ } catch (error) {
464
+ lastErrorMessage = error instanceof Error ? error.message : String(error);
245
465
  }
246
- // Check authentication
247
- let authenticated = false;
248
- try {
249
- const { stdout, stderr } = await execAsync(provider.authCheckCommand, { timeout: 10000 });
250
- // Look for success indicators in output
251
- const output = (stdout + stderr).toLowerCase();
252
- authenticated = output.includes('authenticated') ||
253
- output.includes('logged in') ||
254
- output.includes('valid') ||
255
- !output.includes('not authenticated');
256
- }
257
- catch (error) {
258
- // Some CLIs might not have auth status command, assume authenticated if version works
259
- authenticated = true;
260
- }
261
- // Detect available models for this CLI tool
262
- let default_model;
263
- let available_models;
264
- let model_detection_method;
265
- try {
266
- const modelDetection = await this.detectDefaultModel(provider.id);
267
- default_model = modelDetection.defaultModel;
268
- available_models = modelDetection.availableModels;
269
- model_detection_method = modelDetection.detectionMethod;
270
- }
271
- catch (error) {
272
- console.error(`[CLI Manager] Model detection failed for ${provider.name}:`, error);
273
- // Continue without model info - fallback will be used
274
- }
275
- return {
276
- available: true,
277
- authenticated,
278
- version,
279
- path: executablePath,
280
- lastChecked: new Date(),
281
- default_model,
282
- available_models,
283
- model_detection_method
284
- };
466
+ }
467
+
468
+ return {
469
+ success: false,
470
+ error: `CLI command failed: ${lastErrorMessage || 'Unknown error'}`,
471
+ latency_ms: Date.now() - startTime,
472
+ provider: providerId,
473
+ mode: 'args',
474
+ timestamp: new Date()
475
+ };
476
+
477
+ } catch (error) {
478
+ return {
479
+ success: false,
480
+ error: `CLI execution failed: ${error.message}`,
481
+ latency_ms: Date.now() - startTime,
482
+ provider: providerId,
483
+ mode,
484
+ timestamp: new Date()
485
+ };
285
486
  }
286
- /**
287
- * Send prompt via stdin mode
288
- */
289
- async sendPromptViaStdin(provider, prompt, timeoutMs) {
290
- return new Promise((resolve, reject) => {
291
- const child = (0, child_process_1.spawn)(provider.chatCommand, [], {
292
- stdio: ['pipe', 'pipe', 'pipe'],
293
- timeout: timeoutMs
294
- });
295
- let stdout = '';
296
- let stderr = '';
297
- child.stdout?.on('data', (data) => {
298
- stdout += data.toString();
299
- });
300
- child.stderr?.on('data', (data) => {
301
- stderr += data.toString();
302
- });
303
- child.on('close', (code) => {
304
- if (code === 0) {
305
- resolve(stdout.trim());
306
- }
307
- else {
308
- reject(new Error(`CLI exited with code ${code}: ${stderr}`));
309
- }
310
- });
311
- child.on('error', (error) => {
312
- reject(error);
313
- });
314
- // Send prompt via stdin
315
- if (child.stdin) {
316
- child.stdin.write(prompt);
317
- child.stdin.end();
318
- }
319
- });
487
+ }
488
+
489
+ async executeCliCommand(command, args, mode = 'args', timeoutMs = 30000, stdinInput) {
490
+ // Ensure timeoutMs is valid (not undefined, null, Infinity, or negative)
491
+ if (!timeoutMs || timeoutMs === Infinity || timeoutMs < 1 || timeoutMs > 300000) {
492
+ timeoutMs = 30000 // Default to 30 seconds
320
493
  }
321
- /**
322
- * Send prompt via command arguments
323
- */
324
- async sendPromptViaArgs(provider, prompt, timeoutMs) {
325
- const command = `${provider.chatCommand} "${prompt.replace(/"/g, '\\"')}"`;
326
- try {
327
- const { stdout } = await execAsync(command, { timeout: timeoutMs });
328
- return stdout.trim();
494
+
495
+ return new Promise((resolve, reject) => {
496
+ if (process.env.POLYDEV_CLI_DEBUG) {
497
+ console.log(`[CLI Debug] Executing: ${command} ${args.join(' ')} (mode: ${mode})`);
498
+ }
499
+
500
+ const child = spawn(command, args, {
501
+ stdio: ['pipe', 'pipe', 'pipe'],
502
+ shell: process.platform === 'win32',
503
+ timeout: timeoutMs
504
+ });
505
+
506
+ if (child.stdin) {
507
+ child.stdin.end();
508
+ }
509
+
510
+ let stdout = '';
511
+ let stderr = '';
512
+
513
+ child.stdout?.on('data', (data) => {
514
+ stdout += data.toString();
515
+ });
516
+
517
+ child.stderr?.on('data', (data) => {
518
+ stderr += data.toString();
519
+ });
520
+
521
+ if (mode === 'stdin' && stdinInput && child.stdin) {
522
+ child.stdin.write(`${stdinInput}\n`);
523
+ child.stdin.end();
524
+ }
525
+
526
+ child.on('close', (code) => {
527
+ if (process.env.POLYDEV_CLI_DEBUG) {
528
+ console.log(`[CLI Debug] Command finished with code ${code}`);
329
529
  }
330
- catch (error) {
331
- throw new Error(`CLI command failed: ${error}`);
530
+
531
+ if (code === 0) {
532
+ resolve({ stdout, stderr });
533
+ } else {
534
+ const trimmedStdErr = stderr.trim();
535
+ const trimmedStdOut = stdout.trim();
536
+ const errorMessage = trimmedStdErr || trimmedStdOut || `Command exited with code ${code}`;
537
+ resolve({
538
+ stdout,
539
+ stderr,
540
+ error: errorMessage,
541
+ exit_code: code
542
+ });
332
543
  }
333
- }
334
- /**
335
- * Update CLI status in database using MCP Supabase server
336
- * This integrates with existing MCP infrastructure
337
- */
338
- async updateCliStatusInDatabase(userId, providerId, status) {
339
- try {
340
- // Use existing CLI status API endpoint with MCP Supabase integration
341
- const statusUpdate = {
342
- server: this.getServerNameForProvider(providerId),
343
- tool: 'cli_detection',
344
- args: {
345
- provider: providerId,
346
- available: status.available,
347
- authenticated: status.authenticated,
348
- version: status.version,
349
- path: status.path,
350
- error: status.error
351
- }
352
- };
353
- // Call existing API endpoint that has MCP Supabase integration
354
- const response = await fetch('/api/cli-status', {
355
- method: 'POST',
356
- headers: {
357
- 'Content-Type': 'application/json',
358
- 'User-Agent': 'polydev-cli-manager/1.0.0'
359
- },
360
- body: JSON.stringify(statusUpdate)
361
- });
362
- if (!response.ok) {
363
- throw new Error(`Failed to update CLI status: ${response.status}`);
364
- }
365
- const result = await response.json();
366
- console.error(`[CLI Manager] Updated database via MCP Supabase for ${providerId}: ${status.available}`);
544
+ });
545
+
546
+ child.on('error', (error) => {
547
+ if (process.env.POLYDEV_CLI_DEBUG) {
548
+ console.log(`[CLI Debug] Command error:`, error);
367
549
  }
368
- catch (error) {
369
- console.error(`[CLI Manager] Failed to update database via MCP Supabase:`, error);
550
+ reject(error);
551
+ });
552
+
553
+ let timeoutId;
554
+ const cleanup = () => {
555
+ if (timeoutId) {
556
+ clearTimeout(timeoutId);
557
+ timeoutId = null;
370
558
  }
559
+ };
560
+
561
+ timeoutId = setTimeout(() => {
562
+ cleanup();
563
+ if (!child.killed) {
564
+ child.kill('SIGTERM');
565
+ // Force kill after 2 seconds if still running
566
+ setTimeout(() => {
567
+ if (!child.killed) {
568
+ child.kill('SIGKILL');
569
+ }
570
+ }, 2000);
571
+ }
572
+ reject(new Error(`Command timeout after ${timeoutMs}ms`));
573
+ }, timeoutMs);
574
+
575
+ child.on('close', () => {
576
+ cleanup();
577
+ });
578
+
579
+ child.on('exit', () => {
580
+ cleanup();
581
+ });
582
+ });
583
+ }
584
+
585
+ async executeCodexExec(executable, commandArgs, prompt, timeoutMs) {
586
+ if (!executable) {
587
+ throw new Error('Missing Codex executable');
371
588
  }
372
- /**
373
- * Map provider ID to server name for MCP integration
374
- */
375
- getServerNameForProvider(providerId) {
376
- const serverMap = {
377
- 'claude_code': 'claude-code-cli-bridge',
378
- 'codex_cli': 'cross-llm-bridge-test',
379
- 'gemini_cli': 'gemini-cli-bridge'
380
- };
381
- return serverMap[providerId] || 'unknown-cli-bridge';
382
- }
383
- /**
384
- * Get all CLI providers configuration
385
- */
386
- getProviders() {
387
- return Array.from(this.providers.values());
388
- }
389
- /**
390
- * Get provider by ID
391
- */
392
- getProvider(providerId) {
393
- return this.providers.get(providerId);
589
+
590
+ if (!commandArgs || commandArgs.length === 0) {
591
+ throw new Error('Invalid Codex command configuration');
394
592
  }
395
- /**
396
- * Detect available models for a CLI provider using interactive commands
397
- */
398
- async detectDefaultModel(providerId) {
399
- try {
400
- // Try interactive detection using CLI commands
401
- let command = '';
402
- switch (providerId) {
403
- case 'claude_code':
404
- command = 'models'; // Claude Code model listing command
405
- break;
406
- case 'codex_cli':
407
- command = 'list-models'; // Codex CLI model listing command
408
- break;
409
- case 'gemini_cli':
410
- command = 'models'; // Gemini CLI model listing command
411
- break;
412
- }
413
- if (!command) {
414
- throw new Error(`No model detection command for ${providerId}`);
415
- }
416
- const result = await this.sendCliPrompt(providerId, command, 'args', 10000);
417
- if (result.success && result.content) {
418
- const models = this.parseModelsFromOutput(providerId, result.content);
419
- if (models.length > 0) {
420
- return {
421
- defaultModel: this.extractDefaultModel(providerId, models),
422
- availableModels: models,
423
- detectionMethod: 'interactive'
424
- };
425
- }
426
- }
593
+
594
+ const workingDir = process.cwd();
595
+ const args = [
596
+ ...commandArgs,
597
+ '--sandbox',
598
+ 'workspace-write',
599
+ '--skip-git-repo-check',
600
+ '--cd',
601
+ workingDir,
602
+ prompt
603
+ ];
604
+
605
+ return new Promise((resolve, reject) => {
606
+ const baseTmp = process.env.POLYDEV_CLI_TMPDIR || process.env.TMPDIR || os.tmpdir();
607
+ const tmpDir = path.join(baseTmp, 'polydev-codex');
608
+ try {
609
+ fs.mkdirSync(tmpDir, { recursive: true });
610
+ } catch (error) {
611
+ console.warn('[CLI Debug] Failed to create Codex temp dir:', error);
612
+ }
613
+
614
+ const child = spawn(executable, args, {
615
+ stdio: ['pipe', 'pipe', 'pipe'],
616
+ shell: process.platform === 'win32',
617
+ env: {
618
+ ...process.env,
619
+ TMPDIR: tmpDir,
620
+ TEMP: tmpDir,
621
+ TMP: tmpDir
427
622
  }
428
- catch (error) {
429
- console.error(`Interactive model detection failed for ${providerId}:`, error);
623
+ });
624
+
625
+ console.log(`[CLI Debug] Spawning Codex process: ${executable} ${args.join(' ')}`);
626
+
627
+ if (child.stdin) {
628
+ child.stdin.end();
629
+ }
630
+
631
+ let stdout = '';
632
+ let stderr = '';
633
+ let resolved = false;
634
+
635
+ const stop = (handler) => {
636
+ if (!resolved) {
637
+ resolved = true;
638
+ try { child.kill('SIGTERM'); } catch (_) {}
639
+ handler();
430
640
  }
431
- // Fallback to known defaults if interactive detection fails
432
- return {
433
- defaultModel: this.getDefaultModelFallback(providerId),
434
- availableModels: [this.getDefaultModelFallback(providerId)],
435
- detectionMethod: 'fallback'
436
- };
437
- }
438
- /**
439
- * Parse model names from CLI output
440
- */
441
- parseModelsFromOutput(providerId, output) {
442
- const models = [];
443
- const lines = output.split('\n');
444
- switch (providerId) {
445
- case 'claude_code':
446
- // Parse Claude Code output format
447
- lines.forEach(line => {
448
- const matches = line.match(/claude-[\w\-.]+/gi);
449
- if (matches)
450
- models.push(...matches);
451
- });
452
- break;
453
- case 'codex_cli':
454
- // Parse Codex CLI output format
455
- lines.forEach(line => {
456
- const matches = line.match(/gpt-[\w\-.]+|o1-[\w\-.]+/gi);
457
- if (matches)
458
- models.push(...matches);
459
- });
460
- break;
461
- case 'gemini_cli':
462
- // Parse Gemini CLI output format
463
- lines.forEach(line => {
464
- const matches = line.match(/gemini-[\w\-.]+/gi);
465
- if (matches)
466
- models.push(...matches);
467
- });
468
- break;
641
+ };
642
+
643
+ const timeoutHandle = setTimeout(() => {
644
+ stop(() => reject(new Error(`Codex exec timeout after ${timeoutMs}ms`)));
645
+ }, timeoutMs);
646
+
647
+ const flushIfComplete = () => {
648
+ const match = stdout.match(/•\s*(.+)/);
649
+ if (match && match[1]) {
650
+ clearTimeout(timeoutHandle);
651
+ stop(() => resolve(match[1].trim()));
469
652
  }
470
- return [...new Set(models)]; // Remove duplicates
471
- }
472
- /**
473
- * Extract the default model from available models
474
- */
475
- extractDefaultModel(providerId, models) {
476
- if (models.length === 0)
477
- return this.getDefaultModelFallback(providerId);
478
- switch (providerId) {
479
- case 'claude_code':
480
- // Prefer Claude 3.5 Sonnet, then Claude 3 Sonnet
481
- return models.find(m => m.includes('claude-3-5-sonnet')) ||
482
- models.find(m => m.includes('claude-3-sonnet')) ||
483
- models[0];
484
- case 'codex_cli':
485
- // Prefer GPT-4, then GPT-3.5
486
- return models.find(m => m.includes('gpt-4')) || models[0];
487
- case 'gemini_cli':
488
- // Prefer Gemini Pro, then Gemini Flash
489
- return models.find(m => m.includes('gemini-1.5-pro')) ||
490
- models.find(m => m.includes('gemini-pro')) ||
491
- models[0];
653
+ };
654
+
655
+ child.stdout?.on('data', (data) => {
656
+ stdout += data.toString();
657
+ flushIfComplete();
658
+ });
659
+
660
+ child.stderr?.on('data', (data) => {
661
+ stderr += data.toString();
662
+ });
663
+
664
+ child.on('close', (code) => {
665
+ if (resolved) return;
666
+ resolved = true;
667
+ clearTimeout(timeoutHandle);
668
+
669
+ const trimmedStdout = stdout.trim();
670
+ const trimmedStderr = stderr.trim();
671
+
672
+ if (code === 0 && trimmedStdout) {
673
+ const match = trimmedStdout.match(/•\s*(.+)/);
674
+ if (match && match[1]) {
675
+ resolve(match[1].trim());
676
+ return;
677
+ }
678
+ resolve(trimmedStdout);
679
+ } else {
680
+ reject(new Error(trimmedStderr || trimmedStdout || `Codex exited with code ${code}`));
492
681
  }
493
- return models[0];
494
- }
495
- /**
496
- * Get fallback default model for a provider
497
- */
498
- getDefaultModelFallback(providerId) {
499
- const fallbacks = {
500
- 'claude_code': 'claude-3-sonnet',
501
- 'codex_cli': 'gpt-4',
502
- 'gemini_cli': 'gemini-pro'
503
- };
504
- return fallbacks[providerId] || 'unknown';
505
- }
682
+ });
683
+
684
+ child.on('error', (error) => {
685
+ if (resolved) return;
686
+ resolved = true;
687
+ clearTimeout(timeoutHandle);
688
+ reject(error);
689
+ });
690
+ });
691
+ }
692
+
693
+ cleanCliResponse(response) {
694
+ const cleanResponse = response.replace(/\x1b\[[0-9;]*m/g, '');
695
+
696
+ return cleanResponse
697
+ .replace(/^(\s*>\s*|\s*$)/gm, '')
698
+ .replace(/\n{3,}/g, '\n\n')
699
+ .trim();
700
+ }
701
+
702
+ estimateTokens(text) {
703
+ return Math.ceil(text.length / 4);
704
+ }
705
+
706
+ getAvailableProviders() {
707
+ return Array.from(this.providers.values());
708
+ }
709
+
710
+ getProvider(providerId) {
711
+ return this.providers.get(providerId);
712
+ }
506
713
  }
507
- exports.CLIManager = CLIManager;
508
- exports.default = CLIManager;
714
+
715
+ module.exports = { CLIManager, default: CLIManager };