@mrxkun/mcfast-mcp 3.5.6 → 3.5.8
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/README.md +22 -3
- package/package.json +1 -1
- package/src/index.js +332 -21
- package/src/strategies/edit-strategy.js +83 -15
- package/src/strategies/search-strategy.js +7 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
## 🎛️ 8 Unified Tools (v3.5.
|
|
33
|
+
## 🎛️ 8 Unified Tools (v3.5.7)
|
|
34
34
|
|
|
35
35
|
| Tool | Description | Avg Latency | Best For |
|
|
36
36
|
|------|-------------|-------------|----------|
|
|
@@ -119,7 +119,19 @@ Parallel processing with 8 workers:
|
|
|
119
119
|
- **Hybrid mode**: Native workers (self-hosted) or simulated (serverless)
|
|
120
120
|
- **Auto-scaling**: Adjusts to workload
|
|
121
121
|
|
|
122
|
-
### 4. Intelligent Edit Strategies
|
|
122
|
+
### 4. Intelligent Edit Strategies (v3.5.7 - NEW: MINIMAL_DIFF)
|
|
123
|
+
|
|
124
|
+
**MINIMAL_DIFF Strategy (NEW in v3.5.7):**
|
|
125
|
+
- Extracts exact search/replace pairs from instructions
|
|
126
|
+
- Applies deterministic replacements first (no API call!)
|
|
127
|
+
- Falls back to Mercury only for complex changes
|
|
128
|
+
- Reduces tokens by 40-60% for simple edits
|
|
129
|
+
|
|
130
|
+
Supported patterns:
|
|
131
|
+
- Quoted strings: `replace "foo" with "bar"`
|
|
132
|
+
- Code identifiers: `change function foo to bar`
|
|
133
|
+
- Arrow notation: `oldName -> newName`
|
|
134
|
+
- Variable renames: `rename user to account`
|
|
123
135
|
|
|
124
136
|
**AST-Aware Editing:**
|
|
125
137
|
- Uses Tree-sitter for accurate parsing
|
|
@@ -161,7 +173,7 @@ npm install @mrxkun/mcfast-mcp
|
|
|
161
173
|
|
|
162
174
|
```bash
|
|
163
175
|
mcfast-mcp --version
|
|
164
|
-
# Output: 3.5.
|
|
176
|
+
# Output: 3.5.7
|
|
165
177
|
```
|
|
166
178
|
|
|
167
179
|
---
|
|
@@ -546,6 +558,13 @@ Read API (Total: 6ms)
|
|
|
546
558
|
|
|
547
559
|
## 📝 Changelog
|
|
548
560
|
|
|
561
|
+
### v3.5.7 (2026-02-13)
|
|
562
|
+
- 🚀 **NEW: MINIMAL_DIFF strategy** - Extracts exact search/replace pairs and applies deterministically
|
|
563
|
+
- 🔍 **Enhanced pattern detection** - Supports code identifiers, arrow notation, variable renames
|
|
564
|
+
- ⚡ **Parallel processing** - Multi-file edits processed concurrently
|
|
565
|
+
- 📦 **40-60% token reduction** for simple edits
|
|
566
|
+
- 🎯 **Smart fallback** - Only calls Mercury API when needed
|
|
567
|
+
|
|
549
568
|
### v3.5.1 (2026-02-13)
|
|
550
569
|
- 🐛 Bug fixes and stability improvements
|
|
551
570
|
- 📚 Enhanced documentation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.8",
|
|
4
4
|
"description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. Optimized for AI code assistants with 80-98% latency reduction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -203,7 +203,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
203
203
|
// CORE TOOL 1: edit (consolidates apply_fast + edit_file + apply_search_replace)
|
|
204
204
|
{
|
|
205
205
|
name: "edit",
|
|
206
|
-
description: "**PRIMARY TOOL FOR EDITING FILES** - Intelligent auto-switching strategies: (1) Search & Replace (Fastest) - use 'Replace X with Y'
|
|
206
|
+
description: "**PRIMARY TOOL FOR EDITING FILES** - Intelligent auto-switching strategies: (1) Search & Replace (Fastest) - use 'Replace X with Y'. (2) Placeholder (Efficient) - use '// ... existing code ...' placeholders to save tokens. (3) Mercury AI (Intelligent) - for complex refactoring. Includes Auto-Rollback for syntax errors.",
|
|
207
207
|
inputSchema: {
|
|
208
208
|
type: "object",
|
|
209
209
|
properties: {
|
|
@@ -283,7 +283,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
283
283
|
type: "object",
|
|
284
284
|
properties: {
|
|
285
285
|
path: { type: "string", description: "Root directory path (default: current dir)" },
|
|
286
|
-
depth: { type: "number", description: "Max depth to traverse (default: 5)" }
|
|
286
|
+
depth: { type: "number", description: "Max depth to traverse (default: 5)" },
|
|
287
|
+
offset: { type: "number", description: "Number of files to skip (for pagination, default: 0)" },
|
|
288
|
+
limit: { type: "number", description: "Max files to return (for pagination, default: 500)" }
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
},
|
|
@@ -589,16 +591,140 @@ async function reportAudit(data) {
|
|
|
589
591
|
}
|
|
590
592
|
|
|
591
593
|
// ============================================
|
|
592
|
-
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Handle regex-based filesystem search using grep
|
|
597
|
+
* Uses native grep for high-performance regex searching
|
|
598
|
+
*/
|
|
599
|
+
async function handleRegexSearch({ query, path: searchPath, caseSensitive = false }) {
|
|
600
|
+
const start = Date.now();
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const flags = [
|
|
604
|
+
"-r", // Recursive
|
|
605
|
+
"-n", // Line number
|
|
606
|
+
"-I", // Ignore binary files
|
|
607
|
+
caseSensitive ? "" : "-i" // Case sensitivity
|
|
608
|
+
].filter(Boolean).join(" ");
|
|
609
|
+
|
|
610
|
+
// Exclude common noise directories
|
|
611
|
+
const excludes = [
|
|
612
|
+
"--exclude-dir=node_modules",
|
|
613
|
+
"--exclude-dir=.git",
|
|
614
|
+
"--exclude-dir=.next",
|
|
615
|
+
"--exclude-dir=dist",
|
|
616
|
+
"--exclude-dir=build",
|
|
617
|
+
"--exclude-dir=coverage",
|
|
618
|
+
"--exclude-dir=.cache"
|
|
619
|
+
].join(" ");
|
|
620
|
+
|
|
621
|
+
const command = `grep ${flags} ${excludes} -E "${query.replace(/"/g, '\\"')}" "${searchPath}"`;
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const { stdout } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 }); // 10MB buffer
|
|
625
|
+
const results = stdout.trim().split('\n').filter(Boolean);
|
|
626
|
+
|
|
627
|
+
const latency = Date.now() - start;
|
|
628
|
+
|
|
629
|
+
let output = `🔍 Regex Search Results for "${query}"\n`;
|
|
630
|
+
output += `Path: ${searchPath}\n`;
|
|
631
|
+
output += `Matches: ${results.length} lines\n`;
|
|
632
|
+
output += `Latency: ${latency}ms\n\n`;
|
|
633
|
+
|
|
634
|
+
if (results.length === 0) {
|
|
635
|
+
output += "No matches found.";
|
|
636
|
+
} else {
|
|
637
|
+
// Parse grep output format: file:line:content
|
|
638
|
+
const parsedResults = results.map(line => {
|
|
639
|
+
const match = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
640
|
+
if (match) {
|
|
641
|
+
return { file: match[1], line: parseInt(match[2]), content: match[3] };
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}).filter(Boolean);
|
|
645
|
+
|
|
646
|
+
// Group by file
|
|
647
|
+
const byFile = {};
|
|
648
|
+
parsedResults.forEach(r => {
|
|
649
|
+
if (!byFile[r.file]) byFile[r.file] = [];
|
|
650
|
+
byFile[r.file].push(r);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Output grouped by file
|
|
654
|
+
Object.entries(byFile).slice(0, 20).forEach(([file, matches]) => {
|
|
655
|
+
output += `${colors.yellow}${file}${colors.reset}\n`;
|
|
656
|
+
matches.slice(0, 10).forEach(m => {
|
|
657
|
+
output += ` ${colors.dim}${m.line}:${colors.reset} ${m.content}\n`;
|
|
658
|
+
});
|
|
659
|
+
if (matches.length > 10) {
|
|
660
|
+
output += ` ${colors.dim}... and ${matches.length - 10} more matches${colors.reset}\n`;
|
|
661
|
+
}
|
|
662
|
+
output += "\n";
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (parsedResults.length > 200) {
|
|
666
|
+
output += `${colors.dim}... and ${parsedResults.length - 200} more results${colors.reset}\n`;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
reportAudit({
|
|
671
|
+
tool: 'search_regex',
|
|
672
|
+
instruction: `Regex search: ${query}`,
|
|
673
|
+
status: 'success',
|
|
674
|
+
latency_ms: latency,
|
|
675
|
+
files_count: 0,
|
|
676
|
+
result_summary: JSON.stringify(results.slice(0, 100)),
|
|
677
|
+
input_tokens: Math.ceil(query.length / 4),
|
|
678
|
+
output_tokens: Math.ceil(output.length / 4)
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
return { content: [{ type: "text", text: output }] };
|
|
682
|
+
} catch (execErr) {
|
|
683
|
+
if (execErr.code === 1) { // grep returns 1 when no matches found
|
|
684
|
+
const latency = Date.now() - start;
|
|
685
|
+
return {
|
|
686
|
+
content: [{ type: "text", text: `🔍 Regex Search: No matches found for "${query}" in ${searchPath} (${latency}ms)` }]
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
throw execErr;
|
|
690
|
+
}
|
|
691
|
+
} catch (error) {
|
|
692
|
+
const latency = Date.now() - start;
|
|
693
|
+
|
|
694
|
+
reportAudit({
|
|
695
|
+
tool: 'search_regex',
|
|
696
|
+
instruction: `Regex search: ${query}`,
|
|
697
|
+
status: 'error',
|
|
698
|
+
latency_ms: latency,
|
|
699
|
+
error: error.message
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: "text", text: `❌ Regex search error: ${error.message}` }],
|
|
704
|
+
isError: true
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
593
709
|
// ============================================
|
|
594
710
|
|
|
595
711
|
/**
|
|
596
712
|
* Handle filesystem search using fast-glob and pattern matching
|
|
597
713
|
* Fast alternative to API-based search for local files
|
|
714
|
+
* Now supports regex search using grep
|
|
598
715
|
*/
|
|
599
716
|
async function handleSearchFilesystem({ query, path: searchPath, isRegex = false, caseSensitive = false }) {
|
|
600
717
|
const start = Date.now();
|
|
601
|
-
|
|
718
|
+
|
|
719
|
+
// Default to current directory if no path specified
|
|
720
|
+
const cwd = searchPath || process.cwd();
|
|
721
|
+
|
|
722
|
+
// For regex searches, use grep instead of fast-glob
|
|
723
|
+
if (isRegex) {
|
|
724
|
+
return await handleRegexSearch({ query, path: cwd, caseSensitive });
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Original fast-glob based search for non-regex queries
|
|
602
728
|
try {
|
|
603
729
|
// Determine search pattern
|
|
604
730
|
let pattern = query;
|
|
@@ -607,9 +733,6 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
|
|
|
607
733
|
pattern = `**/*${query}*`;
|
|
608
734
|
}
|
|
609
735
|
|
|
610
|
-
// Default to current directory if no path specified
|
|
611
|
-
const cwd = searchPath || process.cwd();
|
|
612
|
-
|
|
613
736
|
// Use fast-glob to find files
|
|
614
737
|
const files = await fg([pattern], {
|
|
615
738
|
cwd,
|
|
@@ -1071,6 +1194,12 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
1071
1194
|
* Auto-detects best strategy based on input
|
|
1072
1195
|
*/
|
|
1073
1196
|
async function handleSearch({ query, files, path, mode = 'auto', regex = false, caseSensitive = false, contextLines = 2 }) {
|
|
1197
|
+
// For regex mode without files, we need to do content-based regex search
|
|
1198
|
+
// since fast-glob doesn't support full regex patterns
|
|
1199
|
+
if (regex && !files && mode === 'auto') {
|
|
1200
|
+
mode = 'filesystem'; // Will use grep-based search below
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1074
1203
|
const detectedMode = mode === 'auto' ? detectSearchStrategy({ query, files, path, mode }) : mode;
|
|
1075
1204
|
|
|
1076
1205
|
console.error(`${colors.cyan}[SEARCH STRATEGY]${colors.reset} ${detectedMode}`);
|
|
@@ -1087,12 +1216,16 @@ async function handleSearch({ query, files, path, mode = 'auto', regex = false,
|
|
|
1087
1216
|
|
|
1088
1217
|
// Strategy 3: LSP Definition
|
|
1089
1218
|
if (detectedMode === 'definition') {
|
|
1090
|
-
|
|
1219
|
+
// If no path provided, use current working directory
|
|
1220
|
+
const searchPath = path || process.cwd();
|
|
1221
|
+
return await handleGetDefinition({ path: searchPath, symbol: query });
|
|
1091
1222
|
}
|
|
1092
1223
|
|
|
1093
1224
|
// Strategy 4: LSP References
|
|
1094
1225
|
if (detectedMode === 'references') {
|
|
1095
|
-
|
|
1226
|
+
// If no path provided, use current working directory
|
|
1227
|
+
const searchPath = path || process.cwd();
|
|
1228
|
+
return await handleFindReferences({ path: searchPath, symbol: query });
|
|
1096
1229
|
}
|
|
1097
1230
|
|
|
1098
1231
|
// Strategy 5: Filesystem search (fast grep-based)
|
|
@@ -1523,20 +1656,37 @@ async function handleSearchCode({ query, files, regex = false, caseSensitive = f
|
|
|
1523
1656
|
}
|
|
1524
1657
|
}
|
|
1525
1658
|
|
|
1526
|
-
async function handleListFiles({ path: dirPath = process.cwd(), depth = 5 }) {
|
|
1659
|
+
async function handleListFiles({ path: dirPath = process.cwd(), depth = 5, offset = 0, limit = 500 }) {
|
|
1527
1660
|
const start = Date.now();
|
|
1528
1661
|
try {
|
|
1529
1662
|
const files = await getFiles(dirPath, depth);
|
|
1530
1663
|
|
|
1531
|
-
|
|
1664
|
+
// Apply pagination
|
|
1665
|
+
const totalFiles = files.length;
|
|
1666
|
+
const paginatedFiles = files.slice(offset, offset + limit);
|
|
1667
|
+
const hasMore = offset + limit < totalFiles;
|
|
1668
|
+
|
|
1669
|
+
// Build output with pagination info
|
|
1670
|
+
let output = `📁 Files in ${dirPath} (depth: ${depth}):\n`;
|
|
1671
|
+
output += `Showing ${offset + 1}-${Math.min(offset + limit, totalFiles)} of ${totalFiles} files`;
|
|
1672
|
+
if (hasMore) {
|
|
1673
|
+
output += `. Use offset: ${offset + limit} to see more.`;
|
|
1674
|
+
}
|
|
1675
|
+
output += `\n\n${paginatedFiles.join('\n')}`;
|
|
1676
|
+
|
|
1677
|
+
// Check for large output and add helpful message
|
|
1678
|
+
const outputLength = output.length;
|
|
1679
|
+
if (outputLength > 50000) {
|
|
1680
|
+
output += `\n\n💡 Tip: Output is large. Use depth parameter to limit depth (e.g., depth: 2 or 3) to limit the search scope.`;
|
|
1681
|
+
}
|
|
1532
1682
|
|
|
1533
1683
|
reportAudit({
|
|
1534
1684
|
tool: 'list_files_fast',
|
|
1535
1685
|
instruction: dirPath,
|
|
1536
1686
|
status: 'success',
|
|
1537
1687
|
latency_ms: Date.now() - start,
|
|
1538
|
-
files_count:
|
|
1539
|
-
result_summary: JSON.stringify(
|
|
1688
|
+
files_count: totalFiles,
|
|
1689
|
+
result_summary: JSON.stringify(paginatedFiles.slice(0, 500)),
|
|
1540
1690
|
input_tokens: Math.ceil(dirPath.length / 4),
|
|
1541
1691
|
output_tokens: Math.ceil(output.length / 4)
|
|
1542
1692
|
});
|
|
@@ -1546,6 +1696,12 @@ async function handleListFiles({ path: dirPath = process.cwd(), depth = 5 }) {
|
|
|
1546
1696
|
};
|
|
1547
1697
|
|
|
1548
1698
|
} catch (error) {
|
|
1699
|
+
// Error recovery: suggest using depth parameter
|
|
1700
|
+
let errorMessage = `❌ Error listing files: ${error.message}`;
|
|
1701
|
+
if (error.message.includes('too many files') || error.message.includes('EMFILE') || error.message.includes('max')) {
|
|
1702
|
+
errorMessage += `\n\n💡 Tip: Try reducing the depth (e.g., depth: 2 or 3) to limit the search scope.`;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1549
1705
|
reportAudit({
|
|
1550
1706
|
tool: 'list_files_fast',
|
|
1551
1707
|
instruction: dirPath,
|
|
@@ -1554,7 +1710,7 @@ async function handleListFiles({ path: dirPath = process.cwd(), depth = 5 }) {
|
|
|
1554
1710
|
latency_ms: Date.now() - start
|
|
1555
1711
|
});
|
|
1556
1712
|
return {
|
|
1557
|
-
content: [{ type: "text", text:
|
|
1713
|
+
content: [{ type: "text", text: errorMessage }],
|
|
1558
1714
|
isError: true
|
|
1559
1715
|
};
|
|
1560
1716
|
}
|
|
@@ -1843,14 +1999,22 @@ async function handleSearchCodeAI({ query, files, contextLines = 2 }) {
|
|
|
1843
1999
|
async function handleGetDefinition({ path: filePath, symbol }) {
|
|
1844
2000
|
if (!filePath || !symbol) throw new Error("Missing path or symbol");
|
|
1845
2001
|
|
|
1846
|
-
// Check if path is a directory
|
|
1847
2002
|
const absolutePath = path.resolve(filePath);
|
|
1848
|
-
|
|
2003
|
+
|
|
2004
|
+
// Check if path is a directory - search across files in directory
|
|
2005
|
+
let stats;
|
|
2006
|
+
try {
|
|
2007
|
+
stats = await fs.stat(absolutePath);
|
|
2008
|
+
} catch (e) {
|
|
2009
|
+
throw new Error(`Path does not exist: ${filePath}`);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1849
2012
|
if (!stats.isFile()) {
|
|
1850
|
-
|
|
2013
|
+
// It's a directory - search for symbol definition using grep
|
|
2014
|
+
return await handleDefinitionInDirectory({ path: absolutePath, symbol });
|
|
1851
2015
|
}
|
|
1852
2016
|
|
|
1853
|
-
//
|
|
2017
|
+
// It's a file - use tree-sitter to find definition
|
|
1854
2018
|
const content = await fs.readFile(filePath, 'utf8');
|
|
1855
2019
|
const definitions = await findDefinition(content, filePath, symbol);
|
|
1856
2020
|
|
|
@@ -1867,16 +2031,88 @@ async function handleGetDefinition({ path: filePath, symbol }) {
|
|
|
1867
2031
|
};
|
|
1868
2032
|
}
|
|
1869
2033
|
|
|
2034
|
+
async function handleDefinitionInDirectory({ path: dirPath, symbol }) {
|
|
2035
|
+
// Search for symbol definition in directory using grep
|
|
2036
|
+
// Look for common definition patterns: const/let/var, function, class, interface, type, etc.
|
|
2037
|
+
const patterns = [
|
|
2038
|
+
`^\\s*(const|let|var)\\s+${symbol}\\s*=`,
|
|
2039
|
+
`^\\s*function\\s+${symbol}\\s*\\(`,
|
|
2040
|
+
`^\\s*class\\s+${symbol}\\s*[\\{\\s]`,
|
|
2041
|
+
`^\\s*interface\\s+${symbol}\\s*[\\{\\s]`,
|
|
2042
|
+
`^\\s*type\\s+${symbol}\\s*=`,
|
|
2043
|
+
`^\\s*enum\\s+${symbol}\\s*[\\{\\s]`,
|
|
2044
|
+
`^\\s*export\\s+(const|let|var|function|class|interface|type|enum)\\s+${symbol}`,
|
|
2045
|
+
`^\\s*async\\s+function\\s+${symbol}\\s*\\(`,
|
|
2046
|
+
];
|
|
2047
|
+
|
|
2048
|
+
const results = [];
|
|
2049
|
+
for (const pattern of patterns) {
|
|
2050
|
+
const flags = "-rn";
|
|
2051
|
+
const excludes = [
|
|
2052
|
+
"--exclude-dir=node_modules",
|
|
2053
|
+
"--exclude-dir=.git",
|
|
2054
|
+
"--exclude-dir=dist",
|
|
2055
|
+
"--exclude-dir=build"
|
|
2056
|
+
].join(" ");
|
|
2057
|
+
|
|
2058
|
+
try {
|
|
2059
|
+
const { stdout } = await execAsync(
|
|
2060
|
+
`grep ${flags} ${excludes} -E "${pattern}" "${dirPath}" 2>/dev/null`,
|
|
2061
|
+
{ maxBuffer: 5 * 1024 * 1024 }
|
|
2062
|
+
);
|
|
2063
|
+
if (stdout.trim()) {
|
|
2064
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
2065
|
+
lines.forEach(line => {
|
|
2066
|
+
const match = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
2067
|
+
if (match) {
|
|
2068
|
+
results.push({
|
|
2069
|
+
file: match[1],
|
|
2070
|
+
line: parseInt(match[2]),
|
|
2071
|
+
content: match[3].trim()
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
// Ignore grep errors - no matches
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
if (results.length === 0) {
|
|
2082
|
+
return {
|
|
2083
|
+
content: [{ type: "text", text: `No definition found for '${symbol}' in ${dirPath}` }]
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const output = `Definition of '${symbol}' (${results.length} found):\n`;
|
|
2088
|
+
const formatted = results.slice(0, 10).map(r =>
|
|
2089
|
+
`${r.file}:${r.line}\n ${r.content}`
|
|
2090
|
+
).join('\n');
|
|
2091
|
+
|
|
2092
|
+
return {
|
|
2093
|
+
content: [{ type: "text", text: output + formatted + (results.length > 10 ? `\n... and ${results.length - 10} more` : '') }]
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1870
2097
|
async function handleFindReferences({ path: filePath, symbol }) {
|
|
1871
2098
|
if (!filePath || !symbol) throw new Error("Missing path or symbol");
|
|
1872
2099
|
|
|
1873
|
-
// Check if path is a directory
|
|
1874
2100
|
const absolutePath = path.resolve(filePath);
|
|
1875
|
-
|
|
2101
|
+
|
|
2102
|
+
// Check if path is a directory - search across files in directory
|
|
2103
|
+
let stats;
|
|
2104
|
+
try {
|
|
2105
|
+
stats = await fs.stat(absolutePath);
|
|
2106
|
+
} catch (e) {
|
|
2107
|
+
throw new Error(`Path does not exist: ${filePath}`);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
1876
2110
|
if (!stats.isFile()) {
|
|
1877
|
-
|
|
2111
|
+
// It's a directory - search for references using grep
|
|
2112
|
+
return await handleReferencesInDirectory({ path: absolutePath, symbol });
|
|
1878
2113
|
}
|
|
1879
2114
|
|
|
2115
|
+
// It's a file - use tree-sitter to find references
|
|
1880
2116
|
const content = await fs.readFile(filePath, 'utf8');
|
|
1881
2117
|
const references = await findReferences(content, filePath, symbol);
|
|
1882
2118
|
|
|
@@ -1896,6 +2132,81 @@ async function handleFindReferences({ path: filePath, symbol }) {
|
|
|
1896
2132
|
};
|
|
1897
2133
|
}
|
|
1898
2134
|
|
|
2135
|
+
async function handleReferencesInDirectory({ path: dirPath, symbol }) {
|
|
2136
|
+
// Search for symbol references in directory using grep
|
|
2137
|
+
const flags = "-rn";
|
|
2138
|
+
const excludes = [
|
|
2139
|
+
"--exclude-dir=node_modules",
|
|
2140
|
+
"--exclude-dir=.git",
|
|
2141
|
+
"--exclude-dir=dist",
|
|
2142
|
+
"--exclude-dir=build"
|
|
2143
|
+
].join(" ");
|
|
2144
|
+
|
|
2145
|
+
try {
|
|
2146
|
+
const { stdout } = await execAsync(
|
|
2147
|
+
`grep ${flags} ${excludes} -E "\\b${symbol}\\b" "${dirPath}" 2>/dev/null`,
|
|
2148
|
+
{ maxBuffer: 10 * 1024 * 1024 }
|
|
2149
|
+
);
|
|
2150
|
+
|
|
2151
|
+
if (!stdout.trim()) {
|
|
2152
|
+
return {
|
|
2153
|
+
content: [{ type: "text", text: `No references found for '${symbol}' in ${dirPath}` }]
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
2158
|
+
const results = lines.map(line => {
|
|
2159
|
+
const match = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
2160
|
+
if (match) {
|
|
2161
|
+
return {
|
|
2162
|
+
file: match[1],
|
|
2163
|
+
line: parseInt(match[2]),
|
|
2164
|
+
content: match[3].trim()
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
return null;
|
|
2168
|
+
}).filter(Boolean);
|
|
2169
|
+
|
|
2170
|
+
if (results.length === 0) {
|
|
2171
|
+
return {
|
|
2172
|
+
content: [{ type: "text", text: `No references found for '${symbol}' in ${dirPath}` }]
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
let output = `References for '${symbol}' in ${dirPath} (${results.length} found):\n\n`;
|
|
2177
|
+
|
|
2178
|
+
// Group by file
|
|
2179
|
+
const byFile = {};
|
|
2180
|
+
results.forEach(r => {
|
|
2181
|
+
if (!byFile[r.file]) byFile[r.file] = [];
|
|
2182
|
+
byFile[r.file].push(r);
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
Object.entries(byFile).slice(0, 10).forEach(([file, matches]) => {
|
|
2186
|
+
output += `${colors.yellow}${file}${colors.reset}\n`;
|
|
2187
|
+
matches.slice(0, 5).forEach(m => {
|
|
2188
|
+
output += ` ${colors.dim}${m.line}:${colors.reset} ${m.content}\n`;
|
|
2189
|
+
});
|
|
2190
|
+
if (matches.length > 5) {
|
|
2191
|
+
output += ` ${colors.dim}... and ${matches.length - 5} more${colors.reset}\n`;
|
|
2192
|
+
}
|
|
2193
|
+
output += "\n";
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
if (results.length > 50) {
|
|
2197
|
+
output += `${colors.dim}... and ${results.length - 50} more results${colors.reset}\n`;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
return {
|
|
2201
|
+
content: [{ type: "text", text: output }]
|
|
2202
|
+
};
|
|
2203
|
+
} catch (e) {
|
|
2204
|
+
return {
|
|
2205
|
+
content: [{ type: "text", text: `No references found for '${symbol}' in ${dirPath}` }]
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
1899
2210
|
/**
|
|
1900
2211
|
* Start Server
|
|
1901
2212
|
*/
|
|
@@ -7,45 +7,113 @@ import { isDiffBasedEdit } from './fuzzy-patch.js';
|
|
|
7
7
|
import { requiresAST, detectRefactoringPattern } from './ast-detector.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Detect if instruction is a simple search/replace
|
|
10
|
+
* Detect if instruction is a simple search/replace (ENHANCED)
|
|
11
11
|
*/
|
|
12
12
|
export function isSearchReplace(instruction) {
|
|
13
13
|
if (!instruction) return false;
|
|
14
14
|
|
|
15
15
|
const lower = instruction.toLowerCase();
|
|
16
16
|
|
|
17
|
-
// Pattern 1: "Replace X with Y"
|
|
17
|
+
// Pattern 1: "Replace X with Y" - quoted strings
|
|
18
18
|
const replacePattern = /replace\s+["'](.+?)["']\s+with\s+["'](.+?)["']/i;
|
|
19
19
|
if (replacePattern.test(instruction)) return true;
|
|
20
20
|
|
|
21
|
-
// Pattern 2: "Change X to Y"
|
|
21
|
+
// Pattern 2: "Change X to Y" - quoted strings
|
|
22
22
|
const changePattern = /change\s+["'](.+?)["']\s+to\s+["'](.+?)["']/i;
|
|
23
23
|
if (changePattern.test(instruction)) return true;
|
|
24
24
|
|
|
25
25
|
// Pattern 3: Contains both "search" and "replace" keywords
|
|
26
26
|
if (lower.includes('search') && lower.includes('replace')) return true;
|
|
27
27
|
|
|
28
|
+
// NEW: Pattern 4: Code identifiers - function/class/const name changes
|
|
29
|
+
// "rename function foo to bar" or "change function foo to bar"
|
|
30
|
+
const codeRenamePattern = /(?:rename|change|update)\s+(?:function|class|const|let|var|interface|type|enum)\s+(\w+)\s+to\s+(\w+)/i;
|
|
31
|
+
if (codeRenamePattern.test(instruction)) return true;
|
|
32
|
+
|
|
33
|
+
// NEW: Pattern 5: Arrow notation - oldName -> newName
|
|
34
|
+
if (/(\w+)\s*(?:->|→)\s*(\w+)/.test(instruction)) return true;
|
|
35
|
+
|
|
36
|
+
// NEW: Pattern 6: Variable renames
|
|
37
|
+
const varRenamePattern = /(?:rename|change)\s+(\w+)\s+to\s+(\w+)/i;
|
|
38
|
+
if (varRenamePattern.test(instruction)) return true;
|
|
39
|
+
|
|
28
40
|
return false;
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
/**
|
|
32
|
-
* Extract search and replace terms from instruction
|
|
44
|
+
* Extract search and replace terms from instruction (ENHANCED)
|
|
33
45
|
*/
|
|
34
46
|
export function extractSearchReplace(instruction) {
|
|
35
|
-
const
|
|
36
|
-
|
|
47
|
+
const pairs = [];
|
|
48
|
+
|
|
49
|
+
// Pattern 1: Quoted strings - "replace X with Y"
|
|
50
|
+
const replacePattern = /replace\s+["'](.+?)["']\s+with\s+["'](.+?)["']/gi;
|
|
51
|
+
let match;
|
|
52
|
+
while ((match = replacePattern.exec(instruction)) !== null) {
|
|
53
|
+
pairs.push({ search: match[1], replace: match[2] });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Pattern 2: Change to - "change X to Y"
|
|
57
|
+
const changePattern = /change\s+["'](.+?)["']\s+to\s+["'](.+?)["']/gi;
|
|
58
|
+
while ((match = changePattern.exec(instruction)) !== null) {
|
|
59
|
+
pairs.push({ search: match[1], replace: match[2] });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// NEW: Pattern 3: Code identifiers - "change function foo to bar"
|
|
63
|
+
const codePattern = /(?:rename|change|update)\s+(?:function|class|const|let|var|interface|type|enum)\s+(\w+)\s+to\s+(\w+)/gi;
|
|
64
|
+
while ((match = codePattern.exec(instruction)) !== null) {
|
|
65
|
+
pairs.push({ search: match[1], replace: match[2] });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// NEW: Pattern 4: Arrow notation - "oldName -> newName"
|
|
69
|
+
const arrowPattern = /(\w+)\s*(?:->|→)\s*(\w+)/g;
|
|
70
|
+
while ((match = arrowPattern.exec(instruction)) !== null) {
|
|
71
|
+
// Only add if both parts look like identifiers (not common words)
|
|
72
|
+
if (match[1].length >= 2 && match[2].length >= 2) {
|
|
73
|
+
pairs.push({ search: match[1], replace: match[2] });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// NEW: Pattern 5: Variable renames without keywords
|
|
78
|
+
const varPattern = /(?:rename|change)\s+(\w+)\s+to\s+(\w+)/gi;
|
|
79
|
+
while ((match = varPattern.exec(instruction)) !== null) {
|
|
80
|
+
pairs.push({ search: match[1], replace: match[2] });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Return first found pair for backward compatibility
|
|
84
|
+
return pairs.length > 0 ? pairs[0] : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract ALL search/replace pairs (new - for minimal diff)
|
|
89
|
+
*/
|
|
90
|
+
export function extractAllSearchReplace(instruction) {
|
|
91
|
+
const pairs = [];
|
|
92
|
+
|
|
93
|
+
// Pattern 1: Quoted strings
|
|
94
|
+
const patterns = [
|
|
95
|
+
/replace\s+["'](.+?)["']\s+with\s+["'](.+?)["']/gi,
|
|
96
|
+
/change\s+["'](.+?)["']\s+to\s+["'](.+?)["']/gi,
|
|
97
|
+
/(?:rename|change|update)\s+(?:function|class|const|let|var|interface|type|enum)\s+(\w+)\s+to\s+(\w+)/gi,
|
|
98
|
+
];
|
|
37
99
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
100
|
+
for (const pattern of patterns) {
|
|
101
|
+
let match;
|
|
102
|
+
while ((match = pattern.exec(instruction)) !== null) {
|
|
103
|
+
pairs.push({ search: match[1], replace: match[2] });
|
|
104
|
+
}
|
|
41
105
|
}
|
|
42
106
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
107
|
+
// Arrow notation
|
|
108
|
+
const arrowPattern = /(\w+)\s*(?:->|→)\s*(\w+)/g;
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = arrowPattern.exec(instruction)) !== null) {
|
|
111
|
+
if (match[1].length >= 2 && match[2].length >= 2) {
|
|
112
|
+
pairs.push({ search: match[1], replace: match[2] });
|
|
113
|
+
}
|
|
46
114
|
}
|
|
47
115
|
|
|
48
|
-
return
|
|
116
|
+
return pairs;
|
|
49
117
|
}
|
|
50
118
|
|
|
51
119
|
/**
|
|
@@ -64,7 +132,7 @@ export function hasPlaceholders(codeEdit) {
|
|
|
64
132
|
}
|
|
65
133
|
|
|
66
134
|
/**
|
|
67
|
-
* Determine the best edit strategy
|
|
135
|
+
* Determine the best edit strategy (ENHANCED)
|
|
68
136
|
* @returns {'fuzzy_patch' | 'ast_refactor' | 'search_replace' | 'placeholder_merge' | 'mercury_intelligent'}
|
|
69
137
|
*/
|
|
70
138
|
export function detectEditStrategy({ instruction, code_edit, files }) {
|
|
@@ -78,7 +146,7 @@ export function detectEditStrategy({ instruction, code_edit, files }) {
|
|
|
78
146
|
return 'ast_refactor';
|
|
79
147
|
}
|
|
80
148
|
|
|
81
|
-
// Priority 3: Search/Replace (fastest, deterministic)
|
|
149
|
+
// Priority 3: Search/Replace (fastest, deterministic) - ENHANCED to detect more patterns
|
|
82
150
|
if (isSearchReplace(instruction)) {
|
|
83
151
|
return 'search_replace';
|
|
84
152
|
}
|
|
@@ -29,14 +29,19 @@ export function isSemanticQuery(query) {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Determine the best search strategy
|
|
32
|
-
* @returns {'local' | 'ai' | 'filesystem'}
|
|
32
|
+
* @returns {'local' | 'ai' | 'filesystem' | 'definition' | 'references'}
|
|
33
33
|
*/
|
|
34
|
-
export function detectSearchStrategy({ query, files, path, mode }) {
|
|
34
|
+
export function detectSearchStrategy({ query, files, path, mode, regex = false }) {
|
|
35
35
|
// If mode is explicitly set, use it
|
|
36
36
|
if (mode && mode !== 'auto') {
|
|
37
37
|
return mode;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// For regex searches without files, use filesystem (grep-based)
|
|
41
|
+
if (regex && !files) {
|
|
42
|
+
return 'filesystem';
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
// Priority 1: Local search (if files provided in context)
|
|
41
46
|
if (files && Object.keys(files).length > 0) {
|
|
42
47
|
return 'local';
|