@zqagent/core 0.2.20 → 0.3.6

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 (238) hide show
  1. package/README.md +1 -1
  2. package/README.oss.en.md +3 -3
  3. package/README.oss.md +8 -3
  4. package/README.oss.zh-TW.md +3 -3
  5. package/dist/McpClient-CEQBUPuC.d.cts +145 -0
  6. package/dist/McpClient-CEQBUPuC.d.ts +145 -0
  7. package/dist/{ToolRegistry-C3pTFiaJ.d.cts → ToolRegistry-Cy0fQxrb.d.ts} +2 -111
  8. package/dist/{ToolRegistry-C3pTFiaJ.d.ts → ToolRegistry-DouDvn9E.d.cts} +2 -111
  9. package/dist/{explore-RtZtb-V6.d.ts → TurnEngine-BPKhWuSI.d.cts} +108 -73
  10. package/dist/{explore-BgRhj6SP.d.cts → TurnEngine-DtGEuVLT.d.ts} +108 -73
  11. package/dist/conflictGuard-BBOOIxxK.d.cts +42 -0
  12. package/dist/conflictGuard-BBOOIxxK.d.ts +42 -0
  13. package/dist/index.cjs +5418 -1096
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +577 -195
  16. package/dist/index.d.ts +577 -195
  17. package/dist/index.js +5299 -1011
  18. package/dist/index.js.map +1 -1
  19. package/dist/internal/crypto/secrets.cjs +25 -0
  20. package/dist/internal/crypto/secrets.cjs.map +1 -0
  21. package/dist/internal/crypto/secrets.d.cts +2 -0
  22. package/dist/internal/crypto/secrets.d.ts +2 -0
  23. package/dist/internal/crypto/secrets.js +3 -0
  24. package/dist/internal/crypto/secrets.js.map +1 -0
  25. package/dist/internal/engine/FileLogHook.cjs +179 -0
  26. package/dist/internal/engine/FileLogHook.cjs.map +1 -0
  27. package/dist/internal/engine/FileLogHook.d.cts +67 -0
  28. package/dist/internal/engine/FileLogHook.d.ts +67 -0
  29. package/dist/internal/engine/FileLogHook.js +144 -0
  30. package/dist/internal/engine/FileLogHook.js.map +1 -0
  31. package/dist/internal/engine/approvalView.cjs +25 -0
  32. package/dist/internal/engine/approvalView.cjs.map +1 -0
  33. package/dist/internal/engine/approvalView.d.cts +1 -0
  34. package/dist/internal/engine/approvalView.d.ts +1 -0
  35. package/dist/internal/engine/approvalView.js +3 -0
  36. package/dist/internal/engine/approvalView.js.map +1 -0
  37. package/dist/internal/engine/limits.cjs +59 -0
  38. package/dist/internal/engine/limits.cjs.map +1 -0
  39. package/dist/internal/engine/limits.d.cts +32 -0
  40. package/dist/internal/engine/limits.d.ts +32 -0
  41. package/dist/internal/engine/limits.js +31 -0
  42. package/dist/internal/engine/limits.js.map +1 -0
  43. package/dist/internal/engine/sessionNaming.cjs +47 -0
  44. package/dist/internal/engine/sessionNaming.cjs.map +1 -0
  45. package/dist/internal/engine/sessionNaming.d.cts +25 -0
  46. package/dist/internal/engine/sessionNaming.d.ts +25 -0
  47. package/dist/internal/engine/sessionNaming.js +20 -0
  48. package/dist/internal/engine/sessionNaming.js.map +1 -0
  49. package/dist/internal/llmConfig.cjs +561 -0
  50. package/dist/internal/llmConfig.cjs.map +1 -0
  51. package/dist/internal/llmConfig.d.cts +75 -0
  52. package/dist/internal/llmConfig.d.ts +75 -0
  53. package/dist/internal/llmConfig.js +535 -0
  54. package/dist/internal/llmConfig.js.map +1 -0
  55. package/dist/internal/mcp/builtin/index.cjs +685 -0
  56. package/dist/internal/mcp/builtin/index.cjs.map +1 -0
  57. package/dist/internal/mcp/builtin/index.d.cts +21 -0
  58. package/dist/internal/mcp/builtin/index.d.ts +21 -0
  59. package/dist/internal/mcp/builtin/index.js +652 -0
  60. package/dist/internal/mcp/builtin/index.js.map +1 -0
  61. package/dist/internal/mcp/config.cjs +278 -0
  62. package/dist/internal/mcp/config.cjs.map +1 -0
  63. package/dist/internal/mcp/config.d.cts +61 -0
  64. package/dist/internal/mcp/config.d.ts +61 -0
  65. package/dist/internal/mcp/config.js +235 -0
  66. package/dist/internal/mcp/config.js.map +1 -0
  67. package/dist/internal/mcp/presets.cjs +184 -0
  68. package/dist/internal/mcp/presets.cjs.map +1 -0
  69. package/dist/internal/mcp/presets.d.cts +39 -0
  70. package/dist/internal/mcp/presets.d.ts +39 -0
  71. package/dist/internal/mcp/presets.js +158 -0
  72. package/dist/internal/mcp/presets.js.map +1 -0
  73. package/dist/internal/node-name.cjs +45 -0
  74. package/dist/internal/node-name.cjs.map +1 -0
  75. package/dist/internal/node-name.d.cts +3 -0
  76. package/dist/internal/node-name.d.ts +3 -0
  77. package/dist/internal/node-name.js +10 -0
  78. package/dist/internal/node-name.js.map +1 -0
  79. package/dist/internal/paths.cjs +63 -0
  80. package/dist/internal/paths.cjs.map +1 -0
  81. package/dist/internal/paths.d.cts +17 -0
  82. package/dist/internal/paths.d.ts +17 -0
  83. package/dist/internal/paths.js +25 -0
  84. package/dist/internal/paths.js.map +1 -0
  85. package/dist/internal/providers/diagnostics.cjs +288 -0
  86. package/dist/internal/providers/diagnostics.cjs.map +1 -0
  87. package/dist/internal/providers/diagnostics.d.cts +103 -0
  88. package/dist/internal/providers/diagnostics.d.ts +103 -0
  89. package/dist/internal/providers/diagnostics.js +250 -0
  90. package/dist/internal/providers/diagnostics.js.map +1 -0
  91. package/dist/internal/providers/models.cjs +699 -0
  92. package/dist/internal/providers/models.cjs.map +1 -0
  93. package/dist/internal/providers/models.d.cts +136 -0
  94. package/dist/internal/providers/models.d.ts +136 -0
  95. package/dist/internal/providers/models.js +654 -0
  96. package/dist/internal/providers/models.js.map +1 -0
  97. package/dist/internal/terminal.cjs +161 -0
  98. package/dist/internal/terminal.cjs.map +1 -0
  99. package/dist/internal/terminal.d.cts +58 -0
  100. package/dist/internal/terminal.d.ts +58 -0
  101. package/dist/internal/terminal.js +128 -0
  102. package/dist/internal/terminal.js.map +1 -0
  103. package/dist/internal/tools/execAsync.cjs +187 -0
  104. package/dist/internal/tools/execAsync.cjs.map +1 -0
  105. package/dist/internal/tools/execAsync.d.cts +30 -0
  106. package/dist/internal/tools/execAsync.d.ts +30 -0
  107. package/dist/internal/tools/execAsync.js +162 -0
  108. package/dist/internal/tools/execAsync.js.map +1 -0
  109. package/dist/internal/tools/git.cjs +935 -0
  110. package/dist/internal/tools/git.cjs.map +1 -0
  111. package/dist/internal/tools/git.d.cts +206 -0
  112. package/dist/internal/tools/git.d.ts +206 -0
  113. package/dist/internal/tools/git.js +883 -0
  114. package/dist/internal/tools/git.js.map +1 -0
  115. package/dist/internal/tools/pathSafety.cjs +99 -0
  116. package/dist/internal/tools/pathSafety.cjs.map +1 -0
  117. package/dist/internal/tools/pathSafety.d.cts +44 -0
  118. package/dist/internal/tools/pathSafety.d.ts +44 -0
  119. package/dist/internal/tools/pathSafety.js +61 -0
  120. package/dist/internal/tools/pathSafety.js.map +1 -0
  121. package/dist/internal/tools/plan.cjs +146 -0
  122. package/dist/internal/tools/plan.cjs.map +1 -0
  123. package/dist/internal/tools/plan.d.cts +48 -0
  124. package/dist/internal/tools/plan.d.ts +48 -0
  125. package/dist/internal/tools/plan.js +117 -0
  126. package/dist/internal/tools/plan.js.map +1 -0
  127. package/dist/internal/tools/ripgrep.cjs +118 -0
  128. package/dist/internal/tools/ripgrep.cjs.map +1 -0
  129. package/dist/internal/tools/ripgrep.d.cts +6 -0
  130. package/dist/internal/tools/ripgrep.d.ts +6 -0
  131. package/dist/internal/tools/ripgrep.js +89 -0
  132. package/dist/internal/tools/ripgrep.js.map +1 -0
  133. package/dist/internal/tools/sandbox.cjs +253 -0
  134. package/dist/internal/tools/sandbox.cjs.map +1 -0
  135. package/dist/internal/tools/sandbox.d.cts +67 -0
  136. package/dist/internal/tools/sandbox.d.ts +67 -0
  137. package/dist/internal/tools/sandbox.js +213 -0
  138. package/dist/internal/tools/sandbox.js.map +1 -0
  139. package/dist/internal/tools/shell.cjs +771 -0
  140. package/dist/internal/tools/shell.cjs.map +1 -0
  141. package/dist/internal/tools/shell.d.cts +47 -0
  142. package/dist/internal/tools/shell.d.ts +47 -0
  143. package/dist/internal/tools/shell.js +731 -0
  144. package/dist/internal/tools/shell.js.map +1 -0
  145. package/dist/internal/tools/shellRuntime.cjs +220 -0
  146. package/dist/internal/tools/shellRuntime.cjs.map +1 -0
  147. package/dist/internal/tools/shellRuntime.d.cts +67 -0
  148. package/dist/internal/tools/shellRuntime.d.ts +67 -0
  149. package/dist/internal/tools/shellRuntime.js +181 -0
  150. package/dist/internal/tools/shellRuntime.js.map +1 -0
  151. package/dist/internal/update/controller.cjs +1035 -0
  152. package/dist/internal/update/controller.cjs.map +1 -0
  153. package/dist/internal/update/controller.d.cts +150 -0
  154. package/dist/internal/update/controller.d.ts +150 -0
  155. package/dist/internal/update/controller.js +991 -0
  156. package/dist/internal/update/controller.js.map +1 -0
  157. package/dist/internal/update/platform.cjs +234 -0
  158. package/dist/internal/update/platform.cjs.map +1 -0
  159. package/dist/internal/update/platform.d.cts +77 -0
  160. package/dist/internal/update/platform.d.ts +77 -0
  161. package/dist/internal/update/platform.js +184 -0
  162. package/dist/internal/update/platform.js.map +1 -0
  163. package/dist/tools/ask.d.cts +3 -1
  164. package/dist/tools/ask.d.ts +3 -1
  165. package/dist/tools/diff.cjs +17 -6
  166. package/dist/tools/diff.cjs.map +1 -1
  167. package/dist/tools/diff.d.cts +5 -2
  168. package/dist/tools/diff.d.ts +5 -2
  169. package/dist/tools/diff.js +17 -6
  170. package/dist/tools/diff.js.map +1 -1
  171. package/dist/tools/directories.d.cts +3 -1
  172. package/dist/tools/directories.d.ts +3 -1
  173. package/dist/tools/explore.cjs +1778 -204
  174. package/dist/tools/explore.cjs.map +1 -1
  175. package/dist/tools/explore.d.cts +82 -2
  176. package/dist/tools/explore.d.ts +82 -2
  177. package/dist/tools/explore.js +1780 -204
  178. package/dist/tools/explore.js.map +1 -1
  179. package/dist/tools/files.cjs +56 -21
  180. package/dist/tools/files.cjs.map +1 -1
  181. package/dist/tools/files.d.cts +6 -3
  182. package/dist/tools/files.d.ts +6 -3
  183. package/dist/tools/files.js +56 -21
  184. package/dist/tools/files.js.map +1 -1
  185. package/dist/tools/git.cjs +34 -25
  186. package/dist/tools/git.cjs.map +1 -1
  187. package/dist/tools/git.d.cts +4 -204
  188. package/dist/tools/git.d.ts +4 -204
  189. package/dist/tools/git.js +34 -25
  190. package/dist/tools/git.js.map +1 -1
  191. package/dist/tools/media.cjs +280 -3
  192. package/dist/tools/media.cjs.map +1 -1
  193. package/dist/tools/media.d.cts +3 -1
  194. package/dist/tools/media.d.ts +3 -1
  195. package/dist/tools/media.js +278 -1
  196. package/dist/tools/media.js.map +1 -1
  197. package/dist/tools/plan.cjs +62 -0
  198. package/dist/tools/plan.cjs.map +1 -1
  199. package/dist/tools/plan.d.cts +4 -17
  200. package/dist/tools/plan.d.ts +4 -17
  201. package/dist/tools/plan.js +59 -0
  202. package/dist/tools/plan.js.map +1 -1
  203. package/dist/tools/search.cjs +3 -0
  204. package/dist/tools/search.cjs.map +1 -1
  205. package/dist/tools/search.d.cts +3 -1
  206. package/dist/tools/search.d.ts +3 -1
  207. package/dist/tools/search.js +3 -0
  208. package/dist/tools/search.js.map +1 -1
  209. package/dist/tools/shell.cjs +76 -11
  210. package/dist/tools/shell.cjs.map +1 -1
  211. package/dist/tools/shell.d.cts +5 -39
  212. package/dist/tools/shell.d.ts +5 -39
  213. package/dist/tools/shell.js +75 -11
  214. package/dist/tools/shell.js.map +1 -1
  215. package/dist/tools/todo.d.cts +3 -1
  216. package/dist/tools/todo.d.ts +3 -1
  217. package/dist/tools/web.cjs +279 -5
  218. package/dist/tools/web.cjs.map +1 -1
  219. package/dist/tools/web.d.cts +3 -1
  220. package/dist/tools/web.d.ts +3 -1
  221. package/dist/tools/web.js +275 -3
  222. package/dist/tools/web.js.map +1 -1
  223. package/dist/types-BYyXWWPW.d.cts +237 -0
  224. package/dist/types-BYyXWWPW.d.ts +237 -0
  225. package/package.json +26 -37
  226. package/bin/zqagent +0 -2
  227. package/dist/cli/index.cjs +0 -25218
  228. package/dist/cli/index.cjs.map +0 -1
  229. package/dist/cli/index.d.cts +0 -2
  230. package/dist/cli/index.d.ts +0 -2
  231. package/dist/cli/index.js +0 -25201
  232. package/dist/cli/index.js.map +0 -1
  233. package/dist/server/index.cjs +0 -20510
  234. package/dist/server/index.cjs.map +0 -1
  235. package/dist/server/index.d.cts +0 -2
  236. package/dist/server/index.d.ts +0 -2
  237. package/dist/server/index.js +0 -20493
  238. package/dist/server/index.js.map +0 -1
@@ -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 { 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":";AAQA,YAAYA,SAAQ;AACpB,YAAYC,WAAU;;;ACQtB,YAAY,QAAQ;AACpB,YAAY,UAAU;AAStB,SAAS,iBAAiB,KAAsB;AAC9C,SACE,CAAM,gBAAW,GAAG,KAAK,QAAQ,QAAQ,CAAC,IAAI,WAAW,KAAU,QAAG,EAAE;AAE5E;AAGA,SAAS,iBAAiB,GAAoB;AAC5C,SAAY,WAAM,CAAC,EAAE,SAAS;AAChC;AAmBO,SAAS,kBACd,WACA,QACqB;AACrB,QAAM,MAAW,aAAQ,WAAW,MAAM;AAE1C,MAAI;AACJ,MAAI;AACF,eAAc,gBAAa,SAAS;AAAA,EACtC,QAAQ;AAGN,UAAM,UAAe,aAAQ,SAAS;AACtC,QAAI,CAAC,iBAAsB,cAAS,SAAS,GAAG,CAAC,EAAG,QAAO;AAC3D,WAAO,EAAE,MAAM,SAAS,MAAM,IAAI;AAAA,EACpC;AACA,MAAI,iBAAiB,QAAQ,EAAG,QAAO;AAEvC,MAAI;AACJ,MAAI;AACF,WAAU,gBAAa,GAAG;AAAA,EAC5B,QAAQ;AAEN,QAAI,QAAQ;AACZ,UAAM,OAAiB,CAAC;AACxB,eAAS;AAEP,YAAM,SAAY,cAAW,KAAK,KAAQ,aAAU,OAAO,EAAE,gBAAgB,MAAM,CAAC,MAAM;AAC1F,UAAI,QAAQ;AACV,YAAI;AACF,iBAAY,UAAQ,gBAAa,KAAK,GAAG,GAAG,IAAI;AAChD;AAAA,QACF,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AACA,WAAK,QAAa,cAAS,KAAK,CAAC;AACjC,YAAM,SAAc,aAAQ,KAAK;AACjC,UAAI,WAAW,MAAO,QAAO;AAC7B,cAAQ;AAAA,IACV;AAAA,EACF;AAEA,MAAI,CAAC,iBAAsB,cAAS,UAAU,IAAI,CAAC,EAAG,QAAO;AAC7D,SAAO,EAAE,MAAM,UAAU,KAAK;AAChC;;;ADtFA,IAAM,oBAAoB;AAE1B,IAAM,iBAAiB;AAQvB,SAAS,aAAa,WAAmB;AACvC,QAAM,OAAY,cAAQ,SAAS;AAEnC,SAAO,SAAS,UAAU,MAA+B;AACvD,UAAM,WAAW,OAAO,KAAK,QAAQ,EAAE;AACvC,UAAM,YAAY,KAAK,IAAI,GAAG,OAAO,KAAK,cAAc,CAAC,CAAC;AAC1D,UAAM,WAAW,KAAK,IAAI,OAAO,KAAK,aAAa,iBAAiB,GAAG,iBAAiB;AAIxF,UAAM,WAAW,kBAAkB,MAAM,QAAQ;AACjD,QAAI,CAAC,UAAU;AACb,aAAO,EAAE,OAAO,mDAAW;AAAA,IAC7B;AACA,UAAM,SAAS,SAAS;AACxB,QAAI,CAAI,eAAW,MAAM,KAAK,CAAI,aAAS,MAAM,EAAE,OAAO,GAAG;AAC3D,aAAO,EAAE,OAAO,6BAAS,QAAQ,GAAG;AAAA,IACtC;AAGA,UAAM,WAAqB,CAAC;AAC5B,QAAI,QAAQ;AACZ,QAAI;AACF,YAAM,UAAa,iBAAa,QAAQ,OAAO;AAC/C,YAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,cAAQ,MAAM;AACd,eAAS,IAAI,YAAY,GAAG,IAAI,KAAK,IAAI,YAAY,IAAI,UAAU,MAAM,MAAM,GAAG,KAAK;AACrF,YAAI,OAAO,MAAM,CAAC;AAClB,YAAI,KAAK,SAAS,gBAAgB;AAChC,iBAAO,KAAK,MAAM,GAAG,cAAc,IAAI;AAAA,QACzC;AACA,iBAAS,KAAK,GAAG,OAAO,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC,IAAK,IAAI,EAAE;AAAA,MACvD;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,EAAE,OAAO,6BAAS,OAAO,GAAG,CAAC,GAAG;AAAA,IACzC;AAEA,UAAM,MAAM,YAAY,SAAS,SAAS;AAC1C,UAAM,SAAkC;AAAA,MACtC,MAAW,eAAS,SAAS,MAAM,MAAM;AAAA,MACzC,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,aAAa;AAAA,MACb,SAAS,SAAS,KAAK,IAAI;AAAA,IAC7B;AAEA,QAAI,MAAM,OAAO;AACf,aAAO,OAAO,sBAAO,SAAS,IAAI,GAAG,uBAAQ,KAAK,kDAA8B,MAAM,CAAC;AAAA,IACzF;AACA,WAAO;AAAA,EACT;AACF;AAOA,SAAS,cAAc,WAAmB;AACxC,QAAM,OAAY,cAAQ,SAAS;AAEnC,SAAO,SAAS,WAAW,MAA+B;AACxD,UAAM,WAAW,OAAO,KAAK,QAAQ,EAAE;AACvC,UAAM,UAAU,OAAO,KAAK,WAAW,EAAE;AACzC,UAAM,YAAY,KAAK,cAAc;AAGrC,UAAM,WAAW,kBAAkB,MAAM,QAAQ;AACjD,QAAI,CAAC,UAAU;AACb,aAAO,EAAE,OAAO,mDAAW;AAAA,IAC7B;AACA,UAAM,SAAS,SAAS;AACxB,QAAO,eAAW,MAAM,KAAQ,aAAS,MAAM,EAAE,YAAY,GAAG;AAC9D,aAAO,EAAE,OAAO,+DAAa;AAAA,IAC/B;AACA,QAAO,eAAW,MAAM,KAAK,CAAC,WAAW;AACvC,aAAO,EAAE,OAAO,mDAA0B;AAAA,IAC5C;AAEA,QAAI;AACF,MAAG,cAAe,cAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AACtD,MAAG,kBAAc,QAAQ,SAAS,OAAO;AAAA,IAC3C,SAAS,KAAK;AACZ,aAAO,EAAE,OAAO,6BAAS,OAAO,GAAG,CAAC,GAAG;AAAA,IACzC;AAEA,WAAY,eAAS,SAAS,MAAM,MAAM;AAAA,EAC5C;AACF;AAQA,SAAS,cAAc,WAAmB;AACxC,QAAM,OAAY,cAAQ,SAAS;AAEnC,SAAO,SAAS,WAAW,MAA+B;AACxD,UAAM,UAAU,OAAO,KAAK,QAAQ,GAAG;AACvC,UAAM,aAAa,KAAK,IAAI,OAAO,KAAK,eAAe,GAAG,GAAG,GAAG;AAChE,UAAM,WAAW,kBAAkB,MAAM,OAAO;AAChD,QAAI,CAAC,SAAU,QAAO,EAAE,OAAO,mDAAW;AAC1C,UAAM,SAAS,SAAS;AACxB,QAAI,CAAI,eAAW,MAAM,KAAK,CAAI,aAAS,MAAM,EAAE,YAAY,GAAG;AAChE,aAAO,EAAE,OAAO,6BAAS,OAAO,GAAG;AAAA,IACrC;AAEA,QAAI;AACF,YAAM,UAAa,gBAAY,QAAQ,EAAE,eAAe,KAAK,CAAC;AAC9D,YAAM,UAAoB,CAAC;AAC3B,iBAAW,KAAK,SAAS;AACvB,YAAI,QAAQ,UAAU,WAAY;AAClC,YAAI,EAAE,KAAK,WAAW,GAAG,EAAG;AAC5B,YAAI,EAAE,SAAS,kBAAkB,EAAE,SAAS,iBAAiB,EAAE,SAAS,OAAQ;AAChF,gBAAQ,KAAK,EAAE,YAAY,IAAI,GAAG,EAAE,IAAI,MAAM,EAAE,IAAI;AAAA,MACtD;AACA,cAAQ,KAAK;AACb,aAAO,EAAE,MAAW,eAAS,SAAS,MAAM,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,UAAU,EAAE;AAAA,IAC7F,SAAS,KAAK;AACZ,aAAO,EAAE,OAAO,yCAAW,OAAO,GAAG,CAAC,GAAG;AAAA,IAC3C;AAAA,EACF;AACF;AAGA,SAAS,kBAAkB,WAAmB;AAC5C,QAAM,OAAY,cAAQ,SAAS;AAEnC,SAAO,SAAS,gBAAgB,MAA+B;AAC7D,UAAM,WAAW,OAAO,KAAK,QAAQ,EAAE;AACvC,UAAM,SAAS,OAAO,KAAK,OAAO,EAAE;AACpC,UAAM,SAAS,OAAO,KAAK,OAAO,EAAE;AACpC,UAAM,uBAAuB,KAAK,IAAI,GAAG,OAAO,KAAK,yBAAyB,CAAC,CAAC;AAEhF,QAAI,CAAC,OAAQ,QAAO,EAAE,OAAO,+BAAW;AAExC,UAAM,WAAW,kBAAkB,MAAM,QAAQ;AACjD,QAAI,CAAC,SAAU,QAAO,EAAE,OAAO,mDAAW;AAC1C,UAAM,SAAS,SAAS;AACxB,QAAI,CAAI,eAAW,MAAM,KAAK,CAAI,aAAS,MAAM,EAAE,OAAO,GAAG;AAC3D,aAAO,EAAE,OAAO,6BAAS,QAAQ,GAAG;AAAA,IACtC;AAEA,QAAI;AACF,YAAM,UAAa,iBAAa,QAAQ,OAAO;AAC/C,YAAM,QAAQ,QAAQ,MAAM,MAAM,EAAE,SAAS;AAE7C,UAAI,UAAU,sBAAsB;AAClC,eAAO,EAAE,OAAO,gBAAM,oBAAoB,qDAAa,KAAK,YAAO,QAAQ,GAAG;AAAA,MAChF;AAEA,YAAM,UAAU,QAAQ,WAAW,QAAQ,MAAM;AACjD,MAAG,kBAAc,QAAQ,SAAS,OAAO;AACzC,aAAO;AAAA,QACL,MAAW,eAAS,SAAS,MAAM,MAAM;AAAA,QACzC,cAAc;AAAA,QACd,cAAc,QAAQ;AAAA,QACtB,aAAa,QAAQ;AAAA,MACvB;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,EAAE,OAAO,6BAAS,OAAO,GAAG,CAAC,GAAG;AAAA,IACzC;AAAA,EACF;AACF;AAOO,SAAS,0BAA0B,UAAwB,WAAyB;AACzF,QAAM,WAAW,aAAa,SAAS;AACvC,WAAS;AAAA,IACP,CAAC,SAAS,SAAS,IAAI;AAAA,IACvB,EAAE,UAAU,cAAc,WAAW,OAAO,cAAc,CAAC,WAAW,GAAG,kBAAkB,MAAM;AAAA,IACjG;AAAA,MACE,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM;AAAA,QACN,aAAa;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACV,MAAM,EAAE,MAAM,UAAU,aAAa,kEAAgB;AAAA,YACrD,YAAY,EAAE,MAAM,WAAW,aAAa,oEAAkB;AAAA,YAC9D,WAAW,EAAE,MAAM,WAAW,aAAa,0CAAY,iBAAiB,IAAI;AAAA,UAC9E;AAAA,UACA,UAAU,CAAC,MAAM;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,cAAc,SAAS;AACzC,WAAS;AAAA,IACP,CAAC,SAAS,UAAU,IAAI;AAAA,IACxB,EAAE,UAAU,cAAc,WAAW,OAAO,cAAc,CAAC,YAAY,GAAG,kBAAkB,MAAM;AAAA,IAClG;AAAA,MACE,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM;AAAA,QACN,aAAa;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACV,MAAM,EAAE,MAAM,UAAU,aAAa,wFAAuB;AAAA,YAC5D,aAAa,EAAE,MAAM,WAAW,aAAa,oDAAiB;AAAA,UAChE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAQO,SAAS,kBAAkB,UAAwB,WAAyB;AACjF,4BAA0B,UAAU,SAAS;AAE7C,QAAM,YAAY,cAAc,SAAS;AACzC,WAAS;AAAA,IACP,CAAC,SAAS,UAAU,IAAI;AAAA,IACxB,EAAE,UAAU,cAAc,WAAW,UAAU,cAAc,CAAC,YAAY,GAAG,kBAAkB,KAAK;AAAA,IACpG;AAAA,MACE,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM;AAAA,QACN,aAAa;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACV,MAAM,EAAE,MAAM,UAAU,aAAa,kEAAgB;AAAA,YACrD,SAAS,EAAE,MAAM,UAAU,aAAa,mCAAe;AAAA,YACvD,WAAW,EAAE,MAAM,WAAW,aAAa,mFAAuB;AAAA,UACpE;AAAA,UACA,UAAU,CAAC,QAAQ,SAAS;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAAc,kBAAkB,SAAS;AAC/C,WAAS;AAAA,IACP,CAAC,SAAS,YAAY,IAAI;AAAA,IAC1B,EAAE,UAAU,cAAc,WAAW,UAAU,cAAc,CAAC,WAAW,GAAG,kBAAkB,KAAK;AAAA,IACnG;AAAA,MACE,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM;AAAA,QACN,aAAa;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACV,MAAM,EAAE,MAAM,UAAU,aAAa,kEAAgB;AAAA,YACrD,KAAK,EAAE,MAAM,UAAU,aAAa,yDAAY;AAAA,YAChD,KAAK,EAAE,MAAM,UAAU,aAAa,6CAAU;AAAA,YAC9C,uBAAuB,EAAE,MAAM,WAAW,aAAa,8DAAiB;AAAA,UAC1E;AAAA,UACA,UAAU,CAAC,QAAQ,OAAO,KAAK;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["fs","path"]}
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\";\nimport type { ConflictGuard } from \"./conflictGuard.js\";\n\n/** read_file 默认最大读取行数 */\nconst DEFAULT_MAX_LINES = 2000;\n/** 单次多文件读取的总行数上限(防止一次返回撑爆上下文) */\nconst MULTI_READ_MAX_TOTAL_LINES = 1500;\n/** 单行最大字符数(超长行截断,防止输出爆炸) */\nconst MAX_LINE_CHARS = 500;\n\n/**\n * read_file — 行号标注版 (对齐 Python 版)\n *\n * 返回带行号的内容,格式 '12|text',便于引用为 path:line;\n * 大文件通过 start_line / max_lines 窗口化阅读。\n *\n * 行号用最短分隔(`12|`)而不是 `padStart(6) + \\t`:行号前缀实测占\n * read_file 输出的 12.7%,每次迭代都要重发,瘦身属于纯无损节省。\n *\n * 也支持 paths 一次读取多个文件/区间:每次工具往返都要重发全部上下文,\n * 合并读取能直接减少迭代次数。\n */\nfunction makeReadFile(workspace: string, guard?: ConflictGuard) {\n const root = path.resolve(workspace);\n\n /** 读取单个窗口,返回结果对象(不抛错,失败转为 error 字段) */\n function readOne(\n filePath: string,\n startLine: number,\n maxLines: number,\n ): Record<string, unknown> {\n // 路径范围检查 + 存在性/文件类型检查。\n // 用解析后的真实路径做后续 fs 操作,避免校验后 symlink 组件被替换。\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) {\n return { path: filePath, error: \"路径在工作区之外\" };\n }\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { path: filePath, error: `不是文件: ${filePath}` };\n }\n\n // 按窗口截取行,超长行截断并标注\n const selected: string[] = [];\n let total = 0;\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n // 记录本会话读到的版本:后续写入若发现磁盘内容已变(别的会话改过),\n // 会被 ConflictGuard 拦下,而不是静默覆盖\n guard?.noteRead(target, content);\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(`${i + 1}|${text}`);\n }\n } catch (exc) {\n return { path: filePath, 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 return function read_file(args: Record<string, unknown>) {\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 paths = Array.isArray(args.paths)\n ? args.paths.map((p) => String(p ?? \"\")).filter(Boolean)\n : [];\n if (paths.length > 0) {\n const files: Record<string, unknown>[] = [];\n let remainingLines = MULTI_READ_MAX_TOTAL_LINES;\n for (const filePath of paths) {\n const perFile = Math.min(maxLines, Math.max(1, remainingLines));\n const one = readOne(filePath, startLine, perFile);\n files.push(one);\n // 出错的文件不占预算;成功读取的按实际窗口扣减\n if (!one.error) remainingLines -= (one.content as string).split(\"\\n\").length;\n if (remainingLines <= 0) break;\n }\n return {\n files,\n count: files.length,\n ...(files.length < paths.length\n ? { note: `已达到单次多文件读取上限(共 ${MULTI_READ_MAX_TOTAL_LINES} 行),其余路径未读取` }\n : {}),\n };\n }\n\n const filePath = String(args.path ?? \"\");\n if (!filePath) {\n return { error: \"缺少 path(或 paths)参数\" };\n }\n return readOne(filePath, startLine, maxLines);\n };\n}\n\n/**\n * write_file — 路径范围检查 + UTF-8 写入\n *\n * 自动创建父目录;overwrite=false 时拒绝覆盖已存在文件。\n */\nfunction makeWriteFile(workspace: string, guard?: ConflictGuard) {\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 const conflict = guard?.checkWrite(target, filePath);\n if (conflict) return { error: conflict.message };\n\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, content, \"utf-8\");\n guard?.noteWrite(target, content);\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, guard?: ConflictGuard) {\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 const conflict = guard?.checkWrite(target, filePath);\n if (conflict) return { error: conflict.message };\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 guard?.noteWrite(target, updated);\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(\n registry: ToolRegistry,\n workspace: string,\n guard?: ConflictGuard,\n): void {\n const readFile = makeReadFile(workspace, guard);\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|text'),以便引用为 path:line。path 与 paths 至少传一个。大文件窗口化:传递 start_line 从上一次读取的位置继续。需要同时查看多个文件/区间时用 paths 一次读取,减少往返。只读。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n paths: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"一次读取多个文件 (与 path 二选一);多个候选文件应一次读完,不要逐个发请求\",\n },\n start_line: { type: \"integer\", description: \"起始行号 (从1开始,默认1)\" },\n max_lines: { type: \"integer\", description: `最大行数 (默认 ${DEFAULT_MAX_LINES},多文件合计上限 ${MULTI_READ_MAX_TOTAL_LINES})` },\n },\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(\n registry: ToolRegistry,\n workspace: string,\n guard?: ConflictGuard,\n): void {\n registerReadOnlyFileTools(registry, workspace, guard);\n\n const writeFile = makeWriteFile(workspace, guard);\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, guard);\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 *\n * 导出供自行做 `path.relative` 校验的调用方复用(如目录浏览新建目录、\n * git 面板的 repo / path 参数),避免各处重复写 `startsWith(\"..\")`。\n */\nexport function 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","filePath"]}
@@ -51,7 +51,7 @@ __export(git_exports, {
51
51
  });
52
52
  module.exports = __toCommonJS(git_exports);
53
53
  var fs = __toESM(require("fs"), 1);
54
- var path = __toESM(require("path"), 1);
54
+ var path2 = __toESM(require("path"), 1);
55
55
 
56
56
  // src/tools/execAsync.ts
57
57
  var import_node_child_process2 = require("child_process");
@@ -116,12 +116,12 @@ function makeSink(maxBuffer) {
116
116
  };
117
117
  }
118
118
  function execAsync(file, argv, opts = {}) {
119
- return new Promise((resolve2) => {
119
+ return new Promise((resolve3) => {
120
120
  const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
121
121
  const out = makeSink(maxBuffer);
122
122
  const err = makeSink(maxBuffer);
123
123
  if (opts.signal?.aborted) {
124
- resolve2({
124
+ resolve3({
125
125
  status: null,
126
126
  stdout: Buffer.alloc(0),
127
127
  stderr: Buffer.alloc(0),
@@ -142,7 +142,7 @@ function execAsync(file, argv, opts = {}) {
142
142
  windowsHide: true
143
143
  });
144
144
  } catch (e) {
145
- resolve2({
145
+ resolve3({
146
146
  status: null,
147
147
  stdout: Buffer.alloc(0),
148
148
  stderr: Buffer.alloc(0),
@@ -179,7 +179,7 @@ function execAsync(file, argv, opts = {}) {
179
179
  };
180
180
  if (aborted) result.aborted = true;
181
181
  if (spawnError) result.error = spawnError;
182
- resolve2(result);
182
+ resolve3(result);
183
183
  };
184
184
  const killAndSettle = () => {
185
185
  killTree(child.pid);
@@ -338,6 +338,9 @@ var ShellTaskManager = class {
338
338
  }
339
339
  };
340
340
 
341
+ // src/tools/sandbox.ts
342
+ var import_node_child_process4 = require("child_process");
343
+
341
344
  // src/tools/shell.ts
342
345
  var MAX_BUFFER = 1024 * 1024 * 5;
343
346
  function decodeOutput(buf) {
@@ -356,6 +359,12 @@ function decodeOutput(buf) {
356
359
  }
357
360
  var shellTasks = new ShellTaskManager();
358
361
 
362
+ // src/tools/pathSafety.ts
363
+ var path = __toESM(require("path"), 1);
364
+ function isInsideRelative(rel) {
365
+ return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
366
+ }
367
+
359
368
  // src/tools/git.ts
360
369
  async function runGit(repoDir, args, timeout = 15e3) {
361
370
  const res = await execAsync("git", ["-c", "core.quotepath=false", ...args], {
@@ -373,9 +382,9 @@ async function runGit(repoDir, args, timeout = 15e3) {
373
382
  async function resolveRepoRoot(workspace, pathArg) {
374
383
  let dir = workspace;
375
384
  if (pathArg) {
376
- const abs = path.resolve(workspace, pathArg);
385
+ const abs = path2.resolve(workspace, pathArg);
377
386
  try {
378
- dir = fs.statSync(abs).isDirectory() ? abs : path.dirname(abs);
387
+ dir = fs.statSync(abs).isDirectory() ? abs : path2.dirname(abs);
379
388
  } catch {
380
389
  dir = abs;
381
390
  }
@@ -388,24 +397,24 @@ function repoDirArg(workspace, raw) {
388
397
  if (typeof raw !== "string") return void 0;
389
398
  const v = raw.trim();
390
399
  if (!v) return void 0;
391
- const base = path.resolve(workspace);
392
- const abs = path.resolve(base, v);
393
- const rel = path.relative(base, abs);
394
- if (rel.startsWith("..") || path.isAbsolute(rel)) return void 0;
400
+ const base = path2.resolve(workspace);
401
+ const abs = path2.resolve(base, v);
402
+ const rel = path2.relative(base, abs);
403
+ if (!isInsideRelative(rel)) return void 0;
395
404
  return abs;
396
405
  }
397
406
  function toPathspec(repoRoot, workspace, pathArg) {
398
407
  if (!pathArg) return null;
399
- const abs = path.resolve(workspace, pathArg);
400
- let rel = path.relative(repoRoot, abs);
408
+ const abs = path2.resolve(workspace, pathArg);
409
+ let rel = path2.relative(repoRoot, abs);
401
410
  if (rel === "") return ".";
402
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
411
+ if (!isInsideRelative(rel)) {
403
412
  try {
404
413
  const realRepo = fs.realpathSync(repoRoot);
405
414
  const realAbs = fs.realpathSync(abs);
406
- const rel2 = path.relative(realRepo, realAbs);
415
+ const rel2 = path2.relative(realRepo, realAbs);
407
416
  if (rel2 === "") return ".";
408
- if (!rel2.startsWith("..") && !path.isAbsolute(rel2)) return rel2;
417
+ if (isInsideRelative(rel2)) return rel2;
409
418
  } catch {
410
419
  }
411
420
  return null;
@@ -831,16 +840,16 @@ var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".s
831
840
  var SCAN_DEFAULT_DEPTH = 3;
832
841
  var SCAN_MAX_DIRS = 2e3;
833
842
  function readRepoHead(repoDir) {
834
- const dotGit = path.join(repoDir, ".git");
843
+ const dotGit = path2.join(repoDir, ".git");
835
844
  try {
836
845
  let headFile;
837
846
  if (fs.statSync(dotGit).isDirectory()) {
838
- headFile = path.join(dotGit, "HEAD");
847
+ headFile = path2.join(dotGit, "HEAD");
839
848
  } else {
840
849
  const raw = fs.readFileSync(dotGit, "utf-8");
841
850
  const m = /^gitdir:\s*(.+)$/m.exec(raw);
842
851
  if (!m) return {};
843
- headFile = path.join(path.resolve(repoDir, m[1].trim()), "HEAD");
852
+ headFile = path2.join(path2.resolve(repoDir, m[1].trim()), "HEAD");
844
853
  }
845
854
  const head = fs.readFileSync(headFile, "utf-8").trim();
846
855
  const prefix = "ref: refs/heads/";
@@ -851,7 +860,7 @@ function readRepoHead(repoDir) {
851
860
  return {};
852
861
  }
853
862
  function scanGitRepos(workspace, depth = SCAN_DEFAULT_DEPTH) {
854
- const root = path.resolve(workspace);
863
+ const root = path2.resolve(workspace);
855
864
  const maxDepth = Number.isFinite(depth) ? Math.min(Math.max(Math.floor(depth), 0), 6) : SCAN_DEFAULT_DEPTH;
856
865
  const repos = [];
857
866
  const stack = [{ dir: root, depth: 0 }];
@@ -864,11 +873,11 @@ function scanGitRepos(workspace, depth = SCAN_DEFAULT_DEPTH) {
864
873
  truncated = true;
865
874
  break;
866
875
  }
867
- const rel = path.relative(root, dir).split(path.sep).join("/");
868
- if (fs.existsSync(path.join(dir, ".git"))) {
876
+ const rel = path2.relative(root, dir).split(path2.sep).join("/");
877
+ if (fs.existsSync(path2.join(dir, ".git"))) {
869
878
  repos.push({
870
879
  path: rel,
871
- name: rel ? path.basename(dir) : path.basename(root),
880
+ name: rel ? path2.basename(dir) : path2.basename(root),
872
881
  ...readRepoHead(dir)
873
882
  });
874
883
  }
@@ -881,7 +890,7 @@ function scanGitRepos(workspace, depth = SCAN_DEFAULT_DEPTH) {
881
890
  }
882
891
  for (const e of entries) {
883
892
  if (!e.isDirectory() || SCAN_SKIP_DIRS.has(e.name)) continue;
884
- stack.push({ dir: path.join(dir, e.name), depth: d + 1 });
893
+ stack.push({ dir: path2.join(dir, e.name), depth: d + 1 });
885
894
  }
886
895
  }
887
896
  repos.sort(
@@ -891,7 +900,7 @@ function scanGitRepos(workspace, depth = SCAN_DEFAULT_DEPTH) {
891
900
  }
892
901
  function makeGitRepos(workspace) {
893
902
  return async function git_repos(args) {
894
- const root = path.resolve(workspace);
903
+ const root = path2.resolve(workspace);
895
904
  try {
896
905
  if (!fs.statSync(root).isDirectory()) return { root, repos: [], error: "\u5DE5\u4F5C\u533A\u4E0D\u662F\u76EE\u5F55" };
897
906
  } catch {