UNPKG

@context-sync/server

Version:

MCP server for AI context sync with persistent memory, workspace file access, and intelligent code operations

331 lines 12.7 kB
// File and Content Search Operations import * as fs from 'fs'; import * as path from 'path'; export class FileSearcher { workspaceDetector; // Regex pattern cache for better performance patternCache = new Map(); constructor(workspaceDetector) { this.workspaceDetector = workspaceDetector; } /** * Search for files by name or pattern */ searchFiles(pattern, options = {}) { const workspace = this.workspaceDetector.getCurrentWorkspace(); if (!workspace) { return []; } const { maxResults = 50, ignoreCase = true, filePattern } = options; const results = []; const searchPattern = ignoreCase ? pattern.toLowerCase() : pattern; this.searchRecursive(workspace, searchPattern, results, maxResults, ignoreCase, filePattern); return results; } /** * Search file contents for text or regex */ searchContent(query, options = {}) { const workspace = this.workspaceDetector.getCurrentWorkspace(); if (!workspace) { return []; } const { maxResults = 100, regex = false, caseSensitive = false, contextLines = 2, filePattern } = options; const results = []; const searchRegex = this.createSearchRegex(query, regex, caseSensitive); this.searchContentRecursive(workspace, searchRegex, results, maxResults, contextLines, filePattern); return results; } /** * Find symbol definitions (functions, classes, etc.) */ findSymbol(symbol, type) { const workspace = this.workspaceDetector.getCurrentWorkspace(); if (!workspace) { return []; } const patterns = this.getSymbolPatterns(symbol, type || 'all'); const results = []; for (const pattern of patterns) { const matches = this.searchContent(pattern, { regex: true, caseSensitive: true, maxResults: 20 }); results.push(...matches); } return results; } /** * Get unique file extensions in workspace */ getFileExtensions() { const workspace = this.workspaceDetector.getCurrentWorkspace(); if (!workspace) { return new Map(); } const extensions = new Map(); this.countExtensions(workspace, extensions); return extensions; } /** * Get file statistics */ getFileStats() { const workspace = this.workspaceDetector.getCurrentWorkspace(); if (!workspace) { return { totalFiles: 0, totalSize: 0, byExtension: new Map() }; } const stats = { totalFiles: 0, totalSize: 0, byExtension: new Map() }; this.calculateStats(workspace, stats); return stats; } // ========== PRIVATE HELPER METHODS ========== searchRecursive(dirPath, pattern, results, maxResults, ignoreCase, filePattern) { if (results.length >= maxResults) return; try { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (results.length >= maxResults) break; if (this.shouldIgnore(entry.name)) continue; const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { this.searchRecursive(fullPath, pattern, results, maxResults, ignoreCase, filePattern); } else { const name = ignoreCase ? entry.name.toLowerCase() : entry.name; // Check file pattern if specified if (filePattern && !this.matchesPattern(entry.name, filePattern)) { continue; } // Check if name matches search pattern if (name.includes(pattern)) { const stats = fs.statSync(fullPath); const relativePath = path.relative(this.workspaceDetector.getCurrentWorkspace(), fullPath); results.push({ path: relativePath, name: entry.name, size: stats.size, language: this.detectLanguage(entry.name) }); } } } } catch (error) { // Ignore errors (permission denied, etc.) } } searchContentRecursive(dirPath, searchRegex, results, maxResults, contextLines, filePattern) { if (results.length >= maxResults) return; try { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (results.length >= maxResults) break; if (this.shouldIgnore(entry.name)) continue; const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { this.searchContentRecursive(fullPath, searchRegex, results, maxResults, contextLines, filePattern); } else { // Check file pattern if (filePattern && !this.matchesPattern(entry.name, filePattern)) { continue; } // Only search text files if (!this.isTextFile(entry.name)) { continue; } try { const content = fs.readFileSync(fullPath, 'utf8'); const lines = content.split('\n'); const relativePath = path.relative(this.workspaceDetector.getCurrentWorkspace(), fullPath); for (let i = 0; i < lines.length; i++) { if (results.length >= maxResults) break; const line = lines[i]; const match = line.match(searchRegex); if (match) { results.push({ path: relativePath, line: i + 1, content: line.trim(), match: match[0], context: { before: this.getContext(lines, i, -contextLines), after: this.getContext(lines, i, contextLines) } }); } } } catch (error) { // Ignore files that can't be read as text } } } } catch (error) { // Ignore errors } } createSearchRegex(query, regex, caseSensitive) { if (regex) { return new RegExp(query, caseSensitive ? 'g' : 'gi'); } else { // Escape special regex characters const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(escaped, caseSensitive ? 'g' : 'gi'); } } getSymbolPatterns(symbol, type) { const patterns = []; if (type === 'function' || type === 'all') { // Function declarations patterns.push(`function\\s+${symbol}\\s*\\(`); patterns.push(`const\\s+${symbol}\\s*=\\s*\\(`); patterns.push(`${symbol}\\s*:\\s*\\([^)]*\\)\\s*=>`); patterns.push(`async\\s+function\\s+${symbol}\\s*\\(`); } if (type === 'class' || type === 'all') { // Class declarations patterns.push(`class\\s+${symbol}\\s*[{<]`); patterns.push(`interface\\s+${symbol}\\s*[{<]`); patterns.push(`type\\s+${symbol}\\s*=`); } if (type === 'variable' || type === 'all') { // Variable declarations patterns.push(`const\\s+${symbol}\\s*[=:]`); patterns.push(`let\\s+${symbol}\\s*[=:]`); patterns.push(`var\\s+${symbol}\\s*[=:]`); } return patterns; } getContext(lines, index, offset) { const context = []; const start = Math.max(0, index + (offset < 0 ? offset : 1)); const end = Math.min(lines.length, index + (offset < 0 ? 0 : offset + 1)); for (let i = start; i < end; i++) { if (i !== index) { context.push(lines[i].trim()); } } return context; } matchesPattern(filename, pattern) { // Simple glob pattern matching with cached regex for performance if (pattern.includes('*')) { let regex = this.patternCache.get(pattern); if (!regex) { regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); this.patternCache.set(pattern, regex); } return regex.test(filename); } return filename.includes(pattern); } isTextFile(filename) { const textExtensions = [ '.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt', '.css', '.scss', '.html', '.xml', '.yaml', '.yml', '.py', '.rs', '.go', '.java', '.c', '.cpp', '.h', '.rb', '.php', '.swift', '.kt', '.sql', '.sh' ]; const ext = path.extname(filename).toLowerCase(); return textExtensions.includes(ext); } detectLanguage(filename) { const ext = path.extname(filename).toLowerCase(); const langMap = { '.ts': 'TypeScript', '.tsx': 'TypeScript React', '.js': 'JavaScript', '.jsx': 'JavaScript React', '.py': 'Python', '.rs': 'Rust', '.go': 'Go', '.java': 'Java', '.json': 'JSON', '.md': 'Markdown' }; return langMap[ext] || 'Unknown'; } shouldIgnore(name) { const ignorePatterns = [ 'node_modules', '.git', '.next', 'dist', 'build', '.turbo', 'coverage', '.cache' ]; return ignorePatterns.some(pattern => name === pattern || name.startsWith('.')); } countExtensions(dirPath, extensions) { try { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (this.shouldIgnore(entry.name)) continue; const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { this.countExtensions(fullPath, extensions); } else { const ext = path.extname(entry.name); if (ext) { extensions.set(ext, (extensions.get(ext) || 0) + 1); } } } } catch (error) { // Ignore errors } } calculateStats(dirPath, stats) { try { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (this.shouldIgnore(entry.name)) continue; const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { this.calculateStats(fullPath, stats); } else { try { const fileStats = fs.statSync(fullPath); const ext = path.extname(entry.name) || 'no-extension'; stats.totalFiles++; stats.totalSize += fileStats.size; const extStats = stats.byExtension.get(ext) || { count: 0, size: 0 }; extStats.count++; extStats.size += fileStats.size; stats.byExtension.set(ext, extStats); } catch (error) { // Ignore stat errors } } } } catch (error) { // Ignore errors } } } //# sourceMappingURL=file-searcher.js.map