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.
Files changed (46) hide show
  1. package/dist/api/chat.d.ts +0 -8
  2. package/dist/api/chat.js +1 -144
  3. package/dist/api/responses.d.ts +0 -11
  4. package/dist/api/responses.js +1 -189
  5. package/dist/api/systemPrompt.d.ts +1 -1
  6. package/dist/api/systemPrompt.js +90 -295
  7. package/dist/app.d.ts +2 -1
  8. package/dist/app.js +11 -13
  9. package/dist/cli.js +16 -3
  10. package/dist/hooks/useClipboard.js +4 -4
  11. package/dist/hooks/useGlobalNavigation.d.ts +1 -1
  12. package/dist/hooks/useKeyboardInput.d.ts +1 -0
  13. package/dist/hooks/useKeyboardInput.js +8 -4
  14. package/dist/hooks/useTerminalFocus.d.ts +5 -0
  15. package/dist/hooks/useTerminalFocus.js +22 -2
  16. package/dist/mcp/aceCodeSearch.d.ts +58 -4
  17. package/dist/mcp/aceCodeSearch.js +563 -20
  18. package/dist/mcp/filesystem.d.ts +59 -10
  19. package/dist/mcp/filesystem.js +431 -124
  20. package/dist/mcp/ideDiagnostics.d.ts +36 -0
  21. package/dist/mcp/ideDiagnostics.js +92 -0
  22. package/dist/ui/components/ChatInput.js +6 -3
  23. package/dist/ui/pages/ChatScreen.d.ts +4 -2
  24. package/dist/ui/pages/ChatScreen.js +31 -2
  25. package/dist/ui/pages/ConfigProfileScreen.d.ts +7 -0
  26. package/dist/ui/pages/ConfigProfileScreen.js +300 -0
  27. package/dist/ui/pages/{ApiConfigScreen.d.ts → ConfigScreen.d.ts} +1 -1
  28. package/dist/ui/pages/ConfigScreen.js +748 -0
  29. package/dist/ui/pages/WelcomeScreen.js +7 -18
  30. package/dist/utils/apiConfig.d.ts +0 -2
  31. package/dist/utils/apiConfig.js +12 -0
  32. package/dist/utils/configManager.d.ts +45 -0
  33. package/dist/utils/configManager.js +274 -0
  34. package/dist/utils/contextCompressor.js +355 -49
  35. package/dist/utils/escapeHandler.d.ts +79 -0
  36. package/dist/utils/escapeHandler.js +153 -0
  37. package/dist/utils/incrementalSnapshot.js +2 -1
  38. package/dist/utils/mcpToolsManager.js +44 -0
  39. package/dist/utils/retryUtils.js +6 -0
  40. package/dist/utils/textBuffer.js +13 -15
  41. package/dist/utils/vscodeConnection.js +26 -11
  42. package/dist/utils/workspaceSnapshot.js +2 -1
  43. package/package.json +2 -1
  44. package/dist/ui/pages/ApiConfigScreen.js +0 -161
  45. package/dist/ui/pages/ModelConfigScreen.d.ts +0 -8
  46. package/dist/ui/pages/ModelConfigScreen.js +0 -504
@@ -1,9 +1,13 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
- import { execSync } from 'child_process';
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
- // Check if edit is at a code block boundary
153
- const lastLine = editedLines[editedLines.length - 1]?.trim() || '';
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 specified line range
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 (startLine < 1) {
470
+ if (actualStartLine < 1) {
259
471
  throw new Error('Start line must be greater than 0');
260
472
  }
261
- if (endLine < startLine) {
473
+ if (actualEndLine < actualStartLine) {
262
474
  throw new Error('End line must be greater than or equal to start line');
263
475
  }
264
- if (startLine > totalLines) {
265
- throw new Error(`Start line ${startLine} exceeds file length ${totalLines}`);
476
+ if (actualStartLine > totalLines) {
477
+ throw new Error(`Start line ${actualStartLine} exceeds file length ${totalLines}`);
266
478
  }
267
- const start = startLine;
268
- const end = Math.min(totalLines, endLine);
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
- // Normalize whitespace to reduce token usage
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 is SAFER than line-based editing as it automatically handles code boundaries.
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 - Exact content to search for (must match precisely, including whitespace)
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
- const normalizedSearch = searchContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
457
- const normalizedContent = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
458
- // Apply whitespace normalization for matching (same as getFileContent output)
459
- const normalizeWhitespace = (text) => text.replace(/\t/g, ' ').replace(/ +/g, ' ');
460
- const normalizedSearchForMatch = normalizeWhitespace(normalizedSearch);
461
- const normalizedContentForMatch = normalizeWhitespace(normalizedContent);
462
- // Find all matches by comparing normalized versions line by line
463
- // This avoids complex character position mapping
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 searchLines = normalizedSearchForMatch.split('\n');
466
- const contentLines = normalizedContentForMatch.split('\n');
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
- let isMatch = true;
469
- for (let j = 0; j < searchLines.length; j++) {
470
- if (contentLines[i + j] !== searchLines[j]) {
471
- isMatch = false;
472
- break;
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
- if (isMatch) {
476
- const startLine = i + 1; // Convert to 1-indexed
477
- const endLine = startLine + searchLines.length - 1;
478
- matches.push({ startLine, endLine });
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
- // Handle no matches
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
- throw new Error(`Search content not found in file. Please verify the exact content including whitespace and indentation.`);
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.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
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
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
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
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
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 if applicable
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
- execSync(`npx prettier --write "${fullPath}"`, {
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
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
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 VS Code
874
+ // Get diagnostics from IDE (VSCode or JetBrains) - non-blocking, fire-and-forget
576
875
  let diagnostics = [];
577
876
  try {
578
- await new Promise(resolve => setTimeout(resolve, 500));
579
- diagnostics = await vscodeConnection.requestDiagnostics(fullPath);
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
- if (structureAnalysis.codeBlockBoundary &&
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
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
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 (including the lines to be replaced)
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
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
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
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
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
- execSync(`npx prettier --write "${fullPath}"`, {
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 with line numbers
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
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
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 VS Code after editing
1089
+ // Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
793
1090
  let diagnostics = [];
794
1091
  try {
795
- // Wait a bit for VS Code to process the file change
796
- await new Promise(resolve => setTimeout(resolve, 500));
797
- diagnostics = await vscodeConnection.requestDiagnostics(fullPath);
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, they are optional
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
- // Add code block boundary warnings
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 the content of a file within specified line range. The returned content includes line numbers (format: "lineNum→content") for precise editing. You MUST specify startLine and endLine. To read the entire file, use startLine=1 and a large endLine value (e.g., 500). IMPORTANT: When you need to edit a file, you MUST read it first to see the exact line numbers and current content. NOTE: If the path points to a directory, this tool will automatically list its contents instead of throwing an error.',
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
- type: 'string',
934
- description: 'Path to the file to read (or directory to list)',
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, inclusive). Must be >= 1.',
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, inclusive). Can exceed file length (will be capped automatically).',
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', 'startLine', 'endLine'],
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 editing with AUTOMATIC boundary detection. **WHY USE THIS**: (1) NO need to count line numbers - just copy the exact code you want to change, (2) SAFER - automatically handles code boundaries to prevent bracket/tag mismatches, (3) CLEARER intent - shows exactly what you\'re changing. **BEST FOR**: Modifying existing functions, fixing bugs, updating logic. **WORKFLOW**: (1) Read file with filesystem_read, (2) Copy exact code block to change (including whitespace!), (3) Provide the replacement. **HANDLES**: Multiple occurrences, whitespace normalization, structure validation. **TIP**: For new code additions, use filesystem_edit with line numbers instead.',
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: '⚠️ EXACT content to find and replace. MUST match precisely including indentation and whitespace. Copy directly from filesystem_read output (WITHOUT line numbers like "123→").',
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 the search content. Should maintain consistent indentation with surrounding code.',
1338
+ description: 'New content to replace with. Indentation will be preserved automatically.',
1032
1339
  },
1033
1340
  occurrence: {
1034
1341
  type: 'number',
1035
- description: 'Which occurrence to replace if multiple matches found (1-indexed). Default: 1 (first match). Use -1 for all occurrences (not yet supported).',
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: 'Number of context lines to show before/after edit (default: 8)',
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: '🔧 Line-based editing for precise line range replacements. **WHEN TO USE**: (1) Adding completely new code sections, (2) Deleting specific line ranges, (3) When you need exact line-number control. **LIMITATIONS**: Requires manual line number tracking, higher risk of boundary errors. **RECOMMENDATION**: For most edits, use filesystem_edit_search instead - it\'s safer and easier. **BEST PRACTICES**: (1) Keep edits small (≤15 lines), (2) Always read file first to get exact line numbers, (3) Double-check boundaries for brackets/tags. **SMART FEATURES**: Auto-detects bracket/tag mismatches, indentation issues, context auto-extends to show complete code blocks.',
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: {