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/README.md +121 -320
- package/package.json +29 -84
- package/{mcp/stdio-wrapper.js → stdio-wrapper.js} +9 -4
- package/lib/cliManager.ts +0 -755
- package/lib/smartCliCache.ts +0 -189
- package/lib/universalMemoryExtractor.js +0 -607
- package/lib/zeroKnowledgeEncryption.js +0 -289
- package/mcp/README.md +0 -160
- package/mcp/package.json +0 -46
- package/mcp/server.js +0 -959
- package/mcp/stdio-wrapper-fixed.js +0 -169
- /package/{lib/cliManager.js → cliManager.js} +0 -0
- /package/{mcp/manifest.json → manifest.json} +0 -0
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
|