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,359 @@
1
+ /**
2
+ * host/plugin-tools — 插件生命周期模型工具族(03 §5.6——task #89 笔三;
3
+ * §5.8 三面同源之模型面)。
4
+ *
5
+ * 宿主固定八件恒挂载(boot 全局层并入——与装载器产出的插件工具同位,不随
6
+ * 换代卸除):只读三件 model-direct(`effect: 'read'`——plugins_list /
7
+ * events_query / plugin_uninstall_inspect)+ 写类五件(`effect: 'write'`——
8
+ * 守门管道 write-effect 审批对自动执法,safety/gate.ts 单键触发,本件零手写
9
+ * 审批逻辑)。
10
+ *
11
+ * **与 TUI/CLI 两面的分立点**(§5.2/§5.8):
12
+ * 1. **模型面不自动链 reload**——mount 族成功后回执指路 /reload(模型不
13
+ * 擅自重启管线;TUI 命令面自动链、CLI 短命进程「下次启动生效」三面
14
+ * 各自诚实);
15
+ * 2. **uninstall execute 不入模型面**(人面独占)——只到 inspect 为止;
16
+ * reload 不入工具族(TUI /reload 与 CLI 对等承担);
17
+ * 3. **config 变更不设独立 configure 动词**——mount 撞名拒下改配置 =
18
+ * unmount 后重 mount(§5.3 后写胜出)。
19
+ *
20
+ * **addedToolNames 诚实空**(§2.8 双通道):装机/挂载类动作的工具结果携带
21
+ * 新注册工具名清单——但模型面动作时点「装机零生效」(§5.4)且不自动链
22
+ * reload,插件工具注册发生在人面 /reload 后的新代装载;故本族结果恒携带
23
+ * `addedToolNames: []`(通道在位、值诚实空——通道真值的发射位在新代装载
24
+ * 器侧,随 durable 装载史载体批挂账)。
25
+ *
26
+ * 执行件全复用三面同源真源:installPlugin/updatePlugin(plugin-install)、
27
+ * mountRow/unmountRow/toggleRow + readLedger(plugin-store)、inspectUninstall
28
+ * (plugin-uninstall)——本件只做模型面参数 schema + 回执文案 + 前置查。
29
+ */
30
+ import { Type } from 'typebox';
31
+ import { BaseError } from '../contracts/index.js';
32
+ import { formatPluginFailureText } from './boot-failures.js';
33
+ import { checkPluginId } from './manifest.js';
34
+ import { createDefaultSpawnRunner, installPlugin, updatePlugin } from './plugin-install.js';
35
+ import { inspectUninstall } from './plugin-uninstall.js';
36
+ import { mountRow, readEnabledRowsForEdit, readLedger, toggleRow, unmountRow } from './plugin-store.js';
37
+ /** 装载生效指路句(模型面回执统一尾——§5.2 模型面不自动链 reload) */
38
+ const RELOAD_HINT = '装载生效需 /reload(模型面不自动链 reload——03 §5.2;人面 / TUI 命令面可代跑)';
39
+ /** events_query data 摘要截断帽(§5.6——~300 字符归模型面) */
40
+ const DATA_SUMMARY_LIMIT = 300;
41
+ /** 统一异常编码(03 §2.3——BaseError 携码前置披露;obs/session-tools 同款先例) */
42
+ async function guard(run) {
43
+ try {
44
+ return await run();
45
+ }
46
+ catch (error) {
47
+ const code = error instanceof BaseError ? error.code : undefined;
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ return {
50
+ content: [{ type: 'text', text: code ? `[${code}] ${message}` : message }],
51
+ isError: true,
52
+ addedToolNames: [], // §2.8 通道在位——错误结果同样携带(当次无新面)
53
+ };
54
+ }
55
+ }
56
+ /** 纯文本成功回执速记(§2.8——addedToolNames 恒携带,值诚实空) */
57
+ function textResult(text) {
58
+ return { content: [{ type: 'text', text }], addedToolNames: [] };
59
+ }
60
+ /** 纯文本错误回执(数据面错误——照常进上下文配对) */
61
+ function textError(message) {
62
+ return { content: [{ type: 'text', text: message }], isError: true, addedToolNames: [] };
63
+ }
64
+ /** ISO 8601 → 毫秒时间戳(坏形抛错——guard 收口 isError) */
65
+ function parseIsoMs(label, value) {
66
+ const ms = Date.parse(value);
67
+ if (Number.isNaN(ms)) {
68
+ throw new Error(`参数 ${label} 非法 ISO 8601 时间戳(得 "${value}"——形如 2026-09-09T12:00:00Z)`);
69
+ }
70
+ return ms;
71
+ }
72
+ /** data 摘要(JSON 单行化 + ~300 字符截断——§5.6 归模型面的呈现纪律) */
73
+ function summarizeData(data) {
74
+ let text;
75
+ try {
76
+ text = JSON.stringify(data) ?? 'null';
77
+ }
78
+ catch {
79
+ text = String(data); // 结构循环等病态兜底(冻结快照实际不可达——防御位)
80
+ }
81
+ return text.length > DATA_SUMMARY_LIMIT ? `${text.slice(0, DATA_SUMMARY_LIMIT)}…(截断)` : text;
82
+ }
83
+ /** source 徽标判(core: 前缀 → core;用户件查账本;查无 → '?' 诚实缺席) */
84
+ function badgeOf(id, ledgerById) {
85
+ if (id.startsWith('core:'))
86
+ return 'core';
87
+ return ledgerById.get(id)?.source ?? '?';
88
+ }
89
+ /** plugins_list 四态渲染(装载真源 = 内存 boot 报告;装机真源 = 磁盘账本) */
90
+ function renderPluginsList(deps) {
91
+ const lines = [];
92
+ // 装机账本读(installed-unmounted 分区判据 + source 徽标源;坏账本诚实缺席)
93
+ let ledgerById = new Map();
94
+ let ledgerNote;
95
+ if (deps.dataDir !== null) {
96
+ const read = readLedger(deps.dataDir, deps.fs);
97
+ if (read.ok)
98
+ ledgerById = new Map(read.entries.map((entry) => [entry.id, entry]));
99
+ else
100
+ ledgerNote = read.reason;
101
+ }
102
+ const report = deps.report();
103
+ if (report === undefined) {
104
+ lines.push('装载面未装配(noPlugins 诊断形)——装载态三分区缺席,仅呈装机面。');
105
+ }
106
+ else {
107
+ lines.push(`mounted(${report.activated.length}):`);
108
+ for (const item of report.activated) {
109
+ lines.push(` ${item.id} [${badgeOf(item.id, ledgerById)}]`);
110
+ }
111
+ lines.push(`mounted-disabled(${report.skipped.length}):`);
112
+ for (const item of report.skipped) {
113
+ lines.push(` ${item.id} [${badgeOf(item.id, ledgerById)}] ${item.reason}`);
114
+ }
115
+ lines.push(`failed(${report.failed.length}):`);
116
+ for (const item of report.failed) {
117
+ lines.push(` ${item.id} [${badgeOf(item.id, ledgerById)}] ${formatPluginFailureText(item)}`);
118
+ }
119
+ }
120
+ // installed-unmounted:账本 id 无启用行且不在装载报告(core: 恒不在账本——
121
+ // 两域天然无交集;报告 id 一并排除 = 报告/磁盘漂移防御——TUI mount 后
122
+ // 未 reload 的窗内行已在、报告未含,不误报「已装机未挂载」)
123
+ if (deps.dataDir !== null) {
124
+ if (ledgerNote !== undefined) {
125
+ lines.push(`installed-unmounted:缺席(装机账本读取失败:${ledgerNote})`);
126
+ }
127
+ else {
128
+ const rowsRead = readEnabledRowsForEdit(deps.dataDir, deps.fs);
129
+ if (!rowsRead.ok) {
130
+ lines.push(`installed-unmounted:缺席(启用清单读取失败:${rowsRead.message})`);
131
+ }
132
+ else {
133
+ const rowIds = new Set(rowsRead.rows.map((row) => row.id));
134
+ for (const id of report?.activated ?? [])
135
+ rowIds.add(id.id);
136
+ for (const id of report?.skipped ?? [])
137
+ rowIds.add(id.id);
138
+ for (const id of report?.failed ?? [])
139
+ rowIds.add(id.id);
140
+ const unmounted = [...ledgerById.values()].filter((entry) => !rowIds.has(entry.id));
141
+ lines.push(`installed-unmounted(${unmounted.length}):`);
142
+ for (const entry of unmounted) {
143
+ lines.push(` ${entry.id} [${entry.source}]`);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return lines.join('\n');
149
+ }
150
+ /**
151
+ * 模型面工具族工厂(装载位/测试唯一入口)——八件定义数组。写类五件
152
+ * `effect: 'write'`(审批对由守门管道自动执法——写动作永不裸跑);plugin_toggle
153
+ * 额外 `repeatable: false`(翻旗标非幂等——重放即翻回,禁静默重试)。
154
+ */
155
+ export function createPluginLifecycleTools(deps) {
156
+ // 装机/更新共用执行面(auditSink 透传——install 成功尾落 plugin/installed、
157
+ // update 成功尾落 plugin/updated,词形归执行件单源)
158
+ const installDeps = () => deps.dataDir === null
159
+ ? undefined
160
+ : {
161
+ dataDir: deps.dataDir,
162
+ fs: deps.fs,
163
+ spawn: deps.spawn,
164
+ ...(deps.env !== undefined ? { env: deps.env } : {}),
165
+ onLifecycleAudit: deps.auditSink,
166
+ };
167
+ return [
168
+ /* ---------------- 只读三件(model-direct) ---------------- */
169
+ {
170
+ name: 'plugins_list',
171
+ description: '插件装载态一览(四态:mounted 已挂载 / mounted-disabled 挂载但禁用 / ' +
172
+ 'installed-unmounted 已装机未挂载 / failed 装载失败;每项带 source 徽标 ' +
173
+ 'core|npm|git|local)。装载态分区读内存装载报告(/reload 后即新代),' +
174
+ '装机分区读磁盘账本。装机/启用的完整动词面见 plugin_install / plugin_mount 族。',
175
+ parameters: Type.Object({}, { additionalProperties: false }),
176
+ effect: 'read',
177
+ execute: async () => guard(() => Promise.resolve(textResult(renderPluginsList(deps)))),
178
+ },
179
+ {
180
+ name: 'events_query',
181
+ description: '跨会话 durable 事件查询(射程 = 会话事件流:turn/llm/compaction 等核心词;' +
182
+ 'plugin/* 生命周期词落 audit 审计流**不在本流**——本工具查不到)。每次查询先 ' +
183
+ 'flush 持久层(在飞事件落盘后可见——flushFirst 恒 true)。时窗参数用 ISO 8601' +
184
+ '(如 2026-09-09T12:00:00Z)。呈现行不带 session_id——跨会话查询如需定域请加 ' +
185
+ 'session_id 过滤。data 以 JSON 单行摘要呈现(~300 字符截断)。返回 nextCursor ' +
186
+ '时还有更多——原样回传续页。',
187
+ parameters: Type.Object({
188
+ session_id: Type.Optional(Type.String({ description: '会话 id 过滤维(缺省 = 跨会话全量)' })),
189
+ types: Type.Optional(Type.Array(Type.String(), { description: '事件类型过滤维(如 turn/end、llm/usage——本流核心词)' })),
190
+ since: Type.Optional(Type.String({ description: 'ISO 8601 起时窗(含)' })),
191
+ until: Type.Optional(Type.String({ description: 'ISO 8601 止时窗(含)' })),
192
+ limit: Type.Optional(Type.Number({ description: '页帽(缺省 1000,硬帽 10000)' })),
193
+ cursor: Type.Optional(Type.String({ description: '分页游标(上页 nextCursor 原样回传)' })),
194
+ }, { additionalProperties: false }),
195
+ effect: 'read',
196
+ execute: async (args) => guard(async () => {
197
+ // 时窗词形先行(坏形快拒——无 flush 无查询);条件展开构形(filter
198
+ // 键 readonly——非突变式)
199
+ const filter = {
200
+ ...(typeof args.session_id === 'string' && args.session_id !== '' ? { sessionId: args.session_id } : {}),
201
+ ...(Array.isArray(args.types) && args.types.length > 0 ? { types: args.types } : {}),
202
+ ...(typeof args.since === 'string' ? { sinceMs: parseIsoMs('since', args.since) } : {}),
203
+ ...(typeof args.until === 'string' ? { untilMs: parseIsoMs('until', args.until) } : {}),
204
+ ...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
205
+ ...(typeof args.cursor === 'string' && args.cursor !== '' ? { cursor: args.cursor } : {}),
206
+ };
207
+ // flushFirst 恒 true(§5.6——查询前落盘在飞事件)
208
+ await deps.flush();
209
+ const result = deps.queryEvents(filter);
210
+ if (result.events.length === 0) {
211
+ return textResult('(零事件——过滤维/时窗内无匹配;时窗用 ISO 8601、types 须事件词汇)');
212
+ }
213
+ const lines = result.events.map((event) => `${event.seq} ${new Date(event.time).toISOString()} ${event.type} ${summarizeData(event.data)}`);
214
+ const header = `seq time type data(${result.events.length} 条)`;
215
+ const tail = result.nextCursor !== null ? `\nnextCursor: ${result.nextCursor}(还有更多——原样回传续页)` : '';
216
+ return textResult([header, ...lines].join('\n') + tail);
217
+ }),
218
+ },
219
+ {
220
+ name: 'plugin_uninstall_inspect',
221
+ description: '插件卸载预检(只读零副作用——§5.5 UninstallReport 全量呈报将删项:启用' +
222
+ '行/装机物/域表/store_state 域键/数据域体量)。卸载执行(--confirm 两段式)' +
223
+ '是人面独占动词——模型面只到 inspect 为止,执行指路 CLI:' +
224
+ 'berry-agent plugins uninstall <id> --confirm [--data purge]。',
225
+ parameters: Type.Object({ id: Type.String({ description: '装机插件 id(core: 前缀非 uninstall 对象)' }) }, { additionalProperties: false }),
226
+ effect: 'read',
227
+ execute: async (args) => guard(async () => {
228
+ if (deps.dataDir === null) {
229
+ return textError('纯 memory 诊断形无数据目录——卸载预检不可用');
230
+ }
231
+ const outcome = inspectUninstall({ dataDir: deps.dataDir, fs: deps.fs, db: deps.db }, args.id);
232
+ return outcome.ok ? textResult(outcome.text) : textError(outcome.message);
233
+ }),
234
+ },
235
+ /* ---------------- 写类五件(审批对自动执法) ---------------- */
236
+ {
237
+ name: 'plugin_install',
238
+ description: '三源装机(写类——需审批)。ref 自含源前缀:npm:<pkg>[@<version>] / ' +
239
+ 'git:<url>[#<ref>] / local:<abs-path>。供应链四件套执法(钉版/禁 dev/' +
240
+ '静置窗缺省 24h/安装期脚本禁跑);装机零生效(词表账本收割但不装载)——' +
241
+ '启用走 plugin_mount。uninstall 的执行面是人面独占(CLI --confirm)。',
242
+ parameters: Type.Object({
243
+ ref: Type.String({
244
+ description: '三源自含前缀 ref(npm:acme-widgets@1.2.0 / git:https://…#v1 / local:/abs/path)',
245
+ }),
246
+ }, { additionalProperties: false }),
247
+ effect: 'write',
248
+ timeoutMs: 600_000, // npm/git 网络腿长动作(CLI 面无帽同义——10 分钟模型面帽)
249
+ execute: async (args) => guard(async () => {
250
+ const executorDeps = installDeps();
251
+ if (executorDeps === undefined) {
252
+ return textError('纯 memory 诊断形无数据目录——装机动词不可用');
253
+ }
254
+ const outcome = await installPlugin(executorDeps, args.ref);
255
+ return outcome.ok ? textResult(outcome.text) : textError(outcome.message);
256
+ }),
257
+ },
258
+ {
259
+ name: 'plugin_mount',
260
+ description: '挂载已装机插件(写类——需审批):写启用行。已有行(含禁用行)即撞名拒' +
261
+ '——改配置 = 先 plugin_unmount 再重 mount(§5.3 后写胜出,无独立 configure ' +
262
+ '动词)。core: 前缀官方件天然在场免装机查;用户件须先 plugin_install。' +
263
+ '装载生效需 /reload(模型面不自动链)。',
264
+ parameters: Type.Object({
265
+ id: Type.String({ description: '插件 id(小写字母数字连字符;官方件 core: 前缀)' }),
266
+ config: Type.Optional(Type.Unknown({ description: '插件配置值(JSON——行内 config 字段;装载侧深校验)' })),
267
+ }, { additionalProperties: false }),
268
+ effect: 'write',
269
+ execute: async (args) => guard(async () => {
270
+ if (deps.dataDir === null) {
271
+ return textError('纯 memory 诊断形无数据目录——行编辑动词不可用(行无落点)');
272
+ }
273
+ const id = args.id;
274
+ // 前置两查(与 TUI/CLI 同律——03 §5.8 三面同源单源复述)
275
+ if (!checkPluginId(id, { official: true })) {
276
+ return textError(`插件 id 词法违例(${id}——小写字母数字连字符,官方件 core: 前缀同律)`);
277
+ }
278
+ if (!id.startsWith('core:')) {
279
+ const ledgerRead = readLedger(deps.dataDir, deps.fs);
280
+ if (!ledgerRead.ok) {
281
+ return textError(`装机账本损坏:${ledgerRead.reason}——拒写防覆盖(03 §5.4)`);
282
+ }
283
+ if (!ledgerRead.entries.some((entry) => entry.id === id)) {
284
+ return textError(`插件 ${id} 未装机——先走 plugin_install(未装机挂行会在下次启动读侧降级,03 §5.3)`);
285
+ }
286
+ }
287
+ const result = mountRow(deps.dataDir, id, args.config, deps.fs, deps.auditSink);
288
+ if (!result.ok)
289
+ return textError(result.message); // 撞名/坏清单——零副作用
290
+ return textResult(`已挂载:${id}——${RELOAD_HINT}`);
291
+ }),
292
+ },
293
+ {
294
+ name: 'plugin_unmount',
295
+ description: '卸下插件(写类——需审批):删启用行保装机(重挂走 plugin_mount)。' +
296
+ 'core: 官方件内置全启无行可删——临时停用走 plugin_toggle。' +
297
+ '装载生效需 /reload(模型面不自动链)。',
298
+ parameters: Type.Object({ id: Type.String({ description: '插件 id' }) }, { additionalProperties: false }),
299
+ effect: 'write',
300
+ execute: async (args) => guard(async () => {
301
+ if (deps.dataDir === null) {
302
+ return textError('纯 memory 诊断形无数据目录——行编辑动词不可用(行无落点)');
303
+ }
304
+ const result = unmountRow(deps.dataDir, args.id, deps.fs, deps.auditSink);
305
+ if (!result.ok)
306
+ return textError(result.message);
307
+ return textResult(`已卸下:${args.id}(装机保留)——${RELOAD_HINT}`);
308
+ }),
309
+ },
310
+ {
311
+ name: 'plugin_toggle',
312
+ description: '翻转插件禁用旗标(写类——需审批;非幂等——重放翻回)。行在场翻旗标、' +
313
+ '行不在场写禁用行(临时停用内置 core: 件的主路径)。装载生效需 /reload' +
314
+ '(模型面不自动链)。',
315
+ parameters: Type.Object({ id: Type.String({ description: '插件 id' }) }, { additionalProperties: false }),
316
+ effect: 'write',
317
+ repeatable: false, // 翻旗标非幂等——重试决策须回模型不静默重放(03 §2.3)
318
+ execute: async (args) => guard(async () => {
319
+ if (deps.dataDir === null) {
320
+ return textError('纯 memory 诊断形无数据目录——行编辑动词不可用(行无落点)');
321
+ }
322
+ const id = args.id;
323
+ const result = toggleRow(deps.dataDir, id, deps.fs, deps.auditSink);
324
+ if (!result.ok)
325
+ return textError(result.message);
326
+ // 终态回读(诚实值源——toggleRow 回执不含终态,行读单源)
327
+ const after = readEnabledRowsForEdit(deps.dataDir, deps.fs);
328
+ const state = after.ok && after.rows.some((row) => row.id === id && row.disabled === true)
329
+ ? '禁用'
330
+ : after.ok
331
+ ? '启用'
332
+ : '未知(启用清单回读失败)';
333
+ return textResult(`已切换:${id}(禁用态 → ${state})——${RELOAD_HINT}`);
334
+ }),
335
+ },
336
+ {
337
+ name: 'plugin_update',
338
+ description: '按源分派更新已装机插件(写类——需审批):npm 重装拉最新满足窗龄版 / ' +
339
+ 'git 按原 ref 重克隆(新 commit)/ local 直引 no-op(源目录变更下次装载' +
340
+ '即生效)。更新后清单 id 变更拒(装机身份漂移)。运行中装载面换血同样' +
341
+ '需 /reload(模型面不自动链)。',
342
+ parameters: Type.Object({ id: Type.String({ description: '已装机插件 id(装机清单见 plugins_list)' }) }, { additionalProperties: false }),
343
+ effect: 'write',
344
+ timeoutMs: 600_000, // npm/git 网络腿长动作(与 plugin_install 同帽)
345
+ execute: async (args) => guard(async () => {
346
+ const executorDeps = installDeps();
347
+ if (executorDeps === undefined) {
348
+ return textError('纯 memory 诊断形无数据目录——更新动词不可用');
349
+ }
350
+ const outcome = await updatePlugin(executorDeps, args.id);
351
+ if (!outcome.ok)
352
+ return textError(outcome.message);
353
+ return textResult(`${outcome.text}\n(运行中装载面换血同样走 /reload——模型面不自动链,03 §5.2)`);
354
+ }),
355
+ },
356
+ ];
357
+ }
358
+ /** 缺省 spawn 真身重导出(装配根接线位——与 CLI 面同一执行面) */
359
+ export { createDefaultSpawnRunner };
@@ -0,0 +1,257 @@
1
+ /**
2
+ * host/plugin-uninstall — 卸载四段清算编舞(03 §5.5——成熟度缺口 #10 装机面
3
+ * 落码批 10c)。
4
+ *
5
+ * 双相动词:`inspectUninstall`(只读零副作用——UninstallReport 全量呈报)
6
+ * → `executeUninstall`(人面独占——CLI `--confirm` 两段式;模型面只到 inspect
7
+ * 为止,§5.6 表格条款)。四段执行序(幂等 = 残迹收尾式——任一段已空则跳过
8
+ * 继续,重跑收敛到全清):
9
+ * ① 删启用行(行不在 = 跳过不报错);
10
+ * ② 装机物必删——`assertInsideInstallSubtree` 防线(逃逸拒)+ 同包引用计数
11
+ * (多引用同物者最后引用删尽才删物)+ local 源不删用户目录;**连带** DROP
12
+ * 该插件域前缀(`<id>__`)全部表——痕迹可清算(§5.5 段②连带清算条款);
13
+ * ③ 数据域处置 `dataAction: keep|purge` 缺省 keep(Docker 卷律)——purge 恒
14
+ * 只删 `data/<id>/` 单插件子目录(`assertInsidePluginData` 防线)+ store_state
15
+ * 本域前缀键连带删(keep 留待 LRU 自然逐出——§4.5 第四正门对称清算);
16
+ * ④ 落账回执——`plugin/uninstalled` durable 审计事件(audit_events 载体,
17
+ * 载荷 = id/source/version/dataAction/affected)+ `affectedSessionCounts`
18
+ * 诚实缺席(durable 装载史载体 v1 未落——§5.5 注记,不虚构计数 0)。
19
+ *
20
+ * 数据面接线(零装配直开库同款纪律):本件不开运行时不装载插件——调用方
21
+ * (plugins-cmd)以 Persistence 直开 + HOST_MIGRATION_TAIL 全链开库后传
22
+ * `store.sqlite()` 同实例窄面进来(域表 DROP 是宿主固定件正当消费——store.ts
23
+ * sqlite() 注记)。短命进程与运行时同库同链,链尾单源防短链降级。
24
+ */
25
+ import { BaseError } from '../contracts/index.js';
26
+ import { createAuditFace, createLoadHistoryFace } from '../persist/index.js';
27
+ import { assertInsideInstallSubtree, assertInsidePluginData, pluginDataDir, readLedger, readEnabledRowsForEdit, relativizeAgainst, removeLedgerEntry, resolveInstallPath, unmountRow, } from './plugin-store.js';
28
+ /**
29
+ * 受影响会话计数单源(03 §5.5 ④——装载史批 h-4 有源化):装载史面时间窗
30
+ * join 推算「装载过该插件的会话」数(05 §9 load_generations;判据恒
31
+ * activated ∩ 会话存活窗——共现非「使用过」)。件内构造面(audit 同律——
32
+ * 调用方已全链开库,load_generations 恒在场);世代行不随 uninstall 删
33
+ * ⇒ 对已卸插件回执同源可考(立题档裁决 4)。
34
+ */
35
+ function affectedOf(deps, id) {
36
+ return { available: true, count: createLoadHistoryFace(deps.db).querySessionsWithPlugin(id) };
37
+ }
38
+ /** 核心前缀(官方件非装机物——uninstall 吃装机 id) */
39
+ const CORE_PREFIX = 'core:';
40
+ /* ---------------- 域面查询(段②连带 + 段③对称清算的判据源) ---------------- */
41
+ /**
42
+ * 该插件域前缀全部表(sqlite_master 全表列举 + JS 前缀过滤——id 字符集
43
+ * `[a-z0-9-]` 无 `%`/`_` 通配符,但 `__` 分隔后表名自由段可含任意字符,
44
+ * 前缀判走 JS 精确 startsWith 不走 LIKE 模式串,通配符注入结构性不可达)。
45
+ */
46
+ function domainTablesOf(db, id) {
47
+ const prefix = `${id}__`;
48
+ const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all();
49
+ return rows.map((r) => r.name).filter((name) => name.startsWith(prefix));
50
+ }
51
+ /** store_state 本域前缀键(同 JS 前缀判——LRU 帽 256 全表列举代价可忽略) */
52
+ function domainStoreStateKeys(db, id) {
53
+ const prefix = `${id}__`;
54
+ const rows = db.prepare('SELECT key FROM store_state').all();
55
+ return rows.map((r) => r.key).filter((key) => key.startsWith(prefix));
56
+ }
57
+ /**
58
+ * 数据域体量三源合计(§5.5「表数据量计入 dataSizeBytes」条款):
59
+ * ① 文件域 `data/<id>/` 递归字节(fs.size 注入面);
60
+ * ② SQLite 域表页量(dbstat 虚表 SUM(pgsize)——better-sqlite3 内建可用);
61
+ * ③ store_state 域键的 key+value 字节量(该表系宿主私产共享表——按行归属
62
+ * 计值不按页计,页量不可归属于单插件)。
63
+ * 三源皆零/缺 → undefined(报告字段缺席——诚实空非伪造 0 字节)。
64
+ */
65
+ function measureDataSizeBytes(deps, id, tables, keys) {
66
+ let total = 0;
67
+ let measured = false;
68
+ const fileBytes = deps.fs.size(pluginDataDir(deps.dataDir, id));
69
+ if (fileBytes !== null) {
70
+ total += fileBytes;
71
+ measured = true;
72
+ }
73
+ for (const table of tables) {
74
+ const row = deps.db.prepare('SELECT SUM(pgsize) AS n FROM dbstat WHERE name = ?').get(table);
75
+ if (row.n !== null && row.n > 0) {
76
+ total += row.n;
77
+ measured = true;
78
+ }
79
+ }
80
+ if (keys.length > 0) {
81
+ const placeholders = keys.map(() => '?').join(', ');
82
+ const row = deps.db
83
+ .prepare(`SELECT COALESCE(SUM(length(key) + length(value)), 0) AS n FROM store_state WHERE key IN (${placeholders})`)
84
+ .get(...keys);
85
+ total += row.n;
86
+ measured = true;
87
+ }
88
+ return measured ? total : undefined;
89
+ }
90
+ /* ---------------- 段②引用计数(同物多引用最后删尽才删物) ---------------- */
91
+ /** 与目标条目同归一路径的其他账本条目 id(引用计数——多 id 同物共享装机目录) */
92
+ function sharedReferrers(dataDir, entries, target) {
93
+ const targetAbs = resolveInstallPath(dataDir, target.installPath);
94
+ return entries
95
+ .filter((e) => e.id !== target.id && resolveInstallPath(dataDir, e.installPath) === targetAbs)
96
+ .map((e) => e.id);
97
+ }
98
+ /* ---------------- inspect(只读零副作用) ---------------- */
99
+ /**
100
+ * 卸载预检(§5.5 UninstallReport)。装机 id 查无 = ok:false(core: 前缀专报
101
+ * 「官方件非装机物」);坏账本/坏启用清单 = ok:false 拒 inspect(与写动词同律
102
+ * ——真相面缺席不猜)。
103
+ */
104
+ export function inspectUninstall(deps, id) {
105
+ if (id.startsWith(CORE_PREFIX)) {
106
+ return {
107
+ ok: false,
108
+ message: `官方件 ${id} 非 uninstall 对象(uninstall 吃装机 id;官方件停用走 toggle——03 §5.5)`,
109
+ };
110
+ }
111
+ const ledgerRead = readLedger(deps.dataDir, deps.fs);
112
+ if (!ledgerRead.ok) {
113
+ return { ok: false, message: `装机账本损坏:${ledgerRead.reason}——inspect 拒猜(03 §5.4)` };
114
+ }
115
+ const entry = ledgerRead.entries.find((e) => e.id === id);
116
+ if (entry === undefined) {
117
+ return { ok: false, message: `插件 ${id} 未装机——装机清单见 plugins list(uninstall 吃装机 id,非启用行 id)` };
118
+ }
119
+ const rowsRead = readEnabledRowsForEdit(deps.dataDir, deps.fs);
120
+ if (!rowsRead.ok) {
121
+ return { ok: false, message: rowsRead.message };
122
+ }
123
+ const sharedWith = sharedReferrers(deps.dataDir, ledgerRead.entries, entry);
124
+ const absolute = resolveInstallPath(deps.dataDir, entry.installPath);
125
+ const domainTables = domainTablesOf(deps.db, id);
126
+ const storeKeys = domainStoreStateKeys(deps.db, id);
127
+ const dataSizeBytes = measureDataSizeBytes(deps, id, domainTables, storeKeys);
128
+ const report = {
129
+ id,
130
+ source: entry.source,
131
+ ...(entry.version !== undefined ? { version: entry.version } : {}),
132
+ enabledRows: rowsRead.rows.filter((row) => row.id === id).length,
133
+ installPaths: [
134
+ {
135
+ ledgerPath: entry.installPath,
136
+ absolute,
137
+ sharedWith,
138
+ // local 直引不删用户目录;共享引用在——物保留只删账(§5.5 段②)
139
+ willDelete: entry.source !== 'local' && sharedWith.length === 0,
140
+ },
141
+ ],
142
+ domainTables,
143
+ storeStateKeys: storeKeys.length,
144
+ ...(dataSizeBytes !== undefined ? { dataSizeBytes } : {}),
145
+ affectedSessionCounts: affectedOf(deps, id),
146
+ };
147
+ return { ok: true, report, text: formatReport(deps, report) };
148
+ }
149
+ /** inspect 人面文本(全量将删项——execute 前知情面) */
150
+ function formatReport(deps, report) {
151
+ const lines = [];
152
+ lines.push(`卸载预检(inspect):${report.id}`);
153
+ lines.push(` 源:${report.source}${report.version !== undefined ? `(version ${report.version})` : ''}`);
154
+ lines.push(` 启用行:${report.enabledRows > 0 ? '在场(段①删)' : '不在场(段①跳过)'}`);
155
+ for (const p of report.installPaths) {
156
+ if (p.sharedWith.length > 0) {
157
+ lines.push(` 装机物:${relativizeAgainst(deps.dataDir, p.absolute)}——共享引用(${p.sharedWith.join('、')})在,物保留只删账`);
158
+ }
159
+ else if (!p.willDelete) {
160
+ lines.push(` 装机物:${p.absolute}——local 直引源,不删用户目录(只删账本条目)`);
161
+ }
162
+ else {
163
+ lines.push(` 装机物:${relativizeAgainst(deps.dataDir, p.absolute)}(段②删)`);
164
+ }
165
+ }
166
+ lines.push(` 域表:${report.domainTables.length > 0 ? `${report.domainTables.length} 张连带 DROP(${report.domainTables.join('、')})` : '无'}`);
167
+ lines.push(` store_state:${report.storeStateKeys} 个域键(purge 连带删 / keep 留待 LRU)`);
168
+ lines.push(` 数据域:${report.dataSizeBytes !== undefined ? `${report.dataSizeBytes} 字节(文件域 + 域表页量 + 域键值)` : '无可测面(缺席非 0)'}——--data keep(缺省)保留 / purge 清除`);
169
+ lines.push(` 受影响会话:装载过该插件的会话 ${report.affectedSessionCounts.count} 个`);
170
+ lines.push('execute 走 --confirm(人面独占):berry-agent plugins uninstall <id> --confirm [--data purge]');
171
+ return lines.join('\n');
172
+ }
173
+ /* ---------------- execute(四段清算——人面独占) ---------------- */
174
+ /**
175
+ * 卸载执行(§5.5 四段——幂等残迹收尾式:任一段已空跳过继续,重跑收敛全清)。
176
+ * dataAction 缺省 keep(execute 不静默猜 purge——③ 段是唯一问用户的门)。
177
+ * 段②③防线拒(逃逸/坏账本)收口为 ok:false 回执(`PLUGIN_UNINSTALL_REFUSED`
178
+ * 文本直呈)——CLI 面不裸崩;其余 IO 失败同收口(短命进程重跑收敛)。
179
+ */
180
+ export function executeUninstall(deps, id, dataAction = 'keep') {
181
+ // 前置预检复用(查无/坏账本/坏清单同拒——execute 不绕 inspect 判据)
182
+ const pre = inspectUninstall(deps, id);
183
+ if (!pre.ok)
184
+ return pre;
185
+ const report = pre.report;
186
+ try {
187
+ const ledgerRead = readLedger(deps.dataDir, deps.fs);
188
+ if (!ledgerRead.ok)
189
+ throw new BaseError('PLUGIN_UNINSTALL_REFUSED', `装机账本损坏:${ledgerRead.reason}`);
190
+ const entry = ledgerRead.entries.find((e) => e.id === id); // inspect 已核在场
191
+ // ① 删启用行(行不在 = 跳过不报错——幂等腿)
192
+ const rowEdit = unmountRow(deps.dataDir, id, deps.fs);
193
+ if (!rowEdit.ok) {
194
+ return { ok: false, message: `段① 删启用行失败:${rowEdit.message}` };
195
+ }
196
+ // ② 装机物清算:防线 → 引用计数 → local 豁免 → 删物 + 删账;连带 DROP 域表
197
+ const [pathInfo] = report.installPaths;
198
+ if (pathInfo !== undefined && pathInfo.willDelete) {
199
+ assertInsideInstallSubtree(deps.dataDir, pathInfo.absolute); // 逃逸拒(防线档)
200
+ deps.fs.rm(pathInfo.absolute, { recursive: true, force: true });
201
+ }
202
+ for (const table of report.domainTables) {
203
+ deps.db.exec(`DROP TABLE IF EXISTS "${table}"`); // 表名来自 sqlite_master 原值 + 引号包裹——标识符注入结构性不可达
204
+ }
205
+ removeLedgerEntry(deps.dataDir, id, deps.fs);
206
+ // ③ 数据域处置(缺省 keep——Docker 卷律):purge 恒只删 data/<id>/ 单插件
207
+ // 子目录(防线)+ store_state 域键连带删
208
+ if (dataAction === 'purge') {
209
+ const dataDirAbs = pluginDataDir(deps.dataDir, id);
210
+ assertInsidePluginData(deps.dataDir, id, dataDirAbs); // 构造即受防线(同构保底)
211
+ deps.fs.rm(dataDirAbs, { recursive: true, force: true }); // 缺席 force 跳过——幂等腿
212
+ const keys = domainStoreStateKeys(deps.db, id);
213
+ if (keys.length > 0) {
214
+ const placeholders = keys.map(() => '?').join(', ');
215
+ deps.db.prepare(`DELETE FROM store_state WHERE key IN (${placeholders})`).run(...keys);
216
+ }
217
+ }
218
+ // ④ 落账回执:plugin/uninstalled durable 审计事件(四段成功尾——§5.5)
219
+ const audit = createAuditFace(deps.db, deps.now ?? Date.now);
220
+ audit.append('plugin/uninstalled', {
221
+ id,
222
+ source: entry.source,
223
+ ...(entry.version !== undefined ? { version: entry.version } : {}),
224
+ dataAction,
225
+ affected: report.affectedSessionCounts, // 有源计数形(h-4——世代行时间窗 join 推算)
226
+ });
227
+ return { ok: true, report, text: formatExecuteReceipt(deps, report, entry, pathInfo, dataAction) };
228
+ }
229
+ catch (err) {
230
+ const label = err instanceof BaseError ? `${err.code}:` : '';
231
+ return { ok: false, message: `${label}${err instanceof Error ? err.message : String(err)}` };
232
+ }
233
+ }
234
+ /** execute 结算文本(四段逐段点名——痕迹可清算的收口面) */
235
+ function formatExecuteReceipt(deps, report, entry, pathInfo, dataAction) {
236
+ const lines = [];
237
+ lines.push(`已卸载:${report.id}(源 ${entry.source})`);
238
+ lines.push(` ① 启用行:${report.enabledRows > 0 ? '已删' : '不在场(跳过)'}`);
239
+ if (pathInfo === undefined) {
240
+ lines.push(' ② 装机物:账本条目已删(无物面)');
241
+ }
242
+ else if (pathInfo.sharedWith.length > 0) {
243
+ lines.push(` ② 装机物:保留(共享引用 ${pathInfo.sharedWith.join('、')} 在——最后引用删尽才删物);账本条目已删`);
244
+ }
245
+ else if (!pathInfo.willDelete) {
246
+ lines.push(' ② 装机物:保留(local 直引源不删用户目录);账本条目已删');
247
+ }
248
+ else {
249
+ lines.push(` ② 装机物:已删(${relativizeAgainst(deps.dataDir, pathInfo.absolute)})`);
250
+ }
251
+ lines.push(` ②连带 域表:${report.domainTables.length > 0 ? `已 DROP ${report.domainTables.length} 张` : '无'}`);
252
+ lines.push(dataAction === 'purge'
253
+ ? ` ③ 数据域:已清除(${report.dataSizeBytes !== undefined ? `${report.dataSizeBytes} 字节` : '无可测面'} + store_state 域键 ${report.storeStateKeys} 个)`
254
+ : ` ③ 数据域:保留(keep 缺省——Docker 卷律;store_state 域键 ${report.storeStateKeys} 个留待 LRU)`);
255
+ lines.push(` ④ 痕迹:plugin/uninstalled 已落审计流;受影响会话——装载过该插件的会话 ${report.affectedSessionCounts.count} 个`);
256
+ return lines.join('\n');
257
+ }