polydev-ai 1.4.0 → 1.4.2

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.ts DELETED
@@ -1,755 +0,0 @@
1
- /**
2
- * CLI Manager with MCP Server Integration
3
- * Handles detection, authentication, and prompt sending for Claude Code, Codex CLI, and Gemini CLI
4
- * Uses MCP servers for database operations and status reporting
5
- */
6
-
7
- import { spawn, exec } from 'child_process'
8
- import { promisify } from 'util'
9
- import * as path from 'path'
10
- import * as fs from 'fs'
11
- import * as os from 'os'
12
- const which = require('which')
13
-
14
- const execAsync = promisify(exec)
15
-
16
- export interface CLIProvider {
17
- id: 'claude_code' | 'codex_cli' | 'gemini_cli'
18
- name: string
19
- executable: string
20
- versionCommand: string
21
- authCheckCommand: string
22
- chatCommand: string | string[]
23
- alternateChatCommands?: Array<string | string[]>
24
- supportsArgs: boolean
25
- installInstructions: string
26
- authInstructions: string
27
- }
28
-
29
- export interface CLIStatus {
30
- available: boolean
31
- authenticated: boolean
32
- version?: string
33
- path?: string
34
- lastChecked: Date
35
- error?: string
36
- default_model?: string
37
- available_models?: string[]
38
- model_detection_method?: 'interactive' | 'fallback'
39
- }
40
-
41
- export interface CLIResponse {
42
- success: boolean
43
- content?: string
44
- error?: string
45
- tokensUsed?: number
46
- latencyMs?: number
47
- mode?: 'stdin' | 'args'
48
- }
49
-
50
- export class CLIManager {
51
- private providers: Map<string, CLIProvider> = new Map()
52
- private statusCache: Map<string, CLIStatus> = new Map()
53
- private cacheTimeout = 5 * 60 * 1000 // 5 minutes
54
-
55
- constructor() {
56
- this.initializeProviders()
57
- }
58
-
59
- private initializeProviders() {
60
- const providers: CLIProvider[] = [
61
- {
62
- id: 'claude_code',
63
- name: 'Claude Code',
64
- executable: 'claude',
65
- versionCommand: 'claude --version',
66
- authCheckCommand: 'claude auth status',
67
- chatCommand: ['claude', 'chat'],
68
- supportsArgs: true,
69
- installInstructions: 'Install via: npm install -g @anthropic-ai/claude-code',
70
- authInstructions: 'Authenticate with: claude auth login'
71
- },
72
- {
73
- id: 'codex_cli',
74
- name: 'Codex CLI',
75
- executable: 'codex',
76
- versionCommand: 'codex --version',
77
- authCheckCommand: 'codex auth status',
78
- chatCommand: ['exec'],
79
- alternateChatCommands: [
80
- ['chat'],
81
- ['prompt'],
82
- ['ask']
83
- ],
84
- supportsArgs: true,
85
- installInstructions: 'Install Codex CLI from OpenAI',
86
- authInstructions: 'Authenticate with: codex auth'
87
- },
88
- {
89
- id: 'gemini_cli',
90
- name: 'Gemini CLI',
91
- executable: 'gemini',
92
- versionCommand: 'gemini --version',
93
- authCheckCommand: 'gemini auth status',
94
- chatCommand: ['gemini', 'chat'],
95
- supportsArgs: true,
96
- installInstructions: 'Install Gemini CLI from Google',
97
- authInstructions: 'Authenticate with: gemini auth login'
98
- }
99
- ]
100
-
101
- providers.forEach(provider => {
102
- this.providers.set(provider.id, provider)
103
- })
104
- }
105
-
106
- /**
107
- * Force CLI detection for all providers or specific provider
108
- * Updates status cache and reports to MCP server via Supabase
109
- */
110
- async forceCliDetection(userId?: string, providerId?: string): Promise<Record<string, CLIStatus>> {
111
- console.log(`[CLI Manager] Force detection started for ${providerId || 'all providers'}`)
112
- const results: Record<string, CLIStatus> = {}
113
-
114
- const providersToCheck = providerId ? [providerId] : Array.from(this.providers.keys())
115
-
116
- for (const id of providersToCheck) {
117
- const provider = this.providers.get(id)
118
- if (!provider) continue
119
-
120
- try {
121
- const status = await this.detectCLI(provider)
122
- this.statusCache.set(id, status)
123
- results[id] = status
124
-
125
- // Update database via MCP Supabase if userId provided
126
- if (userId) {
127
- await this.updateCliStatusInDatabase(userId, id, status)
128
- }
129
-
130
- console.log(`[CLI Manager] ${provider.name}: ${status.available ? 'Available' : 'Not Available'}`)
131
- } catch (error) {
132
- console.error(`[CLI Manager] Error detecting ${provider.name}:`, error)
133
- const errorStatus: CLIStatus = {
134
- available: false,
135
- authenticated: false,
136
- lastChecked: new Date(),
137
- error: error instanceof Error ? error.message : 'Unknown error'
138
- }
139
- this.statusCache.set(id, errorStatus)
140
- results[id] = errorStatus
141
- }
142
- }
143
-
144
- return results
145
- }
146
-
147
- /**
148
- * Get CLI status with cache support
149
- */
150
- async getCliStatus(providerId: string, userId?: string): Promise<CLIStatus> {
151
- const cached = this.statusCache.get(providerId)
152
- const now = new Date()
153
-
154
- // Return cached result if still valid
155
- if (cached && (now.getTime() - cached.lastChecked.getTime()) < this.cacheTimeout) {
156
- return cached
157
- }
158
-
159
- // Force detection if cache is stale or missing
160
- const results = await this.forceCliDetection(userId, providerId)
161
- return results[providerId] || {
162
- available: false,
163
- authenticated: false,
164
- lastChecked: now,
165
- error: 'Provider not found'
166
- }
167
- }
168
-
169
- /**
170
- * Send prompt to CLI provider
171
- */
172
- async sendCliPrompt(
173
- providerId: string,
174
- prompt: string,
175
- mode: 'stdin' | 'args' = 'args',
176
- timeoutMs: number = 30000
177
- ): Promise<CLIResponse> {
178
- const provider = this.providers.get(providerId)
179
- if (!provider) {
180
- return {
181
- success: false,
182
- error: `Unknown CLI provider: ${providerId}`
183
- }
184
- }
185
-
186
- // Check if CLI is available and authenticated
187
- const status = await this.getCliStatus(providerId)
188
- if (!status.available) {
189
- return {
190
- success: false,
191
- error: `${provider.name} is not available. ${provider.installInstructions}`
192
- }
193
- }
194
-
195
- if (!status.authenticated) {
196
- return {
197
- success: false,
198
- error: `${provider.name} is not authenticated. ${provider.authInstructions}`
199
- }
200
- }
201
-
202
- if (providerId === 'codex_cli' && timeoutMs < 90000) {
203
- timeoutMs = 90000
204
- }
205
-
206
- const startTime = Date.now()
207
-
208
- try {
209
- const commandVariants = [
210
- this.normalizeCommand(provider.chatCommand),
211
- ...(provider.alternateChatCommands || []).map(cmd => this.normalizeCommand(cmd))
212
- ].filter(parts => parts.length > 0)
213
-
214
- if (commandVariants.length === 0) {
215
- throw new Error(`${provider.name} does not have a valid chat command configured`)
216
- }
217
-
218
- if (provider.id === 'codex_cli') {
219
- const execArgs = commandVariants.find(parts => parts.includes('exec')) || commandVariants[0]
220
- const result = await this.executeCodexExec(provider.executable, execArgs, prompt, timeoutMs)
221
- return {
222
- success: true,
223
- content: result,
224
- latencyMs: Date.now() - startTime,
225
- mode: 'args'
226
- }
227
- }
228
-
229
- let result: string | undefined
230
- let effectiveMode: 'stdin' | 'args' = 'args'
231
- let lastError: Error | undefined
232
-
233
- for (const commandParts of commandVariants) {
234
- if (!provider.supportsArgs) {
235
- lastError = new Error(`${provider.name} does not support args mode`)
236
- continue
237
- }
238
-
239
- try {
240
- result = await this.sendPromptViaArgs(commandParts, prompt, timeoutMs)
241
- break
242
- } catch (error) {
243
- lastError = error instanceof Error ? error : new Error(String(error))
244
- }
245
- }
246
-
247
- if (typeof result !== 'string') {
248
- return {
249
- success: false,
250
- error: lastError?.message || 'CLI execution failed',
251
- latencyMs: Date.now() - startTime
252
- }
253
- }
254
-
255
- const latencyMs = Date.now() - startTime
256
-
257
- return {
258
- success: true,
259
- content: result,
260
- latencyMs,
261
- mode: effectiveMode
262
- }
263
-
264
- } catch (error) {
265
- return {
266
- success: false,
267
- error: error instanceof Error ? error.message : 'CLI execution failed',
268
- latencyMs: Date.now() - startTime
269
- }
270
- }
271
- }
272
-
273
- /**
274
- * Detect CLI installation and authentication
275
- */
276
- private async detectCLI(provider: CLIProvider): Promise<CLIStatus> {
277
- const customPath = process.env[`${provider.id.toUpperCase()}_PATH`]
278
- let executablePath: string
279
-
280
- try {
281
- // Try custom path first, then system PATH
282
- if (customPath && fs.existsSync(customPath)) {
283
- executablePath = customPath
284
- } else {
285
- executablePath = await which(provider.executable)
286
- }
287
- } catch (error) {
288
- return {
289
- available: false,
290
- authenticated: false,
291
- lastChecked: new Date(),
292
- error: `${provider.name} not found in PATH. ${provider.installInstructions}`
293
- }
294
- }
295
-
296
- // Check version
297
- let version: string
298
- try {
299
- const { stdout } = await execAsync(provider.versionCommand, { timeout: 10000 })
300
- version = stdout.trim()
301
- } catch (error) {
302
- return {
303
- available: false,
304
- authenticated: false,
305
- lastChecked: new Date(),
306
- path: executablePath,
307
- error: `Failed to get ${provider.name} version`
308
- }
309
- }
310
-
311
- // Check authentication
312
- let authenticated = false
313
- try {
314
- const { stdout, stderr } = await execAsync(provider.authCheckCommand, { timeout: 10000 })
315
- // Look for success indicators in output
316
- const output = (stdout + stderr).toLowerCase()
317
- authenticated = output.includes('authenticated') ||
318
- output.includes('logged in') ||
319
- output.includes('valid') ||
320
- !output.includes('not authenticated')
321
- } catch (error) {
322
- // Some CLIs might not have auth status command, assume authenticated if version works
323
- authenticated = true
324
- }
325
-
326
- // Detect available models for this CLI tool
327
- let default_model: string | undefined
328
- let available_models: string[] | undefined
329
- let model_detection_method: 'interactive' | 'fallback' | undefined
330
-
331
- try {
332
- const modelDetection = await this.detectDefaultModel(provider.id);
333
- default_model = modelDetection.defaultModel;
334
- available_models = modelDetection.availableModels;
335
- model_detection_method = modelDetection.detectionMethod;
336
- } catch (error) {
337
- console.error(`[CLI Manager] Model detection failed for ${provider.name}:`, error);
338
- // Continue without model info - fallback will be used
339
- }
340
-
341
- return {
342
- available: true,
343
- authenticated,
344
- version,
345
- path: executablePath,
346
- lastChecked: new Date(),
347
- default_model,
348
- available_models,
349
- model_detection_method
350
- }
351
- }
352
-
353
- private normalizeCommand(command: string | string[]): string[] {
354
- if (Array.isArray(command)) {
355
- return command.map(part => part.trim()).filter(Boolean)
356
- }
357
- return command.split(/\s+/).map(part => part.trim()).filter(Boolean)
358
- }
359
-
360
- private async sendPromptViaArgs(
361
- commandParts: string[],
362
- prompt: string,
363
- timeoutMs: number
364
- ): Promise<string> {
365
- if (commandParts.length === 0) {
366
- throw new Error('Invalid CLI command configuration')
367
- }
368
-
369
- const [executable, ...baseArgs] = commandParts
370
- const args = [...baseArgs, prompt]
371
-
372
- if (process.env.POLYDEV_CLI_DEBUG) {
373
- console.log(`[CLI Debug] Executing (args) ${executable} ${args.join(' ')}`)
374
- }
375
-
376
- return new Promise((resolve, reject) => {
377
- const baseTmp = process.env.POLYDEV_CLI_TMPDIR || process.env.TMPDIR || os.tmpdir()
378
- const tmpDir = path.join(baseTmp, 'polydev-codex')
379
- try {
380
- fs.mkdirSync(tmpDir, { recursive: true })
381
- } catch (error) {
382
- console.warn('[CLI Debug] Failed to create Codex temp dir:', error)
383
- }
384
-
385
- const child = spawn(executable, args, {
386
- stdio: ['pipe', 'pipe', 'pipe'],
387
- shell: process.platform === 'win32',
388
- env: {
389
- ...process.env,
390
- TMPDIR: tmpDir,
391
- TEMP: tmpDir,
392
- TMP: tmpDir
393
- }
394
- })
395
-
396
- console.log(`[CLI Debug] Spawning Codex process: ${executable} ${args.join(' ')}`)
397
-
398
- // No stdin needed for exec mode; close immediately to avoid hangs.
399
- if (child.stdin) {
400
- child.stdin.end()
401
- }
402
-
403
- let stdout = ''
404
- let stderr = ''
405
- let finished = false
406
-
407
- const timeoutHandle = setTimeout(() => {
408
- if (!finished) {
409
- finished = true
410
- try {
411
- child.kill('SIGTERM')
412
- setTimeout(() => {
413
- if (!child.killed) {
414
- child.kill('SIGKILL')
415
- }
416
- }, 1500)
417
- } catch {}
418
- reject(new Error(`CLI command timeout after ${timeoutMs}ms`))
419
- }
420
- }, timeoutMs)
421
-
422
- child.stdout?.on('data', data => {
423
- stdout += data.toString()
424
- })
425
-
426
- child.stderr?.on('data', data => {
427
- stderr += data.toString()
428
- })
429
-
430
- child.on('close', code => {
431
- if (finished) return
432
- finished = true
433
- clearTimeout(timeoutHandle)
434
-
435
- if (process.env.POLYDEV_CLI_DEBUG) {
436
- console.log(`[CLI Debug] (args) exit code ${code}`)
437
- if (stdout) console.log(`[CLI Debug] stdout: ${stdout.trim().slice(0, 500)}`)
438
- if (stderr) console.log(`[CLI Debug] stderr: ${stderr.trim().slice(0, 500)}`)
439
- }
440
-
441
- const trimmedStdout = stdout.trim()
442
- const trimmedStderr = stderr.trim()
443
-
444
- if (code === 0) {
445
- resolve(trimmedStdout || trimmedStderr)
446
- } else {
447
- const message = trimmedStderr || trimmedStdout || `CLI command failed (code ${code})`
448
- reject(new Error(message))
449
- }
450
- })
451
-
452
- child.on('error', error => {
453
- if (finished) return
454
- finished = true
455
- clearTimeout(timeoutHandle)
456
- reject(error)
457
- })
458
- })
459
- }
460
-
461
- private async executeCodexExec(
462
- executable: string,
463
- commandArgs: string[],
464
- prompt: string,
465
- timeoutMs: number
466
- ): Promise<string> {
467
- if (!executable) {
468
- throw new Error('Missing Codex executable')
469
- }
470
-
471
- if (!commandArgs || commandArgs.length === 0) {
472
- throw new Error('Invalid Codex command configuration')
473
- }
474
-
475
- const workingDir = process.cwd()
476
- const args = [
477
- ...commandArgs,
478
- '--sandbox',
479
- 'workspace-write',
480
- '--skip-git-repo-check',
481
- '--cd',
482
- workingDir,
483
- prompt
484
- ]
485
-
486
- return new Promise((resolve, reject) => {
487
- const child = spawn(executable, args, {
488
- stdio: ['pipe', 'pipe', 'pipe'],
489
- shell: process.platform === 'win32'
490
- })
491
-
492
- if (child.stdin) {
493
- child.stdin.end()
494
- }
495
-
496
- let stdout = ''
497
- let stderr = ''
498
- let resolved = false
499
-
500
- const stop = (handler: () => void) => {
501
- if (!resolved) {
502
- resolved = true
503
- try {
504
- child.kill('SIGTERM')
505
- } catch {}
506
- handler()
507
- }
508
- }
509
-
510
- const timeoutHandle = setTimeout(() => {
511
- stop(() => reject(new Error(`Codex exec timeout after ${timeoutMs}ms`)))
512
- }, timeoutMs)
513
-
514
- const flushIfComplete = () => {
515
- const bulletMatch = stdout.match(/•\s*(.+)/)
516
- if (bulletMatch && bulletMatch[1]) {
517
- const answer = bulletMatch[1].trim()
518
- clearTimeout(timeoutHandle)
519
- stop(() => resolve(answer))
520
- }
521
- }
522
-
523
- child.stdout?.on('data', data => {
524
- stdout += data.toString()
525
- flushIfComplete()
526
- })
527
-
528
- child.stderr?.on('data', data => {
529
- stderr += data.toString()
530
- })
531
-
532
- child.on('close', code => {
533
- if (resolved) return
534
- resolved = true
535
- clearTimeout(timeoutHandle)
536
-
537
- const trimmedStdout = stdout.trim()
538
- const trimmedStderr = stderr.trim()
539
-
540
- if (code === 0 && trimmedStdout) {
541
- const bulletMatch = trimmedStdout.match(/•\s*(.+)/)
542
- if (bulletMatch && bulletMatch[1]) {
543
- resolve(bulletMatch[1].trim())
544
- return
545
- }
546
- resolve(trimmedStdout)
547
- } else {
548
- reject(new Error(trimmedStderr || trimmedStdout || `Codex exited with code ${code}`))
549
- }
550
- })
551
-
552
- child.on('error', error => {
553
- if (resolved) return
554
- resolved = true
555
- clearTimeout(timeoutHandle)
556
- reject(error)
557
- })
558
- })
559
- }
560
-
561
- /**
562
- * Update CLI status in database using MCP Supabase server
563
- * This integrates with existing MCP infrastructure
564
- */
565
- private async updateCliStatusInDatabase(
566
- userId: string,
567
- providerId: string,
568
- status: CLIStatus
569
- ): Promise<void> {
570
- try {
571
- // Use existing CLI status API endpoint with MCP Supabase integration
572
- const statusUpdate = {
573
- server: this.getServerNameForProvider(providerId),
574
- tool: 'cli_detection',
575
- args: {
576
- provider: providerId,
577
- available: status.available,
578
- authenticated: status.authenticated,
579
- version: status.version,
580
- path: status.path,
581
- error: status.error
582
- }
583
- }
584
-
585
- // Call existing API endpoint that has MCP Supabase integration
586
- const response = await fetch('/api/cli-status', {
587
- method: 'POST',
588
- headers: {
589
- 'Content-Type': 'application/json',
590
- 'User-Agent': 'polydev-cli-manager/1.0.0'
591
- },
592
- body: JSON.stringify(statusUpdate)
593
- })
594
-
595
- if (!response.ok) {
596
- throw new Error(`Failed to update CLI status: ${response.status}`)
597
- }
598
-
599
- const result = await response.json()
600
- console.log(`[CLI Manager] Updated database via MCP Supabase for ${providerId}: ${status.available}`)
601
-
602
- } catch (error) {
603
- console.error(`[CLI Manager] Failed to update database via MCP Supabase:`, error)
604
- }
605
- }
606
-
607
- /**
608
- * Map provider ID to server name for MCP integration
609
- */
610
- private getServerNameForProvider(providerId: string): string {
611
- const serverMap = {
612
- 'claude_code': 'claude-code-cli-bridge',
613
- 'codex_cli': 'cross-llm-bridge-test',
614
- 'gemini_cli': 'gemini-cli-bridge'
615
- }
616
- return serverMap[providerId as keyof typeof serverMap] || 'unknown-cli-bridge'
617
- }
618
-
619
- /**
620
- * Get all CLI providers configuration
621
- */
622
- getProviders(): CLIProvider[] {
623
- return Array.from(this.providers.values())
624
- }
625
-
626
- /**
627
- * Get provider by ID
628
- */
629
- getProvider(providerId: string): CLIProvider | undefined {
630
- return this.providers.get(providerId)
631
- }
632
-
633
- /**
634
- * Detect available models for a CLI provider using interactive commands
635
- */
636
- async detectDefaultModel(providerId: string): Promise<{
637
- defaultModel: string;
638
- availableModels: string[];
639
- detectionMethod: 'interactive' | 'fallback';
640
- }> {
641
- try {
642
- // Try interactive detection using CLI commands
643
- let command = '';
644
- switch (providerId) {
645
- case 'claude_code':
646
- command = 'models'; // Claude Code model listing command
647
- break;
648
- case 'codex_cli':
649
- command = 'list-models'; // Codex CLI model listing command
650
- break;
651
- case 'gemini_cli':
652
- command = 'models'; // Gemini CLI model listing command
653
- break;
654
- }
655
-
656
- if (!command) {
657
- throw new Error(`No model detection command for ${providerId}`);
658
- }
659
-
660
- const result = await this.sendCliPrompt(providerId, command, 'args', 10000);
661
-
662
- if (result.success && result.content) {
663
- const models = this.parseModelsFromOutput(providerId, result.content);
664
- if (models.length > 0) {
665
- return {
666
- defaultModel: this.extractDefaultModel(providerId, models),
667
- availableModels: models,
668
- detectionMethod: 'interactive'
669
- };
670
- }
671
- }
672
- } catch (error) {
673
- console.error(`Interactive model detection failed for ${providerId}:`, error);
674
- }
675
-
676
- // Fallback to known defaults if interactive detection fails
677
- return {
678
- defaultModel: this.getDefaultModelFallback(providerId),
679
- availableModels: [this.getDefaultModelFallback(providerId)],
680
- detectionMethod: 'fallback'
681
- };
682
- }
683
-
684
- /**
685
- * Parse model names from CLI output
686
- */
687
- private parseModelsFromOutput(providerId: string, output: string): string[] {
688
- const models: string[] = [];
689
- const lines = output.split('\n');
690
-
691
- switch (providerId) {
692
- case 'claude_code':
693
- // Parse Claude Code output format
694
- lines.forEach(line => {
695
- const matches = line.match(/claude-[\w\-.]+/gi);
696
- if (matches) models.push(...matches);
697
- });
698
- break;
699
- case 'codex_cli':
700
- // Parse Codex CLI output format
701
- lines.forEach(line => {
702
- const matches = line.match(/gpt-[\w\-.]+|o1-[\w\-.]+/gi);
703
- if (matches) models.push(...matches);
704
- });
705
- break;
706
- case 'gemini_cli':
707
- // Parse Gemini CLI output format
708
- lines.forEach(line => {
709
- const matches = line.match(/gemini-[\w\-.]+/gi);
710
- if (matches) models.push(...matches);
711
- });
712
- break;
713
- }
714
-
715
- return [...new Set(models)]; // Remove duplicates
716
- }
717
-
718
- /**
719
- * Extract the default model from available models
720
- */
721
- private extractDefaultModel(providerId: string, models: string[]): string {
722
- if (models.length === 0) return this.getDefaultModelFallback(providerId);
723
-
724
- switch (providerId) {
725
- case 'claude_code':
726
- // Prefer Claude 3.5 Sonnet, then Claude 3 Sonnet
727
- return models.find(m => m.includes('claude-3-5-sonnet')) ||
728
- models.find(m => m.includes('claude-3-sonnet')) ||
729
- models[0];
730
- case 'codex_cli':
731
- // Prefer GPT-4, then GPT-3.5
732
- return models.find(m => m.includes('gpt-4')) || models[0];
733
- case 'gemini_cli':
734
- // Prefer Gemini Pro, then Gemini Flash
735
- return models.find(m => m.includes('gemini-1.5-pro')) ||
736
- models.find(m => m.includes('gemini-pro')) ||
737
- models[0];
738
- }
739
- return models[0];
740
- }
741
-
742
- /**
743
- * Get fallback default model for a provider
744
- */
745
- private getDefaultModelFallback(providerId: string): string {
746
- const fallbacks = {
747
- 'claude_code': 'claude-3-sonnet',
748
- 'codex_cli': 'gpt-4',
749
- 'gemini_cli': 'gemini-pro'
750
- };
751
- return fallbacks[providerId as keyof typeof fallbacks] || 'unknown';
752
- }
753
- }
754
-
755
- export default CLIManager