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.
- package/README.md +359 -0
- package/lib/cliManager.js +541 -0
- package/lib/cliManager.ts +428 -0
- package/mcp/manifest.json +462 -0
- package/mcp/package.json +46 -0
- package/mcp/server.js +598 -0
- package/mcp/stdio-wrapper.js +472 -0
- package/package.json +83 -0
|
@@ -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
|