rovecode 0.4.0-beta.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,398 @@
1
+ /** Hooks v2 (port #29): a small TYPED, file-loadable hook set — ≤10 hooks, versioned (ADR-013:
2
+ * "no 1.8k-line extension API surface — small typed hook set, versioned").
3
+ *
4
+ * Contract (HOOKS_API_VERSION 1): nine hooks, every one optional, sync or async —
5
+ * pre_run(ctx) · post_run(ctx, {status, summary}) · pre_tool(ctx, call) → void | {deny}
6
+ * post_tool(ctx, call, result) → void | {output?} · approval(ctx, req) → void | "allow" | "deny"
7
+ * compaction(ctx, event) · session_open(ctx) · session_close(ctx) · on_event(ctx, ev)
8
+ * ctx = {cwd, sessionId, runId?} and nothing else (no registry/store handles) — the surface stays
9
+ * tiny on purpose; a tenth hook is the budget's last slot, not a target.
10
+ *
11
+ * Loading: `.rovecode/hooks.ts` or `.rovecode/hooks.js` in the project plus `~/.rovecode/hooks.{ts,js}`
12
+ * (ROVECODE_HOME idiom, providers/auth.ts rovecodeHome), each `export default { version: 1, hooks: {…} }`,
13
+ * imported with a plain `await import()` — Bun runs TypeScript natively, so there is no build step
14
+ * and no loader dependency. User scope runs first, project second. Wrong/missing version → skipped
15
+ * with a warning (version gate); a failing import → warning, never a throw. Loaded ONCE per
16
+ * process (Bun's module cache; a `?query` does not bust it on 1.3.14): restart rovecode to pick up
17
+ * edits — the projectContext rule. ROVECODE_NO_HOOKS=1 skips the files (programmatic add() still works).
18
+ *
19
+ * Running: every hook call is timeout-bounded (ROVECODE_HOOK_TIMEOUT_MS, default 5000, on a REF'D
20
+ * timer) and isolated — a throwing or hanging hook records one bounded warning note and the run
21
+ * continues as if the hook had returned void. Results are validated and bounded (deny reason
22
+ * ≤ MAX_DENY_REASON_CHARS; post_tool may grow the tool's output by ≤ MAX_POST_TOOL_GROWTH_CHARS).
23
+ * pre_tool / post_tool ride core/tools.ts dispatch at the existing hook seams; approval rides the
24
+ * ApprovalFn chain as approver() (composed in cli/runtime.ts buildCfg — see Authority); the
25
+ * run-level hooks ride the event stream via observer() in core/loop.ts agentLoop (pre_run /
26
+ * compaction / post_run awaited in order, on_event a fire-and-forget tap — port #39 OTel builds on
27
+ * it); session_open/close are the runtime's lifetime (cli/runtime.ts + the surfaces' close paths).
28
+ *
29
+ * Authority: POLICY WINS. Permission rules (deny-default, last-match) are evaluated BEFORE pre_tool,
30
+ * so a hook never sees — and can never "un-deny" — a rule-rejected call; pre_tool can only deny,
31
+ * and its deny applies in every mode including yolo (a hook is the user's own stricter layer). The
32
+ * approval hook stands in for the HUMAN — literally: approver(human) is an ApprovalFn that buildCfg
33
+ * composes INSIDE execPolicyApprover, so the order is permission rules → execpolicy argv
34
+ * classification (port #9: forbidden → deny before any hook or human; allow-listed → runs, nobody
35
+ * asked) → approval hook → human. Consulted only on a policy "prompt" with nothing cached; "allow"
36
+ * is a one-shot yes (never cached), "deny" denies (the chain's deny shape, as execpolicy's), void →
37
+ * the human, or fail closed when there is none (headless). A hook "allow" is exactly as strong as
38
+ * the human's "once" — never stronger than policy or execpolicy. A failing/timed-out hook cannot
39
+ * deny (fail-open to policy, which already ran).
40
+ *
41
+ * TRUST: hooks are code the user placed in their own project or home dir, executed in-process with
42
+ * the user's privileges — the same trust class as .rovecode/commands and .rovecode/mcp.json (which spawns
43
+ * processes). Opening a checkout that ships a hostile .rovecode/hooks.ts runs it; a project-trust
44
+ * prompt (pi's project_trust event) is a documented follow-up, not in scope here.
45
+ *
46
+ * Sources (pattern references, no code copied; both MIT — covered by the generic MIT credit in the
47
+ * THIRD_PARTY_NOTICES.md preamble, no per-port entry as MIT sources are credited in module headers):
48
+ * - pi @ 853a80d packages/coding-agent/src/core/extensions — loader.ts:498-510 imports the
49
+ * extension module (jiti there) and reads its default export; runner.ts:851-884 emit() runs
50
+ * handlers in registration order with a per-handler try/catch → emitError (isolation);
51
+ * runner.ts:982-1004 emitToolCall: the first `block` result wins; types.ts:1125-1131
52
+ * ToolCallEventResult {block, reason}; types.ts:1144-1149 ToolResultEventResult replaces the
53
+ * result content; types.ts:1247 a handler may be sync or async.
54
+ * - opencode @ ebece6e packages/opencode/src/plugin — loader.ts:135-145 load() is a plain
55
+ * `await import(entry)` returning {ok:false, error} instead of throwing; loader.ts:203-236 failed
56
+ * entries are dropped and the successful order preserved; index.ts:284-297 trigger() runs each
57
+ * plugin's hook sequentially and later hooks see earlier mutations; index.ts:226-243 a plugin
58
+ * that fails to apply is logged and skipped.
59
+ * Deviations: an explicit `version` gate (upstream file plugins have none — opencode's compatibility
60
+ * check is npm-only, loader.ts:123-131); a per-call timeout (neither upstream bounds a hook);
61
+ * validated + bounded results; nine hooks instead of pi's ~45 events / opencode's open Hooks map. */
62
+
63
+ import { existsSync } from "node:fs";
64
+ import { join, resolve } from "node:path";
65
+ import { pathToFileURL } from "node:url";
66
+ import type { ApprovalFn, ApprovalRequest, RunEvent, ToolOutput } from "./types.ts";
67
+ import { rovecodeHome } from "../providers/auth.ts";
68
+
69
+ export const HOOKS_API_VERSION = 1;
70
+ export const DEFAULT_HOOK_TIMEOUT_MS = 5000;
71
+ export const MAX_DENY_REASON_CHARS = 400;
72
+ export const MAX_POST_TOOL_GROWTH_CHARS = 16_000;
73
+ const MAX_WARNINGS = 50;
74
+ const MAX_WARNING_CHARS = 300;
75
+
76
+ // ---------- contract ----------
77
+
78
+ export interface HookCtx { cwd: string; sessionId: string; runId?: string }
79
+ export interface HookToolCall { id: string; tool: string; args: unknown }
80
+ export interface RunResult { status: "done" | "stopped" | "error" | "budget"; summary: string }
81
+ export type CompactionEvent = Extract<RunEvent, { type: "compaction" }>;
82
+ type Hook<A extends unknown[], R = void> = (ctx: HookCtx, ...args: A) => Promise<R | void> | R | void;
83
+
84
+ /** The whole surface. Adding a member here is an API version bump (HOOKS_API_VERSION). */
85
+ export interface HookSet {
86
+ /** run start, after run_start is produced (ctx.runId set) and before the first model turn */
87
+ pre_run?: Hook<[]>;
88
+ /** run end, awaited BEFORE run_end reaches the consumer (so it lands before a cmdRun exit); a run whose
89
+ * CONSUMER closed the generator first (serve disconnect, ACP cancel, TUI Esc) gets it once as {stopped, "run aborted"} */
90
+ post_run?: Hook<[result: RunResult]>;
91
+ /** before a tool executes, after policy allowed it; {deny} fails the call with this reason */
92
+ pre_tool?: Hook<[call: HookToolCall], { deny: string }>;
93
+ /** after a tool ran; {output} replaces what the model sees (growth-bounded), {} = unchanged */
94
+ post_tool?: Hook<[call: HookToolCall, result: ToolOutput], { output?: string }>;
95
+ /** policy said prompt, execpolicy left it to a human, nothing is cached: pre-answer instead of the
96
+ * human ("allow" one-shot / "deny"), or void to ask them — ctx is the runtime's (no runId) */
97
+ approval?: Hook<[req: ApprovalRequest], "allow" | "deny">;
98
+ /** after a history compaction (the event as yielded: strategy, trigger, token counts) */
99
+ compaction?: Hook<[event: CompactionEvent]>;
100
+ /** once per runtime, after the hook files loaded */
101
+ session_open?: Hook<[]>;
102
+ /** once per runtime, at surface teardown, after in-flight on_event taps settled */
103
+ session_close?: Hook<[]>;
104
+ /** every RunEvent, in order, fire-and-forget (not awaited — keep it cheap; port #39 OTel tap) */
105
+ on_event?: Hook<[ev: RunEvent]>;
106
+ }
107
+ export type HookName = keyof HookSet;
108
+ /** exhaustive by construction (`satisfies Record<HookName, 0>` rejects a missing or extra key) */
109
+ export const HOOK_NAMES = Object.keys({
110
+ pre_run: 0, post_run: 0, pre_tool: 0, post_tool: 0, approval: 0, compaction: 0, session_open: 0, session_close: 0, on_event: 0,
111
+ } satisfies Record<HookName, 0>) as HookName[];
112
+ /** shape of a hooks file's default export */
113
+ export interface HookModule { version: number; hooks: HookSet }
114
+ export type HookArgs<K extends HookName> = Parameters<NonNullable<HookSet[K]>>;
115
+ /** what a hook may decide (its non-void return), validated + bounded by the runner */
116
+ export type HookDecision<K extends HookName> = Exclude<Awaited<ReturnType<NonNullable<HookSet[K]>>>, void>;
117
+
118
+ // ---------- loader ----------
119
+
120
+ export interface LoadedHooks { hooks: HookSet[]; sources: string[]; warnings: string[] }
121
+
122
+ /** Load `<home>/hooks.{ts,js}` (user, first) and `<cwd>/.rovecode/hooks.{ts,js}` (project, second).
123
+ * Never throws: every failure is a warning line naming the file. hooks[i] came from sources[i]. */
124
+ export async function loadHooks(cwd: string, opts: { home?: string; timeoutMs?: number } = {}): Promise<LoadedHooks> {
125
+ const out: LoadedHooks = { hooks: [], sources: [], warnings: [] };
126
+ if (process.env.ROVECODE_NO_HOOKS === "1") return out;
127
+ const seen = new Set<string>();
128
+ for (const dir of [opts.home ?? rovecodeHome(), join(cwd, ".rovecode")]) {
129
+ const file = pickHookFile(dir, out.warnings);
130
+ if (file === null || seen.has(resolve(file))) continue; // home inside cwd/.rovecode: one load
131
+ seen.add(resolve(file));
132
+ const set = await importHookSet(file, opts.timeoutMs ?? hookTimeoutMs(), out.warnings);
133
+ if (set) { out.hooks.push(set); out.sources.push(file); }
134
+ }
135
+ return out;
136
+ }
137
+
138
+ function pickHookFile(dir: string, warnings: string[]): string | null {
139
+ const ts = join(dir, "hooks.ts"), js = join(dir, "hooks.js");
140
+ const hasTs = existsSync(ts), hasJs = existsSync(js);
141
+ if (hasTs && hasJs) warnings.push(`${js}: ignored — ${ts} takes precedence`);
142
+ return hasTs ? ts : hasJs ? js : null;
143
+ }
144
+
145
+ async function importHookSet(file: string, timeoutMs: number, warnings: string[]): Promise<HookSet | null> {
146
+ let mod: unknown;
147
+ try {
148
+ mod = await withTimeout(import(pathToFileURL(file).href), timeoutMs);
149
+ } catch (e) {
150
+ warnings.push(`${file}: failed to load — ${errText(e)}`);
151
+ return null;
152
+ }
153
+ if (mod === TIMED_OUT) { warnings.push(`${file}: load timed out after ${timeoutMs}ms (top-level await?) — skipped`); return null; }
154
+ return validateModule(file, isRecord(mod) ? mod["default"] : undefined, warnings);
155
+ }
156
+
157
+ /** default export → HookSet; the version gate lives here. Unknown/non-function members are
158
+ * dropped with a warning, the rest kept (a typo must not silently disable the whole file). */
159
+ function validateModule(file: string, dflt: unknown, warnings: string[]): HookSet | null {
160
+ if (!isRecord(dflt)) { warnings.push(`${file}: default export must be { version: ${HOOKS_API_VERSION}, hooks: {…} } — skipped`); return null; }
161
+ if (dflt["version"] !== HOOKS_API_VERSION) {
162
+ const v = dflt["version"]; // a string "1" is shown quoted, never disguised as the supported number
163
+ const shown = v === undefined ? "missing" : typeof v === "string" ? JSON.stringify(v) : String(v);
164
+ warnings.push(`${file}: hooks API version ${shown} is not supported (this rovecode speaks ${HOOKS_API_VERSION}) — skipped`);
165
+ return null;
166
+ }
167
+ if (!isRecord(dflt["hooks"])) { warnings.push(`${file}: "hooks" must be an object of hook functions — skipped`); return null; }
168
+ const set: Record<string, unknown> = {};
169
+ for (const [name, fn] of Object.entries(dflt["hooks"])) {
170
+ if (!(HOOK_NAMES as string[]).includes(name)) { warnings.push(`${file}: unknown hook "${name}" ignored (known: ${HOOK_NAMES.join(", ")})`); continue; }
171
+ if (typeof fn !== "function") { warnings.push(`${file}: hook "${name}" is not a function — ignored`); continue; }
172
+ set[name] = fn;
173
+ }
174
+ return set as HookSet;
175
+ }
176
+
177
+ /** ROVECODE_HOOK_TIMEOUT_MS: blank/invalid/< 1 → default. */
178
+ export function hookTimeoutMs(env: Record<string, string | undefined> = process.env): number {
179
+ const v = Number(env["ROVECODE_HOOK_TIMEOUT_MS"] ?? "");
180
+ return Number.isFinite(v) && v >= 1 ? Math.floor(v) : DEFAULT_HOOK_TIMEOUT_MS;
181
+ }
182
+
183
+ // ---------- runner ----------
184
+
185
+ /** the loop's per-run view; close() = the run ended WITHOUT a run_end (consumer-closed generator) */
186
+ export interface RunObserver { observe(ev: RunEvent): Promise<void>; close(): Promise<void> }
187
+ export interface HookRunnerOptions { timeoutMs?: number; onWarning?: (note: string) => void }
188
+
189
+ /** Holds the hook sets of one runtime and runs them: sets in attach order, per call a ref'd timeout
190
+ * + try/catch isolation, results validated. Decision hooks: the first decisive result wins
191
+ * (pi emitToolCall), except post_tool which chains (each set sees the previous output). */
192
+ export class HookRunner {
193
+ /** bounded log of load/runtime notes (also streamed to onWarning) */
194
+ readonly warnings: string[] = [];
195
+ readonly timeoutMs: number;
196
+ private readonly entries: { source: string; set: HookSet }[] = [];
197
+ private loading: Promise<void> | null = null;
198
+ private opened = false;
199
+ private closing: Promise<void> | null = null;
200
+ private readonly pending = new Set<Promise<void>>();
201
+ private listener: ((note: string) => void) | undefined;
202
+
203
+ constructor(private readonly base: HookCtx, opts: HookRunnerOptions = {}) {
204
+ this.timeoutMs = opts.timeoutMs ?? hookTimeoutMs();
205
+ this.listener = opts.onWarning;
206
+ }
207
+
208
+ /** attach a set programmatically (port #39 OTel) — before the first run; session_open is open()'s */
209
+ add(set: HookSet, source = "programmatic"): void { this.entries.push({ source, set }); }
210
+ get size(): number { return this.entries.length; }
211
+ has(name: HookName): boolean { return this.entries.some((e) => typeof e.set[name] === "function"); }
212
+ /** settles when the hook files are attached and session_open has run; run() waits for it */
213
+ get ready(): Promise<void> { return this.loading ?? Promise.resolve(); }
214
+
215
+ /** load the hook files (background import) then fire session_open ONCE; idempotent */
216
+ open(cwd = this.base.cwd, opts: { home?: string } = {}): Promise<void> {
217
+ if (this.opened) return this.ready;
218
+ this.opened = true;
219
+ const load = loadHooks(cwd, { ...opts, timeoutMs: this.timeoutMs }).then(async (loaded) => {
220
+ loaded.hooks.forEach((set, i) => this.add(set, loaded.sources[i] ?? "file"));
221
+ for (const w of loaded.warnings) this.warn(w);
222
+ await this.dispatch("session_open", [this.base]);
223
+ }).then(() => { this.loading = null; });
224
+ this.loading = load;
225
+ return load;
226
+ }
227
+
228
+ /** run one hook across all sets (waits for open() first, so no caller can race the load) */
229
+ async run<K extends HookName>(name: K, ...args: HookArgs<K>): Promise<HookDecision<K> | undefined> {
230
+ if (this.loading) await this.loading;
231
+ return this.dispatch(name, args);
232
+ }
233
+
234
+ private async dispatch<K extends HookName>(name: K, args: HookArgs<K>): Promise<HookDecision<K> | undefined> {
235
+ let decision: HookDecision<K> | undefined;
236
+ for (const entry of this.entries) {
237
+ const fn = entry.set[name];
238
+ if (typeof fn !== "function") continue;
239
+ const raw = await this.invoke(entry.source, name, fn as (...a: unknown[]) => unknown, args);
240
+ const d = normalize(name, raw, args, (w) => this.warn(`${entry.source}: ${w}`));
241
+ if (d === undefined) continue;
242
+ if (name !== "post_tool") return d; // first decisive result wins
243
+ decision = d; // post_tool chains: the next set sees this set's output
244
+ const cur = args as unknown as [HookCtx, HookToolCall, ToolOutput];
245
+ const next = (d as { output?: string }).output;
246
+ if (next !== undefined) cur[2] = { ...cur[2], output: next };
247
+ }
248
+ return decision;
249
+ }
250
+
251
+ /** one hook function: sync throw, async rejection and timeout all become `undefined` + a note */
252
+ private invoke(source: string, name: HookName, fn: (...a: unknown[]) => unknown, args: readonly unknown[]): Promise<unknown> {
253
+ let p: Promise<unknown>;
254
+ try { p = Promise.resolve(fn(...args)); }
255
+ catch (e) { this.warn(`${source}: ${name} hook threw: ${errText(e)} — ignored, run continues`); return Promise.resolve(undefined); }
256
+ return withTimeout(p, this.timeoutMs).then(
257
+ (v) => {
258
+ if (v !== TIMED_OUT) return v;
259
+ this.warn(`${source}: ${name} hook timed out after ${this.timeoutMs}ms — ignored, run continues`);
260
+ return undefined;
261
+ },
262
+ (e) => { this.warn(`${source}: ${name} hook threw: ${errText(e)} — ignored, run continues`); return undefined; },
263
+ );
264
+ }
265
+
266
+ /** per-run view for the loop: maps the event stream onto pre_run / compaction / post_run (awaited, in
267
+ * order) and taps on_event for every event (fire-and-forget). close() is the loop's teardown seam
268
+ * (fix-wave 4, #39 MED-1): a consumer that .return()s the generator before run_end still ended the
269
+ * run, so post_run fires ONCE with the abort shape — after a pre_run only, never after a yielded
270
+ * run_end — while on_event gets nothing synthesized (it mirrors the consumer's stream exactly). */
271
+ observer(base: { cwd: string; sessionId: string }): RunObserver {
272
+ let ctx: HookCtx = { cwd: base.cwd, sessionId: base.sessionId };
273
+ let ended = false;
274
+ return {
275
+ observe: async (ev) => {
276
+ if (ev.type === "run_start") ctx = { ...ctx, runId: ev.runId };
277
+ this.tap(ctx, ev);
278
+ if (ev.type === "run_start") await this.run("pre_run", ctx);
279
+ else if (ev.type === "compaction") await this.run("compaction", ctx, ev);
280
+ else if (ev.type === "run_end") { ended = true; await this.run("post_run", ctx, { status: ev.status, summary: ev.summary }); }
281
+ },
282
+ close: async () => {
283
+ if (ended || ctx.runId === undefined) return;
284
+ ended = true; await this.run("post_run", ctx, { status: "stopped", summary: "run aborted" });
285
+ },
286
+ };
287
+ }
288
+
289
+ /** The approval hook as an ApprovalFn for the approver chain — cli/runtime.ts buildCfg composes
290
+ * execPolicyApprover(hooks.approver(human)), so this runs only for prompt-classified calls that
291
+ * execpolicy did not settle (header: Authority). "allow" → "once" (dispatch never caches once),
292
+ * "deny" → "deny", void → the human; no human (headless) → fail closed like execpolicy's prompt
293
+ * arm. ctx is the runtime's {cwd, sessionId}: the chain is composed per config, before any run. */
294
+ approver(human?: ApprovalFn): ApprovalFn {
295
+ return async (req) => {
296
+ const pre = await this.run("approval", this.base, { ...req, args: cloneForHook(req.args), revisedArgs: cloneForHook(req.revisedArgs) });
297
+ if (pre === "allow") return "once";
298
+ if (pre === "deny") return "deny";
299
+ return human ? human(req) : "deny";
300
+ };
301
+ }
302
+
303
+ private tap(ctx: HookCtx, ev: RunEvent): void {
304
+ if (!this.loading && !this.has("on_event")) return; // zero cost without an on_event hook
305
+ const p: Promise<void> = this.run("on_event", ctx, ev).then(() => undefined, () => undefined);
306
+ this.pending.add(p);
307
+ void p.then(() => { this.pending.delete(p); });
308
+ }
309
+
310
+ /** wait for in-flight on_event taps (each bounded by the timeout) */
311
+ async settle(): Promise<void> { await Promise.all([...this.pending]); }
312
+
313
+ /** fire session_close ONCE (after the load and in-flight taps settle); later calls share the promise */
314
+ close(): Promise<void> {
315
+ this.closing ??= (async () => {
316
+ if (this.loading) await this.loading;
317
+ await this.settle();
318
+ await this.dispatch("session_close", [this.base]);
319
+ })();
320
+ return this.closing;
321
+ }
322
+
323
+ /** subscribe a surface (cmdRun → stderr, TUI → note); buffered notes are replayed first */
324
+ onWarning(fn: (note: string) => void): void { this.listener = fn; for (const w of this.warnings) fn(w); }
325
+ drainWarnings(): string[] { return this.warnings.splice(0); }
326
+ private warn(note: string): void {
327
+ const n = clip(note, MAX_WARNING_CHARS);
328
+ this.warnings.push(n);
329
+ if (this.warnings.length > MAX_WARNINGS) this.warnings.shift();
330
+ this.listener?.(n);
331
+ }
332
+ }
333
+
334
+ // ---------- result validation + helpers ----------
335
+
336
+ /** Hooks get COPIES of a call's args and a tool's result: a hook that mutates its argument must not
337
+ * re-aim a call policy already evaluated, rewrite the persisted tool_call, or dodge the post_tool
338
+ * growth bound (which applies to the RETURNED {output} only). JSON-derived values clone; anything
339
+ * structuredClone rejects (never off the wire) passes through as is rather than failing the call. */
340
+ export function cloneForHook<T>(v: T): T { try { return structuredClone(v); } catch { return v; } }
341
+
342
+ function normalize<K extends HookName>(name: K, raw: unknown, args: HookArgs<K>, warn: (w: string) => void): HookDecision<K> | undefined {
343
+ if (raw === undefined || raw === null) return undefined;
344
+ switch (name) {
345
+ case "pre_tool": {
346
+ const deny = isRecord(raw) ? raw["deny"] : undefined;
347
+ if (typeof deny === "string" && deny.trim().length > 0) return { deny: clip(deny.trim(), MAX_DENY_REASON_CHARS) } as HookDecision<K>;
348
+ break;
349
+ }
350
+ case "approval":
351
+ if (raw === "allow" || raw === "deny") return raw as HookDecision<K>;
352
+ break;
353
+ case "post_tool": {
354
+ if (!isRecord(raw)) break;
355
+ if (raw["output"] === undefined) return undefined; // {} = leave the output alone
356
+ if (typeof raw["output"] !== "string") break;
357
+ const original = (args as unknown as [HookCtx, HookToolCall, ToolOutput])[2].output;
358
+ return { output: boundGrowth(raw["output"], original.length) } as HookDecision<K>;
359
+ }
360
+ default:
361
+ return undefined; // void hooks: a stray return value is not an error
362
+ }
363
+ warn(`${name} hook returned an invalid result (${describe(raw)}) — ignored`);
364
+ return undefined;
365
+ }
366
+
367
+ /** a hook may replace the output outright, but may not GROW it past the original by more than the cap */
368
+ function boundGrowth(text: string, originalLen: number): string {
369
+ const cap = originalLen + MAX_POST_TOOL_GROWTH_CHARS;
370
+ if (text.length <= cap) return text;
371
+ return text.slice(0, cap) + `\n… [post_tool output truncated: hooks may add at most ${MAX_POST_TOOL_GROWTH_CHARS} chars]`;
372
+ }
373
+
374
+ const TIMED_OUT: unique symbol = Symbol("rovecode.hook.timeout");
375
+ /** resolves TIMED_OUT after ms on a REF'D timer (Bun unrefs AbortSignal.timeout — providers/retry.ts
376
+ * sleepMs / tools/webfetch.ts idiom); the original promise's later settle is ignored */
377
+ function withTimeout<T>(p: Promise<T>, ms: number): Promise<T | typeof TIMED_OUT> {
378
+ return new Promise((resolvePromise, rejectPromise) => {
379
+ const timer = setTimeout(() => resolvePromise(TIMED_OUT), ms);
380
+ (timer as unknown as { ref?: () => void }).ref?.();
381
+ p.then((v) => { clearTimeout(timer); resolvePromise(v); }, (e: unknown) => { clearTimeout(timer); rejectPromise(e); });
382
+ });
383
+ }
384
+
385
+ function isRecord(v: unknown): v is Record<string, unknown> { return typeof v === "object" && v !== null && !Array.isArray(v); }
386
+ function clip(s: string, max: number): string { return s.length <= max ? s : s.slice(0, max) + "…"; }
387
+ /** Bun's BuildMessage/ResolveMessage are not Error instances but carry .message */
388
+ function errText(e: unknown): string {
389
+ if (e instanceof Error) return e.message;
390
+ if (isRecord(e) && typeof e["message"] === "string") return e["message"];
391
+ return String(e);
392
+ }
393
+ function describe(v: unknown): string {
394
+ if (typeof v === "string") return JSON.stringify(clip(v, 40));
395
+ if (Array.isArray(v)) return "array";
396
+ if (isRecord(v)) return `object with keys ${Object.keys(v).slice(0, 5).join(",") || "(none)"}`;
397
+ return typeof v;
398
+ }
@@ -0,0 +1,230 @@
1
+ /** Image attachments (port #34): file → sniff → caps → ImagePart; the wire blocks both provider
2
+ * adapters emit; the text stand-in for models without vision.
3
+ *
4
+ * Ported from opencode @ ebece6e (MIT), packages/opencode/src unless noted:
5
+ * - util/media.ts:15-26 sniffAttachmentMime — the MIME comes from magic bytes, never the file
6
+ * extension (png 89 50 4E 47.., jpeg FF D8 FF, gif "GIF8", webp "RIFF"…"WEBP"); bmp/pdf are
7
+ * recognised there but not accepted here (neither wire protocol takes them as images);
8
+ * - image/image.ts:10 MAX_BASE64_BYTES = 5 MiB (their cap is on the base64 form and they resize
9
+ * past it with photon; here the cap is on the decoded file, ROVECODE_IMAGE_MAX_BYTES, and an
10
+ * oversize image is an error the user fixes — never a silent re-encode);
11
+ * - session/prompt.ts:66-71 SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES — the same four types;
12
+ * - session/message-v2.ts:213-217 `[Attached <mime>: <filename>]` text stand-in for stripped
13
+ * media and provider/transform.ts:409-441 unsupportedParts (image → text when the model's
14
+ * input modalities lack "image") — the placeholder below is rovecode's own wording.
15
+ * Pattern reference only, no code copied: cline @ 8eb5f3d sdk/packages/shared/src/llms/media.ts
16
+ * SUPPORTED_IMAGE_MEDIA_TYPES (:73-78), DEFAULT_MAX_IMAGE_BASE64_BYTES 5 MiB (:80),
17
+ * IMAGE_UNSUPPORTED_PLACEHOLDER (:6-12: the stored history keeps the real image, only the
18
+ * request is substituted); apps/vscode/src/shared/messages/content.ts:140-153 base64 source ↔
19
+ * `data:<mime>;base64,<data>` URL.
20
+ * Wire shapes: Anthropic `{type:"image", source:{type:"base64", media_type, data}}`; OpenAI
21
+ * `{type:"image_url", image_url:{url:"data:<mime>;base64,<data>", detail}}`.
22
+ * Dimensions are read from container headers only (PNG IHDR, GIF logical screen, JPEG SOFn,
23
+ * WebP VP8/VP8L/VP8X) — no decoding; a header we cannot parse just leaves width/height unset. */
24
+
25
+ import { readFileSync, statSync } from "node:fs";
26
+ import { basename, isAbsolute } from "node:path";
27
+ import type { ImageMime, ImagePart } from "./types.ts";
28
+
29
+ /** decoded-file cap per image (bytes); ROVECODE_IMAGE_MAX_BYTES overrides */
30
+ export const IMAGE_MAX_BYTES = 5 * 1024 * 1024;
31
+ export const IMAGE_MAX_BYTES_ENV = "ROVECODE_IMAGE_MAX_BYTES";
32
+ /** cap per user message — the TUI's /attach queue and the ACP prompt path both check it */
33
+ export const MAX_IMAGES_PER_MESSAGE = 8;
34
+ export const IMAGE_MIMES: readonly ImageMime[] = ["image/png", "image/jpeg", "image/gif", "image/webp"];
35
+
36
+ export interface ImageLoadOptions {
37
+ /** per-image byte cap; default ROVECODE_IMAGE_MAX_BYTES, else IMAGE_MAX_BYTES */
38
+ maxBytes?: number;
39
+ /** env the cap is read from (tests); default process.env */
40
+ env?: Record<string, string | undefined>;
41
+ /** display name; loadImageAttachment uses the file's basename */
42
+ name?: string;
43
+ }
44
+
45
+ export type ImageLoadResult = ImagePart | { error: string };
46
+
47
+ /** The effective per-image cap: a positive integer ROVECODE_IMAGE_MAX_BYTES wins, else the default. */
48
+ export function imageMaxBytes(env: Record<string, string | undefined> = process.env): number {
49
+ const raw = env[IMAGE_MAX_BYTES_ENV]?.trim();
50
+ if (!raw) return IMAGE_MAX_BYTES;
51
+ const n = Number(raw);
52
+ return Number.isInteger(n) && n > 0 ? n : IMAGE_MAX_BYTES;
53
+ }
54
+
55
+ // ---------- sniffing + dimensions ----------
56
+
57
+ const startsWith = (b: Uint8Array, at: number, prefix: number[]): boolean =>
58
+ b.length >= at + prefix.length && prefix.every((v, i) => b[at + i] === v);
59
+
60
+ /** MIME by magic bytes (opencode media.ts:15-26 minus bmp/pdf); undefined = not an image we accept. */
61
+ export function sniffImageMime(bytes: Uint8Array): ImageMime | undefined {
62
+ if (startsWith(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "image/png";
63
+ if (startsWith(bytes, 0, [0xff, 0xd8, 0xff])) return "image/jpeg";
64
+ if (startsWith(bytes, 0, [0x47, 0x49, 0x46, 0x38])) return "image/gif";
65
+ if (startsWith(bytes, 0, [0x52, 0x49, 0x46, 0x46]) && startsWith(bytes, 8, [0x57, 0x45, 0x42, 0x50])) return "image/webp";
66
+ return undefined;
67
+ }
68
+
69
+ const be16 = (b: Uint8Array, i: number): number => (b[i]! << 8) | b[i + 1]!;
70
+ const be32 = (b: Uint8Array, i: number): number => ((b[i]! << 24) | (b[i + 1]! << 16) | (b[i + 2]! << 8) | b[i + 3]!) >>> 0;
71
+ const le16 = (b: Uint8Array, i: number): number => b[i]! | (b[i + 1]! << 8);
72
+ const le24 = (b: Uint8Array, i: number): number => b[i]! | (b[i + 1]! << 8) | (b[i + 2]! << 16);
73
+
74
+ /** Header-only width/height; undefined when the header is truncated or unrecognised. */
75
+ export function imageDimensions(bytes: Uint8Array, mime: ImageMime): { width: number; height: number } | undefined {
76
+ const dims = (width: number, height: number) => (width > 0 && height > 0 ? { width, height } : undefined);
77
+ switch (mime) {
78
+ case "image/png": // IHDR is always the first chunk: length(4) "IHDR"(4) width(4) height(4)
79
+ return bytes.length >= 24 && startsWith(bytes, 12, [0x49, 0x48, 0x44, 0x52]) ? dims(be32(bytes, 16), be32(bytes, 20)) : undefined;
80
+ case "image/gif": // logical screen descriptor right after "GIF89a"/"GIF87a"
81
+ return bytes.length >= 10 ? dims(le16(bytes, 6), le16(bytes, 8)) : undefined;
82
+ case "image/jpeg": return jpegDimensions(bytes);
83
+ case "image/webp": return webpDimensions(bytes);
84
+ }
85
+ }
86
+
87
+ /** Walk the marker segments to the first SOFn (C0-CF except C4 DHT, C8 JPG, CC DAC): height, width. */
88
+ function jpegDimensions(b: Uint8Array): { width: number; height: number } | undefined {
89
+ let i = 2;
90
+ while (i + 3 < b.length) {
91
+ if (b[i] !== 0xff) return undefined;
92
+ const marker = b[i + 1]!;
93
+ if (marker === 0xff) { i += 1; continue; } // fill byte
94
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { i += 2; continue; } // standalone
95
+ if (marker === 0xd9 || marker === 0xda) return undefined; // EOI / SOS before any SOF
96
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
97
+ return i + 8 < b.length ? { height: be16(b, i + 5), width: be16(b, i + 7) } : undefined;
98
+ }
99
+ i += 2 + be16(b, i + 2);
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ /** First chunk after "WEBP": VP8 (lossy frame header), VP8L (lossless 14-bit fields), VP8X (extended canvas). */
105
+ function webpDimensions(b: Uint8Array): { width: number; height: number } | undefined {
106
+ if (b.length < 30) return undefined;
107
+ const fourcc = String.fromCharCode(b[12]!, b[13]!, b[14]!, b[15]!);
108
+ if (fourcc === "VP8 ") return { width: le16(b, 26) & 0x3fff, height: le16(b, 28) & 0x3fff };
109
+ if (fourcc === "VP8L" && b[20] === 0x2f) {
110
+ return { width: 1 + (b[21]! | ((b[22]! & 0x3f) << 8)), height: 1 + ((b[22]! >> 6) | (b[23]! << 2) | ((b[24]! & 0x0f) << 10)) };
111
+ }
112
+ if (fourcc === "VP8X") return { width: 1 + le24(b, 24), height: 1 + le24(b, 27) };
113
+ return undefined;
114
+ }
115
+
116
+ // ---------- loading ----------
117
+
118
+ /** `70 B`, `5121 KB` — the unit both the caps' errors and the placeholder use */
119
+ const fmtBytes = (n: number): string => (n < 1024 ? `${n} B` : `${Math.ceil(n / 1024)} KB`);
120
+
121
+ /** Bytes already in memory → ImagePart (base64 inline). Rejects by magic bytes and by the cap. */
122
+ export function imageFromBytes(bytes: Uint8Array, opts: ImageLoadOptions = {}): ImageLoadResult {
123
+ const label = opts.name ?? "image";
124
+ const mime = sniffImageMime(bytes);
125
+ if (!mime) {
126
+ const head = [...bytes.subarray(0, 4)].map((x) => x.toString(16).padStart(2, "0")).join(" ");
127
+ return { error: `${label}: not a png/jpeg/gif/webp image (magic bytes: ${head || "empty file"})` };
128
+ }
129
+ const max = opts.maxBytes ?? imageMaxBytes(opts.env);
130
+ if (bytes.byteLength > max) return { error: `${label}: ${fmtBytes(bytes.byteLength)} is over the ${fmtBytes(max)} per-image cap (${IMAGE_MAX_BYTES_ENV})` };
131
+ const part: ImagePart = { kind: "image", mime, bytes: Buffer.from(bytes).toString("base64") };
132
+ const d = imageDimensions(bytes, mime);
133
+ if (d) { part.width = d.width; part.height = d.height; }
134
+ if (opts.name !== undefined) part.name = opts.name;
135
+ return part;
136
+ }
137
+
138
+ /** A file on disk → ImagePart. Size is checked from stat BEFORE reading (a 2 GB "image" is
139
+ * rejected without touching its bytes); the type comes from the bytes, never the extension.
140
+ * Never throws: every failure is `{ error }`. */
141
+ export function loadImageAttachment(filePath: string, opts: ImageLoadOptions = {}): ImageLoadResult {
142
+ const name = opts.name ?? basename(filePath);
143
+ let size: number;
144
+ try {
145
+ const st = statSync(filePath);
146
+ if (!st.isFile()) return { error: `${name}: not a file` };
147
+ size = st.size;
148
+ } catch (e) {
149
+ return { error: `${name}: cannot read (${e instanceof Error ? e.message : String(e)})` };
150
+ }
151
+ const max = opts.maxBytes ?? imageMaxBytes(opts.env);
152
+ if (size > max) return { error: `${name}: ${fmtBytes(size)} is over the ${fmtBytes(max)} per-image cap (${IMAGE_MAX_BYTES_ENV})` };
153
+ try {
154
+ return imageFromBytes(readFileSync(filePath), { ...opts, maxBytes: max, name });
155
+ } catch (e) {
156
+ return { error: `${name}: cannot read (${e instanceof Error ? e.message : String(e)})` };
157
+ }
158
+ }
159
+
160
+ /** Transport form (ACP image blocks, HTTP bodies): base64 + declared mime → ImagePart. The bytes
161
+ * decide the type; a declared mime that disagrees is an error (cline media.ts media_type_mismatch). */
162
+ export function imageFromBase64(data: string, declaredMime: string | undefined, opts: ImageLoadOptions = {}): ImageLoadResult {
163
+ const bytes = Buffer.from(data, "base64");
164
+ const res = imageFromBytes(bytes, opts);
165
+ if ("error" in res) return res;
166
+ if (declaredMime !== undefined && declaredMime !== res.mime) return { error: `${opts.name ?? "image"}: declared ${declaredMime} but the bytes are ${res.mime}` };
167
+ return res;
168
+ }
169
+
170
+ /** Per-message image cap: the error to show when `count` images would ride on one message. */
171
+ export function checkImageCount(count: number, max = MAX_IMAGES_PER_MESSAGE): string | undefined {
172
+ return count > max ? `at most ${max} images per message (${count} attached)` : undefined;
173
+ }
174
+
175
+ // ---------- reading back (wire + display) ----------
176
+
177
+ /** The base64 payload: inline bytes, else the sidecar file (absolute path — the session store
178
+ * resolves relative ones on load). undefined when unreadable — callers fall back to text. */
179
+ export function imageData(part: ImagePart): string | undefined {
180
+ if (part.bytes !== undefined) return part.bytes;
181
+ if (part.path === undefined || !isAbsolute(part.path)) return undefined;
182
+ try { return readFileSync(part.path).toString("base64"); } catch { return undefined; }
183
+ }
184
+
185
+ /** Decoded byte size when knowable without reading a sidecar (inline: from the base64 length).
186
+ * Same path rule as imageData(): only an absolute (store-hydrated) path is stat'ed — a persisted
187
+ * relative path must never touch the process cwd (session.ts F2 confinement). */
188
+ export function imageByteSize(part: ImagePart): number | undefined {
189
+ if (part.bytes !== undefined) {
190
+ const pad = part.bytes.endsWith("==") ? 2 : part.bytes.endsWith("=") ? 1 : 0;
191
+ return Math.floor((part.bytes.length * 3) / 4) - pad;
192
+ }
193
+ if (part.path === undefined || !isAbsolute(part.path)) return undefined;
194
+ try { return statSync(part.path).size; } catch { return undefined; }
195
+ }
196
+
197
+ export function imageExt(mime: ImageMime): "png" | "jpg" | "gif" | "webp" {
198
+ return mime === "image/png" ? "png" : mime === "image/jpeg" ? "jpg" : mime === "image/gif" ? "gif" : "webp";
199
+ }
200
+
201
+ /** `name, WxH, N KB` — whatever is known, in that order. */
202
+ export function describeImage(part: ImagePart): string {
203
+ const bits = [part.name ?? part.mime];
204
+ if (part.width !== undefined && part.height !== undefined) bits.push(`${part.width}x${part.height}`);
205
+ const size = imageByteSize(part);
206
+ if (size !== undefined) bits.push(fmtBytes(size));
207
+ return bits.join(", ");
208
+ }
209
+
210
+ /** Text the adapters send in place of an image the model cannot see (or a sidecar that is gone). */
211
+ export function imagePlaceholder(part: ImagePart, reason = "model has no vision"): string {
212
+ return `[image: ${describeImage(part)} — ${reason}]`;
213
+ }
214
+
215
+ /** Transcript chip for a user message's attachment. */
216
+ export function imageChip(part: ImagePart): string {
217
+ return `[image: ${part.name ?? part.mime}]`;
218
+ }
219
+
220
+ /** Anthropic Messages content block; undefined when the bytes are unavailable. */
221
+ export function anthropicImageBlock(part: ImagePart): Record<string, unknown> | undefined {
222
+ const data = imageData(part);
223
+ return data === undefined ? undefined : { type: "image", source: { type: "base64", media_type: part.mime, data } };
224
+ }
225
+
226
+ /** OpenAI chat-completions content part (data URL); undefined when the bytes are unavailable. */
227
+ export function openaiImageBlock(part: ImagePart, detail: "auto" | "low" | "high" = "auto"): Record<string, unknown> | undefined {
228
+ const data = imageData(part);
229
+ return data === undefined ? undefined : { type: "image_url", image_url: { url: `data:${part.mime};base64,${data}`, detail } };
230
+ }