rovecode 0.3.3 → 0.4.0-beta.3

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 (495) hide show
  1. package/CHANGELOG.md +527 -0
  2. package/README.md +46 -48
  3. package/bin/rovecode.js +24 -0
  4. package/dist/cli/app-j6gn14w3.js +2 -0
  5. package/dist/cli/ask-user-cwstt8fz.js +2 -0
  6. package/dist/cli/auth-login-9bbp9915.js +2 -0
  7. package/dist/cli/auth-m8p9grty.js +2 -0
  8. package/dist/cli/bench-16zqdms5.js +9 -0
  9. package/dist/cli/catalog-1xchffa4.js +2 -0
  10. package/dist/cli/cli-1n1zb64f.js +2 -0
  11. package/dist/cli/client-2t9gjkck.js +2 -0
  12. package/dist/cli/commands-exafvm2b.js +2 -0
  13. package/dist/cli/connect-6zde0kn3.js +2 -0
  14. package/dist/cli/context-cmd-5t43wgqt.js +2 -0
  15. package/dist/cli/context-report-kt01pw8y.js +2 -0
  16. package/dist/cli/count-remote-ap7x3vh6.js +2 -0
  17. package/dist/cli/design-ne5zszyh.js +2 -0
  18. package/dist/cli/dispatch-2r5myxye.js +2 -0
  19. package/dist/cli/doctor-ws4fh4tn.js +3 -0
  20. package/dist/cli/executor-bdrjn634.js +2 -0
  21. package/dist/cli/export-1mxb9g5p.js +2 -0
  22. package/dist/cli/files-g104xghh.js +2 -0
  23. package/dist/cli/gauntlet-07xrjpj7.js +2 -0
  24. package/dist/cli/gauntlet-runner-xvy64436.js +10 -0
  25. package/dist/cli/gauntlet-wave3-jm91yt5w.js +5 -0
  26. package/dist/cli/gauntlet-wave4-r13py7p1.js +14 -0
  27. package/dist/cli/hashline-znvrat11.js +2 -0
  28. package/dist/cli/http-xafw6fsh.js +143 -0
  29. package/dist/cli/index-1sgjm25y.js +2 -0
  30. package/dist/cli/init-g2m0tn4m.js +51 -0
  31. package/dist/cli/install-avaqjjqq.js +2 -0
  32. package/dist/cli/loop-mmpfft01.js +2 -0
  33. package/dist/cli/main-0904f6ps.js +5 -0
  34. package/dist/cli/main-0ab9fc26.js +9 -0
  35. package/dist/cli/main-0jys2ccn.js +3 -0
  36. package/dist/cli/main-0mtcdbs7.js +3 -0
  37. package/dist/cli/main-0z1w2zsg.js +3 -0
  38. package/dist/cli/main-1dchs7xv.js +18 -0
  39. package/dist/cli/main-1ereejm1.js +3 -0
  40. package/dist/cli/main-1k1kw6b5.js +3 -0
  41. package/dist/cli/main-27y4sm2k.js +38 -0
  42. package/dist/cli/main-2wwjex5j.js +58 -0
  43. package/dist/cli/main-2yeveeve.js +6 -0
  44. package/dist/cli/main-2yfck9b5.js +3 -0
  45. package/dist/cli/main-2zmzgkwh.js +3 -0
  46. package/dist/cli/main-351pz3z7.js +7 -0
  47. package/dist/cli/main-3gjqfh7a.js +6 -0
  48. package/dist/cli/main-3nf3kgve.js +3 -0
  49. package/dist/cli/main-3pjrb2hd.js +3 -0
  50. package/dist/cli/main-3rxcvgna.js +19 -0
  51. package/dist/cli/main-4b3jgy66.js +19 -0
  52. package/dist/cli/main-4wndhjdc.js +7 -0
  53. package/dist/cli/main-4xcmvxnk.js +3 -0
  54. package/dist/cli/main-5tbz0wbz.js +4 -0
  55. package/dist/cli/main-5ywnwthm.js +3 -0
  56. package/dist/cli/main-6b62vkz0.js +14 -0
  57. package/dist/cli/main-6dnk69vp.js +3 -0
  58. package/dist/cli/main-6genrmhs.js +136 -0
  59. package/dist/cli/main-73g7eff4.js +15 -0
  60. package/dist/cli/main-7c5thhjd.js +5 -0
  61. package/dist/cli/main-7rn6bqje.js +3 -0
  62. package/dist/cli/main-80haw7qk.js +4 -0
  63. package/dist/cli/main-875s60s2.js +4 -0
  64. package/dist/cli/main-8kjxbpw4.js +8 -0
  65. package/dist/cli/main-90ds1z4e.js +10 -0
  66. package/dist/cli/main-9etavkew.js +3 -0
  67. package/dist/cli/main-a9njrkk1.js +3 -0
  68. package/dist/cli/main-aecrjq2d.js +12 -0
  69. package/dist/cli/main-ck9asesq.js +9 -0
  70. package/dist/cli/main-cta9racd.js +4 -0
  71. package/dist/cli/main-ddv7j2ag.js +3 -0
  72. package/dist/cli/main-dfreez27.js +10 -0
  73. package/dist/cli/main-f7rw7des.js +3 -0
  74. package/dist/cli/main-ggcn7rd7.js +5 -0
  75. package/dist/cli/main-gzkmycnv.js +3 -0
  76. package/dist/cli/main-hq51jg8v.js +18 -0
  77. package/dist/cli/main-jft389w9.js +8 -0
  78. package/dist/cli/main-k1eqkg83.js +3 -0
  79. package/dist/cli/main-k2y8a2aw.js +9 -0
  80. package/dist/cli/main-kcpbykxz.js +4 -0
  81. package/dist/cli/main-kd488vje.js +22 -0
  82. package/dist/cli/main-kh32yvgk.js +5 -0
  83. package/dist/cli/main-kqxnqjnv.js +25 -0
  84. package/dist/cli/main-kyn0xnsg.js +3 -0
  85. package/dist/cli/main-m1kk6fp5.js +21 -0
  86. package/dist/cli/main-mv40pcr2.js +4 -0
  87. package/dist/cli/main-n0t3973w.js +3 -0
  88. package/dist/cli/main-nqveez48.js +4 -0
  89. package/dist/cli/main-pknhvrmj.js +3 -0
  90. package/dist/cli/main-pn1w7a7j.js +3 -0
  91. package/dist/cli/main-prxxs70n.js +4 -0
  92. package/dist/cli/main-q3vsesf9.js +3 -0
  93. package/dist/cli/main-qsevpgsv.js +3 -0
  94. package/dist/cli/main-rdgdw24b.js +25 -0
  95. package/dist/cli/main-rfth4tbm.js +16 -0
  96. package/dist/cli/main-rg0wn0xf.js +5 -0
  97. package/dist/cli/main-sdmxhtv8.js +4 -0
  98. package/dist/cli/main-skbp13js.js +18 -0
  99. package/dist/cli/main-t4xnd213.js +7 -0
  100. package/dist/cli/main-vqak588n.js +4 -0
  101. package/dist/cli/main-w2n1303f.js +9 -0
  102. package/dist/cli/main-wbrdspr2.js +5 -0
  103. package/dist/cli/main-wsrg79c1.js +7 -0
  104. package/dist/cli/main-x4r0fne4.js +5 -0
  105. package/dist/cli/main-xea2f3tn.js +6 -0
  106. package/dist/cli/main-xg704a3c.js +3 -0
  107. package/dist/cli/main-xvnrabfp.js +16 -0
  108. package/dist/cli/main-xy53xf0r.js +4 -0
  109. package/dist/cli/main-y1fqy60y.js +3 -0
  110. package/dist/cli/main-yn8cd281.js +34 -0
  111. package/dist/cli/main-yr0ksc0h.js +4 -0
  112. package/dist/cli/main-z2ex2vyf.js +4 -0
  113. package/dist/cli/main-z3aayzvq.js +3 -0
  114. package/dist/cli/main-zaqh35jg.js +3 -0
  115. package/dist/cli/main-zc2e8e46.js +4 -0
  116. package/dist/cli/main-zzrfw6cf.js +13 -0
  117. package/dist/cli/main.js +280 -0
  118. package/dist/cli/market-cmd-e14kmx9n.js +5 -0
  119. package/dist/cli/mcp-login-wq7ktdek.js +2 -0
  120. package/dist/cli/mcp-market-cmd-9mg3jecy.js +2 -0
  121. package/dist/cli/notify-b7qc0cjb.js +2 -0
  122. package/dist/cli/oauth-z8whcgfx.js +2 -0
  123. package/dist/cli/output-b3ewj3ps.js +16 -0
  124. package/dist/cli/profiles-6mr5he5e.js +2 -0
  125. package/dist/cli/provider-config-g7j42q8x.js +2 -0
  126. package/dist/cli/provider-jr1y8vvm.js +2 -0
  127. package/dist/cli/registry-s8yk86g0.js +2 -0
  128. package/dist/cli/registry-t6p8d4mn.js +2 -0
  129. package/dist/cli/repl-bajwe1mh.js +11 -0
  130. package/dist/cli/resume-rwn9nz7y.js +2 -0
  131. package/dist/cli/run-flags-nah7ndpt.js +2 -0
  132. package/dist/cli/runtime-n7gafzhb.js +2 -0
  133. package/dist/cli/sandbox-config-emdy18x4.js +2 -0
  134. package/dist/cli/server-b0nvs2bn.js +5 -0
  135. package/dist/cli/session-arg-y75wd4kj.js +2 -0
  136. package/dist/cli/session-j62evmjq.js +2 -0
  137. package/dist/cli/sessions-cmd-tsnwz0ns.js +7 -0
  138. package/dist/cli/settings-df10wfez.js +2 -0
  139. package/dist/cli/setup-jzvv72fg.js +2 -0
  140. package/dist/cli/sextant-smoke-37m81ke6.js +5 -0
  141. package/dist/cli/skills-cmd-gjxnxnhx.js +2 -0
  142. package/dist/cli/smoke-p7748apt.js +8 -0
  143. package/dist/cli/start-chat-s4st3mm0.js +12 -0
  144. package/dist/cli/stream-gmeyewds.js +2 -0
  145. package/dist/cli/task-gh0kkp3n.js +2 -0
  146. package/dist/cli/tasks-z1kfpe8e.js +2 -0
  147. package/dist/cli/thinking-0eqkrz6t.js +2 -0
  148. package/dist/cli/todo-5brcrt9m.js +2 -0
  149. package/dist/cli/tools-7pzm0vj9.js +2 -0
  150. package/dist/cli/tools-s635p6s8.js +2 -0
  151. package/dist/cli/trust-cmd-cjav8zgm.js +2 -0
  152. package/dist/cli/update-check-pt31bm2f.js +2 -0
  153. package/dist/cli/update-cmd-tk131s9t.js +2 -0
  154. package/dist/cli/voice-56nabd8d.js +2 -0
  155. package/dist/cli/webfetch-xd8q596m.js +2 -0
  156. package/dist/cli/websearch-5hkf98k1.js +2 -0
  157. package/dist/cli/workflow-cmd-cy3cvzjp.js +4 -0
  158. package/dist/cli/workspace-q10g5z3e.js +2 -0
  159. package/dist/lib/index.js +62 -0
  160. package/dist/lib/models-index.json +1 -0
  161. package/dist/lib/plugins.js +55 -0
  162. package/dist/lib/providers.js +17 -0
  163. package/dist/lib/public-api.js +20 -0
  164. package/dist/lib/sdk.js +360 -0
  165. package/package.json +84 -72
  166. package/bin/rovecode.ts +0 -21
  167. package/src/acp/server.ts +0 -374
  168. package/src/cli/auth-login.ts +0 -122
  169. package/src/cli/connect.ts +0 -244
  170. package/src/cli/context-cmd.ts +0 -199
  171. package/src/cli/dispatch.ts +0 -82
  172. package/src/cli/doctor.ts +0 -362
  173. package/src/cli/export.ts +0 -276
  174. package/src/cli/help.ts +0 -293
  175. package/src/cli/is-tui-invocation.ts +0 -8
  176. package/src/cli/main.ts +0 -583
  177. package/src/cli/market-cmd.ts +0 -658
  178. package/src/cli/mcp-login.ts +0 -141
  179. package/src/cli/mcp-market-cmd.ts +0 -302
  180. package/src/cli/output.ts +0 -382
  181. package/src/cli/repl.ts +0 -250
  182. package/src/cli/repomap-root.ts +0 -14
  183. package/src/cli/resume.ts +0 -57
  184. package/src/cli/run-flags.ts +0 -43
  185. package/src/cli/run-limits.ts +0 -78
  186. package/src/cli/runtime.ts +0 -931
  187. package/src/cli/session-arg.ts +0 -30
  188. package/src/cli/sessions-cmd.ts +0 -145
  189. package/src/cli/setup.ts +0 -153
  190. package/src/cli/skills-cmd.ts +0 -194
  191. package/src/cli/start-chat.ts +0 -65
  192. package/src/cli/trust-cmd.ts +0 -52
  193. package/src/coding/bash.ts +0 -148
  194. package/src/coding/checkpoints.ts +0 -327
  195. package/src/coding/diff.ts +0 -138
  196. package/src/coding/files.ts +0 -341
  197. package/src/coding/hashline.ts +0 -274
  198. package/src/coding/lsp-gate.ts +0 -254
  199. package/src/coding/lsp-servers.ts +0 -147
  200. package/src/coding/lsp.ts +0 -283
  201. package/src/coding/repomap-cache.ts +0 -99
  202. package/src/coding/repomap-files.ts +0 -192
  203. package/src/coding/repomap.ts +0 -481
  204. package/src/core/agents.ts +0 -255
  205. package/src/core/compaction.ts +0 -259
  206. package/src/core/config.ts +0 -289
  207. package/src/core/context-report.ts +0 -228
  208. package/src/core/context.ts +0 -60
  209. package/src/core/count-remote.ts +0 -107
  210. package/src/core/execpolicy-rules.ts +0 -196
  211. package/src/core/execpolicy.ts +0 -385
  212. package/src/core/executor.ts +0 -454
  213. package/src/core/guardrails.ts +0 -400
  214. package/src/core/hooks.ts +0 -411
  215. package/src/core/images.ts +0 -230
  216. package/src/core/intro.ts +0 -266
  217. package/src/core/loop.ts +0 -567
  218. package/src/core/modes.ts +0 -372
  219. package/src/core/orchestrator.ts +0 -245
  220. package/src/core/proc-group.ts +0 -48
  221. package/src/core/project-trust.ts +0 -98
  222. package/src/core/reflection.ts +0 -165
  223. package/src/core/sandbox-config.ts +0 -186
  224. package/src/core/session-id.ts +0 -24
  225. package/src/core/session-images.ts +0 -73
  226. package/src/core/session-ops.ts +0 -183
  227. package/src/core/session-text.ts +0 -29
  228. package/src/core/session.ts +0 -469
  229. package/src/core/settings.ts +0 -170
  230. package/src/core/tasks.ts +0 -646
  231. package/src/core/token-scale.ts +0 -108
  232. package/src/core/tools.ts +0 -309
  233. package/src/core/trust.ts +0 -104
  234. package/src/core/types.ts +0 -330
  235. package/src/core/update-check.ts +0 -171
  236. package/src/core/usage.ts +0 -204
  237. package/src/core/validate.ts +0 -121
  238. package/src/core/verify-gate.ts +0 -159
  239. package/src/core/verify.ts +0 -236
  240. package/src/core/voice.ts +0 -158
  241. package/src/core/win-job.ts +0 -183
  242. package/src/core/workspace.ts +0 -184
  243. package/src/design/audit.ts +0 -797
  244. package/src/design/direction.ts +0 -190
  245. package/src/design/rules.ts +0 -157
  246. package/src/eval/bench.ts +0 -150
  247. package/src/eval/gauntlet-runner.ts +0 -215
  248. package/src/eval/gauntlet-support.ts +0 -84
  249. package/src/eval/gauntlet-wave3.ts +0 -269
  250. package/src/eval/gauntlet-wave4.ts +0 -217
  251. package/src/eval/gauntlet.ts +0 -253
  252. package/src/index.ts +0 -17
  253. package/src/lanes/agy.ts +0 -95
  254. package/src/lanes/approval.ts +0 -24
  255. package/src/lanes/claude.ts +0 -129
  256. package/src/lanes/codex.ts +0 -127
  257. package/src/lanes/events.ts +0 -130
  258. package/src/lanes/job.ts +0 -142
  259. package/src/lanes/opencode.ts +0 -122
  260. package/src/lanes/process.ts +0 -184
  261. package/src/lanes/progress.ts +0 -183
  262. package/src/lanes/registry.ts +0 -178
  263. package/src/lanes/runner.ts +0 -124
  264. package/src/lanes/types.ts +0 -112
  265. package/src/market/catalogs/mcp-docs.json +0 -111
  266. package/src/market/catalogs/plugins.json +0 -111
  267. package/src/market/catalogs/skills.json +0 -478
  268. package/src/market/clone.ts +0 -72
  269. package/src/market/context-cost.ts +0 -121
  270. package/src/market/digest.ts +0 -106
  271. package/src/market/index.ts +0 -22
  272. package/src/market/install.ts +0 -578
  273. package/src/market/manifest.ts +0 -187
  274. package/src/market/prereq.ts +0 -145
  275. package/src/market/registry.ts +0 -363
  276. package/src/market/resolve.ts +0 -111
  277. package/src/market/types.ts +0 -236
  278. package/src/market/validate.ts +0 -227
  279. package/src/mcp/client.ts +0 -449
  280. package/src/mcp/config.ts +0 -252
  281. package/src/mcp/local-package.ts +0 -211
  282. package/src/mcp/market-catalog.ts +0 -84
  283. package/src/mcp/market-install.ts +0 -289
  284. package/src/mcp/market.ts +0 -362
  285. package/src/mcp/oauth.ts +0 -251
  286. package/src/mcp/prompts-resources.ts +0 -249
  287. package/src/mcp/shared.ts +0 -149
  288. package/src/mcp/status.ts +0 -67
  289. package/src/mcp/tools.ts +0 -275
  290. package/src/mcp/transport.ts +0 -122
  291. package/src/mcp/trust.ts +0 -25
  292. package/src/memory/blocks.ts +0 -278
  293. package/src/memory/recall.ts +0 -355
  294. package/src/memory/scope.ts +0 -182
  295. package/src/memory/store.ts +0 -105
  296. package/src/memory/tools.ts +0 -99
  297. package/src/plugins/cli.ts +0 -119
  298. package/src/plugins/discover.ts +0 -108
  299. package/src/plugins/index.ts +0 -50
  300. package/src/plugins/install.ts +0 -184
  301. package/src/plugins/load.ts +0 -124
  302. package/src/plugins/manifest.ts +0 -92
  303. package/src/plugins/state.ts +0 -83
  304. package/src/providers/auth.ts +0 -408
  305. package/src/providers/cache.ts +0 -223
  306. package/src/providers/catalog-local.ts +0 -160
  307. package/src/providers/catalog.ts +0 -421
  308. package/src/providers/middleware-context.ts +0 -86
  309. package/src/providers/middleware.ts +0 -373
  310. package/src/providers/model-list.ts +0 -23
  311. package/src/providers/oauth/common.ts +0 -105
  312. package/src/providers/oauth/device-code.ts +0 -107
  313. package/src/providers/oauth/github-copilot.ts +0 -146
  314. package/src/providers/oauth/loopback.ts +0 -158
  315. package/src/providers/oauth/openai.ts +0 -163
  316. package/src/providers/oauth/openrouter.ts +0 -89
  317. package/src/providers/oauth/pkce.ts +0 -45
  318. package/src/providers/oauth/registry.ts +0 -39
  319. package/src/providers/oauth/seam.ts +0 -89
  320. package/src/providers/profile-glm53.ts +0 -111
  321. package/src/providers/profile-sonnet5-persona.ts +0 -65
  322. package/src/providers/profile-sonnet5-voice.ts +0 -23
  323. package/src/providers/profiles.ts +0 -156
  324. package/src/providers/provider-config.ts +0 -311
  325. package/src/providers/registry.ts +0 -333
  326. package/src/providers/responses.ts +0 -209
  327. package/src/providers/retry.ts +0 -234
  328. package/src/providers/router.ts +0 -294
  329. package/src/providers/sse.ts +0 -26
  330. package/src/providers/stream-errors.ts +0 -117
  331. package/src/providers/stream.ts +0 -566
  332. package/src/providers/thinking.ts +0 -189
  333. package/src/providers/wire-messages.ts +0 -129
  334. package/src/providers/wire-responses.ts +0 -79
  335. package/src/providers/wire-select.ts +0 -53
  336. package/src/server/http.ts +0 -291
  337. package/src/server/openapi.ts +0 -246
  338. package/src/sextant/card-hits.ts +0 -102
  339. package/src/sextant/card-keys.ts +0 -55
  340. package/src/sextant/context-source.ts +0 -157
  341. package/src/sextant/crew-cards.ts +0 -350
  342. package/src/sextant/draw-agents.ts +0 -273
  343. package/src/sextant/draw-code.ts +0 -388
  344. package/src/sextant/draw-context.ts +0 -222
  345. package/src/sextant/draw-frame.ts +0 -164
  346. package/src/sextant/draw-market.ts +0 -573
  347. package/src/sextant/draw-messages.ts +0 -386
  348. package/src/sextant/draw-pet.ts +0 -230
  349. package/src/sextant/draw-plan.ts +0 -187
  350. package/src/sextant/draw-tabs.ts +0 -85
  351. package/src/sextant/draw-util.ts +0 -65
  352. package/src/sextant/draw-wizard.ts +0 -378
  353. package/src/sextant/engine.ts +0 -230
  354. package/src/sextant/frame-hits.ts +0 -25
  355. package/src/sextant/frame.ts +0 -101
  356. package/src/sextant/git-status.ts +0 -197
  357. package/src/sextant/grid.ts +0 -59
  358. package/src/sextant/input.ts +0 -119
  359. package/src/sextant/keys.ts +0 -521
  360. package/src/sextant/layout.ts +0 -86
  361. package/src/sextant/local-commands.ts +0 -169
  362. package/src/sextant/market-source.ts +0 -287
  363. package/src/sextant/mentions.ts +0 -200
  364. package/src/sextant/message-hits.ts +0 -26
  365. package/src/sextant/model.ts +0 -387
  366. package/src/sextant/overlays.ts +0 -456
  367. package/src/sextant/panel-hits.ts +0 -38
  368. package/src/sextant/pet.ts +0 -399
  369. package/src/sextant/screen.ts +0 -324
  370. package/src/sextant/scroll-hits.ts +0 -66
  371. package/src/sextant/scrollbar.ts +0 -82
  372. package/src/sextant/sextant-bridge.ts +0 -174
  373. package/src/sextant/sextant-cards.ts +0 -142
  374. package/src/sextant/sextant-diff-base.ts +0 -63
  375. package/src/sextant/sextant-files.ts +0 -154
  376. package/src/sextant/sextant-frame-loop.ts +0 -335
  377. package/src/sextant/sextant-renderer.ts +0 -574
  378. package/src/sextant/sextant-repo.ts +0 -140
  379. package/src/sextant/theme.ts +0 -66
  380. package/src/sextant/tool-rows.ts +0 -189
  381. package/src/sextant/types.ts +0 -493
  382. package/src/skills/index.ts +0 -387
  383. package/src/skills/pack.ts +0 -220
  384. package/src/skills/spec.ts +0 -162
  385. package/src/skills/tools.ts +0 -69
  386. package/src/skills/versioned.ts +0 -227
  387. package/src/telemetry/otel-export.ts +0 -122
  388. package/src/telemetry/otel-lanes.ts +0 -89
  389. package/src/telemetry/otel-logs.ts +0 -131
  390. package/src/telemetry/otel-metrics.ts +0 -136
  391. package/src/telemetry/otel.ts +0 -397
  392. package/src/telemetry/otlp.ts +0 -76
  393. package/src/tools/ask-user.ts +0 -156
  394. package/src/tools/bash-bg.ts +0 -94
  395. package/src/tools/bash-jobs.ts +0 -237
  396. package/src/tools/design.ts +0 -151
  397. package/src/tools/evalcell.ts +0 -338
  398. package/src/tools/html-text.ts +0 -139
  399. package/src/tools/provider.ts +0 -149
  400. package/src/tools/task.ts +0 -250
  401. package/src/tools/todo.ts +0 -320
  402. package/src/tools/webfetch.ts +0 -332
  403. package/src/tools/websearch.ts +0 -359
  404. package/src/tui/agents-cmd.ts +0 -41
  405. package/src/tui/app.ts +0 -749
  406. package/src/tui/attach.ts +0 -127
  407. package/src/tui/boot-notes.ts +0 -41
  408. package/src/tui/builtin-prompts.ts +0 -59
  409. package/src/tui/checkpoints-cmd.ts +0 -70
  410. package/src/tui/clipboard-image.ts +0 -81
  411. package/src/tui/clipboard.ts +0 -78
  412. package/src/tui/commands.ts +0 -283
  413. package/src/tui/config-view.ts +0 -53
  414. package/src/tui/context-cmds.ts +0 -282
  415. package/src/tui/cost.ts +0 -108
  416. package/src/tui/crash-guard.ts +0 -173
  417. package/src/tui/focus-terminal.ts +0 -34
  418. package/src/tui/git-cmds.ts +0 -273
  419. package/src/tui/git-plain.ts +0 -58
  420. package/src/tui/info-cmd.ts +0 -150
  421. package/src/tui/input-plain.ts +0 -76
  422. package/src/tui/mcp-cmd.ts +0 -128
  423. package/src/tui/memory-note.ts +0 -77
  424. package/src/tui/modes-cmd.ts +0 -45
  425. package/src/tui/notify-seq.ts +0 -100
  426. package/src/tui/notify.ts +0 -318
  427. package/src/tui/overlays.ts +0 -97
  428. package/src/tui/pi-renderer.ts +0 -428
  429. package/src/tui/providers-cmd.ts +0 -377
  430. package/src/tui/reasoning-view.ts +0 -56
  431. package/src/tui/renderer.ts +0 -128
  432. package/src/tui/replay-marker.ts +0 -29
  433. package/src/tui/session-cmd.ts +0 -148
  434. package/src/tui/session-manage.ts +0 -95
  435. package/src/tui/sextant-attach.ts +0 -102
  436. package/src/tui/sextant-io.ts +0 -202
  437. package/src/tui/sextant-smoke.ts +0 -110
  438. package/src/tui/shell-cmd.ts +0 -158
  439. package/src/tui/smoke.ts +0 -72
  440. package/src/tui/staged-terminal.ts +0 -50
  441. package/src/tui/startup.ts +0 -12
  442. package/src/tui/theme.ts +0 -59
  443. package/src/tui/todo-label.ts +0 -7
  444. package/src/tui/trust-card.ts +0 -107
  445. package/src/tui/tui-commands.ts +0 -87
  446. package/tsconfig.json +0 -30
  447. package/vendor/pi-tui/LICENSE +0 -21
  448. package/vendor/pi-tui/PATCHES.md +0 -12
  449. package/vendor/pi-tui/PROVENANCE.md +0 -12
  450. package/vendor/pi-tui/README.upstream.md +0 -854
  451. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  452. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  453. package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
  454. package/vendor/pi-tui/src/autocomplete.ts +0 -827
  455. package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
  456. package/vendor/pi-tui/src/components/box.ts +0 -138
  457. package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
  458. package/vendor/pi-tui/src/components/editor.ts +0 -2364
  459. package/vendor/pi-tui/src/components/h-stack.ts +0 -45
  460. package/vendor/pi-tui/src/components/image.ts +0 -128
  461. package/vendor/pi-tui/src/components/input.ts +0 -448
  462. package/vendor/pi-tui/src/components/loader.ts +0 -93
  463. package/vendor/pi-tui/src/components/markdown.ts +0 -1016
  464. package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
  465. package/vendor/pi-tui/src/components/select-list.ts +0 -230
  466. package/vendor/pi-tui/src/components/settings-list.ts +0 -277
  467. package/vendor/pi-tui/src/components/spacer.ts +0 -29
  468. package/vendor/pi-tui/src/components/stack.ts +0 -155
  469. package/vendor/pi-tui/src/components/text.ts +0 -108
  470. package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
  471. package/vendor/pi-tui/src/components/v-stack.ts +0 -34
  472. package/vendor/pi-tui/src/editor-component.ts +0 -75
  473. package/vendor/pi-tui/src/fuzzy.ts +0 -138
  474. package/vendor/pi-tui/src/index.ts +0 -149
  475. package/vendor/pi-tui/src/keybindings.ts +0 -321
  476. package/vendor/pi-tui/src/keys.ts +0 -1402
  477. package/vendor/pi-tui/src/kill-ring.ts +0 -47
  478. package/vendor/pi-tui/src/latex.ts +0 -1381
  479. package/vendor/pi-tui/src/layout-node.ts +0 -52
  480. package/vendor/pi-tui/src/layout.ts +0 -411
  481. package/vendor/pi-tui/src/native-modifiers.ts +0 -60
  482. package/vendor/pi-tui/src/native-module-path.ts +0 -32
  483. package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
  484. package/vendor/pi-tui/src/terminal-colors.ts +0 -74
  485. package/vendor/pi-tui/src/terminal-image.ts +0 -701
  486. package/vendor/pi-tui/src/terminal.ts +0 -554
  487. package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
  488. package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
  489. package/vendor/pi-tui/src/tui.ts +0 -1264
  490. package/vendor/pi-tui/src/undo-stack.ts +0 -29
  491. package/vendor/pi-tui/src/utils.ts +0 -1327
  492. package/vendor/pi-tui/src/word-navigation.ts +0 -118
  493. package/vendor/pi-tui/test/test-themes.ts +0 -39
  494. package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
  495. /package/{src/providers → dist/cli}/models-index.json +0 -0
@@ -1,289 +0,0 @@
1
- /** From a market entry to a line in mcp.json — in three visible steps, so no install is silent:
2
- * planInstall picks the launch form and lists what must be asked; describePlan renders EXACTLY what
3
- * will be written (command + args or URL, source, publisher, version, the env NAMES, the file) for the
4
- * human to read before answering; fillPlan + writeServer put it on disk. Secrets: asked by name through
5
- * the caller's masked prompt, written as values only into the USER file (~/.rovecode/mcp.json, 0o600
6
- * where the OS honours it) — a PROJECT file gets `${NAME}` and the loader fills it from the environment
7
- * at launch (config.ts expandVars), so a token never lands in a repo. Never on the command line: a
8
- * stdio server receives them through `env`, docker through `-e NAME`. */
9
-
10
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
- import { dirname } from "node:path";
12
- import { isRecord, mcpConfigFiles, normalizeEntry, parseConfigFile, placeholderHoles, type McpServerConfig } from "./config.ts";
13
- import type { EnvSpec, MarketEntry, MarketInstall } from "./market.ts";
14
- import { installLabel } from "./market.ts";
15
- import { mcpTrustStatus, trustMcpFile } from "./trust.ts";
16
- import { localPlanLines, localPrefix, npxPackage, plannedLaunchLabel, type NpxPackage } from "./local-package.ts";
17
-
18
- export type McpScope = "user" | "project";
19
- const SERVER_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
20
- const PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_-]*)\}/g;
21
-
22
- export interface InstallPlan {
23
- entry: MarketEntry;
24
- install: MarketInstall;
25
- scope: McpScope;
26
- /** the mcp.json this lands in */
27
- file: string;
28
- /** the server's name in that file (= the tool prefix the model sees) */
29
- name: string;
30
- /** what has to be asked, in order; `secret` ones go through the masked prompt */
31
- asks: EnvSpec[];
32
- /** required arguments nobody can fill for the human (a directory, a database URL) */
33
- pending: string[];
34
- /** where a header placeholder maps back: variable name → header it belongs to */
35
- headerVars: Record<string, string>;
36
- /** install ONCE (mcp/local-package.ts): the npx package this line would run, and the shared prefix npm
37
- * puts it in. Set only when the human asked for it — the default plan is today's npx line. The launch
38
- * line written to the file is then `node <bin>`, known after npm has run, so fillPlan takes it as an
39
- * argument instead of reading it from `install`. */
40
- local?: { pkg: NpxPackage; prefix: string };
41
- }
42
-
43
- /** the server name a registry key gets in mcp.json: its last path segment, lowercased, unsafe runs → "-" */
44
- export function defaultServerName(key: string): string {
45
- const tail = key.split("/").pop() ?? key;
46
- const name = tail.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").slice(0, 64);
47
- return name.length > 0 ? name : "server";
48
- }
49
-
50
- /** env-variable-safe spelling of a header name: `X-Api-Key` → `X_API_KEY` */
51
- function headerVar(name: string): string { return name.toUpperCase().replace(/[^A-Z0-9_]/g, "_").replace(/^[0-9]/, "_$&"); }
52
-
53
- export interface PlanOptions {
54
- scope: McpScope; cwd: string; home: string;
55
- /** which of entry.installs (default: the first) */
56
- pick?: number;
57
- /** override the mcp.json name */
58
- name?: string;
59
- /** install the npx package once and launch it with node (the human said yes to the offer). An error when
60
- * the chosen form is not a plain `npx <package>` line — silently falling back to npx would write a plan
61
- * the human did not approve. */
62
- local?: boolean;
63
- }
64
-
65
- export function planInstall(entry: MarketEntry, opts: PlanOptions): InstallPlan | { error: string } {
66
- if (entry.installs.length === 0) return { error: `${entry.key} lists nothing rovecode can launch or connect to (no stdio package, no streamable-http remote)` };
67
- const ix = opts.pick ?? 0;
68
- const install = entry.installs[ix];
69
- if (!install) return { error: `${entry.key} has ${entry.installs.length} install form(s); --pick ${ix} is out of range` };
70
- const name = opts.name ?? (entry.source === "curated" ? entry.key : defaultServerName(entry.key));
71
- if (!SERVER_NAME.test(name)) return { error: `"${name}" is not a usable server name (lowercase letters, digits, . _ -)` };
72
- const files = mcpConfigFiles(opts.cwd, opts.home);
73
- const file = opts.scope === "project" ? files.project : files.user!;
74
- let local: InstallPlan["local"];
75
- if (opts.local === true) {
76
- // The launch line install-once writes is THIS machine's absolute path under its ROVECODE_HOME. A project
77
- // file is shared with everyone who clones the repo, so that line would be a server none of them can start
78
- // — refused with the way out, rather than written and discovered on someone else's machine.
79
- if (opts.scope === "project") return { error: `${entry.key}: install-once writes this machine's absolute path (node <home>/mcp/…), and a project file is shared with every clone — install it in user scope (drop --project) or keep the npx line` };
80
- const pkg = npxPackage(install);
81
- if (pkg === undefined) return { error: `${entry.key} cannot be installed once: its launch line is not a plain \`npx <package>\` (${installLabel(install)}) — drop --local to write it as it is` };
82
- local = { pkg, prefix: localPrefix(opts.home) };
83
- }
84
- const asks: EnvSpec[] = [], headerVars: Record<string, string> = {};
85
- if (install.kind === "stdio") {
86
- for (const e of install.env) {
87
- // a filled default is not a question; an optional plain value without one is left out and named in the preview
88
- if (e.default !== undefined) continue;
89
- if (e.secret || e.required) asks.push(e);
90
- }
91
- } else {
92
- for (const h of install.headers) {
93
- const vars = [...(h.template ?? "").matchAll(PLACEHOLDER)].map((m) => m[1]!);
94
- if (h.template !== undefined && vars.length === 0) continue; // a literal header, nothing to ask
95
- for (const v of vars.length ? vars : [headerVar(h.name)]) {
96
- headerVars[v] = h.name;
97
- const spec: EnvSpec = { name: v, required: h.required, secret: h.secret };
98
- if (h.description) spec.description = h.description;
99
- asks.push(spec);
100
- }
101
- }
102
- }
103
- return { entry, install, scope: opts.scope, file, name, asks, pending: install.kind === "stdio" ? install.pending : [], headerVars, ...(local ? { local } : {}) };
104
- }
105
-
106
- /** One `pending` fragment → the argv words it contributes. A fragment is literal words followed by one
107
- * `<hole>` — "--root <path>" or "<directory the server may touch>" — so the flag survives whether or not
108
- * anybody answered, and the hole is either the answer or the placeholder itself. Written into the file
109
- * unanswered it is not a silent failure: mcp/config.ts refuses to launch a server that still carries one
110
- * and names the line. Split on the hole rather than on whitespace, because a hole is usually a sentence. */
111
- export function pendingWords(fragment: string, answer?: string): string[] {
112
- const m = /^(.*?)\s*(<[^<>]*>)\s*$/.exec(fragment);
113
- const filled = answer !== undefined && answer.length > 0;
114
- if (!m) return [filled ? answer : fragment]; // no hole at all: an older catalog's bare hint
115
- const lead = m[1]!.length > 0 ? m[1]!.split(/\s+/) : [];
116
- return [...lead, filled ? answer : m[2]!];
117
- }
118
-
119
- /** the raw mcp.json entry, with answers in place. Secrets: a value in the USER file, `${NAME}` in a
120
- * PROJECT file (and `${NAME}` whenever the answer is empty, so a later `export NAME=…` completes it).
121
- * `launch` replaces the entry's own command + args — the install-once path passes `node <bin>` here once
122
- * npm has put the bin on disk (local-package.ts localLaunch); a plan without `local` never sets it. */
123
- export function fillPlan(plan: InstallPlan, answers: Record<string, string>, launch?: { command: string; args: string[] }): Record<string, unknown> {
124
- const ref = (spec: EnvSpec): string | undefined => {
125
- const v = answers[spec.name];
126
- if (v !== undefined && v.length > 0 && !(spec.secret && plan.scope === "project")) return v;
127
- if (v === undefined || v.length === 0) { if (!spec.required && !(v !== undefined && spec.secret)) return undefined; }
128
- return `\${${spec.name}}`;
129
- };
130
- const { install } = plan;
131
- if (install.kind === "stdio") {
132
- const env: Record<string, string> = {};
133
- for (const e of install.env) {
134
- if (e.default !== undefined) { env[e.name] = e.default; continue; }
135
- const v = ref(e);
136
- if (v !== undefined) env[e.name] = v;
137
- }
138
- // `pending` is the entry's required positional arguments — the filesystem server's directory, say.
139
- // They go into args either as the human's answer or, when nobody could be asked (the TUI has no
140
- // prompt, `--yes` did not stop), as the placeholder itself. Dropping them, which is what this did
141
- // first, wrote a server that could never start and a note pointing at a line that was not there.
142
- const positional = install.pending.flatMap((p) => pendingWords(p, answers[p]));
143
- const line = launch ?? { command: install.command, args: install.args };
144
- return { command: line.command, args: [...line.args, ...positional], ...(Object.keys(env).length ? { env } : {}) };
145
- }
146
- const headers: Record<string, string> = {};
147
- for (const h of install.headers) {
148
- if (h.template !== undefined && !PLACEHOLDER.test(h.template)) { headers[h.name] = h.template; PLACEHOLDER.lastIndex = 0; continue; }
149
- PLACEHOLDER.lastIndex = 0;
150
- const vars = Object.entries(plan.headerVars).filter(([, hn]) => hn === h.name).map(([v]) => v);
151
- let value = h.template ?? `{${vars[0] ?? headerVar(h.name)}}`, complete = true;
152
- for (const v of vars) {
153
- const spec = plan.asks.find((a) => a.name === v)!;
154
- const r = ref(spec);
155
- if (r === undefined) { complete = false; break; }
156
- value = value.split(`{${v}}`).join(r);
157
- }
158
- if (complete) headers[h.name] = value;
159
- }
160
- return { type: "http", url: install.url, ...(Object.keys(headers).length ? { headers } : {}) };
161
- }
162
-
163
- /** the asks whose `${NAME}` the filled entry actually carries — what the closing note tells the human to set.
164
- * An optional ask nobody answered is left out of the entry (fillPlan), so naming it would send them to set a
165
- * variable nothing reads. */
166
- export function namesWritten(plan: InstallPlan, raw: Record<string, unknown>): string[] {
167
- const text = JSON.stringify(raw);
168
- return plan.asks.filter((a) => text.includes(`\${${a.name}}`)).map((a) => a.name);
169
- }
170
-
171
- /** the confirmation text — everything the human must see before anything is written. `asking` says how
172
- * the plan's questions get answered: "prompt" (the CLI asks, secrets masked) or "env" (the TUI has no
173
- * masked input, so every asked value is written as `${NAME}` and read from the environment at launch) */
174
- export function describePlan(plan: InstallPlan, asking: "prompt" | "env" = "prompt"): string[] {
175
- const { entry, install } = plan;
176
- const asked = (secret: boolean): string => asking === "env" ? "(${NAME} — from your environment)" : secret ? "(asked, masked, never shown)" : "(asked)";
177
- const lines = [
178
- `${entry.title ?? entry.key}${entry.version ? ` ${entry.version}` : ""}${entry.status ? ` [${entry.status}]` : ""}`,
179
- ` source ${entry.source === "curated" ? "curated list (built into rovecode)" : "MCP registry (registry.modelcontextprotocol.io)"}`,
180
- ` publisher ${entry.publisher ?? "unknown"}`,
181
- ];
182
- if (entry.repository) lines.push(` repo ${entry.repository}`);
183
- // install-once: the launch line the file will hold is `node <bin>`, and the rows under it say — in so many
184
- // words — that a package manager runs and code lands on this machine. That is the plan being approved.
185
- if (plan.local) lines.push(` runs ${plannedLaunchLabel(plan.local.pkg, plan.local.prefix)}`, ...localPlanLines(plan.local.pkg, plan.local.prefix));
186
- else lines.push(install.kind === "stdio" ? ` runs ${installLabel(install)}` : ` connects ${install.url}`);
187
- const envNames = install.kind === "stdio" ? install.env : [];
188
- for (const e of envNames) {
189
- const how = e.default !== undefined ? `= ${e.default}` : plan.asks.includes(e) ? asked(e.secret).replace("NAME", e.name) : "(optional, left unset)";
190
- lines.push(` env ${e.name} ${how}${e.required ? "" : " optional"}`);
191
- }
192
- if (install.kind === "http") for (const h of install.headers) {
193
- const vars = Object.entries(plan.headerVars).filter(([, hn]) => hn === h.name).map(([v]) => v);
194
- lines.push(` header ${h.name}: ${vars.length ? `${h.template ?? `{${vars[0]}}`} ← ${vars.join(", ")} ${asked(h.secret).replace("NAME", vars[0]!)}` : h.template ?? ""}${h.required ? "" : " optional"}`);
195
- }
196
- for (const p of plan.pending) lines.push(` needs ${p} — ${asking === "env" ? "written as the placeholder; fill it in and the server connects" : "asked here; unanswered it is written as the placeholder"}`);
197
- lines.push(` writes ${plan.file} as "${plan.name}"${asking === "prompt" && plan.scope === "project" && plan.asks.some((a) => a.secret) ? " (secrets stay out of this file: ${NAME} is read from your environment)" : ""}`);
198
- return lines;
199
- }
200
-
201
- // ------------------------------------------------------------------ the file
202
-
203
- interface FileShape { json: Record<string, unknown>; servers: Record<string, unknown> }
204
- function readShape(file: string): FileShape {
205
- if (!existsSync(file)) return { json: {}, servers: {} };
206
- const json: unknown = JSON.parse(readFileSync(file, "utf8")); // a broken file is the human's to fix; we do not overwrite it
207
- if (!isRecord(json)) throw new Error(`${file}: root is not an object`);
208
- const servers = isRecord(json.mcpServers) ? json.mcpServers : {};
209
- return { json, servers };
210
- }
211
- function writeShape(file: string, shape: FileShape, secret: boolean): void {
212
- mkdirSync(dirname(file), { recursive: true });
213
- writeFileSync(file, JSON.stringify({ ...shape.json, mcpServers: shape.servers }, null, 2) + "\n", { mode: 0o600 });
214
- if (secret && process.platform !== "win32") chmodSync(file, 0o600);
215
- }
216
-
217
- /** add or replace one server; the entry is normalized first (every `${NAME}` counted as set) so the
218
- * runtime is guaranteed to accept what was written. Returns the loader's view of it. `trustHome`: the
219
- * human just approved this exact content on a card/prompt, so a PROJECT file is recorded as trusted in
220
- * that home's store right after the write (mcp/trust.ts) — the "trust my own" half of the gate. */
221
- export function writeServer(file: string, name: string, raw: Record<string, unknown>, opts: { replace?: boolean; trustHome?: string } = {}): McpServerConfig & { trusted?: boolean } {
222
- const warnings: string[] = [];
223
- // allowPlaceholders: this validates the SHAPE of an entry the human just approved, and an unanswered
224
- // `pending` hole is part of that entry by design — writing it is how the human gets a line to edit. The
225
- // loader (parseConfigFile) applies the same check without the exemption, so the server is named and
226
- // skipped until it is filled rather than launched into an argument error.
227
- const cfg = normalizeEntry(name, raw, file, warnings, new Proxy({}, { get: () => "set" }) as Record<string, string>, { allowPlaceholders: true });
228
- if (!cfg) throw new Error(warnings.join("; ") || `${name}: not a valid server entry`);
229
- const shape = readShape(file);
230
- if (shape.servers[name] !== undefined && !opts.replace) throw new Error(`${file} already has a server named "${name}" — remove it first, or add --force`);
231
- // the human approved THIS entry. The rest of the file is approved only if it already was (or there was
232
- // no file): adding to a cloned, unapproved file must not quietly bless the strangers already in it.
233
- const mayTrust = opts.trustHome !== undefined && (!existsSync(file) || Object.keys(shape.servers).filter((k) => k !== name).length === 0 || mcpTrustStatus(opts.trustHome, file) === "trusted");
234
- shape.servers[name] = raw;
235
- const secret = JSON.stringify(raw).includes("env") || JSON.stringify(raw).includes("headers");
236
- writeShape(file, shape, secret);
237
- if (opts.trustHome === undefined) return cfg;
238
- if (mayTrust) trustMcpFile(opts.trustHome, file);
239
- return { ...cfg, trusted: mayTrust };
240
- }
241
-
242
- /** delete one server; with `trustHome` the file's new bytes stay approved when the file was approved
243
- * before (a removal is the human's edit too) — an unapproved file stays unapproved */
244
- export function removeServer(file: string, name: string, opts: { trustHome?: string } = {}): boolean {
245
- if (!existsSync(file)) return false;
246
- const wasTrusted = opts.trustHome !== undefined && mcpTrustStatus(opts.trustHome, file) === "trusted";
247
- const shape = readShape(file);
248
- if (shape.servers[name] === undefined) return false;
249
- delete shape.servers[name];
250
- writeShape(file, shape, false);
251
- if (wasTrusted) trustMcpFile(opts.trustHome!, file);
252
- return true;
253
- }
254
-
255
- /** one configured server on one line, NAMES of env/headers only — never their values (they may be keys) */
256
- export function serverLine(s: McpServerConfig): string {
257
- const what = s.transport === "stdio" ? [s.command, ...(s.args ?? [])].join(" ") : s.url ?? "";
258
- const env = s.env && Object.keys(s.env).length ? ` env ${Object.keys(s.env).join(", ")}` : "";
259
- const headers = s.headers && Object.keys(s.headers).length ? ` headers ${Object.keys(s.headers).join(", ")}` : "";
260
- // an entry the loader will skip until a hand edits it says so on its own line — it is configured, not launchable
261
- const holes = placeholderHoles(s);
262
- const fill = holes.length ? ` (fill in ${holes.join(", ")})` : "";
263
- return `${s.name.padEnd(24)} ${s.transport.padEnd(5)} ${what}${env}${headers}${fill}${s.enabled === false ? " (disabled)" : ""}`;
264
- }
265
-
266
- /** every configured server with the scope it comes from, most local last — what the files SAY, which is
267
- * more than what the runtime would load: an entry still carrying a `<…>` placeholder is kept (serverLine
268
- * marks it), because the person who was told "fill in the directory after the install" and typed the
269
- * command the docs point at must not be told they have nothing. The loader's own view (skipping such an
270
- * entry with a warning) is loadMcpConfig. `warnings` collects what parsing had to say — an unreadable
271
- * file, invalid JSON, a nameless entry — for the caller to show; it used to be discarded here. */
272
- export function configuredServers(cwd: string, home: string, warnings: string[] = []): { scope: McpScope | "harvest"; file: string; server: McpServerConfig }[] {
273
- const files = mcpConfigFiles(cwd, home);
274
- const all = new Proxy({}, { get: () => "set" }) as Record<string, string>; // list what is configured, not what is launchable right now
275
- const out: { scope: McpScope | "harvest"; file: string; server: McpServerConfig }[] = [];
276
- for (const [scope, file] of [["user", files.user!], ["harvest", files.harvest], ["project", files.project]] as const) {
277
- for (const server of parseConfigFile(file, warnings, all, { allowPlaceholders: true })) out.push({ scope, file, server });
278
- }
279
- return out;
280
- }
281
-
282
- /** The `pending` fragments of a plan whose placeholder is STILL in the written entry — nobody answered for
283
- * them at the prompt (no terminal, or an empty answer). What the closing line of an install must name, and
284
- * only that: a question answered at the prompt must not leave the install telling you to go and edit a line
285
- * that now holds your answer. */
286
- export function unfilledPending(plan: InstallPlan, raw: Record<string, unknown>): string[] {
287
- const args = Array.isArray(raw.args) ? (raw.args as unknown[]) : [];
288
- return plan.pending.filter((p) => pendingWords(p).some((w) => /^<.*>$/.test(w) && args.includes(w)));
289
- }
package/src/mcp/market.ts DELETED
@@ -1,362 +0,0 @@
1
- /** The MCP market: where `rovecode mcp search|info|add` and the TUI's /mcp find servers to install.
2
- * Two sources, merged: the built-in curated list (market-catalog.ts, offline, hand-checked) and the
3
- * official registry at registry.modelcontextprotocol.io. Registry responses are UNTRUSTED DATA: every
4
- * field is re-typed here, every string is capped, every list is capped, and nothing from a response is
5
- * ever executed, evaluated or written to a config file without the human seeing it first
6
- * (market-install.ts owns the preview + write). Responses are cached under ~/.rovecode/cache/
7
- * mcp-market.json with a TTL so a flaky network or an offline train still leaves /mcp usable.
8
- *
9
- * Registry API, verified live on 2026-09-04 (the shape is NOT assumed from docs):
10
- * GET /v0/servers?search=<substring of name>&version=latest&limit=<=100[&cursor=]
11
- * → { servers: [{ server: ServerJson, _meta: { "io.modelcontextprotocol.registry/official": { status, isLatest, … } } }],
12
- * metadata: { nextCursor?, count } }
13
- * GET /v0/servers/<url-encoded name>/versions/latest → { server: ServerJson, _meta }
14
- * ServerJson: { name, description, title?, version, repository?: { url, source, subfolder? }, websiteUrl?,
15
- * packages?: [{ registryType: npm|pypi|oci|nuget|mcpb, identifier, version?, runtimeHint?, transport: { type },
16
- * runtimeArguments?: Arg[], packageArguments?: Arg[], environmentVariables?: Var[] }],
17
- * remotes?: [{ type: streamable-http|sse, url, headers?: Var[] }] }
18
- * Arg: { type: positional|named, name?, value?, default?, isRequired?, valueHint?, description? }
19
- * Var: { name, value?, default?, isRequired?, isSecret?, description? } — `value` may hold "{placeholder}"s. */
20
-
21
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
- import { dirname, join } from "node:path";
23
- import { rovecodeHome } from "../providers/auth.ts";
24
- import { isRecord, message } from "./config.ts";
25
- import { CURATED } from "./market-catalog.ts";
26
-
27
- export const REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0";
28
- /** cache TTL: a day — registry entries change rarely and the human sees the version before installing */
29
- export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
30
- const CACHE_QUERIES = 40;
31
- /** the registry's detail route has been seen taking >8s from here; a CLI can wait this long once, the cache does the rest */
32
- const FETCH_TIMEOUT_MS = 20_000;
33
- /** hard ceilings on what a response may contribute — anything past them is dropped, never trusted */
34
- export const LIMITS = { body: 2 * 1024 * 1024, servers: 100, str: 300, desc: 500, list: 32, packages: 8 } as const;
35
-
36
- /** one environment variable (or header placeholder) a server wants; `secret` ones are asked masked */
37
- export interface EnvSpec { name: string; description?: string; required: boolean; secret: boolean; default?: string }
38
- /** one HTTP header a remote wants; `{name}` placeholders in `template` are asked as EnvSpecs */
39
- export interface HeaderSpec { name: string; template?: string; description?: string; required: boolean; secret: boolean }
40
- export type MarketInstall =
41
- | { kind: "stdio"; runtime: "npx" | "uvx" | "docker" | "other"; command: string; args: string[]; env: EnvSpec[];
42
- /** required arguments the registry could not fill (a path, a token on the command line): named for the human */
43
- pending: string[] }
44
- | { kind: "http"; url: string; headers: HeaderSpec[] };
45
-
46
- export interface MarketEntry {
47
- /** what `mcp add <key>` takes: the curated short name, or the registry's reverse-DNS name */
48
- key: string;
49
- title?: string;
50
- description: string;
51
- version?: string;
52
- source: "curated" | "registry";
53
- /** who publishes it — a curated entry says so, a registry entry derives it from its namespace */
54
- publisher?: string;
55
- /** the registry's listing status when it is anything but active (e.g. "deprecated") */
56
- status?: string;
57
- repository?: string;
58
- homepage?: string;
59
- installs: MarketInstall[];
60
- }
61
-
62
- export interface MarketDeps {
63
- fetch?: typeof fetch;
64
- home?: string;
65
- now?: () => number;
66
- catalog?: readonly MarketEntry[];
67
- registryUrl?: string;
68
- /** skip the network entirely (curated + cache only) */
69
- offline?: boolean;
70
- }
71
- export interface MarketResult { entries: MarketEntry[]; notes: string[]; fromCache: boolean }
72
-
73
- // ------------------------------------------------------------------ validated re-typing of registry JSON
74
-
75
- const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
76
- const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
77
- const COMMAND = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
78
- const PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_-]*)\}/g;
79
-
80
- function str(v: unknown, max: number = LIMITS.str): string | undefined {
81
- if (typeof v !== "string" || v.length === 0) return undefined;
82
- const flat = v.replace(/[\u0000-\u001f\u007f]/g, " ");
83
- return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
84
- }
85
- function list(v: unknown, max: number = LIMITS.list): unknown[] { return Array.isArray(v) ? v.slice(0, max) : []; }
86
- function httpUrl(v: unknown): string | undefined {
87
- const s = str(v, 2000);
88
- if (s === undefined) return undefined;
89
- try { const u = new URL(s); return u.protocol === "http:" || u.protocol === "https:" ? s : undefined; } catch { return undefined; }
90
- }
91
-
92
- function envSpec(raw: unknown): EnvSpec | undefined {
93
- if (!isRecord(raw)) return undefined;
94
- const name = str(raw.name, 128);
95
- if (name === undefined || !ENV_NAME.test(name)) return undefined;
96
- const def = str(raw.value) ?? str(raw.default);
97
- const out: EnvSpec = { name, required: raw.isRequired === true, secret: raw.isSecret === true };
98
- const d = str(raw.description, LIMITS.desc);
99
- if (d !== undefined) out.description = d;
100
- if (def !== undefined && !PLACEHOLDER.test(def)) out.default = def; // a "{placeholder}" is a question, not a value
101
- PLACEHOLDER.lastIndex = 0;
102
- return out;
103
- }
104
- function headerSpec(raw: unknown): HeaderSpec | undefined {
105
- if (!isRecord(raw)) return undefined;
106
- const name = str(raw.name, 64);
107
- if (name === undefined || !HEADER_NAME.test(name)) return undefined;
108
- const out: HeaderSpec = { name, required: raw.isRequired === true, secret: raw.isSecret === true };
109
- const t = str(raw.value) ?? str(raw.default);
110
- if (t !== undefined) out.template = t;
111
- const d = str(raw.description, LIMITS.desc);
112
- if (d !== undefined) out.description = d;
113
- return out;
114
- }
115
-
116
- /** the angle brackets are what marks a hole, here and in mcp/config.ts — a value already wearing them
117
- * keeps the wording the registry chose */
118
- function hole(hint: string): string { return hint.startsWith("<") && hint.endsWith(">") ? hint : `<${hint}>`; }
119
-
120
- /** registry Arg[] → argv words; what has no value but is required goes to `pending` for the human */
121
- function argWords(raws: unknown[], pending: string[]): string[] {
122
- const words: string[] = [];
123
- for (const raw of raws) {
124
- if (!isRecord(raw)) continue;
125
- const value = str(raw.value) ?? str(raw.default);
126
- const name = str(raw.name, 128);
127
- const hint = str(raw.valueHint, 64) ?? name ?? "<argument>";
128
- if (raw.type === "named") {
129
- if (name === undefined) continue;
130
- if (value !== undefined) words.push(name, value);
131
- // a required flag with no value is "--root <path>", not "--root": pending entries are argv
132
- // FRAGMENTS the installer appends, and the <angle-bracketed> part is the hole a human fills.
133
- // Appending the bare flag would write a server that launches and then rejects its own arguments.
134
- else if (raw.isRequired === true) pending.push(`${name} ${hole(str(raw.valueHint, 64) ?? "value")}`);
135
- } else if (value !== undefined) words.push(value);
136
- else if (raw.isRequired === true) pending.push(hole(hint));
137
- }
138
- return words;
139
- }
140
-
141
- /** one registry package → a stdio install, or undefined (unsupported type, non-stdio transport, bad hint) */
142
- export function installFromPackage(raw: unknown, notes: string[]): MarketInstall | undefined {
143
- if (!isRecord(raw)) return undefined;
144
- const type = str(raw.registryType, 32), id = str(raw.identifier, 200), version = str(raw.version, 64);
145
- if (type === undefined || id === undefined) return undefined;
146
- const transport = isRecord(raw.transport) ? str(raw.transport.type, 32) : "stdio";
147
- if (transport !== undefined && transport !== "stdio") return undefined; // an http package is "run it yourself, then connect": not an install
148
- const hint = str(raw.runtimeHint, 64);
149
- if (hint !== undefined && !COMMAND.test(hint)) { notes.push(`${id}: runtime hint "${hint}" refused (not a bare command name)`); return undefined; }
150
- const pending: string[] = [];
151
- const runtimeArgs = argWords(list(raw.runtimeArguments), pending);
152
- const pkgArgs = argWords(list(raw.packageArguments), pending);
153
- const env = list(raw.environmentVariables).map(envSpec).filter((e): e is EnvSpec => e !== undefined);
154
- const pinned = version !== undefined && version !== "latest";
155
- let command: string, args: string[], runtime: Extract<MarketInstall, { kind: "stdio" }>["runtime"];
156
- switch (type) {
157
- case "npm":
158
- command = hint ?? "npx"; runtime = command === "npx" ? "npx" : "other";
159
- args = [...(command === "npx" && !runtimeArgs.includes("-y") ? ["-y"] : []), ...runtimeArgs, pinned ? `${id}@${version}` : id, ...pkgArgs];
160
- break;
161
- case "pypi":
162
- command = hint ?? "uvx"; runtime = command === "uvx" ? "uvx" : "other";
163
- args = [...runtimeArgs, pinned ? `${id}==${version}` : id, ...pkgArgs];
164
- break;
165
- case "oci":
166
- // docker gets `-e NAME` per variable so the value travels through the environment, never argv
167
- command = hint ?? "docker"; runtime = command === "docker" ? "docker" : "other";
168
- args = ["run", "-i", "--rm", ...env.flatMap((e) => ["-e", e.name]), ...runtimeArgs, pinned ? `${id}:${version}` : id, ...pkgArgs];
169
- break;
170
- default:
171
- notes.push(`${id}: package type "${type}" is not something rovecode can launch`);
172
- return undefined;
173
- }
174
- return { kind: "stdio", runtime, command, args, env, pending };
175
- }
176
-
177
- /** one registry remote → an http install, or undefined (sse is legacy and the loader refuses it anyway) */
178
- export function installFromRemote(raw: unknown, notes: string[] = []): MarketInstall | undefined {
179
- if (!isRecord(raw)) return undefined;
180
- const type = str(raw.type, 32);
181
- const url = httpUrl(raw.url);
182
- if (type !== "streamable-http" && type !== "http") {
183
- // the human asked why nothing was written: an sse (legacy) remote is the usual answer — say so, like the package path does
184
- notes.push(`${url ?? "a remote"}: ${type === "sse" ? "sse remote (legacy transport) is not something rovecode can connect to" : `remote type "${type ?? "?"}" is not something rovecode can connect to`}`);
185
- return undefined;
186
- }
187
- if (url === undefined) return undefined;
188
- return { kind: "http", url, headers: list(raw.headers).map(headerSpec).filter((h): h is HeaderSpec => h !== undefined) };
189
- }
190
-
191
- /** `io.github.owner/repo` → "github.com/owner"; `com.example/x` → "example.com" — the namespace the
192
- * registry verified ownership of, shown beside every registry result so the human knows who published it */
193
- export function publisherOf(name: string): string {
194
- const ns = name.split("/")[0] ?? name;
195
- if (ns.startsWith("io.github.")) return `github.com/${ns.slice("io.github.".length)}`;
196
- return ns.split(".").reverse().join(".");
197
- }
198
-
199
- /** one `{ server, _meta }` item → a MarketEntry; undefined when it is not an active, well-formed server */
200
- export function entryFromRegistry(item: unknown, notes: string[]): MarketEntry | undefined {
201
- if (!isRecord(item) || !isRecord(item.server)) return undefined;
202
- const s = item.server;
203
- const name = str(s.name, 200);
204
- if (name === undefined || !/^[a-z0-9][a-z0-9.-]*\/[A-Za-z0-9._-]+$/.test(name)) return undefined;
205
- // the registry's own verdict on the listing (active · deprecated · deleted): a deleted server is gone,
206
- // anything else is kept WITH its status so the human sees "deprecated" before installing
207
- const meta = isRecord(item._meta) ? item._meta["io.modelcontextprotocol.registry/official"] : undefined;
208
- const status = isRecord(meta) ? str(meta.status, 32) : undefined;
209
- if (status === "deleted") return undefined;
210
- const installs: MarketInstall[] = [];
211
- for (const p of list(s.packages, LIMITS.packages)) { const i = installFromPackage(p, notes); if (i) installs.push(i); }
212
- for (const r of list(s.remotes, LIMITS.packages)) { const i = installFromRemote(r, notes); if (i) installs.push(i); }
213
- const out: MarketEntry = { key: name, description: str(s.description, LIMITS.desc) ?? "", source: "registry", publisher: publisherOf(name), installs };
214
- const title = str(s.title, 120), version = str(s.version, 64), home = httpUrl(s.websiteUrl);
215
- const repo = isRecord(s.repository) ? httpUrl(s.repository.url) : undefined;
216
- if (title !== undefined) out.title = title;
217
- if (version !== undefined) out.version = version;
218
- if (status !== undefined && status !== "active") out.status = status;
219
- if (repo !== undefined) out.repository = repo;
220
- if (home !== undefined) out.homepage = home;
221
- return out;
222
- }
223
-
224
- /** a list page → entries (deduped by name, capped at LIMITS.servers) */
225
- /** The cursor for the NEXT page, if the registry says there is one. Kept apart from parseRegistryPage so
226
- * that function's shape (and every caller of it) stays as it was. A cursor is opaque, so it is only
227
- * length-capped and type-checked — never interpreted. */
228
- export function nextCursorOf(json: unknown): string | undefined {
229
- if (!isRecord(json) || !isRecord(json.metadata)) return undefined;
230
- const c = json.metadata.nextCursor;
231
- return typeof c === "string" && c.length > 0 && c.length <= LIMITS.str ? c : undefined;
232
- }
233
-
234
- /** how many pages one search will follow. 5 x 50 = 250 servers is far past what a person reads, and the
235
- * ceiling matters more than the reach: a cursor loop with no bound is a request amplifier pointed at us
236
- * by whatever the registry returns. */
237
- export const MAX_PAGES = 5;
238
-
239
- export function parseRegistryPage(json: unknown, notes: string[]): MarketEntry[] {
240
- if (!isRecord(json)) { notes.push("registry: response is not an object"); return []; }
241
- const seen = new Set<string>(), out: MarketEntry[] = [];
242
- for (const item of list(json.servers, LIMITS.servers)) {
243
- const e = entryFromRegistry(item, notes);
244
- if (e && !seen.has(e.key)) { seen.add(e.key); out.push(e); }
245
- }
246
- return out;
247
- }
248
-
249
- // ------------------------------------------------------------------ cache (~/.rovecode/cache/mcp-market.json)
250
-
251
- interface CacheFile { version: 1; queries: Record<string, { at: number; entries: MarketEntry[] }> }
252
- export function cachePath(home: string): string { return join(home, "cache", "mcp-market.json"); }
253
-
254
- function readCache(home: string): CacheFile {
255
- const path = cachePath(home);
256
- if (!existsSync(path)) return { version: 1, queries: {} };
257
- try {
258
- const json: unknown = JSON.parse(readFileSync(path, "utf8"));
259
- if (isRecord(json) && json.version === 1 && isRecord(json.queries)) return json as unknown as CacheFile;
260
- } catch { /* a broken cache is an empty cache */ }
261
- return { version: 1, queries: {} };
262
- }
263
- function writeCache(home: string, cache: CacheFile): void {
264
- // keep the newest CACHE_QUERIES; the file is a convenience, never a source of truth
265
- const keep = Object.entries(cache.queries).sort((a, b) => b[1].at - a[1].at).slice(0, CACHE_QUERIES);
266
- try {
267
- mkdirSync(dirname(cachePath(home)), { recursive: true });
268
- writeFileSync(cachePath(home), JSON.stringify({ version: 1, queries: Object.fromEntries(keep) }) + "\n");
269
- } catch { /* read-only home: the search still answered */ }
270
- }
271
-
272
- // ------------------------------------------------------------------ registry client
273
-
274
- async function registryGet(path: string, deps: MarketDeps): Promise<unknown> {
275
- const f = deps.fetch ?? fetch;
276
- const base = deps.registryUrl ?? process.env.ROVECODE_MCP_REGISTRY ?? REGISTRY_URL;
277
- const res = await f(`${base}${path}`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
278
- if (!res.ok) throw new Error(`registry answered ${res.status}`);
279
- const text = await res.text();
280
- if (text.length > LIMITS.body) throw new Error(`registry response too large (${text.length} bytes)`);
281
- return JSON.parse(text) as unknown;
282
- }
283
-
284
- /** cache-through: a fresh cached answer is served as is; otherwise the network, then the cache; a
285
- * failed network falls back to a STALE cached answer with a note, else to nothing with the reason */
286
- async function cached(key: string, load: () => Promise<MarketEntry[]>, deps: MarketDeps, notes: string[]): Promise<{ entries: MarketEntry[]; fromCache: boolean }> {
287
- const home = deps.home ?? rovecodeHome(), now = (deps.now ?? Date.now)();
288
- const cache = readCache(home), hit = cache.queries[key];
289
- if (hit && now - hit.at < CACHE_TTL_MS) return { entries: hit.entries, fromCache: true };
290
- if (deps.offline) { if (hit) notes.push("offline: showing cached registry results"); return { entries: hit?.entries ?? [], fromCache: hit !== undefined }; }
291
- try {
292
- const entries = await load();
293
- cache.queries[key] = { at: now, entries };
294
- writeCache(home, cache);
295
- return { entries, fromCache: false };
296
- } catch (err) {
297
- notes.push(`registry unreachable (${message(err)})${hit ? " — showing cached results" : ""}`);
298
- return { entries: hit?.entries ?? [], fromCache: hit !== undefined };
299
- }
300
- }
301
-
302
- /** curated first (all of them for an empty query; substring on key/title/description otherwise), then
303
- * the registry's name matches for a query of two+ characters, minus anything the curated list already names */
304
- export async function searchMarket(query: string, deps: MarketDeps = {}): Promise<MarketResult> {
305
- const notes: string[] = [];
306
- const q = query.trim().toLowerCase();
307
- const catalog = deps.catalog ?? CURATED;
308
- const words = q.split(/\s+/).filter(Boolean);
309
- const curated = catalog.filter((e) => words.every((w) => `${e.key} ${e.title ?? ""} ${e.description}`.toLowerCase().includes(w)));
310
- if (q.length < 2) return { entries: curated, notes, fromCache: false };
311
- const { entries, fromCache } = await cached(`search:${q}`, async () => {
312
- // FOLLOW THE CURSOR. A single page was not a smaller result, it was a WRONG one: the registry answers
313
- // "here are the first 50 and there is more", and stopping there tells the human their server does not
314
- // exist when it is on page two. Bounded three ways — page count, total entries, and a cursor that must
315
- // actually change — because a paginating client that trusts the server's cursor can be walked forever.
316
- const parseNotes: string[] = [];
317
- const all: MarketEntry[] = [];
318
- const seenKeys = new Set<string>(); // parseRegistryPage dedupes WITHIN a page; this spans pages
319
- const seenCursors = new Set<string>();
320
- let cursor: string | undefined;
321
- for (let page = 0; page < MAX_PAGES; page++) {
322
- const url = `/servers?search=${encodeURIComponent(q)}&version=latest&limit=50${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
323
- const json = await registryGet(url, deps);
324
- for (const e of parseRegistryPage(json, parseNotes)) {
325
- if (seenKeys.has(e.key)) continue;
326
- seenKeys.add(e.key);
327
- all.push(e);
328
- }
329
- if (all.length >= LIMITS.servers) break;
330
- const next = nextCursorOf(json);
331
- if (next === undefined || seenCursors.has(next)) break; // no more, or the registry is looping us
332
- seenCursors.add(next);
333
- cursor = next;
334
- }
335
- return all.slice(0, LIMITS.servers);
336
- }, deps, notes);
337
- const taken = new Set(curated.flatMap((e) => [e.key, ...(e.repository ? [e.repository] : [])]));
338
- return { entries: [...curated, ...entries.filter((e) => !taken.has(e.key) && !(e.repository && taken.has(e.repository)))], notes, fromCache };
339
- }
340
-
341
- /** one entry by key: the curated one if the key is a curated short name, else the registry's latest version */
342
- export async function marketInfo(key: string, deps: MarketDeps = {}): Promise<{ entry?: MarketEntry; notes: string[] }> {
343
- const notes: string[] = [];
344
- const name = key.trim();
345
- const catalog = deps.catalog ?? CURATED;
346
- const hit = catalog.find((e) => e.key === name);
347
- if (hit) return { entry: hit, notes };
348
- if (!name.includes("/")) { notes.push(`"${name}" is not a curated name and not a registry name (those look like io.github.owner/repo) — try: rovecode mcp search ${name}`); return { notes }; }
349
- const { entries } = await cached(`info:${name}`, async () => {
350
- const json = await registryGet(`/servers/${encodeURIComponent(name)}/versions/latest`, deps);
351
- const e = entryFromRegistry(json, notes);
352
- return e ? [e] : [];
353
- }, deps, notes);
354
- const entry = entries[0];
355
- if (!entry && notes.length === 0) notes.push(`"${name}" is not in the registry`);
356
- return entry ? { entry, notes } : { notes };
357
- }
358
-
359
- /** what a stdio install runs / an http install connects to — one line, exact, for lists and cards */
360
- export function installLabel(i: MarketInstall): string {
361
- return i.kind === "stdio" ? [i.command, ...i.args].join(" ") : i.url;
362
- }