UNPKG

claude-gpt-collabration

Version:

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

358 lines (357 loc) 14.6 kB
import { glob } from "glob"; import fs from "fs/promises"; import path from "path"; export class SmartFilePreparation { // File patterns by task category static TASK_PATTERNS = { routing: { patterns: ['**/router.*', '**/routes/**', '**/navigation/**', '**/*route*'], keywords: ['route', 'routing', 'navigation', 'navigate', 'path', 'url', 'endpoint'], description: 'Routing and navigation related files' }, auth: { patterns: ['**/auth/**', '**/security/**', '**/guard.*', '**/*auth*', '**/*login*', '**/*session*'], keywords: ['auth', 'authentication', 'login', 'logout', 'security', 'guard', 'permission', 'access', 'token', 'session'], description: 'Authentication and security related files' }, api: { patterns: ['**/api/**', '**/services/**', '**/controllers/**', '**/*service*', '**/*controller*'], keywords: ['api', 'endpoint', 'service', 'controller', 'http', 'request', 'response', 'rest', 'graphql'], description: 'API and service related files' }, database: { patterns: ['**/models/**', '**/entities/**', '**/schemas/**', '**/*model*', '**/*entity*', '**/*schema*'], keywords: ['database', 'db', 'model', 'entity', 'schema', 'migration', 'query', 'sql', 'orm'], description: 'Database and data model files' }, ui: { patterns: ['**/components/**', '**/pages/**', '**/views/**', '**/*.vue', '**/*.jsx', '**/*.tsx'], keywords: ['component', 'ui', 'interface', 'view', 'page', 'frontend', 'react', 'vue', 'angular'], description: 'UI components and frontend files' }, config: { patterns: ['**/*.config.*', '**/settings/**', '**/config/**', '**/*.env*', '**/package.json'], keywords: ['config', 'configuration', 'settings', 'environment', 'setup', 'build'], description: 'Configuration and setup files' }, testing: { patterns: ['**/*.test.*', '**/*.spec.*', '**/tests/**', '**/test/**', '**/__tests__/**'], keywords: ['test', 'testing', 'spec', 'unit', 'integration', 'e2e', 'jest', 'mocha', 'cypress'], description: 'Test files and testing utilities' }, error: { patterns: ['**/*.log', '**/logs/**', '**/error*', '**/debug*'], keywords: ['error', 'debug', 'log', 'exception', 'bug', 'issue', 'problem', 'fail'], description: 'Error logs and debugging files' }, docs: { patterns: ['**/*.md', '**/docs/**', '**/README*', '**/CHANGELOG*', '**/*.txt'], keywords: ['documentation', 'readme', 'docs', 'guide', 'manual', 'help'], description: 'Documentation and help files' }, build: { patterns: ['**/webpack.*', '**/vite.*', '**/rollup.*', '**/build/**', '**/dist/**', '**/*.json'], keywords: ['build', 'compile', 'bundle', 'webpack', 'vite', 'rollup', 'deploy'], description: 'Build and deployment files' } }; // Important files that should have higher priority static IMPORTANT_FILES = [ 'package.json', 'tsconfig.json', 'index.*', 'main.*', 'app.*', 'server.*', 'README.md' ]; /** * Analyze task and prepare relevant files */ async analyzeTaskAndPrepareFiles(task, root, maxFiles = 20) { console.log(`[SmartFilePreparation] Analyzing task: ${task.substring(0, 100)}...`); // 1. Detect patterns from task const detectedPatterns = this.detectTaskPatterns(task); console.log(`[SmartFilePreparation] Detected patterns:`, detectedPatterns.map(p => p.description)); // 2. Find matching files const candidateFiles = await this.findMatchingFiles(detectedPatterns, root); console.log(`[SmartFilePreparation] Found ${candidateFiles.length} candidate files`); // 3. Score and prioritize files const scoredFiles = await this.scoreFiles(candidateFiles, task, root); // 4. Select top files const selectedFiles = scoredFiles .sort((a, b) => b.score - a.score) .slice(0, maxFiles) .map(f => f.path); console.log(`[SmartFilePreparation] Selected ${selectedFiles.length} files:`, selectedFiles); return selectedFiles; } /** * Detect task patterns from keywords */ detectTaskPatterns(task) { const taskLower = task.toLowerCase(); const detectedPatterns = []; for (const [category, pattern] of Object.entries(SmartFilePreparation.TASK_PATTERNS)) { // Check if any keywords match const matchingKeywords = pattern.keywords.filter(keyword => taskLower.includes(keyword.toLowerCase())); if (matchingKeywords.length > 0) { detectedPatterns.push({ ...pattern, keywords: matchingKeywords // Only include matching keywords }); } } // If no specific patterns detected, include config and main files if (detectedPatterns.length === 0) { detectedPatterns.push(SmartFilePreparation.TASK_PATTERNS.config); } return detectedPatterns; } /** * Find files matching patterns */ async findMatchingFiles(patterns, root) { const allFiles = new Set(); for (const pattern of patterns) { for (const globPattern of pattern.patterns) { try { const files = await glob(globPattern, { cwd: root, absolute: false, nodir: true, ignore: [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', '**/*.min.*', '**/*.map' ] }); files.forEach(file => allFiles.add(file)); } catch (error) { console.warn(`[SmartFilePreparation] Pattern failed: ${globPattern}`, error); } } } return Array.from(allFiles); } /** * Score files based on relevance to task */ async scoreFiles(files, task, root) { const scoredFiles = []; const taskLower = task.toLowerCase(); for (const file of files) { const reasons = []; let score = 0; try { const fullPath = path.resolve(root, file); const stats = await fs.stat(fullPath); // Size penalty for very large files (prefer smaller files for initial context) if (stats.size > 1024 * 1024) { // > 1MB score -= 10; reasons.push('large file penalty'); } else if (stats.size < 1024) { // < 1KB score -= 5; reasons.push('very small file penalty'); } // Important file bonus const fileName = path.basename(file).toLowerCase(); const isImportant = SmartFilePreparation.IMPORTANT_FILES.some(pattern => { if (pattern.includes('*')) { return fileName.includes(pattern.replace('*', '')); } return fileName === pattern.toLowerCase(); }); if (isImportant) { score += 20; reasons.push('important file'); } // File name relevance to task const fileNameScore = this.calculateNameRelevance(file, taskLower); score += fileNameScore; if (fileNameScore > 0) { reasons.push(`name relevance: +${fileNameScore}`); } // File extension bonus const ext = path.extname(file).toLowerCase(); const extensionScore = this.getExtensionScore(ext, taskLower); score += extensionScore; if (extensionScore > 0) { reasons.push(`extension bonus: +${extensionScore}`); } // Path depth penalty (prefer files closer to root) const depth = file.split('/').length; if (depth > 3) { score -= depth; reasons.push(`depth penalty: -${depth}`); } // Recent modification bonus const age = Date.now() - stats.mtime.getTime(); const daysSinceModified = age / (1000 * 60 * 60 * 24); if (daysSinceModified < 7) { const bonus = Math.max(0, 10 - daysSinceModified); score += bonus; reasons.push(`recent modification: +${bonus.toFixed(1)}`); } scoredFiles.push({ path: file, score, reasons }); } catch (error) { console.warn(`[SmartFilePreparation] Cannot score file: ${file}`, error); } } return scoredFiles; } /** * Calculate file name relevance to task */ calculateNameRelevance(filePath, taskLower) { const fileName = path.basename(filePath, path.extname(filePath)).toLowerCase(); const pathParts = filePath.toLowerCase().split('/'); let score = 0; // Exact matches in filename const taskWords = taskLower.split(/\s+/).filter(word => word.length > 2); for (const word of taskWords) { if (fileName.includes(word)) { score += 15; } // Check path components for (const part of pathParts) { if (part.includes(word)) { score += 8; } } } return score; } /** * Get extension-specific score based on task */ getExtensionScore(extension, taskLower) { const extensionScores = { '.ts': 15, '.js': 12, '.tsx': 12, '.jsx': 12, '.vue': 10, '.json': 8, '.md': 5, '.txt': 3, '.yml': 6, '.yaml': 6, '.env': 5 }; let score = extensionScores[extension] || 0; // Context-specific bonuses if (taskLower.includes('react') && (extension === '.tsx' || extension === '.jsx')) { score += 10; } if (taskLower.includes('vue') && extension === '.vue') { score += 10; } if (taskLower.includes('typescript') && extension === '.ts') { score += 10; } if (taskLower.includes('config') && (extension === '.json' || extension === '.yml')) { score += 8; } return score; } /** * Get files for specific category */ async getFilesForCategory(category, root) { const pattern = SmartFilePreparation.TASK_PATTERNS[category]; if (!pattern) { return []; } return this.findMatchingFiles([pattern], root); } /** * Predict files based on error patterns */ async predictFilesForError(errorMessage, stackTrace, root) { const allFiles = new Set(); // Extract file paths from stack trace const stackFiles = this.extractFilesFromStackTrace(stackTrace); stackFiles.forEach(file => allFiles.add(file)); // Extract file references from error message const errorFiles = this.extractFilesFromErrorMessage(errorMessage); errorFiles.forEach(file => allFiles.add(file)); // Add related files based on error type const errorType = this.detectErrorType(errorMessage); if (errorType) { const relatedFiles = await this.getFilesForCategory(errorType, root); relatedFiles.slice(0, 10).forEach(file => allFiles.add(file)); } return Array.from(allFiles); } /** * Extract file paths from stack trace */ extractFilesFromStackTrace(stackTrace) { const files = []; // Common stack trace patterns const patterns = [ /at .* \(([^:)]+\.[a-zA-Z]+):\d+:\d+\)/g, /^\s*at ([^:)]+\.[a-zA-Z]+):\d+:\d+/gm, /File "([^"]+)", line \d+/g ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(stackTrace)) !== null) { files.push(match[1]); } } return files.filter(file => !file.includes('node_modules')); } /** * Extract file references from error message */ extractFilesFromErrorMessage(errorMessage) { const files = []; // Look for file paths in error message const patterns = [ /Cannot resolve module ['"']([^'"]+)['"']/g, /Module not found: Error: Can't resolve ['"']([^'"]+)['"']/g, /Failed to load ([^\s]+\.[a-zA-Z]+)/g, /Error in ([^\s:]+\.[a-zA-Z]+)/g ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(errorMessage)) !== null) { files.push(match[1]); } } return files; } /** * Detect error type from message */ detectErrorType(errorMessage) { const message = errorMessage.toLowerCase(); if (message.includes('auth') || message.includes('unauthorized') || message.includes('forbidden')) { return 'auth'; } if (message.includes('route') || message.includes('path') || message.includes('404')) { return 'routing'; } if (message.includes('api') || message.includes('endpoint') || message.includes('request')) { return 'api'; } if (message.includes('database') || message.includes('query') || message.includes('sql')) { return 'database'; } if (message.includes('component') || message.includes('render')) { return 'ui'; } if (message.includes('config') || message.includes('environment')) { return 'config'; } return null; } }