snow-ai 0.2.25 β†’ 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.
@@ -2,8 +2,10 @@ import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
3
  import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
+ // IDE connection supports both VSCode and JetBrains IDEs
5
6
  import { vscodeConnection } from '../utils/vscodeConnection.js';
6
7
  import { incrementalSnapshotManager } from '../utils/incrementalSnapshot.js';
8
+ import { tryUnescapeFix, trimPairIfPossible, isOverEscaped, } from '../utils/escapeHandler.js';
7
9
  const { resolve, dirname, isAbsolute } = path;
8
10
  const execAsync = promisify(exec);
9
11
  /**
@@ -50,7 +52,7 @@ export class FilesystemMCPService {
50
52
  * This normalizes whitespace first to avoid false negatives from spacing differences
51
53
  * Returns a value between 0 (completely different) and 1 (identical)
52
54
  */
53
- calculateSimilarity(str1, str2) {
55
+ calculateSimilarity(str1, str2, threshold = 0) {
54
56
  // Normalize whitespace for comparison: collapse all whitespace to single spaces
55
57
  const normalize = (s) => s.replace(/\s+/g, ' ').trim();
56
58
  const norm1 = normalize(str1);
@@ -61,52 +63,89 @@ export class FilesystemMCPService {
61
63
  return len2 === 0 ? 1 : 0;
62
64
  if (len2 === 0)
63
65
  return 0;
64
- // Use Levenshtein distance for better similarity calculation
66
+ // Quick length check - if lengths differ too much, similarity can't be above threshold
65
67
  const maxLen = Math.max(len1, len2);
66
- const distance = this.levenshteinDistance(norm1, norm2);
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)));
67
75
  return 1 - distance / maxLen;
68
76
  }
69
77
  /**
70
- * Calculate Levenshtein distance between two strings
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
71
83
  */
72
- levenshteinDistance(str1, str2) {
84
+ levenshteinDistance(str1, str2, maxDistance = Infinity) {
73
85
  const len1 = str1.length;
74
86
  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;
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;
82
93
  }
83
- // Fill matrix
94
+ // Use single-row algorithm to save memory (only need previous row)
95
+ let prevRow = Array.from({ length: len2 + 1 }, (_, i) => i);
84
96
  for (let i = 1; i <= len1; i++) {
97
+ const currRow = [i];
98
+ let minInRow = i; // Track minimum value in current row
85
99
  for (let j = 1; j <= len2; j++) {
86
100
  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);
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;
90
111
  }
112
+ prevRow = currRow;
91
113
  }
92
- return matrix[len1][len2];
114
+ return prevRow[len2];
93
115
  }
94
116
  /**
95
117
  * Find the closest matching candidates in the file content
96
118
  * Returns top N candidates sorted by similarity
119
+ * Optimized with safe pre-filtering and early exit
97
120
  */
98
121
  findClosestMatches(searchContent, fileLines, topN = 3) {
99
122
  const searchLines = searchContent.split('\n');
100
123
  const candidates = [];
101
124
  // Normalize whitespace for display only (makes preview more readable)
102
125
  const normalizeForDisplay = (line) => line.replace(/\t/g, ' ').replace(/ +/g, ' ');
103
- // Try to find candidates by sliding window
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
104
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
105
144
  const candidateLines = fileLines.slice(i, i + searchLines.length);
106
145
  const candidateContent = candidateLines.join('\n');
107
- const similarity = this.calculateSimilarity(searchContent, candidateContent);
146
+ const similarity = this.calculateSimilarity(searchContent, candidateContent, threshold);
108
147
  // Only consider candidates with >50% similarity
109
- if (similarity > 0.5) {
148
+ if (similarity > threshold) {
110
149
  candidates.push({
111
150
  startLine: i + 1,
112
151
  endLine: i + searchLines.length,
@@ -115,6 +154,14 @@ export class FilesystemMCPService {
115
154
  .map((line, idx) => `${i + idx + 1}β†’${normalizeForDisplay(line)}`)
116
155
  .join('\n'),
117
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
+ }
118
165
  }
119
166
  }
120
167
  // Sort by similarity descending and return top N
@@ -253,23 +300,8 @@ export class FilesystemMCPService {
253
300
  }
254
301
  }
255
302
  }
256
- // Check if edit is at a code block boundary
257
- const lastLine = editedLines[editedLines.length - 1]?.trim() || '';
258
- const firstLine = editedLines[0]?.trim() || '';
259
- const endsWithOpenBrace = lastLine.endsWith('{') ||
260
- lastLine.endsWith('(') ||
261
- lastLine.endsWith('[');
262
- const startsWithCloseBrace = firstLine.startsWith('}') ||
263
- firstLine.startsWith(')') ||
264
- firstLine.startsWith(']');
265
- if (endsWithOpenBrace || startsWithCloseBrace) {
266
- analysis.codeBlockBoundary = {
267
- isInCompleteBlock: false,
268
- suggestion: endsWithOpenBrace
269
- ? 'Edit ends with an opening bracket - ensure the closing bracket is included in a subsequent edit or already exists in the file'
270
- : 'Edit starts with a closing bracket - ensure the opening bracket exists before this edit',
271
- };
272
- }
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
273
305
  return analysis;
274
306
  }
275
307
  /**
@@ -328,7 +360,7 @@ export class FilesystemMCPService {
328
360
  }
329
361
  /**
330
362
  * Get the content of a file with optional line range
331
- * @param filePath - Path to the file (relative to base path or absolute)
363
+ * @param filePath - Path to the file (relative to base path or absolute) or array of file paths
332
364
  * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1)
333
365
  * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to 500 or file end)
334
366
  * @returns Object containing the requested content with line numbers and metadata
@@ -336,6 +368,76 @@ export class FilesystemMCPService {
336
368
  */
337
369
  async getFileContent(filePath, startLine, endLine) {
338
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
339
441
  const fullPath = this.resolvePath(filePath);
340
442
  // For absolute paths, skip validation to allow access outside base path
341
443
  if (!isAbsolute(filePath)) {
@@ -544,7 +646,7 @@ export class FilesystemMCPService {
544
646
  * @param replaceContent - New content to replace the search content with
545
647
  * @param occurrence - Which occurrence to replace (1-indexed, default: 1, use -1 for all)
546
648
  * @param contextLines - Number of context lines to return before and after the edit (default: 8)
547
- * @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)
548
650
  * @throws Error if search content is not found or multiple matches exist
549
651
  */
550
652
  async editFileBySearch(filePath, searchContent, replaceContent, occurrence = 1, contextLines = 8) {
@@ -558,22 +660,39 @@ export class FilesystemMCPService {
558
660
  const content = await fs.readFile(fullPath, 'utf-8');
559
661
  const lines = content.split('\n');
560
662
  // Normalize line endings
561
- const normalizedSearch = searchContent
663
+ let normalizedSearch = searchContent
562
664
  .replace(/\r\n/g, '\n')
563
665
  .replace(/\r/g, '\n');
564
666
  const normalizedContent = content
565
667
  .replace(/\r\n/g, '\n')
566
668
  .replace(/\r/g, '\n');
567
669
  // Split into lines for matching
568
- const searchLines = normalizedSearch.split('\n');
670
+ let searchLines = normalizedSearch.split('\n');
569
671
  const contentLines = normalizedContent.split('\n');
570
672
  // Find all matches using smart fuzzy matching (auto-handles whitespace)
571
673
  const matches = [];
572
- const threshold = 0.75; // Lower threshold for better tolerance
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
573
681
  for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
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;
690
+ }
691
+ }
692
+ // Full candidate check
574
693
  const candidateLines = contentLines.slice(i, i + searchLines.length);
575
694
  const candidateContent = candidateLines.join('\n');
576
- const similarity = this.calculateSimilarity(normalizedSearch, candidateContent);
695
+ const similarity = this.calculateSimilarity(normalizedSearch, candidateContent, threshold);
577
696
  // Accept matches above threshold
578
697
  if (similarity >= threshold) {
579
698
  matches.push({
@@ -581,47 +700,89 @@ export class FilesystemMCPService {
581
700
  endLine: i + searchLines.length,
582
701
  similarity,
583
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
+ }
584
711
  }
585
712
  }
586
713
  // Sort by similarity descending (best match first)
587
714
  matches.sort((a, b) => b.similarity - a.similarity);
588
- // Handle no matches with enhanced error message
715
+ // Handle no matches: Try escape correction before giving up
589
716
  if (matches.length === 0) {
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`;
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
+ });
608
732
  }
609
733
  }
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`;
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
+ }
614
744
  }
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`;
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);
623
785
  }
624
- throw new Error(errorMessage);
625
786
  }
626
787
  // Handle occurrence selection
627
788
  let selectedMatch;
@@ -710,14 +871,18 @@ export class FilesystemMCPService {
710
871
  // Analyze code structure
711
872
  const editedContentLines = replaceLines;
712
873
  const structureAnalysis = this.analyzeCodeStructure(finalContent, filePath, editedContentLines);
713
- // Get diagnostics from VS Code
874
+ // Get diagnostics from IDE (VSCode or JetBrains) - non-blocking, fire-and-forget
714
875
  let diagnostics = [];
715
876
  try {
716
- await new Promise(resolve => setTimeout(resolve, 500));
717
- 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;
718
883
  }
719
884
  catch (error) {
720
- // Ignore diagnostics errors
885
+ // Ignore diagnostics errors - this is optional functionality
721
886
  }
722
887
  // Build result
723
888
  const result = {
@@ -791,10 +956,7 @@ export class FilesystemMCPService {
791
956
  if (structureAnalysis.indentationWarnings.length > 0) {
792
957
  structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
793
958
  }
794
- if (structureAnalysis.codeBlockBoundary &&
795
- structureAnalysis.codeBlockBoundary.suggestion) {
796
- structureWarnings.push(`Boundary: ${structureAnalysis.codeBlockBoundary.suggestion}`);
797
- }
959
+ // Note: Boundary warnings removed - partial edits are common and expected
798
960
  if (structureWarnings.length > 0) {
799
961
  result.message += `\n\nπŸ” Structure Analysis:\n`;
800
962
  structureWarnings.forEach(warning => {
@@ -818,7 +980,7 @@ export class FilesystemMCPService {
818
980
  * @param endLine - Ending line number (1-indexed, inclusive) - get from filesystem_read output
819
981
  * @param newContent - New content to replace the specified lines (WITHOUT line numbers)
820
982
  * @param contextLines - Number of context lines to return before and after the edit (default: 8)
821
- * @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)
822
984
  * @throws Error if file editing fails
823
985
  */
824
986
  async editFile(filePath, startLine, endLine, newContent, contextLines = 8) {
@@ -924,15 +1086,18 @@ export class FilesystemMCPService {
924
1086
  // Analyze code structure of the edited content (using formatted content if available)
925
1087
  const editedContentLines = finalLines.slice(startLine - 1, startLine - 1 + newContentLines.length);
926
1088
  const structureAnalysis = this.analyzeCodeStructure(finalLines.join('\n'), filePath, editedContentLines);
927
- // Try to get diagnostics from VS Code after editing
1089
+ // Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
928
1090
  let diagnostics = [];
929
1091
  try {
930
- // Wait a bit for VS Code to process the file change
931
- await new Promise(resolve => setTimeout(resolve, 500));
932
- 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;
933
1098
  }
934
1099
  catch (error) {
935
- // Ignore diagnostics errors, they are optional
1100
+ // Ignore diagnostics errors - they are optional
936
1101
  }
937
1102
  const result = {
938
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` +
@@ -1007,11 +1172,7 @@ export class FilesystemMCPService {
1007
1172
  if (structureAnalysis.indentationWarnings.length > 0) {
1008
1173
  structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
1009
1174
  }
1010
- // Add code block boundary warnings
1011
- if (structureAnalysis.codeBlockBoundary &&
1012
- structureAnalysis.codeBlockBoundary.suggestion) {
1013
- structureWarnings.push(`Boundary: ${structureAnalysis.codeBlockBoundary.suggestion}`);
1014
- }
1175
+ // Note: Boundary warnings removed - partial edits are common and expected
1015
1176
  // Format structure warnings
1016
1177
  if (structureWarnings.length > 0) {
1017
1178
  result.message += `\n\nπŸ” Structure Analysis:\n`;
@@ -1056,47 +1217,36 @@ export class FilesystemMCPService {
1056
1217
  }
1057
1218
  // Export a default instance
1058
1219
  export const filesystemService = new FilesystemMCPService();
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
- */
1082
1220
  export const mcpTools = [
1083
1221
  {
1084
1222
  name: 'filesystem_read',
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.',
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.',
1086
1224
  inputSchema: {
1087
1225
  type: 'object',
1088
1226
  properties: {
1089
1227
  filePath: {
1090
- type: 'string',
1091
- 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)',
1092
1242
  },
1093
1243
  startLine: {
1094
1244
  type: 'number',
1095
- description: 'Optional: Starting line number (1-indexed). Omit to read from line 1.',
1245
+ description: 'Optional: Starting line number (1-indexed). Omit to read from line 1. Applied to all files.',
1096
1246
  },
1097
1247
  endLine: {
1098
1248
  type: 'number',
1099
- description: 'Optional: Ending line number (1-indexed). Omit to read to end of file.',
1249
+ description: 'Optional: Ending line number (1-indexed). Omit to read to end of file. Applied to all files.',
1100
1250
  },
1101
1251
  },
1102
1252
  required: ['filePath'],
@@ -0,0 +1,36 @@
1
+ import { type Diagnostic } from '../utils/vscodeConnection.js';
2
+ /**
3
+ * IDE Diagnostics MCP Service
4
+ * Provides access to diagnostics (errors, warnings, hints) from connected IDE
5
+ * Supports both VSCode and JetBrains IDEs
6
+ */
7
+ export declare class IdeDiagnosticsMCPService {
8
+ /**
9
+ * Get diagnostics for a specific file from the connected IDE
10
+ * @param filePath - Absolute path to the file to get diagnostics for
11
+ * @returns Promise that resolves with array of diagnostics
12
+ */
13
+ getDiagnostics(filePath: string): Promise<Diagnostic[]>;
14
+ /**
15
+ * Format diagnostics into human-readable text
16
+ * @param diagnostics - Array of diagnostics to format
17
+ * @param filePath - Path to the file (for display)
18
+ * @returns Formatted string
19
+ */
20
+ formatDiagnostics(diagnostics: Diagnostic[], filePath: string): string;
21
+ }
22
+ export declare const ideDiagnosticsService: IdeDiagnosticsMCPService;
23
+ export declare const mcpTools: {
24
+ name: string;
25
+ description: string;
26
+ inputSchema: {
27
+ type: string;
28
+ properties: {
29
+ filePath: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ };
34
+ required: string[];
35
+ };
36
+ }[];