rovecode 0.3.2 → 0.4.0-beta.2

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 (491) hide show
  1. package/CHANGELOG.md +512 -0
  2. package/README.md +33 -45
  3. package/bin/rovecode.js +24 -0
  4. package/dist/cli/app-dybnr56b.js +2 -0
  5. package/dist/cli/ask-user-p8hq4xgj.js +2 -0
  6. package/dist/cli/auth-login-ewpgw5sm.js +2 -0
  7. package/dist/cli/auth-m8p9grty.js +2 -0
  8. package/dist/cli/bench-xv3ypwev.js +9 -0
  9. package/dist/cli/catalog-737wb2s0.js +2 -0
  10. package/dist/cli/cli-arhg40m0.js +2 -0
  11. package/dist/cli/client-cf2pxx8q.js +2 -0
  12. package/dist/cli/commands-3p7e4xxs.js +2 -0
  13. package/dist/cli/connect-3q93d7cb.js +2 -0
  14. package/dist/cli/context-cmd-eqxmxhzq.js +2 -0
  15. package/dist/cli/context-report-hbw9zfes.js +2 -0
  16. package/dist/cli/count-remote-mby98cd0.js +2 -0
  17. package/dist/cli/design-122y0axd.js +2 -0
  18. package/dist/cli/dispatch-b4egzvvh.js +2 -0
  19. package/dist/cli/doctor-x4jkv72e.js +3 -0
  20. package/dist/cli/executor-ftvg6tsy.js +2 -0
  21. package/dist/cli/export-pdgdhkch.js +2 -0
  22. package/dist/cli/files-cez9a96p.js +2 -0
  23. package/dist/cli/gauntlet-r3xxaszc.js +2 -0
  24. package/dist/cli/gauntlet-runner-r515m7kk.js +10 -0
  25. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +5 -0
  26. package/dist/cli/gauntlet-wave4-acs9s60q.js +14 -0
  27. package/dist/cli/hashline-ewg5hbe3.js +2 -0
  28. package/dist/cli/http-n0kehsk8.js +5 -0
  29. package/dist/cli/index-z5qt1s76.js +2 -0
  30. package/dist/cli/install-80mp63kx.js +2 -0
  31. package/dist/cli/loop-12twjcat.js +2 -0
  32. package/dist/cli/main-01pv9206.js +4 -0
  33. package/dist/cli/main-0jys2ccn.js +3 -0
  34. package/dist/cli/main-1ztz6fkj.js +10 -0
  35. package/dist/cli/main-23q7cmww.js +9 -0
  36. package/dist/cli/main-2rzbexn2.js +3 -0
  37. package/dist/cli/main-2wyax8k9.js +9 -0
  38. package/dist/cli/main-2yeveeve.js +6 -0
  39. package/dist/cli/main-2z3dek0b.js +3 -0
  40. package/dist/cli/main-2zgsknth.js +3 -0
  41. package/dist/cli/main-45ejth3a.js +4 -0
  42. package/dist/cli/main-45rn3trk.js +22 -0
  43. package/dist/cli/main-4p4e2w7x.js +4 -0
  44. package/dist/cli/main-4y0tnfpa.js +16 -0
  45. package/dist/cli/main-5py0rkmc.js +4 -0
  46. package/dist/cli/main-6dtqmbt6.js +7 -0
  47. package/dist/cli/main-6h9x282m.js +4 -0
  48. package/dist/cli/main-6vjeds42.js +3 -0
  49. package/dist/cli/main-78gq4bt9.js +6 -0
  50. package/dist/cli/main-7jd5vh3x.js +4 -0
  51. package/dist/cli/main-7kt6r53y.js +4 -0
  52. package/dist/cli/main-8c1tbazx.js +58 -0
  53. package/dist/cli/main-9a9rnh47.js +19 -0
  54. package/dist/cli/main-9ht36z12.js +3 -0
  55. package/dist/cli/main-a2yfvcy9.js +7 -0
  56. package/dist/cli/main-a3f51n0x.js +5 -0
  57. package/dist/cli/main-b8zq261k.js +3 -0
  58. package/dist/cli/main-bxtvnf6d.js +13 -0
  59. package/dist/cli/main-edxc3yzt.js +4 -0
  60. package/dist/cli/main-evgz4mp5.js +21 -0
  61. package/dist/cli/main-f33fc5je.js +9 -0
  62. package/dist/cli/main-fvnpq46y.js +12 -0
  63. package/dist/cli/main-gbbty4d4.js +3 -0
  64. package/dist/cli/main-gth53dnt.js +25 -0
  65. package/dist/cli/main-hqbz10aw.js +9 -0
  66. package/dist/cli/main-hrrvcfan.js +38 -0
  67. package/dist/cli/main-hzwtsb2m.js +5 -0
  68. package/dist/cli/main-j7ttv0sd.js +34 -0
  69. package/dist/cli/main-jak598k9.js +5 -0
  70. package/dist/cli/main-kba6zeyd.js +6 -0
  71. package/dist/cli/main-kwwsz6rq.js +3 -0
  72. package/dist/cli/main-m8vm17zq.js +3 -0
  73. package/dist/cli/main-mg4f96e1.js +3 -0
  74. package/dist/cli/main-mg9b20ac.js +18 -0
  75. package/dist/cli/main-mgb9ccnx.js +3 -0
  76. package/dist/cli/main-mjt2p7aj.js +3 -0
  77. package/dist/cli/main-n6qrdbmy.js +3 -0
  78. package/dist/cli/main-na7wse0x.js +5 -0
  79. package/dist/cli/main-nqveez48.js +4 -0
  80. package/dist/cli/main-ntqef02r.js +10 -0
  81. package/dist/cli/main-nvc3yjay.js +136 -0
  82. package/dist/cli/main-p0cfn6nr.js +16 -0
  83. package/dist/cli/main-qj2djy17.js +19 -0
  84. package/dist/cli/main-qsevpgsv.js +3 -0
  85. package/dist/cli/main-qvarybsp.js +3 -0
  86. package/dist/cli/main-rebtt91r.js +5 -0
  87. package/dist/cli/main-rpg7h8mb.js +3 -0
  88. package/dist/cli/main-rsy72qmw.js +15 -0
  89. package/dist/cli/main-rvetps99.js +18 -0
  90. package/dist/cli/main-s4bb0jav.js +3 -0
  91. package/dist/cli/main-s9v8k74e.js +3 -0
  92. package/dist/cli/main-tjvwmscs.js +3 -0
  93. package/dist/cli/main-tkgarpjj.js +4 -0
  94. package/dist/cli/main-v8y60bb2.js +3 -0
  95. package/dist/cli/main-vhrrq337.js +3 -0
  96. package/dist/cli/main-vp2dfb7s.js +4 -0
  97. package/dist/cli/main-vqbr22sz.js +8 -0
  98. package/dist/cli/main-vxnwe5xx.js +18 -0
  99. package/dist/cli/main-wgph00xf.js +5 -0
  100. package/dist/cli/main-wk2csfnj.js +5 -0
  101. package/dist/cli/main-wm997zjx.js +3 -0
  102. package/dist/cli/main-wpkyraxh.js +3 -0
  103. package/dist/cli/main-wqt32p5x.js +4 -0
  104. package/dist/cli/main-x9ct6y1a.js +3 -0
  105. package/dist/cli/main-xfekqh9m.js +7 -0
  106. package/dist/cli/main-xt9zc3n6.js +7 -0
  107. package/dist/cli/main-xx2z3zh5.js +4 -0
  108. package/dist/cli/main-y5c82rxr.js +3 -0
  109. package/dist/cli/main-yrjt2sqt.js +14 -0
  110. package/dist/cli/main-ys6zj3yr.js +3 -0
  111. package/dist/cli/main-ywbxshqc.js +8 -0
  112. package/dist/cli/main-z13755t8.js +25 -0
  113. package/dist/cli/main-zc7pyrbj.js +4 -0
  114. package/dist/cli/main.js +279 -0
  115. package/dist/cli/market-cmd-bm5xvn9f.js +5 -0
  116. package/dist/cli/mcp-login-bthtfpt7.js +2 -0
  117. package/dist/cli/mcp-market-cmd-mbeshfyd.js +2 -0
  118. package/dist/cli/notify-54v5z9dz.js +2 -0
  119. package/dist/cli/oauth-g5gme95c.js +2 -0
  120. package/dist/cli/output-satndjap.js +16 -0
  121. package/dist/cli/profiles-sfhpbq3m.js +2 -0
  122. package/dist/cli/provider-config-hv3xtdt4.js +2 -0
  123. package/dist/cli/provider-kwzq6g84.js +2 -0
  124. package/dist/cli/registry-fh0hdnyn.js +2 -0
  125. package/dist/cli/registry-y1y8e94r.js +2 -0
  126. package/dist/cli/repl-t4z03mqq.js +11 -0
  127. package/dist/cli/resume-fqt4chg8.js +2 -0
  128. package/dist/cli/run-flags-rysbag9t.js +2 -0
  129. package/dist/cli/runtime-j19fjbsa.js +2 -0
  130. package/dist/cli/sandbox-config-g4qxd7y5.js +2 -0
  131. package/dist/cli/server-r0b6bksk.js +5 -0
  132. package/dist/cli/session-arg-txmn5g4x.js +2 -0
  133. package/dist/cli/session-ed250d9j.js +2 -0
  134. package/dist/cli/sessions-cmd-adw7svfn.js +7 -0
  135. package/dist/cli/settings-y9rzcqx8.js +2 -0
  136. package/dist/cli/setup-jmbr11j0.js +2 -0
  137. package/dist/cli/sextant-smoke-tcth0vea.js +5 -0
  138. package/dist/cli/skills-cmd-zbdy99v6.js +2 -0
  139. package/dist/cli/smoke-1bg937kx.js +8 -0
  140. package/dist/cli/start-chat-p01cdks3.js +12 -0
  141. package/dist/cli/stream-4wmyaypz.js +2 -0
  142. package/dist/cli/task-eg4s093s.js +2 -0
  143. package/dist/cli/tasks-12v9rr9k.js +2 -0
  144. package/dist/cli/thinking-a5ngvqyh.js +2 -0
  145. package/dist/cli/todo-1wxpcecx.js +2 -0
  146. package/dist/cli/tools-2ftsya7w.js +2 -0
  147. package/dist/cli/tools-x1tj4fxm.js +2 -0
  148. package/dist/cli/trust-cmd-hccxehzb.js +2 -0
  149. package/dist/cli/update-check-ygt3vd7m.js +2 -0
  150. package/dist/cli/update-cmd-v23qhr8c.js +2 -0
  151. package/dist/cli/voice-g1gtck92.js +2 -0
  152. package/dist/cli/webfetch-0nnrjgb5.js +2 -0
  153. package/dist/cli/websearch-f0vr2p7d.js +2 -0
  154. package/dist/cli/workspace-9rq1w4ta.js +2 -0
  155. package/dist/lib/index.js +62 -0
  156. package/dist/lib/models-index.json +1 -0
  157. package/dist/lib/plugins.js +6 -0
  158. package/dist/lib/providers.js +17 -0
  159. package/dist/lib/public-api.js +20 -0
  160. package/dist/rovecode.exe +4 -0
  161. package/package.json +38 -11
  162. package/bin/rovecode.ts +0 -21
  163. package/src/acp/server.ts +0 -374
  164. package/src/cli/auth-login.ts +0 -122
  165. package/src/cli/connect.ts +0 -244
  166. package/src/cli/context-cmd.ts +0 -199
  167. package/src/cli/dispatch.ts +0 -82
  168. package/src/cli/doctor.ts +0 -362
  169. package/src/cli/export.ts +0 -276
  170. package/src/cli/help.ts +0 -293
  171. package/src/cli/is-tui-invocation.ts +0 -8
  172. package/src/cli/main.ts +0 -583
  173. package/src/cli/market-cmd.ts +0 -658
  174. package/src/cli/mcp-login.ts +0 -141
  175. package/src/cli/mcp-market-cmd.ts +0 -302
  176. package/src/cli/output.ts +0 -382
  177. package/src/cli/repl.ts +0 -250
  178. package/src/cli/repomap-root.ts +0 -14
  179. package/src/cli/resume.ts +0 -57
  180. package/src/cli/run-flags.ts +0 -43
  181. package/src/cli/run-limits.ts +0 -78
  182. package/src/cli/runtime.ts +0 -931
  183. package/src/cli/session-arg.ts +0 -30
  184. package/src/cli/sessions-cmd.ts +0 -145
  185. package/src/cli/setup.ts +0 -153
  186. package/src/cli/skills-cmd.ts +0 -194
  187. package/src/cli/start-chat.ts +0 -65
  188. package/src/cli/trust-cmd.ts +0 -52
  189. package/src/coding/bash.ts +0 -148
  190. package/src/coding/checkpoints.ts +0 -327
  191. package/src/coding/diff.ts +0 -138
  192. package/src/coding/files.ts +0 -341
  193. package/src/coding/hashline.ts +0 -274
  194. package/src/coding/lsp-gate.ts +0 -254
  195. package/src/coding/lsp-servers.ts +0 -147
  196. package/src/coding/lsp.ts +0 -283
  197. package/src/coding/repomap-cache.ts +0 -99
  198. package/src/coding/repomap-files.ts +0 -192
  199. package/src/coding/repomap.ts +0 -481
  200. package/src/core/agents.ts +0 -255
  201. package/src/core/compaction.ts +0 -259
  202. package/src/core/config.ts +0 -289
  203. package/src/core/context-report.ts +0 -228
  204. package/src/core/context.ts +0 -60
  205. package/src/core/count-remote.ts +0 -107
  206. package/src/core/execpolicy-rules.ts +0 -196
  207. package/src/core/execpolicy.ts +0 -385
  208. package/src/core/executor.ts +0 -454
  209. package/src/core/guardrails.ts +0 -400
  210. package/src/core/hooks.ts +0 -411
  211. package/src/core/images.ts +0 -230
  212. package/src/core/intro.ts +0 -266
  213. package/src/core/loop.ts +0 -567
  214. package/src/core/modes.ts +0 -372
  215. package/src/core/orchestrator.ts +0 -245
  216. package/src/core/proc-group.ts +0 -48
  217. package/src/core/project-trust.ts +0 -98
  218. package/src/core/reflection.ts +0 -165
  219. package/src/core/sandbox-config.ts +0 -186
  220. package/src/core/session-id.ts +0 -24
  221. package/src/core/session-images.ts +0 -73
  222. package/src/core/session-ops.ts +0 -183
  223. package/src/core/session-text.ts +0 -29
  224. package/src/core/session.ts +0 -469
  225. package/src/core/settings.ts +0 -170
  226. package/src/core/tasks.ts +0 -646
  227. package/src/core/token-scale.ts +0 -108
  228. package/src/core/tools.ts +0 -309
  229. package/src/core/trust.ts +0 -104
  230. package/src/core/types.ts +0 -330
  231. package/src/core/update-check.ts +0 -171
  232. package/src/core/usage.ts +0 -204
  233. package/src/core/validate.ts +0 -121
  234. package/src/core/verify-gate.ts +0 -159
  235. package/src/core/verify.ts +0 -236
  236. package/src/core/voice.ts +0 -158
  237. package/src/core/win-job.ts +0 -183
  238. package/src/core/workspace.ts +0 -184
  239. package/src/design/audit.ts +0 -797
  240. package/src/design/direction.ts +0 -190
  241. package/src/design/rules.ts +0 -157
  242. package/src/eval/bench.ts +0 -150
  243. package/src/eval/gauntlet-runner.ts +0 -215
  244. package/src/eval/gauntlet-support.ts +0 -84
  245. package/src/eval/gauntlet-wave3.ts +0 -269
  246. package/src/eval/gauntlet-wave4.ts +0 -217
  247. package/src/eval/gauntlet.ts +0 -253
  248. package/src/index.ts +0 -17
  249. package/src/lanes/agy.ts +0 -95
  250. package/src/lanes/approval.ts +0 -24
  251. package/src/lanes/claude.ts +0 -129
  252. package/src/lanes/codex.ts +0 -127
  253. package/src/lanes/events.ts +0 -130
  254. package/src/lanes/job.ts +0 -142
  255. package/src/lanes/opencode.ts +0 -122
  256. package/src/lanes/process.ts +0 -184
  257. package/src/lanes/progress.ts +0 -183
  258. package/src/lanes/registry.ts +0 -178
  259. package/src/lanes/runner.ts +0 -124
  260. package/src/lanes/types.ts +0 -112
  261. package/src/market/catalogs/mcp-docs.json +0 -111
  262. package/src/market/catalogs/plugins.json +0 -111
  263. package/src/market/catalogs/skills.json +0 -478
  264. package/src/market/clone.ts +0 -72
  265. package/src/market/context-cost.ts +0 -121
  266. package/src/market/digest.ts +0 -106
  267. package/src/market/index.ts +0 -22
  268. package/src/market/install.ts +0 -578
  269. package/src/market/manifest.ts +0 -187
  270. package/src/market/prereq.ts +0 -145
  271. package/src/market/registry.ts +0 -363
  272. package/src/market/resolve.ts +0 -111
  273. package/src/market/types.ts +0 -236
  274. package/src/market/validate.ts +0 -227
  275. package/src/mcp/client.ts +0 -449
  276. package/src/mcp/config.ts +0 -252
  277. package/src/mcp/local-package.ts +0 -211
  278. package/src/mcp/market-catalog.ts +0 -84
  279. package/src/mcp/market-install.ts +0 -289
  280. package/src/mcp/market.ts +0 -362
  281. package/src/mcp/oauth.ts +0 -251
  282. package/src/mcp/prompts-resources.ts +0 -249
  283. package/src/mcp/shared.ts +0 -149
  284. package/src/mcp/status.ts +0 -67
  285. package/src/mcp/tools.ts +0 -275
  286. package/src/mcp/transport.ts +0 -122
  287. package/src/mcp/trust.ts +0 -25
  288. package/src/memory/blocks.ts +0 -278
  289. package/src/memory/recall.ts +0 -355
  290. package/src/memory/scope.ts +0 -182
  291. package/src/memory/store.ts +0 -105
  292. package/src/memory/tools.ts +0 -99
  293. package/src/plugins/cli.ts +0 -119
  294. package/src/plugins/discover.ts +0 -108
  295. package/src/plugins/index.ts +0 -50
  296. package/src/plugins/install.ts +0 -184
  297. package/src/plugins/load.ts +0 -124
  298. package/src/plugins/manifest.ts +0 -92
  299. package/src/plugins/state.ts +0 -83
  300. package/src/providers/auth.ts +0 -408
  301. package/src/providers/cache.ts +0 -223
  302. package/src/providers/catalog-local.ts +0 -160
  303. package/src/providers/catalog.ts +0 -421
  304. package/src/providers/middleware-context.ts +0 -86
  305. package/src/providers/middleware.ts +0 -373
  306. package/src/providers/model-list.ts +0 -23
  307. package/src/providers/oauth/common.ts +0 -105
  308. package/src/providers/oauth/device-code.ts +0 -107
  309. package/src/providers/oauth/github-copilot.ts +0 -146
  310. package/src/providers/oauth/loopback.ts +0 -158
  311. package/src/providers/oauth/openai.ts +0 -163
  312. package/src/providers/oauth/openrouter.ts +0 -89
  313. package/src/providers/oauth/pkce.ts +0 -45
  314. package/src/providers/oauth/registry.ts +0 -39
  315. package/src/providers/oauth/seam.ts +0 -89
  316. package/src/providers/profile-glm53.ts +0 -111
  317. package/src/providers/profile-sonnet5-persona.ts +0 -65
  318. package/src/providers/profile-sonnet5-voice.ts +0 -23
  319. package/src/providers/profiles.ts +0 -156
  320. package/src/providers/provider-config.ts +0 -311
  321. package/src/providers/registry.ts +0 -333
  322. package/src/providers/responses.ts +0 -209
  323. package/src/providers/retry.ts +0 -234
  324. package/src/providers/router.ts +0 -294
  325. package/src/providers/sse.ts +0 -26
  326. package/src/providers/stream-errors.ts +0 -117
  327. package/src/providers/stream.ts +0 -566
  328. package/src/providers/thinking.ts +0 -189
  329. package/src/providers/wire-messages.ts +0 -129
  330. package/src/providers/wire-responses.ts +0 -79
  331. package/src/providers/wire-select.ts +0 -53
  332. package/src/server/http.ts +0 -291
  333. package/src/server/openapi.ts +0 -246
  334. package/src/sextant/card-hits.ts +0 -102
  335. package/src/sextant/card-keys.ts +0 -55
  336. package/src/sextant/context-source.ts +0 -157
  337. package/src/sextant/crew-cards.ts +0 -350
  338. package/src/sextant/draw-agents.ts +0 -273
  339. package/src/sextant/draw-code.ts +0 -388
  340. package/src/sextant/draw-context.ts +0 -222
  341. package/src/sextant/draw-frame.ts +0 -164
  342. package/src/sextant/draw-market.ts +0 -573
  343. package/src/sextant/draw-messages.ts +0 -386
  344. package/src/sextant/draw-pet.ts +0 -230
  345. package/src/sextant/draw-plan.ts +0 -187
  346. package/src/sextant/draw-tabs.ts +0 -85
  347. package/src/sextant/draw-util.ts +0 -65
  348. package/src/sextant/draw-wizard.ts +0 -378
  349. package/src/sextant/engine.ts +0 -230
  350. package/src/sextant/frame-hits.ts +0 -25
  351. package/src/sextant/frame.ts +0 -101
  352. package/src/sextant/git-status.ts +0 -197
  353. package/src/sextant/grid.ts +0 -59
  354. package/src/sextant/input.ts +0 -119
  355. package/src/sextant/keys.ts +0 -521
  356. package/src/sextant/layout.ts +0 -86
  357. package/src/sextant/local-commands.ts +0 -169
  358. package/src/sextant/market-source.ts +0 -287
  359. package/src/sextant/mentions.ts +0 -200
  360. package/src/sextant/message-hits.ts +0 -26
  361. package/src/sextant/model.ts +0 -387
  362. package/src/sextant/overlays.ts +0 -456
  363. package/src/sextant/panel-hits.ts +0 -38
  364. package/src/sextant/pet.ts +0 -399
  365. package/src/sextant/screen.ts +0 -324
  366. package/src/sextant/scroll-hits.ts +0 -66
  367. package/src/sextant/scrollbar.ts +0 -82
  368. package/src/sextant/sextant-bridge.ts +0 -174
  369. package/src/sextant/sextant-cards.ts +0 -142
  370. package/src/sextant/sextant-diff-base.ts +0 -63
  371. package/src/sextant/sextant-files.ts +0 -154
  372. package/src/sextant/sextant-frame-loop.ts +0 -335
  373. package/src/sextant/sextant-renderer.ts +0 -574
  374. package/src/sextant/sextant-repo.ts +0 -140
  375. package/src/sextant/theme.ts +0 -66
  376. package/src/sextant/tool-rows.ts +0 -189
  377. package/src/sextant/types.ts +0 -493
  378. package/src/skills/index.ts +0 -387
  379. package/src/skills/pack.ts +0 -220
  380. package/src/skills/spec.ts +0 -162
  381. package/src/skills/tools.ts +0 -69
  382. package/src/skills/versioned.ts +0 -227
  383. package/src/telemetry/otel-export.ts +0 -122
  384. package/src/telemetry/otel-lanes.ts +0 -89
  385. package/src/telemetry/otel-logs.ts +0 -131
  386. package/src/telemetry/otel-metrics.ts +0 -136
  387. package/src/telemetry/otel.ts +0 -397
  388. package/src/telemetry/otlp.ts +0 -76
  389. package/src/tools/ask-user.ts +0 -156
  390. package/src/tools/bash-bg.ts +0 -94
  391. package/src/tools/bash-jobs.ts +0 -237
  392. package/src/tools/design.ts +0 -151
  393. package/src/tools/evalcell.ts +0 -338
  394. package/src/tools/html-text.ts +0 -139
  395. package/src/tools/provider.ts +0 -149
  396. package/src/tools/task.ts +0 -250
  397. package/src/tools/todo.ts +0 -320
  398. package/src/tools/webfetch.ts +0 -332
  399. package/src/tools/websearch.ts +0 -359
  400. package/src/tui/agents-cmd.ts +0 -41
  401. package/src/tui/app.ts +0 -749
  402. package/src/tui/attach.ts +0 -127
  403. package/src/tui/boot-notes.ts +0 -41
  404. package/src/tui/builtin-prompts.ts +0 -59
  405. package/src/tui/checkpoints-cmd.ts +0 -70
  406. package/src/tui/clipboard-image.ts +0 -81
  407. package/src/tui/clipboard.ts +0 -78
  408. package/src/tui/commands.ts +0 -283
  409. package/src/tui/config-view.ts +0 -53
  410. package/src/tui/context-cmds.ts +0 -282
  411. package/src/tui/cost.ts +0 -108
  412. package/src/tui/crash-guard.ts +0 -173
  413. package/src/tui/focus-terminal.ts +0 -34
  414. package/src/tui/git-cmds.ts +0 -273
  415. package/src/tui/git-plain.ts +0 -58
  416. package/src/tui/info-cmd.ts +0 -150
  417. package/src/tui/input-plain.ts +0 -76
  418. package/src/tui/mcp-cmd.ts +0 -128
  419. package/src/tui/memory-note.ts +0 -77
  420. package/src/tui/modes-cmd.ts +0 -45
  421. package/src/tui/notify-seq.ts +0 -100
  422. package/src/tui/notify.ts +0 -318
  423. package/src/tui/overlays.ts +0 -97
  424. package/src/tui/pi-renderer.ts +0 -428
  425. package/src/tui/providers-cmd.ts +0 -377
  426. package/src/tui/reasoning-view.ts +0 -56
  427. package/src/tui/renderer.ts +0 -128
  428. package/src/tui/replay-marker.ts +0 -29
  429. package/src/tui/session-cmd.ts +0 -148
  430. package/src/tui/session-manage.ts +0 -95
  431. package/src/tui/sextant-attach.ts +0 -102
  432. package/src/tui/sextant-io.ts +0 -202
  433. package/src/tui/sextant-smoke.ts +0 -110
  434. package/src/tui/shell-cmd.ts +0 -158
  435. package/src/tui/smoke.ts +0 -72
  436. package/src/tui/staged-terminal.ts +0 -50
  437. package/src/tui/startup.ts +0 -12
  438. package/src/tui/theme.ts +0 -59
  439. package/src/tui/todo-label.ts +0 -7
  440. package/src/tui/trust-card.ts +0 -107
  441. package/src/tui/tui-commands.ts +0 -87
  442. package/tsconfig.json +0 -30
  443. package/vendor/pi-tui/LICENSE +0 -21
  444. package/vendor/pi-tui/PATCHES.md +0 -12
  445. package/vendor/pi-tui/PROVENANCE.md +0 -12
  446. package/vendor/pi-tui/README.upstream.md +0 -854
  447. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  448. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  449. package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
  450. package/vendor/pi-tui/src/autocomplete.ts +0 -827
  451. package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
  452. package/vendor/pi-tui/src/components/box.ts +0 -138
  453. package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
  454. package/vendor/pi-tui/src/components/editor.ts +0 -2364
  455. package/vendor/pi-tui/src/components/h-stack.ts +0 -45
  456. package/vendor/pi-tui/src/components/image.ts +0 -128
  457. package/vendor/pi-tui/src/components/input.ts +0 -448
  458. package/vendor/pi-tui/src/components/loader.ts +0 -93
  459. package/vendor/pi-tui/src/components/markdown.ts +0 -1016
  460. package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
  461. package/vendor/pi-tui/src/components/select-list.ts +0 -230
  462. package/vendor/pi-tui/src/components/settings-list.ts +0 -277
  463. package/vendor/pi-tui/src/components/spacer.ts +0 -29
  464. package/vendor/pi-tui/src/components/stack.ts +0 -155
  465. package/vendor/pi-tui/src/components/text.ts +0 -108
  466. package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
  467. package/vendor/pi-tui/src/components/v-stack.ts +0 -34
  468. package/vendor/pi-tui/src/editor-component.ts +0 -75
  469. package/vendor/pi-tui/src/fuzzy.ts +0 -138
  470. package/vendor/pi-tui/src/index.ts +0 -149
  471. package/vendor/pi-tui/src/keybindings.ts +0 -321
  472. package/vendor/pi-tui/src/keys.ts +0 -1402
  473. package/vendor/pi-tui/src/kill-ring.ts +0 -47
  474. package/vendor/pi-tui/src/latex.ts +0 -1381
  475. package/vendor/pi-tui/src/layout-node.ts +0 -52
  476. package/vendor/pi-tui/src/layout.ts +0 -411
  477. package/vendor/pi-tui/src/native-modifiers.ts +0 -60
  478. package/vendor/pi-tui/src/native-module-path.ts +0 -32
  479. package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
  480. package/vendor/pi-tui/src/terminal-colors.ts +0 -74
  481. package/vendor/pi-tui/src/terminal-image.ts +0 -701
  482. package/vendor/pi-tui/src/terminal.ts +0 -554
  483. package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
  484. package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
  485. package/vendor/pi-tui/src/tui.ts +0 -1264
  486. package/vendor/pi-tui/src/undo-stack.ts +0 -29
  487. package/vendor/pi-tui/src/utils.ts +0 -1327
  488. package/vendor/pi-tui/src/word-navigation.ts +0 -118
  489. package/vendor/pi-tui/test/test-themes.ts +0 -39
  490. package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
  491. /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
- }