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