claude-gpt-collabration
Version:
MCP server for GPT-5 interactive file reading and collaboration with Claude Code
206 lines (205 loc) • 7.77 kB
JavaScript
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));
}
}