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,428 @@
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
+ const shell = require('shelljs')
14
+
15
+ const execAsync = promisify(exec)
16
+
17
+ export interface CLIProvider {
18
+ id: 'claude_code' | 'codex_cli' | 'gemini_cli'
19
+ name: string
20
+ executable: string
21
+ versionCommand: string
22
+ authCheckCommand: string
23
+ chatCommand: string
24
+ supportsStdin: boolean
25
+ supportsArgs: boolean
26
+ installInstructions: string
27
+ authInstructions: string
28
+ }
29
+
30
+ export interface CLIStatus {
31
+ available: boolean
32
+ authenticated: boolean
33
+ version?: string
34
+ path?: string
35
+ lastChecked: Date
36
+ error?: string
37
+ }
38
+
39
+ export interface CLIResponse {
40
+ success: boolean
41
+ content?: string
42
+ error?: string
43
+ tokensUsed?: number
44
+ latencyMs?: number
45
+ }
46
+
47
+ export class CLIManager {
48
+ private providers: Map<string, CLIProvider> = new Map()
49
+ private statusCache: Map<string, CLIStatus> = new Map()
50
+ private cacheTimeout = 5 * 60 * 1000 // 5 minutes
51
+
52
+ constructor() {
53
+ this.initializeProviders()
54
+ }
55
+
56
+ private initializeProviders() {
57
+ const providers: CLIProvider[] = [
58
+ {
59
+ id: 'claude_code',
60
+ name: 'Claude Code',
61
+ executable: 'claude',
62
+ versionCommand: 'claude --version',
63
+ authCheckCommand: 'claude auth status',
64
+ chatCommand: 'claude chat',
65
+ supportsStdin: true,
66
+ supportsArgs: true,
67
+ installInstructions: 'Install via: npm install -g @anthropic-ai/claude-code',
68
+ authInstructions: 'Authenticate with: claude auth login'
69
+ },
70
+ {
71
+ id: 'codex_cli',
72
+ name: 'Codex CLI',
73
+ executable: 'codex',
74
+ versionCommand: 'codex --version',
75
+ authCheckCommand: 'codex auth status',
76
+ chatCommand: 'codex chat',
77
+ supportsStdin: true,
78
+ supportsArgs: true,
79
+ installInstructions: 'Install Codex CLI from OpenAI',
80
+ authInstructions: 'Authenticate with: codex auth'
81
+ },
82
+ {
83
+ id: 'gemini_cli',
84
+ name: 'Gemini CLI',
85
+ executable: 'gemini',
86
+ versionCommand: 'gemini --version',
87
+ authCheckCommand: 'gemini auth status',
88
+ chatCommand: 'gemini chat',
89
+ supportsStdin: true,
90
+ supportsArgs: true,
91
+ installInstructions: 'Install Gemini CLI from Google',
92
+ authInstructions: 'Authenticate with: gemini auth login'
93
+ }
94
+ ]
95
+
96
+ providers.forEach(provider => {
97
+ this.providers.set(provider.id, provider)
98
+ })
99
+ }
100
+
101
+ /**
102
+ * Force CLI detection for all providers or specific provider
103
+ * Updates status cache and reports to MCP server via Supabase
104
+ */
105
+ async forceCliDetection(userId?: string, providerId?: string): Promise<Record<string, CLIStatus>> {
106
+ console.log(`[CLI Manager] Force detection started for ${providerId || 'all providers'}`)
107
+ const results: Record<string, CLIStatus> = {}
108
+
109
+ const providersToCheck = providerId ? [providerId] : Array.from(this.providers.keys())
110
+
111
+ for (const id of providersToCheck) {
112
+ const provider = this.providers.get(id)
113
+ if (!provider) continue
114
+
115
+ try {
116
+ const status = await this.detectCLI(provider)
117
+ this.statusCache.set(id, status)
118
+ results[id] = status
119
+
120
+ // Update database via MCP Supabase if userId provided
121
+ if (userId) {
122
+ await this.updateCliStatusInDatabase(userId, id, status)
123
+ }
124
+
125
+ console.log(`[CLI Manager] ${provider.name}: ${status.available ? 'Available' : 'Not Available'}`)
126
+ } catch (error) {
127
+ console.error(`[CLI Manager] Error detecting ${provider.name}:`, error)
128
+ const errorStatus: CLIStatus = {
129
+ available: false,
130
+ authenticated: false,
131
+ lastChecked: new Date(),
132
+ error: error instanceof Error ? error.message : 'Unknown error'
133
+ }
134
+ this.statusCache.set(id, errorStatus)
135
+ results[id] = errorStatus
136
+ }
137
+ }
138
+
139
+ return results
140
+ }
141
+
142
+ /**
143
+ * Get CLI status with cache support
144
+ */
145
+ async getCliStatus(providerId: string, userId?: string): Promise<CLIStatus> {
146
+ const cached = this.statusCache.get(providerId)
147
+ const now = new Date()
148
+
149
+ // Return cached result if still valid
150
+ if (cached && (now.getTime() - cached.lastChecked.getTime()) < this.cacheTimeout) {
151
+ return cached
152
+ }
153
+
154
+ // Force detection if cache is stale or missing
155
+ const results = await this.forceCliDetection(userId, providerId)
156
+ return results[providerId] || {
157
+ available: false,
158
+ authenticated: false,
159
+ lastChecked: now,
160
+ error: 'Provider not found'
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Send prompt to CLI provider
166
+ */
167
+ async sendCliPrompt(
168
+ providerId: string,
169
+ prompt: string,
170
+ mode: 'stdin' | 'args' = 'args',
171
+ timeoutMs: number = 30000
172
+ ): Promise<CLIResponse> {
173
+ const provider = this.providers.get(providerId)
174
+ if (!provider) {
175
+ return {
176
+ success: false,
177
+ error: `Unknown CLI provider: ${providerId}`
178
+ }
179
+ }
180
+
181
+ // Check if CLI is available and authenticated
182
+ const status = await this.getCliStatus(providerId)
183
+ if (!status.available) {
184
+ return {
185
+ success: false,
186
+ error: `${provider.name} is not available. ${provider.installInstructions}`
187
+ }
188
+ }
189
+
190
+ if (!status.authenticated) {
191
+ return {
192
+ success: false,
193
+ error: `${provider.name} is not authenticated. ${provider.authInstructions}`
194
+ }
195
+ }
196
+
197
+ const startTime = Date.now()
198
+
199
+ try {
200
+ let result: string
201
+
202
+ if (mode === 'stdin' && provider.supportsStdin) {
203
+ result = await this.sendPromptViaStdin(provider, prompt, timeoutMs)
204
+ } else if (mode === 'args' && provider.supportsArgs) {
205
+ result = await this.sendPromptViaArgs(provider, prompt, timeoutMs)
206
+ } else {
207
+ return {
208
+ success: false,
209
+ error: `${provider.name} does not support ${mode} mode`
210
+ }
211
+ }
212
+
213
+ const latencyMs = Date.now() - startTime
214
+
215
+ return {
216
+ success: true,
217
+ content: result,
218
+ latencyMs
219
+ }
220
+
221
+ } catch (error) {
222
+ return {
223
+ success: false,
224
+ error: error instanceof Error ? error.message : 'CLI execution failed',
225
+ latencyMs: Date.now() - startTime
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Detect CLI installation and authentication
232
+ */
233
+ private async detectCLI(provider: CLIProvider): Promise<CLIStatus> {
234
+ const customPath = process.env[`${provider.id.toUpperCase()}_PATH`]
235
+ let executablePath: string
236
+
237
+ try {
238
+ // Try custom path first, then system PATH
239
+ if (customPath && fs.existsSync(customPath)) {
240
+ executablePath = customPath
241
+ } else {
242
+ executablePath = await which(provider.executable)
243
+ }
244
+ } catch (error) {
245
+ return {
246
+ available: false,
247
+ authenticated: false,
248
+ lastChecked: new Date(),
249
+ error: `${provider.name} not found in PATH. ${provider.installInstructions}`
250
+ }
251
+ }
252
+
253
+ // Check version
254
+ let version: string
255
+ try {
256
+ const { stdout } = await execAsync(provider.versionCommand, { timeout: 10000 })
257
+ version = stdout.trim()
258
+ } catch (error) {
259
+ return {
260
+ available: false,
261
+ authenticated: false,
262
+ lastChecked: new Date(),
263
+ path: executablePath,
264
+ error: `Failed to get ${provider.name} version`
265
+ }
266
+ }
267
+
268
+ // Check authentication
269
+ let authenticated = false
270
+ try {
271
+ const { stdout, stderr } = await execAsync(provider.authCheckCommand, { timeout: 10000 })
272
+ // Look for success indicators in output
273
+ const output = (stdout + stderr).toLowerCase()
274
+ authenticated = output.includes('authenticated') ||
275
+ output.includes('logged in') ||
276
+ output.includes('valid') ||
277
+ !output.includes('not authenticated')
278
+ } catch (error) {
279
+ // Some CLIs might not have auth status command, assume authenticated if version works
280
+ authenticated = true
281
+ }
282
+
283
+ return {
284
+ available: true,
285
+ authenticated,
286
+ version,
287
+ path: executablePath,
288
+ lastChecked: new Date()
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Send prompt via stdin mode
294
+ */
295
+ private async sendPromptViaStdin(
296
+ provider: CLIProvider,
297
+ prompt: string,
298
+ timeoutMs: number
299
+ ): Promise<string> {
300
+ return new Promise((resolve, reject) => {
301
+ const child = spawn(provider.chatCommand, [], {
302
+ stdio: ['pipe', 'pipe', 'pipe'],
303
+ timeout: timeoutMs
304
+ })
305
+
306
+ let stdout = ''
307
+ let stderr = ''
308
+
309
+ child.stdout?.on('data', (data) => {
310
+ stdout += data.toString()
311
+ })
312
+
313
+ child.stderr?.on('data', (data) => {
314
+ stderr += data.toString()
315
+ })
316
+
317
+ child.on('close', (code) => {
318
+ if (code === 0) {
319
+ resolve(stdout.trim())
320
+ } else {
321
+ reject(new Error(`CLI exited with code ${code}: ${stderr}`))
322
+ }
323
+ })
324
+
325
+ child.on('error', (error) => {
326
+ reject(error)
327
+ })
328
+
329
+ // Send prompt via stdin
330
+ if (child.stdin) {
331
+ child.stdin.write(prompt)
332
+ child.stdin.end()
333
+ }
334
+ })
335
+ }
336
+
337
+ /**
338
+ * Send prompt via command arguments
339
+ */
340
+ private async sendPromptViaArgs(
341
+ provider: CLIProvider,
342
+ prompt: string,
343
+ timeoutMs: number
344
+ ): Promise<string> {
345
+ const command = `${provider.chatCommand} "${prompt.replace(/"/g, '\\"')}"`
346
+
347
+ try {
348
+ const { stdout } = await execAsync(command, { timeout: timeoutMs })
349
+ return stdout.trim()
350
+ } catch (error) {
351
+ throw new Error(`CLI command failed: ${error}`)
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Update CLI status in database using MCP Supabase server
357
+ * This integrates with existing MCP infrastructure
358
+ */
359
+ private async updateCliStatusInDatabase(
360
+ userId: string,
361
+ providerId: string,
362
+ status: CLIStatus
363
+ ): Promise<void> {
364
+ try {
365
+ // Use existing CLI status API endpoint with MCP Supabase integration
366
+ const statusUpdate = {
367
+ server: this.getServerNameForProvider(providerId),
368
+ tool: 'cli_detection',
369
+ args: {
370
+ provider: providerId,
371
+ available: status.available,
372
+ authenticated: status.authenticated,
373
+ version: status.version,
374
+ path: status.path,
375
+ error: status.error
376
+ }
377
+ }
378
+
379
+ // Call existing API endpoint that has MCP Supabase integration
380
+ const response = await fetch('/api/cli-status', {
381
+ method: 'POST',
382
+ headers: {
383
+ 'Content-Type': 'application/json',
384
+ 'User-Agent': 'polydev-cli-manager/1.0.0'
385
+ },
386
+ body: JSON.stringify(statusUpdate)
387
+ })
388
+
389
+ if (!response.ok) {
390
+ throw new Error(`Failed to update CLI status: ${response.status}`)
391
+ }
392
+
393
+ const result = await response.json()
394
+ console.log(`[CLI Manager] Updated database via MCP Supabase for ${providerId}: ${status.available}`)
395
+
396
+ } catch (error) {
397
+ console.error(`[CLI Manager] Failed to update database via MCP Supabase:`, error)
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Map provider ID to server name for MCP integration
403
+ */
404
+ private getServerNameForProvider(providerId: string): string {
405
+ const serverMap = {
406
+ 'claude_code': 'claude-code-cli-bridge',
407
+ 'codex_cli': 'cross-llm-bridge-test',
408
+ 'gemini_cli': 'gemini-cli-bridge'
409
+ }
410
+ return serverMap[providerId as keyof typeof serverMap] || 'unknown-cli-bridge'
411
+ }
412
+
413
+ /**
414
+ * Get all CLI providers configuration
415
+ */
416
+ getProviders(): CLIProvider[] {
417
+ return Array.from(this.providers.values())
418
+ }
419
+
420
+ /**
421
+ * Get provider by ID
422
+ */
423
+ getProvider(providerId: string): CLIProvider | undefined {
424
+ return this.providers.get(providerId)
425
+ }
426
+ }
427
+
428
+ export default CLIManager