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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/docs/MCP-INTERFACE.cn.md +976 -0
- package/docs/MCP-INTERFACE.en.md +881 -0
- package/docs/README.cn.md +128 -0
- package/package.json +27 -0
- package/src/handlers/applyDiff.js +298 -0
- package/src/handlers/insertContent.js +179 -0
- package/src/handlers/listFiles.js +65 -0
- package/src/handlers/readFile.js +109 -0
- package/src/handlers/searchAndReplace.js +198 -0
- package/src/handlers/setWorkspace.js +26 -0
- package/src/handlers/writeFile.js +98 -0
- package/src/index.js +20 -0
- package/src/server.js +85 -0
- package/src/tools/toolDefinitions.js +291 -0
- package/src/utils/fileUtils.js +238 -0
- package/test/ApplyDiffHandler.test.js +754 -0
- package/test/InsertContentHandler.test.js +371 -0
- package/test/ListFilesHandler.test.js +302 -0
- package/test/ReadFileHandler.test.js +213 -0
- package/test/SearchAndReplaceHandler.test.js +505 -0
- package/test/SetWorkspaceHandler.test.js +290 -0
- package/test/WriteFileHandler.test.js +289 -0
- package/test/runAllTests.js +233 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Tool definitions and Schema configuration
|
|
2
|
+
export const toolDefinitions = [
|
|
3
|
+
{
|
|
4
|
+
name: "set_workspace",
|
|
5
|
+
description: "Set the workspace root directory. All relative path operations will be based on this directory. You must call this tool to set the correct workspace before performing any file operations.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
path: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "The absolute path to the workspace root directory (e.g., the directory opened by VSCode)"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
required: ["path"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "read_files",
|
|
19
|
+
description: "Read the entire or partial content of the specified file(s). Supports reading single or multiple files at once. The returned content includes line numbers in the format 'line | content'.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
path: {
|
|
24
|
+
oneOf: [
|
|
25
|
+
{
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "The path of the single file to read. The path can be an absolute path or a workspace-relative path."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
type: "string"
|
|
33
|
+
},
|
|
34
|
+
description: "An array of file paths to read. Each path can be an absolute path or a workspace-relative path."
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
line_range: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Optional line range, format 'start-end' (only for single file)",
|
|
41
|
+
pattern: "^\\d+-\\d+$"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: ["path"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "write_files",
|
|
49
|
+
description: "Create new files or completely overwrite existing files. Supports single or multiple files at once.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
path: {
|
|
54
|
+
oneOf: [
|
|
55
|
+
{
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "The path of the single target file. The path can be an absolute path or a workspace-relative path."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: "array",
|
|
61
|
+
items: {
|
|
62
|
+
type: "string"
|
|
63
|
+
},
|
|
64
|
+
description: "An array of target file paths. Each path can be an absolute path or a workspace-relative path."
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
content: {
|
|
69
|
+
oneOf: [
|
|
70
|
+
{
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "The full content to write, for single file or same content for all files"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: "array",
|
|
76
|
+
items: {
|
|
77
|
+
type: "string"
|
|
78
|
+
},
|
|
79
|
+
description: "An array of content for each file, for different content per file"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
line_count: {
|
|
84
|
+
oneOf: [
|
|
85
|
+
{
|
|
86
|
+
type: "integer",
|
|
87
|
+
description: "Total number of lines in the file, for single file or same for all files",
|
|
88
|
+
minimum: 0
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: "array",
|
|
92
|
+
items: {
|
|
93
|
+
type: "integer",
|
|
94
|
+
minimum: 0
|
|
95
|
+
},
|
|
96
|
+
description: "An array of line counts for each file, for different line counts per file"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
required: ["path", "content", "line_count"]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "list_files",
|
|
106
|
+
description: "List the files and subdirectories in the specified directory.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
path: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "The directory path to list contents of. The path can be an absolute path or a workspace-relative path."
|
|
113
|
+
},
|
|
114
|
+
recursive: {
|
|
115
|
+
type: "boolean",
|
|
116
|
+
description: "Whether to recursively list subdirectory contents",
|
|
117
|
+
default: false
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
required: ["path"]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "insert_contents",
|
|
125
|
+
description: "Insert new content at the specified position in the file. Supports editing single or multiple files at once. Negative line numbers are supported for insertion from the end.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
path: {
|
|
130
|
+
oneOf: [
|
|
131
|
+
{
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "The path of the single target file. The path can be an absolute path or a workspace-relative path."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: "array",
|
|
137
|
+
items: {
|
|
138
|
+
type: "string"
|
|
139
|
+
},
|
|
140
|
+
description: "An array of target file paths. Each path can be an absolute path or a workspace-relative path."
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
line: {
|
|
145
|
+
oneOf: [
|
|
146
|
+
{
|
|
147
|
+
type: "integer",
|
|
148
|
+
description: "The line number to insert (positive: 1-based, 0: end of file, negative: from end, -1 before last line), for single file"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: "array",
|
|
152
|
+
items: {
|
|
153
|
+
type: "integer"
|
|
154
|
+
},
|
|
155
|
+
description: "An array of line numbers for each file (positive: insert, 0: end, negative: from end), for multiple files"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
content: {
|
|
160
|
+
oneOf: [
|
|
161
|
+
{
|
|
162
|
+
type: "string",
|
|
163
|
+
description: "The content to insert, for single file or same content for all files"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: "array",
|
|
167
|
+
items: {
|
|
168
|
+
type: "string"
|
|
169
|
+
},
|
|
170
|
+
description: "An array of content for each file, for different content per file"
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
required: ["path", "line", "content"]
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "apply_diffs",
|
|
180
|
+
description: "Perform precise block-based search and replace operations on existing files. Supports single or multiple diff operations on a single file. When processing multiple diffs, the tool automatically handles line number offsets - all start_line values should be based on the original file structure. By default, operates in atomic mode for safe batch operations.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
path: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "The path of the file to modify. The path can be an absolute path or a workspace-relative path."
|
|
187
|
+
},
|
|
188
|
+
search_content: {
|
|
189
|
+
oneOf: [
|
|
190
|
+
{
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "The original content to match precisely (for single diff)"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
type: "array",
|
|
196
|
+
items: {
|
|
197
|
+
type: "string"
|
|
198
|
+
},
|
|
199
|
+
description: "Array of original content to match precisely (for multiple diffs)"
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
replace_content: {
|
|
204
|
+
oneOf: [
|
|
205
|
+
{
|
|
206
|
+
type: "string",
|
|
207
|
+
description: "The new content to replace with (for single diff)"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: "array",
|
|
211
|
+
items: {
|
|
212
|
+
type: "string"
|
|
213
|
+
},
|
|
214
|
+
description: "Array of new content to replace with (for multiple diffs)"
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
start_line: {
|
|
219
|
+
oneOf: [
|
|
220
|
+
{
|
|
221
|
+
type: "integer",
|
|
222
|
+
description: "The starting line number for searching content (for single diff, 1-based line number from original file)",
|
|
223
|
+
minimum: 1
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: "array",
|
|
227
|
+
items: {
|
|
228
|
+
type: "integer",
|
|
229
|
+
minimum: 1
|
|
230
|
+
},
|
|
231
|
+
description: "Array of starting line numbers for searching content (for multiple diffs, all 1-based line numbers from original file - tool automatically handles line offset adjustments during processing)"
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
atomic: {
|
|
236
|
+
type: "boolean",
|
|
237
|
+
description: "Whether to use atomic mode (all-or-nothing). When true (default), validates all diffs before applying any. When false, applies diffs one by one, continuing on failures.",
|
|
238
|
+
default: true
|
|
239
|
+
},
|
|
240
|
+
trim: {
|
|
241
|
+
type: "boolean",
|
|
242
|
+
description: "Whether to trim whitespace from the beginning and end of each line when comparing search_content with file content. Only affects search and matching - replace_content is inserted exactly as provided. Default is false.",
|
|
243
|
+
default: false
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
required: ["path", "search_content", "replace_content", "start_line"]
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "search_and_replace",
|
|
251
|
+
description: "Search and replace text or regular expressions in a single file.",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
path: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "The target file path. The path can be an absolute path or a workspace-relative path."
|
|
258
|
+
},
|
|
259
|
+
search: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "The text or regular expression to search for"
|
|
262
|
+
},
|
|
263
|
+
replace: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "The text to replace with"
|
|
266
|
+
},
|
|
267
|
+
use_regex: {
|
|
268
|
+
type: "boolean",
|
|
269
|
+
description: "Whether to use regular expressions for searching",
|
|
270
|
+
default: false
|
|
271
|
+
},
|
|
272
|
+
ignore_case: {
|
|
273
|
+
type: "boolean",
|
|
274
|
+
description: "Whether to ignore case when searching",
|
|
275
|
+
default: false
|
|
276
|
+
},
|
|
277
|
+
start_line: {
|
|
278
|
+
type: "integer",
|
|
279
|
+
description: "The starting line of the search range",
|
|
280
|
+
minimum: 1
|
|
281
|
+
},
|
|
282
|
+
end_line: {
|
|
283
|
+
type: "integer",
|
|
284
|
+
description: "The ending line of the search range",
|
|
285
|
+
minimum: 1
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
required: ["path", "search", "replace"]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
];
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { resolve, dirname, isAbsolute, join } from 'path';
|
|
3
|
+
import { existsSync, lstatSync } from 'fs';
|
|
4
|
+
import { cwd } from 'process';
|
|
5
|
+
import pathIsInside from 'path-is-inside';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* General file operation utility class
|
|
9
|
+
*/
|
|
10
|
+
export class FileUtils {
|
|
11
|
+
|
|
12
|
+
// Current workspace root directory, initially null, must be set via set_workspace
|
|
13
|
+
static WORKSPACE_ROOT = null; /**
|
|
14
|
+
* Set workspace root directory
|
|
15
|
+
* @param {string} workspaceRoot - New workspace root directory path
|
|
16
|
+
* @throws {Error} If directory does not exist or is not accessible
|
|
17
|
+
*/
|
|
18
|
+
static setWorkspaceRoot(workspaceRoot) {
|
|
19
|
+
// Input validation
|
|
20
|
+
if (!workspaceRoot || typeof workspaceRoot !== 'string') {
|
|
21
|
+
throw new Error('Invalid workspace root: must be a non-empty string');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Prevent null bytes and other dangerous characters
|
|
25
|
+
if (workspaceRoot.includes('\0') || workspaceRoot.includes('\x00')) {
|
|
26
|
+
throw new Error('Invalid workspace root: contains null bytes');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resolvedPath = resolve(workspaceRoot);
|
|
30
|
+
|
|
31
|
+
if (!existsSync(resolvedPath)) {
|
|
32
|
+
throw new Error(`Workspace directory does not exist: ${workspaceRoot}`);
|
|
33
|
+
} // Verify it is a directory, not a file
|
|
34
|
+
try {
|
|
35
|
+
const stats = lstatSync(resolvedPath);
|
|
36
|
+
if (!stats.isDirectory()) {
|
|
37
|
+
throw new Error(`Workspace path is not a directory: ${workspaceRoot}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code !== 'ENOENT') {
|
|
41
|
+
throw new Error(`Cannot access workspace directory: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
FileUtils.WORKSPACE_ROOT = resolvedPath;
|
|
46
|
+
console.error(`Workspace root set to: ${FileUtils.WORKSPACE_ROOT}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current workspace root directory
|
|
51
|
+
* @returns {string} Current workspace root directory
|
|
52
|
+
*/
|
|
53
|
+
static getWorkspaceRoot() {
|
|
54
|
+
return FileUtils.WORKSPACE_ROOT;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Securely resolve file path, ensuring it does not go outside the workspace
|
|
59
|
+
* Uses a mature third-party library to prevent path traversal attacks
|
|
60
|
+
* @param {string} filePath - Input file path
|
|
61
|
+
* @returns {string} Secure absolute path
|
|
62
|
+
* @throws {Error} If path tries to access files outside the workspace or workspace is not set
|
|
63
|
+
*/
|
|
64
|
+
static getSecurePath(filePath) {
|
|
65
|
+
// First check if workspace is set
|
|
66
|
+
if (!FileUtils.WORKSPACE_ROOT) {
|
|
67
|
+
throw new Error('Workspace not set. Please call set_workspace first to establish a secure workspace root directory.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Input validation
|
|
71
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
72
|
+
throw new Error('Invalid file path: must be a non-empty string');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Prevent null bytes and other dangerous characters
|
|
76
|
+
if (filePath.includes('\0') || filePath.includes('\x00')) {
|
|
77
|
+
throw new Error('Invalid file path: contains null bytes');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let targetPath;
|
|
81
|
+
|
|
82
|
+
if (isAbsolute(filePath)) {
|
|
83
|
+
// Absolute path: use directly
|
|
84
|
+
targetPath = filePath;
|
|
85
|
+
} else {
|
|
86
|
+
// Relative path: join to workspace root
|
|
87
|
+
targetPath = join(FileUtils.WORKSPACE_ROOT, filePath);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve to canonical absolute path, handle symlinks
|
|
91
|
+
let resolvedPath;
|
|
92
|
+
try {
|
|
93
|
+
resolvedPath = resolve(targetPath);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new Error(`Failed to resolve path '${filePath}': ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Use mature third-party library to check if path is inside workspace
|
|
99
|
+
if (!pathIsInside(resolvedPath, FileUtils.WORKSPACE_ROOT) && resolvedPath !== FileUtils.WORKSPACE_ROOT) {
|
|
100
|
+
throw new Error(`Access denied: Path is outside the workspace boundary`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return resolvedPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if file exists
|
|
108
|
+
* @param {string} filePath - File path
|
|
109
|
+
* @returns {boolean} Whether file exists
|
|
110
|
+
*/
|
|
111
|
+
static fileExists(filePath) {
|
|
112
|
+
const securePath = FileUtils.getSecurePath(filePath);
|
|
113
|
+
return existsSync(securePath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read file content
|
|
118
|
+
* @param {string} filePath - File path
|
|
119
|
+
* @returns {Promise<string>} File content
|
|
120
|
+
*/
|
|
121
|
+
static async readFile(filePath) {
|
|
122
|
+
const securePath = FileUtils.getSecurePath(filePath);
|
|
123
|
+
if (!existsSync(securePath)) {
|
|
124
|
+
throw new Error(`File not found: ${securePath}`);
|
|
125
|
+
}
|
|
126
|
+
return await fs.readFile(securePath, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Write file content
|
|
131
|
+
* @param {string} filePath - File path
|
|
132
|
+
* @param {string} content - File content
|
|
133
|
+
*/
|
|
134
|
+
static async writeFile(filePath, content) {
|
|
135
|
+
const securePath = FileUtils.getSecurePath(filePath);
|
|
136
|
+
const dir = dirname(securePath);
|
|
137
|
+
|
|
138
|
+
// Ensure directory exists
|
|
139
|
+
await fs.mkdir(dir, { recursive: true });
|
|
140
|
+
await fs.writeFile(securePath, content, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create MCP standard response format
|
|
145
|
+
* @param {string} text - Response text
|
|
146
|
+
* @returns {Object} MCP response format
|
|
147
|
+
*/
|
|
148
|
+
static createResponse(text) {
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: text
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create MCP error response format
|
|
161
|
+
* @param {string} errorMessage - Error message
|
|
162
|
+
* @returns {Object} MCP error response format
|
|
163
|
+
*/
|
|
164
|
+
static createErrorResponse(errorMessage) {
|
|
165
|
+
return {
|
|
166
|
+
isError: true,
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: `Error: ${errorMessage}`
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Count number of lines in a string
|
|
178
|
+
* @param {string} content - String content
|
|
179
|
+
* @returns {number} Number of lines
|
|
180
|
+
*/
|
|
181
|
+
static countLines(content) {
|
|
182
|
+
return content.split('\n').length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get specified line range from file
|
|
187
|
+
* @param {string} content - File content
|
|
188
|
+
* @param {string} lineRange - Line range, e.g. "1-10"
|
|
189
|
+
* @returns {string} Content of specified range
|
|
190
|
+
*/ static getLineRange(content, lineRange) {
|
|
191
|
+
// Validate line range format
|
|
192
|
+
if (!lineRange || !lineRange.includes('-')) {
|
|
193
|
+
throw new Error(`Invalid line range format: ${lineRange}. Expected format: 'start-end'`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const [start, end] = lineRange.split('-').map(Number);
|
|
197
|
+
|
|
198
|
+
// Validate line numbers are valid numbers
|
|
199
|
+
if (isNaN(start) || isNaN(end)) {
|
|
200
|
+
throw new Error(`Invalid line range: ${lineRange}. Start and end must be valid numbers`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Validate line numbers are positive
|
|
204
|
+
if (start < 1 || end < 1) {
|
|
205
|
+
throw new Error(`Invalid line range: ${lineRange}. Line numbers must be positive`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Validate start line cannot be greater than end line
|
|
209
|
+
if (start > end) {
|
|
210
|
+
throw new Error(`Invalid line range: ${lineRange}. Start line cannot be greater than end line`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const lines = content.split('\n');
|
|
214
|
+
|
|
215
|
+
// Validate line numbers do not exceed file range
|
|
216
|
+
if (start > lines.length) {
|
|
217
|
+
throw new Error(`Line range ${lineRange} exceeds file length (${lines.length} lines)`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const selectedLines = lines.slice(start - 1, end);
|
|
221
|
+
return selectedLines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Format content with line numbers
|
|
226
|
+
* @param {string} content - Original content
|
|
227
|
+
* @param {number} startLine - Starting line number, default is 1
|
|
228
|
+
* @returns {string} Formatted content with line numbers
|
|
229
|
+
*/
|
|
230
|
+
static formatWithLineNumbers(content, startLine = 1) {
|
|
231
|
+
const lines = content.split('\n');
|
|
232
|
+
const formattedLines = lines.map((line, index) => {
|
|
233
|
+
const lineNumber = startLine + index;
|
|
234
|
+
return `${lineNumber} | ${line}`;
|
|
235
|
+
});
|
|
236
|
+
return formattedLines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
}
|