polydev-ai 1.0.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.
@@ -0,0 +1,541 @@
1
+ const { exec, spawn } = require('child_process');
2
+ const { promisify } = require('util');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const which = require('which');
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ class CLIManager {
10
+ constructor() {
11
+ this.providers = new Map();
12
+ this.statusCache = new Map();
13
+ this.CACHE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
14
+ this.initializeProviders();
15
+ }
16
+
17
+ initializeProviders() {
18
+ const providers = [
19
+ {
20
+ id: 'claude_code',
21
+ name: 'Claude Code',
22
+ command: process.env.CLAUDE_CODE_PATH || 'claude',
23
+ subcommands: {
24
+ chat: [],
25
+ version: ['--version'],
26
+ auth_status: ['--print', 'test auth'], // Use --print to test auth
27
+ test_prompt: ['--print']
28
+ },
29
+ install_instructions: 'Install via: npm install -g @anthropic-ai/claude-code',
30
+ auth_instructions: 'Authenticate with Claude Code'
31
+ },
32
+ {
33
+ id: 'codex_cli',
34
+ name: 'Codex CLI',
35
+ command: process.env.CODEX_CLI_PATH || 'codex',
36
+ subcommands: {
37
+ chat: [],
38
+ version: ['--version'],
39
+ auth_status: ['login', 'status'], // Correct command: codex login status
40
+ test_prompt: ['exec']
41
+ },
42
+ install_instructions: 'Install Codex CLI from OpenAI',
43
+ auth_instructions: 'Authenticate with: codex login'
44
+ },
45
+ {
46
+ id: 'gemini_cli',
47
+ name: 'Gemini CLI',
48
+ command: process.env.GEMINI_CLI_PATH || 'gemini',
49
+ subcommands: {
50
+ chat: ['chat'],
51
+ version: ['--version'],
52
+ auth_status: ['auth-status'] // gemini-mcp auth-status command
53
+ },
54
+ install_instructions: 'Install Gemini CLI from Google',
55
+ auth_instructions: 'Authenticate with: gemini (then /auth login)'
56
+ }
57
+ ];
58
+
59
+ providers.forEach(provider => {
60
+ this.providers.set(provider.id, provider);
61
+ });
62
+ }
63
+
64
+ async forceCliDetection(specificProvider) {
65
+ const results = {};
66
+ const providersToCheck = specificProvider
67
+ ? [this.providers.get(specificProvider)].filter(Boolean)
68
+ : Array.from(this.providers.values());
69
+
70
+ // Log system environment for debugging
71
+ console.log(`[Polydev CLI] Detecting CLI providers - Node.js ${process.version}, Platform: ${process.platform}`);
72
+
73
+ for (const provider of providersToCheck) {
74
+ if (provider) {
75
+ try {
76
+ results[provider.id] = await this.detectCliProvider(provider);
77
+ this.statusCache.set(provider.id, results[provider.id]);
78
+
79
+ // Log compatibility issues for user awareness
80
+ if (results[provider.id].error && results[provider.id].error.includes('Compatibility Issue')) {
81
+ console.warn(`[Polydev CLI] ⚠️ ${provider.name} compatibility issue detected. See error details for solutions.`);
82
+ }
83
+ } catch (error) {
84
+ results[provider.id] = {
85
+ available: false,
86
+ authenticated: false,
87
+ error: `Detection failed: ${error.message}`,
88
+ last_checked: new Date()
89
+ };
90
+ }
91
+ }
92
+ }
93
+
94
+ return results;
95
+ }
96
+
97
+ async getCliStatus(specificProvider) {
98
+ const results = {};
99
+ const providersToCheck = specificProvider
100
+ ? [this.providers.get(specificProvider)].filter(Boolean)
101
+ : Array.from(this.providers.values());
102
+
103
+ for (const provider of providersToCheck) {
104
+ if (provider) {
105
+ const cached = this.statusCache.get(provider.id);
106
+ if (cached && this.isCacheValid(cached)) {
107
+ results[provider.id] = cached;
108
+ } else {
109
+ const detection = await this.forceCliDetection(provider.id);
110
+ results[provider.id] = detection[provider.id];
111
+ }
112
+ }
113
+ }
114
+
115
+ return results;
116
+ }
117
+
118
+ isCacheValid(status) {
119
+ if (!status.last_checked) return false;
120
+ const now = new Date().getTime();
121
+ const checked = new Date(status.last_checked).getTime();
122
+ return (now - checked) < this.CACHE_TIMEOUT_MS;
123
+ }
124
+
125
+ async detectCliProvider(provider) {
126
+ try {
127
+ const cliPath = await this.findCliPath(provider.command);
128
+ if (!cliPath) {
129
+ return {
130
+ available: false,
131
+ authenticated: false,
132
+ error: `${provider.name} not found in PATH. ${provider.install_instructions}`,
133
+ last_checked: new Date()
134
+ };
135
+ }
136
+
137
+ let version;
138
+ try {
139
+ const versionResult = await this.executeCliCommand(
140
+ provider.command,
141
+ provider.subcommands.version,
142
+ 'args',
143
+ 5000
144
+ );
145
+ version = versionResult.stdout?.trim();
146
+ } catch (versionError) {
147
+ if (process.env.POLYDEV_CLI_DEBUG) {
148
+ console.log(`[CLI Debug] Version check failed for ${provider.id}:`, versionError);
149
+ }
150
+ }
151
+
152
+ let authenticated = false;
153
+
154
+ // For Claude Code, skip command-based auth check and use file-based detection directly
155
+ // This avoids the recursion issue when running from within Claude Code
156
+ if (provider.id === 'claude_code') {
157
+ authenticated = await this.checkAuthenticationByFiles(provider.id);
158
+
159
+ if (process.env.POLYDEV_CLI_DEBUG) {
160
+ console.log(`[CLI Debug] File-based auth check for ${provider.id}:`, authenticated);
161
+ }
162
+ } else {
163
+ // For other providers, try command-based auth check first
164
+ try {
165
+ const authResult = await this.executeCliCommand(
166
+ provider.command,
167
+ provider.subcommands.auth_status,
168
+ 'args',
169
+ 5000 // Reduced timeout to 5 seconds
170
+ );
171
+
172
+ // If command succeeds, check output for authentication indicators
173
+ const authOutput = (authResult.stdout + ' ' + authResult.stderr).toLowerCase();
174
+
175
+ if (process.env.POLYDEV_CLI_DEBUG) {
176
+ console.log(`[CLI Debug] Auth output for ${provider.id}: "${authOutput}"`);
177
+ }
178
+
179
+ authenticated = this.parseAuthenticationStatus(provider.id, authOutput);
180
+
181
+ } catch (authError) {
182
+ if (process.env.POLYDEV_CLI_DEBUG) {
183
+ console.log(`[CLI Debug] Auth check failed for ${provider.id}:`, authError);
184
+ }
185
+
186
+ // Fallback to file-based authentication detection
187
+ authenticated = await this.checkAuthenticationByFiles(provider.id);
188
+
189
+ if (process.env.POLYDEV_CLI_DEBUG) {
190
+ console.log(`[CLI Debug] File-based auth check for ${provider.id}:`, authenticated);
191
+ }
192
+ }
193
+ }
194
+
195
+ // Special handling for Gemini CLI Node.js compatibility issues
196
+ let errorMessage = undefined;
197
+ if (!authenticated) {
198
+ if (provider.id === 'gemini_cli') {
199
+ // Check if the issue is Node.js compatibility
200
+ try {
201
+ const authResult = await this.executeCliCommand(
202
+ provider.command,
203
+ ['--help'],
204
+ 'args',
205
+ 2000
206
+ );
207
+ const testOutput = (authResult.stdout + ' ' + authResult.stderr).toLowerCase();
208
+ if (testOutput.includes('referenceerror: file is not defined') ||
209
+ testOutput.includes('undici/lib/web/webidl')) {
210
+ errorMessage = `⚠️ Gemini CLI Compatibility Issue: Node.js v${process.version} doesn't support the 'File' global that Gemini CLI requires.
211
+
212
+ Solutions:
213
+ • Update to Node.js v20+ (recommended): nvm install 20 && nvm use 20
214
+ • Reinstall Gemini CLI: npm uninstall -g @google/gemini-cli && npm install -g @google/gemini-cli@latest
215
+ • Alternative: Use Google AI Studio directly or switch to Claude/OpenAI providers
216
+
217
+ This is a known issue with @google/gemini-cli@0.3.4 and older Node.js versions.`;
218
+ } else {
219
+ errorMessage = `Not authenticated. ${provider.auth_instructions}`;
220
+ }
221
+ } catch {
222
+ errorMessage = `Not authenticated. ${provider.auth_instructions}`;
223
+ }
224
+ } else {
225
+ errorMessage = `Not authenticated. ${provider.auth_instructions}`;
226
+ }
227
+ }
228
+
229
+ return {
230
+ available: true,
231
+ authenticated,
232
+ version,
233
+ path: cliPath,
234
+ last_checked: new Date(),
235
+ error: errorMessage
236
+ };
237
+
238
+ } catch (error) {
239
+ return {
240
+ available: false,
241
+ authenticated: false,
242
+ error: `Detection failed: ${error.message}`,
243
+ last_checked: new Date()
244
+ };
245
+ }
246
+ }
247
+
248
+ async findCliPath(command) {
249
+ try {
250
+ return await which(command);
251
+ } catch (error) {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ async checkAuthenticationByFiles(providerId) {
257
+ const os = require('os');
258
+
259
+ try {
260
+ switch (providerId) {
261
+ case 'claude_code':
262
+ // Check for Claude Code session files
263
+ const claudeConfigPath = path.join(os.homedir(), '.claude.json');
264
+ if (fs.existsSync(claudeConfigPath)) {
265
+ const configContent = fs.readFileSync(claudeConfigPath, 'utf8');
266
+ // Look for session or auth tokens in the config
267
+ return configContent.length > 100 &&
268
+ (configContent.includes('session') ||
269
+ configContent.includes('token') ||
270
+ configContent.includes('auth'));
271
+ }
272
+ return false;
273
+
274
+ case 'codex_cli':
275
+ // Check for Codex auth files
276
+ const codexAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
277
+ if (fs.existsSync(codexAuthPath)) {
278
+ const authContent = fs.readFileSync(codexAuthPath, 'utf8');
279
+ try {
280
+ const authData = JSON.parse(authContent);
281
+ return authData && (authData.token || authData.access_token || authData.authenticated);
282
+ } catch {
283
+ return authContent.length > 10; // Has some auth content
284
+ }
285
+ }
286
+ return false;
287
+
288
+ case 'gemini_cli':
289
+ // Check for Gemini CLI auth files (if any)
290
+ const geminiConfigPath = path.join(os.homedir(), '.config', 'gemini-cli', 'config.json');
291
+ if (fs.existsSync(geminiConfigPath)) {
292
+ const configContent = fs.readFileSync(geminiConfigPath, 'utf8');
293
+ return configContent.includes('auth') || configContent.includes('token');
294
+ }
295
+ return false;
296
+
297
+ default:
298
+ return false;
299
+ }
300
+ } catch (error) {
301
+ if (process.env.POLYDEV_CLI_DEBUG) {
302
+ console.log(`[CLI Debug] File-based auth check error for ${providerId}:`, error.message);
303
+ }
304
+ return false;
305
+ }
306
+ }
307
+
308
+ parseAuthenticationStatus(providerId, authOutput) {
309
+
310
+ switch (providerId) {
311
+ case 'claude_code':
312
+ // If --print "test auth" works without error, Claude Code is authenticated
313
+ // Look for actual response content (not authentication errors)
314
+ const claudeAuth = !authOutput.includes('not authenticated') &&
315
+ !authOutput.includes('please log in') &&
316
+ !authOutput.includes('authentication required') &&
317
+ !authOutput.includes('login required') &&
318
+ authOutput.length > 10; // Has actual content response
319
+
320
+ return claudeAuth;
321
+
322
+ case 'codex_cli':
323
+ // Look for specific codex login status responses
324
+ const hasLoggedIn = authOutput.includes('logged in using');
325
+ const hasAuthenticated = authOutput.includes('authenticated');
326
+ const hasChatGpt = authOutput.includes('chatgpt') && !authOutput.includes('not logged in');
327
+
328
+
329
+ return hasLoggedIn || hasAuthenticated || hasChatGpt;
330
+
331
+ case 'gemini_cli':
332
+ // Check for Node.js compatibility issues first
333
+ if (authOutput.includes('referenceerror: file is not defined') ||
334
+ authOutput.includes('undici/lib/web/webidl') ||
335
+ authOutput.includes('file is not defined')) {
336
+ return false; // CLI is broken due to Node.js compatibility
337
+ }
338
+
339
+ return !authOutput.includes('not authenticated') &&
340
+ !authOutput.includes('please login') &&
341
+ (authOutput.includes('authenticated') || authOutput.includes('logged in'));
342
+
343
+ default:
344
+ return authOutput.includes('authenticated') || authOutput.includes('logged in');
345
+ }
346
+ }
347
+
348
+ async sendCliPrompt(providerId, prompt, mode = 'args', timeoutMs = null) {
349
+ // Set provider-specific default timeouts
350
+ if (timeoutMs === null) {
351
+ timeoutMs = providerId === 'claude_code' ? 60000 : 30000; // 60s for Claude Code, 30s for others
352
+ }
353
+ const startTime = Date.now();
354
+
355
+ try {
356
+ const provider = this.providers.get(providerId);
357
+ if (!provider) {
358
+ return {
359
+ success: false,
360
+ error: `Unknown provider: ${providerId}`,
361
+ latency_ms: Date.now() - startTime,
362
+ timestamp: new Date()
363
+ };
364
+ }
365
+
366
+ const status = await this.getCliStatus(providerId);
367
+ const providerStatus = status[providerId];
368
+
369
+ if (!providerStatus?.available) {
370
+ return {
371
+ success: false,
372
+ error: `${provider.name} is not available. ${provider.install_instructions}`,
373
+ latency_ms: Date.now() - startTime,
374
+ timestamp: new Date()
375
+ };
376
+ }
377
+
378
+ if (!providerStatus.authenticated) {
379
+ return {
380
+ success: false,
381
+ error: `${provider.name} is not authenticated. ${provider.auth_instructions}`,
382
+ latency_ms: Date.now() - startTime,
383
+ timestamp: new Date()
384
+ };
385
+ }
386
+
387
+ const args = [...provider.subcommands.test_prompt];
388
+ if (mode === 'args') {
389
+ args.push(prompt);
390
+ }
391
+
392
+ const result = await this.executeCliCommand(
393
+ provider.command,
394
+ args,
395
+ mode,
396
+ timeoutMs,
397
+ mode === 'stdin' ? prompt : undefined
398
+ );
399
+
400
+ if (result.error) {
401
+ return {
402
+ success: false,
403
+ error: `CLI command failed: ${result.error}`,
404
+ latency_ms: Date.now() - startTime,
405
+ provider: providerId,
406
+ mode,
407
+ timestamp: new Date()
408
+ };
409
+ }
410
+
411
+ const content = this.cleanCliResponse(result.stdout || '');
412
+ const tokens_used = this.estimateTokens(prompt + content);
413
+
414
+ return {
415
+ success: true,
416
+ content,
417
+ tokens_used,
418
+ latency_ms: Date.now() - startTime,
419
+ provider: providerId,
420
+ mode,
421
+ timestamp: new Date()
422
+ };
423
+
424
+ } catch (error) {
425
+ return {
426
+ success: false,
427
+ error: `CLI execution failed: ${error.message}`,
428
+ latency_ms: Date.now() - startTime,
429
+ provider: providerId,
430
+ mode,
431
+ timestamp: new Date()
432
+ };
433
+ }
434
+ }
435
+
436
+ async executeCliCommand(command, args, mode = 'args', timeoutMs = 30000, stdinInput) {
437
+ return new Promise((resolve, reject) => {
438
+ if (process.env.POLYDEV_CLI_DEBUG) {
439
+ console.log(`[CLI Debug] Executing: ${command} ${args.join(' ')} (mode: ${mode})`);
440
+ }
441
+
442
+ const child = spawn(command, args, {
443
+ stdio: mode === 'stdin' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
444
+ shell: process.platform === 'win32',
445
+ timeout: timeoutMs
446
+ });
447
+
448
+ let stdout = '';
449
+ let stderr = '';
450
+
451
+ child.stdout?.on('data', (data) => {
452
+ stdout += data.toString();
453
+ });
454
+
455
+ child.stderr?.on('data', (data) => {
456
+ stderr += data.toString();
457
+ });
458
+
459
+ if (mode === 'stdin' && stdinInput && child.stdin) {
460
+ child.stdin.write(stdinInput);
461
+ child.stdin.end();
462
+ }
463
+
464
+ child.on('close', (code) => {
465
+ if (process.env.POLYDEV_CLI_DEBUG) {
466
+ console.log(`[CLI Debug] Command finished with code ${code}`);
467
+ }
468
+
469
+ if (code === 0) {
470
+ resolve({ stdout, stderr });
471
+ } else {
472
+ resolve({
473
+ stdout,
474
+ stderr,
475
+ error: `Command exited with code ${code}`
476
+ });
477
+ }
478
+ });
479
+
480
+ child.on('error', (error) => {
481
+ if (process.env.POLYDEV_CLI_DEBUG) {
482
+ console.log(`[CLI Debug] Command error:`, error);
483
+ }
484
+ reject(error);
485
+ });
486
+
487
+ let timeoutId;
488
+ const cleanup = () => {
489
+ if (timeoutId) {
490
+ clearTimeout(timeoutId);
491
+ timeoutId = null;
492
+ }
493
+ };
494
+
495
+ timeoutId = setTimeout(() => {
496
+ cleanup();
497
+ if (!child.killed) {
498
+ child.kill('SIGTERM');
499
+ // Force kill after 2 seconds if still running
500
+ setTimeout(() => {
501
+ if (!child.killed) {
502
+ child.kill('SIGKILL');
503
+ }
504
+ }, 2000);
505
+ }
506
+ reject(new Error(`Command timeout after ${timeoutMs}ms`));
507
+ }, timeoutMs);
508
+
509
+ child.on('close', () => {
510
+ cleanup();
511
+ });
512
+
513
+ child.on('exit', () => {
514
+ cleanup();
515
+ });
516
+ });
517
+ }
518
+
519
+ cleanCliResponse(response) {
520
+ const cleanResponse = response.replace(/\x1b\[[0-9;]*m/g, '');
521
+
522
+ return cleanResponse
523
+ .replace(/^(\s*>\s*|\s*$)/gm, '')
524
+ .replace(/\n{3,}/g, '\n\n')
525
+ .trim();
526
+ }
527
+
528
+ estimateTokens(text) {
529
+ return Math.ceil(text.length / 4);
530
+ }
531
+
532
+ getAvailableProviders() {
533
+ return Array.from(this.providers.values());
534
+ }
535
+
536
+ getProvider(providerId) {
537
+ return this.providers.get(providerId);
538
+ }
539
+ }
540
+
541
+ module.exports = { CLIManager, default: CLIManager };