fileditor-mcp 1.0.2 → 1.0.3

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.
@@ -1,324 +1,298 @@
1
- import { FileUtils } from '../utils/fileUtils.js';
2
-
3
- /**
4
- * apply_diffs tool handler
5
- */
6
- export class ApplyDiffHandler {
7
-
8
- /**
9
- * Format content with line numbers for error display
10
- * @param {string} content - Content to format
11
- * @param {number} startLine - Starting line number
12
- * @returns {string} Formatted content with line numbers
13
- */
14
- static formatContentWithLineNumbers(content, startLine) {
15
- const lines = content.split('\n');
16
- return lines.map((line, index) => `${startLine + index} | ${line}`).join('\n');
17
- } /**
18
- * Apply or validate a single diff operation
19
- * @param {Object} diff - Diff operation
20
- * @param {Array} lines - File lines
21
- * @param {number} lineOffset - Current line offset
22
- * @param {boolean} trim - Whether to trim whitespace when comparing
23
- * @param {boolean} dryRun - Whether this is a validation-only run
24
- * @returns {Object} Operation result
25
- */
26
- static applySingleDiff(diff, lines, lineOffset, trim = false, dryRun = false) {
27
- const { search_content: searchContent, replace_content: replaceContent, start_line: originalStartLine, originalIndex } = diff;
28
-
29
- try {
30
- // Calculate actual start line with offset
31
- const actualStartLine = originalStartLine + lineOffset;
32
-
33
- // Check if start line number exceeds current file range
34
- if (actualStartLine > lines.length) {
35
- throw new Error(`start_line (${originalStartLine} -> ${actualStartLine}) exceeds file length (${lines.length} lines)`);
36
- }
37
-
38
- // Split search content into line arrays
39
- const searchLines = searchContent.split('\n');
40
-
41
- // Determine the end line number for search
42
- const endLine = actualStartLine + searchLines.length - 1;
43
-
44
- // Check if search range exceeds file range
45
- if (endLine > lines.length) {
46
- throw new Error(`Search content extends beyond file length. Start line: ${originalStartLine} (actual: ${actualStartLine}), search lines: ${searchLines.length}, file lines: ${lines.length}`);
47
- }
48
-
49
- // Extract content to match
50
- const targetLines = lines.slice(actualStartLine - 1, endLine);
51
- const targetContent = targetLines.join('\n');
52
-
53
- // Prepare content for comparison based on trim setting
54
- let searchForComparison = searchContent;
55
- let targetForComparison = targetContent;
56
-
57
- if (trim) {
58
- // Apply trim to each line and rejoin
59
- const searchLinesForComparison = searchContent.split('\n').map(line => line.trim());
60
- const targetLinesForComparison = targetLines.map(line => line.trim());
61
-
62
- searchForComparison = searchLinesForComparison.join('\n');
63
- targetForComparison = targetLinesForComparison.join('\n');
64
- }
65
-
66
- // Exact match check (using trimmed or original content based on trim setting)
67
- if (targetForComparison !== searchForComparison) {
68
- // Always show original content in error messages for debugging
69
- const expectedFormatted = this.formatContentWithLineNumbers(searchContent, actualStartLine);
70
- const actualFormatted = this.formatContentWithLineNumbers(targetContent, actualStartLine);
71
- const detailedError = `Content mismatch at line ${originalStartLine} (actual: ${actualStartLine}).\n\nExpected content:\n${expectedFormatted}\n\nActual content:\n${actualFormatted}\n\nThis is diff #${originalIndex + 1} in the batch.`;
72
- throw new Error(detailedError);
73
- }
74
-
75
- if (dryRun) {
76
- // Validation run: just return metadata for offset calculation
77
- const replaceLines = replaceContent.split('\n');
78
- const lineDiff = replaceLines.length - searchLines.length;
79
-
80
- return {
81
- success: true,
82
- actualStartLine,
83
- endLine,
84
- searchLines,
85
- replaceLines,
86
- lineDiff,
87
- originalIndex
88
- };
89
- } else {
90
- // Actual application: modify the lines array
91
- const replaceLines = replaceContent.split('\n');
92
-
93
- // Perform replacement
94
- const beforeLines = lines.slice(0, actualStartLine - 1);
95
- const afterLines = lines.slice(endLine);
96
- const newLines = [...beforeLines, ...replaceLines, ...afterLines];
97
-
98
- // Calculate line diff for offset tracking
99
- const lineDiff = replaceLines.length - searchLines.length;
100
-
101
- return {
102
- success: true,
103
- newLines,
104
- lineDiff,
105
- actualStartLine,
106
- searchLines,
107
- replaceLines,
108
- originalIndex
109
- };
110
- }
111
- } catch (error) {
112
- return {
113
- success: false,
114
- error: error.message,
115
- originalIndex,
116
- originalStartLine
117
- };
118
- }
119
- }
120
-
121
-
122
-
123
- /**
124
- * Handle apply_diffs request (supports both single diff and batch diffs)
125
- * @param {Object} args - Request parameters
126
- * @returns {Promise<Object>} Response result
127
- */
128
- static async handle(args) {
129
- const { path, search_content, replace_content, start_line, atomic = true, trim = false } = args;
130
-
131
- // Check if file exists
132
- if (!FileUtils.fileExists(path)) {
133
- throw new Error(`File not found: ${path}`);
134
- }
135
-
136
- // Normalize all parameters to arrays
137
- let searchArray, replaceArray, startLineArray;
138
-
139
- if (Array.isArray(search_content)) {
140
- searchArray = search_content;
141
- replaceArray = Array.isArray(replace_content) ? replace_content : [replace_content];
142
- startLineArray = Array.isArray(start_line) ? start_line : [start_line];
143
- } else {
144
- searchArray = [search_content];
145
- replaceArray = Array.isArray(replace_content) ? replace_content : [replace_content];
146
- startLineArray = Array.isArray(start_line) ? start_line : [start_line];
147
- }
148
-
149
- // Validate array lengths match
150
- if (searchArray.length !== replaceArray.length || searchArray.length !== startLineArray.length) {
151
- throw new Error("Array lengths for search_content, replace_content, and start_line must match");
152
- }
153
-
154
- const diffCount = searchArray.length;
155
-
156
- // Validate all start_line values
157
- for (let i = 0; i < diffCount; i++) {
158
- if (startLineArray[i] < 1) {
159
- throw new Error(`Invalid start_line: ${startLineArray[i]} at index ${i}. Line numbers start from 1.`);
160
- }
161
- }
162
-
163
- // Create diff objects and sort by start_line
164
- const diffs = searchArray.map((searchContent, index) => ({
165
- search_content: searchContent,
166
- replace_content: replaceArray[index],
167
- start_line: startLineArray[index],
168
- originalIndex: index
169
- })).sort((a, b) => a.start_line - b.start_line);
170
-
171
- // Read file content
172
- const content = await FileUtils.readFile(path);
173
- let lines = content.split('\n'); if (atomic && diffCount > 1) {
174
- // Atomic mode: validate all diffs first
175
- let lineOffset = 0;
176
- const validationErrors = [];
177
- let tempLines = [...lines]; // Work with a copy for validation
178
-
179
- for (const diff of diffs) {
180
- const validation = this.applySingleDiff(diff, tempLines, lineOffset, trim, true);
181
-
182
- if (!validation.success) {
183
- validationErrors.push({
184
- index: validation.originalIndex,
185
- start_line: validation.originalStartLine,
186
- status: "fail",
187
- message: validation.error
188
- });
189
- break; // Stop validation on first error
190
- } else {
191
- // Simulate the replacement to update tempLines and lineOffset
192
- const beforeLines = tempLines.slice(0, validation.actualStartLine - 1);
193
- const afterLines = tempLines.slice(validation.endLine);
194
- tempLines = [...beforeLines, ...validation.replaceLines, ...afterLines];
195
-
196
- // Update line offset for next validation
197
- lineOffset += validation.lineDiff;
198
- }
199
- }
200
-
201
- if (validationErrors.length > 0) {
202
- // In atomic mode, if any diff fails, abort the entire operation
203
- const results = new Array(diffCount);
204
- validationErrors.forEach(error => {
205
- results[error.index] = error;
206
- });
207
-
208
- // Fill in success placeholders for non-failed diffs
209
- for (let i = 0; i < diffCount; i++) {
210
- if (!results[i]) {
211
- results[i] = {
212
- index: i,
213
- start_line: startLineArray[i],
214
- status: "aborted",
215
- message: "Operation aborted due to validation failures in atomic mode"
216
- };
217
- }
218
- }
219
-
220
- // Create detailed error message with formatted content
221
- let detailedMessage = `Atomic operation failed: ${validationErrors.length}/${diffCount} diffs would fail. No changes applied.\n\n`;
222
- detailedMessage += "Detailed results:\n";
223
- results.forEach((result, index) => {
224
- detailedMessage += `\nDiff ${index + 1}:\n`;
225
- detailedMessage += ` Status: ${result.status}\n`;
226
- detailedMessage += ` Start Line: ${result.start_line}\n`;
227
- detailedMessage += ` Message: ${result.message}\n`;
228
- });
229
-
230
- throw new Error(detailedMessage);
231
- }
232
- }
233
-
234
- // Track results for each diff
235
- const results = new Array(diffCount);
236
- let lineOffset = 0; // Track cumulative line offset
237
- let appliedCount = 0; // Process each diff in order
238
- for (const diff of diffs) {
239
- const { search_content: searchContent, replace_content: replaceContent, start_line: originalStartLine, originalIndex } = diff;
240
-
241
- if (!atomic || diffCount === 1) {
242
- // Non-atomic mode or single diff: validate and apply one by one
243
- const result = this.applySingleDiff(diff, lines, lineOffset, trim, false);
244
-
245
- if (!result.success) {
246
- results[originalIndex] = {
247
- index: originalIndex,
248
- start_line: originalStartLine,
249
- status: "fail",
250
- message: result.error
251
- };
252
- continue;
253
- }
254
-
255
- // Update lines and offset
256
- lines = result.newLines;
257
- lineOffset += result.lineDiff;
258
- appliedCount++;
259
-
260
- // Record success
261
- results[originalIndex] = {
262
- index: originalIndex,
263
- start_line: originalStartLine,
264
- status: "success",
265
- message: `Replaced ${result.searchLines.length} line(s) at line ${originalStartLine}${result.lineDiff !== 0 ? ` (${result.lineDiff > 0 ? 'added' : 'removed'} ${Math.abs(result.lineDiff)} line(s))` : ''}`
266
- };
267
- } else {
268
- // Atomic mode: just apply the diff (validation already done)
269
- const result = this.applySingleDiff(diff, lines, lineOffset, trim, false);
270
-
271
- // Update lines and offset
272
- lines = result.newLines;
273
- lineOffset += result.lineDiff;
274
- appliedCount++;
275
-
276
- // Record success
277
- results[originalIndex] = {
278
- index: originalIndex,
279
- start_line: originalStartLine,
280
- status: "success",
281
- message: `Replaced ${result.searchLines.length} line(s) at line ${originalStartLine}${result.lineDiff !== 0 ? ` (${result.lineDiff > 0 ? 'added' : 'removed'} ${Math.abs(result.lineDiff)} line(s))` : ''}`
282
- };
283
- }
284
- }
285
-
286
- // Write back to file
287
- const newContent = lines.join('\n');
288
- await FileUtils.writeFile(path, newContent);
289
-
290
- // Build response
291
- const failedCount = diffCount - appliedCount;
292
- const isBatchMode = diffCount > 1;
293
-
294
- let message;
295
- if (isBatchMode) {
296
- const mode = atomic ? "atomic" : "non-atomic";
297
- message = `Batch diff operation (${mode}) completed: ${appliedCount}/${diffCount} diffs applied successfully to ${path}`;
298
- if (failedCount > 0) {
299
- message += ` (${failedCount} failed)`;
300
- }
301
- message += `. File now has ${lines.length} lines.\n\n`;
302
-
303
- // Add detailed results for batch mode
304
- message += "Detailed results:\n";
305
- results.forEach((result, index) => {
306
- message += `\nDiff ${index + 1}:\n`;
307
- message += ` Status: ${result.status}\n`;
308
- message += ` Start Line: ${result.start_line}\n`;
309
- message += ` Message: ${result.message}\n`;
310
- });
311
- } else {
312
- // Single mode - maintain original message format
313
- const result = results[0];
314
- if (result.status === "success") {
315
- message = `Successfully applied diff to ${path}: ${result.message}. File now has ${lines.length} lines.`;
316
- } else {
317
- throw new Error(`Single diff failed:\n\n${result.message}`);
318
- }
319
- }
320
-
321
- // Create success response using FileUtils
322
- return FileUtils.createResponse(message);
323
- }
324
- }
1
+ import { FileUtils } from '../utils/fileUtils.js';
2
+
3
+ /**
4
+ * apply_diffs tool handler
5
+ */
6
+ export class ApplyDiffHandler {
7
+
8
+ /**
9
+ * Format content with line numbers for error display
10
+ * @param {string} content - Content to format
11
+ * @param {number} startLine - Starting line number
12
+ * @returns {string} Formatted content with line numbers
13
+ */
14
+ static formatContentWithLineNumbers(content, startLine) {
15
+ const lines = content.split('\n');
16
+ return lines.map((line, index) => `${startLine + index} | ${line}`).join('\n');
17
+ }
18
+
19
+ /**
20
+ * Validate a single diff operation
21
+ * @param {Object} diff - Diff operation to validate
22
+ * @param {Array} lines - File lines
23
+ * @param {number} lineOffset - Current line offset
24
+ * @param {boolean} trim - Whether to trim whitespace when comparing
25
+ * @returns {Object} Validation result
26
+ */
27
+ static validateDiff(diff, lines, lineOffset, trim = false) {
28
+ const { search_content: searchContent, start_line: originalStartLine, originalIndex } = diff;
29
+
30
+ try {
31
+ // Calculate actual start line with offset
32
+ const actualStartLine = originalStartLine + lineOffset;
33
+
34
+ // Check if start line number exceeds current file range
35
+ if (actualStartLine > lines.length) {
36
+ throw new Error(`start_line (${originalStartLine} -> ${actualStartLine}) exceeds file length (${lines.length} lines)`);
37
+ }
38
+
39
+ // Split search content into line arrays
40
+ const searchLines = searchContent.split('\n');
41
+
42
+ // Determine the end line number for search
43
+ const endLine = actualStartLine + searchLines.length - 1;
44
+
45
+ // Check if search range exceeds file range
46
+ if (endLine > lines.length) {
47
+ throw new Error(`Search content extends beyond file length. Start line: ${originalStartLine} (actual: ${actualStartLine}), search lines: ${searchLines.length}, file lines: ${lines.length}`);
48
+ }
49
+
50
+ // Extract content to match
51
+ const targetLines = lines.slice(actualStartLine - 1, endLine);
52
+ const targetContent = targetLines.join('\n');
53
+
54
+ // Prepare content for comparison based on trim setting
55
+ let searchForComparison = searchContent;
56
+ let targetForComparison = targetContent;
57
+
58
+ if (trim) {
59
+ // Apply trim to each line and rejoin
60
+ const searchLinesForComparison = searchContent.split('\n').map(line => line.trim());
61
+ const targetLinesForComparison = targetLines.map(line => line.trim());
62
+
63
+ searchForComparison = searchLinesForComparison.join('\n');
64
+ targetForComparison = targetLinesForComparison.join('\n');
65
+ }
66
+
67
+ // Exact match check (using trimmed or original content based on trim setting)
68
+ if (targetForComparison !== searchForComparison) {
69
+ // Always show original content in error messages for debugging
70
+ const expectedFormatted = this.formatContentWithLineNumbers(searchContent, actualStartLine);
71
+ const actualFormatted = this.formatContentWithLineNumbers(targetContent, actualStartLine);
72
+ const detailedError = `Content mismatch at line ${originalStartLine} (actual: ${actualStartLine}).\n\nExpected content:\n${expectedFormatted}\n\nActual content:\n${actualFormatted}\n\nThis is diff #${originalIndex + 1} in the batch.`;
73
+ throw new Error(detailedError);
74
+ }
75
+
76
+ return {
77
+ success: true,
78
+ actualStartLine,
79
+ endLine,
80
+ searchLines
81
+ };
82
+ } catch (error) {
83
+ return {
84
+ success: false,
85
+ error: error.message,
86
+ originalIndex,
87
+ originalStartLine
88
+ };
89
+ }
90
+ }
91
+
92
+
93
+
94
+ /**
95
+ * Handle apply_diffs request (supports both single diff and batch diffs)
96
+ * @param {Object} args - Request parameters
97
+ * @returns {Promise<Object>} Response result
98
+ */
99
+ static async handle(args) {
100
+ const { path, search_content, replace_content, start_line, atomic = true, trim = false } = args;
101
+
102
+ // Check if file exists
103
+ if (!FileUtils.fileExists(path)) {
104
+ throw new Error(`File not found: ${path}`);
105
+ }
106
+
107
+ // Normalize all parameters to arrays
108
+ let searchArray, replaceArray, startLineArray;
109
+
110
+ if (Array.isArray(search_content)) {
111
+ searchArray = search_content;
112
+ replaceArray = Array.isArray(replace_content) ? replace_content : [replace_content];
113
+ startLineArray = Array.isArray(start_line) ? start_line : [start_line];
114
+ } else {
115
+ searchArray = [search_content];
116
+ replaceArray = Array.isArray(replace_content) ? replace_content : [replace_content];
117
+ startLineArray = Array.isArray(start_line) ? start_line : [start_line];
118
+ }
119
+
120
+ // Validate array lengths match
121
+ if (searchArray.length !== replaceArray.length || searchArray.length !== startLineArray.length) {
122
+ throw new Error("Array lengths for search_content, replace_content, and start_line must match");
123
+ }
124
+
125
+ const diffCount = searchArray.length;
126
+
127
+ // Validate all start_line values
128
+ for (let i = 0; i < diffCount; i++) {
129
+ if (startLineArray[i] < 1) {
130
+ throw new Error(`Invalid start_line: ${startLineArray[i]} at index ${i}. Line numbers start from 1.`);
131
+ }
132
+ }
133
+
134
+ // Create diff objects and sort by start_line
135
+ const diffs = searchArray.map((searchContent, index) => ({
136
+ search_content: searchContent,
137
+ replace_content: replaceArray[index],
138
+ start_line: startLineArray[index],
139
+ originalIndex: index
140
+ })).sort((a, b) => a.start_line - b.start_line);
141
+
142
+ // Read file content
143
+ const content = await FileUtils.readFile(path);
144
+ let lines = content.split('\n');
145
+ if (atomic && diffCount > 1) {
146
+ // Atomic mode: validate all diffs first
147
+ let lineOffset = 0;
148
+ const validationErrors = [];
149
+ let tempLines = [...lines]; // Work with a copy for validation
150
+
151
+ for (const diff of diffs) {
152
+ const validation = this.validateDiff(diff, tempLines, lineOffset, trim);
153
+
154
+ if (!validation.success) {
155
+ validationErrors.push({
156
+ index: validation.originalIndex,
157
+ start_line: validation.originalStartLine,
158
+ status: "fail",
159
+ message: validation.error
160
+ });
161
+ break; // Stop validation on first error
162
+ } else {
163
+ // Simulate the replacement to update tempLines and lineOffset
164
+ const replaceLines = diff.replace_content.split('\n');
165
+ const actualStartLine = diff.start_line + lineOffset;
166
+ const endLine = actualStartLine + validation.searchLines.length - 1;
167
+
168
+ const beforeLines = tempLines.slice(0, actualStartLine - 1);
169
+ const afterLines = tempLines.slice(endLine);
170
+ tempLines = [...beforeLines, ...replaceLines, ...afterLines];
171
+
172
+ // Update line offset for next validation
173
+ const lineDiff = replaceLines.length - validation.searchLines.length;
174
+ lineOffset += lineDiff;
175
+ }
176
+ }
177
+
178
+ if (validationErrors.length > 0) {
179
+ // In atomic mode, if any diff fails, abort the entire operation
180
+ const results = new Array(diffCount);
181
+ validationErrors.forEach(error => {
182
+ results[error.index] = error;
183
+ });
184
+
185
+ // Fill in success placeholders for non-failed diffs
186
+ for (let i = 0; i < diffCount; i++) {
187
+ if (!results[i]) {
188
+ results[i] = {
189
+ index: i,
190
+ start_line: startLineArray[i],
191
+ status: "aborted",
192
+ message: "Operation aborted due to validation failures in atomic mode"
193
+ };
194
+ }
195
+ }
196
+
197
+ // Create detailed error message with formatted content
198
+ let detailedMessage = `Atomic operation failed: ${validationErrors.length}/${diffCount} diffs would fail. No changes applied.\n\n`;
199
+ detailedMessage += "Detailed results:\n";
200
+ results.forEach((result, index) => {
201
+ detailedMessage += `\nDiff ${index + 1}:\n`;
202
+ detailedMessage += ` Status: ${result.status}\n`;
203
+ detailedMessage += ` Start Line: ${result.start_line}\n`;
204
+ detailedMessage += ` Message: ${result.message}\n`;
205
+ });
206
+
207
+ throw new Error(detailedMessage);
208
+ }
209
+ }
210
+
211
+ // Track results for each diff
212
+ const results = new Array(diffCount);
213
+ let lineOffset = 0; // Track cumulative line offset
214
+ let appliedCount = 0;
215
+
216
+ // Process each diff in order
217
+ for (const diff of diffs) {
218
+ const { search_content: searchContent, replace_content: replaceContent, start_line: originalStartLine, originalIndex } = diff;
219
+
220
+ if (!atomic || diffCount === 1) {
221
+ // Non-atomic mode or single diff: validate and apply one by one
222
+ const validation = this.validateDiff(diff, lines, lineOffset, trim);
223
+
224
+ if (!validation.success) {
225
+ results[originalIndex] = {
226
+ index: originalIndex,
227
+ start_line: originalStartLine,
228
+ status: "fail",
229
+ message: validation.error
230
+ };
231
+ continue;
232
+ }
233
+ }
234
+
235
+ // Apply the diff
236
+ const actualStartLine = originalStartLine + lineOffset;
237
+ const searchLines = searchContent.split('\n');
238
+ const replaceLines = replaceContent.split('\n');
239
+ const endLine = actualStartLine + searchLines.length - 1;
240
+
241
+ // Perform replacement
242
+ const beforeLines = lines.slice(0, actualStartLine - 1);
243
+ const afterLines = lines.slice(endLine);
244
+ lines = [...beforeLines, ...replaceLines, ...afterLines];
245
+
246
+ // Update line offset for subsequent diffs
247
+ const lineDiff = replaceLines.length - searchLines.length;
248
+ lineOffset += lineDiff;
249
+ appliedCount++;
250
+
251
+ // Record success
252
+ results[originalIndex] = {
253
+ index: originalIndex,
254
+ start_line: originalStartLine,
255
+ status: "success",
256
+ message: `Replaced ${searchLines.length} line(s) at line ${originalStartLine}${lineDiff !== 0 ? ` (${lineDiff > 0 ? 'added' : 'removed'} ${Math.abs(lineDiff)} line(s))` : ''}`
257
+ };
258
+ }
259
+
260
+ // Write back to file
261
+ const newContent = lines.join('\n');
262
+ await FileUtils.writeFile(path, newContent);
263
+
264
+ // Build response
265
+ const failedCount = diffCount - appliedCount;
266
+ const isBatchMode = diffCount > 1;
267
+
268
+ let message;
269
+ if (isBatchMode) {
270
+ const mode = atomic ? "atomic" : "non-atomic";
271
+ message = `Batch diff operation (${mode}) completed: ${appliedCount}/${diffCount} diffs applied successfully to ${path}`;
272
+ if (failedCount > 0) {
273
+ message += ` (${failedCount} failed)`;
274
+ }
275
+ message += `. File now has ${lines.length} lines.\n\n`;
276
+
277
+ // Add detailed results for batch mode
278
+ message += "Detailed results:\n";
279
+ results.forEach((result, index) => {
280
+ message += `\nDiff ${index + 1}:\n`;
281
+ message += ` Status: ${result.status}\n`;
282
+ message += ` Start Line: ${result.start_line}\n`;
283
+ message += ` Message: ${result.message}\n`;
284
+ });
285
+ } else {
286
+ // Single mode - maintain original message format
287
+ const result = results[0];
288
+ if (result.status === "success") {
289
+ message = `Successfully applied diff to ${path}: ${result.message}. File now has ${lines.length} lines.`;
290
+ } else {
291
+ throw new Error(`Single diff failed:\n\n${result.message}`);
292
+ }
293
+ }
294
+
295
+ // Create success response using FileUtils
296
+ return FileUtils.createResponse(message);
297
+ }
298
+ }