snow-ai 0.2.24 → 0.2.26
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/chat.d.ts +0 -8
- package/dist/api/chat.js +1 -144
- package/dist/api/responses.d.ts +0 -11
- package/dist/api/responses.js +1 -189
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +90 -295
- package/dist/app.d.ts +2 -1
- package/dist/app.js +11 -13
- package/dist/cli.js +16 -3
- package/dist/hooks/useClipboard.js +4 -4
- package/dist/hooks/useGlobalNavigation.d.ts +1 -1
- package/dist/hooks/useKeyboardInput.d.ts +1 -0
- package/dist/hooks/useKeyboardInput.js +8 -4
- package/dist/hooks/useTerminalFocus.d.ts +5 -0
- package/dist/hooks/useTerminalFocus.js +22 -2
- package/dist/mcp/aceCodeSearch.d.ts +58 -4
- package/dist/mcp/aceCodeSearch.js +563 -20
- package/dist/mcp/filesystem.d.ts +59 -10
- package/dist/mcp/filesystem.js +431 -124
- package/dist/mcp/ideDiagnostics.d.ts +36 -0
- package/dist/mcp/ideDiagnostics.js +92 -0
- package/dist/ui/components/ChatInput.js +6 -3
- package/dist/ui/pages/ChatScreen.d.ts +4 -2
- package/dist/ui/pages/ChatScreen.js +31 -2
- package/dist/ui/pages/ConfigProfileScreen.d.ts +7 -0
- package/dist/ui/pages/ConfigProfileScreen.js +300 -0
- package/dist/ui/pages/{ApiConfigScreen.d.ts → ConfigScreen.d.ts} +1 -1
- package/dist/ui/pages/ConfigScreen.js +748 -0
- package/dist/ui/pages/WelcomeScreen.js +7 -18
- package/dist/utils/apiConfig.d.ts +0 -2
- package/dist/utils/apiConfig.js +12 -0
- package/dist/utils/configManager.d.ts +45 -0
- package/dist/utils/configManager.js +274 -0
- package/dist/utils/contextCompressor.js +355 -49
- package/dist/utils/escapeHandler.d.ts +79 -0
- package/dist/utils/escapeHandler.js +153 -0
- package/dist/utils/incrementalSnapshot.js +2 -1
- package/dist/utils/mcpToolsManager.js +44 -0
- package/dist/utils/retryUtils.js +6 -0
- package/dist/utils/textBuffer.js +13 -15
- package/dist/utils/vscodeConnection.js +26 -11
- package/dist/utils/workspaceSnapshot.js +2 -1
- package/package.json +2 -1
- package/dist/ui/pages/ApiConfigScreen.js +0 -161
- package/dist/ui/pages/ModelConfigScreen.d.ts +0 -8
- package/dist/ui/pages/ModelConfigScreen.js +0 -504
package/dist/mcp/filesystem.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
// IDE connection supports both VSCode and JetBrains IDEs
|
|
4
6
|
import { vscodeConnection } from '../utils/vscodeConnection.js';
|
|
5
7
|
import { incrementalSnapshotManager } from '../utils/incrementalSnapshot.js';
|
|
8
|
+
import { tryUnescapeFix, trimPairIfPossible, isOverEscaped, } from '../utils/escapeHandler.js';
|
|
6
9
|
const { resolve, dirname, isAbsolute } = path;
|
|
10
|
+
const execAsync = promisify(exec);
|
|
7
11
|
/**
|
|
8
12
|
* Filesystem MCP Service
|
|
9
13
|
* Provides basic file operations: read, create, and delete files
|
|
@@ -43,6 +47,153 @@ export class FilesystemMCPService {
|
|
|
43
47
|
});
|
|
44
48
|
this.basePath = resolve(basePath);
|
|
45
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Calculate similarity between two strings using a smarter algorithm
|
|
52
|
+
* This normalizes whitespace first to avoid false negatives from spacing differences
|
|
53
|
+
* Returns a value between 0 (completely different) and 1 (identical)
|
|
54
|
+
*/
|
|
55
|
+
calculateSimilarity(str1, str2, threshold = 0) {
|
|
56
|
+
// Normalize whitespace for comparison: collapse all whitespace to single spaces
|
|
57
|
+
const normalize = (s) => s.replace(/\s+/g, ' ').trim();
|
|
58
|
+
const norm1 = normalize(str1);
|
|
59
|
+
const norm2 = normalize(str2);
|
|
60
|
+
const len1 = norm1.length;
|
|
61
|
+
const len2 = norm2.length;
|
|
62
|
+
if (len1 === 0)
|
|
63
|
+
return len2 === 0 ? 1 : 0;
|
|
64
|
+
if (len2 === 0)
|
|
65
|
+
return 0;
|
|
66
|
+
// Quick length check - if lengths differ too much, similarity can't be above threshold
|
|
67
|
+
const maxLen = Math.max(len1, len2);
|
|
68
|
+
const minLen = Math.min(len1, len2);
|
|
69
|
+
const lengthRatio = minLen / maxLen;
|
|
70
|
+
if (threshold > 0 && lengthRatio < threshold) {
|
|
71
|
+
return lengthRatio; // Can't possibly meet threshold
|
|
72
|
+
}
|
|
73
|
+
// Use Levenshtein distance for better similarity calculation
|
|
74
|
+
const distance = this.levenshteinDistance(norm1, norm2, Math.ceil(maxLen * (1 - threshold)));
|
|
75
|
+
return 1 - distance / maxLen;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Calculate Levenshtein distance between two strings with early termination
|
|
79
|
+
* @param str1 First string
|
|
80
|
+
* @param str2 Second string
|
|
81
|
+
* @param maxDistance Maximum distance to compute (early exit if exceeded)
|
|
82
|
+
* @returns Levenshtein distance, or maxDistance+1 if exceeded
|
|
83
|
+
*/
|
|
84
|
+
levenshteinDistance(str1, str2, maxDistance = Infinity) {
|
|
85
|
+
const len1 = str1.length;
|
|
86
|
+
const len2 = str2.length;
|
|
87
|
+
// Quick exit for identical strings
|
|
88
|
+
if (str1 === str2)
|
|
89
|
+
return 0;
|
|
90
|
+
// Quick exit if length difference already exceeds maxDistance
|
|
91
|
+
if (Math.abs(len1 - len2) > maxDistance) {
|
|
92
|
+
return maxDistance + 1;
|
|
93
|
+
}
|
|
94
|
+
// Use single-row algorithm to save memory (only need previous row)
|
|
95
|
+
let prevRow = Array.from({ length: len2 + 1 }, (_, i) => i);
|
|
96
|
+
for (let i = 1; i <= len1; i++) {
|
|
97
|
+
const currRow = [i];
|
|
98
|
+
let minInRow = i; // Track minimum value in current row
|
|
99
|
+
for (let j = 1; j <= len2; j++) {
|
|
100
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
101
|
+
const val = Math.min(prevRow[j] + 1, // deletion
|
|
102
|
+
currRow[j - 1] + 1, // insertion
|
|
103
|
+
prevRow[j - 1] + cost // substitution
|
|
104
|
+
);
|
|
105
|
+
currRow[j] = val;
|
|
106
|
+
minInRow = Math.min(minInRow, val);
|
|
107
|
+
}
|
|
108
|
+
// Early termination: if minimum in this row exceeds maxDistance, we can stop
|
|
109
|
+
if (minInRow > maxDistance) {
|
|
110
|
+
return maxDistance + 1;
|
|
111
|
+
}
|
|
112
|
+
prevRow = currRow;
|
|
113
|
+
}
|
|
114
|
+
return prevRow[len2];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Find the closest matching candidates in the file content
|
|
118
|
+
* Returns top N candidates sorted by similarity
|
|
119
|
+
* Optimized with safe pre-filtering and early exit
|
|
120
|
+
*/
|
|
121
|
+
findClosestMatches(searchContent, fileLines, topN = 3) {
|
|
122
|
+
const searchLines = searchContent.split('\n');
|
|
123
|
+
const candidates = [];
|
|
124
|
+
// Normalize whitespace for display only (makes preview more readable)
|
|
125
|
+
const normalizeForDisplay = (line) => line.replace(/\t/g, ' ').replace(/ +/g, ' ');
|
|
126
|
+
// Fast pre-filter: use first line as anchor (only for multi-line searches)
|
|
127
|
+
const searchFirstLine = searchLines[0]?.replace(/\s+/g, ' ').trim() || '';
|
|
128
|
+
const threshold = 0.5;
|
|
129
|
+
const usePreFilter = searchLines.length >= 5; // Only for 5+ line searches
|
|
130
|
+
const preFilterThreshold = 0.2; // Very conservative - only skip completely unrelated lines
|
|
131
|
+
// Try to find candidates by sliding window with optimizations
|
|
132
|
+
const maxCandidates = topN * 3; // Collect more candidates, then pick best
|
|
133
|
+
for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
|
|
134
|
+
// Quick pre-filter: check first line similarity (only for multi-line)
|
|
135
|
+
if (usePreFilter) {
|
|
136
|
+
const firstLineCandidate = fileLines[i]?.replace(/\s+/g, ' ').trim() || '';
|
|
137
|
+
const firstLineSimilarity = this.calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
|
|
138
|
+
// Skip only if first line is very different
|
|
139
|
+
if (firstLineSimilarity < preFilterThreshold) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Full candidate check
|
|
144
|
+
const candidateLines = fileLines.slice(i, i + searchLines.length);
|
|
145
|
+
const candidateContent = candidateLines.join('\n');
|
|
146
|
+
const similarity = this.calculateSimilarity(searchContent, candidateContent, threshold);
|
|
147
|
+
// Only consider candidates with >50% similarity
|
|
148
|
+
if (similarity > threshold) {
|
|
149
|
+
candidates.push({
|
|
150
|
+
startLine: i + 1,
|
|
151
|
+
endLine: i + searchLines.length,
|
|
152
|
+
similarity,
|
|
153
|
+
preview: candidateLines
|
|
154
|
+
.map((line, idx) => `${i + idx + 1}→${normalizeForDisplay(line)}`)
|
|
155
|
+
.join('\n'),
|
|
156
|
+
});
|
|
157
|
+
// Early exit if we found a nearly perfect match
|
|
158
|
+
if (similarity >= 0.95) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
// Limit candidates to avoid excessive computation
|
|
162
|
+
if (candidates.length >= maxCandidates) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Sort by similarity descending and return top N
|
|
168
|
+
return candidates
|
|
169
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
170
|
+
.slice(0, topN);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Generate a helpful diff message showing differences between search and actual content
|
|
174
|
+
* Note: This is ONLY for display purposes. Tabs/spaces are normalized for better readability.
|
|
175
|
+
*/
|
|
176
|
+
generateDiffMessage(searchContent, actualContent, maxLines = 10) {
|
|
177
|
+
const searchLines = searchContent.split('\n');
|
|
178
|
+
const actualLines = actualContent.split('\n');
|
|
179
|
+
const diffLines = [];
|
|
180
|
+
const maxLen = Math.max(searchLines.length, actualLines.length);
|
|
181
|
+
// Normalize whitespace for display only (makes diff more readable)
|
|
182
|
+
const normalizeForDisplay = (line) => line.replace(/\t/g, ' ').replace(/ +/g, ' ');
|
|
183
|
+
for (let i = 0; i < Math.min(maxLen, maxLines); i++) {
|
|
184
|
+
const searchLine = searchLines[i] || '';
|
|
185
|
+
const actualLine = actualLines[i] || '';
|
|
186
|
+
if (searchLine !== actualLine) {
|
|
187
|
+
diffLines.push(`Line ${i + 1}:`);
|
|
188
|
+
diffLines.push(` Search: ${JSON.stringify(normalizeForDisplay(searchLine))}`);
|
|
189
|
+
diffLines.push(` Actual: ${JSON.stringify(normalizeForDisplay(actualLine))}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (maxLen > maxLines) {
|
|
193
|
+
diffLines.push(`... (${maxLen - maxLines} more lines)`);
|
|
194
|
+
}
|
|
195
|
+
return diffLines.join('\n');
|
|
196
|
+
}
|
|
46
197
|
/**
|
|
47
198
|
* Analyze code structure for balance and completeness
|
|
48
199
|
* Helps AI identify bracket mismatches, unclosed tags, and boundary issues
|
|
@@ -149,23 +300,8 @@ export class FilesystemMCPService {
|
|
|
149
300
|
}
|
|
150
301
|
}
|
|
151
302
|
}
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
const firstLine = editedLines[0]?.trim() || '';
|
|
155
|
-
const endsWithOpenBrace = lastLine.endsWith('{') ||
|
|
156
|
-
lastLine.endsWith('(') ||
|
|
157
|
-
lastLine.endsWith('[');
|
|
158
|
-
const startsWithCloseBrace = firstLine.startsWith('}') ||
|
|
159
|
-
firstLine.startsWith(')') ||
|
|
160
|
-
firstLine.startsWith(']');
|
|
161
|
-
if (endsWithOpenBrace || startsWithCloseBrace) {
|
|
162
|
-
analysis.codeBlockBoundary = {
|
|
163
|
-
isInCompleteBlock: false,
|
|
164
|
-
suggestion: endsWithOpenBrace
|
|
165
|
-
? 'Edit ends with an opening bracket - ensure the closing bracket is included in a subsequent edit or already exists in the file'
|
|
166
|
-
: 'Edit starts with a closing bracket - ensure the opening bracket exists before this edit',
|
|
167
|
-
};
|
|
168
|
-
}
|
|
303
|
+
// Note: Boundary checking removed - AI should be free to edit partial code blocks
|
|
304
|
+
// The bracket balance check above is sufficient for detecting real issues
|
|
169
305
|
return analysis;
|
|
170
306
|
}
|
|
171
307
|
/**
|
|
@@ -223,15 +359,85 @@ export class FilesystemMCPService {
|
|
|
223
359
|
return { start: contextStart, end: contextEnd, extended };
|
|
224
360
|
}
|
|
225
361
|
/**
|
|
226
|
-
* Get the content of a file with
|
|
227
|
-
* @param filePath - Path to the file (relative to base path or absolute)
|
|
228
|
-
* @param startLine - Starting line number (1-indexed, inclusive)
|
|
229
|
-
* @param endLine - Ending line number (1-indexed, inclusive)
|
|
362
|
+
* Get the content of a file with optional line range
|
|
363
|
+
* @param filePath - Path to the file (relative to base path or absolute) or array of file paths
|
|
364
|
+
* @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1)
|
|
365
|
+
* @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to 500 or file end)
|
|
230
366
|
* @returns Object containing the requested content with line numbers and metadata
|
|
231
367
|
* @throws Error if file doesn't exist or cannot be read
|
|
232
368
|
*/
|
|
233
369
|
async getFileContent(filePath, startLine, endLine) {
|
|
234
370
|
try {
|
|
371
|
+
// Handle array of files
|
|
372
|
+
if (Array.isArray(filePath)) {
|
|
373
|
+
const filesData = [];
|
|
374
|
+
const allContents = [];
|
|
375
|
+
for (const file of filePath) {
|
|
376
|
+
try {
|
|
377
|
+
const fullPath = this.resolvePath(file);
|
|
378
|
+
// For absolute paths, skip validation to allow access outside base path
|
|
379
|
+
if (!isAbsolute(file)) {
|
|
380
|
+
await this.validatePath(fullPath);
|
|
381
|
+
}
|
|
382
|
+
// Check if the path is a directory, if so, list its contents instead
|
|
383
|
+
const stats = await fs.stat(fullPath);
|
|
384
|
+
if (stats.isDirectory()) {
|
|
385
|
+
const dirFiles = await this.listFiles(file);
|
|
386
|
+
const fileList = dirFiles.join('\n');
|
|
387
|
+
allContents.push(`📁 Directory: ${file}\n${fileList}`);
|
|
388
|
+
filesData.push({
|
|
389
|
+
path: file,
|
|
390
|
+
startLine: 1,
|
|
391
|
+
endLine: dirFiles.length,
|
|
392
|
+
totalLines: dirFiles.length,
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
397
|
+
const lines = content.split('\n');
|
|
398
|
+
const totalLines = lines.length;
|
|
399
|
+
// Default values and logic
|
|
400
|
+
const actualStartLine = startLine ?? 1;
|
|
401
|
+
const actualEndLine = endLine ?? totalLines;
|
|
402
|
+
// Validate and adjust line numbers
|
|
403
|
+
if (actualStartLine < 1) {
|
|
404
|
+
throw new Error(`Start line must be greater than 0 for ${file}`);
|
|
405
|
+
}
|
|
406
|
+
if (actualEndLine < actualStartLine) {
|
|
407
|
+
throw new Error(`End line must be greater than or equal to start line for ${file}`);
|
|
408
|
+
}
|
|
409
|
+
if (actualStartLine > totalLines) {
|
|
410
|
+
throw new Error(`Start line ${actualStartLine} exceeds file length ${totalLines} for ${file}`);
|
|
411
|
+
}
|
|
412
|
+
const start = actualStartLine;
|
|
413
|
+
const end = Math.min(totalLines, actualEndLine);
|
|
414
|
+
// Extract specified lines
|
|
415
|
+
const selectedLines = lines.slice(start - 1, end);
|
|
416
|
+
const numberedLines = selectedLines.map((line, index) => {
|
|
417
|
+
const lineNum = start + index;
|
|
418
|
+
return `${lineNum}→${line}`;
|
|
419
|
+
});
|
|
420
|
+
const fileContent = `📄 ${file} (lines ${start}-${end}/${totalLines})\n${numberedLines.join('\n')}`;
|
|
421
|
+
allContents.push(fileContent);
|
|
422
|
+
filesData.push({
|
|
423
|
+
path: file,
|
|
424
|
+
startLine: start,
|
|
425
|
+
endLine: end,
|
|
426
|
+
totalLines,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
431
|
+
allContents.push(`❌ ${file}: ${errorMsg}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
content: allContents.join('\n\n'),
|
|
436
|
+
files: filesData,
|
|
437
|
+
totalFiles: filePath.length,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
// Original single file logic
|
|
235
441
|
const fullPath = this.resolvePath(filePath);
|
|
236
442
|
// For absolute paths, skip validation to allow access outside base path
|
|
237
443
|
if (!isAbsolute(filePath)) {
|
|
@@ -254,29 +460,30 @@ export class FilesystemMCPService {
|
|
|
254
460
|
// Parse lines
|
|
255
461
|
const lines = content.split('\n');
|
|
256
462
|
const totalLines = lines.length;
|
|
463
|
+
// Default values and logic:
|
|
464
|
+
// - No params: read entire file (1 to totalLines)
|
|
465
|
+
// - Only startLine: read from startLine to end of file
|
|
466
|
+
// - Both params: read from startLine to endLine
|
|
467
|
+
const actualStartLine = startLine ?? 1;
|
|
468
|
+
const actualEndLine = endLine ?? totalLines;
|
|
257
469
|
// Validate and adjust line numbers
|
|
258
|
-
if (
|
|
470
|
+
if (actualStartLine < 1) {
|
|
259
471
|
throw new Error('Start line must be greater than 0');
|
|
260
472
|
}
|
|
261
|
-
if (
|
|
473
|
+
if (actualEndLine < actualStartLine) {
|
|
262
474
|
throw new Error('End line must be greater than or equal to start line');
|
|
263
475
|
}
|
|
264
|
-
if (
|
|
265
|
-
throw new Error(`Start line ${
|
|
476
|
+
if (actualStartLine > totalLines) {
|
|
477
|
+
throw new Error(`Start line ${actualStartLine} exceeds file length ${totalLines}`);
|
|
266
478
|
}
|
|
267
|
-
const start =
|
|
268
|
-
const end = Math.min(totalLines,
|
|
479
|
+
const start = actualStartLine;
|
|
480
|
+
const end = Math.min(totalLines, actualEndLine);
|
|
269
481
|
// Extract specified lines (convert to 0-indexed) and add line numbers
|
|
270
482
|
const selectedLines = lines.slice(start - 1, end);
|
|
271
483
|
// Format with line numbers (no padding to save tokens)
|
|
272
|
-
// Normalize whitespace: tabs → single space, multiple spaces → single space
|
|
273
484
|
const numberedLines = selectedLines.map((line, index) => {
|
|
274
485
|
const lineNum = start + index;
|
|
275
|
-
|
|
276
|
-
const normalizedLine = line
|
|
277
|
-
.replace(/\t/g, ' ') // Convert tabs to single space
|
|
278
|
-
.replace(/ +/g, ' '); // Compress multiple spaces to single space
|
|
279
|
-
return `${lineNum}→${normalizedLine}`;
|
|
486
|
+
return `${lineNum}→${line}`;
|
|
280
487
|
});
|
|
281
488
|
const partialContent = numberedLines.join('\n');
|
|
282
489
|
return {
|
|
@@ -432,14 +639,14 @@ export class FilesystemMCPService {
|
|
|
432
639
|
}
|
|
433
640
|
/**
|
|
434
641
|
* Edit a file by searching for exact content and replacing it
|
|
435
|
-
* This method
|
|
642
|
+
* This method uses SMART MATCHING to handle whitespace differences automatically.
|
|
436
643
|
*
|
|
437
644
|
* @param filePath - Path to the file to edit
|
|
438
|
-
* @param searchContent -
|
|
645
|
+
* @param searchContent - Content to search for (whitespace will be normalized automatically)
|
|
439
646
|
* @param replaceContent - New content to replace the search content with
|
|
440
647
|
* @param occurrence - Which occurrence to replace (1-indexed, default: 1, use -1 for all)
|
|
441
648
|
* @param contextLines - Number of context lines to return before and after the edit (default: 8)
|
|
442
|
-
* @returns Object containing success message, before/after comparison, and diagnostics
|
|
649
|
+
* @returns Object containing success message, before/after comparison, and diagnostics from IDE (VSCode or JetBrains)
|
|
443
650
|
* @throws Error if search content is not found or multiple matches exist
|
|
444
651
|
*/
|
|
445
652
|
async editFileBySearch(filePath, searchContent, replaceContent, occurrence = 1, contextLines = 8) {
|
|
@@ -453,34 +660,129 @@ export class FilesystemMCPService {
|
|
|
453
660
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
454
661
|
const lines = content.split('\n');
|
|
455
662
|
// Normalize line endings
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
|
|
663
|
+
let normalizedSearch = searchContent
|
|
664
|
+
.replace(/\r\n/g, '\n')
|
|
665
|
+
.replace(/\r/g, '\n');
|
|
666
|
+
const normalizedContent = content
|
|
667
|
+
.replace(/\r\n/g, '\n')
|
|
668
|
+
.replace(/\r/g, '\n');
|
|
669
|
+
// Split into lines for matching
|
|
670
|
+
let searchLines = normalizedSearch.split('\n');
|
|
671
|
+
const contentLines = normalizedContent.split('\n');
|
|
672
|
+
// Find all matches using smart fuzzy matching (auto-handles whitespace)
|
|
464
673
|
const matches = [];
|
|
465
|
-
const
|
|
466
|
-
|
|
674
|
+
const threshold = 0.6; // Lowered to 60% to allow smaller partial edits (was 0.75)
|
|
675
|
+
// Fast pre-filter: use first line as anchor to skip unlikely positions
|
|
676
|
+
// Only apply pre-filter for multi-line searches to avoid missing valid matches
|
|
677
|
+
const searchFirstLine = searchLines[0]?.replace(/\s+/g, ' ').trim() || '';
|
|
678
|
+
const usePreFilter = searchLines.length >= 5; // Only pre-filter for 5+ line searches
|
|
679
|
+
const preFilterThreshold = 0.2; // Very low threshold - only skip completely unrelated lines
|
|
680
|
+
const maxMatches = 10; // Limit matches to avoid excessive computation
|
|
467
681
|
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
682
|
+
// Quick pre-filter: check first line similarity (only for multi-line searches)
|
|
683
|
+
if (usePreFilter) {
|
|
684
|
+
const firstLineCandidate = contentLines[i]?.replace(/\s+/g, ' ').trim() || '';
|
|
685
|
+
const firstLineSimilarity = this.calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
|
|
686
|
+
// Skip only if first line is very different (< 30% match)
|
|
687
|
+
// This is safe because if first line differs this much, full match unlikely
|
|
688
|
+
if (firstLineSimilarity < preFilterThreshold) {
|
|
689
|
+
continue;
|
|
473
690
|
}
|
|
474
691
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
692
|
+
// Full candidate check
|
|
693
|
+
const candidateLines = contentLines.slice(i, i + searchLines.length);
|
|
694
|
+
const candidateContent = candidateLines.join('\n');
|
|
695
|
+
const similarity = this.calculateSimilarity(normalizedSearch, candidateContent, threshold);
|
|
696
|
+
// Accept matches above threshold
|
|
697
|
+
if (similarity >= threshold) {
|
|
698
|
+
matches.push({
|
|
699
|
+
startLine: i + 1,
|
|
700
|
+
endLine: i + searchLines.length,
|
|
701
|
+
similarity,
|
|
702
|
+
});
|
|
703
|
+
// Early exit if we found a nearly perfect match
|
|
704
|
+
if (similarity >= 0.95) {
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
// Limit matches to avoid excessive computation
|
|
708
|
+
if (matches.length >= maxMatches) {
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
479
711
|
}
|
|
480
712
|
}
|
|
481
|
-
//
|
|
713
|
+
// Sort by similarity descending (best match first)
|
|
714
|
+
matches.sort((a, b) => b.similarity - a.similarity);
|
|
715
|
+
// Handle no matches: Try escape correction before giving up
|
|
482
716
|
if (matches.length === 0) {
|
|
483
|
-
|
|
717
|
+
// Step 1: Try unescape correction (lightweight, no LLM)
|
|
718
|
+
const unescapeFix = tryUnescapeFix(normalizedContent, normalizedSearch, 1);
|
|
719
|
+
if (unescapeFix) {
|
|
720
|
+
// Unescape succeeded! Re-run the matching with corrected content
|
|
721
|
+
const correctedSearchLines = unescapeFix.correctedString.split('\n');
|
|
722
|
+
for (let i = 0; i <= contentLines.length - correctedSearchLines.length; i++) {
|
|
723
|
+
const candidateLines = contentLines.slice(i, i + correctedSearchLines.length);
|
|
724
|
+
const candidateContent = candidateLines.join('\n');
|
|
725
|
+
const similarity = this.calculateSimilarity(unescapeFix.correctedString, candidateContent);
|
|
726
|
+
if (similarity >= threshold) {
|
|
727
|
+
matches.push({
|
|
728
|
+
startLine: i + 1,
|
|
729
|
+
endLine: i + correctedSearchLines.length,
|
|
730
|
+
similarity,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
matches.sort((a, b) => b.similarity - a.similarity);
|
|
735
|
+
// If unescape fix worked, also fix replaceContent if needed
|
|
736
|
+
if (matches.length > 0) {
|
|
737
|
+
const trimResult = trimPairIfPossible(unescapeFix.correctedString, replaceContent, normalizedContent, 1);
|
|
738
|
+
// Update searchContent and replaceContent for the edit
|
|
739
|
+
normalizedSearch = trimResult.target;
|
|
740
|
+
replaceContent = trimResult.paired;
|
|
741
|
+
// Also update searchLines for later use
|
|
742
|
+
searchLines.splice(0, searchLines.length, ...normalizedSearch.split('\n'));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// If still no matches after unescape, provide detailed error
|
|
746
|
+
if (matches.length === 0) {
|
|
747
|
+
// Find closest matches for suggestions
|
|
748
|
+
const closestMatches = this.findClosestMatches(normalizedSearch, normalizedContent.split('\n'), 3);
|
|
749
|
+
let errorMessage = `❌ Search content not found in file: ${filePath}\n\n`;
|
|
750
|
+
errorMessage += `🔍 Using smart fuzzy matching (threshold: 60%)\n`;
|
|
751
|
+
if (isOverEscaped(searchContent)) {
|
|
752
|
+
errorMessage += `⚠️ Detected over-escaped content, automatic fix attempted but failed\n`;
|
|
753
|
+
}
|
|
754
|
+
errorMessage += `\n`;
|
|
755
|
+
if (closestMatches.length > 0) {
|
|
756
|
+
errorMessage += `💡 Found ${closestMatches.length} similar location(s):\n\n`;
|
|
757
|
+
closestMatches.forEach((candidate, idx) => {
|
|
758
|
+
errorMessage += `${idx + 1}. Lines ${candidate.startLine}-${candidate.endLine} (${(candidate.similarity * 100).toFixed(0)}% match):\n`;
|
|
759
|
+
errorMessage += `${candidate.preview}\n\n`;
|
|
760
|
+
});
|
|
761
|
+
// Show diff with the closest match
|
|
762
|
+
const bestMatch = closestMatches[0];
|
|
763
|
+
if (bestMatch) {
|
|
764
|
+
const bestMatchLines = lines.slice(bestMatch.startLine - 1, bestMatch.endLine);
|
|
765
|
+
const bestMatchContent = bestMatchLines.join('\n');
|
|
766
|
+
const diffMsg = this.generateDiffMessage(normalizedSearch, bestMatchContent, 5);
|
|
767
|
+
if (diffMsg) {
|
|
768
|
+
errorMessage += `📊 Difference with closest match:\n${diffMsg}\n\n`;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
errorMessage += `💡 Suggestions:\n`;
|
|
772
|
+
errorMessage += ` • Make sure you copied content from filesystem_read (without "123→")\n`;
|
|
773
|
+
errorMessage += ` • Whitespace differences are automatically handled\n`;
|
|
774
|
+
errorMessage += ` • Try copying a larger or smaller code block\n`;
|
|
775
|
+
errorMessage += ` • If multiple filesystem_edit_search attempts fail, use terminal_execute to edit via command line (e.g. sed, printf)\n`;
|
|
776
|
+
errorMessage += `⚠️ No similar content found in the file.\n\n`;
|
|
777
|
+
errorMessage += `📝 What you searched for (first 5 lines, formatted):\n`;
|
|
778
|
+
const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ').trim();
|
|
779
|
+
searchLines.slice(0, 5).forEach((line, idx) => {
|
|
780
|
+
errorMessage += `${idx + 1}. ${JSON.stringify(normalizeForDisplay(line))}\n`;
|
|
781
|
+
});
|
|
782
|
+
errorMessage += `\n💡 Copy exact content from filesystem_read (without line numbers)\n`;
|
|
783
|
+
}
|
|
784
|
+
throw new Error(errorMessage);
|
|
785
|
+
}
|
|
484
786
|
}
|
|
485
787
|
// Handle occurrence selection
|
|
486
788
|
let selectedMatch;
|
|
@@ -494,9 +796,7 @@ export class FilesystemMCPService {
|
|
|
494
796
|
}
|
|
495
797
|
}
|
|
496
798
|
else if (occurrence < 1 || occurrence > matches.length) {
|
|
497
|
-
throw new Error(`Invalid occurrence ${occurrence}. Found ${matches.length} match(es) at lines: ${matches
|
|
498
|
-
.map(m => m.startLine)
|
|
499
|
-
.join(', ')}`);
|
|
799
|
+
throw new Error(`Invalid occurrence ${occurrence}. Found ${matches.length} match(es) at lines: ${matches.map(m => m.startLine).join(', ')}`);
|
|
500
800
|
}
|
|
501
801
|
else {
|
|
502
802
|
selectedMatch = matches[occurrence - 1];
|
|
@@ -505,19 +805,21 @@ export class FilesystemMCPService {
|
|
|
505
805
|
// Backup file before editing
|
|
506
806
|
await incrementalSnapshotManager.backupFile(fullPath);
|
|
507
807
|
// Perform the replacement by replacing the matched lines
|
|
508
|
-
const normalizedReplace = replaceContent
|
|
808
|
+
const normalizedReplace = replaceContent
|
|
809
|
+
.replace(/\r\n/g, '\n')
|
|
810
|
+
.replace(/\r/g, '\n');
|
|
509
811
|
const beforeLines = lines.slice(0, startLine - 1);
|
|
510
812
|
const afterLines = lines.slice(endLine);
|
|
511
813
|
const replaceLines = normalizedReplace.split('\n');
|
|
512
814
|
const modifiedLines = [...beforeLines, ...replaceLines, ...afterLines];
|
|
513
815
|
const modifiedContent = modifiedLines.join('\n');
|
|
514
|
-
// Calculate replaced content for display
|
|
816
|
+
// Calculate replaced content for display (compress whitespace for readability)
|
|
817
|
+
const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
|
|
515
818
|
const replacedLines = lines.slice(startLine - 1, endLine);
|
|
516
819
|
const replacedContent = replacedLines
|
|
517
820
|
.map((line, idx) => {
|
|
518
821
|
const lineNum = startLine + idx;
|
|
519
|
-
|
|
520
|
-
return `${lineNum}→${normalizedLine}`;
|
|
822
|
+
return `${lineNum}→${normalizeForDisplay(line)}`;
|
|
521
823
|
})
|
|
522
824
|
.join('\n');
|
|
523
825
|
// Calculate context boundaries
|
|
@@ -525,18 +827,17 @@ export class FilesystemMCPService {
|
|
|
525
827
|
const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, endLine, contextLines);
|
|
526
828
|
const contextStart = smartBoundaries.start;
|
|
527
829
|
const contextEnd = smartBoundaries.end;
|
|
528
|
-
// Extract old content for context
|
|
830
|
+
// Extract old content for context (compress whitespace for readability)
|
|
529
831
|
const oldContextLines = lines.slice(contextStart - 1, contextEnd);
|
|
530
832
|
const oldContent = oldContextLines
|
|
531
833
|
.map((line, idx) => {
|
|
532
834
|
const lineNum = contextStart + idx;
|
|
533
|
-
|
|
534
|
-
return `${lineNum}→${normalizedLine}`;
|
|
835
|
+
return `${lineNum}→${normalizeForDisplay(line)}`;
|
|
535
836
|
})
|
|
536
837
|
.join('\n');
|
|
537
838
|
// Write the modified content
|
|
538
839
|
await fs.writeFile(fullPath, modifiedContent, 'utf-8');
|
|
539
|
-
// Format with Prettier
|
|
840
|
+
// Format with Prettier asynchronously (non-blocking)
|
|
540
841
|
let finalContent = modifiedContent;
|
|
541
842
|
let finalLines = modifiedLines;
|
|
542
843
|
let finalTotalLines = modifiedLines.length;
|
|
@@ -546,8 +847,7 @@ export class FilesystemMCPService {
|
|
|
546
847
|
const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
|
|
547
848
|
if (shouldFormat) {
|
|
548
849
|
try {
|
|
549
|
-
|
|
550
|
-
stdio: 'pipe',
|
|
850
|
+
await execAsync(`npx prettier --write "${fullPath}"`, {
|
|
551
851
|
encoding: 'utf-8',
|
|
552
852
|
});
|
|
553
853
|
// Re-read the file after formatting
|
|
@@ -560,26 +860,29 @@ export class FilesystemMCPService {
|
|
|
560
860
|
// Continue with unformatted content
|
|
561
861
|
}
|
|
562
862
|
}
|
|
563
|
-
// Extract new content for context
|
|
863
|
+
// Extract new content for context (compress whitespace for readability)
|
|
564
864
|
const newContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
|
|
565
865
|
const newContextContent = newContextLines
|
|
566
866
|
.map((line, idx) => {
|
|
567
867
|
const lineNum = contextStart + idx;
|
|
568
|
-
|
|
569
|
-
return `${lineNum}→${normalizedLine}`;
|
|
868
|
+
return `${lineNum}→${normalizeForDisplay(line)}`;
|
|
570
869
|
})
|
|
571
870
|
.join('\n');
|
|
572
871
|
// Analyze code structure
|
|
573
872
|
const editedContentLines = replaceLines;
|
|
574
873
|
const structureAnalysis = this.analyzeCodeStructure(finalContent, filePath, editedContentLines);
|
|
575
|
-
// Get diagnostics from
|
|
874
|
+
// Get diagnostics from IDE (VSCode or JetBrains) - non-blocking, fire-and-forget
|
|
576
875
|
let diagnostics = [];
|
|
577
876
|
try {
|
|
578
|
-
|
|
579
|
-
|
|
877
|
+
// Request diagnostics without blocking (with timeout protection)
|
|
878
|
+
const diagnosticsPromise = Promise.race([
|
|
879
|
+
vscodeConnection.requestDiagnostics(fullPath),
|
|
880
|
+
new Promise(resolve => setTimeout(() => resolve([]), 1000)), // 1s max wait
|
|
881
|
+
]);
|
|
882
|
+
diagnostics = await diagnosticsPromise;
|
|
580
883
|
}
|
|
581
884
|
catch (error) {
|
|
582
|
-
// Ignore diagnostics errors
|
|
885
|
+
// Ignore diagnostics errors - this is optional functionality
|
|
583
886
|
}
|
|
584
887
|
// Build result
|
|
585
888
|
const result = {
|
|
@@ -653,10 +956,7 @@ export class FilesystemMCPService {
|
|
|
653
956
|
if (structureAnalysis.indentationWarnings.length > 0) {
|
|
654
957
|
structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
|
|
655
958
|
}
|
|
656
|
-
|
|
657
|
-
structureAnalysis.codeBlockBoundary.suggestion) {
|
|
658
|
-
structureWarnings.push(`Boundary: ${structureAnalysis.codeBlockBoundary.suggestion}`);
|
|
659
|
-
}
|
|
959
|
+
// Note: Boundary warnings removed - partial edits are common and expected
|
|
660
960
|
if (structureWarnings.length > 0) {
|
|
661
961
|
result.message += `\n\n🔍 Structure Analysis:\n`;
|
|
662
962
|
structureWarnings.forEach(warning => {
|
|
@@ -680,7 +980,7 @@ export class FilesystemMCPService {
|
|
|
680
980
|
* @param endLine - Ending line number (1-indexed, inclusive) - get from filesystem_read output
|
|
681
981
|
* @param newContent - New content to replace the specified lines (WITHOUT line numbers)
|
|
682
982
|
* @param contextLines - Number of context lines to return before and after the edit (default: 8)
|
|
683
|
-
* @returns Object containing success message, precise before/after comparison, and diagnostics
|
|
983
|
+
* @returns Object containing success message, precise before/after comparison, and diagnostics from IDE (VSCode or JetBrains)
|
|
684
984
|
* @throws Error if file editing fails
|
|
685
985
|
*/
|
|
686
986
|
async editFile(filePath, startLine, endLine, newContent, contextLines = 8) {
|
|
@@ -710,25 +1010,25 @@ export class FilesystemMCPService {
|
|
|
710
1010
|
// Backup file before editing
|
|
711
1011
|
await incrementalSnapshotManager.backupFile(fullPath);
|
|
712
1012
|
// Extract the lines that will be replaced (for comparison)
|
|
1013
|
+
// Compress whitespace for display readability
|
|
1014
|
+
const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
|
|
713
1015
|
const replacedLines = lines.slice(startLine - 1, adjustedEndLine);
|
|
714
1016
|
const replacedContent = replacedLines
|
|
715
1017
|
.map((line, idx) => {
|
|
716
1018
|
const lineNum = startLine + idx;
|
|
717
|
-
|
|
718
|
-
return `${lineNum}→${normalizedLine}`;
|
|
1019
|
+
return `${lineNum}→${normalizeForDisplay(line)}`;
|
|
719
1020
|
})
|
|
720
1021
|
.join('\n');
|
|
721
1022
|
// Calculate context range using smart boundary detection
|
|
722
1023
|
const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, adjustedEndLine, contextLines);
|
|
723
1024
|
const contextStart = smartBoundaries.start;
|
|
724
1025
|
const contextEnd = smartBoundaries.end;
|
|
725
|
-
// Extract old content for context (
|
|
1026
|
+
// Extract old content for context (compress whitespace for readability)
|
|
726
1027
|
const oldContextLines = lines.slice(contextStart - 1, contextEnd);
|
|
727
1028
|
const oldContent = oldContextLines
|
|
728
1029
|
.map((line, idx) => {
|
|
729
1030
|
const lineNum = contextStart + idx;
|
|
730
|
-
|
|
731
|
-
return `${lineNum}→${normalizedLine}`;
|
|
1031
|
+
return `${lineNum}→${normalizeForDisplay(line)}`;
|
|
732
1032
|
})
|
|
733
1033
|
.join('\n');
|
|
734
1034
|
// Replace the specified lines
|
|
@@ -740,13 +1040,12 @@ export class FilesystemMCPService {
|
|
|
740
1040
|
const newTotalLines = modifiedLines.length;
|
|
741
1041
|
const lineDifference = newContentLines.length - (adjustedEndLine - startLine + 1);
|
|
742
1042
|
const newContextEnd = Math.min(newTotalLines, contextEnd + lineDifference);
|
|
743
|
-
// Extract new content for context with line numbers
|
|
1043
|
+
// Extract new content for context with line numbers (compress whitespace)
|
|
744
1044
|
const newContextLines = modifiedLines.slice(contextStart - 1, newContextEnd);
|
|
745
1045
|
const newContextContent = newContextLines
|
|
746
1046
|
.map((line, idx) => {
|
|
747
1047
|
const lineNum = contextStart + idx;
|
|
748
|
-
|
|
749
|
-
return `${lineNum}→${normalizedLine}`;
|
|
1048
|
+
return `${lineNum}→${normalizeForDisplay(line)}`;
|
|
750
1049
|
})
|
|
751
1050
|
.join('\n');
|
|
752
1051
|
// Write the modified content back to file
|
|
@@ -761,8 +1060,7 @@ export class FilesystemMCPService {
|
|
|
761
1060
|
const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
|
|
762
1061
|
if (shouldFormat) {
|
|
763
1062
|
try {
|
|
764
|
-
|
|
765
|
-
stdio: 'pipe',
|
|
1063
|
+
await execAsync(`npx prettier --write "${fullPath}"`, {
|
|
766
1064
|
encoding: 'utf-8',
|
|
767
1065
|
});
|
|
768
1066
|
// Re-read the file after formatting to get the formatted content
|
|
@@ -771,13 +1069,12 @@ export class FilesystemMCPService {
|
|
|
771
1069
|
finalTotalLines = finalLines.length;
|
|
772
1070
|
// Recalculate the context end line based on formatted content
|
|
773
1071
|
finalContextEnd = Math.min(finalTotalLines, contextStart + (newContextEnd - contextStart));
|
|
774
|
-
// Extract formatted content for context
|
|
1072
|
+
// Extract formatted content for context (compress whitespace)
|
|
775
1073
|
const formattedContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
|
|
776
1074
|
finalContextContent = formattedContextLines
|
|
777
1075
|
.map((line, idx) => {
|
|
778
1076
|
const lineNum = contextStart + idx;
|
|
779
|
-
|
|
780
|
-
return `${lineNum}→${normalizedLine}`;
|
|
1077
|
+
return `${lineNum}→${normalizeForDisplay(line)}`;
|
|
781
1078
|
})
|
|
782
1079
|
.join('\n');
|
|
783
1080
|
}
|
|
@@ -789,15 +1086,18 @@ export class FilesystemMCPService {
|
|
|
789
1086
|
// Analyze code structure of the edited content (using formatted content if available)
|
|
790
1087
|
const editedContentLines = finalLines.slice(startLine - 1, startLine - 1 + newContentLines.length);
|
|
791
1088
|
const structureAnalysis = this.analyzeCodeStructure(finalLines.join('\n'), filePath, editedContentLines);
|
|
792
|
-
// Try to get diagnostics from
|
|
1089
|
+
// Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
|
|
793
1090
|
let diagnostics = [];
|
|
794
1091
|
try {
|
|
795
|
-
//
|
|
796
|
-
|
|
797
|
-
|
|
1092
|
+
// Request diagnostics without blocking (with timeout protection)
|
|
1093
|
+
const diagnosticsPromise = Promise.race([
|
|
1094
|
+
vscodeConnection.requestDiagnostics(fullPath),
|
|
1095
|
+
new Promise(resolve => setTimeout(() => resolve([]), 1000)), // 1s max wait
|
|
1096
|
+
]);
|
|
1097
|
+
diagnostics = await diagnosticsPromise;
|
|
798
1098
|
}
|
|
799
1099
|
catch (error) {
|
|
800
|
-
// Ignore diagnostics errors
|
|
1100
|
+
// Ignore diagnostics errors - they are optional
|
|
801
1101
|
}
|
|
802
1102
|
const result = {
|
|
803
1103
|
message: `✅ File edited successfully,Please check the edit results and pay attention to code boundary issues to avoid syntax errors caused by missing closed parts: ${filePath}\n` +
|
|
@@ -872,11 +1172,7 @@ export class FilesystemMCPService {
|
|
|
872
1172
|
if (structureAnalysis.indentationWarnings.length > 0) {
|
|
873
1173
|
structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
|
|
874
1174
|
}
|
|
875
|
-
//
|
|
876
|
-
if (structureAnalysis.codeBlockBoundary &&
|
|
877
|
-
structureAnalysis.codeBlockBoundary.suggestion) {
|
|
878
|
-
structureWarnings.push(`Boundary: ${structureAnalysis.codeBlockBoundary.suggestion}`);
|
|
879
|
-
}
|
|
1175
|
+
// Note: Boundary warnings removed - partial edits are common and expected
|
|
880
1176
|
// Format structure warnings
|
|
881
1177
|
if (structureWarnings.length > 0) {
|
|
882
1178
|
result.message += `\n\n🔍 Structure Analysis:\n`;
|
|
@@ -921,28 +1217,39 @@ export class FilesystemMCPService {
|
|
|
921
1217
|
}
|
|
922
1218
|
// Export a default instance
|
|
923
1219
|
export const filesystemService = new FilesystemMCPService();
|
|
924
|
-
// MCP Tool definitions for integration
|
|
925
1220
|
export const mcpTools = [
|
|
926
1221
|
{
|
|
927
1222
|
name: 'filesystem_read',
|
|
928
|
-
description: 'Read
|
|
1223
|
+
description: '📖 Read file content with line numbers. **SUPPORTS MULTIPLE FILES**: Pass either a single file path (string) or multiple file paths (array of strings) to read in one call. ⚠️ **IMPORTANT WORKFLOW**: (1) ALWAYS use ACE search tools FIRST (ace_text_search/ace_search_symbols/ace_file_outline) to locate the relevant code, (2) ONLY use filesystem_read when you know the approximate location and need precise line numbers for editing. **ANTI-PATTERN**: Reading files line-by-line from the top wastes tokens - use search instead! **USAGE**: Call without parameters to read entire file(s), or specify startLine/endLine for partial reads. Returns content with line numbers (format: "123→code") for precise editing. **MULTI-FILE EXAMPLE**: filePath=["src/component.ts", "src/utils.ts"] reads both files together.',
|
|
929
1224
|
inputSchema: {
|
|
930
1225
|
type: 'object',
|
|
931
1226
|
properties: {
|
|
932
1227
|
filePath: {
|
|
933
|
-
|
|
934
|
-
|
|
1228
|
+
oneOf: [
|
|
1229
|
+
{
|
|
1230
|
+
type: 'string',
|
|
1231
|
+
description: 'Path to a single file to read',
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
type: 'array',
|
|
1235
|
+
items: {
|
|
1236
|
+
type: 'string',
|
|
1237
|
+
},
|
|
1238
|
+
description: 'Array of file paths to read in one call',
|
|
1239
|
+
},
|
|
1240
|
+
],
|
|
1241
|
+
description: 'Path to the file(s) to read (single string or array of strings)',
|
|
935
1242
|
},
|
|
936
1243
|
startLine: {
|
|
937
1244
|
type: 'number',
|
|
938
|
-
description: 'Starting line number (1-indexed
|
|
1245
|
+
description: 'Optional: Starting line number (1-indexed). Omit to read from line 1. Applied to all files.',
|
|
939
1246
|
},
|
|
940
1247
|
endLine: {
|
|
941
1248
|
type: 'number',
|
|
942
|
-
description: 'Ending line number (1-indexed
|
|
1249
|
+
description: 'Optional: Ending line number (1-indexed). Omit to read to end of file. Applied to all files.',
|
|
943
1250
|
},
|
|
944
1251
|
},
|
|
945
|
-
required: ['filePath'
|
|
1252
|
+
required: ['filePath'],
|
|
946
1253
|
},
|
|
947
1254
|
},
|
|
948
1255
|
{
|
|
@@ -982,17 +1289,17 @@ export const mcpTools = [
|
|
|
982
1289
|
oneOf: [
|
|
983
1290
|
{
|
|
984
1291
|
type: 'string',
|
|
985
|
-
description: 'Path to a single file to delete'
|
|
1292
|
+
description: 'Path to a single file to delete',
|
|
986
1293
|
},
|
|
987
1294
|
{
|
|
988
1295
|
type: 'array',
|
|
989
1296
|
items: {
|
|
990
|
-
type: 'string'
|
|
1297
|
+
type: 'string',
|
|
991
1298
|
},
|
|
992
|
-
description: 'Array of file paths to delete'
|
|
993
|
-
}
|
|
1299
|
+
description: 'Array of file paths to delete',
|
|
1300
|
+
},
|
|
994
1301
|
],
|
|
995
|
-
description: 'Single file path or array of file paths to delete'
|
|
1302
|
+
description: 'Single file path or array of file paths to delete',
|
|
996
1303
|
},
|
|
997
1304
|
},
|
|
998
1305
|
// Make both optional, but at least one is required (validated in code)
|
|
@@ -1014,7 +1321,7 @@ export const mcpTools = [
|
|
|
1014
1321
|
},
|
|
1015
1322
|
{
|
|
1016
1323
|
name: 'filesystem_edit_search',
|
|
1017
|
-
description: '🎯 **RECOMMENDED** for most edits: Search-and-replace
|
|
1324
|
+
description: '🎯 **RECOMMENDED** for most edits: Search-and-replace with SMART FUZZY MATCHING that automatically handles whitespace differences. **WORKFLOW**: (1) Use ace_text_search/ace_search_symbols to locate code, (2) Use filesystem_read to view content, (3) Copy the code block you want to change (without line numbers), (4) Use THIS tool - whitespace will be normalized automatically. **WHY**: No line tracking, auto-handles spacing/tabs, finds best match. **BEST FOR**: Modifying functions, fixing bugs, updating logic.',
|
|
1018
1325
|
inputSchema: {
|
|
1019
1326
|
type: 'object',
|
|
1020
1327
|
properties: {
|
|
@@ -1024,20 +1331,20 @@ export const mcpTools = [
|
|
|
1024
1331
|
},
|
|
1025
1332
|
searchContent: {
|
|
1026
1333
|
type: 'string',
|
|
1027
|
-
description: '
|
|
1334
|
+
description: 'Content to find and replace. Copy from filesystem_read output WITHOUT line numbers (e.g., "123→"). Whitespace differences are automatically handled - focus on getting the content right.',
|
|
1028
1335
|
},
|
|
1029
1336
|
replaceContent: {
|
|
1030
1337
|
type: 'string',
|
|
1031
|
-
description: 'New content to replace
|
|
1338
|
+
description: 'New content to replace with. Indentation will be preserved automatically.',
|
|
1032
1339
|
},
|
|
1033
1340
|
occurrence: {
|
|
1034
1341
|
type: 'number',
|
|
1035
|
-
description: 'Which
|
|
1342
|
+
description: 'Which match to replace if multiple found (1-indexed). Default: 1 (best match first). Use -1 for all (not yet supported).',
|
|
1036
1343
|
default: 1,
|
|
1037
1344
|
},
|
|
1038
1345
|
contextLines: {
|
|
1039
1346
|
type: 'number',
|
|
1040
|
-
description: '
|
|
1347
|
+
description: 'Context lines to show before/after (default: 8)',
|
|
1041
1348
|
default: 8,
|
|
1042
1349
|
},
|
|
1043
1350
|
},
|
|
@@ -1046,7 +1353,7 @@ export const mcpTools = [
|
|
|
1046
1353
|
},
|
|
1047
1354
|
{
|
|
1048
1355
|
name: 'filesystem_edit',
|
|
1049
|
-
description:
|
|
1356
|
+
description: "🔧 Line-based editing for precise control. **WHEN TO USE**: (1) Adding completely new code sections, (2) Deleting specific line ranges, (3) When search-replace is not suitable. **WORKFLOW**: (1) Use ace_text_search/ace_file_outline to locate relevant area, (2) Use filesystem_read to get exact line numbers, (3) Use THIS tool with precise line ranges. **RECOMMENDATION**: For modifying existing code, use filesystem_edit_search instead - it's safer. **BEST PRACTICES**: Keep edits small (≤15 lines), double-check line numbers, verify bracket closure.",
|
|
1050
1357
|
inputSchema: {
|
|
1051
1358
|
type: 'object',
|
|
1052
1359
|
properties: {
|