@mrxkun/mcfast-mcp 2.0.0 → 2.1.0
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 +2 -2
- package/src/index.js +62 -5
- package/src/strategies/edit-strategy.js +11 -4
- package/src/strategies/fuzzy-patch.js +235 -0
- package/src/utils/backup.js +202 -0
- package/src/utils/error-formatter.js +122 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Ultra-fast code editing with
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Ultra-fast code editing with fuzzy patching, auto-rollback, and 5 unified tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mcfast-mcp": "src/index.js"
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { promisify } from "util";
|
|
|
15
15
|
import fg from "fast-glob";
|
|
16
16
|
import { detectEditStrategy, extractSearchReplace } from './strategies/edit-strategy.js';
|
|
17
17
|
import { detectSearchStrategy } from './strategies/search-strategy.js';
|
|
18
|
+
import { applyFuzzyPatch } from './strategies/fuzzy-patch.js';
|
|
19
|
+
import { safeEdit } from './utils/backup.js';
|
|
20
|
+
import { formatError } from './utils/error-formatter.js';
|
|
18
21
|
|
|
19
22
|
const execAsync = promisify(exec);
|
|
20
23
|
|
|
@@ -494,7 +497,63 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
494
497
|
|
|
495
498
|
console.error(`${colors.cyan}[EDIT STRATEGY]${colors.reset} ${strategy}`);
|
|
496
499
|
|
|
497
|
-
// Strategy 1:
|
|
500
|
+
// Strategy 1: Fuzzy Patch (unified diff format)
|
|
501
|
+
if (strategy === 'fuzzy_patch') {
|
|
502
|
+
const diffText = code_edit || instruction;
|
|
503
|
+
const filePath = Object.keys(files)[0]; // Assume single file for now
|
|
504
|
+
const fileContent = files[filePath];
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
// Apply fuzzy patch with backup/rollback
|
|
508
|
+
let patchResult;
|
|
509
|
+
const editResult = await safeEdit(filePath, async () => {
|
|
510
|
+
patchResult = applyFuzzyPatch(fileContent, diffText);
|
|
511
|
+
|
|
512
|
+
if (!patchResult.success) {
|
|
513
|
+
throw new Error(patchResult.message);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Write patched content
|
|
517
|
+
await fs.writeFile(filePath, patchResult.content, 'utf8');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
if (!editResult.success) {
|
|
521
|
+
// Rollback occurred
|
|
522
|
+
return {
|
|
523
|
+
content: [{
|
|
524
|
+
type: "text",
|
|
525
|
+
text: formatError('syntax_error', {
|
|
526
|
+
filePath,
|
|
527
|
+
error: editResult.error,
|
|
528
|
+
backupPath: editResult.backupPath
|
|
529
|
+
})
|
|
530
|
+
}],
|
|
531
|
+
isError: true
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
content: [{
|
|
537
|
+
type: "text",
|
|
538
|
+
text: `✅ Fuzzy Patch Applied Successfully\n\n${patchResult.message}\nConfidence: ${(patchResult.confidence * 100).toFixed(1)}%\n\nBackup: ${editResult.backupPath}`
|
|
539
|
+
}]
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
} catch (error) {
|
|
543
|
+
return {
|
|
544
|
+
content: [{
|
|
545
|
+
type: "text",
|
|
546
|
+
text: formatError('fuzzy_match_failed', {
|
|
547
|
+
filePath,
|
|
548
|
+
error: error.message
|
|
549
|
+
})
|
|
550
|
+
}],
|
|
551
|
+
isError: true
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Strategy 2: Search/Replace (deterministic, fastest)
|
|
498
557
|
if (strategy === 'search_replace') {
|
|
499
558
|
const extracted = extractSearchReplace(instruction);
|
|
500
559
|
if (extracted) {
|
|
@@ -507,10 +566,8 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
507
566
|
}
|
|
508
567
|
}
|
|
509
568
|
|
|
510
|
-
// Strategy
|
|
569
|
+
// Strategy 3: Placeholder Merge (token-efficient)
|
|
511
570
|
if (strategy === 'placeholder_merge' && code_edit) {
|
|
512
|
-
// Use edit_file logic for placeholder-based editing
|
|
513
|
-
// For now, we'll route to Mercury with a hint
|
|
514
571
|
return await handleApplyFast({
|
|
515
572
|
instruction: `${instruction}\n\nUSE PLACEHOLDER MERGE STRATEGY. Code snippet:\n${code_edit}`,
|
|
516
573
|
files,
|
|
@@ -519,7 +576,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
519
576
|
});
|
|
520
577
|
}
|
|
521
578
|
|
|
522
|
-
// Strategy
|
|
579
|
+
// Strategy 4: Mercury Intelligent (most flexible, default)
|
|
523
580
|
return await handleApplyFast({
|
|
524
581
|
instruction,
|
|
525
582
|
files,
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Determines the best editing strategy based on input parameters
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { isDiffBasedEdit } from './fuzzy-patch.js';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Detect if instruction is a simple search/replace
|
|
8
10
|
*/
|
|
@@ -62,19 +64,24 @@ export function hasPlaceholders(codeEdit) {
|
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* Determine the best edit strategy
|
|
65
|
-
* @returns {'search_replace' | 'placeholder_merge' | 'mercury_intelligent'}
|
|
67
|
+
* @returns {'fuzzy_patch' | 'search_replace' | 'placeholder_merge' | 'mercury_intelligent'}
|
|
66
68
|
*/
|
|
67
69
|
export function detectEditStrategy({ instruction, code_edit, files }) {
|
|
68
|
-
// Priority 1:
|
|
70
|
+
// Priority 1: Fuzzy Patch (unified diff format)
|
|
71
|
+
if (isDiffBasedEdit(instruction) || isDiffBasedEdit(code_edit || '')) {
|
|
72
|
+
return 'fuzzy_patch';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Priority 2: Search/Replace (fastest, deterministic)
|
|
69
76
|
if (isSearchReplace(instruction)) {
|
|
70
77
|
return 'search_replace';
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
// Priority
|
|
80
|
+
// Priority 3: Placeholder Merge (token-efficient)
|
|
74
81
|
if (code_edit && hasPlaceholders(code_edit)) {
|
|
75
82
|
return 'placeholder_merge';
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
// Priority
|
|
85
|
+
// Priority 4: Mercury Intelligent (most flexible)
|
|
79
86
|
return 'mercury_intelligent';
|
|
80
87
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuzzy Patching Engine for mcfast v2.1
|
|
3
|
+
* Applies code changes with tolerance for whitespace and minor formatting differences
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate Levenshtein distance between two strings
|
|
8
|
+
* Used for fuzzy matching to find best location for patch
|
|
9
|
+
*/
|
|
10
|
+
function levenshteinDistance(str1, str2) {
|
|
11
|
+
const len1 = str1.length;
|
|
12
|
+
const len2 = str2.length;
|
|
13
|
+
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
|
|
16
|
+
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
|
|
17
|
+
|
|
18
|
+
for (let i = 1; i <= len1; i++) {
|
|
19
|
+
for (let j = 1; j <= len2; j++) {
|
|
20
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
21
|
+
matrix[i][j] = Math.min(
|
|
22
|
+
matrix[i - 1][j] + 1, // deletion
|
|
23
|
+
matrix[i][j - 1] + 1, // insertion
|
|
24
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return matrix[len1][len2];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize whitespace for comparison
|
|
34
|
+
* Converts tabs to spaces, trims lines, removes trailing whitespace
|
|
35
|
+
*/
|
|
36
|
+
function normalizeWhitespace(text) {
|
|
37
|
+
return text
|
|
38
|
+
.split('\n')
|
|
39
|
+
.map(line => line.replace(/\t/g, ' ').trimEnd())
|
|
40
|
+
.join('\n')
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculate similarity score between two strings (0-1)
|
|
46
|
+
* 1.0 = identical, 0.0 = completely different
|
|
47
|
+
*/
|
|
48
|
+
function similarityScore(str1, str2) {
|
|
49
|
+
const normalized1 = normalizeWhitespace(str1);
|
|
50
|
+
const normalized2 = normalizeWhitespace(str2);
|
|
51
|
+
|
|
52
|
+
const maxLen = Math.max(normalized1.length, normalized2.length);
|
|
53
|
+
if (maxLen === 0) return 1.0;
|
|
54
|
+
|
|
55
|
+
const distance = levenshteinDistance(normalized1, normalized2);
|
|
56
|
+
return 1.0 - (distance / maxLen);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse unified diff format
|
|
61
|
+
* Returns array of hunks with line numbers and changes
|
|
62
|
+
*/
|
|
63
|
+
export function parseDiff(diffText) {
|
|
64
|
+
const hunks = [];
|
|
65
|
+
const lines = diffText.split('\n');
|
|
66
|
+
let currentHunk = null;
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const line = lines[i];
|
|
70
|
+
|
|
71
|
+
// Parse hunk header: @@ -1,3 +1,4 @@
|
|
72
|
+
if (line.startsWith('@@')) {
|
|
73
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
74
|
+
|
|
75
|
+
const match = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
76
|
+
if (match) {
|
|
77
|
+
currentHunk = {
|
|
78
|
+
oldStart: parseInt(match[1]),
|
|
79
|
+
oldLines: parseInt(match[2] || '1'),
|
|
80
|
+
newStart: parseInt(match[3]),
|
|
81
|
+
newLines: parseInt(match[4] || '1'),
|
|
82
|
+
lines: []
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
} else if (currentHunk) {
|
|
86
|
+
// Parse diff lines
|
|
87
|
+
if (line.startsWith('-')) {
|
|
88
|
+
currentHunk.lines.push({ type: 'remove', content: line.slice(1) });
|
|
89
|
+
} else if (line.startsWith('+')) {
|
|
90
|
+
currentHunk.lines.push({ type: 'add', content: line.slice(1) });
|
|
91
|
+
} else if (line.startsWith(' ')) {
|
|
92
|
+
currentHunk.lines.push({ type: 'context', content: line.slice(1) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
98
|
+
return hunks;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find best match location for a pattern in target text
|
|
103
|
+
* Returns { index, score, lineNumber } or null if no good match
|
|
104
|
+
*/
|
|
105
|
+
export function findBestMatch(targetLines, patternLines, threshold = 0.8) {
|
|
106
|
+
let bestMatch = null;
|
|
107
|
+
let bestScore = 0;
|
|
108
|
+
|
|
109
|
+
// Sliding window search
|
|
110
|
+
for (let i = 0; i <= targetLines.length - patternLines.length; i++) {
|
|
111
|
+
const window = targetLines.slice(i, i + patternLines.length);
|
|
112
|
+
const windowText = window.join('\n');
|
|
113
|
+
const patternText = patternLines.join('\n');
|
|
114
|
+
|
|
115
|
+
const score = similarityScore(windowText, patternText);
|
|
116
|
+
|
|
117
|
+
if (score > bestScore && score >= threshold) {
|
|
118
|
+
bestScore = score;
|
|
119
|
+
bestMatch = {
|
|
120
|
+
index: i,
|
|
121
|
+
score: score,
|
|
122
|
+
lineNumber: i + 1
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return bestMatch;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Apply a single hunk to file content with fuzzy matching
|
|
132
|
+
* Returns { success, content, confidence, message }
|
|
133
|
+
*/
|
|
134
|
+
export function applyHunk(fileContent, hunk, threshold = 0.8) {
|
|
135
|
+
const lines = fileContent.split('\n');
|
|
136
|
+
|
|
137
|
+
// Extract context lines (lines that should match)
|
|
138
|
+
const contextLines = hunk.lines
|
|
139
|
+
.filter(l => l.type === 'context' || l.type === 'remove')
|
|
140
|
+
.map(l => l.content);
|
|
141
|
+
|
|
142
|
+
// Find best match location
|
|
143
|
+
const match = findBestMatch(lines, contextLines, threshold);
|
|
144
|
+
|
|
145
|
+
if (!match) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
content: fileContent,
|
|
149
|
+
confidence: 0,
|
|
150
|
+
message: `Could not find matching location (threshold: ${threshold})`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Apply changes at matched location
|
|
155
|
+
const newLines = [...lines];
|
|
156
|
+
let offset = match.index;
|
|
157
|
+
|
|
158
|
+
for (const diffLine of hunk.lines) {
|
|
159
|
+
if (diffLine.type === 'remove' || diffLine.type === 'context') {
|
|
160
|
+
// Remove or skip context line
|
|
161
|
+
if (diffLine.type === 'remove') {
|
|
162
|
+
newLines.splice(offset, 1);
|
|
163
|
+
} else {
|
|
164
|
+
offset++;
|
|
165
|
+
}
|
|
166
|
+
} else if (diffLine.type === 'add') {
|
|
167
|
+
// Insert new line
|
|
168
|
+
newLines.splice(offset, 0, diffLine.content);
|
|
169
|
+
offset++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
content: newLines.join('\n'),
|
|
176
|
+
confidence: match.score,
|
|
177
|
+
message: `Applied at line ${match.lineNumber} (confidence: ${(match.score * 100).toFixed(1)}%)`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Apply complete diff with fuzzy matching
|
|
183
|
+
* Main entry point for fuzzy patching
|
|
184
|
+
*/
|
|
185
|
+
export function applyFuzzyPatch(fileContent, diffText, options = {}) {
|
|
186
|
+
const threshold = options.threshold || parseFloat(process.env.MCFAST_FUZZY_THRESHOLD || '0.8');
|
|
187
|
+
const hunks = parseDiff(diffText);
|
|
188
|
+
|
|
189
|
+
if (hunks.length === 0) {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
content: fileContent,
|
|
193
|
+
message: 'No valid hunks found in diff'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let currentContent = fileContent;
|
|
198
|
+
const results = [];
|
|
199
|
+
|
|
200
|
+
for (const hunk of hunks) {
|
|
201
|
+
const result = applyHunk(currentContent, hunk, threshold);
|
|
202
|
+
results.push(result);
|
|
203
|
+
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
content: fileContent,
|
|
208
|
+
message: `Failed to apply hunk: ${result.message}`,
|
|
209
|
+
partialResults: results
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
currentContent = result.content;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const avgConfidence = results.reduce((sum, r) => sum + r.confidence, 0) / results.length;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
content: currentContent,
|
|
221
|
+
confidence: avgConfidence,
|
|
222
|
+
message: `Applied ${hunks.length} hunk(s) successfully`,
|
|
223
|
+
details: results
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Detect if instruction is a diff-based edit
|
|
229
|
+
* Returns true if instruction contains unified diff markers
|
|
230
|
+
*/
|
|
231
|
+
export function isDiffBasedEdit(instruction) {
|
|
232
|
+
if (!instruction) return false;
|
|
233
|
+
// Check for unified diff hunk markers (@@)
|
|
234
|
+
return instruction.includes('@@');
|
|
235
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup and Rollback Manager for mcfast v2.1
|
|
3
|
+
* Handles file backups before edits and auto-rollback on errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
const BACKUP_DIR = '.mcfast-backup';
|
|
14
|
+
const MAX_BACKUPS = 5;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create backup of a file before editing
|
|
18
|
+
* Returns backup path
|
|
19
|
+
*/
|
|
20
|
+
export async function createBackup(filePath) {
|
|
21
|
+
const timestamp = Date.now();
|
|
22
|
+
const fileName = path.basename(filePath);
|
|
23
|
+
const backupPath = path.join(BACKUP_DIR, `${fileName}.${timestamp}`);
|
|
24
|
+
|
|
25
|
+
// Ensure backup directory exists
|
|
26
|
+
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Copy file to backup
|
|
29
|
+
await fs.copyFile(filePath, backupPath);
|
|
30
|
+
|
|
31
|
+
// Cleanup old backups
|
|
32
|
+
await cleanupOldBackups(fileName);
|
|
33
|
+
|
|
34
|
+
return backupPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Restore file from backup
|
|
39
|
+
*/
|
|
40
|
+
export async function restoreFromBackup(filePath, backupPath) {
|
|
41
|
+
await fs.copyFile(backupPath, filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cleanup old backups, keeping only the most recent MAX_BACKUPS
|
|
46
|
+
*/
|
|
47
|
+
async function cleanupOldBackups(fileName) {
|
|
48
|
+
try {
|
|
49
|
+
const files = await fs.readdir(BACKUP_DIR);
|
|
50
|
+
const backups = files
|
|
51
|
+
.filter(f => f.startsWith(fileName + '.'))
|
|
52
|
+
.map(f => ({
|
|
53
|
+
name: f,
|
|
54
|
+
timestamp: parseInt(f.split('.').pop())
|
|
55
|
+
}))
|
|
56
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
57
|
+
|
|
58
|
+
// Remove old backups
|
|
59
|
+
for (let i = MAX_BACKUPS; i < backups.length; i++) {
|
|
60
|
+
await fs.unlink(path.join(BACKUP_DIR, backups[i].name));
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Ignore cleanup errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate JavaScript/TypeScript syntax
|
|
69
|
+
* Returns { valid: boolean, error?: string }
|
|
70
|
+
*/
|
|
71
|
+
export async function validateJavaScript(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
const { stderr } = await execAsync(`node --check "${filePath}"`, {
|
|
74
|
+
timeout: 5000
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (stderr && stderr.includes('SyntaxError')) {
|
|
78
|
+
return { valid: false, error: stderr };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { valid: true };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return { valid: false, error: error.message };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate Python syntax
|
|
89
|
+
*/
|
|
90
|
+
export async function validatePython(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
await execAsync(`python -m py_compile "${filePath}"`, {
|
|
93
|
+
timeout: 5000
|
|
94
|
+
});
|
|
95
|
+
return { valid: true };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return { valid: false, error: error.message };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generic file validation (check for truncation, empty file)
|
|
103
|
+
*/
|
|
104
|
+
export async function validateGeneric(filePath, originalSize) {
|
|
105
|
+
try {
|
|
106
|
+
const stats = await fs.stat(filePath);
|
|
107
|
+
|
|
108
|
+
// Check if file was truncated
|
|
109
|
+
if (stats.size === 0 && originalSize > 0) {
|
|
110
|
+
return { valid: false, error: 'File was truncated to 0 bytes' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if file size changed dramatically (>90% reduction)
|
|
114
|
+
if (originalSize > 0 && stats.size < originalSize * 0.1) {
|
|
115
|
+
return { valid: false, error: 'File size reduced by >90%, possible corruption' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { valid: true };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return { valid: false, error: error.message };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate file based on extension
|
|
126
|
+
*/
|
|
127
|
+
export async function validateFile(filePath, originalSize) {
|
|
128
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
129
|
+
|
|
130
|
+
// JavaScript/TypeScript
|
|
131
|
+
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(ext)) {
|
|
132
|
+
return await validateJavaScript(filePath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Python
|
|
136
|
+
if (['.py'].includes(ext)) {
|
|
137
|
+
return await validatePython(filePath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Generic validation for other files
|
|
141
|
+
return await validateGeneric(filePath, originalSize);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Safe edit with automatic rollback
|
|
146
|
+
* Returns { success, backupPath?, error? }
|
|
147
|
+
*/
|
|
148
|
+
export async function safeEdit(filePath, editFn) {
|
|
149
|
+
let backupPath = null;
|
|
150
|
+
let originalSize = 0;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Get original size
|
|
154
|
+
const stats = await fs.stat(filePath);
|
|
155
|
+
originalSize = stats.size;
|
|
156
|
+
|
|
157
|
+
// Create backup
|
|
158
|
+
backupPath = await createBackup(filePath);
|
|
159
|
+
|
|
160
|
+
// Apply edit
|
|
161
|
+
await editFn();
|
|
162
|
+
|
|
163
|
+
// Validate result
|
|
164
|
+
const validation = await validateFile(filePath, originalSize);
|
|
165
|
+
|
|
166
|
+
if (!validation.valid) {
|
|
167
|
+
// Rollback on validation failure
|
|
168
|
+
await restoreFromBackup(filePath, backupPath);
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: `Validation failed: ${validation.error}`,
|
|
172
|
+
rolledBack: true
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
backupPath
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Rollback on any error
|
|
183
|
+
if (backupPath) {
|
|
184
|
+
try {
|
|
185
|
+
await restoreFromBackup(filePath, backupPath);
|
|
186
|
+
} catch (rollbackError) {
|
|
187
|
+
// Rollback failed, critical error
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
error: error.message,
|
|
191
|
+
rollbackError: rollbackError.message
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
error: error.message,
|
|
199
|
+
rolledBack: !!backupPath
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Formatter for mcfast v2.1
|
|
3
|
+
* Provides actionable error messages with context and suggestions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format error with context and suggestions
|
|
8
|
+
*/
|
|
9
|
+
export function formatError(errorType, context = {}) {
|
|
10
|
+
const formatters = {
|
|
11
|
+
'pattern_not_found': () => ({
|
|
12
|
+
title: '❌ Edit Failed: Pattern not found in target file',
|
|
13
|
+
suggestions: [
|
|
14
|
+
'The code may have changed since last read',
|
|
15
|
+
'Try using \'read\' tool to get current content',
|
|
16
|
+
'Use fuzzy mode for tolerance: { "fuzzy": true }'
|
|
17
|
+
],
|
|
18
|
+
context: {
|
|
19
|
+
'Looking for': context.pattern ? `"${context.pattern.substring(0, 50)}..."` : 'N/A',
|
|
20
|
+
'In file': context.filePath || 'N/A',
|
|
21
|
+
'File size': context.fileSize ? `${context.fileSize} lines` : 'N/A'
|
|
22
|
+
}
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
'syntax_error': () => ({
|
|
26
|
+
title: '❌ Syntax Error Detected (Auto-Rolled Back)',
|
|
27
|
+
suggestions: [
|
|
28
|
+
'The edit introduced a syntax error',
|
|
29
|
+
'File has been restored from backup',
|
|
30
|
+
'Review the instruction and try again'
|
|
31
|
+
],
|
|
32
|
+
context: {
|
|
33
|
+
'File': context.filePath || 'N/A',
|
|
34
|
+
'Error': context.error || 'N/A',
|
|
35
|
+
'Backup': context.backupPath || 'N/A'
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
'fuzzy_match_failed': () => ({
|
|
40
|
+
title: '❌ Fuzzy Patch Failed: No suitable match found',
|
|
41
|
+
suggestions: [
|
|
42
|
+
`Confidence threshold: ${context.threshold || 0.8} (adjust with MCFAST_FUZZY_THRESHOLD)`,
|
|
43
|
+
'The diff may be too different from current file state',
|
|
44
|
+
'Try using Mercury AI for complex changes'
|
|
45
|
+
],
|
|
46
|
+
context: {
|
|
47
|
+
'File': context.filePath || 'N/A',
|
|
48
|
+
'Best match score': context.bestScore ? `${(context.bestScore * 100).toFixed(1)}%` : 'N/A'
|
|
49
|
+
}
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
'file_not_found': () => ({
|
|
53
|
+
title: '❌ File Not Found',
|
|
54
|
+
suggestions: [
|
|
55
|
+
'Verify the file path is correct',
|
|
56
|
+
'Use \'list_files\' to browse available files',
|
|
57
|
+
'Check if file was moved or deleted'
|
|
58
|
+
],
|
|
59
|
+
context: {
|
|
60
|
+
'Path': context.filePath || 'N/A'
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
'permission_denied': () => ({
|
|
65
|
+
title: '❌ Permission Denied',
|
|
66
|
+
suggestions: [
|
|
67
|
+
'Check file permissions',
|
|
68
|
+
'Ensure you have write access to the file',
|
|
69
|
+
'Try running with appropriate permissions'
|
|
70
|
+
],
|
|
71
|
+
context: {
|
|
72
|
+
'File': context.filePath || 'N/A'
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
'generic': () => ({
|
|
77
|
+
title: '❌ Operation Failed',
|
|
78
|
+
suggestions: [
|
|
79
|
+
'Check the error message below for details',
|
|
80
|
+
'Verify your input parameters',
|
|
81
|
+
'Try again or use a different approach'
|
|
82
|
+
],
|
|
83
|
+
context: {
|
|
84
|
+
'Error': context.error || 'Unknown error'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const formatter = formatters[errorType] || formatters['generic'];
|
|
90
|
+
const formatted = formatter();
|
|
91
|
+
|
|
92
|
+
// Build output
|
|
93
|
+
let output = `${formatted.title}\n\n`;
|
|
94
|
+
|
|
95
|
+
if (formatted.suggestions.length > 0) {
|
|
96
|
+
output += 'Suggestions:\n';
|
|
97
|
+
formatted.suggestions.forEach(s => {
|
|
98
|
+
output += ` • ${s}\n`;
|
|
99
|
+
});
|
|
100
|
+
output += '\n';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (formatted.context && Object.keys(formatted.context).length > 0) {
|
|
104
|
+
output += 'Context:\n';
|
|
105
|
+
Object.entries(formatted.context).forEach(([key, value]) => {
|
|
106
|
+
output += ` ${key}: ${value}\n`;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wrap error with formatting
|
|
115
|
+
*/
|
|
116
|
+
export function createFormattedError(errorType, context) {
|
|
117
|
+
const message = formatError(errorType, context);
|
|
118
|
+
const error = new Error(message);
|
|
119
|
+
error.formatted = true;
|
|
120
|
+
error.errorType = errorType;
|
|
121
|
+
return error;
|
|
122
|
+
}
|