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.
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +20 -10
- package/dist/cli.js +8 -0
- package/dist/hooks/useClipboard.js +4 -4
- package/dist/hooks/useKeyboardInput.d.ts +1 -0
- package/dist/hooks/useKeyboardInput.js +8 -4
- package/dist/hooks/useTerminalFocus.d.ts +5 -0
- package/dist/hooks/useTerminalFocus.js +22 -2
- package/dist/mcp/aceCodeSearch.d.ts +58 -4
- package/dist/mcp/aceCodeSearch.js +563 -20
- package/dist/mcp/filesystem.d.ts +35 -29
- package/dist/mcp/filesystem.js +272 -122
- package/dist/mcp/ideDiagnostics.d.ts +36 -0
- package/dist/mcp/ideDiagnostics.js +92 -0
- package/dist/ui/components/ChatInput.js +6 -3
- package/dist/ui/pages/ConfigProfileScreen.d.ts +7 -0
- package/dist/ui/pages/ConfigProfileScreen.js +300 -0
- package/dist/ui/pages/ConfigScreen.js +228 -29
- package/dist/ui/pages/WelcomeScreen.js +1 -1
- package/dist/utils/apiConfig.js +12 -0
- package/dist/utils/configManager.d.ts +45 -0
- package/dist/utils/configManager.js +274 -0
- package/dist/utils/contextCompressor.js +0 -8
- package/dist/utils/escapeHandler.d.ts +79 -0
- package/dist/utils/escapeHandler.js +153 -0
- package/dist/utils/incrementalSnapshot.js +2 -1
- package/dist/utils/mcpToolsManager.js +44 -0
- package/dist/utils/textBuffer.js +13 -15
- package/dist/utils/vscodeConnection.js +26 -11
- package/dist/utils/workspaceSnapshot.js +2 -1
- package/package.json +2 -1
package/dist/mcp/filesystem.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
//
|
|
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 >
|
|
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
|
-
//
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
715
|
+
// Handle no matches: Try escape correction before giving up
|
|
589
716
|
if (matches.length === 0) {
|
|
590
|
-
//
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
874
|
+
// Get diagnostics from IDE (VSCode or JetBrains) - non-blocking, fire-and-forget
|
|
714
875
|
let diagnostics = [];
|
|
715
876
|
try {
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
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
|
|
1089
|
+
// Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
|
|
928
1090
|
let diagnostics = [];
|
|
929
1091
|
try {
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1091
|
-
|
|
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
|
+
}[];
|