@ghenya/clinn 0.7.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,408 @@
1
+ /**
2
+ * Trae IDE + Claude Hermes 风格扩展工具集
3
+ * 参考 Trae: TodoWrite / SearchCodebase / Glob / Grep / Read / WebSearch / WebFetch
4
+ * SearchReplace / Write / DeleteFile / RunCommand / CheckCommandStatus
5
+ * GetDiagnostics / OpenPreview / Skill
6
+ * 参考 Hermes: 同上 + 对话管理 / 技能调用
7
+ */
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { execSync, spawn } = require("child_process");
11
+ const https = require("https");
12
+ const http = require("http");
13
+
14
+ const todoWriteTool = {
15
+ name: "todo_write",
16
+ description: "创建和更新结构化任务列表, 追踪当前会话中所有任务进度. 每次变更都传完整列表",
17
+ parameters: {
18
+ todos: { type: "string", required: true, description: "JSON字符串, 格式: [{\"id\":\"1\",\"content\":\"任务描述\",\"status\":\"pending|in_progress|completed\",\"priority\":\"high|medium|low\"}], 最多10项" },
19
+ },
20
+ execute: async ({ todos }) => {
21
+ try {
22
+ const list = JSON.parse(todos);
23
+ if (!Array.isArray(list)) return "[失败] todos 必须是数组";
24
+ if (list.length > 10) return `[警告] 最多10项, 收到${list.length}项`;
25
+ const counts = { pending: 0, in_progress: 0, completed: 0 };
26
+ const lines = [];
27
+ for (const t of list) {
28
+ counts[t.status] = (counts[t.status] || 0) + 1;
29
+ const icon = t.status === "completed" ? "✓" : t.status === "in_progress" ? "▶" : "○";
30
+ lines.push(` ${icon} [${t.id}] ${t.content}`);
31
+ }
32
+ return `[Todo | ${list.length}项 | ▶${counts.in_progress} ○${counts.pending} ✓${counts.completed}]\n${lines.join("\n")}`;
33
+ } catch (e) {
34
+ return `[失败] JSON解析错误: ${e.message}`;
35
+ }
36
+ },
37
+ };
38
+
39
+ const searchReplaceTool = {
40
+ name: "search_replace",
41
+ description: "搜索并替换文件内容(精确匹配). 比edit_lines更安全: 必须唯一匹配才能替换",
42
+ dangerous: true,
43
+ parameters: {
44
+ filePath: { type: "string", required: true, description: "要编辑的文件路径" },
45
+ oldStr: { type: "string", required: true, description: "要替换的原始文本块(必须与文件中完全一致)" },
46
+ newStr: { type: "string", required: true, description: "替换后的新文本块" },
47
+ },
48
+ execute: async ({ filePath, oldStr, newStr }) => {
49
+ if (!fs.existsSync(filePath)) return `[不存在] ${filePath}`;
50
+ const content = fs.readFileSync(filePath, "utf-8");
51
+ const idx = content.indexOf(oldStr);
52
+ if (idx === -1) return `[失败] 未找到匹配文本, 请用 read_file 确认文件内容`;
53
+ const secondIdx = content.indexOf(oldStr, idx + 1);
54
+ if (secondIdx !== -1) return `[失败] 匹配到多处(至少2处), 请提供更精确的上下文使其唯一`;
55
+ const newContent = content.slice(0, idx) + newStr + content.slice(idx + oldStr.length);
56
+ fs.writeFileSync(filePath, newContent, "utf-8");
57
+ const oldLines = oldStr.split("\n").length;
58
+ const newLines = newStr.split("\n").length;
59
+ return `[OK] 已替换 ${filePath}: ${oldLines}行 → ${newLines}行`;
60
+ },
61
+ };
62
+
63
+ const globTool = {
64
+ name: "glob",
65
+ description: "文件模式匹配: 支持 **/*.js / src/**/*.ts 等glob模式, 按修改时间排序",
66
+ parameters: {
67
+ pattern: { type: "string", required: true, description: "glob模式, 如 '**/*.js' 或 'src/**/*.test.ts'" },
68
+ dirPath: { type: "string", required: false, description: "搜索根目录, 默认当前目录" },
69
+ },
70
+ execute: async ({ pattern, dirPath }) => {
71
+ const root = dirPath || process.cwd();
72
+ if (!fs.existsSync(root)) return `[不存在] ${root}`;
73
+ try {
74
+ const cmd = `Get-ChildItem -Path "${root}" -Filter "${pattern.split("/").pop()}" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch 'node_modules|\\\\\.git\\\\' } | Select-Object -First 100 | ForEach-Object { $_.FullName }`;
75
+ const output = execSync(cmd, { cwd: root, encoding: "utf-8", timeout: 8000, maxBuffer: 1024 * 1024, shell: "powershell.exe" });
76
+ const files = output.trim().split("\r\n").filter(Boolean);
77
+ return files.length > 0 ? files.join("\n") : `[无匹配] ${pattern} 在 ${root}`;
78
+ } catch (e) {
79
+ return `[无匹配] ${pattern} 在 ${root}`;
80
+ }
81
+ },
82
+ };
83
+
84
+ const grepTool = {
85
+ name: "grep",
86
+ description: "正则搜索文件内容(ripgrep风格). 返回 文件路径:行号:内容",
87
+ parameters: {
88
+ pattern: { type: "string", required: true, description: "正则表达式" },
89
+ dirPath: { type: "string", required: false, description: "搜索目录, 默认当前目录" },
90
+ glob: { type: "string", required: false, description: "文件过滤glob, 如 '*.js'" },
91
+ headLimit: { type: "number", required: false, description: "结果上限, 默认50" },
92
+ ignoreCase: { type: "boolean", required: false, description: "忽略大小写, 默认true" },
93
+ },
94
+ execute: async ({ pattern, dirPath, glob, headLimit, ignoreCase }) => {
95
+ const root = dirPath || process.cwd();
96
+ if (!fs.existsSync(root)) return `[不存在] ${root}`;
97
+ const max = headLimit || 50;
98
+ const ic = ignoreCase !== false;
99
+ const globFilter = glob ? ` --include="${glob}"` : "";
100
+ const icFlag = ic ? " -i" : "";
101
+ try {
102
+ const cmd = `grep -rn${icFlag}${globFilter} -m ${max} "${pattern.replace(/"/g, '\\"')}" "${root}"`;
103
+ const output = execSync(cmd, { cwd: root, encoding: "utf-8", timeout: 15000, maxBuffer: 2 * 1024 * 1024 });
104
+ const lines = output.trim().split("\n").slice(0, max);
105
+ return lines.length > 0 ? lines.join("\n") : `[无匹配] "${pattern}" 在 ${root}`;
106
+ } catch (e) {
107
+ if (e.stdout) return e.stdout.trim().split("\n").slice(0, max).join("\n");
108
+ return `[无匹配] "${pattern}" 在 ${root}`;
109
+ }
110
+ },
111
+ };
112
+
113
+ const readTool = {
114
+ name: "read",
115
+ description: "读取文件内容(支持行范围). 比read_file更灵活: 可指定offset+limit",
116
+ parameters: {
117
+ filePath: { type: "string", required: true, description: "文件绝对路径" },
118
+ offset: { type: "number", required: false, description: "起始行号(1开始), 默认1" },
119
+ limit: { type: "number", required: false, description: "读取行数, 默认200" },
120
+ },
121
+ execute: async ({ filePath, offset, limit }) => {
122
+ if (!fs.existsSync(filePath)) return `[不存在] ${filePath}`;
123
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
124
+ const s = (offset || 1) - 1;
125
+ const e = Math.min(lines.length, s + (limit || 200));
126
+ if (s >= lines.length) return `[越界] 行${offset || 1}, 文件共 ${lines.length} 行`;
127
+ return lines.slice(s, e).map((l, i) => `${s + i + 1}| ${l}`).join("\n") + (e < lines.length ? `\n(共${lines.length}行, 显示${s + 1}-${e})` : "");
128
+ },
129
+ };
130
+
131
+ const writeTool = {
132
+ name: "write",
133
+ description: "写入文件(创建或覆盖, 自动建父目录). 内容中 \\n 表示换行",
134
+ dangerous: true,
135
+ parameters: {
136
+ filePath: { type: "string", required: true, description: "文件绝对路径" },
137
+ content: { type: "string", required: true, description: "写入内容" },
138
+ },
139
+ execute: async ({ filePath, content }) => {
140
+ const dir = path.dirname(filePath);
141
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
142
+ fs.writeFileSync(filePath, content, "utf-8");
143
+ return `[OK] 已写入 ${filePath} (${content.length} 字符, ${content.split("\n").length} 行)`;
144
+ },
145
+ };
146
+
147
+ const lsTool = {
148
+ name: "ls",
149
+ description: "列出目录内容, 按类型分组显示",
150
+ parameters: {
151
+ dirPath: { type: "string", required: false, description: "目录路径, 默认当前目录" },
152
+ ignore: { type: "string", required: false, description: "忽略的glob模式, JSON数组字符串, 如 '[\"node_modules\",\".git\"]'" },
153
+ },
154
+ execute: async ({ dirPath, ignore }) => {
155
+ const root = dirPath || process.cwd();
156
+ if (!fs.existsSync(root)) return `[不存在] ${root}`;
157
+ const ignoreList = ignore ? JSON.parse(ignore) : [];
158
+ const entries = fs.readdirSync(root, { withFileTypes: true })
159
+ .filter((e) => !ignoreList.some((p) => e.name.includes(p) || e.name === p));
160
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => `[DIR] ${e.name}/`);
161
+ const files = entries.filter((e) => e.isFile()).map((e) => {
162
+ try { return `[FILE] ${e.name} (${fs.statSync(path.join(root, e.name)).size}B)`; }
163
+ catch (_) { return `[FILE] ${e.name}`; }
164
+ });
165
+ return `${root}\n${dirs.concat(files).join("\n") || "(空目录)"}`;
166
+ },
167
+ };
168
+
169
+ const treeTool = {
170
+ name: "tree",
171
+ description: "递归生成目录文件树(一次性看完整个项目结构, 不用逐目录 ls). 自动忽略 node_modules/.git 等",
172
+ parameters: {
173
+ dirPath: { type: "string", required: false, description: "根目录路径, 默认当前目录" },
174
+ maxDepth: { type: "number", required: false, description: "最大递归深度, 默认5, 最大8" },
175
+ ignore: { type: "string", required: false, description: "额外忽略的glob模式, JSON数组字符串, 如 '[\"dist\",\"*.log\"]'" },
176
+ },
177
+ execute: async ({ dirPath, maxDepth, ignore }) => {
178
+ const root = dirPath || process.cwd();
179
+ if (!fs.existsSync(root)) return `[不存在] ${root}`;
180
+ const maxD = Math.min(maxDepth || 5, 8);
181
+ const extraIgnore = ignore ? JSON.parse(ignore) : [];
182
+ const defaultIgnore = ["node_modules", ".git", ".svn", "__pycache__", ".DS_Store", "Thumbs.db", ".idea", ".vscode"];
183
+ const ignoreSet = new Set([...defaultIgnore, ...extraIgnore]);
184
+
185
+ function shouldIgnore(name) {
186
+ return ignoreSet.has(name) || extraIgnore.some((p) => {
187
+ if (p.includes("*")) {
188
+ const re = new RegExp("^" + p.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
189
+ return re.test(name);
190
+ }
191
+ return false;
192
+ });
193
+ }
194
+
195
+ function walk(dir, depth, prefix) {
196
+ if (depth > maxD) return "";
197
+ let result = "";
198
+ let entries;
199
+ try {
200
+ entries = fs.readdirSync(dir, { withFileTypes: true })
201
+ .filter((e) => !shouldIgnore(e.name));
202
+ } catch (_) {
203
+ return prefix + "(无权限)\n";
204
+ }
205
+ const dirs = entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
206
+ const files = entries.filter((e) => e.isFile()).sort((a, b) => a.name.localeCompare(b.name));
207
+ const all = [...dirs, ...files];
208
+ for (let i = 0; i < all.length; i++) {
209
+ const entry = all[i];
210
+ const isLast = i === all.length - 1;
211
+ const connector = isLast ? "└── " : "├── ";
212
+ const childPrefix = prefix + (isLast ? " " : "│ ");
213
+ if (entry.isDirectory()) {
214
+ result += prefix + connector + entry.name + "/\n";
215
+ result += walk(path.join(dir, entry.name), depth + 1, childPrefix);
216
+ } else {
217
+ let size = "";
218
+ try { size = ` (${fs.statSync(path.join(dir, entry.name)).size}B)`; } catch (_) {}
219
+ result += prefix + connector + entry.name + size + "\n";
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+
225
+ const rootName = path.basename(root) || root;
226
+ let output = rootName + "/\n";
227
+ output += walk(root, 1, "");
228
+ const lineCount = output.split("\n").filter(Boolean).length;
229
+ return output.trimEnd() + `\n\n${lineCount} 项 (深度≤${maxD})`;
230
+ },
231
+ };
232
+
233
+ const webSearchTool = {
234
+ name: "web_search",
235
+ description: "用Bing搜索互联网获取实时信息. 用于查文档/新闻/最新资料. 自动回退浏览器",
236
+ parameters: {
237
+ query: { type: "string", required: true, description: "搜索关键词" },
238
+ num: { type: "number", required: false, description: "结果数量, 默认5, 最多10" },
239
+ },
240
+ execute: async ({ query, num }) => {
241
+ const n = Math.min(num || 5, 10);
242
+
243
+ const bingSearch = async () => {
244
+ const bingUrl = `https://cn.bing.com/search?q=${encodeURIComponent(query)}&count=${n}`;
245
+ return new Promise((resolve) => {
246
+ const mod = bingUrl.startsWith("https") ? https : http;
247
+ const req = mod.get(bingUrl, {
248
+ headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36" },
249
+ }, (res) => {
250
+ if (res.statusCode !== 200) { resolve(null); return; }
251
+ let d = "";
252
+ res.on("data", (c) => (d += c));
253
+ res.on("end", () => {
254
+ const lis = d.match(/<li class="b_algo"[^>]*>([\s\S]*?)<\/li>/g) || [];
255
+ const results = [];
256
+ for (let i = 0; i < Math.min(n, lis.length); i++) {
257
+ const titleM = lis[i].match(/<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
258
+ if (!titleM) continue;
259
+ const title = titleM[2].replace(/<[^>]+>/g, "").trim().slice(0, 120);
260
+ const urlFound = titleM[1];
261
+ const descM = lis[i].match(/<p[^>]*>([\s\S]*?)<\/p>/);
262
+ const desc = descM ? descM[1].replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&ensp;|&#0183;|&nbsp;/g, " ").replace(/\s+/g, " ").trim().slice(0, 200) : "";
263
+ results.push(`${i + 1}. ${title}${desc ? "\n " + desc : ""}${urlFound ? "\n " + urlFound : ""}`);
264
+ }
265
+ resolve(results.length > 0 ? `[Bing搜索: ${query}]\n${results.join("\n")}` : null);
266
+ });
267
+ });
268
+ req.on("error", () => resolve(null));
269
+ req.setTimeout(12000, () => { req.destroy(); resolve(null); });
270
+ });
271
+ };
272
+
273
+ const browserSearch = async () => {
274
+ try {
275
+ const { browse_page: bp } = require("./browser");
276
+ const raw = await bp.execute({
277
+ url: `https://cn.bing.com/search?q=${encodeURIComponent(query)}&count=${n}`,
278
+ waitMs: 1500,
279
+ extractLinks: false,
280
+ });
281
+ const lines = raw.split("\n").map(l => l.trim()).filter(l => l.length > 20);
282
+ const results = [];
283
+ for (const line of lines) {
284
+ if (results.length >= n) break;
285
+ if (line.includes("[浏览器") || line.includes("[HTTP")) continue;
286
+ results.push(`${results.length + 1}. ${line.slice(0, 200)}`);
287
+ }
288
+ if (results.length > 0) return `[浏览器搜索: ${query}]\n${results.join("\n")}`;
289
+ } catch (_) {}
290
+ return null;
291
+ };
292
+
293
+ const bingResult = await bingSearch();
294
+ if (bingResult) return bingResult;
295
+
296
+ const browserResult = await browserSearch();
297
+ if (browserResult) return browserResult;
298
+
299
+ return `[搜索: ${query}] 未获取到有效结果, 建议用 browse_page 直接搜索`;
300
+ },
301
+ };
302
+
303
+ const checkStatusTool = {
304
+ name: "check_command_status",
305
+ description: "检查之前启动的非阻塞命令的状态. 用于 dev server / watch 等长时间运行的命令",
306
+ parameters: {
307
+ commandId: { type: "string", required: false, description: "命令ID(exec_console返回的PID或标识)" },
308
+ },
309
+ execute: async ({ commandId }) => {
310
+ return `[提示] 当前无活跃后台命令. 如需检查进程: 用 exec_console 执行 tasklist / Get-Process 查询.`;
311
+ },
312
+ };
313
+
314
+ const openPreviewTool = {
315
+ name: "open_preview",
316
+ description: "尝试在浏览器中打开URL预览(如本地开发服务器)",
317
+ parameters: {
318
+ url: { type: "string", required: true, description: "预览URL, 如 http://localhost:3000" },
319
+ },
320
+ execute: async ({ url }) => {
321
+ try {
322
+ execSync(`start "${url}"`, { shell: true, timeout: 3000 });
323
+ return `[OK] 已在浏览器中打开 ${url}`;
324
+ } catch (_) {
325
+ return `[未打开] 无法启动浏览器, 请手动访问: ${url}`;
326
+ }
327
+ },
328
+ };
329
+
330
+ const getDiagnosticsTool = {
331
+ name: "get_diagnostics",
332
+ description: "获取当前项目的语言诊断信息(语法错误/类型错误等). 调用后自动运行 lint check",
333
+ parameters: {},
334
+ execute: async () => {
335
+ const cwd = process.cwd();
336
+ const results = [];
337
+ try {
338
+ const out = execSync("npx tsc --noEmit 2>&1 || echo ''", { cwd, encoding: "utf-8", timeout: 30000, maxBuffer: 512 * 1024 });
339
+ const errors = out.trim().split("\n").filter((l) => l.includes("error TS"));
340
+ if (errors.length > 0) results.push(`[TypeScript] ${errors.length} 个错误`);
341
+ results.push(...errors.slice(0, 20));
342
+ } catch (_) {}
343
+ try {
344
+ const out = execSync("npx eslint . --format compact 2>&1 || echo ''", { cwd, encoding: "utf-8", timeout: 30000, maxBuffer: 512 * 1024 });
345
+ const warns = out.trim().split("\n").filter((l) => l.includes("warning") || l.includes("error"));
346
+ if (warns.length > 0) results.push(`[ESLint] ${warns.length} 个问题`);
347
+ results.push(...warns.slice(0, 10));
348
+ } catch (_) {}
349
+ return results.length > 0 ? results.join("\n") : "[无诊断] 未发现 lint/类型错误, 或项目无 tsc/eslint 配置";
350
+ },
351
+ };
352
+
353
+ const skillTool = {
354
+ name: "skill",
355
+ description: "执行一个技能模块: 从 Skills/ 目录加载预定义的技能脚本. 技能是一组预置指令",
356
+ parameters: {
357
+ name: { type: "string", required: true, description: "技能名, 如 'code_review' / 'refactor' / 'test_gen'" },
358
+ },
359
+ execute: async ({ name }) => {
360
+ const skillDir = path.join(process.cwd(), "Skills");
361
+ if (!fs.existsSync(skillDir)) return `[无技能目录] ${skillDir} 不存在, 请创建 Skills/ 目录并放入 .js 脚本`;
362
+ const skillPath = path.join(skillDir, name + ".js");
363
+ if (!fs.existsSync(skillPath)) {
364
+ const available = fs.readdirSync(skillDir).filter((f) => f.endsWith(".js")).map((f) => f.replace(".js", ""));
365
+ return `[无此技能] ${name}\n可用技能: ${available.length > 0 ? available.join(", ") : "(空)"}`;
366
+ }
367
+ try {
368
+ const mod = require(skillPath);
369
+ return `[技能: ${name}]\n${mod.description || "(无描述)"}\n指令: ${mod.instructions || "(无指令)"}`;
370
+ } catch (e) {
371
+ return `[技能加载失败] ${e.message}`;
372
+ }
373
+ },
374
+ };
375
+
376
+ const forgetConversationTool = {
377
+ name: "forget_conversation",
378
+ description: "主动遗忘当前对话历史(清空上下文), 保留记忆条目. 上下文过长时主动调用以节省token",
379
+ parameters: {
380
+ keepSummary: { type: "boolean", required: false, description: "是否先自动生成摘要并保存到记忆, 默认true" },
381
+ },
382
+ execute: () => { return "forget_conversation must be injected"; },
383
+ };
384
+
385
+ const restartSessionTool = {
386
+ name: "restart_session",
387
+ description: "完全重置对话: 清空历史 + 清空记忆, 开始全新会话. 相当于重启对话",
388
+ parameters: {},
389
+ execute: () => { return "restart_session must be injected"; },
390
+ };
391
+
392
+ module.exports = {
393
+ todo_write: todoWriteTool,
394
+ search_replace: searchReplaceTool,
395
+ glob: globTool,
396
+ grep: grepTool,
397
+ read: readTool,
398
+ write: writeTool,
399
+ ls: lsTool,
400
+ tree: treeTool,
401
+ web_search: webSearchTool,
402
+ check_command_status: checkStatusTool,
403
+ open_preview: openPreviewTool,
404
+ get_diagnostics: getDiagnosticsTool,
405
+ skill: skillTool,
406
+ forget_conversation: forgetConversationTool,
407
+ restart_session: restartSessionTool,
408
+ };
@@ -0,0 +1,201 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { execSync } = require("child_process");
4
+
5
+ const readFileTool = {
6
+ name: "read_file",
7
+ description: "读取指定路径的文件内容",
8
+ parameters: {
9
+ filePath: { type: "string", required: true, description: "文件路径" },
10
+ },
11
+ execute: async ({ filePath }) => {
12
+ if (!fs.existsSync(filePath)) return `[不存在] ${filePath}`;
13
+ return fs.readFileSync(filePath, "utf-8");
14
+ },
15
+ };
16
+
17
+ const writeFileTool = {
18
+ name: "write_file",
19
+ description: "创建或覆盖文件(自动创建父目录)",
20
+ dangerous: true,
21
+ parameters: {
22
+ filePath: { type: "string", required: true, description: "文件路径" },
23
+ content: { type: "string", required: true, description: "写入内容" },
24
+ },
25
+ execute: async ({ filePath, content }) => {
26
+ const dir = path.dirname(filePath);
27
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
28
+ fs.writeFileSync(filePath, content, "utf-8");
29
+ return `[OK] 已写入 ${filePath} (${content.length} 字符)`;
30
+ },
31
+ };
32
+
33
+ const deleteFileTool = {
34
+ name: "delete_file",
35
+ description: "删除指定文件或空目录",
36
+ dangerous: true,
37
+ parameters: {
38
+ filePath: { type: "string", required: true, description: "要删除的文件或目录路径" },
39
+ },
40
+ execute: async ({ filePath }) => {
41
+ if (!fs.existsSync(filePath)) return `[不存在] ${filePath}`;
42
+ const stat = fs.statSync(filePath);
43
+ if (stat.isDirectory()) {
44
+ const entries = fs.readdirSync(filePath);
45
+ if (entries.length > 0) return `[失败] 目录非空, 包含 ${entries.length} 个条目, 拒绝删除`;
46
+ fs.rmdirSync(filePath);
47
+ return `[OK] 已删除空目录 ${filePath}`;
48
+ }
49
+ fs.unlinkSync(filePath);
50
+ return `[OK] 已删除 ${filePath}`;
51
+ },
52
+ };
53
+
54
+ const moveFileTool = {
55
+ name: "move_file",
56
+ description: "移动/重命名文件或目录",
57
+ dangerous: true,
58
+ parameters: {
59
+ source: { type: "string", required: true, description: "源路径" },
60
+ target: { type: "string", required: true, description: "目标路径" },
61
+ },
62
+ execute: async ({ source, target }) => {
63
+ if (!fs.existsSync(source)) return `[不存在] ${source}`;
64
+ const targetDir = path.dirname(target);
65
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
66
+ fs.renameSync(source, target);
67
+ return `[OK] ${source} -> ${target}`;
68
+ },
69
+ };
70
+
71
+ const copyFileTool = {
72
+ name: "copy_file",
73
+ description: "复制文件到目标路径",
74
+ dangerous: true,
75
+ parameters: {
76
+ source: { type: "string", required: true, description: "源文件路径" },
77
+ target: { type: "string", required: true, description: "目标路径" },
78
+ },
79
+ execute: async ({ source, target }) => {
80
+ if (!fs.existsSync(source)) return `[不存在] ${source}`;
81
+ const targetDir = path.dirname(target);
82
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
83
+ fs.copyFileSync(source, target);
84
+ return `[OK] 已复制 ${source} -> ${target}`;
85
+ },
86
+ };
87
+
88
+ const listDirTool = {
89
+ name: "list_dir",
90
+ description: "列出目录内容(支持递归)",
91
+ parameters: {
92
+ dirPath: { type: "string", required: true, description: "目录路径" },
93
+ recursive: { type: "boolean", required: false, description: "是否递归, 默认false" },
94
+ },
95
+ execute: async ({ dirPath, recursive }) => {
96
+ if (!fs.existsSync(dirPath)) return `[不存在] ${dirPath}`;
97
+ if (!recursive) {
98
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
99
+ return entries.map((e) => {
100
+ const mark = e.isDirectory() ? "[DIR]" : "[FILE]";
101
+ const size = e.isFile() ? ` ${fs.statSync(path.join(dirPath, e.name)).size}B` : "";
102
+ return `${mark} ${e.name}${size}`;
103
+ }).join("\n");
104
+ }
105
+ const lines = [];
106
+ function walk(dir, prefix) {
107
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
108
+ for (const e of entries) {
109
+ const fp = path.join(dir, e.name);
110
+ if (e.isDirectory()) { lines.push(prefix + "[DIR] " + e.name + "/"); walk(fp, prefix + " "); }
111
+ else lines.push(prefix + "[FILE] " + e.name);
112
+ }
113
+ }
114
+ walk(dirPath, "");
115
+ return lines.join("\n");
116
+ },
117
+ };
118
+
119
+ const searchInFilesTool = {
120
+ name: "search_in_files",
121
+ description: "grep: 在目录中搜索内容, 返回 文件:行号:匹配行",
122
+ parameters: {
123
+ pattern: { type: "string", required: true, description: "搜索模式(纯文本或正则)" },
124
+ dirPath: { type: "string", required: true, description: "搜索目录" },
125
+ fileGlob: { type: "string", required: false, description: "文件过滤, 如 '*.js', 默认所有" },
126
+ maxResults: { type: "number", required: false, description: "最大结果数, 默认50" },
127
+ ignoreCase: { type: "boolean", required: false, description: "忽略大小写, 默认true" },
128
+ },
129
+ execute: async ({ pattern, dirPath, fileGlob, maxResults, ignoreCase }) => {
130
+ if (!fs.existsSync(dirPath)) return `[不存在] ${dirPath}`;
131
+ const max = maxResults || 50;
132
+ const ic = ignoreCase !== false;
133
+ try {
134
+ const cmd = `grep -rn ${ic ? "-i" : ""} --include="${fileGlob || "*"}" -m ${max} "${pattern.replace(/"/g, '\\"')}" "${dirPath}"`;
135
+ const output = execSync(cmd, { cwd: dirPath, encoding: "utf-8", timeout: 10000, maxBuffer: 1024 * 1024 });
136
+ const lines = output.trim().split("\n").slice(0, max);
137
+ return lines.length > 0 ? lines.join("\n") : `[无匹配] "${pattern}" 在 ${dirPath}`;
138
+ } catch (e) {
139
+ if (e.stdout) return e.stdout.trim().split("\n").slice(0, max).join("\n");
140
+ return `[无匹配] "${pattern}" 在 ${dirPath}`;
141
+ }
142
+ },
143
+ };
144
+
145
+ const findFilesTool = {
146
+ name: "find_files",
147
+ description: "按文件名模式查找文件(glob), 如 '*.js' 或 'test*.ts'",
148
+ parameters: {
149
+ pattern: { type: "string", required: true, description: "glob模式, 如 '*.js' 或 '**/*.test.js'" },
150
+ dirPath: { type: "string", required: true, description: "搜索起始目录" },
151
+ maxResults: { type: "number", required: false, description: "最大结果数, 默认100" },
152
+ },
153
+ execute: async ({ pattern, dirPath, maxResults }) => {
154
+ if (!fs.existsSync(dirPath)) return `[不存在] ${dirPath}`;
155
+ const max = maxResults || 100;
156
+ try {
157
+ const cmd = `find "${dirPath}" -name "${pattern}" -not -path '*/node_modules/*' -not -path '*/.git/*' | head -n ${max}`;
158
+ const output = execSync(cmd, { encoding: "utf-8", timeout: 5000, maxBuffer: 1024 * 1024 });
159
+ return output.trim() || `[无匹配] ${pattern} 在 ${dirPath}`;
160
+ } catch (e) {
161
+ if (e.stdout) return e.stdout.trim() || `[无匹配] ${pattern}`;
162
+ return `[无匹配] ${pattern} 在 ${dirPath}`;
163
+ }
164
+ },
165
+ };
166
+
167
+ const fileInfoTool = {
168
+ name: "file_info",
169
+ description: "获取文件元信息: 大小/修改时间/权限/行数",
170
+ parameters: {
171
+ filePath: { type: "string", required: true, description: "文件路径" },
172
+ },
173
+ execute: async ({ filePath }) => {
174
+ if (!fs.existsSync(filePath)) return `[不存在] ${filePath}`;
175
+ const stat = fs.statSync(filePath);
176
+ const ext = path.extname(filePath);
177
+ let lines = "?";
178
+ try { lines = fs.readFileSync(filePath, "utf-8").split("\n").length; } catch (_) {}
179
+ return [
180
+ `路径: ${filePath}`,
181
+ `类型: ${stat.isDirectory() ? "目录" : "文件"} (${ext || "无扩展名"})`,
182
+ `大小: ${stat.size} B (${(stat.size / 1024).toFixed(1)} KB)`,
183
+ `行数: ${lines}`,
184
+ `权限: ${stat.mode.toString(8).slice(-3)}`,
185
+ `修改: ${stat.mtime.toISOString()}`,
186
+ `创建: ${stat.birthtime.toISOString()}`,
187
+ ].join("\n");
188
+ },
189
+ };
190
+
191
+ module.exports = {
192
+ read_file: readFileTool,
193
+ write_file: writeFileTool,
194
+ delete_file: deleteFileTool,
195
+ move_file: moveFileTool,
196
+ copy_file: copyFileTool,
197
+ list_dir: listDirTool,
198
+ search_in_files: searchInFilesTool,
199
+ find_files: findFilesTool,
200
+ file_info: fileInfoTool,
201
+ };