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,108 @@
1
+ /** /cost note body (ports #5+#6), extracted from app.ts for the ADR-002 line cap.
2
+ * Usage is priced PER MESSAGE at the model recorded in Message.origin —
3
+ * a session that switched models mid-way is not silently re-priced at the current model.
4
+ * Messages without an origin fall back to the current model WITH an explicit caveat; messages
5
+ * whose model has no catalog pricing are excluded and flagged (cost becomes a lower bound).
6
+ * Context health counts ALL parts (partsTokenText): tool calls/results dominate agentic
7
+ * sessions, and a text-only count reads ~0% forever.
8
+ * Port #44: the same math feeds the sextant usage panel through sessionUsage(). */
9
+
10
+ import { partsTokenText } from "../core/loop.ts";
11
+ import { ModelCatalog, ratesFor } from "../providers/catalog.ts";
12
+ import { contextHealth, costUsdTiered, countTokens, countTokensIfLoaded } from "../core/usage.ts";
13
+ import { estimateTokens } from "../core/context.ts";
14
+ import { tokenScaleFor } from "../core/token-scale.ts";
15
+ import type { Message } from "../core/types.ts";
16
+
17
+ export interface UsageSummary {
18
+ inTok: number; outTok: number; cacheRead: number; cacheWrite: number;
19
+ /** USD over the priced messages; `priced`/`unpriced`/`noOrigin` say how much of the transcript it covers */
20
+ cost: number; priced: number; unpriced: number; noOrigin: number;
21
+ /** estimated prompt tokens, corrected towards the current model's own tokenizer, and the catalog's
22
+ * window when it knows one. `estRaw` is the uncorrected count the correction was applied to, and
23
+ * `counter` says which estimator produced it — both are estimates, with different measured errors
24
+ * (core/token-scale.ts): "o200k" is countTokens (exact for OpenAI models), "chars" is estimateTokens
25
+ * (chars/4, what compaction budgets with). The scale applied is the one measured for that counter. */
26
+ est: number; estRaw: number; counter: "o200k" | "chars"; scale: { scale: number; measured: boolean; note: string }; window?: number;
27
+ }
28
+
29
+ export interface SummarizeOptions {
30
+ /** "exact": count with o200k, loading its table if it is not resident (385–520 ms and ~136 MB the
31
+ * first time — fine for /cost, which a person asked for). "cheap": o200k only if the table is already
32
+ * resident, else chars/4 — for the sextant usage panel, which is computed at boot and after every
33
+ * turn and must never be the reason the table loads. Default "exact". */
34
+ counter?: "exact" | "cheap";
35
+ }
36
+
37
+ export function summarizeUsage(messages: Message[], catalog: ModelCatalog, current: { provider: string; model: string }, opts: SummarizeOptions = {}): UsageSummary {
38
+ const u: UsageSummary = { inTok: 0, outTok: 0, cacheRead: 0, cacheWrite: 0, cost: 0, priced: 0, unpriced: 0, noOrigin: 0, est: 0, estRaw: 0, counter: "o200k", scale: { scale: 1, measured: false, note: "" } };
39
+ for (const m of messages) {
40
+ const usage = m.usage;
41
+ if (!usage) continue;
42
+ u.inTok += usage.input; u.outTok += usage.output;
43
+ u.cacheRead += usage.cacheRead ?? 0; u.cacheWrite += usage.cacheWrite ?? 0;
44
+ if (usage.input === 0 && usage.output === 0 && !usage.cacheRead && !usage.cacheWrite) continue; // nothing to price
45
+ if (!m.origin) u.noOrigin += 1;
46
+ const origin = m.origin ?? current;
47
+ const info = catalog.lookup(origin.provider, origin.model);
48
+ const n = { input: usage.input, output: usage.output, cacheRead: usage.cacheRead ?? 0, cacheWrite: usage.cacheWrite ?? 0 };
49
+ // the prompt this turn actually carried decides the rate on a tiered model: xAI and Google bill a
50
+ // prompt over 200k at the upper rate — xAI for the whole request. A flat model's breakdown is trivial.
51
+ const c = info?.pricing
52
+ ? costUsdTiered(n, ratesFor(info, n.input + n.cacheRead + n.cacheWrite))
53
+ : undefined;
54
+ if (c === undefined) u.unpriced += 1;
55
+ else { u.cost += c; u.priced += 1; }
56
+ }
57
+ const window = catalog.lookup(current.provider, current.model)?.contextWindow;
58
+ if (window) u.window = window;
59
+ const text = messages.map((m) => partsTokenText(m.parts)).join("\n");
60
+ const exact = opts.counter === "cheap" ? countTokensIfLoaded(text) : countTokens(text);
61
+ // Neither counter is this model's tokenizer (o200k is OpenAI's; chars/4 is nobody's). `rovecode context`
62
+ // has corrected for that since the factors were measured; this panel had not, so the live meter a user
63
+ // actually watches read up to 1.8x low on Claude 5 — the worst place for it, because this is the number
64
+ // you look at to decide whether there is room for another turn. Each counter gets ITS OWN measured
65
+ // factor: the chars/4 error is not the o200k error (token-scale.ts charScale).
66
+ const ts = tokenScaleFor(current);
67
+ if (exact !== null) {
68
+ u.counter = "o200k"; u.estRaw = exact;
69
+ u.scale = { scale: ts.scale, measured: ts.measured, note: ts.note };
70
+ } else {
71
+ u.counter = "chars"; u.estRaw = estimateTokens(text);
72
+ u.scale = { scale: ts.charScale, measured: ts.measured, note: ts.note };
73
+ }
74
+ u.est = Math.ceil(u.estRaw * u.scale.scale);
75
+ return u;
76
+ }
77
+
78
+ /** the sextant usage panel's numbers: cost = the priced lower bound (null until something is priced), context = the
79
+ * estimate. Runs at boot and after every turn, so it is the "cheap" counter: the panel's "estimated context fill"
80
+ * is chars/4 × the model's measured char factor until someone loads the o200k table (/cost, /context), and the
81
+ * o200k figure from then on. Both are estimates; neither is ever presented as the provider's count — that is
82
+ * drift() in core/context-report.ts, which counts exactly and compares against what the provider reported. */
83
+ export function sessionUsage(messages: Message[], catalog: ModelCatalog, current: { provider: string; model: string }): { costUsd: number | null; contextTokens: number; counter: "o200k" | "chars" } {
84
+ const u = summarizeUsage(messages, catalog, current, { counter: "cheap" });
85
+ return { costUsd: u.priced > 0 ? u.cost : null, contextTokens: u.est, counter: u.counter };
86
+ }
87
+
88
+ export function buildCostNote(messages: Message[], catalog: ModelCatalog, current: { provider: string; model: string }): string {
89
+ const u = summarizeUsage(messages, catalog, current);
90
+ const health = u.window ? contextHealth(u.est, u.window) : undefined;
91
+ let costLine: string;
92
+ if (u.priced === 0 && u.unpriced > 0) {
93
+ costLine = `pricing unknown for ${current.provider}/${current.model}`;
94
+ } else {
95
+ costLine = `estimated cost: $${u.cost.toFixed(4)}`;
96
+ if (u.unpriced > 0) costLine += ` — ${u.unpriced} message${u.unpriced > 1 ? "s" : ""} unpriced (lower bound)`;
97
+ if (u.noOrigin > 0) costLine += ` — ${u.noOrigin} without origin priced at the current model`;
98
+ }
99
+ return [
100
+ `tokens: ${u.inTok} in / ${u.outTok} out · cache: ${u.cacheRead} read / ${u.cacheWrite} written`,
101
+ health
102
+ ? `context: ~${u.est} of ${u.window} (${Math.round(health.fraction * 100)}%${health.nearLimit ? " — near limit" : ""})`
103
+ : `context: ~${u.est} tokens (window unknown)`,
104
+ // a corrected number that does not say it was corrected is indistinguishable from a wrong one
105
+ ...(u.scale.scale !== 1 ? [` ${u.counter === "o200k" ? "o200k counted" : "chars/4 estimated"} ${u.estRaw}, scaled ${u.scale.scale}× for ${current.model}`] : []),
106
+ costLine,
107
+ ].join("\n");
108
+ }
@@ -0,0 +1,144 @@
1
+ /** TUI info commands, extracted from app.ts for the ADR-002 cap (wiring pass): /help /status
2
+ * /cost /skills /memory /export read state and print a note; /todos (port #32) and /tasks
3
+ * (port #26) surface the agent-maintained lists. Nothing here moves the session leaf — the
4
+ * CALLER owns the swappable store/blocks and the status slice, so every read runs through
5
+ * the injected ctx (same shape as session-cmd / checkpoints-cmd). */
6
+
7
+ import type { Runtime } from "../cli/runtime.ts";
8
+ import { exportSession } from "../cli/export.ts";
9
+ import { describeSandbox } from "../core/sandbox-config.ts";
10
+ import type { SessionStore } from "../core/session.ts";
11
+ import { formatTaskList, isTerminal } from "../core/tasks.ts";
12
+ import type { BlockStore } from "../memory/blocks.ts";
13
+ import type { ModelCatalog } from "../providers/catalog.ts";
14
+ import { loadTodos, renderTodos, todoStatusLabel } from "../tools/todo.ts";
15
+ import { helpForCommands, type CustomCommand } from "./commands.ts";
16
+ import { buildCostNote } from "./cost.ts";
17
+ import type { Renderer, SlashCommand } from "./renderer.ts";
18
+ import { join } from "node:path";
19
+
20
+ export interface InfoStateSlice { provider: string; model: string; turns: number; tokensIn: number; tokensOut: number }
21
+
22
+ export interface InfoCmdCtx {
23
+ renderer: Renderer;
24
+ /** the runtime slices the info commands read: cwd, config provenance, sandbox rung, skills, tasks */
25
+ rt: Pick<Runtime, "cwd" | "projectContext" | "sandbox" | "skillStore" | "tasks">;
26
+ /** <cwd>/.rovecode/sessions — todos.json lives under <sessionsDir>/<session id> */
27
+ sessionsDir: string;
28
+ /** the ACTIVE session store, read live (/sessions and a root /rewind swap it) */
29
+ store(): SessionStore;
30
+ /** the ACTIVE memory block store, read live (swapped with the session) */
31
+ blocks(): BlockStore;
32
+ /** provider/model/turns/tokens exactly as the status line shows them */
33
+ state: InfoStateSlice;
34
+ commands: { builtin: readonly SlashCommand[]; custom: readonly CustomCommand[] };
35
+ /** /cost pricing + context window (offline snapshot; `/cost refresh` is the TUI's one fetch) */
36
+ catalog: ModelCatalog;
37
+ }
38
+
39
+ /** /help topics in display order; a command with an unknown or missing group lands under "more" at the end */
40
+ export const HELP_GROUP_ORDER: readonly string[] = ["start here", "session", "model & provider", "modes & safety", "files & history", "info"];
41
+
42
+ /** /help — the built-ins grouped by topic (SlashCommand.group), one plain line each, then the "custom:" tail (port #30). */
43
+ export function cmdHelp(ctx: InfoCmdCtx): void {
44
+ ctx.renderer.addSystemNote(groupedHelp(ctx.commands.builtin) + helpForCommands(ctx.commands.custom));
45
+ }
46
+
47
+ /** pure: the grouped body /help prints (tests read it without a renderer) */
48
+ export function groupedHelp(builtin: readonly SlashCommand[]): string {
49
+ const groups = new Map<string, SlashCommand[]>();
50
+ for (const c of builtin) { const g = c.group !== undefined && HELP_GROUP_ORDER.includes(c.group) ? c.group : "more"; const l = groups.get(g) ?? []; l.push(c); groups.set(g, l); }
51
+ const order = [...HELP_GROUP_ORDER, "more"].filter((g) => groups.has(g));
52
+ return order.map((g) => `${g}\n` + groups.get(g)!.map((c) => ` /${c.name} — ${c.description}`).join("\n")).join("\n");
53
+ }
54
+
55
+ /** /status — provider/model/turns/tokens, the active executor rung + its origin (port #27), and
56
+ * config provenance (port #8 HIGH-2: dropped/truncated sources must be visible). */
57
+ export function cmdStatus(ctx: InfoCmdCtx): void {
58
+ const pc = ctx.rt.projectContext;
59
+ const cfgBits = pc.sources.map((s) => s.chars === 0 ? `${s.path} (dropped)` : s.truncated ? `${s.path} (truncated)` : s.path);
60
+ if (pc.skippedFiles > 0) cfgBits.push(`+${pc.skippedFiles} skipped (file cap)`);
61
+ const s = ctx.state;
62
+ ctx.renderer.addSystemNote(
63
+ `provider=${s.provider} model=${s.model} turns=${s.turns} tokens=${s.tokensIn}in/${s.tokensOut}out` +
64
+ `\nsandbox: ${describeSandbox(ctx.rt.sandbox)}` +
65
+ `\nconfig: ${cfgBits.length > 0 ? cfgBits.join(", ") : "(none)"}`,
66
+ );
67
+ }
68
+
69
+ /** /cost [refresh] — ports #5+#6: normalized usage (incl. cache traffic) priced per message at its
70
+ * origin model; `refresh` re-fetches models.dev pricing (24h disk cache) — the live half is
71
+ * user-invoked only, so the TUI stays network-free unless asked. */
72
+ export function cmdCost(ctx: InfoCmdCtx, arg: string): void {
73
+ if (arg === "refresh") {
74
+ void ctx.catalog.refresh().then((ok) => ctx.renderer.addSystemNote(
75
+ ok ? "model catalog refreshed from models.dev" : "catalog refresh failed — using the offline snapshot",
76
+ ok ? "info" : "warn",
77
+ ));
78
+ return;
79
+ }
80
+ ctx.renderer.addSystemNote(buildCostNote(ctx.store().messages(), ctx.catalog, { provider: ctx.state.provider, model: ctx.state.model }));
81
+ }
82
+
83
+ /** /skills — installed skills, one `name — description` row each. */
84
+ export function cmdSkills(ctx: InfoCmdCtx): void {
85
+ const rows = ctx.rt.skillStore.list().map((s) => `${s.name} — ${s.description}`);
86
+ ctx.renderer.addSystemNote(rows.length ? rows.join("\n") : "(no skills installed)");
87
+ }
88
+
89
+ /** /memory — the active session's memory blocks as the prompt sees them. */
90
+ export function cmdMemory(ctx: InfoCmdCtx): void {
91
+ ctx.renderer.addSystemNote(ctx.blocks().renderForPrompt() || "(empty)");
92
+ }
93
+
94
+ /** /export [--json] [path] [--force] — port #38: write THIS session as markdown (raw JSONL with
95
+ * --json), local only. Read-only over the store (exportSession re-reads from disk), so no busy
96
+ * gate; a spaced path stays whole (every non-flag word joins the path). */
97
+ export function cmdExport(ctx: InfoCmdCtx, arg: string): void {
98
+ try {
99
+ const words = arg.split(/\s+/).filter(Boolean);
100
+ const res = exportSession(ctx.sessionsDir, ctx.store().id, {
101
+ json: words.includes("--json"), force: words.includes("--force"),
102
+ out: words.filter((w) => !w.startsWith("-")).join(" ") || undefined, cwd: ctx.rt.cwd,
103
+ });
104
+ ctx.renderer.addSystemNote(`exported ${res.format} → ${res.path}`);
105
+ } catch (e) {
106
+ ctx.renderer.addSystemNote(e instanceof Error ? e.message : String(e), "error");
107
+ }
108
+ }
109
+
110
+ /** /todos — port #32: the agent-maintained list at <session>/todos.json as checkbox rows. A
111
+ * corrupt file is a warning above the (empty) list, never a throw (loadTodos contract). */
112
+ export function cmdTodos(ctx: InfoCmdCtx): void {
113
+ const { items, note } = loadTodos(join(ctx.sessionsDir, ctx.store().id));
114
+ if (note) ctx.renderer.addSystemNote(note, "warn");
115
+ ctx.renderer.addSystemNote(items.length > 0 ? renderTodos(items) : "(no todos — the agent maintains the list with todo_write)");
116
+ }
117
+
118
+ /** Status-bar label for the session's list ("todos 1/3"); undefined when empty, so the app omits
119
+ * the key and plain surfaces never see a blank segment. */
120
+ export function todoLabel(sessionDir: string): string | undefined {
121
+ return todoStatusLabel(loadTodos(sessionDir).items) || undefined;
122
+ }
123
+
124
+ /** /tasks [cancel <id>|cancel all] — port #26: the runtime TaskManager's snapshot (the same rows
125
+ * as the tool's `list` action). Cancel acknowledges the REQUEST here; the "cancelled" line itself
126
+ * arrives through the app's task subscription when the child run has actually settled (a queued
127
+ * task settles at once, a running one when its aborted run returns) — one terminal note per task. */
128
+ export function cmdTasks(ctx: InfoCmdCtx, arg: string): void {
129
+ const words = arg.split(/\s+/).filter(Boolean);
130
+ const tasks = ctx.rt.tasks;
131
+ if (words.length === 0) { ctx.renderer.addSystemNote(formatTaskList(tasks.list())); return; }
132
+ if (words[0] !== "cancel" || words.length !== 2) { ctx.renderer.addSystemNote("usage: /tasks [cancel <id>|cancel all]", "warn"); return; }
133
+ const id = words[1]!;
134
+ if (id === "all") {
135
+ const n = tasks.cancelAll();
136
+ ctx.renderer.addSystemNote(n === 0 ? "no queued or running tasks to cancel" : `cancelling ${n} task${n === 1 ? "" : "s"}`);
137
+ return;
138
+ }
139
+ const before = tasks.status(id);
140
+ if (!before) { ctx.renderer.addSystemNote(`unknown task '${id}' — list with /tasks`, "warn"); return; }
141
+ if (isTerminal(before.status)) { ctx.renderer.addSystemNote(`task ${id} already ${before.status}`); return; }
142
+ tasks.cancel(id);
143
+ ctx.renderer.addSystemNote(`cancelling task ${id} (${before.label})`);
144
+ }
@@ -0,0 +1,128 @@
1
+ /** /mcp — the MCP market inside the TUI, through the two cards the surface already has. `/mcp [query]`
2
+ * opens the palette (Renderer.pickOne: the same box, keys and fuzzy filter as ⌃k) over the curated
3
+ * shelf plus the registry's matches; Enter on a row → if the server has several launch forms, one more
4
+ * pick; then the APPROVAL card (Renderer.askApproval) whose detail is the exact plan — command + args or
5
+ * URL, source, publisher, version, the env NAMES, the file — and only a yes writes. Kept out of app.ts
6
+ * (ADR-002 cap) like providers-cmd.ts.
7
+ *
8
+ * Secrets: the TUI has no masked input, so nothing is ever asked here. An install that wants a key is
9
+ * written with `${NAME}` (config.ts fills it from the environment at launch) and the closing note says
10
+ * which names to export — or to run `rovecode mcp add <name>` on a shell, where the prompt is masked. */
11
+
12
+ import type { Renderer, PickItem } from "./renderer.ts";
13
+ import { installLabel, searchMarket, type MarketDeps, type MarketEntry } from "../mcp/market.ts";
14
+ import { describePlan, fillPlan, namesWritten, planInstall, serverLine, writeServer, type McpScope } from "../mcp/market-install.ts";
15
+ import { parseConfigFile } from "../mcp/config.ts";
16
+ import { mcpTrustStatus, projectMcpFiles, trustMcpFile } from "../mcp/trust.ts";
17
+ import { installLocalPackage, localLaunch, npxPackage } from "../mcp/local-package.ts";
18
+ import { buildRecord, recordInstall } from "../market/manifest.ts";
19
+ import { rovecodeHome } from "../providers/auth.ts";
20
+
21
+ /** the subcommands the sextant offers after `/mcp ` (Enter completes the word, the name comes next) */
22
+ export const MCP_SUBCOMMANDS = ["search", "info", "add", "remove", "list", "show", "trust", "untrust"] as const;
23
+ export const MCP_COMMAND = { name: "mcp", choices: MCP_SUBCOMMANDS, choicesThen: "complete" as const, description: "Find and install an MCP server: /mcp [query] [--project] — the curated shelf, then the registry · /mcp trust approves this repo's MCP files", group: "modes & safety" };
24
+
25
+ /** `/mcp trust` — one approval card per project MCP file: the file as preview, its servers as detail; a yes
26
+ * records the file's current bytes as trusted (mcp/trust.ts), so the NEXT launch loads it */
27
+ async function trustProjectFiles(ctx: McpCmdCtx, home: string): Promise<void> {
28
+ const files = projectMcpFiles(ctx.cwd);
29
+ if (files.length === 0) { ctx.renderer.addSystemNote("mcp: no project MCP files here (.rovecode/mcp.json, .mcp.json)"); return; }
30
+ const anySet = new Proxy({}, { get: () => "set" }) as Record<string, string>;
31
+ for (const file of files) {
32
+ if (mcpTrustStatus(home, file) === "trusted") { ctx.renderer.addSystemNote(`mcp: ${file} is already trusted as it is now`); continue; }
33
+ const warnings: string[] = [];
34
+ const lines = parseConfigFile(file, warnings, anySet).map(serverLine).concat(warnings.map((w) => `! ${w}`));
35
+ const answer = await ctx.renderer.askApproval("mcp trust", file, lines.join("\n") || "(no servers in it)");
36
+ if (answer === "deny") { ctx.renderer.addSystemNote(`mcp: ${file} stays untrusted — nothing in it loads`); continue; }
37
+ const r = trustMcpFile(home, file);
38
+ ctx.renderer.addSystemNote(r.ok ? `mcp: trusted ${file} — restart me to connect; an edit asks again` : `mcp: ${r.reason}`, r.ok ? "info" : "warn");
39
+ }
40
+ }
41
+
42
+ export interface McpCmdCtx {
43
+ renderer: Renderer;
44
+ cwd: string;
45
+ home?: string;
46
+ /** registry access — tests inject a fixture fetch or `offline` */
47
+ market?: MarketDeps;
48
+ /** how `npm install` runs for the install-once pick — tests inject one that writes a fake node_modules */
49
+ spawn?: import("../mcp/local-package.ts").Spawn;
50
+ }
51
+
52
+ const clip = (s: string, n: number): string => (s.length > n ? s.slice(0, n - 1) + "…" : s);
53
+
54
+ function pickItem(e: MarketEntry): PickItem {
55
+ const src = e.source === "curated" ? "curated" : `registry · ${e.publisher ?? "?"}`;
56
+ return { value: e.key, label: e.title ?? e.key, description: `${src}${e.status ? ` · ${e.status}` : ""} · ${clip(e.description, 60)}` };
57
+ }
58
+
59
+ export async function cmdMcp(ctx: McpCmdCtx, arg: string): Promise<void> {
60
+ const { renderer } = ctx;
61
+ const words = arg.split(/\s+/).filter(Boolean);
62
+ const scope: McpScope = words.includes("--project") ? "project" : "user";
63
+ const query = words.filter((w) => !w.startsWith("--")).join(" ");
64
+ const home = ctx.home ?? rovecodeHome();
65
+ if (words[0] === "trust") { await trustProjectFiles(ctx, home); return; }
66
+ const market: MarketDeps = { home, ...ctx.market };
67
+ const found = await searchMarket(query, market);
68
+ for (const n of found.notes) renderer.addSystemNote(`mcp: ${n}`, "warn");
69
+ if (found.entries.length === 0) { renderer.addSystemNote(`mcp: nothing matches "${query}" — the registry matches on the server's name`, "warn"); return; }
70
+ const key = await renderer.pickOne(found.entries.map(pickItem), query ? `mcp market · ${query}` : "mcp market");
71
+ if (key === null) return;
72
+ const entry = found.entries.find((e) => e.key === key);
73
+ if (!entry) return;
74
+ let pick = 0;
75
+ if (entry.installs.length > 1) {
76
+ const how = await renderer.pickOne(entry.installs.map((i, ix) => ({ value: String(ix), label: i.kind === "stdio" ? `run ${installLabel(i)}` : `connect ${i.url}` })), `${entry.title ?? entry.key} · how`);
77
+ if (how === null) return;
78
+ pick = Number(how);
79
+ }
80
+ // the install-once offer (mcp/local-package.ts) as one more pick, BEFORE the plan: the card the human
81
+ // approves is then the plan that runs. Esc here writes nothing; "as today" is the npx line unchanged.
82
+ const chosen = entry.installs[pick];
83
+ const offer = chosen !== undefined ? npxPackage(chosen) : undefined;
84
+ let local = false;
85
+ if (offer !== undefined && scope === "user") { // a project file is shared: install-once's absolute path has no place in it (planInstall refuses it too)
86
+ const how = await renderer.pickOne([
87
+ { value: "local", label: `install once — node starts it in ~0.4 s`, description: `runs npm install now: ${offer.spec}'s code lands under ~/.rovecode/mcp (typically 20–30 MB, one time); no network needed to start` },
88
+ { value: "npx", label: `run through npx at every start — as today`, description: `~2 s per start, re-resolves the package and asks the npm registry each time; nothing installed now` },
89
+ ], `${entry.title ?? entry.key} · how to start it`);
90
+ if (how === null) return;
91
+ local = how === "local";
92
+ }
93
+ const plan = planInstall(entry, { scope, cwd: ctx.cwd, home, pick, ...(local ? { local: true } : {}) });
94
+ if ("error" in plan) { renderer.addSystemNote(`mcp: ${plan.error}`, "warn"); return; }
95
+ // the approval card: title = what is being done, preview = the one line that runs, detail = the whole plan
96
+ // (for install-once the detail says, in words, that npm runs and code lands on this machine)
97
+ const answer = await renderer.askApproval("mcp add", `${plan.name} ← ${plan.local ? `node ${plan.local.pkg.spec} (installed once)` : installLabel(plan.install)}`, describePlan(plan, "env").join("\n"));
98
+ if (answer === "deny") { renderer.addSystemNote("mcp: nothing written"); return; }
99
+ // install-once: npm first; only its success reaches the file, and what landed goes on record
100
+ let launch: { command: string; args: string[] } | undefined;
101
+ let pkgRecord: NonNullable<Parameters<typeof buildRecord>[1]["package"]> | undefined;
102
+ if (plan.local) {
103
+ renderer.addSystemNote(`mcp: npm install ${plan.local.pkg.spec} → ${plan.local.prefix} …`);
104
+ const lr = await installLocalPackage(plan.local.pkg, plan.local.prefix, ctx.spawn ? { spawn: ctx.spawn } : {});
105
+ if (!lr.ok) { renderer.addSystemNote(`mcp: ${lr.error} — nothing written`, "error"); return; }
106
+ launch = localLaunch(lr.pkg, plan.local.pkg.rest);
107
+ pkgRecord = { name: lr.pkg.name, version: lr.pkg.version, prefix: plan.local.prefix, bin: lr.pkg.bin, missing: lr.pkg.missing,
108
+ ...(lr.pkg.integrity !== undefined ? { integrity: lr.pkg.integrity } : {}), ...(lr.pkg.resolved !== undefined ? { resolved: lr.pkg.resolved } : {}) };
109
+ }
110
+ let trusted: boolean | undefined;
111
+ const raw = fillPlan(plan, {}, launch); // no answers: required asks become ${NAME}, optional ones are left out — the file works without them
112
+ try {
113
+ // the card just approved this exact content: a project file is trusted as written (mcp/trust.ts)
114
+ trusted = writeServer(plan.file, plan.name, raw, scope === "project" ? { trustHome: home } : {}).trusted;
115
+ } catch (e) { renderer.addSystemNote(`mcp: ${e instanceof Error ? e.message : String(e)}`, "error"); return; }
116
+ renderer.addSystemNote(`mcp: added "${plan.name}" → ${plan.file}${trusted === true ? " (trusted as written)" : ""} — restart me to connect (servers are read once per process)`);
117
+ if (pkgRecord) {
118
+ recordInstall(buildRecord({ kind: "mcp", id: entry.key, source: entry.source, ...(entry.version !== undefined ? { version: entry.version } : {}) },
119
+ { scope, target: plan.file, package: pkgRecord, installedBy: "mcp add" }), { cwd: ctx.cwd, home });
120
+ renderer.addSystemNote(`mcp: installed ${pkgRecord.name} ${pkgRecord.version} once → ${plan.local!.prefix}${pkgRecord.integrity !== undefined ? " (integrity recorded in installed.json)" : ""}`);
121
+ if (pkgRecord.missing?.length) renderer.addSystemNote(`mcp: record incomplete: ${pkgRecord.missing.join("; ")}`, "warn");
122
+ }
123
+ if (trusted === false) renderer.addSystemNote("mcp: that file already held servers you have not approved, so it is NOT trusted yet — /mcp trust shows them", "warn");
124
+ // only the names the FILE now refers to: an optional ask that was left out is not something to go and set
125
+ const named = namesWritten(plan, raw);
126
+ if (named.length) renderer.addSystemNote(`mcp: set ${named.join(", ")} in your environment before the restart — or run \`rovecode mcp add ${entry.key}\` on a shell, which asks for them masked`, "warn");
127
+ if (plan.pending.length) renderer.addSystemNote(`mcp: fill in ${plan.pending.join(", ")} in that file's args before use`, "warn");
128
+ }
@@ -0,0 +1,45 @@
1
+ /** TUI glue for plan/act modes (port #20), extracted from app.ts for the ADR-002 cap.
2
+ * Pure functions over the ModeManager + the app's mutable state slice. */
3
+
4
+ import { ModeManager, applyModeRules, buildModeChangeEntry, modeSwitchOf, planModePromptSection, type AgentMode } from "../core/modes.ts";
5
+ import type { SessionStore } from "../core/session.ts";
6
+ import type { AgentDefinition, RunConfig } from "../core/types.ts";
7
+ import type { Renderer } from "./renderer.ts";
8
+
9
+ export interface ModeStateSlice { provider: string; model: string; mode: AgentMode; busy: boolean }
10
+
11
+ /** /plan and /act — busy-gated toggle; updates the state slice from the mode's model slot. */
12
+ export function togglePlanAct(modes: ModeManager, cmd: AgentMode, state: ModeStateSlice, renderer: Renderer, pushStatus: () => void): void {
13
+ if (state.busy) { renderer.addSystemNote("finish or interrupt the run first (Esc)", "warn"); return; }
14
+ const sw = modes.toggle(cmd);
15
+ if (!sw) { renderer.addSystemNote(`already in ${cmd} mode`); return; }
16
+ const cur = modes.modelFor();
17
+ state.mode = modes.mode; state.model = cur.model; state.provider = cur.provider;
18
+ renderer.addSystemNote(cmd === "plan" ? "plan mode: read-only tools — writes/shell/spawn denied by policy" : "act mode: full toolset restored");
19
+ pushStatus();
20
+ }
21
+
22
+ /** Run-start enforcement: plan mode appends read-only rules AFTER the base set
23
+ * (last-match-wins overrides even yolo) and steers the system prompt. */
24
+ export function applyModeToRun(modes: ModeManager, cfg: RunConfig, def: AgentDefinition): void {
25
+ cfg.permissionRules = applyModeRules(modes.mode, cfg.permissionRules);
26
+ if (modes.mode !== "plan") return;
27
+ delete cfg.verify; // plan mode changes nothing and runs nothing: the verify gate (core/verify-gate.ts) is off here, not merely idle
28
+ const base = def.systemPrompt;
29
+ def.systemPrompt = (v) => (typeof base === "function" ? base(v) : base) + "\n\n" + planModePromptSection();
30
+ }
31
+
32
+ /** MED-2: a pending mode switch must survive quit and session swaps, not just the
33
+ * next submit — flush it as a durable entry so resume restores the last mode.
34
+ * Round-trip cancellations have no pending switch, so nothing lands for them. */
35
+ export function flushModeSwitch(modes: ModeManager, store: SessionStore): void {
36
+ const sw = modes.consumeSwitchNotice();
37
+ if (sw) store.append(buildModeChangeEntry(sw, store.messages().at(-1)?.id ?? null));
38
+ }
39
+
40
+ /** LOW-3: replay label for a system entry — mode switches render as a human line
41
+ * ("mode → plan"), never the raw <mode_notice> XML the entry carries. */
42
+ export function replayLabel(entry: unknown, text: string): string {
43
+ const sw = modeSwitchOf(entry);
44
+ return sw ? `mode → ${sw.to}` : text;
45
+ }
@@ -0,0 +1,97 @@
1
+ /** Overlay bodies for PiTuiRenderer: the port #24 approval diff card and the port #33 question
2
+ * card. Each is a pi-tui Component that routes keys to the SelectList (or Input) it wraps, so
3
+ * the renderer's overlay lifecycle (show → settle → hide → refocus editor) is shared and the
4
+ * wrapped list's bindings are untouched. Only pi-renderer.ts, this module and theme.ts may
5
+ * import from vendor/pi-tui. */
6
+
7
+ import {
8
+ type Component,
9
+ Input,
10
+ type SelectList,
11
+ truncateToWidth,
12
+ visibleWidth,
13
+ wrapTextWithAnsi,
14
+ } from "../../vendor/pi-tui/src/index.ts";
15
+ import { pal, st } from "./theme.ts";
16
+
17
+ /** One overlay row: leading space, clipped to width-2, padded so the right gutter stays clean. */
18
+ export function fitLine(s: string, width: number): string {
19
+ const t = truncateToWidth(s, width - 2, "…");
20
+ return ` ${t}${" ".repeat(Math.max(0, width - 1 - visibleWidth(t)))}`;
21
+ }
22
+
23
+ /** Port #24: body of an edit/write approval overlay — title, the bounded unified diff
24
+ * (+ green, - red, headers/@@ dim), a spacer, then the verdict list. Keys go straight
25
+ * to the list, so verdicts and bindings are identical to the plain approval overlay. */
26
+ const MORE_RE = /^… \+(\d+) more line/;
27
+ export class ApprovalCard implements Component {
28
+ constructor(
29
+ private readonly title: string,
30
+ private readonly diff: string[],
31
+ private readonly list: SelectList,
32
+ private readonly rows: () => number,
33
+ ) {}
34
+ handleInput(data: string): void { this.list.handleInput(data); }
35
+ invalidate(): void { this.list.invalidate(); }
36
+ render(width: number): string[] {
37
+ // physical bound: title, list and some transcript must stay visible. previewDiff already
38
+ // clipped logically — fold its marker's count into ours rather than stacking two markers.
39
+ const max = Math.max(4, this.rows() - 12);
40
+ let lines = this.diff;
41
+ if (lines.length > max) {
42
+ const tail = MORE_RE.exec(lines[lines.length - 1]!);
43
+ const hidden = lines.length - max + (tail ? Number(tail[1]) - 1 : 0);
44
+ lines = [...lines.slice(0, max), `… +${hidden} more line${hidden === 1 ? "" : "s"}`];
45
+ }
46
+ return [fitLine(pal.warn(this.title), width), ...lines.map((l, i) => fitLine(paintDiff(l, i), width)), " ".repeat(width), ...this.list.render(width)];
47
+ }
48
+ }
49
+
50
+ function paintDiff(line: string, idx: number): string {
51
+ if ((idx === 0 && line.startsWith("--- ")) || (idx === 1 && line.startsWith("+++ "))) return st.dim(line);
52
+ const c = line[0];
53
+ return c === "+" ? pal.ok(line) : c === "-" ? pal.err(line) : c === " " ? line : st.dim(line);
54
+ }
55
+
56
+ /** SelectList value of the "type an answer…" entry — can never collide with an option index. */
57
+ export const FREE_TEXT = "\u0000free-text";
58
+
59
+ /** SelectList value of the "skip this question" entry: the NON-destructive decline (resolves null →
60
+ * the tool reports "user declined to answer"), so a busy run — where Escape means "stop the run" —
61
+ * can still be left unanswered without killing it (port #33 critic LOW). Same escape idiom as
62
+ * FREE_TEXT: an option index can never look like this. */
63
+ export const SKIP_QUESTION = "\u0000skip-question";
64
+
65
+ /** Port #33: body of a question overlay — title, the wrapped question (bounded by the terminal),
66
+ * a spacer, then either the option list (options + the free-text entry + skip) or a one-line
67
+ * Input for a typed answer, each with a key hint. Keys route to whichever is active; the renderer
68
+ * wires the list/input callbacks (select, submit, escape) and flips `typing`. */
69
+ export class QuestionCard implements Component {
70
+ private typing: boolean;
71
+ readonly input = new Input();
72
+ constructor(
73
+ private readonly question: string,
74
+ private readonly list: SelectList,
75
+ private readonly optionCount: number,
76
+ private readonly freeText: boolean,
77
+ private readonly rows: () => number,
78
+ /** true while a run is in flight — decides what Escape means in the hint */
79
+ private readonly busy: () => boolean,
80
+ ) { this.typing = optionCount === 0; } // no options: straight to the input
81
+ get isTyping(): boolean { return this.typing; }
82
+ setTyping(on: boolean): void { this.typing = on; }
83
+ handleInput(data: string): void { if (this.typing) this.input.handleInput(data); else this.list.handleInput(data); }
84
+ invalidate(): void { this.list.invalidate(); this.input.invalidate(); }
85
+ render(width: number): string[] {
86
+ const bodyRows = this.typing ? 1 : this.optionCount + (this.freeText ? 1 : 0) + 1; // + the skip entry
87
+ // physical bound: title, body, hint and some transcript must stay visible
88
+ const max = Math.max(2, this.rows() - 9 - bodyRows);
89
+ let lines = wrapTextWithAnsi(this.question, width - 2);
90
+ if (lines.length > max) lines = [...lines.slice(0, max - 1), st.dim(`… +${lines.length - max + 1} more lines`)];
91
+ const esc = this.busy() ? "Esc stop the run" : "Esc skip";
92
+ const body = this.typing
93
+ ? [fitLine(this.input.render(width - 2)[0] ?? "", width), fitLine(st.dim(this.optionCount > 0 ? "Enter sends · Esc back to the options" : `Enter sends · ${esc}`), width)]
94
+ : [...this.list.render(width), fitLine(st.dim(`↑↓ choose · Enter answer · ${esc}`), width)];
95
+ return [fitLine(pal.warn("question"), width), ...lines.map((l) => fitLine(l, width)), " ".repeat(width), ...body];
96
+ }
97
+ }