berry-agent 0.1.0-alpha.1

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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * L2 tools — 检索族工具(find / grep——04 §7 检索族段;与 fs 族同件聚落)。
3
+ *
4
+ * 设计要点(04 §7 检索族批定名):
5
+ * - 纯 TS 首发:node:fs 遍历 + `ignore` 包 gitignore 语义 + 手写 glob→RegExp
6
+ * (`*` 段内 / `**` 独占段跨任意层目录 / `?` 单字符)——零新依赖零新概念
7
+ * (ripgrep 外部二进制已被 07 篇「native 依赖唯一宿主 persist」纪律否决;
8
+ * 性能大库场景挂账观察);glob 匹配器不引第三方包同理;
9
+ * - 遍历语义与技能发现/快照遍历三副本同笔同判(06 篇 §11.1 gitignore 前缀
10
+ * 化锚定判据——纯 basename 模式前缀化插 `**`+`/` 双段保深层同配);常量剪枝
11
+ * node_modules / .git;不跟随符号链(Dirent.isDirectory 对符号链返回
12
+ * false——防环;symlink 文件同样不进结果);
13
+ * - 编码纪律同律(本仓与蓝本分叉点):扫描读与 read 同口径 UTF-8 严格——
14
+ * 非 UTF-8 文件跳过并计数(不 lossy 容错、命中行绝不带 U+FFFD 进上下文);
15
+ * 截断文件的截点先回退 UTF-8 安全边界再解码(不因截断误报非 UTF-8);
16
+ * - 护栏两条帽(find 路径数 / grep 文件数或行数缺省 200 达限早停;单文件
17
+ * 扫描 256 KiB 只扫前段)+ 二进制跳过(前 8 KiB 含 NUL 字节启发)——
18
+ * 全部在结果内注明非静默;
19
+ * - 只读族天然免审批面(effect: 'read');不登记观察态(与 ls 同判:检索
20
+ * 是扫描不是精读,CAS 的「观察」只锚 read 工具);
21
+ * - 参数语法错(glob 非法 / 正则编译失败)复用 TOOL_INVALID_ARGS,不新增
22
+ * 错误码(错误码注册表零膨胀)。
23
+ */
24
+ import { open, readdir, readFile, stat } from 'node:fs/promises';
25
+ import { isAbsolute, join, relative, resolve as resolvePath } from 'node:path';
26
+ import ignore from 'ignore';
27
+ import { Type } from 'typebox';
28
+ import { BaseError } from '../contracts/index.js';
29
+ import { canonicalWorkspaceRoot } from '../context/index.js';
30
+ import { canonicalize } from './fs.js';
31
+ import { assertInodeNotProtected, rejectProtectedReadPath } from './protected-read.js';
32
+ /** 常量剪枝表:无论 .gitignore 写没写都跳过的目录名(依赖目录与 git 元数据) */
33
+ const PRUNE_DIRS = new Set(['node_modules', '.git']);
34
+ /** 平台路径分隔符归一为 /(glob 与 gitignore 模式语义都在 posix 路径上) */
35
+ function toPosix(p) {
36
+ return p.split('\\').join('/');
37
+ }
38
+ /**
39
+ * glob 模式编译为「相对遍历根路径」匹配 RegExp(04 §7 检索族段词汇面)。
40
+ *
41
+ * 语法(最小词汇面,无字符类/花括号展开——够用即止防面膨胀):
42
+ * - `**` 独占一段 = 任意层目录(含零层):尾段形 `a/**` = a 下任意深度任意
43
+ * 路径;非尾段 = 任意层目录前缀;中段 = 两侧之间任意层;
44
+ * - `*` = 段内任意字符(不跨 `/`);
45
+ * - `?` = 段内单个字符;
46
+ * - 其余字符字面匹配(正则元字符转义)。
47
+ *
48
+ * @throws BaseError(TOOL_INVALID_ARGS) 段内混嵌 `**`(如 `a**b`)——语义不明拒绝
49
+ */
50
+ export function globToRegExp(pattern) {
51
+ const segments = pattern.split('/');
52
+ let source = '';
53
+ for (let i = 0; i < segments.length; i++) {
54
+ const seg = segments[i];
55
+ if (seg === undefined)
56
+ continue; // noUncheckedIndexedAccess 收窄(split 产物实际不会缺)
57
+ const isLast = i === segments.length - 1;
58
+ if (seg === '**') {
59
+ // 尾段 `a/**` = a 下任意深度任意路径;非尾段 = 任意层目录前缀(含零层)
60
+ source += isLast ? '[^/]*(?:/[^/]+)*' : '(?:[^/]+/)*';
61
+ continue;
62
+ }
63
+ // 段内编译:`*`/`?` 通配,其余字面转义;段中出现 `**` 片段(非整段)即非法
64
+ let segRe = '';
65
+ for (let j = 0; j < seg.length; j++) {
66
+ const ch = seg[j]; // 索引安全(长度内取值)
67
+ if (ch === '*' && seg[j + 1] === '*') {
68
+ throw new BaseError('TOOL_INVALID_ARGS', `[TOOL_INVALID_ARGS] glob 模式非法:\`**\` 须独占一段(收到 ${JSON.stringify(seg)})`);
69
+ }
70
+ if (ch === '*')
71
+ segRe += '[^/]*';
72
+ else if (ch === '?')
73
+ segRe += '[^/]';
74
+ else
75
+ segRe += ch.replace(/[\\^$.+()[\]{}|]/g, '\\$&');
76
+ }
77
+ source += segRe + (isLast ? '' : '/');
78
+ }
79
+ return new RegExp(`^${source}$`);
80
+ }
81
+ /**
82
+ * 单行 gitignore 模式前缀化(06 篇 §11.1 锚定判据——与技能发现/快照遍历三副
83
+ * 本同笔同判):注释/空行丢弃;`!` 否定与 `\!`/`\#` 转义保留。
84
+ *
85
+ * 锚定判据:模式体(去尾随目录标记斜杠——尾随 / 是目录标记不是锚点)含斜
86
+ * 杠或带前导 `/` = 相对路径锚定本层精确匹配;纯 basename 模式(如 `build`、
87
+ * `secret/`)= 所在目录子树**任意深度** basename 匹配——前缀化须插 `**`+`/` 才
88
+ * 能同时匹配本层(sub/build)与深层(sub/x/build);直接拼前缀会造出锚定路
89
+ * 径,深层漏配——被忽略目录里的文件仍入检索面。根 .gitignore(prefix 空)
90
+ * basename 形原样透传天然任意深度;前导 / 锚定形保留前导 /(锚定根层)。
91
+ */
92
+ function prefixIgnorePattern(line, prefix) {
93
+ const trimmed = line.trim();
94
+ if (!trimmed)
95
+ return null;
96
+ if (trimmed.startsWith('#') && !trimmed.startsWith('\\#'))
97
+ return null;
98
+ let pattern = line;
99
+ let negated = false;
100
+ if (pattern.startsWith('!')) {
101
+ negated = true;
102
+ pattern = pattern.slice(1);
103
+ }
104
+ else if (pattern.startsWith('\\!')) {
105
+ pattern = pattern.slice(1);
106
+ }
107
+ // 前导 / = 显式锚定标记(先记下再去掉;锚定形相对本目录精确匹配)
108
+ const rooted = pattern.startsWith('/');
109
+ if (rooted)
110
+ pattern = pattern.slice(1);
111
+ if (!prefix) {
112
+ // 根 .gitignore 锚定形保留前导 /(git 语义:前导分隔符 = 锚定本层——剥掉
113
+ // 透传会把锚定降级成任意层匹配,根层同名深层被误剪;技能发现/快照遍历
114
+ // 同笔同判同修——2026-09-07 批 14b 回归锁在「根层锚定只剪本层」例)
115
+ const passed = rooted ? `/${pattern}` : pattern;
116
+ return negated ? `!${passed}` : passed;
117
+ }
118
+ const body = pattern.endsWith('/') ? pattern.slice(0, -1) : pattern;
119
+ const anchored = rooted || body.includes('/');
120
+ const prefixed = anchored ? `${prefix}${pattern}` : `${prefix}**/${pattern}`;
121
+ return negated ? `!${prefixed}` : prefixed;
122
+ }
123
+ /**
124
+ * 读取 dir 下 .gitignore 并把模式按所在目录前缀化挂上匹配器(嵌套
125
+ * .gitignore 的规则只作用于其所在目录子树)。文件不存在/读失败静默跳过。
126
+ */
127
+ async function addIgnoreRules(matcher, dir, rootDir) {
128
+ let content;
129
+ try {
130
+ content = await readFile(join(dir, '.gitignore'), 'utf8');
131
+ }
132
+ catch {
133
+ return; // 无 .gitignore 或读失败(权限等)——本目录无额外规则
134
+ }
135
+ const prefix = toPosix(relative(rootDir, dir));
136
+ const prefixed = prefix ? `${prefix}/` : '';
137
+ const patterns = content
138
+ .split(/\r?\n/)
139
+ .map((line) => prefixIgnorePattern(line, prefixed))
140
+ .filter((line) => line !== null);
141
+ if (patterns.length > 0)
142
+ matcher.add(patterns);
143
+ }
144
+ /**
145
+ * gitignore-aware 递归遍历:枚举 root 子树全部普通文件(yield 顺序 = 目录序 ×
146
+ * 名称字典序,结果确定)。消费者 break 即早停(generator 自然终止)。
147
+ *
148
+ * 剪枝:PRUNE_DIRS 常量表 + 祖先链上所有 .gitignore 规则(逐目录前缀化挂载,
149
+ * 进入时读取);符号链目录不跟随(Dirent.isDirectory 对符号链返回 false——
150
+ * 防环)。
151
+ */
152
+ async function* walkFiles(root) {
153
+ const matcher = ignore();
154
+ const stack = [root]; // DFS 显式栈(目录序确定)
155
+ while (stack.length > 0) {
156
+ const dir = stack.pop();
157
+ await addIgnoreRules(matcher, dir, root);
158
+ let entries;
159
+ try {
160
+ entries = await readdir(dir, { withFileTypes: true });
161
+ }
162
+ catch {
163
+ continue; // 目录读失败(权限/竞态消失)——跳过本目录继续其余子树
164
+ }
165
+ // 倒序入栈 + pop 取首 → 保持名称字典序访问
166
+ const sorted = [...entries].sort((a, b) => b.name.localeCompare(a.name));
167
+ for (const entry of sorted) {
168
+ if (entry.isDirectory()) {
169
+ if (PRUNE_DIRS.has(entry.name))
170
+ continue;
171
+ // 根目录层 relative 为空串——不得拼出前导 /(ignore 包要求纯相对路径)
172
+ const dirPrefix = toPosix(relative(root, dir));
173
+ const rel = dirPrefix ? `${dirPrefix}/${entry.name}` : entry.name;
174
+ // 目录双测(带/不带尾斜杠):兼容 `dir/` 与 `dir` 两种规则写法
175
+ if (matcher.ignores(rel) || matcher.ignores(`${rel}/`))
176
+ continue;
177
+ stack.push(join(dir, entry.name));
178
+ }
179
+ else if (entry.isFile()) {
180
+ const rel = toPosix(relative(root, join(dir, entry.name)));
181
+ if (matcher.ignores(rel))
182
+ continue;
183
+ yield { rel, abs: join(dir, entry.name) };
184
+ }
185
+ }
186
+ }
187
+ }
188
+ /** 纯文本结果的快捷构造(与 fs.ts 同款私有小函数,不跨文件抽共享) */
189
+ function textResult(text, details) {
190
+ return { content: [{ type: 'text', text }], ...(details !== undefined ? { details } : {}) };
191
+ }
192
+ /**
193
+ * UTF-8 安全截点:buf[0..end) 末尾若含不完整多字节序列(lead 字节引导的
194
+ * 字符缺后续字节,或字符被腰斩)则回退到该序列 lead 之前——丢一个不完整
195
+ * 字符,保证截断产物必是合法 UTF-8(截断不误报非 UTF-8)。与 fs.ts 的保
196
+ * 头截断同语义;实现取「末尾完备性检测」形——本函数的 buf 常常自身就是
197
+ * 截断产物(length === maxBytes),不能假设截点后还有字节可查。
198
+ */
199
+ function utf8SafeEnd(buf, end) {
200
+ const safe = Math.min(end, buf.length);
201
+ if (safe === 0)
202
+ return 0;
203
+ // 从末字节向前跳过全部 continuation 字节,停在该字符的 lead 字节
204
+ let i = safe - 1;
205
+ while (i > 0 && (buf[i] & 0xc0) === 0x80)
206
+ i--;
207
+ const lead = buf[i];
208
+ // lead 字节高 4 位给出编码长度(ASCII=1 / C0-DF=2 / E0-EF=3 / F0-F7=4)
209
+ const need = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
210
+ // 序列在 [i, safe) 内的实际长度不足 = 末字符被截断 → 截到 lead 前(整字符丢弃)
211
+ return safe - i >= need ? safe : i;
212
+ }
213
+ /**
214
+ * 读文件前段用于扫描(04 §7 定形④统一形——2026-09-08 P0①):单 open + 句柄
215
+ * fstat(guard 挂入)+ 按大小分支读,消除 stat 与 read 两路间的 TOCTOU。
216
+ * 超大文件不整读(只取前段防占内存);解码严格 UTF-8(fatal——lossy 即标记
217
+ * nonUtf8,由调用方跳过该文件;绝不产 U+FFFD 乱码进命中行)。
218
+ */
219
+ async function readCappedStrict(abs, maxBytes, protectedReadFiles) {
220
+ const fh = await open(abs, 'r');
221
+ try {
222
+ // open 后 inode 判:硬链别名 + TOCTOU 换靶同判收口(与 read 工具同一判据)
223
+ const fst = await fh.stat();
224
+ await assertInodeNotProtected(abs, fst, protectedReadFiles);
225
+ if (fst.size <= maxBytes) {
226
+ return decodeStrict(await fh.readFile(), false);
227
+ }
228
+ const buf = Buffer.alloc(maxBytes);
229
+ await fh.read(buf, 0, maxBytes, 0);
230
+ // 截点回退 UTF-8 安全边界后再解码——截断不误报非 UTF-8
231
+ return decodeStrict(buf.subarray(0, utf8SafeEnd(buf, maxBytes)), true);
232
+ }
233
+ finally {
234
+ await fh.close();
235
+ }
236
+ }
237
+ /** 严格 UTF-8 解码(fatal TextDecoder——04 §7 编码纪律同律;lossy 标记 nonUtf8) */
238
+ function decodeStrict(raw, truncated) {
239
+ const decoder = new TextDecoder('utf-8', { fatal: true });
240
+ try {
241
+ return { text: decoder.decode(raw), truncated, nonUtf8: false };
242
+ }
243
+ catch {
244
+ // 非 UTF-8 文件:跳过计数(调用方职责)——与 read 的拒绝面同一指引(bash 转档)
245
+ return { text: '', truncated, nonUtf8: true };
246
+ }
247
+ }
248
+ /** 单文件正则扫描:返回命中行(1 起行号)与截断/二进制/非 UTF-8 标记 */
249
+ async function scanFile(abs, re, maxScanBytes, protectedReadFiles) {
250
+ const { text, truncated, nonUtf8 } = await readCappedStrict(abs, maxScanBytes, protectedReadFiles);
251
+ if (nonUtf8)
252
+ return { lines: [], truncated: false, binary: false, nonUtf8: true };
253
+ // 二进制探测:前 8 KiB 含 NUL 字节视为二进制(NUL 是合法 UTF-8——与编码探测独立)
254
+ if (text.substring(0, 8192).includes('\0')) {
255
+ return { lines: [], truncated: false, binary: true, nonUtf8: false };
256
+ }
257
+ const lines = [];
258
+ const rawLines = text.split('\n');
259
+ for (let i = 0; i < rawLines.length; i++) {
260
+ const raw = rawLines[i];
261
+ if (raw === undefined)
262
+ continue; // noUncheckedIndexedAccess 收窄(split 产物实际不会缺)
263
+ if (re.test(raw)) {
264
+ // 行内 \r 去;超长行截 500 字符(防单行 minified 文件刷屏)
265
+ const line = raw.replace(/\r$/, '');
266
+ lines.push({ no: i + 1, text: line.length > 500 ? `${line.slice(0, 500)}…(行超长已截断)` : line });
267
+ }
268
+ }
269
+ return { lines, truncated, binary: false, nonUtf8: false };
270
+ }
271
+ /**
272
+ * 组装检索族工具(find / grep——04 §7 检索族段)。与 fs 工具族同装配点并列
273
+ * 注册(装配层把族注册进驱动层/全局层)。
274
+ */
275
+ export function createSearchTools(opts = {}) {
276
+ const workspace = opts.workspace ?? (() => canonicalWorkspaceRoot());
277
+ const maxScanBytes = opts.maxScanBytes ?? 256 * 1024;
278
+ const maxResults = opts.maxResults ?? 200;
279
+ const protectedReadFiles = opts.protectedReadFiles ?? (() => []);
280
+ /** 用户给出的路径 → 绝对路径(相对路径锚 workspace;与 fs.ts 同语义) */
281
+ const resolveTarget = (p) => (isAbsolute(p) ? resolvePath(p) : resolvePath(workspace(), p));
282
+ /* ---------------- find:glob 模式找文件路径 ---------------- */
283
+ const findTool = {
284
+ name: 'find',
285
+ effect: 'read',
286
+ description: '按 glob 模式查找文件路径(`*` 匹配段内任意字符、`**` 跨任意层目录、`?` 单字符,如 "**/*.ts"、"src/**")。遍历尊重 .gitignore(逐目录规则),不进 node_modules 与 .git,不跟随符号链。返回按路径排序的相对路径列表,最多 200 条(达限截断注记)。',
287
+ parameters: Type.Object({
288
+ pattern: Type.String({ description: 'glob 模式(匹配相对遍历根的路径)' }),
289
+ path: Type.Optional(Type.String({ description: '遍历起点目录(缺省工作区根;相对路径锚工作区根)' })),
290
+ }),
291
+ execute: async (args) => {
292
+ const root = resolveTarget(args.path ?? '.');
293
+ // 遍历起点必须在场且是目录(不存在 = 与 ls 同判 FS_NOT_FOUND)
294
+ const rootSt = await stat(root).catch((err) => {
295
+ if (err.code === 'ENOENT') {
296
+ throw new BaseError('FS_NOT_FOUND', `[FS_NOT_FOUND] 目录不存在:${root}`);
297
+ }
298
+ throw err;
299
+ });
300
+ if (!rootSt.isDirectory()) {
301
+ throw new BaseError('TOOL_INVALID_ARGS', `[TOOL_INVALID_ARGS] find 遍历起点不是目录:${root}`);
302
+ }
303
+ // 模式编译失败(`**` 混嵌等)前置拒绝——参数语法错属调用方可修复
304
+ const re = globToRegExp(args.pattern);
305
+ const matches = [];
306
+ let truncated = false;
307
+ let scanned = 0;
308
+ for await (const file of walkFiles(root)) {
309
+ scanned++;
310
+ if (!re.test(file.rel))
311
+ continue;
312
+ if (matches.length >= maxResults) {
313
+ truncated = true;
314
+ break; // 达输出上限早停(遍历剩余子树不再扫)
315
+ }
316
+ matches.push(file.rel);
317
+ }
318
+ matches.sort((a, b) => a.localeCompare(b));
319
+ const body = matches.length === 0
320
+ ? '(无匹配文件)'
321
+ : `${matches.join('\n')}${truncated ? `\n…(已达 ${maxResults} 条上限,结果截断——收窄 pattern 或指定更小 path)` : ''}`;
322
+ return textResult(body, { root, matches: matches.length, scanned, truncated });
323
+ },
324
+ };
325
+ /* ---------------- grep:正则扫描内容(两种输出模式) ---------------- */
326
+ const grepTool = {
327
+ name: 'grep',
328
+ effect: 'read',
329
+ description: '按正则表达式搜索文件内容(JavaScript RegExp 语法)。目标可以是目录(递归遍历,尊重 .gitignore、跳过 node_modules/.git、二进制与非 UTF-8 文件)或单个文件(用户意图直扫,不做过滤)。output_mode=files_with_matches 返回命中文件路径列表(缺省);content 返回「路径:行号:命中行」。单文件超 256 KiB 只扫前段。最多 200 条命中(达限截断注记)。',
330
+ parameters: Type.Object({
331
+ pattern: Type.String({ description: '正则表达式(JavaScript RegExp 语法)' }),
332
+ path: Type.Optional(Type.String({ description: '搜索目标:目录(递归)或文件;缺省工作区根' })),
333
+ output_mode: Type.Optional(Type.Union([Type.Literal('files_with_matches'), Type.Literal('content')], {
334
+ description: '输出模式:files_with_matches(命中文件列表,缺省)| content(带行号命中行)',
335
+ })),
336
+ glob: Type.Optional(Type.String({ description: '文件名过滤(glob 语法同 find;只搜名字匹配的文件,如 "**/*.ts")' })),
337
+ }),
338
+ execute: async (args) => {
339
+ const target = resolveTarget(args.path ?? '.');
340
+ const mode = args.output_mode ?? 'files_with_matches';
341
+ // 正则编译失败前置拒绝(参数语法错);'u' 旗标不设——部分旧式模式含裸量词组
342
+ let re;
343
+ try {
344
+ re = new RegExp(args.pattern);
345
+ }
346
+ catch (err) {
347
+ throw new BaseError('TOOL_INVALID_ARGS', `[TOOL_INVALID_ARGS] 正则编译失败:${args.pattern}(${err.message})`);
348
+ }
349
+ // 可选 glob 过滤(相对遍历根匹配——与 find 同一编译器)
350
+ const globRe = args.glob !== undefined ? globToRegExp(args.glob) : undefined;
351
+ const files = []; // files 模式:命中文件路径
352
+ const contentLines = []; // content 模式:rel:行号:行
353
+ let truncated = false;
354
+ let skippedBinary = 0;
355
+ let skippedNonUtf8 = 0;
356
+ let skippedOversize = 0;
357
+ let scanned = 0;
358
+ /** 单文件扫描 + 计数入账(达限返回 true 供早停) */
359
+ const consumeFile = async (rel, abs) => {
360
+ scanned++;
361
+ const result = await scanFile(abs, re, maxScanBytes, protectedReadFiles);
362
+ if (result.nonUtf8) {
363
+ skippedNonUtf8++;
364
+ return false;
365
+ }
366
+ if (result.binary) {
367
+ skippedBinary++;
368
+ return false;
369
+ }
370
+ if (result.truncated)
371
+ skippedOversize++;
372
+ if (result.lines.length === 0)
373
+ return false;
374
+ if (mode === 'files_with_matches') {
375
+ if (files.length >= maxResults)
376
+ return true;
377
+ files.push(rel);
378
+ return files.length >= maxResults;
379
+ }
380
+ for (const line of result.lines) {
381
+ if (contentLines.length >= maxResults)
382
+ return true;
383
+ contentLines.push(`${rel}:${line.no}:${line.text}`);
384
+ }
385
+ return contentLines.length >= maxResults;
386
+ };
387
+ // 目标形态分派:单文件显式指定 = 用户意图(不做 ignore/glob 过滤);
388
+ // 目录 = 遍历 + glob 过滤 + gitignore 剪枝。读侧 carve-out(04 §7 定形
389
+ // ③——2026-09-08 P0①):单文件 open 前路径判(canonical 剥符号链别
390
+ // 名);目录遍历命中敏感件 = **整调用硬拒**(不剪枝数据目录——剪枝即
391
+ // 向模型泄露「这里有什么不能扫」的存在性测绘面)。walkFiles 不跟随符
392
+ // 号链 → 遍历根的 canonical 前缀是唯一别名面(根 canonicalize 一次即
393
+ // 钉死;产出路径逐件过路径判,open 后另有 inode 判兜底)
394
+ const st = await stat(target).catch((err) => {
395
+ if (err.code === 'ENOENT') {
396
+ throw new BaseError('FS_NOT_FOUND', `[FS_NOT_FOUND] 搜索目标不存在:${target}`);
397
+ }
398
+ throw err;
399
+ });
400
+ if (st.isFile()) {
401
+ // 显示路径:workspace 内相对形;workspace 外退绝对路径(用户可见形)。
402
+ // 单文件 content 模式命中行也可能达限(超长文件)——返回值同样驱动截断注记
403
+ const rel = toPosix(relative(workspace(), target)) || toPosix(target);
404
+ rejectProtectedReadPath(await canonicalize(target), protectedReadFiles);
405
+ truncated = await consumeFile(rel, target);
406
+ }
407
+ else if (st.isDirectory()) {
408
+ // 遍历根 canonicalize 一次钉死别名面(规范定形③原文——如 macOS
409
+ // /var → /private/var:别名根下逐件路径判会全 miss 只剩 inode 兜底);
410
+ // walkFiles 不跟随符号链 → 根前缀是子树内唯一符号链面,此后产出路径
411
+ // 恒 canonical
412
+ for await (const file of walkFiles(await canonicalize(target))) {
413
+ if (globRe !== undefined && !globRe.test(file.rel))
414
+ continue;
415
+ // 扫描脸执法(04 §7 定形③):过了 glob 过滤、即将被 open 扫描的
416
+ // 文件逐件 pre-open 路径比对——命中整调用硬拒;glob 排除在扫描面
417
+ // 外的文件不触发(扫描脸语义——不扫不拒)。数据目录不剪枝(剪枝
418
+ // 即向模型泄露存在性测绘面);walkFiles 不跟随符号链 → 根的
419
+ // canonical 前缀是唯一别名面(根 canonicalize 一次即钉死)
420
+ rejectProtectedReadPath(file.abs, protectedReadFiles);
421
+ if (await consumeFile(file.rel, file.abs)) {
422
+ truncated = true; // 达限早停 + 截断注记(结果内注明非静默——04 §7 护栏条款)
423
+ break;
424
+ }
425
+ }
426
+ }
427
+ else {
428
+ throw new BaseError('TOOL_INVALID_ARGS', `[TOOL_INVALID_ARGS] 非普通文件/目录目标:${target}`);
429
+ }
430
+ const truncNote = truncated ? `\n…(已达 ${maxResults} 条上限,结果截断——收窄 pattern/path)` : '';
431
+ const body = mode === 'files_with_matches'
432
+ ? files.length === 0
433
+ ? '(无命中文件)'
434
+ : `${files.join('\n')}${truncNote}`
435
+ : contentLines.length === 0
436
+ ? '(无命中行)'
437
+ : `${contentLines.join('\n')}${truncNote}`;
438
+ return textResult(body, {
439
+ mode,
440
+ target,
441
+ scanned,
442
+ matched: mode === 'files_with_matches' ? files.length : contentLines.length,
443
+ skippedBinary,
444
+ skippedNonUtf8,
445
+ skippedOversize,
446
+ truncated,
447
+ });
448
+ },
449
+ };
450
+ return { tools: [findTool, grepTool] };
451
+ }