fileditor-mcp 1.0.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.
@@ -0,0 +1,128 @@
1
+ # FileEditor MCP Server
2
+
3
+ [📖 English Interface Doc](./MCP-INTERFACE.en.md) | [📖 中文接口文档](./MCP-INTERFACE.cn.md) | [📋 English README](../README.md)
4
+
5
+ **专为 AI 编程助手优化的文件操作服务器** - 基于 Model Context Protocol 构建,针对代码编辑、重构、批量修改等AI编程场景深度优化,提供高精度、高效率的文件系统操作能力。
6
+
7
+ ## 🎯 设计理念
8
+
9
+ **为 AI 编程而生** - 本项目专门针对 AI 模型的代码编辑需求进行设计,提供精确的块级操作、智能匹配算法和批量处理能力,让 AI 能够安全、高效地执行复杂的代码修改任务。
10
+
11
+ ## 🚀 核心特性
12
+
13
+ ### 📁 专业文件操作工具 (7个)
14
+ - **`set_workspace`** - 安全工作区管理 (强制隔离,防止误操作)
15
+ - **`read_files`** - 智能批量读取 (支持多文件、行范围、带行号定位)
16
+ - **`write_files`** - 高效文件创建 (批量写入、原子性保证)
17
+ - **`list_files`** - 完整目录遍历 (递归扫描、结构化输出)
18
+ - **`insert_contents`** - 精确内容插入 (多点插入、负索引、末尾追加)
19
+ - **`apply_diffs`** - **AI友好的差异应用** (智能空格处理、批量原子操作、容错匹配)
20
+ - **`search_and_replace`** - 强大模式替换 (正则支持、范围限定、大小写控制)
21
+
22
+ ### 🛡️ 企业级安全保障
23
+ - **工作区强隔离**: 严格边界控制,防止目录穿透和文件泄露
24
+ - **路径智能解析**: 自动验证和标准化文件路径
25
+ - **原子事务**: 批量操作要么全部成功,要么完全回滚
26
+
27
+ ### ⚡ AI 优化功能
28
+ - **批量处理引擎**: 单次 API 调用处理大量文件操作
29
+ - **智能匹配算法**: `trim` 模式处理代码格式差异,容错性强
30
+ - **详细操作反馈**: 完整的成功/失败信息,便于 AI 调试和决策
31
+ - **非阻塞错误处理**: 部分失败不影响其他操作继续执行
32
+
33
+ ## 🛠️ 快速开始
34
+
35
+ ```bash
36
+ # 安装依赖 (推荐使用 pnpm)
37
+ pnpm install
38
+
39
+ # 启动生产服务器
40
+ pnpm start
41
+
42
+ # 开发模式 (文件变更自动重启)
43
+ pnpm dev
44
+
45
+ # 运行完整测试套件
46
+ pnpm test
47
+ ```
48
+
49
+ **环境要求**: Node.js ≥18, pnpm 包管理器
50
+
51
+ ## 💡 典型使用场景
52
+
53
+ ```json
54
+ // 场景1: 工作区初始化 (必须首先执行)
55
+ {
56
+ "name": "set_workspace",
57
+ "arguments": { "path": "/path/to/your/project" }
58
+ }
59
+
60
+ // 场景2: 批量代码文件分析
61
+ {
62
+ "name": "read_files",
63
+ "arguments": {
64
+ "path": ["src/main.js", "src/utils.js", "package.json"],
65
+ "line_range": "1-50" // 可选:仅读取前50行
66
+ }
67
+ }
68
+
69
+ // 场景3: 智能代码重构 (容错空格差异)
70
+ {
71
+ "name": "apply_diffs",
72
+ "arguments": {
73
+ "path": "src/config.js",
74
+ "search_content": [
75
+ "const API_URL = 'localhost';",
76
+ "const PORT = 3000;"
77
+ ],
78
+ "replace_content": [
79
+ "const API_URL = process.env.API_URL || 'localhost';",
80
+ "const PORT = process.env.PORT || 3000;"
81
+ ],
82
+ "start_line": [5, 7],
83
+ "atomic": true, // 原子模式:全部成功或全部回滚
84
+ "trim": true // 智能模式:忽略空格差异
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## 🏗️ 架构设计
90
+
91
+ ```
92
+ src/
93
+ ├── index.js # 服务启动入口
94
+ ├── server.js # MCP 协议服务器
95
+ ├── tools/
96
+ │ └── toolDefinitions.js # 工具定义和 JSON Schema
97
+ ├── handlers/ # 核心处理器 (7个)
98
+ │ ├── applyDiff.js # 差异应用 (支持批量+原子+智能匹配)
99
+ │ ├── readFile.js # 文件读取器
100
+ │ ├── writeFile.js # 文件写入器
101
+ │ ├── listFiles.js # 目录扫描器
102
+ │ ├── insertContent.js # 内容插入器
103
+ │ ├── searchAndReplace.js # 模式替换器
104
+ │ └── setWorkspace.js # 工作区管理器
105
+ └── utils/
106
+ └── fileUtils.js # 通用文件操作库
107
+ ```
108
+
109
+ **架构优势**: 高度模块化、职责明确、易于维护、便于 AI 理解和调用
110
+
111
+ ## 📊 质量保证
112
+
113
+ - ✅ **100% 测试覆盖率** - 7个完整的处理器测试套件
114
+ - ✅ **边界条件验证** - 异常输入和错误状态处理
115
+ - ✅ **批量操作验证** - 原子性和一致性保证
116
+ - ✅ **安全性测试** - 路径注入和权限验证
117
+
118
+ ## 🎉 为什么选择 FileEditor MCP
119
+
120
+ 1. **AI 原生设计** - 专门为 AI 编程助手的工作模式优化
121
+ 2. **高性能批处理** - 减少 API 调用次数,提升处理效率
122
+ 3. **智能容错** - 处理现实代码中的格式和空格差异
123
+ 4. **企业级安全** - 严格的工作区隔离和权限控制
124
+ 5. **完整测试覆盖** - 可靠性和稳定性保证
125
+
126
+ ## 📄 开源协议
127
+
128
+ MIT License - 欢迎贡献和使用
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "fileditor-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for file operations",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node src/index.js",
9
+ "dev": "node --watch src/index.js",
10
+ "test": "node test/runAllTests.js"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "file-operations",
15
+ "server"
16
+ ],
17
+ "author": "lansya",
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^0.5.0",
21
+ "path-is-inside": "^1.0.2"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
27
+ }
@@ -0,0 +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
+
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
+ }
@@ -0,0 +1,179 @@
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
+ }