snow-ai 0.2.18 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +87 -30
- package/dist/cli.js +25 -0
- package/dist/hooks/useCommandHandler.js +24 -16
- package/dist/hooks/useConversation.js +87 -136
- package/dist/hooks/useKeyboardInput.js +9 -0
- package/dist/mcp/aceCodeSearch.d.ts +314 -0
- package/dist/mcp/aceCodeSearch.js +822 -0
- package/dist/mcp/filesystem.d.ts +63 -79
- package/dist/mcp/filesystem.js +321 -165
- package/dist/ui/components/DiffViewer.d.ts +2 -1
- package/dist/ui/components/DiffViewer.js +33 -16
- package/dist/ui/components/MarkdownRenderer.js +65 -16
- package/dist/ui/components/ToolConfirmation.js +9 -0
- package/dist/ui/components/ToolResultPreview.js +146 -24
- package/dist/ui/pages/ChatScreen.js +54 -7
- package/dist/utils/commandExecutor.d.ts +1 -0
- package/dist/utils/commands/ide.js +12 -12
- package/dist/utils/mcpToolsManager.js +54 -9
- package/dist/utils/sessionConverter.js +8 -2
- package/dist/utils/vscodeConnection.d.ts +1 -9
- package/dist/utils/vscodeConnection.js +24 -68
- package/package.json +1 -1
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Language-specific parsers configuration
|
|
5
|
+
*/
|
|
6
|
+
const LANGUAGE_CONFIG = {
|
|
7
|
+
typescript: {
|
|
8
|
+
extensions: ['.ts', '.tsx'],
|
|
9
|
+
parser: 'typescript',
|
|
10
|
+
symbolPatterns: {
|
|
11
|
+
function: /(?:export\s+)?(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/,
|
|
12
|
+
class: /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
13
|
+
variable: /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/,
|
|
14
|
+
import: /import\s+(?:{[^}]+}|\w+)\s+from\s+['"]([^'"]+)['"]/,
|
|
15
|
+
export: /export\s+(?:default\s+)?(?:class|function|const|let|var|interface|type|enum)\s+(\w+)/,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
javascript: {
|
|
19
|
+
extensions: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
20
|
+
parser: 'javascript',
|
|
21
|
+
symbolPatterns: {
|
|
22
|
+
function: /(?:export\s+)?(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/,
|
|
23
|
+
class: /(?:export\s+)?class\s+(\w+)/,
|
|
24
|
+
variable: /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/,
|
|
25
|
+
import: /import\s+(?:{[^}]+}|\w+)\s+from\s+['"]([^'"]+)['"]/,
|
|
26
|
+
export: /export\s+(?:default\s+)?(?:class|function|const|let|var)\s+(\w+)/,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
python: {
|
|
30
|
+
extensions: ['.py', '.pyx', '.pyi'],
|
|
31
|
+
parser: 'python',
|
|
32
|
+
symbolPatterns: {
|
|
33
|
+
function: /def\s+(\w+)\s*\(/,
|
|
34
|
+
class: /class\s+(\w+)\s*[(:]/,
|
|
35
|
+
variable: /(\w+)\s*=\s*[^=]/,
|
|
36
|
+
import: /(?:from\s+[\w.]+\s+)?import\s+([\w, ]+)/,
|
|
37
|
+
export: /^(\w+)\s*=\s*/, // Python doesn't have explicit exports
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
go: {
|
|
41
|
+
extensions: ['.go'],
|
|
42
|
+
parser: 'go',
|
|
43
|
+
symbolPatterns: {
|
|
44
|
+
function: /func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/,
|
|
45
|
+
class: /type\s+(\w+)\s+struct/,
|
|
46
|
+
variable: /(?:var|const)\s+(\w+)\s+/,
|
|
47
|
+
import: /import\s+(?:"([^"]+)"|[(]([^)]+)[)])/,
|
|
48
|
+
export: /^(?:func|type|var|const)\s+([A-Z]\w+)/, // Go exports start with capital letter
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
rust: {
|
|
52
|
+
extensions: ['.rs'],
|
|
53
|
+
parser: 'rust',
|
|
54
|
+
symbolPatterns: {
|
|
55
|
+
function: /(?:pub\s+)?(?:async\s+)?fn\s+(\w+)\s*[<(]/,
|
|
56
|
+
class: /(?:pub\s+)?struct\s+(\w+)|(?:pub\s+)?enum\s+(\w+)|(?:pub\s+)?trait\s+(\w+)/,
|
|
57
|
+
variable: /(?:pub\s+)?(?:static|const)\s+(\w+)\s*:/,
|
|
58
|
+
import: /use\s+([^;]+);/,
|
|
59
|
+
export: /pub\s+(?:fn|struct|enum|trait|const|static)\s+(\w+)/,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
java: {
|
|
63
|
+
extensions: ['.java'],
|
|
64
|
+
parser: 'java',
|
|
65
|
+
symbolPatterns: {
|
|
66
|
+
function: /(?:public|private|protected|static|\s)+[\w<>\[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/,
|
|
67
|
+
class: /(?:public|private|protected)?\s*(?:abstract|final)?\s*class\s+(\w+)/,
|
|
68
|
+
variable: /(?:public|private|protected|static|final|\s)+[\w<>\[\]]+\s+(\w+)\s*[=;]/,
|
|
69
|
+
import: /import\s+([\w.]+);/,
|
|
70
|
+
export: /public\s+(?:class|interface|enum)\s+(\w+)/,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
csharp: {
|
|
74
|
+
extensions: ['.cs'],
|
|
75
|
+
parser: 'csharp',
|
|
76
|
+
symbolPatterns: {
|
|
77
|
+
function: /(?:public|private|protected|internal|static|\s)+[\w<>\[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/,
|
|
78
|
+
class: /(?:public|private|protected|internal)?\s*(?:abstract|sealed|static)?\s*class\s+(\w+)/,
|
|
79
|
+
variable: /(?:public|private|protected|internal|static|readonly|\s)+[\w<>\[\]]+\s+(\w+)\s*[=;]/,
|
|
80
|
+
import: /using\s+([\w.]+);/,
|
|
81
|
+
export: /public\s+(?:class|interface|enum|struct)\s+(\w+)/,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export class ACECodeSearchService {
|
|
86
|
+
constructor(basePath = process.cwd()) {
|
|
87
|
+
Object.defineProperty(this, "basePath", {
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: true,
|
|
90
|
+
writable: true,
|
|
91
|
+
value: void 0
|
|
92
|
+
});
|
|
93
|
+
Object.defineProperty(this, "indexCache", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
writable: true,
|
|
97
|
+
value: new Map()
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, "lastIndexTime", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
writable: true,
|
|
103
|
+
value: 0
|
|
104
|
+
});
|
|
105
|
+
Object.defineProperty(this, "INDEX_CACHE_DURATION", {
|
|
106
|
+
enumerable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
writable: true,
|
|
109
|
+
value: 60000
|
|
110
|
+
}); // 1 minute
|
|
111
|
+
this.basePath = path.resolve(basePath);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Detect programming language from file extension
|
|
115
|
+
*/
|
|
116
|
+
detectLanguage(filePath) {
|
|
117
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
118
|
+
for (const [lang, config] of Object.entries(LANGUAGE_CONFIG)) {
|
|
119
|
+
if (config.extensions.includes(ext)) {
|
|
120
|
+
return lang;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parse file content to extract code symbols using regex patterns
|
|
127
|
+
*/
|
|
128
|
+
async parseFileSymbols(filePath, content) {
|
|
129
|
+
const symbols = [];
|
|
130
|
+
const language = this.detectLanguage(filePath);
|
|
131
|
+
if (!language || !LANGUAGE_CONFIG[language]) {
|
|
132
|
+
return symbols;
|
|
133
|
+
}
|
|
134
|
+
const config = LANGUAGE_CONFIG[language];
|
|
135
|
+
const lines = content.split('\n');
|
|
136
|
+
// Parse each line for symbols
|
|
137
|
+
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
const line = lines[i];
|
|
139
|
+
if (!line)
|
|
140
|
+
continue;
|
|
141
|
+
const lineNumber = i + 1;
|
|
142
|
+
// Extract functions
|
|
143
|
+
if (config.symbolPatterns.function) {
|
|
144
|
+
const match = line.match(config.symbolPatterns.function);
|
|
145
|
+
if (match) {
|
|
146
|
+
const name = match[1] || match[2] || match[3];
|
|
147
|
+
if (name) {
|
|
148
|
+
// Get function signature (current line + next few lines)
|
|
149
|
+
const contextLines = lines.slice(i, Math.min(i + 3, lines.length));
|
|
150
|
+
const signature = contextLines.join('\n').trim();
|
|
151
|
+
symbols.push({
|
|
152
|
+
name,
|
|
153
|
+
type: 'function',
|
|
154
|
+
filePath: path.relative(this.basePath, filePath),
|
|
155
|
+
line: lineNumber,
|
|
156
|
+
column: line.indexOf(name) + 1,
|
|
157
|
+
signature,
|
|
158
|
+
language,
|
|
159
|
+
context: this.getContext(lines, i, 2),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Extract classes
|
|
165
|
+
if (config.symbolPatterns.class) {
|
|
166
|
+
const match = line.match(config.symbolPatterns.class);
|
|
167
|
+
if (match) {
|
|
168
|
+
const name = match[1] || match[2] || match[3];
|
|
169
|
+
if (name) {
|
|
170
|
+
symbols.push({
|
|
171
|
+
name,
|
|
172
|
+
type: 'class',
|
|
173
|
+
filePath: path.relative(this.basePath, filePath),
|
|
174
|
+
line: lineNumber,
|
|
175
|
+
column: line.indexOf(name) + 1,
|
|
176
|
+
signature: line.trim(),
|
|
177
|
+
language,
|
|
178
|
+
context: this.getContext(lines, i, 2),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Extract variables
|
|
184
|
+
if (config.symbolPatterns.variable) {
|
|
185
|
+
const match = line.match(config.symbolPatterns.variable);
|
|
186
|
+
if (match) {
|
|
187
|
+
const name = match[1];
|
|
188
|
+
if (name) {
|
|
189
|
+
symbols.push({
|
|
190
|
+
name,
|
|
191
|
+
type: 'variable',
|
|
192
|
+
filePath: path.relative(this.basePath, filePath),
|
|
193
|
+
line: lineNumber,
|
|
194
|
+
column: line.indexOf(name) + 1,
|
|
195
|
+
signature: line.trim(),
|
|
196
|
+
language,
|
|
197
|
+
context: this.getContext(lines, i, 1),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Extract imports
|
|
203
|
+
if (config.symbolPatterns.import) {
|
|
204
|
+
const match = line.match(config.symbolPatterns.import);
|
|
205
|
+
if (match) {
|
|
206
|
+
const name = match[1] || match[2];
|
|
207
|
+
if (name) {
|
|
208
|
+
symbols.push({
|
|
209
|
+
name,
|
|
210
|
+
type: 'import',
|
|
211
|
+
filePath: path.relative(this.basePath, filePath),
|
|
212
|
+
line: lineNumber,
|
|
213
|
+
column: line.indexOf(name) + 1,
|
|
214
|
+
signature: line.trim(),
|
|
215
|
+
language,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Extract exports
|
|
221
|
+
if (config.symbolPatterns.export) {
|
|
222
|
+
const match = line.match(config.symbolPatterns.export);
|
|
223
|
+
if (match) {
|
|
224
|
+
const name = match[1];
|
|
225
|
+
if (name) {
|
|
226
|
+
symbols.push({
|
|
227
|
+
name,
|
|
228
|
+
type: 'export',
|
|
229
|
+
filePath: path.relative(this.basePath, filePath),
|
|
230
|
+
line: lineNumber,
|
|
231
|
+
column: line.indexOf(name) + 1,
|
|
232
|
+
signature: line.trim(),
|
|
233
|
+
language,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return symbols;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get context lines around a specific line
|
|
243
|
+
*/
|
|
244
|
+
getContext(lines, lineIndex, contextSize) {
|
|
245
|
+
const start = Math.max(0, lineIndex - contextSize);
|
|
246
|
+
const end = Math.min(lines.length, lineIndex + contextSize + 1);
|
|
247
|
+
return lines.slice(start, end).filter(l => l !== undefined).join('\n').trim();
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Build or refresh the code symbol index
|
|
251
|
+
*/
|
|
252
|
+
async buildIndex(forceRefresh = false) {
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
// Use cache if available and not expired
|
|
255
|
+
if (!forceRefresh && this.indexCache.size > 0 && (now - this.lastIndexTime) < this.INDEX_CACHE_DURATION) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.indexCache.clear();
|
|
259
|
+
const searchInDirectory = async (dirPath) => {
|
|
260
|
+
try {
|
|
261
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
264
|
+
if (entry.isDirectory()) {
|
|
265
|
+
// Skip common ignored directories
|
|
266
|
+
if (entry.name === 'node_modules' ||
|
|
267
|
+
entry.name === '.git' ||
|
|
268
|
+
entry.name === 'dist' ||
|
|
269
|
+
entry.name === 'build' ||
|
|
270
|
+
entry.name === '__pycache__' ||
|
|
271
|
+
entry.name === 'target' ||
|
|
272
|
+
entry.name.startsWith('.')) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
await searchInDirectory(fullPath);
|
|
276
|
+
}
|
|
277
|
+
else if (entry.isFile()) {
|
|
278
|
+
const language = this.detectLanguage(fullPath);
|
|
279
|
+
if (language) {
|
|
280
|
+
try {
|
|
281
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
282
|
+
const symbols = await this.parseFileSymbols(fullPath, content);
|
|
283
|
+
if (symbols.length > 0) {
|
|
284
|
+
this.indexCache.set(fullPath, symbols);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
// Skip files that cannot be read
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
// Skip directories that cannot be accessed
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
await searchInDirectory(this.basePath);
|
|
299
|
+
this.lastIndexTime = now;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Search for symbols by name with fuzzy matching
|
|
303
|
+
*/
|
|
304
|
+
async searchSymbols(query, symbolType, language, maxResults = 100) {
|
|
305
|
+
const startTime = Date.now();
|
|
306
|
+
await this.buildIndex();
|
|
307
|
+
const symbols = [];
|
|
308
|
+
const queryLower = query.toLowerCase();
|
|
309
|
+
// Fuzzy match scoring
|
|
310
|
+
const calculateScore = (symbolName) => {
|
|
311
|
+
const nameLower = symbolName.toLowerCase();
|
|
312
|
+
// Exact match
|
|
313
|
+
if (nameLower === queryLower)
|
|
314
|
+
return 100;
|
|
315
|
+
// Starts with
|
|
316
|
+
if (nameLower.startsWith(queryLower))
|
|
317
|
+
return 80;
|
|
318
|
+
// Contains
|
|
319
|
+
if (nameLower.includes(queryLower))
|
|
320
|
+
return 60;
|
|
321
|
+
// Camel case match (e.g., "gfc" matches "getFileContent")
|
|
322
|
+
const camelCaseMatch = symbolName.split(/(?=[A-Z])/).map(s => s[0]?.toLowerCase() || '').join('');
|
|
323
|
+
if (camelCaseMatch.includes(queryLower))
|
|
324
|
+
return 40;
|
|
325
|
+
// Fuzzy match
|
|
326
|
+
let score = 0;
|
|
327
|
+
let queryIndex = 0;
|
|
328
|
+
for (let i = 0; i < nameLower.length && queryIndex < queryLower.length; i++) {
|
|
329
|
+
if (nameLower[i] === queryLower[queryIndex]) {
|
|
330
|
+
score += 20;
|
|
331
|
+
queryIndex++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (queryIndex === queryLower.length)
|
|
335
|
+
return score;
|
|
336
|
+
return 0;
|
|
337
|
+
};
|
|
338
|
+
// Search through all indexed symbols
|
|
339
|
+
for (const fileSymbols of this.indexCache.values()) {
|
|
340
|
+
for (const symbol of fileSymbols) {
|
|
341
|
+
// Apply filters
|
|
342
|
+
if (symbolType && symbol.type !== symbolType)
|
|
343
|
+
continue;
|
|
344
|
+
if (language && symbol.language !== language)
|
|
345
|
+
continue;
|
|
346
|
+
const score = calculateScore(symbol.name);
|
|
347
|
+
if (score > 0) {
|
|
348
|
+
symbols.push({ ...symbol });
|
|
349
|
+
}
|
|
350
|
+
if (symbols.length >= maxResults)
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
if (symbols.length >= maxResults)
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
// Sort by relevance
|
|
357
|
+
symbols.sort((a, b) => calculateScore(b.name) - calculateScore(a.name));
|
|
358
|
+
const searchTime = Date.now() - startTime;
|
|
359
|
+
return {
|
|
360
|
+
query,
|
|
361
|
+
symbols,
|
|
362
|
+
references: [], // References would be populated by findReferences
|
|
363
|
+
totalResults: symbols.length,
|
|
364
|
+
searchTime,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Find all references to a symbol
|
|
369
|
+
*/
|
|
370
|
+
async findReferences(symbolName, maxResults = 100) {
|
|
371
|
+
const references = [];
|
|
372
|
+
const searchInDirectory = async (dirPath) => {
|
|
373
|
+
try {
|
|
374
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
if (references.length >= maxResults)
|
|
377
|
+
break;
|
|
378
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
379
|
+
if (entry.isDirectory()) {
|
|
380
|
+
if (entry.name === 'node_modules' ||
|
|
381
|
+
entry.name === '.git' ||
|
|
382
|
+
entry.name === 'dist' ||
|
|
383
|
+
entry.name === 'build' ||
|
|
384
|
+
entry.name.startsWith('.')) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
await searchInDirectory(fullPath);
|
|
388
|
+
}
|
|
389
|
+
else if (entry.isFile()) {
|
|
390
|
+
const language = this.detectLanguage(fullPath);
|
|
391
|
+
if (language) {
|
|
392
|
+
try {
|
|
393
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
394
|
+
const lines = content.split('\n');
|
|
395
|
+
// Search for symbol usage
|
|
396
|
+
for (let i = 0; i < lines.length; i++) {
|
|
397
|
+
const line = lines[i];
|
|
398
|
+
if (!line)
|
|
399
|
+
continue;
|
|
400
|
+
const regex = new RegExp(`\\b${symbolName}\\b`, 'g');
|
|
401
|
+
let match;
|
|
402
|
+
while ((match = regex.exec(line)) !== null) {
|
|
403
|
+
if (references.length >= maxResults)
|
|
404
|
+
break;
|
|
405
|
+
// Determine reference type
|
|
406
|
+
let referenceType = 'usage';
|
|
407
|
+
if (line.includes('import') && line.includes(symbolName)) {
|
|
408
|
+
referenceType = 'import';
|
|
409
|
+
}
|
|
410
|
+
else if (line.match(new RegExp(`(?:function|class|const|let|var)\\s+${symbolName}`))) {
|
|
411
|
+
referenceType = 'definition';
|
|
412
|
+
}
|
|
413
|
+
else if (line.includes(':') && line.includes(symbolName)) {
|
|
414
|
+
referenceType = 'type';
|
|
415
|
+
}
|
|
416
|
+
references.push({
|
|
417
|
+
symbol: symbolName,
|
|
418
|
+
filePath: path.relative(this.basePath, fullPath),
|
|
419
|
+
line: i + 1,
|
|
420
|
+
column: match.index + 1,
|
|
421
|
+
context: this.getContext(lines, i, 1),
|
|
422
|
+
referenceType,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
// Skip files that cannot be read
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
// Skip directories that cannot be accessed
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
await searchInDirectory(this.basePath);
|
|
439
|
+
return references;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Find symbol definition (go to definition)
|
|
443
|
+
*/
|
|
444
|
+
async findDefinition(symbolName, contextFile) {
|
|
445
|
+
await this.buildIndex();
|
|
446
|
+
// Search in the same file first if context is provided
|
|
447
|
+
if (contextFile) {
|
|
448
|
+
const fullPath = path.resolve(this.basePath, contextFile);
|
|
449
|
+
const fileSymbols = this.indexCache.get(fullPath);
|
|
450
|
+
if (fileSymbols) {
|
|
451
|
+
const symbol = fileSymbols.find(s => s.name === symbolName && (s.type === 'function' || s.type === 'class' || s.type === 'variable'));
|
|
452
|
+
if (symbol)
|
|
453
|
+
return symbol;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Search in all files
|
|
457
|
+
for (const fileSymbols of this.indexCache.values()) {
|
|
458
|
+
const symbol = fileSymbols.find(s => s.name === symbolName && (s.type === 'function' || s.type === 'class' || s.type === 'variable'));
|
|
459
|
+
if (symbol)
|
|
460
|
+
return symbol;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Fast text search using built-in Node.js (no external dependencies)
|
|
466
|
+
* Searches for text patterns across files with glob filtering
|
|
467
|
+
*/
|
|
468
|
+
async textSearch(pattern, fileGlob, isRegex = false, maxResults = 100) {
|
|
469
|
+
const results = [];
|
|
470
|
+
// Compile search pattern
|
|
471
|
+
let searchRegex;
|
|
472
|
+
try {
|
|
473
|
+
if (isRegex) {
|
|
474
|
+
searchRegex = new RegExp(pattern, 'gi');
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
// Escape special regex characters for literal search
|
|
478
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
479
|
+
searchRegex = new RegExp(escaped, 'gi');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
throw new Error(`Invalid regex pattern: ${pattern}`);
|
|
484
|
+
}
|
|
485
|
+
// Parse glob pattern if provided
|
|
486
|
+
const globRegex = fileGlob ? this.globToRegex(fileGlob) : null;
|
|
487
|
+
// Search recursively
|
|
488
|
+
const searchInDirectory = async (dirPath) => {
|
|
489
|
+
if (results.length >= maxResults)
|
|
490
|
+
return;
|
|
491
|
+
try {
|
|
492
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
493
|
+
for (const entry of entries) {
|
|
494
|
+
if (results.length >= maxResults)
|
|
495
|
+
break;
|
|
496
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
497
|
+
if (entry.isDirectory()) {
|
|
498
|
+
// Skip ignored directories
|
|
499
|
+
if (entry.name === 'node_modules' ||
|
|
500
|
+
entry.name === '.git' ||
|
|
501
|
+
entry.name === 'dist' ||
|
|
502
|
+
entry.name === 'build' ||
|
|
503
|
+
entry.name === '__pycache__' ||
|
|
504
|
+
entry.name === 'target' ||
|
|
505
|
+
entry.name === '.next' ||
|
|
506
|
+
entry.name === '.nuxt' ||
|
|
507
|
+
entry.name === 'coverage' ||
|
|
508
|
+
entry.name.startsWith('.')) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
await searchInDirectory(fullPath);
|
|
512
|
+
}
|
|
513
|
+
else if (entry.isFile()) {
|
|
514
|
+
// Filter by glob if specified
|
|
515
|
+
if (globRegex && !globRegex.test(fullPath)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
// Skip binary files
|
|
519
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
520
|
+
const binaryExts = [
|
|
521
|
+
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg',
|
|
522
|
+
'.pdf', '.zip', '.tar', '.gz', '.rar', '.7z',
|
|
523
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
524
|
+
'.mp3', '.mp4', '.avi', '.mov',
|
|
525
|
+
'.woff', '.woff2', '.ttf', '.eot',
|
|
526
|
+
'.class', '.jar', '.war',
|
|
527
|
+
'.o', '.a', '.lib'
|
|
528
|
+
];
|
|
529
|
+
if (binaryExts.includes(ext)) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
534
|
+
const lines = content.split('\n');
|
|
535
|
+
for (let i = 0; i < lines.length; i++) {
|
|
536
|
+
if (results.length >= maxResults)
|
|
537
|
+
break;
|
|
538
|
+
const line = lines[i];
|
|
539
|
+
if (!line)
|
|
540
|
+
continue;
|
|
541
|
+
// Reset regex for each line
|
|
542
|
+
searchRegex.lastIndex = 0;
|
|
543
|
+
const match = searchRegex.exec(line);
|
|
544
|
+
if (match) {
|
|
545
|
+
results.push({
|
|
546
|
+
filePath: path.relative(this.basePath, fullPath),
|
|
547
|
+
line: i + 1,
|
|
548
|
+
column: match.index + 1,
|
|
549
|
+
content: line.trim(),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
// Skip files that cannot be read (binary, permissions, etc.)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
// Skip directories that cannot be accessed
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
await searchInDirectory(this.basePath);
|
|
565
|
+
return results;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Convert glob pattern to RegExp
|
|
569
|
+
* Supports: *, **, ?, [abc], {js,ts}
|
|
570
|
+
*/
|
|
571
|
+
globToRegex(glob) {
|
|
572
|
+
// Escape special regex characters except glob wildcards
|
|
573
|
+
let pattern = glob
|
|
574
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
575
|
+
.replace(/\*\*/g, '<<<DOUBLESTAR>>>') // Temporarily replace **
|
|
576
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
577
|
+
.replace(/<<<DOUBLESTAR>>>/g, '.*') // ** matches everything
|
|
578
|
+
.replace(/\?/g, '[^/]'); // ? matches single char except /
|
|
579
|
+
// Handle {js,ts} alternatives
|
|
580
|
+
pattern = pattern.replace(/\\\{([^}]+)\\\}/g, (_, alternatives) => {
|
|
581
|
+
return '(' + alternatives.split(',').join('|') + ')';
|
|
582
|
+
});
|
|
583
|
+
// Handle [abc] character classes (already valid regex)
|
|
584
|
+
pattern = pattern.replace(/\\\[([^\]]+)\\\]/g, '[$1]');
|
|
585
|
+
return new RegExp(pattern, 'i');
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Get code outline for a file (all symbols in the file)
|
|
589
|
+
*/
|
|
590
|
+
async getFileOutline(filePath) {
|
|
591
|
+
const fullPath = path.resolve(this.basePath, filePath);
|
|
592
|
+
try {
|
|
593
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
594
|
+
return await this.parseFileSymbols(fullPath, content);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
throw new Error(`Failed to get outline for ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Search with language-specific context (cross-reference search)
|
|
602
|
+
*/
|
|
603
|
+
async semanticSearch(query, searchType = 'all', language, maxResults = 50) {
|
|
604
|
+
const startTime = Date.now();
|
|
605
|
+
// Get symbol search results
|
|
606
|
+
const symbolResults = await this.searchSymbols(query, undefined, language, maxResults);
|
|
607
|
+
// Get reference results if needed
|
|
608
|
+
let references = [];
|
|
609
|
+
if (searchType === 'usage' || searchType === 'all') {
|
|
610
|
+
// Find references for the top matching symbols
|
|
611
|
+
const topSymbols = symbolResults.symbols.slice(0, 5);
|
|
612
|
+
for (const symbol of topSymbols) {
|
|
613
|
+
const symbolRefs = await this.findReferences(symbol.name, maxResults);
|
|
614
|
+
references.push(...symbolRefs);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Filter results based on search type
|
|
618
|
+
let filteredSymbols = symbolResults.symbols;
|
|
619
|
+
if (searchType === 'definition') {
|
|
620
|
+
filteredSymbols = symbolResults.symbols.filter(s => s.type === 'function' || s.type === 'class' || s.type === 'interface');
|
|
621
|
+
}
|
|
622
|
+
else if (searchType === 'usage') {
|
|
623
|
+
filteredSymbols = [];
|
|
624
|
+
}
|
|
625
|
+
else if (searchType === 'implementation') {
|
|
626
|
+
filteredSymbols = symbolResults.symbols.filter(s => s.type === 'function' || s.type === 'method' || s.type === 'class');
|
|
627
|
+
}
|
|
628
|
+
const searchTime = Date.now() - startTime;
|
|
629
|
+
return {
|
|
630
|
+
query,
|
|
631
|
+
symbols: filteredSymbols,
|
|
632
|
+
references,
|
|
633
|
+
totalResults: filteredSymbols.length + references.length,
|
|
634
|
+
searchTime,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Clear the symbol index cache
|
|
639
|
+
*/
|
|
640
|
+
clearCache() {
|
|
641
|
+
this.indexCache.clear();
|
|
642
|
+
this.lastIndexTime = 0;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Get index statistics
|
|
646
|
+
*/
|
|
647
|
+
getIndexStats() {
|
|
648
|
+
let totalSymbols = 0;
|
|
649
|
+
const languageBreakdown = {};
|
|
650
|
+
for (const symbols of this.indexCache.values()) {
|
|
651
|
+
totalSymbols += symbols.length;
|
|
652
|
+
for (const symbol of symbols) {
|
|
653
|
+
languageBreakdown[symbol.language] = (languageBreakdown[symbol.language] || 0) + 1;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
totalFiles: this.indexCache.size,
|
|
658
|
+
totalSymbols,
|
|
659
|
+
languageBreakdown,
|
|
660
|
+
cacheAge: Date.now() - this.lastIndexTime,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Export a default instance
|
|
665
|
+
export const aceCodeSearchService = new ACECodeSearchService();
|
|
666
|
+
// MCP Tool definitions for integration
|
|
667
|
+
export const mcpTools = [
|
|
668
|
+
{
|
|
669
|
+
name: 'ace_search_symbols',
|
|
670
|
+
description: 'ACE Code Search: Intelligent symbol search across the codebase. Finds functions, classes, variables, and other code symbols with fuzzy matching. Supports multiple programming languages (TypeScript, JavaScript, Python, Go, Rust, Java, C#). Returns precise file locations with line numbers and context.',
|
|
671
|
+
inputSchema: {
|
|
672
|
+
type: 'object',
|
|
673
|
+
properties: {
|
|
674
|
+
query: {
|
|
675
|
+
type: 'string',
|
|
676
|
+
description: 'Symbol name to search for (supports fuzzy matching, e.g., "gfc" can match "getFileContent")',
|
|
677
|
+
},
|
|
678
|
+
symbolType: {
|
|
679
|
+
type: 'string',
|
|
680
|
+
enum: ['function', 'class', 'method', 'variable', 'constant', 'interface', 'type', 'enum', 'import', 'export'],
|
|
681
|
+
description: 'Filter by specific symbol type (optional)',
|
|
682
|
+
},
|
|
683
|
+
language: {
|
|
684
|
+
type: 'string',
|
|
685
|
+
enum: ['typescript', 'javascript', 'python', 'go', 'rust', 'java', 'csharp'],
|
|
686
|
+
description: 'Filter by programming language (optional)',
|
|
687
|
+
},
|
|
688
|
+
maxResults: {
|
|
689
|
+
type: 'number',
|
|
690
|
+
description: 'Maximum number of results to return (default: 100)',
|
|
691
|
+
default: 100,
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
required: ['query'],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
name: 'ace_find_definition',
|
|
699
|
+
description: 'ACE Code Search: Find the definition of a symbol (Go to Definition). Locates where a function, class, or variable is defined in the codebase. Returns precise location with full signature and context.',
|
|
700
|
+
inputSchema: {
|
|
701
|
+
type: 'object',
|
|
702
|
+
properties: {
|
|
703
|
+
symbolName: {
|
|
704
|
+
type: 'string',
|
|
705
|
+
description: 'Name of the symbol to find definition for',
|
|
706
|
+
},
|
|
707
|
+
contextFile: {
|
|
708
|
+
type: 'string',
|
|
709
|
+
description: 'Current file path for context-aware search (optional, searches current file first)',
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
required: ['symbolName'],
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: 'ace_find_references',
|
|
717
|
+
description: 'ACE Code Search: Find all references to a symbol (Find All References). Shows where a function, class, or variable is used throughout the codebase. Categorizes references as definition, usage, import, or type reference.',
|
|
718
|
+
inputSchema: {
|
|
719
|
+
type: 'object',
|
|
720
|
+
properties: {
|
|
721
|
+
symbolName: {
|
|
722
|
+
type: 'string',
|
|
723
|
+
description: 'Name of the symbol to find references for',
|
|
724
|
+
},
|
|
725
|
+
maxResults: {
|
|
726
|
+
type: 'number',
|
|
727
|
+
description: 'Maximum number of references to return (default: 100)',
|
|
728
|
+
default: 100,
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
required: ['symbolName'],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: 'ace_semantic_search',
|
|
736
|
+
description: 'ACE Code Search: Advanced semantic search with context understanding. Searches for symbols with intelligent filtering by search type (definition, usage, implementation, all). Combines symbol search with cross-reference analysis.',
|
|
737
|
+
inputSchema: {
|
|
738
|
+
type: 'object',
|
|
739
|
+
properties: {
|
|
740
|
+
query: {
|
|
741
|
+
type: 'string',
|
|
742
|
+
description: 'Search query (symbol name or pattern)',
|
|
743
|
+
},
|
|
744
|
+
searchType: {
|
|
745
|
+
type: 'string',
|
|
746
|
+
enum: ['definition', 'usage', 'implementation', 'all'],
|
|
747
|
+
description: 'Type of search: definition (find declarations), usage (find usages), implementation (find implementations), all (comprehensive search)',
|
|
748
|
+
default: 'all',
|
|
749
|
+
},
|
|
750
|
+
language: {
|
|
751
|
+
type: 'string',
|
|
752
|
+
enum: ['typescript', 'javascript', 'python', 'go', 'rust', 'java', 'csharp'],
|
|
753
|
+
description: 'Filter by programming language (optional)',
|
|
754
|
+
},
|
|
755
|
+
maxResults: {
|
|
756
|
+
type: 'number',
|
|
757
|
+
description: 'Maximum number of results to return (default: 50)',
|
|
758
|
+
default: 50,
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
required: ['query'],
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'ace_file_outline',
|
|
766
|
+
description: 'ACE Code Search: Get complete code outline for a file. Shows all functions, classes, variables, and other symbols defined in the file with their locations. Similar to VS Code\'s outline view.',
|
|
767
|
+
inputSchema: {
|
|
768
|
+
type: 'object',
|
|
769
|
+
properties: {
|
|
770
|
+
filePath: {
|
|
771
|
+
type: 'string',
|
|
772
|
+
description: 'Path to the file to get outline for (relative to workspace root)',
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
required: ['filePath'],
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: 'ace_text_search',
|
|
780
|
+
description: 'ACE Code Search: Fast text search across the entire codebase using Node.js built-in features (no external dependencies required). Search for exact patterns or regex across all files. Useful for finding strings, comments, TODOs, or any text patterns. Supports glob filtering.',
|
|
781
|
+
inputSchema: {
|
|
782
|
+
type: 'object',
|
|
783
|
+
properties: {
|
|
784
|
+
pattern: {
|
|
785
|
+
type: 'string',
|
|
786
|
+
description: 'Text pattern or regex to search for (e.g., "TODO:", "import.*from", "throw new Error")',
|
|
787
|
+
},
|
|
788
|
+
fileGlob: {
|
|
789
|
+
type: 'string',
|
|
790
|
+
description: 'Glob pattern to filter files (e.g., "*.ts" for TypeScript only, "**/*.{js,ts}" for JS and TS, "src/**/*.py" for Python in src)',
|
|
791
|
+
},
|
|
792
|
+
isRegex: {
|
|
793
|
+
type: 'boolean',
|
|
794
|
+
description: 'Whether the pattern is a regular expression (default: false for literal text search)',
|
|
795
|
+
default: false,
|
|
796
|
+
},
|
|
797
|
+
maxResults: {
|
|
798
|
+
type: 'number',
|
|
799
|
+
description: 'Maximum number of results to return (default: 100)',
|
|
800
|
+
default: 100,
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
required: ['pattern'],
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: 'ace_index_stats',
|
|
808
|
+
description: 'ACE Code Search: Get statistics about the code index. Shows number of indexed files, symbols, language breakdown, and cache status. Useful for understanding search coverage.',
|
|
809
|
+
inputSchema: {
|
|
810
|
+
type: 'object',
|
|
811
|
+
properties: {},
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
name: 'ace_clear_cache',
|
|
816
|
+
description: 'ACE Code Search: Clear the symbol index cache and force a full re-index on next search. Use when codebase has changed significantly or search results seem stale.',
|
|
817
|
+
inputSchema: {
|
|
818
|
+
type: 'object',
|
|
819
|
+
properties: {},
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
];
|