@mrxkun/mcfast-mcp 2.1.0 → 2.1.2
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/package.json +1 -1
- package/src/index.js +14 -2
- package/src/strategies/fuzzy-patch.js +90 -12
- package/src/utils/backup.js +6 -2
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -518,11 +518,23 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
518
518
|
});
|
|
519
519
|
|
|
520
520
|
if (!editResult.success) {
|
|
521
|
-
//
|
|
521
|
+
// Determine error type based on result
|
|
522
|
+
let errorType = 'generic';
|
|
523
|
+
|
|
524
|
+
if (editResult.error.includes('ENOENT')) {
|
|
525
|
+
errorType = 'file_not_found';
|
|
526
|
+
} else if (editResult.error.includes('EACCES')) {
|
|
527
|
+
errorType = 'permission_denied';
|
|
528
|
+
} else if (editResult.error.includes('Validation failed')) {
|
|
529
|
+
errorType = 'syntax_error';
|
|
530
|
+
} else if (editResult.error.includes('Failed to apply hunk') || editResult.error.includes('Could not find matching location')) {
|
|
531
|
+
errorType = 'fuzzy_match_failed';
|
|
532
|
+
}
|
|
533
|
+
|
|
522
534
|
return {
|
|
523
535
|
content: [{
|
|
524
536
|
type: "text",
|
|
525
|
-
text: formatError(
|
|
537
|
+
text: formatError(errorType, {
|
|
526
538
|
filePath,
|
|
527
539
|
error: editResult.error,
|
|
528
540
|
backupPath: editResult.backupPath
|
|
@@ -6,27 +6,49 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Calculate Levenshtein distance between two strings
|
|
8
8
|
* Used for fuzzy matching to find best location for patch
|
|
9
|
+
* Optimized with early termination and length-based shortcuts
|
|
9
10
|
*/
|
|
10
|
-
function levenshteinDistance(str1, str2) {
|
|
11
|
+
function levenshteinDistance(str1, str2, maxDistance = Infinity) {
|
|
11
12
|
const len1 = str1.length;
|
|
12
13
|
const len2 = str2.length;
|
|
13
|
-
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Quick optimization: if length difference exceeds maxDistance, return early
|
|
16
|
+
if (Math.abs(len1 - len2) > maxDistance) {
|
|
17
|
+
return maxDistance + 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Optimization: swap to ensure str1 is shorter (reduces memory)
|
|
21
|
+
if (len1 > len2) {
|
|
22
|
+
return levenshteinDistance(str2, str1, maxDistance);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Use single array instead of matrix (space optimization)
|
|
26
|
+
let prevRow = Array(len2 + 1).fill(0).map((_, i) => i);
|
|
17
27
|
|
|
18
28
|
for (let i = 1; i <= len1; i++) {
|
|
29
|
+
let currentRow = [i];
|
|
30
|
+
let minInRow = i;
|
|
31
|
+
|
|
19
32
|
for (let j = 1; j <= len2; j++) {
|
|
20
33
|
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
const val = Math.min(
|
|
35
|
+
prevRow[j] + 1, // deletion
|
|
36
|
+
currentRow[j - 1] + 1, // insertion
|
|
37
|
+
prevRow[j - 1] + cost // substitution
|
|
25
38
|
);
|
|
39
|
+
currentRow.push(val);
|
|
40
|
+
minInRow = Math.min(minInRow, val);
|
|
26
41
|
}
|
|
42
|
+
|
|
43
|
+
// Early termination: if minimum in row exceeds maxDistance, abort
|
|
44
|
+
if (minInRow > maxDistance) {
|
|
45
|
+
return maxDistance + 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
prevRow = currentRow;
|
|
27
49
|
}
|
|
28
50
|
|
|
29
|
-
return
|
|
51
|
+
return prevRow[len2];
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
/**
|
|
@@ -103,16 +125,40 @@ export function parseDiff(diffText) {
|
|
|
103
125
|
* Returns { index, score, lineNumber } or null if no good match
|
|
104
126
|
*/
|
|
105
127
|
export function findBestMatch(targetLines, patternLines, threshold = 0.8) {
|
|
128
|
+
// Input validation
|
|
129
|
+
if (!targetLines || !patternLines || patternLines.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (targetLines.length < patternLines.length) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Performance limit: skip if pattern is too large (>500 lines)
|
|
138
|
+
if (patternLines.length > 500) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
let bestMatch = null;
|
|
107
143
|
let bestScore = 0;
|
|
144
|
+
const maxIterations = Math.min(targetLines.length - patternLines.length + 1, 10000);
|
|
108
145
|
|
|
109
|
-
// Sliding window search
|
|
110
|
-
for (let i = 0; i
|
|
146
|
+
// Sliding window search with iteration limit
|
|
147
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
111
148
|
const window = targetLines.slice(i, i + patternLines.length);
|
|
112
149
|
const windowText = window.join('\n');
|
|
113
150
|
const patternText = patternLines.join('\n');
|
|
114
151
|
|
|
115
|
-
|
|
152
|
+
// Calculate max acceptable distance based on threshold
|
|
153
|
+
const maxLen = Math.max(windowText.length, patternText.length);
|
|
154
|
+
const maxDistance = Math.ceil(maxLen * (1 - threshold));
|
|
155
|
+
|
|
156
|
+
const distance = levenshteinDistance(windowText, patternText, maxDistance);
|
|
157
|
+
|
|
158
|
+
// Skip if distance exceeds threshold
|
|
159
|
+
if (distance > maxDistance) continue;
|
|
160
|
+
|
|
161
|
+
const score = 1.0 - (distance / maxLen);
|
|
116
162
|
|
|
117
163
|
if (score > bestScore && score >= threshold) {
|
|
118
164
|
bestScore = score;
|
|
@@ -121,6 +167,11 @@ export function findBestMatch(targetLines, patternLines, threshold = 0.8) {
|
|
|
121
167
|
score: score,
|
|
122
168
|
lineNumber: i + 1
|
|
123
169
|
};
|
|
170
|
+
|
|
171
|
+
// Early exit if perfect match found
|
|
172
|
+
if (score >= 0.99) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
124
175
|
}
|
|
125
176
|
}
|
|
126
177
|
|
|
@@ -183,7 +234,34 @@ export function applyHunk(fileContent, hunk, threshold = 0.8) {
|
|
|
183
234
|
* Main entry point for fuzzy patching
|
|
184
235
|
*/
|
|
185
236
|
export function applyFuzzyPatch(fileContent, diffText, options = {}) {
|
|
237
|
+
// Input validation
|
|
238
|
+
if (!fileContent || typeof fileContent !== 'string') {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
content: fileContent || '',
|
|
242
|
+
message: 'Invalid file content provided'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!diffText || typeof diffText !== 'string') {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
content: fileContent,
|
|
250
|
+
message: 'Invalid diff text provided'
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
186
254
|
const threshold = options.threshold || parseFloat(process.env.MCFAST_FUZZY_THRESHOLD || '0.8');
|
|
255
|
+
|
|
256
|
+
// Validate threshold range
|
|
257
|
+
if (threshold < 0 || threshold > 1) {
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
content: fileContent,
|
|
261
|
+
message: 'Threshold must be between 0 and 1'
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
187
265
|
const hunks = parseDiff(diffText);
|
|
188
266
|
|
|
189
267
|
if (hunks.length === 0) {
|
package/src/utils/backup.js
CHANGED
|
@@ -70,7 +70,9 @@ async function cleanupOldBackups(fileName) {
|
|
|
70
70
|
*/
|
|
71
71
|
export async function validateJavaScript(filePath) {
|
|
72
72
|
try {
|
|
73
|
-
|
|
73
|
+
// Escape file path for shell command
|
|
74
|
+
const escapedPath = filePath.replace(/"/g, '\\"');
|
|
75
|
+
const { stderr } = await execAsync(`node --check "${escapedPath}"`, {
|
|
74
76
|
timeout: 5000
|
|
75
77
|
});
|
|
76
78
|
|
|
@@ -89,7 +91,9 @@ export async function validateJavaScript(filePath) {
|
|
|
89
91
|
*/
|
|
90
92
|
export async function validatePython(filePath) {
|
|
91
93
|
try {
|
|
92
|
-
|
|
94
|
+
// Escape file path for shell command
|
|
95
|
+
const escapedPath = filePath.replace(/"/g, '\\"');
|
|
96
|
+
await execAsync(`python -m py_compile "${escapedPath}"`, {
|
|
93
97
|
timeout: 5000
|
|
94
98
|
});
|
|
95
99
|
return { valid: true };
|