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.
- package/dist/agents/compactAgent.d.ts +55 -0
- package/dist/agents/compactAgent.js +301 -0
- package/dist/api/chat.d.ts +0 -8
- package/dist/api/chat.js +1 -144
- package/dist/api/responses.d.ts +0 -11
- package/dist/api/responses.js +1 -189
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +80 -206
- package/dist/app.d.ts +2 -1
- package/dist/app.js +11 -13
- package/dist/cli.js +23 -3
- package/dist/hooks/useConversation.js +51 -7
- package/dist/hooks/useGlobalNavigation.d.ts +1 -1
- package/dist/hooks/useKeyboardInput.js +14 -8
- package/dist/mcp/filesystem.d.ts +49 -6
- package/dist/mcp/filesystem.js +243 -86
- package/dist/mcp/websearch.d.ts +118 -0
- package/dist/mcp/websearch.js +451 -0
- package/dist/ui/components/ToolResultPreview.js +60 -1
- package/dist/ui/pages/ChatScreen.d.ts +4 -2
- package/dist/ui/pages/ChatScreen.js +62 -14
- package/dist/ui/pages/{ApiConfigScreen.d.ts → ConfigScreen.d.ts} +1 -1
- package/dist/ui/pages/ConfigScreen.js +549 -0
- package/dist/ui/pages/{ModelConfigScreen.d.ts → ProxyConfigScreen.d.ts} +1 -1
- package/dist/ui/pages/ProxyConfigScreen.js +143 -0
- package/dist/ui/pages/WelcomeScreen.js +15 -15
- package/dist/utils/apiConfig.d.ts +8 -2
- package/dist/utils/apiConfig.js +21 -0
- package/dist/utils/commandExecutor.d.ts +1 -1
- package/dist/utils/contextCompressor.js +363 -49
- package/dist/utils/mcpToolsManager.d.ts +1 -1
- package/dist/utils/mcpToolsManager.js +106 -6
- package/dist/utils/resourceMonitor.d.ts +65 -0
- package/dist/utils/resourceMonitor.js +175 -0
- package/dist/utils/retryUtils.js +6 -0
- package/dist/utils/sessionManager.d.ts +1 -0
- package/dist/utils/sessionManager.js +10 -0
- package/dist/utils/textBuffer.js +7 -2
- package/dist/utils/toolExecutor.d.ts +2 -2
- package/dist/utils/toolExecutor.js +4 -4
- package/package.json +5 -1
- package/dist/ui/pages/ApiConfigScreen.js +0 -161
- package/dist/ui/pages/ModelConfigScreen.js +0 -467
package/dist/mcp/filesystem.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
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
|
|
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 (
|
|
368
|
+
if (actualStartLine < 1) {
|
|
259
369
|
throw new Error('Start line must be greater than 0');
|
|
260
370
|
}
|
|
261
|
-
if (
|
|
371
|
+
if (actualEndLine < actualStartLine) {
|
|
262
372
|
throw new Error('End line must be greater than or equal to start line');
|
|
263
373
|
}
|
|
264
|
-
if (
|
|
265
|
-
throw new Error(`Start line ${
|
|
374
|
+
if (actualStartLine > totalLines) {
|
|
375
|
+
throw new Error(`Start line ${actualStartLine} exceeds file length ${totalLines}`);
|
|
266
376
|
}
|
|
267
|
-
const start =
|
|
268
|
-
const end = Math.min(totalLines,
|
|
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
|
-
|
|
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
|
|
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 -
|
|
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
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
|
|
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
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1099
|
+
description: 'Optional: Ending line number (1-indexed). Omit to read to end of file.',
|
|
943
1100
|
},
|
|
944
1101
|
},
|
|
945
|
-
required: ['filePath'
|
|
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
|
|
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: '
|
|
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
|
|
1188
|
+
description: 'New content to replace with. Indentation will be preserved automatically.',
|
|
1032
1189
|
},
|
|
1033
1190
|
occurrence: {
|
|
1034
1191
|
type: 'number',
|
|
1035
|
-
description: 'Which
|
|
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: '
|
|
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:
|
|
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: {
|