fileditor-mcp 1.0.2 → 1.0.4
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/1.exe +0 -0
- package/1.json +12 -0
- package/LICENSE +21 -21
- package/README.md +128 -128
- package/docs/MCP-INTERFACE.cn.md +975 -1061
- package/docs/MCP-INTERFACE.en.md +881 -974
- package/docs/README.cn.md +128 -128
- package/package.json +30 -31
- package/src/handlers/applyDiff.js +298 -324
- package/src/handlers/insertContent.js +179 -153
- package/src/handlers/listFiles.js +65 -54
- package/src/handlers/readFile.js +109 -109
- package/src/handlers/searchAndReplace.js +198 -217
- package/src/handlers/setWorkspace.js +26 -26
- package/src/handlers/writeFile.js +97 -91
- package/src/index.js +22 -22
- package/src/server.js +85 -85
- package/src/tools/toolDefinitions.js +291 -301
- package/src/utils/fileUtils.js +238 -716
- package/test/ApplyDiffHandler.test.js +754 -754
- package/test/InsertContentHandler.test.js +371 -369
- package/test/ListFilesHandler.test.js +302 -300
- package/test/ReadFileHandler.test.js +213 -213
- package/test/SearchAndReplaceHandler.test.js +505 -505
- package/test/SetWorkspaceHandler.test.js +290 -290
- package/test/WriteFileHandler.test.js +289 -289
- package/test/runAllTests.js +233 -233
|
@@ -1,153 +1,179 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
* @param {string}
|
|
35
|
-
* @
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
positionDesc =
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { FileUtils } from '../utils/fileUtils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* insert_content tool handler
|
|
7
|
+
*/
|
|
8
|
+
export class InsertContentHandler {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handle insert_content request
|
|
12
|
+
* @param {Object} args - Request parameters
|
|
13
|
+
* @returns {Promise<Object>} Response result
|
|
14
|
+
*/
|
|
15
|
+
static async handle(args) {
|
|
16
|
+
const { path, line, content } = args;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Check if single file or multiple files
|
|
20
|
+
if (Array.isArray(path)) {
|
|
21
|
+
// Insert into multiple files
|
|
22
|
+
return await InsertContentHandler.insertContentMultipleFiles(path, line, content);
|
|
23
|
+
} else {
|
|
24
|
+
// Insert into single file
|
|
25
|
+
return await InsertContentHandler.insertContentSingleFile(path, line, content);
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`Failed to insert content: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Insert content into a single file
|
|
34
|
+
* @param {string} filePath - File path
|
|
35
|
+
* @param {number} line - Line number
|
|
36
|
+
* @param {string} content - Content
|
|
37
|
+
* @returns {Promise<Object>} Response result
|
|
38
|
+
*/
|
|
39
|
+
static async insertContentSingleFile(filePath, line, content) {
|
|
40
|
+
const fullPath = FileUtils.getSecurePath(filePath);
|
|
41
|
+
|
|
42
|
+
// Check if file exists
|
|
43
|
+
if (!FileUtils.fileExists(filePath)) {
|
|
44
|
+
throw new Error(`File not found: ${filePath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read existing content
|
|
48
|
+
const existingContent = await FileUtils.readFile(filePath);
|
|
49
|
+
const lines = existingContent.split('\n');
|
|
50
|
+
|
|
51
|
+
// Handle insert position
|
|
52
|
+
let insertPosition;
|
|
53
|
+
if (line === 0) {
|
|
54
|
+
// 0 means end of file
|
|
55
|
+
insertPosition = lines.length;
|
|
56
|
+
} else if (line > 0) {
|
|
57
|
+
// Positive: 1-based index
|
|
58
|
+
insertPosition = line - 1; // Convert to 0-based index
|
|
59
|
+
|
|
60
|
+
// Check if insert position is valid
|
|
61
|
+
if (insertPosition > lines.length) {
|
|
62
|
+
throw new Error(`Line number ${line} exceeds file length (${lines.length} lines)`);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Negative: calculate insert position from end
|
|
66
|
+
// -1 means before last line, -2 means before second last line, etc.
|
|
67
|
+
insertPosition = lines.length + line;
|
|
68
|
+
|
|
69
|
+
if (insertPosition < 0) {
|
|
70
|
+
throw new Error(`Negative line number ${line} is out of range for file with ${lines.length} lines`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle content to insert (may contain multiple lines)
|
|
75
|
+
const newLines = content.split('\n');
|
|
76
|
+
|
|
77
|
+
// Insert new content (both negative and positive are insert operations)
|
|
78
|
+
lines.splice(insertPosition, 0, ...newLines);
|
|
79
|
+
|
|
80
|
+
// Write back to file
|
|
81
|
+
await fs.writeFile(fullPath, lines.join('\n'), 'utf8');
|
|
82
|
+
|
|
83
|
+
// Build response message
|
|
84
|
+
let positionDesc;
|
|
85
|
+
if (line === 0) {
|
|
86
|
+
positionDesc = 'end of file';
|
|
87
|
+
} else if (line > 0) {
|
|
88
|
+
positionDesc = `line ${line}`;
|
|
89
|
+
} else {
|
|
90
|
+
positionDesc = `line ${Math.abs(line)} from end (before current line ${insertPosition + 1})`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const message = `Successfully inserted ${newLines.length} line(s) at ${positionDesc} in ${filePath}. File now has ${lines.length} lines.`;
|
|
94
|
+
return FileUtils.createResponse(message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Insert content into multiple files
|
|
99
|
+
* @param {string[]} filePaths - Array of file paths
|
|
100
|
+
* @param {number|number[]} lines - Line number or array of line numbers
|
|
101
|
+
* @param {string|string[]} contents - Content or array of contents
|
|
102
|
+
* @returns {Promise<Object>} Response result
|
|
103
|
+
*/
|
|
104
|
+
static async insertContentMultipleFiles(filePaths, lines, contents) {
|
|
105
|
+
const results = [];
|
|
106
|
+
const errors = [];
|
|
107
|
+
|
|
108
|
+
// Parameter validation
|
|
109
|
+
const fileCount = filePaths.length;
|
|
110
|
+
|
|
111
|
+
// Handle line parameter (can be a single number or array)
|
|
112
|
+
let lineArray;
|
|
113
|
+
if (Array.isArray(lines)) {
|
|
114
|
+
if (lines.length !== fileCount) {
|
|
115
|
+
throw new Error(`Line array length (${lines.length}) must match file count (${fileCount})`);
|
|
116
|
+
}
|
|
117
|
+
lineArray = lines;
|
|
118
|
+
} else {
|
|
119
|
+
// If single number, apply to all files
|
|
120
|
+
lineArray = new Array(fileCount).fill(lines);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle content parameter (can be a single string or array)
|
|
124
|
+
let contentArray;
|
|
125
|
+
if (Array.isArray(contents)) {
|
|
126
|
+
if (contents.length !== fileCount) {
|
|
127
|
+
throw new Error(`Content array length (${contents.length}) must match file count (${fileCount})`);
|
|
128
|
+
}
|
|
129
|
+
contentArray = contents;
|
|
130
|
+
} else {
|
|
131
|
+
// If single string, apply to all files
|
|
132
|
+
contentArray = new Array(fileCount).fill(contents);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Perform insert operation for each file
|
|
136
|
+
for (let i = 0; i < fileCount; i++) {
|
|
137
|
+
const filePath = filePaths[i];
|
|
138
|
+
const line = lineArray[i];
|
|
139
|
+
const content = contentArray[i];
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const result = await InsertContentHandler.insertContentSingleFile(filePath, line, content);
|
|
143
|
+
results.push({
|
|
144
|
+
file: filePath,
|
|
145
|
+
success: true,
|
|
146
|
+
message: result.content[0].text
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
errors.push({
|
|
150
|
+
file: filePath,
|
|
151
|
+
error: error.message
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Build return message
|
|
157
|
+
let responseText = '';
|
|
158
|
+
|
|
159
|
+
if (results.length > 0) {
|
|
160
|
+
responseText += `Successfully processed ${results.length} file(s):\n\n`;
|
|
161
|
+
for (const result of results) {
|
|
162
|
+
responseText += `✅ ${result.file}: ${result.message}\n`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (errors.length > 0) {
|
|
167
|
+
responseText += `\nErrors encountered:\n`;
|
|
168
|
+
for (const error of errors) {
|
|
169
|
+
responseText += `❌ ${error.file}: ${error.error}\n`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (results.length === 0 && errors.length > 0) {
|
|
174
|
+
throw new Error(`Failed to insert content in any files: ${errors.map(e => e.error).join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return FileUtils.createResponse(responseText.trim());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -1,54 +1,65 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { FileUtils } from '../utils/fileUtils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* list_files tool handler
|
|
8
|
+
*/
|
|
9
|
+
export class ListFilesHandler {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle list_files request
|
|
13
|
+
* @param {Object} args - Request parameters
|
|
14
|
+
* @returns {Promise<Object>} Response result
|
|
15
|
+
*/
|
|
16
|
+
static async handle(args) {
|
|
17
|
+
const { path, recursive = false } = args;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const fullPath = FileUtils.getSecurePath(path);
|
|
21
|
+
|
|
22
|
+
if (!existsSync(fullPath)) {
|
|
23
|
+
throw new Error(`Directory not found: ${path}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = [];
|
|
27
|
+
|
|
28
|
+
if (recursive) {
|
|
29
|
+
await ListFilesHandler.listFilesRecursive(fullPath, result, '');
|
|
30
|
+
} else {
|
|
31
|
+
const items = await fs.readdir(fullPath, { withFileTypes: true });
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const type = item.isDirectory() ? 'directory' : 'file';
|
|
34
|
+
result.push(`${type}: ${item.name}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return FileUtils.createResponse(result.join('\n'));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`Failed to list files: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Recursively list files
|
|
46
|
+
* @param {string} dirPath - Directory path
|
|
47
|
+
* @param {string[]} result - Result array
|
|
48
|
+
* @param {string} prefix - Path prefix
|
|
49
|
+
*/
|
|
50
|
+
static async listFilesRecursive(dirPath, result, prefix) {
|
|
51
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
52
|
+
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const itemPath = join(dirPath, item.name);
|
|
55
|
+
const displayPath = prefix ? `${prefix}/${item.name}` : item.name;
|
|
56
|
+
|
|
57
|
+
if (item.isDirectory()) {
|
|
58
|
+
result.push(`directory: ${displayPath}`);
|
|
59
|
+
await ListFilesHandler.listFilesRecursive(itemPath, result, displayPath);
|
|
60
|
+
} else {
|
|
61
|
+
result.push(`file: ${displayPath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|