@zqagent/core 0.1.30 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CONTRIBUTING.md +65 -0
  2. package/LICENSE +190 -0
  3. package/README.md +42 -165
  4. package/README.oss.en.md +161 -0
  5. package/README.oss.md +165 -0
  6. package/README.oss.zh-TW.md +165 -0
  7. package/SECURITY.md +51 -0
  8. package/dist/{ToolRegistry-BmvPxgMr.d.cts → ToolRegistry-C3pTFiaJ.d.cts} +13 -4
  9. package/dist/{ToolRegistry-BmvPxgMr.d.ts → ToolRegistry-C3pTFiaJ.d.ts} +13 -4
  10. package/dist/cli/index.cjs +17799 -2062
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +16729 -985
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/{explore-OI6htHN1.d.cts → explore-D4lCKXbA.d.ts} +160 -19
  15. package/dist/{explore-Pmhyk4gv.d.ts → explore-Dj1j_GyN.d.cts} +160 -19
  16. package/dist/index.cjs +3626 -453
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +337 -27
  19. package/dist/index.d.ts +337 -27
  20. package/dist/index.js +3617 -452
  21. package/dist/index.js.map +1 -1
  22. package/dist/server/index.cjs +14029 -1482
  23. package/dist/server/index.cjs.map +1 -1
  24. package/dist/server/index.js +14036 -1482
  25. package/dist/server/index.js.map +1 -1
  26. package/dist/tools/ask.cjs +14 -0
  27. package/dist/tools/ask.cjs.map +1 -1
  28. package/dist/tools/ask.d.cts +8 -2
  29. package/dist/tools/ask.d.ts +8 -2
  30. package/dist/tools/ask.js +13 -0
  31. package/dist/tools/ask.js.map +1 -1
  32. package/dist/tools/diff.cjs +59 -30
  33. package/dist/tools/diff.cjs.map +1 -1
  34. package/dist/tools/diff.d.cts +1 -1
  35. package/dist/tools/diff.d.ts +1 -1
  36. package/dist/tools/diff.js +59 -30
  37. package/dist/tools/diff.js.map +1 -1
  38. package/dist/tools/directories.d.cts +1 -1
  39. package/dist/tools/directories.d.ts +1 -1
  40. package/dist/tools/explore.cjs +1050 -309
  41. package/dist/tools/explore.cjs.map +1 -1
  42. package/dist/tools/explore.d.cts +2 -2
  43. package/dist/tools/explore.d.ts +2 -2
  44. package/dist/tools/explore.js +1057 -309
  45. package/dist/tools/explore.js.map +1 -1
  46. package/dist/tools/files.cjs +58 -43
  47. package/dist/tools/files.cjs.map +1 -1
  48. package/dist/tools/files.d.cts +10 -4
  49. package/dist/tools/files.d.ts +10 -4
  50. package/dist/tools/files.js +56 -42
  51. package/dist/tools/files.js.map +1 -1
  52. package/dist/tools/git.cjs +816 -63
  53. package/dist/tools/git.cjs.map +1 -1
  54. package/dist/tools/git.d.cts +200 -2
  55. package/dist/tools/git.d.ts +200 -2
  56. package/dist/tools/git.js +788 -62
  57. package/dist/tools/git.js.map +1 -1
  58. package/dist/tools/media.cjs +1191 -0
  59. package/dist/tools/media.cjs.map +1 -0
  60. package/dist/tools/media.d.cts +31 -0
  61. package/dist/tools/media.d.ts +31 -0
  62. package/dist/tools/media.js +1159 -0
  63. package/dist/tools/media.js.map +1 -0
  64. package/dist/tools/plan.d.cts +1 -1
  65. package/dist/tools/plan.d.ts +1 -1
  66. package/dist/tools/search.cjs +513 -33
  67. package/dist/tools/search.cjs.map +1 -1
  68. package/dist/tools/search.d.cts +12 -1
  69. package/dist/tools/search.d.ts +12 -1
  70. package/dist/tools/search.js +508 -33
  71. package/dist/tools/search.js.map +1 -1
  72. package/dist/tools/shell.cjs +631 -41
  73. package/dist/tools/shell.cjs.map +1 -1
  74. package/dist/tools/shell.d.cts +37 -4
  75. package/dist/tools/shell.d.ts +37 -4
  76. package/dist/tools/shell.js +626 -40
  77. package/dist/tools/shell.js.map +1 -1
  78. package/dist/tools/todo.d.cts +1 -1
  79. package/dist/tools/todo.d.ts +1 -1
  80. package/dist/tools/web.cjs +51 -18
  81. package/dist/tools/web.cjs.map +1 -1
  82. package/dist/tools/web.d.cts +1 -1
  83. package/dist/tools/web.d.ts +1 -1
  84. package/dist/tools/web.js +51 -18
  85. package/dist/tools/web.js.map +1 -1
  86. package/package.json +43 -8
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/tools/files.ts
31
31
  var files_exports = {};
32
32
  __export(files_exports, {
33
- registerFileTools: () => registerFileTools
33
+ registerFileTools: () => registerFileTools,
34
+ registerReadOnlyFileTools: () => registerReadOnlyFileTools
34
35
  });
35
36
  module.exports = __toCommonJS(files_exports);
36
37
  var fs2 = __toESM(require("fs"), 1);
@@ -39,17 +40,23 @@ var path2 = __toESM(require("path"), 1);
39
40
  // src/tools/pathSafety.ts
40
41
  var fs = __toESM(require("fs"), 1);
41
42
  var path = __toESM(require("path"), 1);
42
- function isWithinRoot(workspace, target) {
43
+ function isInsideRelative(rel) {
44
+ return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
45
+ }
46
+ function isFilesystemRoot(p) {
47
+ return path.parse(p).root === p;
48
+ }
49
+ function resolveWithinRoot(workspace, target) {
50
+ const abs = path.resolve(workspace, target);
43
51
  let rootReal;
44
52
  try {
45
53
  rootReal = fs.realpathSync(workspace);
46
54
  } catch {
47
55
  const rootAbs = path.resolve(workspace);
48
- const abs2 = path.resolve(workspace, target);
49
- const rel2 = path.relative(rootAbs, abs2);
50
- return !rel2.startsWith("..") && !path.isAbsolute(rel2);
56
+ if (!isInsideRelative(path.relative(rootAbs, abs))) return null;
57
+ return { root: rootAbs, real: abs };
51
58
  }
52
- const abs = path.resolve(workspace, target);
59
+ if (isFilesystemRoot(rootReal)) return null;
53
60
  let real;
54
61
  try {
55
62
  real = fs.realpathSync(abs);
@@ -63,17 +70,17 @@ function isWithinRoot(workspace, target) {
63
70
  real = path.join(fs.realpathSync(probe), ...tail);
64
71
  break;
65
72
  } catch {
66
- return false;
73
+ return null;
67
74
  }
68
75
  }
69
76
  tail.unshift(path.basename(probe));
70
77
  const parent = path.dirname(probe);
71
- if (parent === probe) return false;
78
+ if (parent === probe) return null;
72
79
  probe = parent;
73
80
  }
74
81
  }
75
- const rel = path.relative(rootReal, real);
76
- return !rel.startsWith("..") && !path.isAbsolute(rel);
82
+ if (!isInsideRelative(path.relative(rootReal, real))) return null;
83
+ return { root: rootReal, real };
77
84
  }
78
85
 
79
86
  // src/tools/files.ts
@@ -85,10 +92,11 @@ function makeReadFile(workspace) {
85
92
  const filePath = String(args.path ?? "");
86
93
  const startLine = Math.max(1, Number(args.start_line ?? 1));
87
94
  const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
88
- const target = path2.resolve(root, filePath);
89
- if (!isWithinRoot(root, target)) {
95
+ const resolved = resolveWithinRoot(root, filePath);
96
+ if (!resolved) {
90
97
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
91
98
  }
99
+ const target = resolved.real;
92
100
  if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
93
101
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
94
102
  }
@@ -110,7 +118,7 @@ function makeReadFile(workspace) {
110
118
  }
111
119
  const end = startLine + selected.length - 1;
112
120
  const result = {
113
- path: path2.relative(root, target),
121
+ path: path2.relative(resolved.root, target),
114
122
  start_line: startLine,
115
123
  end_line: end,
116
124
  total_lines: total,
@@ -128,10 +136,11 @@ function makeWriteFile(workspace) {
128
136
  const filePath = String(args.path ?? "");
129
137
  const content = String(args.content ?? "");
130
138
  const overwrite = args.overwrite !== false;
131
- const target = path2.resolve(root, filePath);
132
- if (!isWithinRoot(root, target)) {
139
+ const resolved = resolveWithinRoot(root, filePath);
140
+ if (!resolved) {
133
141
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
134
142
  }
143
+ const target = resolved.real;
135
144
  if (fs2.existsSync(target) && fs2.statSync(target).isDirectory()) {
136
145
  return { error: "\u8DEF\u5F84\u662F\u76EE\u5F55\uFF0C\u4E0D\u662F\u6587\u4EF6" };
137
146
  }
@@ -144,7 +153,7 @@ function makeWriteFile(workspace) {
144
153
  } catch (exc) {
145
154
  return { error: `\u5199\u5165\u5931\u8D25: ${String(exc)}` };
146
155
  }
147
- return path2.relative(root, target);
156
+ return path2.relative(resolved.root, target);
148
157
  };
149
158
  }
150
159
  function makeListFiles(workspace) {
@@ -152,8 +161,9 @@ function makeListFiles(workspace) {
152
161
  return function list_files(args) {
153
162
  const dirPath = String(args.path ?? ".");
154
163
  const maxResults = Math.min(Number(args.max_results ?? 100), 100);
155
- const target = path2.resolve(root, dirPath);
156
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
164
+ const resolved = resolveWithinRoot(root, dirPath);
165
+ if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
166
+ const target = resolved.real;
157
167
  if (!fs2.existsSync(target) || !fs2.statSync(target).isDirectory()) {
158
168
  return { error: `\u4E0D\u662F\u76EE\u5F55: ${dirPath}` };
159
169
  }
@@ -167,7 +177,7 @@ function makeListFiles(workspace) {
167
177
  results.push(e.isDirectory() ? `${e.name}/` : e.name);
168
178
  }
169
179
  results.sort();
170
- return { path: path2.relative(root, target), entries: results.slice(0, maxResults) };
180
+ return { path: path2.relative(resolved.root, target), entries: results.slice(0, maxResults) };
171
181
  } catch (exc) {
172
182
  return { error: `\u5217\u51FA\u6587\u4EF6\u5931\u8D25: ${String(exc)}` };
173
183
  }
@@ -181,8 +191,9 @@ function makeReplaceInFile(workspace) {
181
191
  const newStr = String(args.new ?? "");
182
192
  const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));
183
193
  if (!oldStr) return { error: "old \u4E0D\u80FD\u4E3A\u7A7A" };
184
- const target = path2.resolve(root, filePath);
185
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
194
+ const resolved = resolveWithinRoot(root, filePath);
195
+ if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
196
+ const target = resolved.real;
186
197
  if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
187
198
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
188
199
  }
@@ -195,7 +206,7 @@ function makeReplaceInFile(workspace) {
195
206
  const updated = content.replaceAll(oldStr, newStr);
196
207
  fs2.writeFileSync(target, updated, "utf-8");
197
208
  return {
198
- path: path2.relative(root, target),
209
+ path: path2.relative(resolved.root, target),
199
210
  replacements: count,
200
211
  chars_before: content.length,
201
212
  chars_after: updated.length
@@ -205,7 +216,7 @@ function makeReplaceInFile(workspace) {
205
216
  }
206
217
  };
207
218
  }
208
- function registerFileTools(registry, workspace) {
219
+ function registerReadOnlyFileTools(registry, workspace) {
209
220
  const readFile = makeReadFile(workspace);
210
221
  registry.register(
211
222
  (args) => readFile(args),
@@ -227,6 +238,28 @@ function registerFileTools(registry, workspace) {
227
238
  }
228
239
  }
229
240
  );
241
+ const listFiles = makeListFiles(workspace);
242
+ registry.register(
243
+ (args) => listFiles(args),
244
+ { category: "filesystem", riskLevel: "low", capabilities: ["list_files"], requiresApproval: false },
245
+ {
246
+ type: "function",
247
+ function: {
248
+ name: "list_files",
249
+ description: "\u5217\u51FA\u76EE\u5F55\u4E0B\u7684\u6587\u4EF6\u548C\u5B50\u76EE\u5F55\u3002\u8DF3\u8FC7\u9690\u85CF\u6587\u4EF6\u548C node_modules \u7B49\u76EE\u5F55\u3002",
250
+ parameters: {
251
+ type: "object",
252
+ properties: {
253
+ path: { type: "string", description: "\u76EE\u5F55\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A\uFF0C\u9ED8\u8BA4 '.')" },
254
+ max_results: { type: "integer", description: "\u6700\u5927\u7ED3\u679C\u6570 (\u9ED8\u8BA4 100)" }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ );
260
+ }
261
+ function registerFileTools(registry, workspace) {
262
+ registerReadOnlyFileTools(registry, workspace);
230
263
  const writeFile = makeWriteFile(workspace);
231
264
  registry.register(
232
265
  (args) => writeFile(args),
@@ -270,28 +303,10 @@ function registerFileTools(registry, workspace) {
270
303
  }
271
304
  }
272
305
  );
273
- const listFiles = makeListFiles(workspace);
274
- registry.register(
275
- (args) => listFiles(args),
276
- { category: "filesystem", riskLevel: "low", capabilities: ["list_files"], requiresApproval: false },
277
- {
278
- type: "function",
279
- function: {
280
- name: "list_files",
281
- description: "\u5217\u51FA\u76EE\u5F55\u4E0B\u7684\u6587\u4EF6\u548C\u5B50\u76EE\u5F55\u3002\u8DF3\u8FC7\u9690\u85CF\u6587\u4EF6\u548C node_modules \u7B49\u76EE\u5F55\u3002",
282
- parameters: {
283
- type: "object",
284
- properties: {
285
- path: { type: "string", description: "\u76EE\u5F55\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A\uFF0C\u9ED8\u8BA4 '.')" },
286
- max_results: { type: "integer", description: "\u6700\u5927\u7ED3\u679C\u6570 (\u9ED8\u8BA4 100)" }
287
- }
288
- }
289
- }
290
- }
291
- );
292
306
  }
293
307
  // Annotate the CommonJS export names for ESM import in node:
294
308
  0 && (module.exports = {
295
- registerFileTools
309
+ registerFileTools,
310
+ registerReadOnlyFileTools
296
311
  });
297
312
  //# sourceMappingURL=files.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/files.ts","../../src/tools/pathSafety.ts"],"sourcesContent":["/**\n * 文件系统工具\n *\n * 提供 read_file / write_file / list_files 三个文件工具的实现,\n * 并注册到 ToolRegistry。所有工具都做了路径范围检查(限制在\n * 工作区内,防止通过 ../ 逃逸)。实现为工厂函数(makeXxx),\n * 以便为每个引擎实例绑定各自的工作区根目录。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { ToolRegistry, RiskClass, type ToolMetadata } from \"./ToolRegistry.js\";\nimport { isWithinRoot } from \"./pathSafety.js\";\n\n/** read_file 默认最大读取行数 */\nconst DEFAULT_MAX_LINES = 2000;\n/** 单行最大字符数(超长行截断,防止输出爆炸) */\nconst MAX_LINE_CHARS = 500;\n\n/**\n * read_file — 行号标注版 (对齐 Python 版)\n *\n * 返回带行号的内容,格式 ' 12\\ttext',便于引用为 path:line;\n * 大文件通过 start_line / max_lines 窗口化阅读。\n */\nfunction makeReadFile(workspace: string) {\n const root = path.resolve(workspace);\n\n return function read_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const startLine = Math.max(1, Number(args.start_line ?? 1));\n const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);\n\n // 路径范围检查 + 存在性/文件类型检查\n const target = path.resolve(root, filePath);\n if (!isWithinRoot(root, target)) {\n return { error: \"路径在工作区之外\" };\n }\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { error: `不是文件: ${filePath}` };\n }\n\n // 按窗口截取行,超长行截断并标注\n const selected: string[] = [];\n let total = 0;\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n const lines = content.split(\"\\n\");\n total = lines.length;\n for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {\n let text = lines[i];\n if (text.length > MAX_LINE_CHARS) {\n text = text.slice(0, MAX_LINE_CHARS) + \"… (line truncated)\";\n }\n selected.push(`${String(i + 1).padStart(6)}\\t${text}`);\n }\n } catch (exc) {\n return { error: `读取失败: ${String(exc)}` };\n }\n\n const end = startLine + selected.length - 1;\n const result: Record<string, unknown> = {\n path: path.relative(root, target),\n start_line: startLine,\n end_line: end,\n total_lines: total,\n content: selected.join(\"\\n\"),\n };\n // 文件未读完时提示继续阅读的入口\n if (end < total) {\n result.note = `显示第 ${startLine}-${end} 行,共 ${total} 行;调用 read_file(start_line=${end + 1}) 继续阅读`;\n }\n return result;\n };\n}\n\n/**\n * write_file — 路径范围检查 + UTF-8 写入\n *\n * 自动创建父目录;overwrite=false 时拒绝覆盖已存在文件。\n */\nfunction makeWriteFile(workspace: string) {\n const root = path.resolve(workspace);\n\n return function write_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const content = String(args.content ?? \"\");\n const overwrite = args.overwrite !== false;\n\n // 路径范围 + 类型检查(不能写目录、默认不覆盖已有文件)\n const target = path.resolve(root, filePath);\n if (!isWithinRoot(root, target)) {\n return { error: \"路径在工作区之外\" };\n }\n if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {\n return { error: \"路径是目录,不是文件\" };\n }\n if (fs.existsSync(target) && !overwrite) {\n return { error: \"文件已存在 (overwrite=false)\" };\n }\n\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, content, \"utf-8\");\n } catch (exc) {\n return { error: `写入失败: ${String(exc)}` };\n }\n\n return path.relative(root, target);\n };\n}\n\n/**\n * list_files — 列出目录内容\n *\n * 跳过隐藏文件(. 开头)及 node_modules / __pycache__ / dist 等\n * 无意义目录;目录项以 / 后缀标识;结果按名称排序。\n */\nfunction makeListFiles(workspace: string) {\n const root = path.resolve(workspace);\n\n return function list_files(args: Record<string, unknown>) {\n const dirPath = String(args.path ?? \".\");\n const maxResults = Math.min(Number(args.max_results ?? 100), 100);\n const target = path.resolve(root, dirPath);\n if (!isWithinRoot(root, target)) return { error: \"路径在工作区之外\" };\n if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {\n return { error: `不是目录: ${dirPath}` };\n }\n\n try {\n const entries = fs.readdirSync(target, { withFileTypes: true });\n const results: string[] = [];\n for (const e of entries) {\n if (results.length >= maxResults) break;\n if (e.name.startsWith(\".\")) continue;\n if (e.name === \"node_modules\" || e.name === \"__pycache__\" || e.name === \"dist\") continue;\n results.push(e.isDirectory() ? `${e.name}/` : e.name);\n }\n results.sort();\n return { path: path.relative(root, target), entries: results.slice(0, maxResults) };\n } catch (exc) {\n return { error: `列出文件失败: ${String(exc)}` };\n }\n };\n}\n\n/** replace_in_file — 精确文本替换 (对齐 Python 版 aisuite) */\nfunction makeReplaceInFile(workspace: string) {\n const root = path.resolve(workspace);\n\n return function replace_in_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const oldStr = String(args.old ?? \"\");\n const newStr = String(args.new ?? \"\");\n const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));\n\n if (!oldStr) return { error: \"old 不能为空\" };\n\n const target = path.resolve(root, filePath);\n if (!isWithinRoot(root, target)) return { error: \"路径在工作区之外\" };\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { error: `不是文件: ${filePath}` };\n }\n\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n const count = content.split(oldStr).length - 1;\n\n if (count !== expectedReplacements) {\n return { error: `期望 ${expectedReplacements} 处替换,实际找到 ${count} 处: ${filePath}` };\n }\n\n const updated = content.replaceAll(oldStr, newStr);\n fs.writeFileSync(target, updated, \"utf-8\");\n return {\n path: path.relative(root, target),\n replacements: count,\n chars_before: content.length,\n chars_after: updated.length,\n };\n } catch (exc) {\n return { error: `替换失败: ${String(exc)}` };\n }\n };\n}\n\n/**\n * 注册文件工具到 ToolRegistry\n *\n * read_file / list_files 为低风险(无需审批);write_file 为\n * 中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。\n */\nexport function registerFileTools(registry: ToolRegistry, workspace: string): void {\n const readFile = makeReadFile(workspace);\n registry.register(\n (args) => readFile(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"read_file\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"read_file\",\n description: \"读取文本文件,返回带行号的内容 (格式: ' 12\\\\\\\\ttext'),以便引用为 path:line。大文件窗口化:传递 start_line 从上一次读取的位置继续。只读。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n start_line: { type: \"integer\", description: \"起始行号 (从1开始,默认1)\" },\n max_lines: { type: \"integer\", description: `最大行数 (默认 ${DEFAULT_MAX_LINES})` },\n },\n required: [\"path\"],\n },\n },\n },\n );\n\n const writeFile = makeWriteFile(workspace);\n registry.register(\n (args) => writeFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"write_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"write_file\",\n description: \"写入一个 UTF-8 文本文件。用于创建新文件或完全重写现有文件。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n content: { type: \"string\", description: \"文件内容 (UTF-8)\" },\n overwrite: { type: \"boolean\", description: \"是否覆盖已存在的文件 (默认 true)\" },\n },\n required: [\"path\", \"content\"],\n },\n },\n },\n );\n\n const replaceFile = makeReplaceInFile(workspace);\n registry.register(\n (args) => replaceFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"edit_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"replace_in_file\",\n description: \"在文件中精确替换一段文本。先按预期次数精确匹配 old 字符串,然后替换为 new。用于小范围精确编辑。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n old: { type: \"string\", description: \"要被替换的精确文本\" },\n new: { type: \"string\", description: \"替换后的新文本\" },\n expected_replacements: { type: \"integer\", description: \"期望的替换次数 (默认 1)\" },\n },\n required: [\"path\", \"old\", \"new\"],\n },\n },\n },\n );\n\n const listFiles = makeListFiles(workspace);\n registry.register(\n (args) => listFiles(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"list_files\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"list_files\",\n description: \"列出目录下的文件和子目录。跳过隐藏文件和 node_modules 等目录。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"目录路径 (相对于工作区,默认 '.')\" },\n max_results: { type: \"integer\", description: \"最大结果数 (默认 100)\" },\n },\n },\n },\n },\n );\n}\n","/**\n * 路径安全辅助函数\n *\n * 判断目标路径是否真正位于工作区根目录之内。\n *\n * 仅做字符串比较(path.relative)无法防御符号链接逃逸:\n * 工作区内若存在指向外部的 symlink,`workspace/link/secret.txt`\n * 的字符串前缀合法,但实际读写发生在链接目标(工作区外)。\n *\n * 因此这里用 fs.realpathSync 解析符号链接后再与根目录的 realpath\n * 比较。目标尚不存在(如 write_file 新建文件)时,向上回溯到最近\n * 已存在的祖先目录解析,再拼回剩余路径段。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/**\n * 判断 target(相对 workspace 的路径)是否安全地位于 workspace 内。\n *\n * - 解析符号链接:路径中任何 symlink 组件都会先 realpath 再校验;\n * broken symlink(存在但无法解析)一律拒绝。\n * - 目标不存在时(写新文件):回溯到最近存在的祖先做 realpath,\n * 再拼接剩余段,仍能捕获祖先链中的 symlink 逃逸。\n */\nexport function isWithinRoot(workspace: string, target: string): boolean {\n let rootReal: string;\n try {\n rootReal = fs.realpathSync(workspace);\n } catch {\n // 根目录不存在(如权限引擎尚未初始化工作区):\n // 没有实际目录结构即无 symlink 逃逸面,退化为纯字符串路径比较。\n const rootAbs = path.resolve(workspace);\n const abs = path.resolve(workspace, target);\n const rel = path.relative(rootAbs, abs);\n return !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n }\n\n const abs = path.resolve(workspace, target);\n let real: string;\n try {\n real = fs.realpathSync(abs);\n } catch {\n // abs 不存在:向上回溯找最近存在的祖先\n let probe = abs;\n const tail: string[] = [];\n for (;;) {\n // 组件存在(含 broken symlink)但 realpath 失败 → 无法解析,拒绝\n const exists = fs.existsSync(probe) || fs.lstatSync(probe, { throwIfNoEntry: false }) !== undefined;\n if (exists) {\n try {\n real = path.join(fs.realpathSync(probe), ...tail);\n break;\n } catch {\n return false;\n }\n }\n tail.unshift(path.basename(probe));\n const parent = path.dirname(probe);\n if (parent === probe) return false; // 已到文件系统根\n probe = parent;\n }\n }\n\n const rel = path.relative(rootReal, real);\n return !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n}\n"],"mappings":"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","names":["fs","path","abs","rel"]}
1
+ {"version":3,"sources":["../../src/tools/files.ts","../../src/tools/pathSafety.ts"],"sourcesContent":["/**\n * 文件系统工具\n *\n * 提供 read_file / write_file / list_files 三个文件工具的实现,\n * 并注册到 ToolRegistry。所有工具都做了路径范围检查(限制在\n * 工作区内,防止通过 ../ 逃逸)。实现为工厂函数(makeXxx),\n * 以便为每个引擎实例绑定各自的工作区根目录。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { ToolRegistry, RiskClass, type ToolMetadata } from \"./ToolRegistry.js\";\nimport { resolveWithinRoot } from \"./pathSafety.js\";\n\n/** read_file 默认最大读取行数 */\nconst DEFAULT_MAX_LINES = 2000;\n/** 单行最大字符数(超长行截断,防止输出爆炸) */\nconst MAX_LINE_CHARS = 500;\n\n/**\n * read_file — 行号标注版 (对齐 Python 版)\n *\n * 返回带行号的内容,格式 ' 12\\ttext',便于引用为 path:line;\n * 大文件通过 start_line / max_lines 窗口化阅读。\n */\nfunction makeReadFile(workspace: string) {\n const root = path.resolve(workspace);\n\n return function read_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const startLine = Math.max(1, Number(args.start_line ?? 1));\n const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);\n\n // 路径范围检查 + 存在性/文件类型检查。\n // 用解析后的真实路径做后续 fs 操作,避免校验后 symlink 组件被替换。\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) {\n return { error: \"路径在工作区之外\" };\n }\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { error: `不是文件: ${filePath}` };\n }\n\n // 按窗口截取行,超长行截断并标注\n const selected: string[] = [];\n let total = 0;\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n const lines = content.split(\"\\n\");\n total = lines.length;\n for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {\n let text = lines[i];\n if (text.length > MAX_LINE_CHARS) {\n text = text.slice(0, MAX_LINE_CHARS) + \"… (line truncated)\";\n }\n selected.push(`${String(i + 1).padStart(6)}\\t${text}`);\n }\n } catch (exc) {\n return { error: `读取失败: ${String(exc)}` };\n }\n\n const end = startLine + selected.length - 1;\n const result: Record<string, unknown> = {\n path: path.relative(resolved.root, target),\n start_line: startLine,\n end_line: end,\n total_lines: total,\n content: selected.join(\"\\n\"),\n };\n // 文件未读完时提示继续阅读的入口\n if (end < total) {\n result.note = `显示第 ${startLine}-${end} 行,共 ${total} 行;调用 read_file(start_line=${end + 1}) 继续阅读`;\n }\n return result;\n };\n}\n\n/**\n * write_file — 路径范围检查 + UTF-8 写入\n *\n * 自动创建父目录;overwrite=false 时拒绝覆盖已存在文件。\n */\nfunction makeWriteFile(workspace: string) {\n const root = path.resolve(workspace);\n\n return function write_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const content = String(args.content ?? \"\");\n const overwrite = args.overwrite !== false;\n\n // 路径范围 + 类型检查(不能写目录、默认不覆盖已有文件)\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) {\n return { error: \"路径在工作区之外\" };\n }\n const target = resolved.real;\n if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {\n return { error: \"路径是目录,不是文件\" };\n }\n if (fs.existsSync(target) && !overwrite) {\n return { error: \"文件已存在 (overwrite=false)\" };\n }\n\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, content, \"utf-8\");\n } catch (exc) {\n return { error: `写入失败: ${String(exc)}` };\n }\n\n return path.relative(resolved.root, target);\n };\n}\n\n/**\n * list_files — 列出目录内容\n *\n * 跳过隐藏文件(. 开头)及 node_modules / __pycache__ / dist 等\n * 无意义目录;目录项以 / 后缀标识;结果按名称排序。\n */\nfunction makeListFiles(workspace: string) {\n const root = path.resolve(workspace);\n\n return function list_files(args: Record<string, unknown>) {\n const dirPath = String(args.path ?? \".\");\n const maxResults = Math.min(Number(args.max_results ?? 100), 100);\n const resolved = resolveWithinRoot(root, dirPath);\n if (!resolved) return { error: \"路径在工作区之外\" };\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {\n return { error: `不是目录: ${dirPath}` };\n }\n\n try {\n const entries = fs.readdirSync(target, { withFileTypes: true });\n const results: string[] = [];\n for (const e of entries) {\n if (results.length >= maxResults) break;\n if (e.name.startsWith(\".\")) continue;\n if (e.name === \"node_modules\" || e.name === \"__pycache__\" || e.name === \"dist\") continue;\n results.push(e.isDirectory() ? `${e.name}/` : e.name);\n }\n results.sort();\n return { path: path.relative(resolved.root, target), entries: results.slice(0, maxResults) };\n } catch (exc) {\n return { error: `列出文件失败: ${String(exc)}` };\n }\n };\n}\n\n/** replace_in_file — 精确文本替换 (对齐 Python 版 aisuite) */\nfunction makeReplaceInFile(workspace: string) {\n const root = path.resolve(workspace);\n\n return function replace_in_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const oldStr = String(args.old ?? \"\");\n const newStr = String(args.new ?? \"\");\n const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));\n\n if (!oldStr) return { error: \"old 不能为空\" };\n\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) return { error: \"路径在工作区之外\" };\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { error: `不是文件: ${filePath}` };\n }\n\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n const count = content.split(oldStr).length - 1;\n\n if (count !== expectedReplacements) {\n return { error: `期望 ${expectedReplacements} 处替换,实际找到 ${count} 处: ${filePath}` };\n }\n\n const updated = content.replaceAll(oldStr, newStr);\n fs.writeFileSync(target, updated, \"utf-8\");\n return {\n path: path.relative(resolved.root, target),\n replacements: count,\n chars_before: content.length,\n chars_after: updated.length,\n };\n } catch (exc) {\n return { error: `替换失败: ${String(exc)}` };\n }\n };\n}\n\n/**\n * 注册只读文件工具(read_file / list_files)。\n *\n * 供 explore 子代理等「只读」场景使用,使声明的 schema 与实际能力一致。\n */\nexport function registerReadOnlyFileTools(registry: ToolRegistry, workspace: string): void {\n const readFile = makeReadFile(workspace);\n registry.register(\n (args) => readFile(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"read_file\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"read_file\",\n description: \"读取文本文件,返回带行号的内容 (格式: ' 12\\\\\\\\ttext'),以便引用为 path:line。大文件窗口化:传递 start_line 从上一次读取的位置继续。只读。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n start_line: { type: \"integer\", description: \"起始行号 (从1开始,默认1)\" },\n max_lines: { type: \"integer\", description: `最大行数 (默认 ${DEFAULT_MAX_LINES})` },\n },\n required: [\"path\"],\n },\n },\n },\n );\n\n const listFiles = makeListFiles(workspace);\n registry.register(\n (args) => listFiles(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"list_files\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"list_files\",\n description: \"列出目录下的文件和子目录。跳过隐藏文件和 node_modules 等目录。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"目录路径 (相对于工作区,默认 '.')\" },\n max_results: { type: \"integer\", description: \"最大结果数 (默认 100)\" },\n },\n },\n },\n },\n );\n}\n\n/**\n * 注册文件工具到 ToolRegistry\n *\n * read_file / list_files 为低风险(无需审批);write_file / replace_in_file\n * 为中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。\n */\nexport function registerFileTools(registry: ToolRegistry, workspace: string): void {\n registerReadOnlyFileTools(registry, workspace);\n\n const writeFile = makeWriteFile(workspace);\n registry.register(\n (args) => writeFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"write_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"write_file\",\n description: \"写入一个 UTF-8 文本文件。用于创建新文件或完全重写现有文件。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n content: { type: \"string\", description: \"文件内容 (UTF-8)\" },\n overwrite: { type: \"boolean\", description: \"是否覆盖已存在的文件 (默认 true)\" },\n },\n required: [\"path\", \"content\"],\n },\n },\n },\n );\n\n const replaceFile = makeReplaceInFile(workspace);\n registry.register(\n (args) => replaceFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"edit_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"replace_in_file\",\n description: \"在文件中精确替换一段文本。先按预期次数精确匹配 old 字符串,然后替换为 new。用于小范围精确编辑。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n old: { type: \"string\", description: \"要被替换的精确文本\" },\n new: { type: \"string\", description: \"替换后的新文本\" },\n expected_replacements: { type: \"integer\", description: \"期望的替换次数 (默认 1)\" },\n },\n required: [\"path\", \"old\", \"new\"],\n },\n },\n },\n );\n}\n","/**\n * 路径安全辅助函数\n *\n * 判断目标路径是否真正位于工作区根目录之内。\n *\n * 仅做字符串比较(path.relative)无法防御符号链接逃逸:\n * 工作区内若存在指向外部的 symlink,`workspace/link/secret.txt`\n * 的字符串前缀合法,但实际读写发生在链接目标(工作区外)。\n *\n * 因此这里用 fs.realpathSync 解析符号链接后再与根目录的 realpath\n * 比较。目标尚不存在(如 write_file 新建文件)时,向上回溯到最近\n * 已存在的祖先目录解析,再拼回剩余路径段。\n *\n * 校验通过时把解析后的真实路径一并返回:调用方必须用它(而非原始\n * 相对路径)做实际 fs 操作,否则「校验后、操作前」路径组件被替换为\n * 指向外部的 symlink 仍会逃逸(TOCTOU)。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/**\n * 判断 `path.relative(root, target)` 的结果是否落在 root 之内。\n *\n * 逃逸必然恰好是 `..` 或以 `..` + 分隔符开头,不能用 `startsWith(\"..\")`:\n * 工作区里名为 `..foo` 的合法条目会被误判为逃逸而拒绝。\n * 跨盘符时(Windows)path.relative 返回绝对路径,一并拒绝。\n */\nfunction isInsideRelative(rel: string): boolean {\n return (\n !path.isAbsolute(rel) && rel !== \"..\" && !rel.startsWith(`..${path.sep}`)\n );\n}\n\n/** 是否为文件系统根(\"/\"、\"C:\\\\\") */\nfunction isFilesystemRoot(p: string): boolean {\n return path.parse(p).root === p;\n}\n\n/** 校验通过的路径:root 为工作区根的真实路径,real 为目标的真实路径 */\nexport interface ResolvedPath {\n root: string;\n real: string;\n}\n\n/**\n * 校验 target(相对 workspace 的路径)是否位于 workspace 内,并返回\n * 解析符号链接后的真实路径;不在工作区内时返回 null。\n *\n * - 解析符号链接:路径中任何 symlink 组件都会先 realpath 再校验;\n * broken symlink(存在但无法解析)一律拒绝。\n * - 目标不存在时(写新文件):回溯到最近存在的祖先做 realpath,\n * 再拼接剩余段,仍能捕获祖先链中的 symlink 逃逸。\n * - 工作区根若就是文件系统根(`WORKSPACE=/`):此时任何绝对路径都在\n * 其内,等同没有沙箱,直接判为越界。\n */\nexport function resolveWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n\n let rootReal: string;\n try {\n rootReal = fs.realpathSync(workspace);\n } catch {\n // 根目录不存在(如权限引擎尚未初始化工作区):\n // 没有实际目录结构即无 symlink 逃逸面,退化为纯字符串路径比较。\n const rootAbs = path.resolve(workspace);\n if (!isInsideRelative(path.relative(rootAbs, abs))) return null;\n return { root: rootAbs, real: abs };\n }\n if (isFilesystemRoot(rootReal)) return null;\n\n let real: string;\n try {\n real = fs.realpathSync(abs);\n } catch {\n // abs 不存在:向上回溯找最近存在的祖先\n let probe = abs;\n const tail: string[] = [];\n for (;;) {\n // 组件存在(含 broken symlink)但 realpath 失败 → 无法解析,拒绝\n const exists = fs.existsSync(probe) || fs.lstatSync(probe, { throwIfNoEntry: false }) !== undefined;\n if (exists) {\n try {\n real = path.join(fs.realpathSync(probe), ...tail);\n break;\n } catch {\n return null;\n }\n }\n tail.unshift(path.basename(probe));\n const parent = path.dirname(probe);\n if (parent === probe) return null; // 已到文件系统根\n probe = parent;\n }\n }\n\n if (!isInsideRelative(path.relative(rootReal, real))) return null;\n return { root: rootReal, real };\n}\n\n/**\n * 判断 target 是否安全地位于 workspace 内(不需要真实路径时用这个)。\n *\n * 注意:删除类操作不应据此改用 realpath 去 unlink —— 那会删掉符号链接\n * 指向的目标而非链接本身;这类操作请用 resolveEntryWithinRoot。\n */\nexport function isWithinRoot(workspace: string, target: string): boolean {\n return resolveWithinRoot(workspace, target) !== null;\n}\n\n/**\n * 解析「目录项本身」的路径:父目录取 realpath,最后一段保持原样。\n *\n * 供 unlink / rename 等作用于条目自身的操作用:父目录不可被 symlink\n * 替换,而最后一段若是符号链接,被操作的是链接本身,不会被跟随。\n */\nexport function resolveEntryWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n const parent = resolveWithinRoot(workspace, path.dirname(abs));\n if (!parent) return null;\n return { root: parent.root, real: path.join(parent.real, path.basename(abs)) };\n}\n"],"mappings":"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","names":["fs","path"]}
@@ -1,11 +1,17 @@
1
- import { T as ToolRegistry } from '../ToolRegistry-BmvPxgMr.cjs';
1
+ import { T as ToolRegistry } from '../ToolRegistry-C3pTFiaJ.cjs';
2
2
 
3
+ /**
4
+ * 注册只读文件工具(read_file / list_files)。
5
+ *
6
+ * 供 explore 子代理等「只读」场景使用,使声明的 schema 与实际能力一致。
7
+ */
8
+ declare function registerReadOnlyFileTools(registry: ToolRegistry, workspace: string): void;
3
9
  /**
4
10
  * 注册文件工具到 ToolRegistry
5
11
  *
6
- * read_file / list_files 为低风险(无需审批);write_file 为
7
- * 中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。
12
+ * read_file / list_files 为低风险(无需审批);write_file / replace_in_file
13
+ * 为中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。
8
14
  */
9
15
  declare function registerFileTools(registry: ToolRegistry, workspace: string): void;
10
16
 
11
- export { registerFileTools };
17
+ export { registerFileTools, registerReadOnlyFileTools };
@@ -1,11 +1,17 @@
1
- import { T as ToolRegistry } from '../ToolRegistry-BmvPxgMr.js';
1
+ import { T as ToolRegistry } from '../ToolRegistry-C3pTFiaJ.js';
2
2
 
3
+ /**
4
+ * 注册只读文件工具(read_file / list_files)。
5
+ *
6
+ * 供 explore 子代理等「只读」场景使用,使声明的 schema 与实际能力一致。
7
+ */
8
+ declare function registerReadOnlyFileTools(registry: ToolRegistry, workspace: string): void;
3
9
  /**
4
10
  * 注册文件工具到 ToolRegistry
5
11
  *
6
- * read_file / list_files 为低风险(无需审批);write_file 为
7
- * 中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。
12
+ * read_file / list_files 为低风险(无需审批);write_file / replace_in_file
13
+ * 为中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。
8
14
  */
9
15
  declare function registerFileTools(registry: ToolRegistry, workspace: string): void;
10
16
 
11
- export { registerFileTools };
17
+ export { registerFileTools, registerReadOnlyFileTools };
@@ -5,17 +5,23 @@ import * as path2 from "path";
5
5
  // src/tools/pathSafety.ts
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
- function isWithinRoot(workspace, target) {
8
+ function isInsideRelative(rel) {
9
+ return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
10
+ }
11
+ function isFilesystemRoot(p) {
12
+ return path.parse(p).root === p;
13
+ }
14
+ function resolveWithinRoot(workspace, target) {
15
+ const abs = path.resolve(workspace, target);
9
16
  let rootReal;
10
17
  try {
11
18
  rootReal = fs.realpathSync(workspace);
12
19
  } catch {
13
20
  const rootAbs = path.resolve(workspace);
14
- const abs2 = path.resolve(workspace, target);
15
- const rel2 = path.relative(rootAbs, abs2);
16
- return !rel2.startsWith("..") && !path.isAbsolute(rel2);
21
+ if (!isInsideRelative(path.relative(rootAbs, abs))) return null;
22
+ return { root: rootAbs, real: abs };
17
23
  }
18
- const abs = path.resolve(workspace, target);
24
+ if (isFilesystemRoot(rootReal)) return null;
19
25
  let real;
20
26
  try {
21
27
  real = fs.realpathSync(abs);
@@ -29,17 +35,17 @@ function isWithinRoot(workspace, target) {
29
35
  real = path.join(fs.realpathSync(probe), ...tail);
30
36
  break;
31
37
  } catch {
32
- return false;
38
+ return null;
33
39
  }
34
40
  }
35
41
  tail.unshift(path.basename(probe));
36
42
  const parent = path.dirname(probe);
37
- if (parent === probe) return false;
43
+ if (parent === probe) return null;
38
44
  probe = parent;
39
45
  }
40
46
  }
41
- const rel = path.relative(rootReal, real);
42
- return !rel.startsWith("..") && !path.isAbsolute(rel);
47
+ if (!isInsideRelative(path.relative(rootReal, real))) return null;
48
+ return { root: rootReal, real };
43
49
  }
44
50
 
45
51
  // src/tools/files.ts
@@ -51,10 +57,11 @@ function makeReadFile(workspace) {
51
57
  const filePath = String(args.path ?? "");
52
58
  const startLine = Math.max(1, Number(args.start_line ?? 1));
53
59
  const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
54
- const target = path2.resolve(root, filePath);
55
- if (!isWithinRoot(root, target)) {
60
+ const resolved = resolveWithinRoot(root, filePath);
61
+ if (!resolved) {
56
62
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
57
63
  }
64
+ const target = resolved.real;
58
65
  if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
59
66
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
60
67
  }
@@ -76,7 +83,7 @@ function makeReadFile(workspace) {
76
83
  }
77
84
  const end = startLine + selected.length - 1;
78
85
  const result = {
79
- path: path2.relative(root, target),
86
+ path: path2.relative(resolved.root, target),
80
87
  start_line: startLine,
81
88
  end_line: end,
82
89
  total_lines: total,
@@ -94,10 +101,11 @@ function makeWriteFile(workspace) {
94
101
  const filePath = String(args.path ?? "");
95
102
  const content = String(args.content ?? "");
96
103
  const overwrite = args.overwrite !== false;
97
- const target = path2.resolve(root, filePath);
98
- if (!isWithinRoot(root, target)) {
104
+ const resolved = resolveWithinRoot(root, filePath);
105
+ if (!resolved) {
99
106
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
100
107
  }
108
+ const target = resolved.real;
101
109
  if (fs2.existsSync(target) && fs2.statSync(target).isDirectory()) {
102
110
  return { error: "\u8DEF\u5F84\u662F\u76EE\u5F55\uFF0C\u4E0D\u662F\u6587\u4EF6" };
103
111
  }
@@ -110,7 +118,7 @@ function makeWriteFile(workspace) {
110
118
  } catch (exc) {
111
119
  return { error: `\u5199\u5165\u5931\u8D25: ${String(exc)}` };
112
120
  }
113
- return path2.relative(root, target);
121
+ return path2.relative(resolved.root, target);
114
122
  };
115
123
  }
116
124
  function makeListFiles(workspace) {
@@ -118,8 +126,9 @@ function makeListFiles(workspace) {
118
126
  return function list_files(args) {
119
127
  const dirPath = String(args.path ?? ".");
120
128
  const maxResults = Math.min(Number(args.max_results ?? 100), 100);
121
- const target = path2.resolve(root, dirPath);
122
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
129
+ const resolved = resolveWithinRoot(root, dirPath);
130
+ if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
131
+ const target = resolved.real;
123
132
  if (!fs2.existsSync(target) || !fs2.statSync(target).isDirectory()) {
124
133
  return { error: `\u4E0D\u662F\u76EE\u5F55: ${dirPath}` };
125
134
  }
@@ -133,7 +142,7 @@ function makeListFiles(workspace) {
133
142
  results.push(e.isDirectory() ? `${e.name}/` : e.name);
134
143
  }
135
144
  results.sort();
136
- return { path: path2.relative(root, target), entries: results.slice(0, maxResults) };
145
+ return { path: path2.relative(resolved.root, target), entries: results.slice(0, maxResults) };
137
146
  } catch (exc) {
138
147
  return { error: `\u5217\u51FA\u6587\u4EF6\u5931\u8D25: ${String(exc)}` };
139
148
  }
@@ -147,8 +156,9 @@ function makeReplaceInFile(workspace) {
147
156
  const newStr = String(args.new ?? "");
148
157
  const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));
149
158
  if (!oldStr) return { error: "old \u4E0D\u80FD\u4E3A\u7A7A" };
150
- const target = path2.resolve(root, filePath);
151
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
159
+ const resolved = resolveWithinRoot(root, filePath);
160
+ if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
161
+ const target = resolved.real;
152
162
  if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
153
163
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
154
164
  }
@@ -161,7 +171,7 @@ function makeReplaceInFile(workspace) {
161
171
  const updated = content.replaceAll(oldStr, newStr);
162
172
  fs2.writeFileSync(target, updated, "utf-8");
163
173
  return {
164
- path: path2.relative(root, target),
174
+ path: path2.relative(resolved.root, target),
165
175
  replacements: count,
166
176
  chars_before: content.length,
167
177
  chars_after: updated.length
@@ -171,7 +181,7 @@ function makeReplaceInFile(workspace) {
171
181
  }
172
182
  };
173
183
  }
174
- function registerFileTools(registry, workspace) {
184
+ function registerReadOnlyFileTools(registry, workspace) {
175
185
  const readFile = makeReadFile(workspace);
176
186
  registry.register(
177
187
  (args) => readFile(args),
@@ -193,6 +203,28 @@ function registerFileTools(registry, workspace) {
193
203
  }
194
204
  }
195
205
  );
206
+ const listFiles = makeListFiles(workspace);
207
+ registry.register(
208
+ (args) => listFiles(args),
209
+ { category: "filesystem", riskLevel: "low", capabilities: ["list_files"], requiresApproval: false },
210
+ {
211
+ type: "function",
212
+ function: {
213
+ name: "list_files",
214
+ description: "\u5217\u51FA\u76EE\u5F55\u4E0B\u7684\u6587\u4EF6\u548C\u5B50\u76EE\u5F55\u3002\u8DF3\u8FC7\u9690\u85CF\u6587\u4EF6\u548C node_modules \u7B49\u76EE\u5F55\u3002",
215
+ parameters: {
216
+ type: "object",
217
+ properties: {
218
+ path: { type: "string", description: "\u76EE\u5F55\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A\uFF0C\u9ED8\u8BA4 '.')" },
219
+ max_results: { type: "integer", description: "\u6700\u5927\u7ED3\u679C\u6570 (\u9ED8\u8BA4 100)" }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ );
225
+ }
226
+ function registerFileTools(registry, workspace) {
227
+ registerReadOnlyFileTools(registry, workspace);
196
228
  const writeFile = makeWriteFile(workspace);
197
229
  registry.register(
198
230
  (args) => writeFile(args),
@@ -236,27 +268,9 @@ function registerFileTools(registry, workspace) {
236
268
  }
237
269
  }
238
270
  );
239
- const listFiles = makeListFiles(workspace);
240
- registry.register(
241
- (args) => listFiles(args),
242
- { category: "filesystem", riskLevel: "low", capabilities: ["list_files"], requiresApproval: false },
243
- {
244
- type: "function",
245
- function: {
246
- name: "list_files",
247
- description: "\u5217\u51FA\u76EE\u5F55\u4E0B\u7684\u6587\u4EF6\u548C\u5B50\u76EE\u5F55\u3002\u8DF3\u8FC7\u9690\u85CF\u6587\u4EF6\u548C node_modules \u7B49\u76EE\u5F55\u3002",
248
- parameters: {
249
- type: "object",
250
- properties: {
251
- path: { type: "string", description: "\u76EE\u5F55\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A\uFF0C\u9ED8\u8BA4 '.')" },
252
- max_results: { type: "integer", description: "\u6700\u5927\u7ED3\u679C\u6570 (\u9ED8\u8BA4 100)" }
253
- }
254
- }
255
- }
256
- }
257
- );
258
271
  }
259
272
  export {
260
- registerFileTools
273
+ registerFileTools,
274
+ registerReadOnlyFileTools
261
275
  };
262
276
  //# sourceMappingURL=files.js.map