snow-ai 0.2.18 → 0.2.19
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 +87 -25
- package/dist/hooks/useConversation.js +5 -2
- package/dist/mcp/aceCodeSearch.d.ts +314 -0
- package/dist/mcp/aceCodeSearch.js +822 -0
- package/dist/mcp/filesystem.d.ts +65 -77
- package/dist/mcp/filesystem.js +286 -144
- package/dist/ui/components/DiffViewer.d.ts +2 -1
- package/dist/ui/components/DiffViewer.js +33 -16
- package/dist/ui/components/ToolConfirmation.js +9 -0
- package/dist/ui/components/ToolResultPreview.js +146 -24
- package/dist/ui/pages/ChatScreen.js +6 -1
- package/dist/utils/mcpToolsManager.js +54 -9
- package/dist/utils/sessionConverter.js +8 -2
- package/package.json +1 -1
package/dist/mcp/filesystem.js
CHANGED
|
@@ -16,6 +16,31 @@ export class FilesystemMCPService {
|
|
|
16
16
|
writable: true,
|
|
17
17
|
value: void 0
|
|
18
18
|
});
|
|
19
|
+
/**
|
|
20
|
+
* File extensions supported by Prettier for automatic formatting
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(this, "prettierSupportedExtensions", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: [
|
|
27
|
+
'.js',
|
|
28
|
+
'.jsx',
|
|
29
|
+
'.ts',
|
|
30
|
+
'.tsx',
|
|
31
|
+
'.json',
|
|
32
|
+
'.css',
|
|
33
|
+
'.scss',
|
|
34
|
+
'.less',
|
|
35
|
+
'.html',
|
|
36
|
+
'.vue',
|
|
37
|
+
'.yaml',
|
|
38
|
+
'.yml',
|
|
39
|
+
'.md',
|
|
40
|
+
'.graphql',
|
|
41
|
+
'.gql',
|
|
42
|
+
]
|
|
43
|
+
});
|
|
19
44
|
this.basePath = resolve(basePath);
|
|
20
45
|
}
|
|
21
46
|
/**
|
|
@@ -403,6 +428,234 @@ export class FilesystemMCPService {
|
|
|
403
428
|
throw new Error(`Failed to get file info for ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
404
429
|
}
|
|
405
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Edit a file by searching for exact content and replacing it
|
|
433
|
+
* This method is SAFER than line-based editing as it automatically handles code boundaries.
|
|
434
|
+
*
|
|
435
|
+
* @param filePath - Path to the file to edit
|
|
436
|
+
* @param searchContent - Exact content to search for (must match precisely, including whitespace)
|
|
437
|
+
* @param replaceContent - New content to replace the search content with
|
|
438
|
+
* @param occurrence - Which occurrence to replace (1-indexed, default: 1, use -1 for all)
|
|
439
|
+
* @param contextLines - Number of context lines to return before and after the edit (default: 8)
|
|
440
|
+
* @returns Object containing success message, before/after comparison, and diagnostics
|
|
441
|
+
* @throws Error if search content is not found or multiple matches exist
|
|
442
|
+
*/
|
|
443
|
+
async editFileBySearch(filePath, searchContent, replaceContent, occurrence = 1, contextLines = 8) {
|
|
444
|
+
try {
|
|
445
|
+
const fullPath = this.resolvePath(filePath);
|
|
446
|
+
// For absolute paths, skip validation to allow access outside base path
|
|
447
|
+
if (!isAbsolute(filePath)) {
|
|
448
|
+
await this.validatePath(fullPath);
|
|
449
|
+
}
|
|
450
|
+
// Read the entire file
|
|
451
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
452
|
+
const lines = content.split('\n');
|
|
453
|
+
// Normalize search content (handle different line ending styles)
|
|
454
|
+
const normalizedSearch = searchContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
455
|
+
const normalizedContent = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
456
|
+
// Find all matches
|
|
457
|
+
const matches = [];
|
|
458
|
+
let searchIndex = 0;
|
|
459
|
+
while (true) {
|
|
460
|
+
const matchIndex = normalizedContent.indexOf(normalizedSearch, searchIndex);
|
|
461
|
+
if (matchIndex === -1)
|
|
462
|
+
break;
|
|
463
|
+
// Calculate line numbers for this match
|
|
464
|
+
const beforeMatch = normalizedContent.substring(0, matchIndex);
|
|
465
|
+
const startLine = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
466
|
+
const matchLines = (normalizedSearch.match(/\n/g) || []).length;
|
|
467
|
+
const endLine = startLine + matchLines;
|
|
468
|
+
matches.push({ index: matchIndex, line: startLine, endLine });
|
|
469
|
+
searchIndex = matchIndex + normalizedSearch.length;
|
|
470
|
+
}
|
|
471
|
+
// Handle no matches
|
|
472
|
+
if (matches.length === 0) {
|
|
473
|
+
throw new Error(`Search content not found in file. Please verify the exact content including whitespace and indentation.`);
|
|
474
|
+
}
|
|
475
|
+
// Handle occurrence selection
|
|
476
|
+
let selectedMatch;
|
|
477
|
+
if (occurrence === -1) {
|
|
478
|
+
// Replace all occurrences
|
|
479
|
+
if (matches.length === 1) {
|
|
480
|
+
selectedMatch = matches[0];
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
throw new Error(`Found ${matches.length} matches. Please specify which occurrence to replace (1-${matches.length}), or use occurrence=-1 to replace all (not yet implemented for safety).`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else if (occurrence < 1 || occurrence > matches.length) {
|
|
487
|
+
throw new Error(`Invalid occurrence ${occurrence}. Found ${matches.length} match(es) at lines: ${matches
|
|
488
|
+
.map(m => m.line)
|
|
489
|
+
.join(', ')}`);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
selectedMatch = matches[occurrence - 1];
|
|
493
|
+
}
|
|
494
|
+
const { line: startLine, endLine } = selectedMatch;
|
|
495
|
+
// Backup file before editing
|
|
496
|
+
await incrementalSnapshotManager.backupFile(fullPath);
|
|
497
|
+
// Perform the replacement
|
|
498
|
+
const normalizedReplace = replaceContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
499
|
+
const beforeContent = normalizedContent.substring(0, selectedMatch.index);
|
|
500
|
+
const afterContent = normalizedContent.substring(selectedMatch.index + normalizedSearch.length);
|
|
501
|
+
const modifiedContent = beforeContent + normalizedReplace + afterContent;
|
|
502
|
+
// Calculate replaced content for display
|
|
503
|
+
const replacedLines = lines.slice(startLine - 1, endLine);
|
|
504
|
+
const maxLineNumWidth = String(endLine).length;
|
|
505
|
+
const replacedContent = replacedLines
|
|
506
|
+
.map((line, idx) => {
|
|
507
|
+
const lineNum = startLine + idx;
|
|
508
|
+
const paddedNum = String(lineNum).padStart(maxLineNumWidth, ' ');
|
|
509
|
+
return `${paddedNum}→${line}`;
|
|
510
|
+
})
|
|
511
|
+
.join('\n');
|
|
512
|
+
// Calculate context boundaries
|
|
513
|
+
const modifiedLines = modifiedContent.split('\n');
|
|
514
|
+
const replaceLines = normalizedReplace.split('\n');
|
|
515
|
+
const lineDifference = replaceLines.length - (endLine - startLine);
|
|
516
|
+
const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, endLine, contextLines);
|
|
517
|
+
const contextStart = smartBoundaries.start;
|
|
518
|
+
const contextEnd = smartBoundaries.end;
|
|
519
|
+
// Extract old content for context
|
|
520
|
+
const oldContextLines = lines.slice(contextStart - 1, contextEnd);
|
|
521
|
+
const oldContent = oldContextLines
|
|
522
|
+
.map((line, idx) => {
|
|
523
|
+
const lineNum = contextStart + idx;
|
|
524
|
+
const paddedNum = String(lineNum).padStart(String(contextEnd).length, ' ');
|
|
525
|
+
return `${paddedNum}→${line}`;
|
|
526
|
+
})
|
|
527
|
+
.join('\n');
|
|
528
|
+
// Write the modified content
|
|
529
|
+
await fs.writeFile(fullPath, modifiedContent, 'utf-8');
|
|
530
|
+
// Format with Prettier if applicable
|
|
531
|
+
let finalContent = modifiedContent;
|
|
532
|
+
let finalLines = modifiedLines;
|
|
533
|
+
let finalTotalLines = modifiedLines.length;
|
|
534
|
+
let finalContextEnd = Math.min(finalTotalLines, contextEnd + lineDifference);
|
|
535
|
+
// Check if Prettier supports this file type
|
|
536
|
+
const fileExtension = path.extname(fullPath).toLowerCase();
|
|
537
|
+
const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
|
|
538
|
+
if (shouldFormat) {
|
|
539
|
+
try {
|
|
540
|
+
execSync(`npx prettier --write "${fullPath}"`, {
|
|
541
|
+
stdio: 'pipe',
|
|
542
|
+
encoding: 'utf-8',
|
|
543
|
+
});
|
|
544
|
+
// Re-read the file after formatting
|
|
545
|
+
finalContent = await fs.readFile(fullPath, 'utf-8');
|
|
546
|
+
finalLines = finalContent.split('\n');
|
|
547
|
+
finalTotalLines = finalLines.length;
|
|
548
|
+
finalContextEnd = Math.min(finalTotalLines, contextStart + (contextEnd - contextStart) + lineDifference);
|
|
549
|
+
}
|
|
550
|
+
catch (formatError) {
|
|
551
|
+
// Continue with unformatted content
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Extract new content for context
|
|
555
|
+
const newContextLines = finalLines.slice(contextStart - 1, finalContextEnd);
|
|
556
|
+
const newContextContent = newContextLines
|
|
557
|
+
.map((line, idx) => {
|
|
558
|
+
const lineNum = contextStart + idx;
|
|
559
|
+
const paddedNum = String(lineNum).padStart(String(finalContextEnd).length, ' ');
|
|
560
|
+
return `${paddedNum}→${line}`;
|
|
561
|
+
})
|
|
562
|
+
.join('\n');
|
|
563
|
+
// Analyze code structure
|
|
564
|
+
const editedContentLines = replaceLines;
|
|
565
|
+
const structureAnalysis = this.analyzeCodeStructure(finalContent, filePath, editedContentLines);
|
|
566
|
+
// Get diagnostics from VS Code
|
|
567
|
+
let diagnostics = [];
|
|
568
|
+
try {
|
|
569
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
570
|
+
diagnostics = await vscodeConnection.requestDiagnostics(fullPath);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
// Ignore diagnostics errors
|
|
574
|
+
}
|
|
575
|
+
// Build result
|
|
576
|
+
const result = {
|
|
577
|
+
message: `✅ File edited successfully using search-replace (safer boundary detection): ${filePath}\n` +
|
|
578
|
+
` Matched: lines ${startLine}-${endLine} (occurrence ${occurrence}/${matches.length})\n` +
|
|
579
|
+
` Result: ${replaceLines.length} new lines` +
|
|
580
|
+
(smartBoundaries.extended
|
|
581
|
+
? `\n 📍 Context auto-extended to show complete code block (lines ${contextStart}-${finalContextEnd})`
|
|
582
|
+
: ''),
|
|
583
|
+
oldContent,
|
|
584
|
+
newContent: newContextContent,
|
|
585
|
+
replacedContent,
|
|
586
|
+
matchLocation: { startLine, endLine },
|
|
587
|
+
contextStartLine: contextStart,
|
|
588
|
+
contextEndLine: finalContextEnd,
|
|
589
|
+
totalLines: finalTotalLines,
|
|
590
|
+
structureAnalysis,
|
|
591
|
+
completeOldContent: content,
|
|
592
|
+
completeNewContent: finalContent,
|
|
593
|
+
diagnostics: undefined,
|
|
594
|
+
};
|
|
595
|
+
// Add diagnostics if found
|
|
596
|
+
if (diagnostics.length > 0) {
|
|
597
|
+
result.diagnostics = diagnostics;
|
|
598
|
+
const errorCount = diagnostics.filter(d => d.severity === 'error').length;
|
|
599
|
+
const warningCount = diagnostics.filter(d => d.severity === 'warning').length;
|
|
600
|
+
if (errorCount > 0 || warningCount > 0) {
|
|
601
|
+
result.message += `\n\n⚠️ Diagnostics detected: ${errorCount} error(s), ${warningCount} warning(s)`;
|
|
602
|
+
const formattedDiagnostics = diagnostics
|
|
603
|
+
.filter(d => d.severity === 'error' || d.severity === 'warning')
|
|
604
|
+
.map(d => {
|
|
605
|
+
const icon = d.severity === 'error' ? '❌' : '⚠️';
|
|
606
|
+
const location = `${filePath}:${d.line}:${d.character}`;
|
|
607
|
+
return ` ${icon} [${d.source || 'unknown'}] ${location}\n ${d.message}`;
|
|
608
|
+
})
|
|
609
|
+
.join('\n\n');
|
|
610
|
+
result.message += `\n\n📋 Diagnostic Details:\n${formattedDiagnostics}`;
|
|
611
|
+
result.message += `\n\n ⚡ TIP: Review the errors above and make another edit to fix them`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Add structure analysis warnings
|
|
615
|
+
const structureWarnings = [];
|
|
616
|
+
if (!structureAnalysis.bracketBalance.curly.balanced) {
|
|
617
|
+
const diff = structureAnalysis.bracketBalance.curly.open -
|
|
618
|
+
structureAnalysis.bracketBalance.curly.close;
|
|
619
|
+
structureWarnings.push(`Curly brackets: ${diff > 0 ? `${diff} unclosed {` : `${Math.abs(diff)} extra }`}`);
|
|
620
|
+
}
|
|
621
|
+
if (!structureAnalysis.bracketBalance.round.balanced) {
|
|
622
|
+
const diff = structureAnalysis.bracketBalance.round.open -
|
|
623
|
+
structureAnalysis.bracketBalance.round.close;
|
|
624
|
+
structureWarnings.push(`Round brackets: ${diff > 0 ? `${diff} unclosed (` : `${Math.abs(diff)} extra )`}`);
|
|
625
|
+
}
|
|
626
|
+
if (!structureAnalysis.bracketBalance.square.balanced) {
|
|
627
|
+
const diff = structureAnalysis.bracketBalance.square.open -
|
|
628
|
+
structureAnalysis.bracketBalance.square.close;
|
|
629
|
+
structureWarnings.push(`Square brackets: ${diff > 0 ? `${diff} unclosed [` : `${Math.abs(diff)} extra ]`}`);
|
|
630
|
+
}
|
|
631
|
+
if (structureAnalysis.htmlTags && !structureAnalysis.htmlTags.balanced) {
|
|
632
|
+
if (structureAnalysis.htmlTags.unclosedTags.length > 0) {
|
|
633
|
+
structureWarnings.push(`Unclosed HTML tags: ${structureAnalysis.htmlTags.unclosedTags.join(', ')}`);
|
|
634
|
+
}
|
|
635
|
+
if (structureAnalysis.htmlTags.unopenedTags.length > 0) {
|
|
636
|
+
structureWarnings.push(`Unopened closing tags: ${structureAnalysis.htmlTags.unopenedTags.join(', ')}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (structureAnalysis.indentationWarnings.length > 0) {
|
|
640
|
+
structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
|
|
641
|
+
}
|
|
642
|
+
if (structureAnalysis.codeBlockBoundary &&
|
|
643
|
+
structureAnalysis.codeBlockBoundary.suggestion) {
|
|
644
|
+
structureWarnings.push(`Boundary: ${structureAnalysis.codeBlockBoundary.suggestion}`);
|
|
645
|
+
}
|
|
646
|
+
if (structureWarnings.length > 0) {
|
|
647
|
+
result.message += `\n\n🔍 Structure Analysis:\n`;
|
|
648
|
+
structureWarnings.forEach(warning => {
|
|
649
|
+
result.message += ` ⚠️ ${warning}\n`;
|
|
650
|
+
});
|
|
651
|
+
result.message += `\n 💡 TIP: These warnings help identify potential issues. If intentional (e.g., opening a block), you can ignore them.`;
|
|
652
|
+
}
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
406
659
|
/**
|
|
407
660
|
* Edit a file by replacing lines within a specified range
|
|
408
661
|
* BEST PRACTICE: Keep edits small and focused (≤15 lines recommended) for better accuracy.
|
|
@@ -490,12 +743,8 @@ export class FilesystemMCPService {
|
|
|
490
743
|
let finalContextEnd = newContextEnd;
|
|
491
744
|
let finalContextContent = newContextContent;
|
|
492
745
|
// Check if Prettier supports this file type
|
|
493
|
-
const prettierSupportedExtensions = [
|
|
494
|
-
'.js', '.jsx', '.ts', '.tsx', '.json', '.css', '.scss', '.less',
|
|
495
|
-
'.html', '.vue', '.yaml', '.yml', '.md', '.graphql', '.gql'
|
|
496
|
-
];
|
|
497
746
|
const fileExtension = path.extname(fullPath).toLowerCase();
|
|
498
|
-
const shouldFormat = prettierSupportedExtensions.includes(fileExtension);
|
|
747
|
+
const shouldFormat = this.prettierSupportedExtensions.includes(fileExtension);
|
|
499
748
|
if (shouldFormat) {
|
|
500
749
|
try {
|
|
501
750
|
execSync(`npx prettier --write "${fullPath}"`, {
|
|
@@ -628,102 +877,6 @@ export class FilesystemMCPService {
|
|
|
628
877
|
throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
629
878
|
}
|
|
630
879
|
}
|
|
631
|
-
/**
|
|
632
|
-
* Search for code keywords in files within a directory
|
|
633
|
-
* @param query - Search keyword or pattern
|
|
634
|
-
* @param dirPath - Directory to search in (default: current directory)
|
|
635
|
-
* @param fileExtensions - Array of file extensions to search (e.g., ['.ts', '.tsx', '.js']). If empty, search all files.
|
|
636
|
-
* @param caseSensitive - Whether the search should be case-sensitive (default: false)
|
|
637
|
-
* @param maxResults - Maximum number of results to return (default: 100)
|
|
638
|
-
* @returns Search results with file paths, line numbers, and matched content
|
|
639
|
-
*/
|
|
640
|
-
async searchCode(query, dirPath = '.', fileExtensions = [], caseSensitive = false, maxResults = 100, searchMode = 'text') {
|
|
641
|
-
const matches = [];
|
|
642
|
-
let searchedFiles = 0;
|
|
643
|
-
const fullDirPath = this.resolvePath(dirPath);
|
|
644
|
-
// Prepare search regex based on mode
|
|
645
|
-
const flags = caseSensitive ? 'g' : 'gi';
|
|
646
|
-
let searchRegex = null;
|
|
647
|
-
if (searchMode === 'text') {
|
|
648
|
-
// Escape special regex characters for literal text search
|
|
649
|
-
searchRegex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
|
|
650
|
-
}
|
|
651
|
-
else if (searchMode === 'regex') {
|
|
652
|
-
// Use query as-is for regex search
|
|
653
|
-
searchRegex = new RegExp(query, flags);
|
|
654
|
-
}
|
|
655
|
-
// Recursively search files
|
|
656
|
-
const searchInDirectory = async (currentPath) => {
|
|
657
|
-
try {
|
|
658
|
-
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
659
|
-
for (const entry of entries) {
|
|
660
|
-
if (matches.length >= maxResults) {
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
const fullPath = path.join(currentPath, entry.name);
|
|
664
|
-
// Skip common directories that should be ignored
|
|
665
|
-
if (entry.isDirectory()) {
|
|
666
|
-
const dirName = entry.name;
|
|
667
|
-
if (dirName === 'node_modules' ||
|
|
668
|
-
dirName === '.git' ||
|
|
669
|
-
dirName === 'dist' ||
|
|
670
|
-
dirName === 'build' ||
|
|
671
|
-
dirName.startsWith('.')) {
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
await searchInDirectory(fullPath);
|
|
675
|
-
}
|
|
676
|
-
else if (entry.isFile()) {
|
|
677
|
-
// Filter by file extension if specified
|
|
678
|
-
if (fileExtensions.length > 0) {
|
|
679
|
-
const ext = path.extname(entry.name);
|
|
680
|
-
if (!fileExtensions.includes(ext)) {
|
|
681
|
-
continue;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
searchedFiles++;
|
|
685
|
-
try {
|
|
686
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
687
|
-
// Text or Regex search mode
|
|
688
|
-
if (searchRegex) {
|
|
689
|
-
const lines = content.split('\n');
|
|
690
|
-
lines.forEach((line, index) => {
|
|
691
|
-
if (matches.length >= maxResults) {
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
// Reset regex for each line
|
|
695
|
-
searchRegex.lastIndex = 0;
|
|
696
|
-
const match = searchRegex.exec(line);
|
|
697
|
-
if (match) {
|
|
698
|
-
matches.push({
|
|
699
|
-
filePath: path.relative(this.basePath, fullPath),
|
|
700
|
-
lineNumber: index + 1,
|
|
701
|
-
lineContent: line.trim(),
|
|
702
|
-
column: match.index + 1,
|
|
703
|
-
matchedText: match[0],
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
catch (error) {
|
|
710
|
-
// Skip files that cannot be read (binary files, permission issues, etc.)
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
catch (error) {
|
|
716
|
-
// Skip directories that cannot be accessed
|
|
717
|
-
}
|
|
718
|
-
};
|
|
719
|
-
await searchInDirectory(fullDirPath);
|
|
720
|
-
return {
|
|
721
|
-
query,
|
|
722
|
-
totalMatches: matches.length,
|
|
723
|
-
matches,
|
|
724
|
-
searchedFiles,
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
880
|
/**
|
|
728
881
|
* Resolve path relative to base path and normalize it
|
|
729
882
|
* @private
|
|
@@ -846,77 +999,66 @@ export const mcpTools = [
|
|
|
846
999
|
},
|
|
847
1000
|
},
|
|
848
1001
|
{
|
|
849
|
-
name: '
|
|
850
|
-
description: '🎯
|
|
1002
|
+
name: 'filesystem_edit_search',
|
|
1003
|
+
description: '🎯 **RECOMMENDED** for most edits: Search-and-replace editing with AUTOMATIC boundary detection. **WHY USE THIS**: (1) NO need to count line numbers - just copy the exact code you want to change, (2) SAFER - automatically handles code boundaries to prevent bracket/tag mismatches, (3) CLEARER intent - shows exactly what you\'re changing. **BEST FOR**: Modifying existing functions, fixing bugs, updating logic. **WORKFLOW**: (1) Read file with filesystem_read, (2) Copy exact code block to change (including whitespace!), (3) Provide the replacement. **HANDLES**: Multiple occurrences, whitespace normalization, structure validation. **TIP**: For new code additions, use filesystem_edit with line numbers instead.',
|
|
851
1004
|
inputSchema: {
|
|
852
1005
|
type: 'object',
|
|
853
1006
|
properties: {
|
|
854
1007
|
filePath: {
|
|
855
1008
|
type: 'string',
|
|
856
|
-
description: 'Path to the file to edit
|
|
1009
|
+
description: 'Path to the file to edit',
|
|
857
1010
|
},
|
|
858
|
-
|
|
859
|
-
type: '
|
|
860
|
-
description: '⚠️
|
|
861
|
-
},
|
|
862
|
-
endLine: {
|
|
863
|
-
type: 'number',
|
|
864
|
-
description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. 💡 TIP: Keep edits small (≤15 lines recommended) for better accuracy.',
|
|
1011
|
+
searchContent: {
|
|
1012
|
+
type: 'string',
|
|
1013
|
+
description: '⚠️ EXACT content to find and replace. MUST match precisely including indentation and whitespace. Copy directly from filesystem_read output (WITHOUT line numbers like "123→").',
|
|
865
1014
|
},
|
|
866
|
-
|
|
1015
|
+
replaceContent: {
|
|
867
1016
|
type: 'string',
|
|
868
|
-
description: 'New content to replace
|
|
1017
|
+
description: 'New content to replace the search content. Should maintain consistent indentation with surrounding code.',
|
|
1018
|
+
},
|
|
1019
|
+
occurrence: {
|
|
1020
|
+
type: 'number',
|
|
1021
|
+
description: 'Which occurrence to replace if multiple matches found (1-indexed). Default: 1 (first match). Use -1 for all occurrences (not yet supported).',
|
|
1022
|
+
default: 1,
|
|
869
1023
|
},
|
|
870
1024
|
contextLines: {
|
|
871
1025
|
type: 'number',
|
|
872
|
-
description: 'Number of context lines to show before/after edit
|
|
1026
|
+
description: 'Number of context lines to show before/after edit (default: 8)',
|
|
873
1027
|
default: 8,
|
|
874
1028
|
},
|
|
875
1029
|
},
|
|
876
|
-
required: ['filePath', '
|
|
1030
|
+
required: ['filePath', 'searchContent', 'replaceContent'],
|
|
877
1031
|
},
|
|
878
1032
|
},
|
|
879
1033
|
{
|
|
880
|
-
name: '
|
|
881
|
-
description:
|
|
1034
|
+
name: 'filesystem_edit',
|
|
1035
|
+
description: '🔧 Line-based editing for precise line range replacements. **WHEN TO USE**: (1) Adding completely new code sections, (2) Deleting specific line ranges, (3) When you need exact line-number control. **LIMITATIONS**: Requires manual line number tracking, higher risk of boundary errors. **RECOMMENDATION**: For most edits, use filesystem_edit_search instead - it\'s safer and easier. **BEST PRACTICES**: (1) Keep edits small (≤15 lines), (2) Always read file first to get exact line numbers, (3) Double-check boundaries for brackets/tags. **SMART FEATURES**: Auto-detects bracket/tag mismatches, indentation issues, context auto-extends to show complete code blocks.',
|
|
882
1036
|
inputSchema: {
|
|
883
1037
|
type: 'object',
|
|
884
1038
|
properties: {
|
|
885
|
-
|
|
886
|
-
type: 'string',
|
|
887
|
-
description: 'The keyword or text to search for (e.g., function name, variable name, or any code pattern)',
|
|
888
|
-
},
|
|
889
|
-
dirPath: {
|
|
1039
|
+
filePath: {
|
|
890
1040
|
type: 'string',
|
|
891
|
-
description: '
|
|
892
|
-
default: '.',
|
|
893
|
-
},
|
|
894
|
-
fileExtensions: {
|
|
895
|
-
type: 'array',
|
|
896
|
-
items: {
|
|
897
|
-
type: 'string',
|
|
898
|
-
},
|
|
899
|
-
description: 'Array of file extensions to search (e.g., [".ts", ".tsx", ".js"]). If empty, searches all text files.',
|
|
900
|
-
default: [],
|
|
1041
|
+
description: 'Path to the file to edit (absolute or relative)',
|
|
901
1042
|
},
|
|
902
|
-
|
|
903
|
-
type: '
|
|
904
|
-
description: '
|
|
905
|
-
default: false,
|
|
1043
|
+
startLine: {
|
|
1044
|
+
type: 'number',
|
|
1045
|
+
description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. Double-check this value!',
|
|
906
1046
|
},
|
|
907
|
-
|
|
1047
|
+
endLine: {
|
|
908
1048
|
type: 'number',
|
|
909
|
-
description: '
|
|
910
|
-
default: 100,
|
|
1049
|
+
description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. 💡 TIP: Keep edits small (≤15 lines recommended) for better accuracy.',
|
|
911
1050
|
},
|
|
912
|
-
|
|
1051
|
+
newContent: {
|
|
913
1052
|
type: 'string',
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1053
|
+
description: 'New content to replace specified lines. ⚠️ Do NOT include line numbers. ⚠️ Ensure proper indentation and bracket closure. Keep changes MINIMAL and FOCUSED.',
|
|
1054
|
+
},
|
|
1055
|
+
contextLines: {
|
|
1056
|
+
type: 'number',
|
|
1057
|
+
description: 'Number of context lines to show before/after edit for verification (default: 8)',
|
|
1058
|
+
default: 8,
|
|
917
1059
|
},
|
|
918
1060
|
},
|
|
919
|
-
required: ['
|
|
1061
|
+
required: ['filePath', 'startLine', 'endLine', 'newContent'],
|
|
920
1062
|
},
|
|
921
1063
|
},
|
|
922
1064
|
];
|
|
@@ -5,6 +5,7 @@ interface Props {
|
|
|
5
5
|
filename?: string;
|
|
6
6
|
completeOldContent?: string;
|
|
7
7
|
completeNewContent?: string;
|
|
8
|
+
startLineNumber?: number;
|
|
8
9
|
}
|
|
9
|
-
export default function DiffViewer({ oldContent, newContent, filename, completeOldContent, completeNewContent, }: Props): React.JSX.Element;
|
|
10
|
+
export default function DiffViewer({ oldContent, newContent, filename, completeOldContent, completeNewContent, startLineNumber, }: Props): React.JSX.Element;
|
|
10
11
|
export {};
|
|
@@ -12,7 +12,7 @@ function stripLineNumbers(content) {
|
|
|
12
12
|
})
|
|
13
13
|
.join('\n');
|
|
14
14
|
}
|
|
15
|
-
export default function DiffViewer({ oldContent = '', newContent, filename, completeOldContent, completeNewContent, }) {
|
|
15
|
+
export default function DiffViewer({ oldContent = '', newContent, filename, completeOldContent, completeNewContent, startLineNumber = 1, }) {
|
|
16
16
|
// If complete file contents are provided, use them for intelligent diff
|
|
17
17
|
const useCompleteContent = completeOldContent && completeNewContent;
|
|
18
18
|
const diffOldContent = useCompleteContent
|
|
@@ -38,8 +38,8 @@ export default function DiffViewer({ oldContent = '', newContent, filename, comp
|
|
|
38
38
|
// Generate line-by-line diff
|
|
39
39
|
const diffResult = Diff.diffLines(diffOldContent, diffNewContent);
|
|
40
40
|
const allChanges = [];
|
|
41
|
-
let oldLineNum =
|
|
42
|
-
let newLineNum =
|
|
41
|
+
let oldLineNum = startLineNumber;
|
|
42
|
+
let newLineNum = startLineNumber;
|
|
43
43
|
diffResult.forEach((part) => {
|
|
44
44
|
const lines = part.value.replace(/\n$/, '').split('\n');
|
|
45
45
|
lines.forEach((line) => {
|
|
@@ -127,20 +127,37 @@ export default function DiffViewer({ oldContent = '', newContent, filename, comp
|
|
|
127
127
|
' ',
|
|
128
128
|
filename))),
|
|
129
129
|
React.createElement(Box, { flexDirection: "column" },
|
|
130
|
-
hunks.map((hunk, hunkIndex) => (React.createElement(Box, { key: hunkIndex, flexDirection: "column", marginBottom: 1 },
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
hunks.map((hunk, hunkIndex) => (React.createElement(Box, { key: hunkIndex, flexDirection: "column", marginBottom: 1 },
|
|
131
|
+
React.createElement(Text, { color: "cyan", dimColor: true },
|
|
132
|
+
"@@ Lines ",
|
|
133
|
+
hunk.startLine,
|
|
134
|
+
"-",
|
|
135
|
+
hunk.endLine,
|
|
136
|
+
" @@"),
|
|
137
|
+
hunk.changes.map((change, changeIndex) => {
|
|
138
|
+
// Calculate line number to display
|
|
139
|
+
const lineNum = change.type === 'added'
|
|
140
|
+
? change.newLineNum
|
|
141
|
+
: change.oldLineNum;
|
|
142
|
+
const lineNumStr = lineNum ? String(lineNum).padStart(4, ' ') : ' ';
|
|
143
|
+
if (change.type === 'added') {
|
|
144
|
+
return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#006400" },
|
|
145
|
+
lineNumStr,
|
|
146
|
+
" + ",
|
|
147
|
+
change.content));
|
|
148
|
+
}
|
|
149
|
+
if (change.type === 'removed') {
|
|
150
|
+
return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#8B0000" },
|
|
151
|
+
lineNumStr,
|
|
152
|
+
" - ",
|
|
153
|
+
change.content));
|
|
154
|
+
}
|
|
155
|
+
// Unchanged lines (context)
|
|
156
|
+
return (React.createElement(Text, { key: changeIndex, dimColor: true },
|
|
157
|
+
lineNumStr,
|
|
158
|
+
" ",
|
|
139
159
|
change.content));
|
|
140
|
-
}
|
|
141
|
-
// Unchanged lines (context)
|
|
142
|
-
return (React.createElement(Text, { key: changeIndex, dimColor: true }, change.content));
|
|
143
|
-
})))),
|
|
160
|
+
})))),
|
|
144
161
|
hunks.length > 1 && (React.createElement(Box, { marginTop: 1 },
|
|
145
162
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
146
163
|
"Total: ",
|
|
@@ -22,6 +22,15 @@ function formatArgumentsAsTree(args, toolName) {
|
|
|
22
22
|
if (toolName === 'filesystem-edit') {
|
|
23
23
|
excludeFields.add('newContent');
|
|
24
24
|
}
|
|
25
|
+
if (toolName === 'filesystem-edit_search') {
|
|
26
|
+
excludeFields.add('searchContent');
|
|
27
|
+
excludeFields.add('replaceContent');
|
|
28
|
+
}
|
|
29
|
+
// For ACE tools, exclude large result fields that may contain extensive code
|
|
30
|
+
if (toolName?.startsWith('ace-')) {
|
|
31
|
+
excludeFields.add('context'); // ACE tools may return large context strings
|
|
32
|
+
excludeFields.add('signature'); // Function signatures can be verbose
|
|
33
|
+
}
|
|
25
34
|
const keys = Object.keys(args).filter(key => !excludeFields.has(key));
|
|
26
35
|
return keys.map((key, index) => ({
|
|
27
36
|
key,
|