@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
JavaScript
// 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