UNPKG

claude-gpt-collabration

Version:

MCP server for GPT-5 interactive file reading and collaboration with Claude Code

206 lines (205 loc) 7.77 kB
import OpenAI from "openai"; import fs from "fs/promises"; import * as fsSync from "fs"; import path from "path"; import { glob } from "glob"; import crypto from "crypto"; /** * Alternative file manager using Assistants API instead of Vector Stores * This works with current OpenAI SDK versions */ export class AssistantFileManager { client; assistantCache = new Map(); constructor(apiKey) { this.client = new OpenAI({ apiKey: apiKey || process.env.OPENAI_API_KEY, }); } /** * Upload files and create an assistant with file search capability */ async uploadFilesAndCreateAssistant(options) { const startTime = Date.now(); // Resolve file paths let filePaths = []; if (options.paths) { filePaths = options.paths; } else if (options.pattern) { filePaths = await glob(options.pattern, { cwd: options.root, absolute: false, nodir: true }); } else { // Default pattern for common development files filePaths = await glob("**/*.{ts,tsx,js,jsx,json,md,txt}", { cwd: options.root, absolute: false, nodir: true, ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**'] }); } // Convert to absolute paths and validate const absolutePaths = filePaths.map(p => path.resolve(options.root, p)); const validPaths = await this.validatePaths(absolutePaths); if (validPaths.length === 0) { throw new Error('No valid files found to upload'); } // Upload files to OpenAI const fileResults = []; let totalSize = 0; for (const filePath of validPaths) { try { const stats = await fs.stat(filePath); if (stats.size > 512 * 1024 * 1024) { // 512MB limit console.warn(`[AssistantFileManager] Skipping large file: ${filePath} (${stats.size} bytes)`); continue; } const content = await fs.readFile(filePath); const hash = crypto.createHash('sha256').update(content).digest('hex'); // Upload to OpenAI Files API const file = await this.client.files.create({ file: fsSync.createReadStream(filePath), purpose: 'assistants' }); fileResults.push({ path: path.relative(process.cwd(), filePath), fileId: file.id, size: stats.size, hash: hash }); totalSize += stats.size; } catch (uploadError) { console.error(`[AssistantFileManager] Failed to upload ${filePath}:`, uploadError); // Continue with other files } } if (fileResults.length === 0) { throw new Error('Failed to upload any files'); } // Create assistant with uploaded files const assistant = await this.client.beta.assistants.create({ name: options.assistantName || 'File Analysis Assistant', instructions: options.instructions || 'You are a helpful assistant that can analyze uploaded files. Use the files to answer questions about the code and project.', model: 'gpt-4-turbo', tools: [{ type: 'file_search' }], tool_resources: { file_search: { file_ids: fileResults.map(f => f.fileId) } } }); return { assistantId: assistant.id, files: fileResults, totalSize, uploadTime: Date.now() - startTime }; } /** * Query an assistant with uploaded files */ async queryAssistant(assistantId, query) { try { // Create thread const thread = await this.client.beta.threads.create(); // Add message await this.client.beta.threads.messages.create(thread.id, { role: 'user', content: query }); // Create run const run = await this.client.beta.threads.runs.create(thread.id, { assistant_id: assistantId }); // Poll for completion let runStatus = run; let attempts = 0; const maxAttempts = 30; // 5 minutes max while (['queued', 'in_progress'].includes(runStatus.status) && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds runStatus = await this.client.beta.threads.runs.retrieve(thread.id, runStatus.id); attempts++; } if (runStatus.status === 'completed') { // Get messages from thread const messages = await this.client.beta.threads.messages.list(thread.id); const assistantMessage = messages.data.find(msg => msg.role === 'assistant'); if (assistantMessage && assistantMessage.content[0]?.type === 'text') { return assistantMessage.content[0].text.value; } } else { console.error(`[AssistantFileManager] Run failed with status: ${runStatus.status}`); if (runStatus.last_error) { console.error('[AssistantFileManager] Run error:', runStatus.last_error); } } throw new Error(`Assistant query failed with status: ${runStatus.status}`); } catch (error) { console.error('[AssistantFileManager] Query failed:', error); throw error; } } /** * Clean up assistant */ async cleanupAssistant(assistantId) { try { await this.client.beta.assistants.delete(assistantId); console.log(`[AssistantFileManager] Cleaned up assistant: ${assistantId}`); } catch (error) { console.warn(`[AssistantFileManager] Failed to cleanup assistant ${assistantId}:`, error); } } /** * Validate file paths for security */ async validatePaths(filePaths) { const validPaths = []; for (const filePath of filePaths) { try { // Check if file exists and is readable await fs.access(filePath); const stats = await fs.stat(filePath); if (!stats.isFile()) { console.warn(`[AssistantFileManager] Skipping non-file: ${filePath}`); continue; } // Security validation if (this.isPathBlocked(filePath)) { console.warn(`[AssistantFileManager] Blocked path: ${filePath}`); continue; } validPaths.push(filePath); } catch (error) { console.warn(`[AssistantFileManager] Cannot access file: ${filePath}`, error); } } return validPaths; } /** * Check if path should be blocked for security */ isPathBlocked(filePath) { const blockedPatterns = [ /\.ssh/, /\.env/, /\.key$/, /\.pem$/, /node_modules/, /\.git/, /password|secret|token/i, /\.lock$/, /\.log$/ ]; return blockedPatterns.some(pattern => pattern.test(filePath)); } }