@saber2pr/ai-agent 0.0.43 → 0.0.45
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/lib/core/agent-graph.js +1 -4
- package/lib/tools/filesystem/index.js +225 -134
- package/lib/tools/ts-lsp/index.js +20 -17
- package/package.json +1 -1
package/lib/core/agent-graph.js
CHANGED
|
@@ -426,12 +426,9 @@ class McpGraphAgent {
|
|
|
426
426
|
3. **禁止空操作**:如果你认为任务已完成或不需要调用工具,请不要输出任何 Action 结构。严禁使用 "None"、"null" 或空字符串作为工具名称。
|
|
427
427
|
|
|
428
428
|
# 🎯 核心指令
|
|
429
|
-
1.
|
|
429
|
+
1. **任务终结判定**:当你已经读取了用户要求的文件、回答了问题或完成了代码编写时,必须立即提供最终回复。
|
|
430
430
|
2. **回复格式**:任务完成时,请以 "Final Answer:" 开头进行总结,此时不再调用任何工具。
|
|
431
431
|
|
|
432
|
-
# 📝 审计任务专项
|
|
433
|
-
1. 避免在同一个文件上陷入无限循环尝试。
|
|
434
|
-
2. 优先通过 \`list_directory\` 了解全局,再深入具体文件。
|
|
435
432
|
{extraPrompt}`;
|
|
436
433
|
// 2. 核心逻辑:处理消息上下文
|
|
437
434
|
let inputMessages;
|
|
@@ -14,40 +14,33 @@ const lib_1 = require("./lib");
|
|
|
14
14
|
const ReadTextFileArgsSchema = zod_1.z.object({
|
|
15
15
|
path: zod_1.z.string(),
|
|
16
16
|
tail: zod_1.z.number().optional().describe('If provided, returns only the last N lines of the file'),
|
|
17
|
-
head: zod_1.z.number().optional().describe('If provided, returns only the first N lines of the file')
|
|
17
|
+
head: zod_1.z.number().optional().describe('If provided, returns only the first N lines of the file'),
|
|
18
18
|
});
|
|
19
19
|
const ReadMultipleFilesArgsSchema = zod_1.z.object({
|
|
20
20
|
paths: zod_1.z
|
|
21
21
|
.array(zod_1.z.string())
|
|
22
|
-
.min(1,
|
|
23
|
-
.describe(
|
|
22
|
+
.min(1, 'At least one file path must be provided')
|
|
23
|
+
.describe('Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.'),
|
|
24
24
|
});
|
|
25
25
|
const WriteFileArgsSchema = zod_1.z.object({
|
|
26
26
|
path: zod_1.z.string(),
|
|
27
27
|
content: zod_1.z.string(),
|
|
28
28
|
});
|
|
29
|
-
const EditOperation = zod_1.z.object({
|
|
30
|
-
oldText: zod_1.z.string().describe('Text to search for - must match exactly'),
|
|
31
|
-
newText: zod_1.z.string().describe('Text to replace with')
|
|
32
|
-
});
|
|
33
|
-
const EditFileArgsSchema = zod_1.z.object({
|
|
34
|
-
path: zod_1.z.string(),
|
|
35
|
-
edits: zod_1.z.array(EditOperation),
|
|
36
|
-
dryRun: zod_1.z.boolean().optional().default(false).describe('Preview changes using git-style diff format')
|
|
37
|
-
});
|
|
38
29
|
const CreateDirectoryArgsSchema = zod_1.z.object({
|
|
39
30
|
path: zod_1.z.string(),
|
|
40
31
|
});
|
|
41
|
-
const ListDirectoryArgsSchema = zod_1.z.object({
|
|
42
|
-
path: zod_1.z.string(),
|
|
43
|
-
});
|
|
44
32
|
const ListDirectoryWithSizesArgsSchema = zod_1.z.object({
|
|
45
33
|
path: zod_1.z.string(),
|
|
46
|
-
sortBy: zod_1.z
|
|
34
|
+
sortBy: zod_1.z
|
|
35
|
+
.enum(['name', 'size'])
|
|
36
|
+
.optional()
|
|
37
|
+
.default('name')
|
|
38
|
+
.describe('Sort entries by name or size'),
|
|
47
39
|
});
|
|
48
40
|
const DirectoryTreeArgsSchema = zod_1.z.object({
|
|
49
41
|
path: zod_1.z.string(),
|
|
50
|
-
excludePatterns: zod_1.z.array(zod_1.z.string()).optional().default([])
|
|
42
|
+
excludePatterns: zod_1.z.array(zod_1.z.string()).optional().default([]),
|
|
43
|
+
depth: zod_1.z.number().optional().default(2).describe('递归深度,默认 2 层。增加深度会消耗更多 Token'),
|
|
51
44
|
});
|
|
52
45
|
const MoveFileArgsSchema = zod_1.z.object({
|
|
53
46
|
source: zod_1.z.string(),
|
|
@@ -56,18 +49,33 @@ const MoveFileArgsSchema = zod_1.z.object({
|
|
|
56
49
|
const SearchFilesArgsSchema = zod_1.z.object({
|
|
57
50
|
path: zod_1.z.string(),
|
|
58
51
|
pattern: zod_1.z.string(),
|
|
59
|
-
excludePatterns: zod_1.z.array(zod_1.z.string()).optional().default([])
|
|
52
|
+
excludePatterns: zod_1.z.array(zod_1.z.string()).optional().default([]),
|
|
60
53
|
});
|
|
61
54
|
const GetFileInfoArgsSchema = zod_1.z.object({
|
|
62
55
|
path: zod_1.z.string(),
|
|
63
56
|
});
|
|
57
|
+
const GrepSearchArgsSchema = zod_1.z.object({
|
|
58
|
+
path: zod_1.z.string().describe('搜索的起始目录路径'),
|
|
59
|
+
query: zod_1.z.string().describe('要搜索的文本关键字'),
|
|
60
|
+
includePattern: zod_1.z.string().optional().default('**/*').describe('匹配模式,例如 "**/*.ts"'),
|
|
61
|
+
maxFiles: zod_1.z.number().optional().default(100).describe('最大扫描文件数,防止大型项目超时'),
|
|
62
|
+
});
|
|
63
|
+
const PatchEditArgsSchema = zod_1.z.object({
|
|
64
|
+
path: zod_1.z.string().describe('文件路径'),
|
|
65
|
+
patches: zod_1.z.array(zod_1.z.object({
|
|
66
|
+
startLine: zod_1.z.number().describe('起始行号(包含)'),
|
|
67
|
+
endLine: zod_1.z.number().describe('结束行号(包含)'),
|
|
68
|
+
replacement: zod_1.z.string().describe('要插入的新代码内容'),
|
|
69
|
+
originalSnippet: zod_1.z.string().optional().describe('可选:该行范围内的原始代码片段,用于二次校验防止行号偏移'),
|
|
70
|
+
})).describe('补丁列表。注意:若有多个补丁,建议从文件尾部向头部执行,或确保行号不重叠'),
|
|
71
|
+
});
|
|
64
72
|
const getFilesystemTools = (targetDir) => {
|
|
65
73
|
(0, lib_1.setAllowedDirectories)([targetDir]);
|
|
66
74
|
// read_file (deprecated) and read_text_file
|
|
67
75
|
const readTextFileHandler = async (args) => {
|
|
68
76
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
69
77
|
if (args.head && args.tail) {
|
|
70
|
-
throw new Error(
|
|
78
|
+
throw new Error('Cannot specify both head and tail parameters simultaneously');
|
|
71
79
|
}
|
|
72
80
|
let content;
|
|
73
81
|
if (args.tail) {
|
|
@@ -82,101 +90,77 @@ const getFilesystemTools = (targetDir) => {
|
|
|
82
90
|
return content;
|
|
83
91
|
};
|
|
84
92
|
const readTextFileTool = (0, createTool_1.createTool)({
|
|
85
|
-
name:
|
|
86
|
-
description:
|
|
87
|
-
"Handles various text encodings and provides detailed error messages " +
|
|
88
|
-
"if the file cannot be read. Use this tool when you need to examine " +
|
|
89
|
-
"the contents of a single file. Use the 'head' parameter to read only " +
|
|
90
|
-
"the first N lines of a file, or the 'tail' parameter to read only " +
|
|
91
|
-
"the last N lines of a file. Operates on the file as text regardless of extension.",
|
|
93
|
+
name: 'read_text_file',
|
|
94
|
+
description: '读取文件全文。若超过100行则禁止使用,必须改用 read_file_range。支持 head/tail 参数。',
|
|
92
95
|
parameters: ReadTextFileArgsSchema,
|
|
93
|
-
handler: readTextFileHandler
|
|
96
|
+
handler: readTextFileHandler,
|
|
94
97
|
});
|
|
95
98
|
const readMultipleFilesTool = (0, createTool_1.createTool)({
|
|
96
|
-
name:
|
|
97
|
-
description:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"path as a reference. Failed reads for individual files won't stop " +
|
|
101
|
-
"the entire operation. Only works within allowed directories.",
|
|
99
|
+
name: 'read_multiple_files',
|
|
100
|
+
description: '同时读取多个文件的内容。当你需要对比多个文件或分析跨文件关联时使用。' +
|
|
101
|
+
'注意:为了防止 Token 溢出,本工具一次最多读取 10 个文件,且每个文件仅展示前 6000 字符。' +
|
|
102
|
+
'若需查看完整大文件或特定逻辑,请改用 read_file_range。',
|
|
102
103
|
parameters: ReadMultipleFilesArgsSchema,
|
|
103
104
|
handler: async (args) => {
|
|
104
|
-
|
|
105
|
+
// 保护 1:文件数量限制 (防止 AI 一次传入几十个文件)
|
|
106
|
+
const MAX_FILES = 10;
|
|
107
|
+
const pathsToRead = args.paths.slice(0, MAX_FILES);
|
|
108
|
+
const isTruncatedByCount = args.paths.length > MAX_FILES;
|
|
109
|
+
// 保护 2:单文件字符数限制 (防止读入超大型二进制或日志文件)
|
|
110
|
+
const MAX_CHARS_PER_FILE = 6000;
|
|
111
|
+
const results = await Promise.all(pathsToRead.map(async (filePath) => {
|
|
105
112
|
try {
|
|
113
|
+
// 沿用你现有的路径验证逻辑
|
|
106
114
|
const validPath = await (0, lib_1.validatePath)(targetDir, filePath);
|
|
107
115
|
const content = await (0, lib_1.readFileContent)(validPath);
|
|
116
|
+
if (content.length > MAX_CHARS_PER_FILE) {
|
|
117
|
+
return `${filePath} (内容已截断):\n${content.substring(0, MAX_CHARS_PER_FILE)}\n\n[... 内容过长,仅展示前 ${MAX_CHARS_PER_FILE} 字符。若需查看后续内容,请使用 read_file_range 指定行号读取 ...]`;
|
|
118
|
+
}
|
|
108
119
|
return `${filePath}:\n${content}\n`;
|
|
109
120
|
}
|
|
110
121
|
catch (error) {
|
|
111
122
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
112
|
-
return `${filePath}:
|
|
123
|
+
return `${filePath}: 读取失败 - ${errorMessage}`;
|
|
113
124
|
}
|
|
114
125
|
}));
|
|
115
|
-
|
|
126
|
+
let text = results.join('\n---\n');
|
|
127
|
+
if (isTruncatedByCount) {
|
|
128
|
+
text += `\n\n⚠️ 注意:一次请求最多处理 ${MAX_FILES} 个文件。剩余 ${args.paths.length - MAX_FILES} 个文件未读取,请分批请求。`;
|
|
129
|
+
}
|
|
116
130
|
return text;
|
|
117
|
-
}
|
|
131
|
+
},
|
|
118
132
|
});
|
|
119
133
|
const writeFileTool = (0, createTool_1.createTool)({
|
|
120
|
-
name:
|
|
121
|
-
description:
|
|
122
|
-
"Use with caution as it will overwrite existing files without warning. " +
|
|
123
|
-
"Handles text content with proper encoding. Only works within allowed directories.",
|
|
134
|
+
name: 'write_file',
|
|
135
|
+
description: '仅用于创建新文件。严禁用于修改现有源代码。',
|
|
124
136
|
parameters: WriteFileArgsSchema,
|
|
125
137
|
handler: async (args) => {
|
|
126
138
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
127
139
|
await (0, lib_1.writeFileContent)(validPath, args.content);
|
|
128
140
|
const text = `Successfully wrote to ${args.path}`;
|
|
129
141
|
return text;
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
const editFileTool = (0, createTool_1.createTool)({
|
|
133
|
-
name: "edit_file",
|
|
134
|
-
description: "Make line-based edits to a text file. Each edit replaces exact line sequences " +
|
|
135
|
-
"with new content. Returns a git-style diff showing the changes made. " +
|
|
136
|
-
"Only works within allowed directories.",
|
|
137
|
-
parameters: EditFileArgsSchema,
|
|
138
|
-
handler: async (args) => {
|
|
139
|
-
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
140
|
-
const result = await (0, lib_1.applyFileEdits)(validPath, args.edits, args.dryRun);
|
|
141
|
-
return result;
|
|
142
|
-
}
|
|
142
|
+
},
|
|
143
143
|
});
|
|
144
144
|
const createDirectoryTool = (0, createTool_1.createTool)({
|
|
145
|
-
name:
|
|
146
|
-
description:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
name: 'create_directory',
|
|
146
|
+
description: 'Create a new directory or ensure a directory exists. Can create multiple ' +
|
|
147
|
+
'nested directories in one operation. If the directory already exists, ' +
|
|
148
|
+
'this operation will succeed silently. Perfect for setting up directory ' +
|
|
149
|
+
'structures for projects or ensuring required paths exist. Only works within allowed directories.',
|
|
150
150
|
parameters: CreateDirectoryArgsSchema,
|
|
151
151
|
handler: async (args) => {
|
|
152
152
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
153
153
|
await promises_1.default.mkdir(validPath, { recursive: true });
|
|
154
154
|
const text = `Successfully created directory ${args.path}`;
|
|
155
155
|
return text;
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
const listDirectoryTool = (0, createTool_1.createTool)({
|
|
159
|
-
name: "list_directory",
|
|
160
|
-
description: "Get a detailed listing of all files and directories in a specified path. " +
|
|
161
|
-
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
|
162
|
-
"prefixes. This tool is essential for understanding directory structure and " +
|
|
163
|
-
"finding specific files within a directory. Only works within allowed directories.",
|
|
164
|
-
parameters: ListDirectoryArgsSchema,
|
|
165
|
-
handler: async (args) => {
|
|
166
|
-
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
167
|
-
const entries = await promises_1.default.readdir(validPath, { withFileTypes: true });
|
|
168
|
-
const formatted = entries
|
|
169
|
-
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
|
|
170
|
-
.join("\n");
|
|
171
|
-
return formatted;
|
|
172
|
-
}
|
|
156
|
+
},
|
|
173
157
|
});
|
|
174
158
|
const listDirectoryWithSizesTool = (0, createTool_1.createTool)({
|
|
175
|
-
name:
|
|
176
|
-
description:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
159
|
+
name: 'list_directory',
|
|
160
|
+
description: 'Get a detailed listing of all files and directories in a specified path, including sizes. ' +
|
|
161
|
+
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
|
|
162
|
+
'prefixes. This tool is useful for understanding directory structure and ' +
|
|
163
|
+
'finding specific files within a directory. Only works within allowed directories.',
|
|
180
164
|
parameters: ListDirectoryWithSizesArgsSchema,
|
|
181
165
|
handler: async (args) => {
|
|
182
166
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -190,7 +174,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
190
174
|
name: entry.name,
|
|
191
175
|
isDirectory: entry.isDirectory(),
|
|
192
176
|
size: stats.size,
|
|
193
|
-
mtime: stats.mtime
|
|
177
|
+
mtime: stats.mtime,
|
|
194
178
|
};
|
|
195
179
|
}
|
|
196
180
|
catch (error) {
|
|
@@ -198,7 +182,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
198
182
|
name: entry.name,
|
|
199
183
|
isDirectory: entry.isDirectory(),
|
|
200
184
|
size: 0,
|
|
201
|
-
mtime: new Date(0)
|
|
185
|
+
mtime: new Date(0),
|
|
202
186
|
};
|
|
203
187
|
}
|
|
204
188
|
}));
|
|
@@ -211,70 +195,60 @@ const getFilesystemTools = (targetDir) => {
|
|
|
211
195
|
return a.name.localeCompare(b.name);
|
|
212
196
|
});
|
|
213
197
|
// Format the output
|
|
214
|
-
const formattedEntries = sortedEntries.map(entry => `${entry.isDirectory ?
|
|
198
|
+
const formattedEntries = sortedEntries.map(entry => `${entry.isDirectory ? '[DIR]' : '[FILE]'} ${entry.name.padEnd(30)} ${entry.isDirectory ? '' : (0, lib_1.formatSize)(entry.size).padStart(10)}`);
|
|
215
199
|
// Add summary
|
|
216
200
|
const totalFiles = detailedEntries.filter(e => !e.isDirectory).length;
|
|
217
201
|
const totalDirs = detailedEntries.filter(e => e.isDirectory).length;
|
|
218
202
|
const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);
|
|
219
203
|
const summary = [
|
|
220
|
-
|
|
204
|
+
'',
|
|
221
205
|
`Total: ${totalFiles} files, ${totalDirs} directories`,
|
|
222
|
-
`Combined size: ${(0, lib_1.formatSize)(totalSize)}
|
|
206
|
+
`Combined size: ${(0, lib_1.formatSize)(totalSize)}`,
|
|
223
207
|
];
|
|
224
|
-
const text = [...formattedEntries, ...summary].join(
|
|
208
|
+
const text = [...formattedEntries, ...summary].join('\n');
|
|
225
209
|
return text;
|
|
226
|
-
}
|
|
210
|
+
},
|
|
227
211
|
});
|
|
228
212
|
const directoryTreeTool = (0, createTool_1.createTool)({
|
|
229
|
-
name:
|
|
230
|
-
description:
|
|
231
|
-
|
|
232
|
-
"Files have no children array, while directories always have a children array (which may be empty). " +
|
|
233
|
-
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
|
213
|
+
name: 'directory_tree',
|
|
214
|
+
description: '获取目录的递归树状 JSON 结构。' +
|
|
215
|
+
'默认仅展示 2 层深度以节省 Token。如果需要看更深层级,请调大 depth 参数。',
|
|
234
216
|
parameters: DirectoryTreeArgsSchema,
|
|
235
217
|
handler: async (args) => {
|
|
236
218
|
const rootPath = args.path;
|
|
237
|
-
async function buildTree(currentPath, excludePatterns = []) {
|
|
219
|
+
async function buildTree(currentPath, currentDepth, maxDepth, excludePatterns = []) {
|
|
220
|
+
if (currentDepth > maxDepth)
|
|
221
|
+
return []; // 深度限制
|
|
238
222
|
const validPath = await (0, lib_1.validatePath)(targetDir, currentPath);
|
|
239
223
|
const entries = await promises_1.default.readdir(validPath, { withFileTypes: true });
|
|
240
224
|
const result = [];
|
|
241
225
|
for (const entry of entries) {
|
|
242
226
|
const relativePath = path_1.default.relative(rootPath, path_1.default.join(currentPath, entry.name));
|
|
243
|
-
const shouldExclude = excludePatterns.some(pattern => {
|
|
244
|
-
if (pattern.includes('*')) {
|
|
245
|
-
return (0, minimatch_1.minimatch)(relativePath, pattern, { dot: true });
|
|
246
|
-
}
|
|
247
|
-
// For files: match exact name or as part of path
|
|
248
|
-
// For directories: match as directory path
|
|
249
|
-
return (0, minimatch_1.minimatch)(relativePath, pattern, { dot: true }) ||
|
|
250
|
-
(0, minimatch_1.minimatch)(relativePath, `**/${pattern}`, { dot: true }) ||
|
|
251
|
-
(0, minimatch_1.minimatch)(relativePath, `**/${pattern}/**`, { dot: true });
|
|
252
|
-
});
|
|
227
|
+
const shouldExclude = excludePatterns.some(pattern => (0, minimatch_1.minimatch)(relativePath, pattern, { dot: true }));
|
|
253
228
|
if (shouldExclude)
|
|
254
229
|
continue;
|
|
255
230
|
const entryData = {
|
|
256
231
|
name: entry.name,
|
|
257
|
-
type: entry.isDirectory() ? 'directory' : 'file'
|
|
232
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
258
233
|
};
|
|
259
|
-
if (entry.isDirectory()) {
|
|
234
|
+
if (entry.isDirectory() && currentDepth < maxDepth) {
|
|
260
235
|
const subPath = path_1.default.join(currentPath, entry.name);
|
|
261
|
-
entryData.children = await buildTree(subPath, excludePatterns);
|
|
236
|
+
entryData.children = await buildTree(subPath, currentDepth + 1, maxDepth, excludePatterns);
|
|
262
237
|
}
|
|
263
238
|
result.push(entryData);
|
|
264
239
|
}
|
|
265
240
|
return result;
|
|
266
241
|
}
|
|
267
|
-
const treeData = await buildTree(rootPath, args.excludePatterns);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
242
|
+
const treeData = await buildTree(rootPath, 1, args.depth, args.excludePatterns);
|
|
243
|
+
return JSON.stringify(treeData, null, 2);
|
|
244
|
+
},
|
|
271
245
|
});
|
|
272
246
|
const moveFileTool = (0, createTool_1.createTool)({
|
|
273
|
-
name:
|
|
274
|
-
description:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
247
|
+
name: 'move_file',
|
|
248
|
+
description: 'Move or rename files and directories. Can move files between directories ' +
|
|
249
|
+
'and rename them in a single operation. If the destination exists, the ' +
|
|
250
|
+
'operation will fail. Works across different directories and can be used ' +
|
|
251
|
+
'for simple renaming within the same directory. Both source and destination must be within allowed directories.',
|
|
278
252
|
parameters: MoveFileArgsSchema,
|
|
279
253
|
handler: async (args) => {
|
|
280
254
|
const validSourcePath = await (0, lib_1.validatePath)(targetDir, args.source);
|
|
@@ -282,46 +256,163 @@ const getFilesystemTools = (targetDir) => {
|
|
|
282
256
|
await promises_1.default.rename(validSourcePath, validDestPath);
|
|
283
257
|
const text = `Successfully moved ${args.source} to ${args.destination}`;
|
|
284
258
|
return text;
|
|
285
|
-
}
|
|
259
|
+
},
|
|
286
260
|
});
|
|
287
261
|
const searchFilesTool = (0, createTool_1.createTool)({
|
|
288
|
-
name:
|
|
289
|
-
description:
|
|
290
|
-
|
|
262
|
+
name: 'search_files',
|
|
263
|
+
description: 'Search for files matching a specific pattern in a specified path. ' +
|
|
264
|
+
'Returns a list of files that match the pattern. Only works within allowed directories.' +
|
|
265
|
+
'Used only for filename search',
|
|
291
266
|
parameters: SearchFilesArgsSchema,
|
|
292
267
|
handler: async (args) => {
|
|
293
268
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
294
|
-
const results = await (0, lib_1.searchFilesWithValidation)(targetDir, validPath, args.pattern, [], {
|
|
295
|
-
|
|
269
|
+
const results = await (0, lib_1.searchFilesWithValidation)(targetDir, validPath, args.pattern, [targetDir], {
|
|
270
|
+
excludePatterns: ['node_modules', ...((args === null || args === void 0 ? void 0 : args.excludePatterns) || [])],
|
|
271
|
+
});
|
|
272
|
+
const text = results.length > 0 ? results.join('\n') : 'No matches found';
|
|
296
273
|
return text;
|
|
297
|
-
}
|
|
274
|
+
},
|
|
298
275
|
});
|
|
299
276
|
const getFileInfoTool = (0, createTool_1.createTool)({
|
|
300
|
-
name:
|
|
301
|
-
description:
|
|
302
|
-
|
|
277
|
+
name: 'get_file_info',
|
|
278
|
+
description: 'Get detailed information about a file, including its size, last modified time, and type. ' +
|
|
279
|
+
'Only works within allowed directories.',
|
|
303
280
|
parameters: GetFileInfoArgsSchema,
|
|
304
281
|
handler: async (args) => {
|
|
305
282
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
306
283
|
const info = await (0, lib_1.getFileStats)(validPath);
|
|
307
284
|
const text = Object.entries(info)
|
|
308
285
|
.map(([key, value]) => `${key}: ${value}`)
|
|
309
|
-
.join(
|
|
286
|
+
.join('\n');
|
|
310
287
|
return text;
|
|
311
|
-
}
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const grepSearchTool = (0, createTool_1.createTool)({
|
|
291
|
+
name: 'grep_search',
|
|
292
|
+
description: '在指定目录的文件内容中搜索关键字。' +
|
|
293
|
+
'该工具会返回包含关键字的文件路径及匹配行的预览。' +
|
|
294
|
+
'请尽量通过 includePattern 缩小搜索范围。',
|
|
295
|
+
parameters: GrepSearchArgsSchema,
|
|
296
|
+
handler: async (args) => {
|
|
297
|
+
const startPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
298
|
+
const allFiles = await (0, lib_1.searchFilesWithValidation)(targetDir, startPath, args.includePattern, [targetDir], { excludePatterns: ['node_modules', 'dist', '.git', 'build'] });
|
|
299
|
+
// 限制扫描文件数,防止爆炸
|
|
300
|
+
const filesToScan = allFiles.slice(0, args.maxFiles);
|
|
301
|
+
const matches = [];
|
|
302
|
+
const concurrencyLimit = 10;
|
|
303
|
+
for (let i = 0; i < filesToScan.length; i += concurrencyLimit) {
|
|
304
|
+
const chunk = filesToScan.slice(i, i + concurrencyLimit);
|
|
305
|
+
await Promise.all(chunk.map(async (filePath) => {
|
|
306
|
+
try {
|
|
307
|
+
const stats = await promises_1.default.stat(filePath);
|
|
308
|
+
if (!stats.isFile())
|
|
309
|
+
return;
|
|
310
|
+
const content = await (0, lib_1.readFileContent)(filePath);
|
|
311
|
+
if (content.includes(args.query)) {
|
|
312
|
+
const relativePath = path_1.default.relative(targetDir, filePath);
|
|
313
|
+
// 找到匹配的那一行(预览用)
|
|
314
|
+
const lines = content.split('\n');
|
|
315
|
+
const matchLineIndex = lines.findIndex(l => l.includes(args.query));
|
|
316
|
+
matches.push(`${relativePath} (Line ${matchLineIndex + 1}: "${lines[matchLineIndex].trim().substring(0, 100)}")`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch { /* 忽略错误文件 */ }
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
let response = matches.length > 0
|
|
323
|
+
? `找到关键词 "${args.query}" 的位置如下:\n${matches.join('\n')}`
|
|
324
|
+
: `未找到包含 "${args.query}" 的内容。`;
|
|
325
|
+
if (allFiles.length > args.maxFiles) {
|
|
326
|
+
response += `\n\n注意:搜索已达到限制,仅扫描了前 ${args.maxFiles} 个文件。若未找到结果,请提供更精确的 path 或 includePattern。`;
|
|
327
|
+
}
|
|
328
|
+
return response;
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const readFileRangeTool = (0, createTool_1.createTool)({
|
|
332
|
+
name: 'read_file_range',
|
|
333
|
+
description: '精准读取指定行范围(包含行号前缀)。修改代码前或根据报错定位时必用。',
|
|
334
|
+
parameters: zod_1.z.object({
|
|
335
|
+
path: zod_1.z.string().describe('相对于目标目录的文件路径'),
|
|
336
|
+
startLine: zod_1.z.number().describe('起始行号(从 1 开始计)'),
|
|
337
|
+
endLine: zod_1.z.number().describe('结束行号'),
|
|
338
|
+
}),
|
|
339
|
+
handler: async (args) => {
|
|
340
|
+
// 1. 验证路径安全(沿用你代码中的 validatePath 逻辑)
|
|
341
|
+
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
342
|
+
try {
|
|
343
|
+
const content = await promises_1.default.readFile(validPath, 'utf-8');
|
|
344
|
+
const lines = content.split('\n');
|
|
345
|
+
const totalLines = lines.length;
|
|
346
|
+
// 2. 边界保护:确保行号不越界
|
|
347
|
+
const start = Math.max(1, args.startLine);
|
|
348
|
+
const end = Math.min(totalLines, args.endLine);
|
|
349
|
+
if (start > totalLines) {
|
|
350
|
+
return `错误:文件仅有 ${totalLines} 行,起始行号 ${start} 超出范围。`;
|
|
351
|
+
}
|
|
352
|
+
if (start > end) {
|
|
353
|
+
return `错误:起始行号 ${start} 不能大于结束行号 ${end}。`;
|
|
354
|
+
}
|
|
355
|
+
// 3. 截取并添加行号索引(核心:增强 AI 的位置感)
|
|
356
|
+
const selectedLines = lines.slice(start - 1, end);
|
|
357
|
+
const formattedContent = selectedLines
|
|
358
|
+
.map((line, index) => `${start + index}| ${line}`)
|
|
359
|
+
.join('\n');
|
|
360
|
+
return `[文件: ${args.path} | 第 ${start} 至 ${end} 行 / 共 ${totalLines} 行]\n${formattedContent}`;
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return `读取文件范围失败: ${error.message}`;
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const editFileTool = (0, createTool_1.createTool)({
|
|
368
|
+
name: 'edit_file',
|
|
369
|
+
description: '基于行号范围替换代码。修改逻辑的唯一工具。调用前须通过 read_file_range 获取最新行号。支持删除(空内容)或单行替换。',
|
|
370
|
+
parameters: PatchEditArgsSchema,
|
|
371
|
+
handler: async (args) => {
|
|
372
|
+
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
373
|
+
try {
|
|
374
|
+
const content = await promises_1.default.readFile(validPath, 'utf-8');
|
|
375
|
+
let lines = content.split('\n');
|
|
376
|
+
// 按起始行号从大到小排序,这样修改前面的行不会影响后面待修改行的索引
|
|
377
|
+
const sortedPatches = [...args.patches].sort((a, b) => b.startLine - a.startLine);
|
|
378
|
+
for (const patch of sortedPatches) {
|
|
379
|
+
// 校验行号合法性
|
|
380
|
+
if (patch.startLine < 1 || patch.endLine > lines.length || patch.startLine > patch.endLine) {
|
|
381
|
+
return `错误:行号范围 ${patch.startLine}-${patch.endLine} 超出文件实际范围 (1-${lines.length})`;
|
|
382
|
+
}
|
|
383
|
+
// 可选:二次校验(防止 AI 记忆了错误的行号)
|
|
384
|
+
if (patch.originalSnippet) {
|
|
385
|
+
const currentText = lines.slice(patch.startLine - 1, patch.endLine).join('\n');
|
|
386
|
+
// 模糊对比,如果差异太大则报错
|
|
387
|
+
if (!currentText.includes(patch.originalSnippet.trim()) && currentText.trim().length > 0) {
|
|
388
|
+
return `警告:第 ${patch.startLine} 行的内容已发生变动,与你预想的代码不符。请重新读取文件获取最新行号。`;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// 执行替换:splice(开始索引, 删除数量, 替换内容)
|
|
392
|
+
// 索引需要减 1
|
|
393
|
+
lines.splice(patch.startLine - 1, (patch.endLine - patch.startLine) + 1, patch.replacement);
|
|
394
|
+
}
|
|
395
|
+
await promises_1.default.writeFile(validPath, lines.join('\n'), 'utf-8');
|
|
396
|
+
return `成功通过行号更新了 ${args.path} 的 ${args.patches.length} 处代码。`;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
return `Patch 失败: ${error.message}`;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
312
402
|
});
|
|
313
403
|
return [
|
|
404
|
+
readFileRangeTool,
|
|
405
|
+
editFileTool,
|
|
406
|
+
directoryTreeTool,
|
|
407
|
+
listDirectoryWithSizesTool,
|
|
408
|
+
grepSearchTool,
|
|
409
|
+
getFileInfoTool,
|
|
314
410
|
readTextFileTool,
|
|
315
411
|
readMultipleFilesTool,
|
|
412
|
+
searchFilesTool,
|
|
316
413
|
writeFileTool,
|
|
317
|
-
editFileTool,
|
|
318
414
|
createDirectoryTool,
|
|
319
|
-
listDirectoryTool,
|
|
320
|
-
listDirectoryWithSizesTool,
|
|
321
|
-
directoryTreeTool,
|
|
322
415
|
moveFileTool,
|
|
323
|
-
searchFilesTool,
|
|
324
|
-
getFileInfoTool,
|
|
325
416
|
];
|
|
326
417
|
};
|
|
327
418
|
exports.getFilesystemTools = getFilesystemTools;
|
|
@@ -8,8 +8,19 @@ const getTsLspTools = (targetDir) => {
|
|
|
8
8
|
const engine = new ts_context_mcp_1.PromptEngine(targetDir);
|
|
9
9
|
return [
|
|
10
10
|
(0, createTool_1.createTool)({
|
|
11
|
-
name:
|
|
12
|
-
description:
|
|
11
|
+
name: 'get_method_body',
|
|
12
|
+
description: '【仅限TS/JS】通过方法名提取代码块。比行号读取更抗干扰,参考逻辑时首选。',
|
|
13
|
+
parameters: zod_1.z.object({
|
|
14
|
+
filePath: zod_1.z.string().describe('文件相对路径'),
|
|
15
|
+
methodName: zod_1.z.string().describe('方法名或函数名'),
|
|
16
|
+
}),
|
|
17
|
+
handler: async ({ filePath, methodName }) => {
|
|
18
|
+
return engine.getMethodImplementation(filePath, methodName);
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
(0, createTool_1.createTool)({
|
|
22
|
+
name: 'get_repo_map',
|
|
23
|
+
description: '获取项目全局文件结构及导出清单,用于快速定位代码',
|
|
13
24
|
parameters: zod_1.z.object({}),
|
|
14
25
|
handler: async () => {
|
|
15
26
|
engine.refresh();
|
|
@@ -17,18 +28,18 @@ const getTsLspTools = (targetDir) => {
|
|
|
17
28
|
},
|
|
18
29
|
}),
|
|
19
30
|
(0, createTool_1.createTool)({
|
|
20
|
-
name:
|
|
21
|
-
description:
|
|
22
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe(
|
|
31
|
+
name: 'analyze_deps',
|
|
32
|
+
description: '分析指定文件的依赖关系,支持 tsconfig 路径别名解析',
|
|
33
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('文件相对路径') }),
|
|
23
34
|
handler: async ({ filePath }) => engine.getDeps(filePath),
|
|
24
35
|
}),
|
|
25
36
|
(0, createTool_1.createTool)({
|
|
26
|
-
name:
|
|
27
|
-
description:
|
|
28
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe(
|
|
37
|
+
name: 'read_skeleton',
|
|
38
|
+
description: '提取文件的结构定义(接口、类、方法签名),忽略具体实现以节省 Token',
|
|
39
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('文件相对路径') }),
|
|
29
40
|
handler: async (args) => {
|
|
30
41
|
const pathArg = args === null || args === void 0 ? void 0 : args.filePath;
|
|
31
|
-
if (typeof pathArg !==
|
|
42
|
+
if (typeof pathArg !== 'string' || pathArg.trim() === '') {
|
|
32
43
|
return `Error: 参数 'filePath' 无效。收到的是: ${JSON.stringify(pathArg)}`;
|
|
33
44
|
}
|
|
34
45
|
try {
|
|
@@ -41,14 +52,6 @@ const getTsLspTools = (targetDir) => {
|
|
|
41
52
|
}
|
|
42
53
|
},
|
|
43
54
|
}),
|
|
44
|
-
(0, createTool_1.createTool)({
|
|
45
|
-
name: "get_method_body",
|
|
46
|
-
description: "获取指定文件内某个方法或函数的完整实现代码",
|
|
47
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe("文件相对路径"), methodName: zod_1.z.string().describe("方法名或函数名") }),
|
|
48
|
-
handler: async ({ filePath, methodName }) => {
|
|
49
|
-
return engine.getMethodImplementation(filePath, methodName);
|
|
50
|
-
},
|
|
51
|
-
}),
|
|
52
55
|
];
|
|
53
56
|
};
|
|
54
57
|
exports.getTsLspTools = getTsLspTools;
|