openai-cli-unofficial
Version:
A powerful OpenAI CLI Coding Agent built with TypeScript
887 lines (886 loc) • 112 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileSystemService = void 0;
const cli_highlight_1 = require("cli-highlight");
const diff_1 = require("diff");
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const checkpoint_1 = require("../../services/checkpoint");
const storage_1 = require("../../services/storage");
const file_types_1 = require("../../utils/file-types");
const base_service_1 = require("../base-service");
// Unified file system MCP service combining reading, operations, and editing
class FileSystemService extends base_service_1.BaseMCPService {
constructor() {
super('file-system', '1.0.0');
}
getTools() {
return [
// File reading tools
{
name: 'read_file',
description: 'Read a specific range of lines from a file to understand its content. For optimal performance and context, it is recommended to read about 300 lines at a time. The response will include line numbers.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path to read (supports both relative and absolute paths). Example: "src/utils/helpers.ts"'
},
encoding: {
type: 'string',
description: 'File encoding format. Example: "utf8"',
default: 'utf8',
enum: ['utf8', 'ascii', 'base64', 'hex', 'binary']
},
startLine: {
type: 'number',
description: 'Starting line number (1-based) for reading. Example: 1',
minimum: 1
},
endLine: {
type: 'number',
description: 'Ending line number (1-based) for reading. Example: 300',
minimum: 1
}
},
required: ['path', 'startLine', 'endLine']
}
},
{
name: 'list_directory',
description: 'List the contents of a directory to understand the project structure. Provides a tree view and detailed file information.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Directory path to list (supports both relative and absolute paths). Example: "src/services/"',
default: '.'
}
},
required: ['path']
}
},
{
name: 'search_files',
description: 'Search for files by filename. When the user does not mention a file path, proactively search for the file.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The filename or partial filename to search for. Example: "service.ts"'
},
basePath: {
type: 'string',
description: 'The directory path to start searching from. Example: "src/"',
default: '.'
}
},
required: ['query']
}
},
{
name: 'search_file_content',
description: 'Fuzzy search for keywords, get all file paths containing the keyword content. When the user does not mention a file, proactively search for the file when only mentioning a function or feature.',
inputSchema: {
type: 'object',
properties: {
keyword: {
type: 'string',
description: 'The keyword or code snippet to search for. Example: "handleReadFile"'
},
basePath: {
type: 'string',
description: 'The directory path to start searching from. Example: "src/"',
default: '.'
}
},
required: ['keyword']
}
},
{
name: 'code_reference_search',
description: '🔍 **CRITICAL TOOL** - Advanced code reference and dependency analysis with high-performance concurrent processing. Uses intelligent file prioritization, parallel search across CPU cores, and smart result aggregation for blazing-fast results in large codebases. This is the MOST IMPORTANT tool for understanding code structure, relationships, and dependencies. Use this tool frequently for any code analysis tasks! Provides intelligent symbol search, dependency tracking, import/export analysis, and comprehensive cross-reference mapping.',
inputSchema: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'The symbol, function name, class name, variable, or identifier to search for. Supports fuzzy matching. Example: "getUserData", "handleSubmit", "UserService"'
},
searchType: {
type: 'string',
description: 'Type of search to perform',
enum: ['definition', 'references', 'usage', 'dependencies', 'reverse-dependencies', 'all'],
default: 'all'
},
basePath: {
type: 'string',
description: 'The directory path to start searching from (optional, defaults to current directory)',
default: '.'
},
includeComments: {
type: 'boolean',
description: 'Include matches found in code comments',
default: false
},
fuzzyMatch: {
type: 'boolean',
description: 'Enable fuzzy matching for better search results (finds similar symbols)',
default: true
},
maxResults: {
type: 'number',
description: 'Maximum number of results to return (default: 50, max: 100). Lower values provide faster results in large projects.',
default: 50,
minimum: 1,
maximum: 100
},
includeDependencies: {
type: 'boolean',
description: 'Include dependency analysis (what files the symbol\'s file imports/requires)',
default: true
},
includeReverseDependencies: {
type: 'boolean',
description: 'Include reverse dependency analysis (what files depend on the symbol\'s file)',
default: true
},
analyzeImports: {
type: 'boolean',
description: 'Analyze import/export statements for relationship mapping',
default: true
},
depthLevel: {
type: 'number',
description: 'Dependency analysis depth level (1-3, default: 2)',
default: 2,
minimum: 1,
maximum: 3
}
},
required: ['symbol']
}
},
// File operation tools
{
name: 'create_file',
description: 'Create a new file with optional content. This is useful for adding new features or modules to the project. Parent directories will be created if they do not exist. If the file content is large, you can create part of the content first, and then call the `edit_file` tool to edit the file.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path to create. Example: "src/new_feature/main.ts"'
},
content: {
type: 'string',
description: 'Initial content for the file. Can be empty. Example: "export function newFunc() { console.log(\'hello\'); }"',
default: ''
},
encoding: {
type: 'string',
description: 'File encoding format. Example: "utf8"',
default: 'utf8',
enum: ['utf8', 'ascii', 'base64', 'hex', 'binary']
}
},
required: ['path']
}
},
{
name: 'delete_file',
description: 'Delete a file or an entire directory. Use with caution, as this operation is permanent. The `recursive` option must be used for non-empty directories.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File or directory path to delete. Example: "src/old_feature/main.ts"'
},
recursive: {
type: 'boolean',
description: 'If true, allows deletion of non-empty directories. Example: false',
default: false
}
},
required: ['path']
}
},
{
name: 'create_directory',
description: 'Create a new directory. This is useful for structuring new modules or features. Can create parent directories recursively by default.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Directory path to create. Example: "src/new_module/components/"'
},
recursive: {
type: 'boolean',
description: 'Create parent directories if they do not exist. Example: true',
default: true
}
},
required: ['path']
}
},
// File editing tool
{
name: 'edit_file',
description: `Edits a file by replacing a range of lines. This is the primary tool for modifying code. For safe and effective editing, follow these steps: 1. **Read First**: Always use 'read_file' to get current line numbers before editing. 2. **Check Syntax**: After every edit, meticulously check for syntax errors like unclosed brackets. 3. **Verify Changes**: Read the file again after editing to confirm changes. 4. **Use Terminal**: For complex changes, use the terminal to run tests or linters to catch issues from atomic edits.`,
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path to edit. Example: "src/utils/helpers.ts"'
},
startLine: {
type: 'number',
description: 'The first line of the code to be replaced (1-based). Example: 10',
minimum: 1
},
endLine: {
type: 'number',
description: 'The last line of the code to be replaced (inclusive). Example: 15',
minimum: 1
},
newContent: {
type: 'string',
description: 'The new content to insert. Use an empty string to delete lines. Example: "const newVar = \'updated value\';"'
},
encoding: {
type: 'string',
description: 'File encoding format. Example: "utf8"',
default: 'utf8',
enum: ['utf8', 'ascii', 'base64', 'hex', 'binary']
}
},
required: ['path', 'startLine', 'endLine', 'newContent']
}
}
];
}
async handleRequest(request) {
try {
switch (request.method) {
case 'read_file':
return await this.handleReadFile(request);
case 'list_directory':
return await this.handleListDirectory(request);
case 'search_files':
return await this.handleSearchFiles(request);
case 'search_file_content':
return await this.handleSearchFileContent(request);
case 'code_reference_search':
return await this.handleCodeReferenceSearch(request);
case 'create_file':
return await this.handleCreateFile(request);
case 'delete_file':
return await this.handleDeleteFile(request);
case 'create_directory':
return await this.handleCreateDirectory(request);
case 'edit_file':
return await this.handleEditFile(request);
default:
return this.createErrorResponse(request.id, -32601, `Unsupported method: ${request.method}`);
}
}
catch (error) {
return this.createErrorResponse(request.id, -32603, 'Internal server error', error instanceof Error ? error.message : String(error));
}
}
// File reading methods
async handleReadFile(request) {
const validationError = this.validateParams(request.params, ['path', 'startLine', 'endLine']);
if (validationError) {
return this.createErrorResponse(request.id, -32602, validationError);
}
const params = request.params;
const encoding = params.encoding || 'utf8';
try {
const targetPath = path.resolve(params.path);
if (!fs.existsSync(targetPath)) {
return this.createSuccessResponse(request.id, `❌ **Error: File does not exist**\n\nThe file "${params.path}" was not found.\n\nPossible solutions:\n- Check the file path spelling\n- Verify the file exists\n- Use the correct relative or absolute path\n\n*Please verify the file path and try again.*`);
}
const stats = fs.statSync(targetPath);
if (stats.isDirectory()) {
return this.createSuccessResponse(request.id, `❌ **Error: Path is a directory**\n\nThe path "${params.path}" is a directory, not a file.\n\nPossible solutions:\n- Use list_directory to view directory contents\n- Specify a file path instead\n- Choose a file within this directory\n\n*Use list_directory tool for directory contents.*`);
}
const content = fs.readFileSync(targetPath, encoding);
const lines = content.split('\n');
const totalLines = lines.length;
let finalContent = '';
let isPartial = false;
let lineRange;
const startLine = params.startLine;
const endLine = params.endLine;
if (startLine > endLine) {
return this.createSuccessResponse(request.id, `❌ **Error: Invalid range**\n\nStart line (${startLine}) cannot be greater than end line (${endLine}).\n\n*Please ensure startLine ≤ endLine.*`);
}
if (startLine > totalLines) {
return this.createSuccessResponse(request.id, `✅ **File read successfully**\n\nStart line (${startLine}) is beyond the end of the file (total lines: ${totalLines}). Returning empty content.`);
}
const effectiveEndLine = Math.min(endLine, totalLines);
const selectedLines = lines.slice(startLine - 1, effectiveEndLine);
const contentWithLineNumbers = selectedLines
.map((line, index) => {
const lineNum = startLine + index;
return `${lineNum.toString().padStart(5, ' ')}: ${line}`;
})
.join('\n');
finalContent = contentWithLineNumbers;
isPartial = true;
lineRange = { start: startLine, end: effectiveEndLine };
// Calculate token count (rough estimation)
const tokenCount = this.estimateTokenCount(finalContent);
// 打印文件读取信息到控制台
console.log(`📃 Read file: ${targetPath} (${isPartial ? `${lineRange.start}-${lineRange.end} of ${totalLines}` : totalLines})`);
const message = `✅ **File read successfully**\n\n**File:** \`${targetPath}\`\n**Size:** ${this.formatFileSize(stats.size)}\n**Lines:** ${isPartial ? `${lineRange.start}-${lineRange.end} of ${totalLines}` : totalLines}\n**Tokens:** ~${tokenCount}\n**Modified:** ${stats.mtime.toLocaleString()}\n\n${isPartial ? '*Partial content - use startLine/endLine to read different sections.*' : '*Complete file content loaded.*'}`;
const response = `${message}\n\n---\n\n${finalContent}`;
return this.createSuccessResponse(request.id, response);
}
catch (error) {
return this.handleFileReadError(request.id, params.path, error);
}
}
async handleListDirectory(request) {
const validationError = this.validateParams(request.params, ['path']);
if (validationError) {
return this.createErrorResponse(request.id, -32602, validationError);
}
const params = request.params;
const targetPath = path.resolve(params.path);
try {
if (!fs.existsSync(targetPath)) {
return this.createSuccessResponse(request.id, `❌ **Error: Directory does not exist**\n\nThe directory "${params.path}" was not found.\n\nPossible solutions:\n- Check the directory path spelling\n- Verify the directory exists\n- Use the correct relative or absolute path\n\n*Please verify the directory path and try again.*`);
}
const stats = fs.statSync(targetPath);
if (!stats.isDirectory()) {
return this.createSuccessResponse(request.id, `❌ **Error: Path is not a directory**\n\nThe path "${params.path}" is a file, not a directory.\n\nPossible solutions:\n- Use read_file to read file contents\n- Use the parent directory path\n- Choose a valid directory path\n\n*Use read_file tool for file contents.*`);
}
const items = fs.readdirSync(targetPath, { withFileTypes: true });
const directoryItems = items.map(item => {
const itemPath = path.join(targetPath, item.name);
const itemStats = fs.statSync(itemPath);
return {
name: item.name,
type: item.isDirectory() ? 'directory' : 'file',
path: itemPath,
size: item.isFile() ? itemStats.size : undefined,
lastModified: itemStats.mtime.toISOString()
};
});
const totalFiles = directoryItems.filter(item => item.type === 'file').length;
const totalDirectories = directoryItems.filter(item => item.type === 'directory').length;
// Generate tree structure
const structure = this.generateDirectoryTree(targetPath, directoryItems);
const resultMessage = `✅ **Directory listed successfully**\n\n**Path:** \`${targetPath}\`\n**Contains:** ${totalFiles} files, ${totalDirectories} directories\n\n${structure}`;
console.log(structure);
return this.createSuccessResponse(request.id, resultMessage);
}
catch (error) {
return this.handleFileReadError(request.id, params.path, error);
}
}
async handleSearchFiles(request) {
const validationError = this.validateParams(request.params, ['query']);
if (validationError) {
return this.createErrorResponse(request.id, -32602, validationError);
}
const { query, basePath = '.' } = request.params;
const startPath = path.resolve(basePath);
try {
const foundFiles = this.findFilesRecursive(startPath, (filePath) => path.basename(filePath).includes(query), (dirPath) => !['node_modules', '.git'].some(excluded => path.basename(dirPath) === excluded));
if (foundFiles.length === 0) {
return this.createSuccessResponse(request.id, `🟡 **No files found for "${query}"**\n\nYour search did not match any files.\n\nPossible solutions:\n- Check your spelling\n- Try a different or broader query\n- Specify a different base path to search in`);
}
const resultMessage = `✅ **File search results for "${query}"**\n\nFound ${foundFiles.length} files:\n\n${foundFiles.map(f => `- \`${path.relative(process.cwd(), f)}\``).join('\n')}`;
const consoleOutput = `✅ File search results for "${query}"\nFound ${foundFiles.length} files:\n${foundFiles.map(f => ` - ${path.relative(process.cwd(), f)}`).join('\n')}`;
console.log((0, cli_highlight_1.highlight)(consoleOutput, { language: 'markdown', ignoreIllegals: true }));
return this.createSuccessResponse(request.id, resultMessage);
}
catch (error) {
return this.handleFileReadError(request.id, basePath, error);
}
}
async handleSearchFileContent(request) {
const validationError = this.validateParams(request.params, ['keyword']);
if (validationError) {
return this.createErrorResponse(request.id, -32602, validationError);
}
const { keyword, basePath = '.' } = request.params;
const startPath = path.resolve(basePath);
const binaryExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.zip', '.pdf', '.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.ico'];
try {
const matchingResults = new Map();
const allFiles = this.findFilesRecursive(startPath, () => true, (dirPath) => !['node_modules', '.git'].some(excluded => path.basename(dirPath) === excluded));
for (const file of allFiles) {
if (binaryExtensions.includes(path.extname(file).toLowerCase())) {
continue;
}
try {
const stats = fs.statSync(file);
if (stats.size > 2 * 1024 * 1024) { // 2MB limit
continue;
}
const content = fs.readFileSync(file, 'utf-8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes(keyword)) {
const relativePath = path.relative(process.cwd(), file);
if (!matchingResults.has(relativePath)) {
matchingResults.set(relativePath, []);
}
matchingResults.get(relativePath).push({
lineNumber: i + 1,
lineContent: line.trim()
});
}
}
}
catch (readError) {
// Ignore file read errors (e.g., permission denied on a specific file)
}
}
if (matchingResults.size === 0) {
return this.createSuccessResponse(request.id, `🟡 **No content matches for "${keyword}"**\n\nYour search did not find any files containing the keyword.\n\nPossible solutions:\n- Check your spelling or try different keywords\n- Widen the search by changing the base path\n- The content may be in a file type that is excluded from search (e.g., binary files)`);
}
let resultMessage = `✅ **Content search results for "${keyword}"**\n\nFound keyword in ${matchingResults.size} files:\n`;
let consoleOutput = `✅ Content search results for "${keyword}"\nFound keyword in ${matchingResults.size} files:\n`;
for (const [filePath, matches] of matchingResults.entries()) {
const language = (0, file_types_1.getLanguageForFile)(filePath);
const codeBlockForUi = matches.map(match => `${match.lineNumber}: ${match.lineContent}`).join('\n');
resultMessage += `\n**File:** \`${filePath}\`\n\`\`\`${language}\n${codeBlockForUi}\n\`\`\`\n`;
const codeBlockForConsole = matches.map(match => {
const lineNum = `${match.lineNumber}`.padStart(5, ' ');
return `${lineNum}: ${match.lineContent}`;
}).join('\n');
consoleOutput += `\n--------------------\nFile: ${filePath}\n--------------------\n`;
consoleOutput += `${(0, cli_highlight_1.highlight)(codeBlockForConsole, { language, ignoreIllegals: true })}\n`;
}
console.log(consoleOutput);
return this.createSuccessResponse(request.id, resultMessage);
}
catch (error) {
return this.handleFileReadError(request.id, basePath, error);
}
}
async handleCodeReferenceSearch(request) {
const validationError = this.validateParams(request.params, ['symbol']);
if (validationError) {
return this.createErrorResponse(request.id, -32602, validationError);
}
const { symbol, searchType = 'all', basePath = '.', includeComments = false, fuzzyMatch = true, maxResults = 50, includeDependencies = true, includeReverseDependencies = true, analyzeImports = true, depthLevel = 2 } = request.params;
const startPath = path.resolve(basePath);
try {
const results = await this.searchCodeReferences(startPath, symbol, searchType, undefined, // Auto-detect language
includeComments, fuzzyMatch, maxResults);
if (results.length === 0) {
return this.createSuccessResponse(request.id, `🟡 **No code references found for "${symbol}"**\n\nYour search did not match any symbols.\n\nPossible solutions:\n- Check symbol spelling\n- Try enabling fuzzy matching (fuzzyMatch: true)\n- Try a different search type\n- Expand the search path\n- Check if the symbol exists in the codebase`);
}
// Get dependency analysis
let dependencyAnalysis = '';
if (includeDependencies || includeReverseDependencies || analyzeImports) {
const uniqueFiles = [...new Set(results.map(r => r.file))];
for (const file of uniqueFiles.slice(0, 5)) { // Analyze up to 5 most relevant files
const analysis = await this.analyzeDependencies(path.resolve(startPath, file), startPath, includeDependencies, includeReverseDependencies, analyzeImports, depthLevel);
if (analysis) {
dependencyAnalysis += `\n## 📁 **Dependency Analysis for \`${file}\`**\n\n${analysis}\n`;
}
}
}
let resultMessage = `✅ **Code reference search results for "${symbol}"**\n\nFound ${results.length} matches${results.length >= maxResults ? ` (limited to ${maxResults})` : ''}:\n\n`;
// Group results by file for better organization
const resultsByFile = new Map();
for (const result of results) {
if (!resultsByFile.has(result.file)) {
resultsByFile.set(result.file, []);
}
resultsByFile.get(result.file).push(result);
}
for (const [file, fileResults] of resultsByFile.entries()) {
const language = this.getLanguageFromExtension(file);
resultMessage += `### 📄 **File:** \`${file}\`\n\n`;
for (const result of fileResults) {
resultMessage += `**Type:** ${result.type} | **Line:** ${result.line}`;
if (result.confidence && result.confidence < 1.0) {
resultMessage += ` | **Match:** ${Math.round(result.confidence * 100)}%`;
}
resultMessage += `\n\`\`\`${language}\n${result.line}: ${result.content.trim()}\n\`\`\`\n\n`;
}
}
// Add dependency analysis
if (dependencyAnalysis) {
resultMessage += `\n---\n\n# 🔗 **Dependency Analysis**\n${dependencyAnalysis}`;
}
console.log(`✅ Code reference search for "${symbol}"\nFound ${results.length} matches across ${resultsByFile.size} files`);
return this.createSuccessResponse(request.id, resultMessage);
}
catch (error) {
return this.handleFileReadError(request.id, basePath, error);
}
}
async searchCodeReferences(startPath, symbol, searchType, language, includeComments = false, fuzzyMatch = true, maxResults = 50) {
const results = [];
const binaryExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.zip', '.pdf', '.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.ico'];
// Performance optimizations: limit search scope for large projects
const MAX_FILES_TO_SEARCH = 500; // 限制搜索文件数量
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB 文件大小限制
let filesSearched = 0;
// Language-specific file extensions with enhanced coverage
const languageExtensions = {
typescript: ['.ts', '.tsx', '.d.ts'],
javascript: ['.js', '.jsx', '.mjs', '.cjs', '.es6'],
python: ['.py', '.pyw', '.pyi', '.pyx'],
java: ['.java', '.scala', '.kotlin', '.kt', '.kts', '.groovy'],
csharp: ['.cs', '.csx', '.vb', '.fs', '.fsx'],
go: ['.go'],
rust: ['.rs', '.rlib'],
cpp: ['.cpp', '.cxx', '.cc', '.c', '.h', '.hpp', '.hxx'],
php: ['.php', '.phtml', '.php3', '.php4', '.php5', '.phps'],
ruby: ['.rb', '.rake', '.gemspec'],
swift: ['.swift'],
kotlin: ['.kt', '.kts'],
dart: ['.dart'],
elixir: ['.ex', '.exs'],
erlang: ['.erl', '.hrl'],
haskell: ['.hs', '.lhs'],
clojure: ['.clj', '.cljs', '.cljc', '.edn'],
lua: ['.lua'],
perl: ['.pl', '.pm', '.t'],
r: ['.r', '.R'],
matlab: ['.m'],
shell: ['.sh', '.bash', '.zsh', '.fish'],
powershell: ['.ps1', '.psm1', '.psd1'],
// Web frameworks and templates
vue: ['.vue'],
svelte: ['.svelte'],
angular: ['.ts', '.js', '.html'],
react: ['.jsx', '.tsx'],
// Configuration and data formats
yaml: ['.yaml', '.yml'],
json: ['.json', '.jsonc'],
xml: ['.xml', '.xsd', '.xsl', '.xslt'],
toml: ['.toml'],
ini: ['.ini', '.cfg', '.conf'],
// Database
sql: ['.sql', '.plsql', '.mysql', '.pgsql'],
// Mobile development
objective_c: ['.m', '.mm', '.h'],
flutter: ['.dart'],
};
// Auto-detect common programming language files if no language specified
const allProgrammingExtensions = Object.values(languageExtensions).flat();
const allFiles = this.findFilesRecursive(startPath, (filePath) => {
const ext = path.extname(filePath).toLowerCase();
if (binaryExtensions.includes(ext))
return false;
if (language) {
const validExts = languageExtensions[language.toLowerCase()];
return validExts ? validExts.includes(ext) : true;
}
// Auto-detect: include common programming language files
return allProgrammingExtensions.includes(ext) ||
['.json', '.yaml', '.yml', '.xml', '.html', '.css', '.scss', '.less', '.md'].includes(ext);
}, (dirPath) => !['node_modules', '.git', 'dist', 'build', 'coverage', '.next', 'target'].some(excluded => path.basename(dirPath) === excluded));
// Performance optimization: prioritize files and limit search scope
const prioritizedFiles = this.prioritizeFilesForSearch(allFiles, symbol, startPath);
const filesToSearch = prioritizedFiles.slice(0, MAX_FILES_TO_SEARCH);
// Concurrent processing with worker pool
const CONCURRENCY_LIMIT = Math.min(8, Math.max(2, Math.floor(os.cpus().length / 2))); // 使用 CPU 核心数的一半,最小2最大8
const chunks = this.chunkArray(filesToSearch, Math.ceil(filesToSearch.length / CONCURRENCY_LIMIT));
console.log(`🚀 Starting concurrent search: ${filesToSearch.length} files in ${chunks.length} chunks (concurrency: ${CONCURRENCY_LIMIT})`);
// Process chunks concurrently
const chunkPromises = chunks.map((chunk, chunkIndex) => this.searchFilesChunk(chunk, symbol, searchType, includeComments, fuzzyMatch, maxResults, chunkIndex));
try {
const chunkResults = await Promise.all(chunkPromises);
// Merge and sort results from all chunks
const allResults = [];
let totalFilesSearched = 0;
for (const chunkResult of chunkResults) {
allResults.push(...chunkResult.results);
totalFilesSearched += chunkResult.filesSearched;
}
// Sort by confidence and limit results
const sortedResults = allResults
.sort((a, b) => (b.confidence || 1) - (a.confidence || 1) ||
a.file.localeCompare(b.file) ||
a.line - b.line)
.slice(0, maxResults);
console.log(`📊 Concurrent search completed: Found ${sortedResults.length} matches in ${totalFilesSearched} files`);
return sortedResults;
}
catch (error) {
console.error('❌ Concurrent search failed, falling back to sequential search:', error);
return this.searchFilesSequential(filesToSearch, symbol, searchType, includeComments, fuzzyMatch, maxResults);
}
}
// 新增:数组分块方法
chunkArray(array, chunkSize) {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
// 新增:并发搜索文件块
async searchFilesChunk(files, symbol, searchType, includeComments, fuzzyMatch, maxResults, chunkIndex) {
const results = [];
let filesSearched = 0;
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
for (const file of files) {
// Early termination if we have enough results globally
if (results.length >= Math.ceil(maxResults / 4)) { // 每个块最多返回总数的1/4
break;
}
try {
const stats = fs.statSync(file);
if (stats.size > MAX_FILE_SIZE)
continue;
const content = fs.readFileSync(file, 'utf-8');
const lines = content.split('\n');
const relativePath = path.relative(process.cwd(), file);
filesSearched++;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;
if (!includeComments && this.isCommentLine(line, file)) {
continue;
}
const matches = this.findSymbolMatches(line, symbol, searchType, fuzzyMatch);
for (const match of matches) {
results.push({
file: relativePath,
line: lineNum,
content: line,
type: match.type,
confidence: match.confidence
});
if (results.length >= Math.ceil(maxResults / 2)) { // 单个块的硬限制
return { results, filesSearched };
}
}
}
}
catch (readError) {
// Skip files that can't be read
continue;
}
}
return { results, filesSearched };
}
// 新增:回退的顺序搜索方法
async searchFilesSequential(files, symbol, searchType, includeComments, fuzzyMatch, maxResults) {
const results = [];
let filesSearched = 0;
const MAX_FILE_SIZE = 2 * 1024 * 1024;
for (const file of files) {
if (results.length >= maxResults)
break;
try {
const stats = fs.statSync(file);
if (stats.size > MAX_FILE_SIZE)
continue;
const content = fs.readFileSync(file, 'utf-8');
const lines = content.split('\n');
const relativePath = path.relative(process.cwd(), file);
filesSearched++;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;
if (!includeComments && this.isCommentLine(line, file)) {
continue;
}
const matches = this.findSymbolMatches(line, symbol, searchType, fuzzyMatch);
for (const match of matches) {
results.push({
file: relativePath,
line: lineNum,
content: line,
type: match.type,
confidence: match.confidence
});
if (results.length >= maxResults) {
console.log(`📊 Sequential search completed: Found ${results.length} matches in ${filesSearched} files`);
return results.sort((a, b) => (b.confidence || 1) - (a.confidence || 1) ||
a.file.localeCompare(b.file) ||
a.line - b.line);
}
}
}
}
catch (readError) {
continue;
}
}
console.log(`📊 Sequential search completed: Found ${results.length} matches in ${filesSearched} files`);
return results.sort((a, b) => (b.confidence || 1) - (a.confidence || 1) ||
a.file.localeCompare(b.file) ||
a.line - b.line);
}
// 新增:文件优先级排序方法
prioritizeFilesForSearch(files, symbol, basePath) {
const currentDir = process.cwd();
return files.sort((a, b) => {
let scoreA = 0;
let scoreB = 0;
const filenameA = path.basename(a).toLowerCase();
const filenameB = path.basename(b).toLowerCase();
const symbolLower = symbol.toLowerCase();
// 1. 优先级:文件名包含搜索符号
if (filenameA.includes(symbolLower))
scoreA += 100;
if (filenameB.includes(symbolLower))
scoreB += 100;
// 2. 优先级:离当前目录更近的文件
const depthA = path.relative(currentDir, a).split(path.sep).length;
const depthB = path.relative(currentDir, b).split(path.sep).length;
scoreA += Math.max(0, 20 - depthA);
scoreB += Math.max(0, 20 - depthB);
// 3. 优先级:常见的重要文件类型
const importantExts = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs'];
const extA = path.extname(a).toLowerCase();
const extB = path.extname(b).toLowerCase();
if (importantExts.includes(extA))
scoreA += 10;
if (importantExts.includes(extB))
scoreB += 10;
// 4. 优先级:非测试文件
if (!filenameA.includes('test') && !filenameA.includes('spec'))
scoreA += 5;
if (!filenameB.includes('test') && !filenameB.includes('spec'))
scoreB += 5;
// 5. 优先级:src 目录中的文件
if (a.includes('/src/') || a.includes('\\src\\'))
scoreA += 5;
if (b.includes('/src/') || b.includes('\\src\\'))
scoreB += 5;
return scoreB - scoreA;
});
}
findSymbolMatches(line, symbol, searchType, fuzzyMatch = true) {
const matches = [];
const escapedSymbol = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Helper function to calculate fuzzy match confidence using Jaro-Winkler similarity
const calculateFuzzyConfidence = (found, target) => {
if (found === target)
return 1.0;
if (found.toLowerCase() === target.toLowerCase())
return 0.95;
const foundLower = found.toLowerCase();
const targetLower = target.toLowerCase();
// Exact substring match
if (foundLower.includes(targetLower) || targetLower.includes(foundLower)) {
const longer = foundLower.length > targetLower.length ? foundLower : targetLower;
const shorter = foundLower.length <= targetLower.length ? foundLower : targetLower;
return 0.8 + (shorter.length / longer.length) * 0.15;
}
// Improved similarity calculation using common subsequence
const jaro = this.calculateJaroSimilarity(foundLower, targetLower);
return jaro > 0.6 ? jaro : 0;
};
// Enhanced patterns with more comprehensive coverage
const patterns = {
definition: [
// JavaScript/TypeScript definitions
new RegExp(`(?:export\\s+)?(?:async\\s+)?(?:function|const|let|var|class|interface|type|enum)\\s+(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
new RegExp(`(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\s*[:=]\\s*(?:async\\s+)?(?:function|\\([^)]*\\)\\s*=>|{|class)`, 'gi'),
new RegExp(`(?:export\\s+)?(?:default\\s+)?(?:class|function)\\s+(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
// React component definitions
new RegExp(`(?:export\\s+)?(?:default\\s+)?(?:const|let|var)\\s+(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\s*[:=]\\s*(?:React\\.)?(?:FC|FunctionComponent|Component)`, 'gi'),
new RegExp(`(?:export\\s+)?(?:default\\s+)?(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\s*=\\s*(?:React\\.)?(?:memo|forwardRef|lazy)\\(`, 'gi'),
// Python definitions
new RegExp(`(?:async\\s+)?def\\s+(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
new RegExp(`class\\s+(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
// Java/C# definitions
new RegExp(`(?:public|private|protected|static|abstract|final)*\\s*(?:class|interface|enum)\\s+(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
new RegExp(`(?:public|private|protected|static|abstract|final)*\\s*(?:[\\w<>\\[\\]]+\\s+)?(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\s*\\([^)]*\\)\\s*\\{`, 'gi'),
// Go definitions
new RegExp(`(?:func|type)\\s+(?:\\([^)]*\\)\\s+)?(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
// Rust definitions
new RegExp(`(?:pub\\s+)?(?:fn|struct|enum|trait|impl)\\s+(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
],
references: [
// Function/method calls
new RegExp(`\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\s*\\(`, 'gi'),
new RegExp(`\\.(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\s*\\(`, 'gi'),
// Property access
new RegExp(`\\.(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b(?!\\s*\\()`, 'gi'),
new RegExp(`\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\s*\\.`, 'gi'),
// Bracket notation
new RegExp(`\\[(["'])(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\1\\]`, 'gi'),
// Destructuring
new RegExp(`\\{[^}]*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b[^}]*\\}`, 'gi'),
// JSX/React usage
new RegExp(`<(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)(?:\\s|>|/>)`, 'gi'),
// Hook dependencies
new RegExp(`\\[([^\\]]*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b[^\\]]*)\\]`, 'gi'),
],
usage: [
// Any usage with word boundaries
new RegExp(`\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
// String literals (for dynamic references)
new RegExp(`(["'\`])(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\1`, 'gi'),
],
dependencies: [
// Import/require statements
new RegExp(`import.*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b.*from`, 'gi'),
new RegExp(`import\\s*\\{[^}]*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b[^}]*\\}`, 'gi'),
new RegExp(`require\\s*\\([^)]*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b[^)]*\\)`, 'gi'),
// Dynamic imports
new RegExp(`import\\s*\\([^)]*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b[^)]*\\)`, 'gi'),
],
'reverse-dependencies': [
// Export statements
new RegExp(`export.*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
new RegExp(`export\\s*\\{[^}]*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b[^}]*\\}`, 'gi'),
new RegExp(`module\\.exports.*\\b(\\w*${fuzzyMatch ? '\\w*' : ''}${escapedSymbol}\\w*)\\b`, 'gi'),
]
};
// Handle exact matching when fuzzy is disabled
if (!fuzzyMatch) {
const exactPatterns = {
definition: [
new RegExp(`(?:export\\s+)?(?:async\\s+)?(?:function|const|let|var|class|interface|type|enum)\\s+${escapedSymbol}\\b`, 'gi'),
new RegExp(`${escapedSymbol}\\s*[:=]\\s*(?:async\\s+)?(?:function|\\(|{|class)`, 'gi'),
new RegExp(`(?:async\\s+)?def\\s+${escapedSymbol}\\b`, 'gi'), // Python