snow-ai 0.2.23 → 0.2.25

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 (43) hide show
  1. package/dist/agents/compactAgent.d.ts +55 -0
  2. package/dist/agents/compactAgent.js +301 -0
  3. package/dist/api/chat.d.ts +0 -8
  4. package/dist/api/chat.js +1 -144
  5. package/dist/api/responses.d.ts +0 -11
  6. package/dist/api/responses.js +1 -189
  7. package/dist/api/systemPrompt.d.ts +1 -1
  8. package/dist/api/systemPrompt.js +80 -206
  9. package/dist/app.d.ts +2 -1
  10. package/dist/app.js +11 -13
  11. package/dist/cli.js +23 -3
  12. package/dist/hooks/useConversation.js +51 -7
  13. package/dist/hooks/useGlobalNavigation.d.ts +1 -1
  14. package/dist/hooks/useKeyboardInput.js +14 -8
  15. package/dist/mcp/filesystem.d.ts +49 -6
  16. package/dist/mcp/filesystem.js +243 -86
  17. package/dist/mcp/websearch.d.ts +118 -0
  18. package/dist/mcp/websearch.js +451 -0
  19. package/dist/ui/components/ToolResultPreview.js +60 -1
  20. package/dist/ui/pages/ChatScreen.d.ts +4 -2
  21. package/dist/ui/pages/ChatScreen.js +62 -14
  22. package/dist/ui/pages/{ApiConfigScreen.d.ts → ConfigScreen.d.ts} +1 -1
  23. package/dist/ui/pages/ConfigScreen.js +549 -0
  24. package/dist/ui/pages/{ModelConfigScreen.d.ts → ProxyConfigScreen.d.ts} +1 -1
  25. package/dist/ui/pages/ProxyConfigScreen.js +143 -0
  26. package/dist/ui/pages/WelcomeScreen.js +15 -15
  27. package/dist/utils/apiConfig.d.ts +8 -2
  28. package/dist/utils/apiConfig.js +21 -0
  29. package/dist/utils/commandExecutor.d.ts +1 -1
  30. package/dist/utils/contextCompressor.js +363 -49
  31. package/dist/utils/mcpToolsManager.d.ts +1 -1
  32. package/dist/utils/mcpToolsManager.js +106 -6
  33. package/dist/utils/resourceMonitor.d.ts +65 -0
  34. package/dist/utils/resourceMonitor.js +175 -0
  35. package/dist/utils/retryUtils.js +6 -0
  36. package/dist/utils/sessionManager.d.ts +1 -0
  37. package/dist/utils/sessionManager.js +10 -0
  38. package/dist/utils/textBuffer.js +7 -2
  39. package/dist/utils/toolExecutor.d.ts +2 -2
  40. package/dist/utils/toolExecutor.js +4 -4
  41. package/package.json +5 -1
  42. package/dist/ui/pages/ApiConfigScreen.js +0 -161
  43. package/dist/ui/pages/ModelConfigScreen.js +0 -467
@@ -1,9 +1,11 @@
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';
4
5
  import { vscodeConnection } from '../utils/vscodeConnection.js';
5
6
  import { incrementalSnapshotManager } from '../utils/incrementalSnapshot.js';
6
7
  const { resolve, dirname, isAbsolute } = path;
8
+ const execAsync = promisify(exec);
7
9
  /**
8
10
  * Filesystem MCP Service
9
11
  * Provides basic file operations: read, create, and delete files
@@ -43,6 +45,108 @@ export class FilesystemMCPService {
43
45
  });
44
46
  this.basePath = resolve(basePath);
45
47
  }
48
+ /**
49
+ * Calculate similarity between two strings using a smarter algorithm
50
+ * This normalizes whitespace first to avoid false negatives from spacing differences
51
+ * Returns a value between 0 (completely different) and 1 (identical)
52
+ */
53
+ calculateSimilarity(str1, str2) {
54
+ // Normalize whitespace for comparison: collapse all whitespace to single spaces
55
+ const normalize = (s) => s.replace(/\s+/g, ' ').trim();
56
+ const norm1 = normalize(str1);
57
+ const norm2 = normalize(str2);
58
+ const len1 = norm1.length;
59
+ const len2 = norm2.length;
60
+ if (len1 === 0)
61
+ return len2 === 0 ? 1 : 0;
62
+ if (len2 === 0)
63
+ return 0;
64
+ // Use Levenshtein distance for better similarity calculation
65
+ const maxLen = Math.max(len1, len2);
66
+ const distance = this.levenshteinDistance(norm1, norm2);
67
+ return 1 - distance / maxLen;
68
+ }
69
+ /**
70
+ * Calculate Levenshtein distance between two strings
71
+ */
72
+ levenshteinDistance(str1, str2) {
73
+ const len1 = str1.length;
74
+ const len2 = str2.length;
75
+ // Create distance matrix
76
+ const matrix = [];
77
+ for (let i = 0; i <= len1; i++) {
78
+ matrix[i] = [i];
79
+ }
80
+ for (let j = 0; j <= len2; j++) {
81
+ matrix[0][j] = j;
82
+ }
83
+ // Fill matrix
84
+ for (let i = 1; i <= len1; i++) {
85
+ for (let j = 1; j <= len2; j++) {
86
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
87
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
88
+ matrix[i][j - 1] + 1, // insertion
89
+ matrix[i - 1][j - 1] + cost);
90
+ }
91
+ }
92
+ return matrix[len1][len2];
93
+ }
94
+ /**
95
+ * Find the closest matching candidates in the file content
96
+ * Returns top N candidates sorted by similarity
97
+ */
98
+ findClosestMatches(searchContent, fileLines, topN = 3) {
99
+ const searchLines = searchContent.split('\n');
100
+ const candidates = [];
101
+ // Normalize whitespace for display only (makes preview more readable)
102
+ const normalizeForDisplay = (line) => line.replace(/\t/g, ' ').replace(/ +/g, ' ');
103
+ // Try to find candidates by sliding window
104
+ for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
105
+ const candidateLines = fileLines.slice(i, i + searchLines.length);
106
+ const candidateContent = candidateLines.join('\n');
107
+ const similarity = this.calculateSimilarity(searchContent, candidateContent);
108
+ // Only consider candidates with >50% similarity
109
+ if (similarity > 0.5) {
110
+ candidates.push({
111
+ startLine: i + 1,
112
+ endLine: i + searchLines.length,
113
+ similarity,
114
+ preview: candidateLines
115
+ .map((line, idx) => `${i + idx + 1}→${normalizeForDisplay(line)}`)
116
+ .join('\n'),
117
+ });
118
+ }
119
+ }
120
+ // Sort by similarity descending and return top N
121
+ return candidates
122
+ .sort((a, b) => b.similarity - a.similarity)
123
+ .slice(0, topN);
124
+ }
125
+ /**
126
+ * Generate a helpful diff message showing differences between search and actual content
127
+ * Note: This is ONLY for display purposes. Tabs/spaces are normalized for better readability.
128
+ */
129
+ generateDiffMessage(searchContent, actualContent, maxLines = 10) {
130
+ const searchLines = searchContent.split('\n');
131
+ const actualLines = actualContent.split('\n');
132
+ const diffLines = [];
133
+ const maxLen = Math.max(searchLines.length, actualLines.length);
134
+ // Normalize whitespace for display only (makes diff more readable)
135
+ const normalizeForDisplay = (line) => line.replace(/\t/g, ' ').replace(/ +/g, ' ');
136
+ for (let i = 0; i < Math.min(maxLen, maxLines); i++) {
137
+ const searchLine = searchLines[i] || '';
138
+ const actualLine = actualLines[i] || '';
139
+ if (searchLine !== actualLine) {
140
+ diffLines.push(`Line ${i + 1}:`);
141
+ diffLines.push(` Search: ${JSON.stringify(normalizeForDisplay(searchLine))}`);
142
+ diffLines.push(` Actual: ${JSON.stringify(normalizeForDisplay(actualLine))}`);
143
+ }
144
+ }
145
+ if (maxLen > maxLines) {
146
+ diffLines.push(`... (${maxLen - maxLines} more lines)`);
147
+ }
148
+ return diffLines.join('\n');
149
+ }
46
150
  /**
47
151
  * Analyze code structure for balance and completeness
48
152
  * Helps AI identify bracket mismatches, unclosed tags, and boundary issues
@@ -223,10 +327,10 @@ export class FilesystemMCPService {
223
327
  return { start: contextStart, end: contextEnd, extended };
224
328
  }
225
329
  /**
226
- * Get the content of a file with specified line range
330
+ * Get the content of a file with optional line range
227
331
  * @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)
332
+ * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1)
333
+ * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to 500 or file end)
230
334
  * @returns Object containing the requested content with line numbers and metadata
231
335
  * @throws Error if file doesn't exist or cannot be read
232
336
  */
@@ -254,29 +358,30 @@ export class FilesystemMCPService {
254
358
  // Parse lines
255
359
  const lines = content.split('\n');
256
360
  const totalLines = lines.length;
361
+ // Default values and logic:
362
+ // - No params: read entire file (1 to totalLines)
363
+ // - Only startLine: read from startLine to end of file
364
+ // - Both params: read from startLine to endLine
365
+ const actualStartLine = startLine ?? 1;
366
+ const actualEndLine = endLine ?? totalLines;
257
367
  // Validate and adjust line numbers
258
- if (startLine < 1) {
368
+ if (actualStartLine < 1) {
259
369
  throw new Error('Start line must be greater than 0');
260
370
  }
261
- if (endLine < startLine) {
371
+ if (actualEndLine < actualStartLine) {
262
372
  throw new Error('End line must be greater than or equal to start line');
263
373
  }
264
- if (startLine > totalLines) {
265
- throw new Error(`Start line ${startLine} exceeds file length ${totalLines}`);
374
+ if (actualStartLine > totalLines) {
375
+ throw new Error(`Start line ${actualStartLine} exceeds file length ${totalLines}`);
266
376
  }
267
- const start = startLine;
268
- const end = Math.min(totalLines, endLine);
377
+ const start = actualStartLine;
378
+ const end = Math.min(totalLines, actualEndLine);
269
379
  // Extract specified lines (convert to 0-indexed) and add line numbers
270
380
  const selectedLines = lines.slice(start - 1, end);
271
381
  // Format with line numbers (no padding to save tokens)
272
- // Normalize whitespace: tabs → single space, multiple spaces → single space
273
382
  const numberedLines = selectedLines.map((line, index) => {
274
383
  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}`;
384
+ return `${lineNum}→${line}`;
280
385
  });
281
386
  const partialContent = numberedLines.join('\n');
282
387
  return {
@@ -432,10 +537,10 @@ export class FilesystemMCPService {
432
537
  }
433
538
  /**
434
539
  * 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.
540
+ * This method uses SMART MATCHING to handle whitespace differences automatically.
436
541
  *
437
542
  * @param filePath - Path to the file to edit
438
- * @param searchContent - Exact content to search for (must match precisely, including whitespace)
543
+ * @param searchContent - Content to search for (whitespace will be normalized automatically)
439
544
  * @param replaceContent - New content to replace the search content with
440
545
  * @param occurrence - Which occurrence to replace (1-indexed, default: 1, use -1 for all)
441
546
  * @param contextLines - Number of context lines to return before and after the edit (default: 8)
@@ -453,34 +558,70 @@ export class FilesystemMCPService {
453
558
  const content = await fs.readFile(fullPath, 'utf-8');
454
559
  const lines = content.split('\n');
455
560
  // 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
561
+ const normalizedSearch = searchContent
562
+ .replace(/\r\n/g, '\n')
563
+ .replace(/\r/g, '\n');
564
+ const normalizedContent = content
565
+ .replace(/\r\n/g, '\n')
566
+ .replace(/\r/g, '\n');
567
+ // Split into lines for matching
568
+ const searchLines = normalizedSearch.split('\n');
569
+ const contentLines = normalizedContent.split('\n');
570
+ // Find all matches using smart fuzzy matching (auto-handles whitespace)
464
571
  const matches = [];
465
- const searchLines = normalizedSearchForMatch.split('\n');
466
- const contentLines = normalizedContentForMatch.split('\n');
572
+ const threshold = 0.75; // Lower threshold for better tolerance
467
573
  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;
473
- }
474
- }
475
- if (isMatch) {
476
- const startLine = i + 1; // Convert to 1-indexed
477
- const endLine = startLine + searchLines.length - 1;
478
- matches.push({ startLine, endLine });
574
+ const candidateLines = contentLines.slice(i, i + searchLines.length);
575
+ const candidateContent = candidateLines.join('\n');
576
+ const similarity = this.calculateSimilarity(normalizedSearch, candidateContent);
577
+ // Accept matches above threshold
578
+ if (similarity >= threshold) {
579
+ matches.push({
580
+ startLine: i + 1,
581
+ endLine: i + searchLines.length,
582
+ similarity,
583
+ });
479
584
  }
480
585
  }
481
- // Handle no matches
586
+ // Sort by similarity descending (best match first)
587
+ matches.sort((a, b) => b.similarity - a.similarity);
588
+ // Handle no matches with enhanced error message
482
589
  if (matches.length === 0) {
483
- throw new Error(`Search content not found in file. Please verify the exact content including whitespace and indentation.`);
590
+ // Find closest matches for suggestions
591
+ const closestMatches = this.findClosestMatches(normalizedSearch, normalizedContent.split('\n'), 3);
592
+ let errorMessage = `❌ Search content not found in file: ${filePath}\n\n`;
593
+ errorMessage += `🔍 Using smart fuzzy matching (threshold: 75%)\n\n`;
594
+ if (closestMatches.length > 0) {
595
+ errorMessage += `💡 Found ${closestMatches.length} similar location(s):\n\n`;
596
+ closestMatches.forEach((candidate, idx) => {
597
+ errorMessage += `${idx + 1}. Lines ${candidate.startLine}-${candidate.endLine} (${(candidate.similarity * 100).toFixed(0)}% match):\n`;
598
+ errorMessage += `${candidate.preview}\n\n`;
599
+ });
600
+ // Show diff with the closest match
601
+ const bestMatch = closestMatches[0];
602
+ if (bestMatch) {
603
+ const bestMatchLines = lines.slice(bestMatch.startLine - 1, bestMatch.endLine);
604
+ const bestMatchContent = bestMatchLines.join('\n');
605
+ const diffMsg = this.generateDiffMessage(normalizedSearch, bestMatchContent, 5);
606
+ if (diffMsg) {
607
+ errorMessage += `📊 Difference with closest match:\n${diffMsg}\n\n`;
608
+ }
609
+ }
610
+ errorMessage += `💡 Suggestions:\n`;
611
+ errorMessage += ` • Make sure you copied content from filesystem_read (without "123→")\n`;
612
+ errorMessage += ` • Whitespace differences are automatically handled\n`;
613
+ errorMessage += ` • Try copying a larger or smaller code block\n`;
614
+ }
615
+ else {
616
+ errorMessage += `⚠️ No similar content found in the file.\n\n`;
617
+ errorMessage += `📝 What you searched for (first 5 lines, formatted):\n`;
618
+ const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ').trim();
619
+ searchLines.slice(0, 5).forEach((line, idx) => {
620
+ errorMessage += `${idx + 1}. ${JSON.stringify(normalizeForDisplay(line))}\n`;
621
+ });
622
+ errorMessage += `\n💡 Copy exact content from filesystem_read (without line numbers)\n`;
623
+ }
624
+ throw new Error(errorMessage);
484
625
  }
485
626
  // Handle occurrence selection
486
627
  let selectedMatch;
@@ -494,9 +635,7 @@ export class FilesystemMCPService {
494
635
  }
495
636
  }
496
637
  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(', ')}`);
638
+ throw new Error(`Invalid occurrence ${occurrence}. Found ${matches.length} match(es) at lines: ${matches.map(m => m.startLine).join(', ')}`);
500
639
  }
501
640
  else {
502
641
  selectedMatch = matches[occurrence - 1];
@@ -505,19 +644,21 @@ export class FilesystemMCPService {
505
644
  // Backup file before editing
506
645
  await incrementalSnapshotManager.backupFile(fullPath);
507
646
  // Perform the replacement by replacing the matched lines
508
- const normalizedReplace = replaceContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
647
+ const normalizedReplace = replaceContent
648
+ .replace(/\r\n/g, '\n')
649
+ .replace(/\r/g, '\n');
509
650
  const beforeLines = lines.slice(0, startLine - 1);
510
651
  const afterLines = lines.slice(endLine);
511
652
  const replaceLines = normalizedReplace.split('\n');
512
653
  const modifiedLines = [...beforeLines, ...replaceLines, ...afterLines];
513
654
  const modifiedContent = modifiedLines.join('\n');
514
- // Calculate replaced content for display
655
+ // Calculate replaced content for display (compress whitespace for readability)
656
+ const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
515
657
  const replacedLines = lines.slice(startLine - 1, endLine);
516
658
  const replacedContent = replacedLines
517
659
  .map((line, idx) => {
518
660
  const lineNum = startLine + idx;
519
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
520
- return `${lineNum}→${normalizedLine}`;
661
+ return `${lineNum}→${normalizeForDisplay(line)}`;
521
662
  })
522
663
  .join('\n');
523
664
  // Calculate context boundaries
@@ -525,18 +666,17 @@ export class FilesystemMCPService {
525
666
  const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, endLine, contextLines);
526
667
  const contextStart = smartBoundaries.start;
527
668
  const contextEnd = smartBoundaries.end;
528
- // Extract old content for context
669
+ // Extract old content for context (compress whitespace for readability)
529
670
  const oldContextLines = lines.slice(contextStart - 1, contextEnd);
530
671
  const oldContent = oldContextLines
531
672
  .map((line, idx) => {
532
673
  const lineNum = contextStart + idx;
533
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
534
- return `${lineNum}→${normalizedLine}`;
674
+ return `${lineNum}→${normalizeForDisplay(line)}`;
535
675
  })
536
676
  .join('\n');
537
677
  // Write the modified content
538
678
  await fs.writeFile(fullPath, modifiedContent, 'utf-8');
539
- // Format with Prettier if applicable
679
+ // Format with Prettier asynchronously (non-blocking)
540
680
  let finalContent = modifiedContent;
541
681
  let finalLines = modifiedLines;
542
682
  let finalTotalLines = modifiedLines.length;
@@ -546,8 +686,7 @@ export class FilesystemMCPService {
546
686
  const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
547
687
  if (shouldFormat) {
548
688
  try {
549
- execSync(`npx prettier --write "${fullPath}"`, {
550
- stdio: 'pipe',
689
+ await execAsync(`npx prettier --write "${fullPath}"`, {
551
690
  encoding: 'utf-8',
552
691
  });
553
692
  // Re-read the file after formatting
@@ -560,13 +699,12 @@ export class FilesystemMCPService {
560
699
  // Continue with unformatted content
561
700
  }
562
701
  }
563
- // Extract new content for context
702
+ // Extract new content for context (compress whitespace for readability)
564
703
  const newContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
565
704
  const newContextContent = newContextLines
566
705
  .map((line, idx) => {
567
706
  const lineNum = contextStart + idx;
568
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
569
- return `${lineNum}→${normalizedLine}`;
707
+ return `${lineNum}→${normalizeForDisplay(line)}`;
570
708
  })
571
709
  .join('\n');
572
710
  // Analyze code structure
@@ -710,25 +848,25 @@ export class FilesystemMCPService {
710
848
  // Backup file before editing
711
849
  await incrementalSnapshotManager.backupFile(fullPath);
712
850
  // Extract the lines that will be replaced (for comparison)
851
+ // Compress whitespace for display readability
852
+ const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
713
853
  const replacedLines = lines.slice(startLine - 1, adjustedEndLine);
714
854
  const replacedContent = replacedLines
715
855
  .map((line, idx) => {
716
856
  const lineNum = startLine + idx;
717
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
718
- return `${lineNum}→${normalizedLine}`;
857
+ return `${lineNum}→${normalizeForDisplay(line)}`;
719
858
  })
720
859
  .join('\n');
721
860
  // Calculate context range using smart boundary detection
722
861
  const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, adjustedEndLine, contextLines);
723
862
  const contextStart = smartBoundaries.start;
724
863
  const contextEnd = smartBoundaries.end;
725
- // Extract old content for context (including the lines to be replaced)
864
+ // Extract old content for context (compress whitespace for readability)
726
865
  const oldContextLines = lines.slice(contextStart - 1, contextEnd);
727
866
  const oldContent = oldContextLines
728
867
  .map((line, idx) => {
729
868
  const lineNum = contextStart + idx;
730
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
731
- return `${lineNum}→${normalizedLine}`;
869
+ return `${lineNum}→${normalizeForDisplay(line)}`;
732
870
  })
733
871
  .join('\n');
734
872
  // Replace the specified lines
@@ -740,13 +878,12 @@ export class FilesystemMCPService {
740
878
  const newTotalLines = modifiedLines.length;
741
879
  const lineDifference = newContentLines.length - (adjustedEndLine - startLine + 1);
742
880
  const newContextEnd = Math.min(newTotalLines, contextEnd + lineDifference);
743
- // Extract new content for context with line numbers
881
+ // Extract new content for context with line numbers (compress whitespace)
744
882
  const newContextLines = modifiedLines.slice(contextStart - 1, newContextEnd);
745
883
  const newContextContent = newContextLines
746
884
  .map((line, idx) => {
747
885
  const lineNum = contextStart + idx;
748
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
749
- return `${lineNum}→${normalizedLine}`;
886
+ return `${lineNum}→${normalizeForDisplay(line)}`;
750
887
  })
751
888
  .join('\n');
752
889
  // Write the modified content back to file
@@ -761,8 +898,7 @@ export class FilesystemMCPService {
761
898
  const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
762
899
  if (shouldFormat) {
763
900
  try {
764
- execSync(`npx prettier --write "${fullPath}"`, {
765
- stdio: 'pipe',
901
+ await execAsync(`npx prettier --write "${fullPath}"`, {
766
902
  encoding: 'utf-8',
767
903
  });
768
904
  // Re-read the file after formatting to get the formatted content
@@ -771,13 +907,12 @@ export class FilesystemMCPService {
771
907
  finalTotalLines = finalLines.length;
772
908
  // Recalculate the context end line based on formatted content
773
909
  finalContextEnd = Math.min(finalTotalLines, contextStart + (newContextEnd - contextStart));
774
- // Extract formatted content for context with line numbers
910
+ // Extract formatted content for context (compress whitespace)
775
911
  const formattedContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
776
912
  finalContextContent = formattedContextLines
777
913
  .map((line, idx) => {
778
914
  const lineNum = contextStart + idx;
779
- const normalizedLine = line.replace(/\t/g, ' ').replace(/ +/g, ' ');
780
- return `${lineNum}→${normalizedLine}`;
915
+ return `${lineNum}→${normalizeForDisplay(line)}`;
781
916
  })
782
917
  .join('\n');
783
918
  }
@@ -921,11 +1056,33 @@ export class FilesystemMCPService {
921
1056
  }
922
1057
  // Export a default instance
923
1058
  export const filesystemService = new FilesystemMCPService();
924
- // MCP Tool definitions for integration
1059
+ /**
1060
+ * MCP Tool definitions for integration
1061
+ *
1062
+ * 🎯 **RECOMMENDED WORKFLOW FOR AI AGENTS**:
1063
+ *
1064
+ * 1️⃣ **SEARCH FIRST** (DON'T skip this!):
1065
+ * - Use ace_text_search() to find code patterns/strings
1066
+ * - Use ace_search_symbols() to find functions/classes by name
1067
+ * - Use ace_file_outline() to understand file structure
1068
+ *
1069
+ * 2️⃣ **READ STRATEGICALLY** (Only after search):
1070
+ * - Use filesystem_read() WITHOUT line numbers to read entire file
1071
+ * - OR use filesystem_read(filePath, startLine, endLine) to read specific range
1072
+ * - ⚠️ AVOID reading files line-by-line from top - wastes tokens!
1073
+ *
1074
+ * 3️⃣ **EDIT SAFELY**:
1075
+ * - PREFER filesystem_edit_search() for modifying existing code (no line counting!)
1076
+ * - Use filesystem_edit() only for adding new code or when search-replace doesn't fit
1077
+ *
1078
+ * 📊 **TOKEN EFFICIENCY**:
1079
+ * - ❌ BAD: Read file top-to-bottom, repeat reading, blind scanning
1080
+ * - ✅ GOOD: Search → Targeted read → Edit with context
1081
+ */
925
1082
  export const mcpTools = [
926
1083
  {
927
1084
  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.',
1085
+ description: '📖 Read file content with line numbers. ⚠️ **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, or specify startLine/endLine for partial reads. Returns content with line numbers (format: "123→code") for precise editing.',
929
1086
  inputSchema: {
930
1087
  type: 'object',
931
1088
  properties: {
@@ -935,14 +1092,14 @@ export const mcpTools = [
935
1092
  },
936
1093
  startLine: {
937
1094
  type: 'number',
938
- description: 'Starting line number (1-indexed, inclusive). Must be >= 1.',
1095
+ description: 'Optional: Starting line number (1-indexed). Omit to read from line 1.',
939
1096
  },
940
1097
  endLine: {
941
1098
  type: 'number',
942
- description: 'Ending line number (1-indexed, inclusive). Can exceed file length (will be capped automatically).',
1099
+ description: 'Optional: Ending line number (1-indexed). Omit to read to end of file.',
943
1100
  },
944
1101
  },
945
- required: ['filePath', 'startLine', 'endLine'],
1102
+ required: ['filePath'],
946
1103
  },
947
1104
  },
948
1105
  {
@@ -982,17 +1139,17 @@ export const mcpTools = [
982
1139
  oneOf: [
983
1140
  {
984
1141
  type: 'string',
985
- description: 'Path to a single file to delete'
1142
+ description: 'Path to a single file to delete',
986
1143
  },
987
1144
  {
988
1145
  type: 'array',
989
1146
  items: {
990
- type: 'string'
1147
+ type: 'string',
991
1148
  },
992
- description: 'Array of file paths to delete'
993
- }
1149
+ description: 'Array of file paths to delete',
1150
+ },
994
1151
  ],
995
- description: 'Single file path or array of file paths to delete'
1152
+ description: 'Single file path or array of file paths to delete',
996
1153
  },
997
1154
  },
998
1155
  // Make both optional, but at least one is required (validated in code)
@@ -1014,7 +1171,7 @@ export const mcpTools = [
1014
1171
  },
1015
1172
  {
1016
1173
  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.',
1174
+ 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
1175
  inputSchema: {
1019
1176
  type: 'object',
1020
1177
  properties: {
@@ -1024,20 +1181,20 @@ export const mcpTools = [
1024
1181
  },
1025
1182
  searchContent: {
1026
1183
  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→").',
1184
+ 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
1185
  },
1029
1186
  replaceContent: {
1030
1187
  type: 'string',
1031
- description: 'New content to replace the search content. Should maintain consistent indentation with surrounding code.',
1188
+ description: 'New content to replace with. Indentation will be preserved automatically.',
1032
1189
  },
1033
1190
  occurrence: {
1034
1191
  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).',
1192
+ description: 'Which match to replace if multiple found (1-indexed). Default: 1 (best match first). Use -1 for all (not yet supported).',
1036
1193
  default: 1,
1037
1194
  },
1038
1195
  contextLines: {
1039
1196
  type: 'number',
1040
- description: 'Number of context lines to show before/after edit (default: 8)',
1197
+ description: 'Context lines to show before/after (default: 8)',
1041
1198
  default: 8,
1042
1199
  },
1043
1200
  },
@@ -1046,7 +1203,7 @@ export const mcpTools = [
1046
1203
  },
1047
1204
  {
1048
1205
  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.',
1206
+ 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
1207
  inputSchema: {
1051
1208
  type: 'object',
1052
1209
  properties: {