rovecode 0.4.0-beta.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (428) hide show
  1. package/README.md +67 -69
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -37
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -512
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-dybnr56b.js +0 -2
  272. package/dist/cli/ask-user-p8hq4xgj.js +0 -2
  273. package/dist/cli/auth-login-ewpgw5sm.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-xv3ypwev.js +0 -9
  276. package/dist/cli/catalog-737wb2s0.js +0 -2
  277. package/dist/cli/cli-arhg40m0.js +0 -2
  278. package/dist/cli/client-cf2pxx8q.js +0 -2
  279. package/dist/cli/commands-3p7e4xxs.js +0 -2
  280. package/dist/cli/connect-3q93d7cb.js +0 -2
  281. package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
  282. package/dist/cli/context-report-hbw9zfes.js +0 -2
  283. package/dist/cli/count-remote-mby98cd0.js +0 -2
  284. package/dist/cli/design-122y0axd.js +0 -2
  285. package/dist/cli/dispatch-b4egzvvh.js +0 -2
  286. package/dist/cli/doctor-x4jkv72e.js +0 -3
  287. package/dist/cli/executor-ftvg6tsy.js +0 -2
  288. package/dist/cli/export-pdgdhkch.js +0 -2
  289. package/dist/cli/files-cez9a96p.js +0 -2
  290. package/dist/cli/gauntlet-r3xxaszc.js +0 -2
  291. package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
  292. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
  293. package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
  294. package/dist/cli/hashline-ewg5hbe3.js +0 -2
  295. package/dist/cli/http-n0kehsk8.js +0 -5
  296. package/dist/cli/index-z5qt1s76.js +0 -2
  297. package/dist/cli/install-80mp63kx.js +0 -2
  298. package/dist/cli/loop-12twjcat.js +0 -2
  299. package/dist/cli/main-01pv9206.js +0 -4
  300. package/dist/cli/main-0jys2ccn.js +0 -3
  301. package/dist/cli/main-1ztz6fkj.js +0 -10
  302. package/dist/cli/main-23q7cmww.js +0 -9
  303. package/dist/cli/main-2rzbexn2.js +0 -3
  304. package/dist/cli/main-2wyax8k9.js +0 -9
  305. package/dist/cli/main-2yeveeve.js +0 -6
  306. package/dist/cli/main-2z3dek0b.js +0 -3
  307. package/dist/cli/main-2zgsknth.js +0 -3
  308. package/dist/cli/main-45ejth3a.js +0 -4
  309. package/dist/cli/main-45rn3trk.js +0 -22
  310. package/dist/cli/main-4p4e2w7x.js +0 -4
  311. package/dist/cli/main-4y0tnfpa.js +0 -16
  312. package/dist/cli/main-5py0rkmc.js +0 -4
  313. package/dist/cli/main-6dtqmbt6.js +0 -7
  314. package/dist/cli/main-6h9x282m.js +0 -4
  315. package/dist/cli/main-6vjeds42.js +0 -3
  316. package/dist/cli/main-78gq4bt9.js +0 -6
  317. package/dist/cli/main-7jd5vh3x.js +0 -4
  318. package/dist/cli/main-7kt6r53y.js +0 -4
  319. package/dist/cli/main-8c1tbazx.js +0 -58
  320. package/dist/cli/main-9a9rnh47.js +0 -19
  321. package/dist/cli/main-9ht36z12.js +0 -3
  322. package/dist/cli/main-a2yfvcy9.js +0 -7
  323. package/dist/cli/main-a3f51n0x.js +0 -5
  324. package/dist/cli/main-b8zq261k.js +0 -3
  325. package/dist/cli/main-bxtvnf6d.js +0 -13
  326. package/dist/cli/main-edxc3yzt.js +0 -4
  327. package/dist/cli/main-evgz4mp5.js +0 -21
  328. package/dist/cli/main-f33fc5je.js +0 -9
  329. package/dist/cli/main-fvnpq46y.js +0 -12
  330. package/dist/cli/main-gbbty4d4.js +0 -3
  331. package/dist/cli/main-gth53dnt.js +0 -25
  332. package/dist/cli/main-hqbz10aw.js +0 -9
  333. package/dist/cli/main-hrrvcfan.js +0 -38
  334. package/dist/cli/main-hzwtsb2m.js +0 -5
  335. package/dist/cli/main-j7ttv0sd.js +0 -34
  336. package/dist/cli/main-jak598k9.js +0 -5
  337. package/dist/cli/main-kba6zeyd.js +0 -6
  338. package/dist/cli/main-kwwsz6rq.js +0 -3
  339. package/dist/cli/main-m8vm17zq.js +0 -3
  340. package/dist/cli/main-mg4f96e1.js +0 -3
  341. package/dist/cli/main-mg9b20ac.js +0 -18
  342. package/dist/cli/main-mgb9ccnx.js +0 -3
  343. package/dist/cli/main-mjt2p7aj.js +0 -3
  344. package/dist/cli/main-n6qrdbmy.js +0 -3
  345. package/dist/cli/main-na7wse0x.js +0 -5
  346. package/dist/cli/main-nqveez48.js +0 -4
  347. package/dist/cli/main-ntqef02r.js +0 -10
  348. package/dist/cli/main-nvc3yjay.js +0 -136
  349. package/dist/cli/main-p0cfn6nr.js +0 -16
  350. package/dist/cli/main-qj2djy17.js +0 -19
  351. package/dist/cli/main-qsevpgsv.js +0 -3
  352. package/dist/cli/main-qvarybsp.js +0 -3
  353. package/dist/cli/main-rebtt91r.js +0 -5
  354. package/dist/cli/main-rpg7h8mb.js +0 -3
  355. package/dist/cli/main-rsy72qmw.js +0 -15
  356. package/dist/cli/main-rvetps99.js +0 -18
  357. package/dist/cli/main-s4bb0jav.js +0 -3
  358. package/dist/cli/main-s9v8k74e.js +0 -3
  359. package/dist/cli/main-tjvwmscs.js +0 -3
  360. package/dist/cli/main-tkgarpjj.js +0 -4
  361. package/dist/cli/main-v8y60bb2.js +0 -3
  362. package/dist/cli/main-vhrrq337.js +0 -3
  363. package/dist/cli/main-vp2dfb7s.js +0 -4
  364. package/dist/cli/main-vqbr22sz.js +0 -8
  365. package/dist/cli/main-vxnwe5xx.js +0 -18
  366. package/dist/cli/main-wgph00xf.js +0 -5
  367. package/dist/cli/main-wk2csfnj.js +0 -5
  368. package/dist/cli/main-wm997zjx.js +0 -3
  369. package/dist/cli/main-wpkyraxh.js +0 -3
  370. package/dist/cli/main-wqt32p5x.js +0 -4
  371. package/dist/cli/main-x9ct6y1a.js +0 -3
  372. package/dist/cli/main-xfekqh9m.js +0 -7
  373. package/dist/cli/main-xt9zc3n6.js +0 -7
  374. package/dist/cli/main-xx2z3zh5.js +0 -4
  375. package/dist/cli/main-y5c82rxr.js +0 -3
  376. package/dist/cli/main-yrjt2sqt.js +0 -14
  377. package/dist/cli/main-ys6zj3yr.js +0 -3
  378. package/dist/cli/main-ywbxshqc.js +0 -8
  379. package/dist/cli/main-z13755t8.js +0 -25
  380. package/dist/cli/main-zc7pyrbj.js +0 -4
  381. package/dist/cli/main.js +0 -279
  382. package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
  383. package/dist/cli/mcp-login-bthtfpt7.js +0 -2
  384. package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
  385. package/dist/cli/notify-54v5z9dz.js +0 -2
  386. package/dist/cli/oauth-g5gme95c.js +0 -2
  387. package/dist/cli/output-satndjap.js +0 -16
  388. package/dist/cli/profiles-sfhpbq3m.js +0 -2
  389. package/dist/cli/provider-config-hv3xtdt4.js +0 -2
  390. package/dist/cli/provider-kwzq6g84.js +0 -2
  391. package/dist/cli/registry-fh0hdnyn.js +0 -2
  392. package/dist/cli/registry-y1y8e94r.js +0 -2
  393. package/dist/cli/repl-t4z03mqq.js +0 -11
  394. package/dist/cli/resume-fqt4chg8.js +0 -2
  395. package/dist/cli/run-flags-rysbag9t.js +0 -2
  396. package/dist/cli/runtime-j19fjbsa.js +0 -2
  397. package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
  398. package/dist/cli/server-r0b6bksk.js +0 -5
  399. package/dist/cli/session-arg-txmn5g4x.js +0 -2
  400. package/dist/cli/session-ed250d9j.js +0 -2
  401. package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
  402. package/dist/cli/settings-y9rzcqx8.js +0 -2
  403. package/dist/cli/setup-jmbr11j0.js +0 -2
  404. package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
  405. package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
  406. package/dist/cli/smoke-1bg937kx.js +0 -8
  407. package/dist/cli/start-chat-p01cdks3.js +0 -12
  408. package/dist/cli/stream-4wmyaypz.js +0 -2
  409. package/dist/cli/task-eg4s093s.js +0 -2
  410. package/dist/cli/tasks-12v9rr9k.js +0 -2
  411. package/dist/cli/thinking-a5ngvqyh.js +0 -2
  412. package/dist/cli/todo-1wxpcecx.js +0 -2
  413. package/dist/cli/tools-2ftsya7w.js +0 -2
  414. package/dist/cli/tools-x1tj4fxm.js +0 -2
  415. package/dist/cli/trust-cmd-hccxehzb.js +0 -2
  416. package/dist/cli/update-check-ygt3vd7m.js +0 -2
  417. package/dist/cli/update-cmd-v23qhr8c.js +0 -2
  418. package/dist/cli/voice-g1gtck92.js +0 -2
  419. package/dist/cli/webfetch-0nnrjgb5.js +0 -2
  420. package/dist/cli/websearch-f0vr2p7d.js +0 -2
  421. package/dist/cli/workspace-9rq1w4ta.js +0 -2
  422. package/dist/lib/index.js +0 -62
  423. package/dist/lib/models-index.json +0 -1
  424. package/dist/lib/plugins.js +0 -6
  425. package/dist/lib/providers.js +0 -17
  426. package/dist/lib/public-api.js +0 -20
  427. package/dist/rovecode.exe +0 -4
  428. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,211 @@
1
+ /** Install-once for MCP servers the market would otherwise launch with `npx -y <package>`.
2
+ *
3
+ * Why this exists, in numbers (Windows 11, node 24, npm 11, the memory and filesystem servers, medians of
4
+ * 3–5 runs through McpManager, measured 2026-09-06): a warm `npx -y` takes 1.8–2.1 s from spawn to the
5
+ * initialize handshake; the same package installed once and launched with `node <its bin>` takes
6
+ * 0.35–0.47 s. The difference is npx itself — `npx --version` alone is 0.6 s, and every warm launch still
7
+ * asks registry.npmjs.org to revalidate the package (0.3–1.3 s, and no network means a timeout path). A
8
+ * cold cache is 7 s and a 47 MB download, repeated whenever upstream publishes, because `-y <package>`
9
+ * means "latest". The one-time install is ~6 s and ~30 MB for two servers sharing one SDK copy.
10
+ *
11
+ * What it is NOT: silent, or the only path. The human is asked, in the plan they approve, and "no" leaves
12
+ * the npx line exactly as it is today. Installing code is a bigger act than writing a config line, and
13
+ * the plan says so in those words. The record in installed.json carries the package name, the version
14
+ * that landed and npm's integrity hash from the lockfile — an install that is recorded is auditable; an
15
+ * `npx -y` that runs whatever "latest" is at every start is not.
16
+ *
17
+ * Where: one shared prefix, `~/.rovecode/mcp/`, with its own package.json so npm never mistakes a parent
18
+ * folder for the project. N servers share one node_modules and one SDK copy. The launch line is `node`
19
+ * plus the ABSOLUTE path of the package's bin — never the .cmd shim (it means cmd.exe and its quoting),
20
+ * and the path survives a space because it is one argv entry, not a shell string. */
21
+
22
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
+ import { join, resolve } from "node:path";
24
+ import type { McpServerConfig } from "./config.ts";
25
+ import type { MarketInstall } from "./market.ts";
26
+
27
+ export type Spawn = (cmd: string[], cwd: string) => Promise<{ code: number; stderr: string }>;
28
+
29
+ /** the package an `npx …` launch line runs, taken apart */
30
+ export interface NpxPackage {
31
+ /** the package name, `@scope/name` or `name` */
32
+ name: string;
33
+ /** what npx was given: name, or `name@version` when the catalog pinned one */
34
+ spec: string;
35
+ /** the pinned version, when the spec carried one */
36
+ version?: string;
37
+ /** the arguments the SERVER receives — everything after the package spec */
38
+ rest: string[];
39
+ }
40
+
41
+ const NPX_FLAGS_NO_VALUE = new Set(["-y", "--yes", "-q", "--quiet", "--no-install", "--prefer-offline", "--prefer-online"]);
42
+ /** `@scope/name`, `name`, optionally `@version` — a registry package and nothing else (no git URL, no tarball, no path) */
43
+ const PACKAGE_SPEC = /^(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)(?:@([^@\s/]+))?$/;
44
+
45
+ /** The package a stdio `npx` launch runs, or undefined when the line is not a plain `npx [flags] <package>
46
+ * [args]` — an `npx -p x cmd`, a git URL or a tarball is left to npx, since we cannot say what we would be
47
+ * installing. Undefined means "no offer", never an error: the npx line still works. */
48
+ export function npxPackage(install: MarketInstall): NpxPackage | undefined {
49
+ if (install.kind !== "stdio" || install.runtime !== "npx" || install.command !== "npx") return undefined;
50
+ let i = 0;
51
+ while (i < install.args.length && install.args[i]!.startsWith("-")) {
52
+ if (!NPX_FLAGS_NO_VALUE.has(install.args[i]!)) return undefined; // a flag with a value (-p, --package, -c): not our shape
53
+ i += 1;
54
+ }
55
+ const spec = install.args[i];
56
+ if (spec === undefined) return undefined;
57
+ const m = PACKAGE_SPEC.exec(spec);
58
+ if (!m) return undefined;
59
+ const out: NpxPackage = { name: m[1]!, spec, rest: install.args.slice(i + 1) };
60
+ if (m[2] !== undefined) out.version = m[2];
61
+ return out;
62
+ }
63
+
64
+ /** the shared prefix every install-once server lives in */
65
+ export function localPrefix(home: string): string { return join(home, "mcp"); }
66
+
67
+ /** what landed on disk, read back after npm finished — the record's source of truth */
68
+ export interface LocalPackage {
69
+ name: string;
70
+ version: string;
71
+ /** ABSOLUTE path of the package's bin script — what `node` runs */
72
+ bin: string;
73
+ /** npm's integrity hash for the tarball, from package-lock.json */
74
+ integrity?: string;
75
+ /** the tarball URL npm resolved, from package-lock.json */
76
+ resolved?: string;
77
+ /** what could not be recorded and why — an empty list is the normal case. Written into installed.json
78
+ * as it is, so a record with a hole says where the hole is instead of leaving a field silently absent. */
79
+ missing: string[];
80
+ }
81
+
82
+ export interface InstallLocalDeps {
83
+ /** how `npm install` runs — tests inject one that writes a fake node_modules and never touches the network */
84
+ spawn?: Spawn;
85
+ }
86
+
87
+ const defaultSpawn: Spawn = async (cmd, cwd) => {
88
+ const p = Bun.spawn(cmd, { cwd, stdout: "ignore", stderr: "pipe", stdin: "ignore" });
89
+ return { code: await p.exited, stderr: await new Response(p.stderr).text() };
90
+ };
91
+
92
+ /** the exact argv `installLocalPackage` runs — exported so the plan can show it verbatim */
93
+ export function npmInstallArgv(pkg: NpxPackage, prefix: string): string[] {
94
+ return ["npm", "install", "--prefix", prefix, "--save", "--no-fund", "--no-audit", "--loglevel=error", pkg.spec];
95
+ }
96
+
97
+ /** Run `npm install` for one package into the shared prefix, then read back what landed. Never throws for
98
+ * an npm failure — the caller shows the error and nothing has been written to mcp.json yet. */
99
+ export async function installLocalPackage(pkg: NpxPackage, prefix: string, deps: InstallLocalDeps = {}): Promise<{ ok: true; pkg: LocalPackage } | { ok: false; error: string }> {
100
+ const spawn = deps.spawn ?? defaultSpawn;
101
+ try {
102
+ mkdirSync(prefix, { recursive: true });
103
+ // a package.json of its own, so npm treats the prefix as the project: without one it can walk up and
104
+ // install into whatever package.json it finds above ~/.rovecode
105
+ const manifest = join(prefix, "package.json");
106
+ if (!existsSync(manifest)) {
107
+ writeFileSync(manifest, JSON.stringify({ name: "rovecode-mcp-servers", private: true, description: "MCP servers installed once by rovecode's market — launched with node, not npx (docs/mcp-market.md)" }, null, 2) + "\n");
108
+ }
109
+ const r = await spawn(npmInstallArgv(pkg, prefix), prefix);
110
+ if (r.code !== 0) return { ok: false, error: `npm install ${pkg.spec} failed (exit ${r.code})${r.stderr.trim() ? `: ${r.stderr.trim().split("\n").slice(-3).join(" · ")}` : ""}` };
111
+ } catch (e) {
112
+ return { ok: false, error: `npm install ${pkg.spec} could not run: ${e instanceof Error ? e.message : String(e)}` };
113
+ }
114
+ return readLocalPackage(pkg.name, prefix);
115
+ }
116
+
117
+ /** What is on disk for one package under the prefix: version and bin from its package.json, integrity from
118
+ * the lockfile. `ok: false` when the package or its bin is not there — then there is nothing to launch. */
119
+ export function readLocalPackage(name: string, prefix: string): { ok: true; pkg: LocalPackage } | { ok: false; error: string } {
120
+ const dir = join(prefix, "node_modules", ...name.split("/"));
121
+ const manifestPath = join(dir, "package.json");
122
+ if (!existsSync(manifestPath)) return { ok: false, error: `npm reported success but ${manifestPath} is not there` };
123
+ let manifest: Record<string, unknown>;
124
+ try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>; }
125
+ catch (e) { return { ok: false, error: `${manifestPath}: ${e instanceof Error ? e.message : String(e)}` }; }
126
+ const version = typeof manifest.version === "string" ? manifest.version : undefined;
127
+ if (version === undefined) return { ok: false, error: `${manifestPath} states no version` };
128
+ const binRel = binOf(name, manifest.bin);
129
+ if (binRel === undefined) return { ok: false, error: `${name} declares no bin — there is nothing for node to run; npx would have failed the same way` };
130
+ const bin = resolve(dir, binRel);
131
+ if (!existsSync(bin)) return { ok: false, error: `${name}'s bin ${bin} is not on disk` };
132
+ const missing: string[] = [];
133
+ const lock = lockEntry(prefix, name, missing);
134
+ const pkg: LocalPackage = { name, version, bin, missing };
135
+ if (lock?.integrity !== undefined) pkg.integrity = lock.integrity;
136
+ if (lock?.resolved !== undefined) pkg.resolved = lock.resolved;
137
+ return { ok: true, pkg };
138
+ }
139
+
140
+ /** the bin a package declares: a string, or a map — the entry named like the package, else the only one */
141
+ function binOf(name: string, bin: unknown): string | undefined {
142
+ if (typeof bin === "string") return bin;
143
+ if (typeof bin !== "object" || bin === null) return undefined;
144
+ const entries = Object.entries(bin as Record<string, unknown>).filter((e): e is [string, string] => typeof e[1] === "string");
145
+ if (entries.length === 0) return undefined;
146
+ const short = name.split("/").pop()!;
147
+ return (entries.find(([k]) => k === short) ?? entries[0]!)[1];
148
+ }
149
+
150
+ /** package-lock.json (lockfileVersion 2/3) → the entry for `node_modules/<name>`. Every way it can fall
151
+ * short is written into `missing` in words, because a silent undefined here becomes a record that looks
152
+ * complete and is not. */
153
+ function lockEntry(prefix: string, name: string, missing: string[]): { integrity?: string; resolved?: string } | undefined {
154
+ const lockPath = join(prefix, "package-lock.json");
155
+ if (!existsSync(lockPath)) { missing.push(`integrity: ${lockPath} was not written by npm`); return undefined; }
156
+ let lock: unknown;
157
+ try { lock = JSON.parse(readFileSync(lockPath, "utf8")); }
158
+ catch { missing.push(`integrity: ${lockPath} is not valid JSON`); return undefined; }
159
+ const packages = typeof lock === "object" && lock !== null ? (lock as { packages?: unknown }).packages : undefined;
160
+ if (typeof packages !== "object" || packages === null) { missing.push(`integrity: ${lockPath} has no "packages" map (lockfileVersion 1?)`); return undefined; }
161
+ const entry = (packages as Record<string, unknown>)[`node_modules/${name}`];
162
+ if (typeof entry !== "object" || entry === null) { missing.push(`integrity: ${lockPath} has no entry for node_modules/${name}`); return undefined; }
163
+ const e = entry as { integrity?: unknown; resolved?: unknown };
164
+ const out: { integrity?: string; resolved?: string } = {};
165
+ if (typeof e.integrity === "string") out.integrity = e.integrity; else missing.push(`integrity: the lockfile entry for ${name} carries no integrity field`);
166
+ if (typeof e.resolved === "string") out.resolved = e.resolved;
167
+ return out;
168
+ }
169
+
170
+ /** the mcp.json launch line for an installed package: `node` + the bin's absolute path + the server's own
171
+ * arguments. One argv entry per item — a path with a space in it is still one argument, because nothing
172
+ * here goes through a shell (mcp/client.ts spawns with shell: false). */
173
+ export function localLaunch(pkg: LocalPackage, rest: string[]): { command: "node"; args: string[] } {
174
+ return { command: "node", args: [pkg.bin, ...rest] };
175
+ }
176
+
177
+ /** the launch line as the plan can show it BEFORE the install: the bin's exact filename is read from the
178
+ * package after npm has put it there, so the preview names the folder and marks the file as pending */
179
+ export function plannedLaunchLabel(pkg: NpxPackage, prefix: string): string {
180
+ return ["node", `${join(prefix, "node_modules", ...pkg.name.split("/"))}${process.platform === "win32" ? "\\" : "/"}<its bin, read after the install>`, ...pkg.rest].join(" ");
181
+ }
182
+
183
+ /** the plan rows that say what installing once means — every word the human should read before the yes */
184
+ export function localPlanLines(pkg: NpxPackage, prefix: string): string[] {
185
+ return [
186
+ ` installs ${npmInstallArgv(pkg, prefix).join(" ")}`,
187
+ ` rovecode runs a package manager for you here. npm downloads ${pkg.spec} and everything it depends`,
188
+ ` on and puts their CODE on this machine, under ${prefix} — typically 20–30 MB and a few seconds, once.`,
189
+ ` In return the server starts in ~0.4 s instead of ~2 s and needs no network to start.`,
190
+ ` Requires npm (it comes with Node.js, as npx does).`,
191
+ ` records package name, version and npm's integrity hash in installed.json — what ran is on record`,
192
+ ` (an npx line runs whatever "latest" is at every start, and records nothing)`,
193
+ ];
194
+ }
195
+
196
+ /** a configured server that still launches through npx — the rows `mcp list` may offer the install-once line for */
197
+ export function launchesViaNpx(server: McpServerConfig): boolean {
198
+ return server.transport === "stdio" && server.command === "npx" && server.enabled !== false;
199
+ }
200
+
201
+ /** the one-line offer `mcp list` prints under the rows when some of them start through npx. Says what
202
+ * changes, what it costs and that nothing happens until the human runs the command — no config is
203
+ * rewritten by a listing. */
204
+ export function npxOfferLine(names: string[]): string | undefined {
205
+ if (names.length === 0) return undefined;
206
+ const n = names.length;
207
+ // `<catalog name>`, not the server name the row shows: `mcp add` takes the name you installed it by (a
208
+ // registry server is `io.github.acme/widgets`, its row is `widgets`), and a renamed one needs its `--as` back
209
+ return `${n} server${n === 1 ? "" : "s"} start${n === 1 ? "s" : ""} through npx, which re-resolves the package at every start (~2 s each): ${names.join(", ")}. `
210
+ + `To start in ~0.4 s, reinstall with \`rovecode mcp add <catalog name> --local --force\` (the name you installed it by; add \`--as <server name>\` if you renamed it; installs the package once, ~25 MB). Nothing changes until you do.`;
211
+ }
@@ -0,0 +1,84 @@
1
+ /** The curated half of the MCP market: servers we have looked at, from publishers we can name, with
2
+ * the exact launch line spelled out. Offline by design — `rovecode mcp search` answers from this list
3
+ * before it asks the registry, and /mcp with an empty query shows only this list. Rules for an entry:
4
+ * the publisher is the org that owns the package or the host; secrets travel by environment variable
5
+ * (or a header for a remote), never as an argument; anything OAuth-only is left out because our http
6
+ * transport carries a header, not a browser flow. Keep it short — this is a shelf, not a mirror. */
7
+
8
+ import type { MarketEntry } from "./market.ts";
9
+
10
+ const npx = (pkg: string, ...rest: string[]): string[] => ["-y", pkg, ...rest];
11
+
12
+ export const CURATED: readonly MarketEntry[] = [
13
+ { key: "filesystem", title: "Filesystem", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
14
+ description: "Read, write, search and move files under the directories you name.",
15
+ repository: "https://github.com/modelcontextprotocol/servers",
16
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-filesystem"), env: [], pending: ["<directory the server may touch>"] }] },
17
+ { key: "memory", title: "Memory", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
18
+ description: "A knowledge graph the model can remember across sessions with (entities, relations, observations).",
19
+ repository: "https://github.com/modelcontextprotocol/servers",
20
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-memory"), env: [], pending: [] }] },
21
+ { key: "sequential-thinking", title: "Sequential Thinking", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
22
+ description: "A scratchpad tool for step-by-step reasoning with revisions and branches.",
23
+ repository: "https://github.com/modelcontextprotocol/servers",
24
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-sequential-thinking"), env: [], pending: [] }] },
25
+ { key: "everything", title: "Everything (test server)", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
26
+ description: "Exercises every MCP feature — prompts, resources, sampling, progress. For testing a client, not for work.",
27
+ repository: "https://github.com/modelcontextprotocol/servers",
28
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-everything"), env: [], pending: [] }] },
29
+ { key: "fetch", title: "Fetch", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
30
+ description: "Fetch a URL and hand the page back as markdown (Python; needs uv).",
31
+ repository: "https://github.com/modelcontextprotocol/servers",
32
+ installs: [{ kind: "stdio", runtime: "uvx", command: "uvx", args: ["mcp-server-fetch"], env: [], pending: [] }] },
33
+ { key: "git", title: "Git", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
34
+ description: "Read and search git repositories: log, diff, show, status (Python; needs uv).",
35
+ repository: "https://github.com/modelcontextprotocol/servers",
36
+ installs: [{ kind: "stdio", runtime: "uvx", command: "uvx", args: ["mcp-server-git"], env: [], pending: [] }] },
37
+ { key: "time", title: "Time", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
38
+ description: "Current time and timezone conversion (Python; needs uv).",
39
+ repository: "https://github.com/modelcontextprotocol/servers",
40
+ installs: [{ kind: "stdio", runtime: "uvx", command: "uvx", args: ["mcp-server-time"], env: [], pending: [] }] },
41
+ { key: "github", title: "GitHub", source: "curated", publisher: "GitHub",
42
+ description: "Issues, pull requests, code search and Actions on GitHub — remote with a personal access token, or the official image via docker.",
43
+ repository: "https://github.com/github/github-mcp-server",
44
+ installs: [
45
+ { kind: "http", url: "https://api.githubcopilot.com/mcp/", headers: [{ name: "Authorization", template: "Bearer {GITHUB_PAT}", required: true, secret: true, description: "a GitHub personal access token" }] },
46
+ { kind: "stdio", runtime: "docker", command: "docker", args: ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
47
+ env: [{ name: "GITHUB_PERSONAL_ACCESS_TOKEN", required: true, secret: true, description: "a GitHub personal access token" }], pending: [] },
48
+ ] },
49
+ { key: "playwright", title: "Playwright", source: "curated", publisher: "Microsoft",
50
+ description: "Drive a real browser through accessibility snapshots: navigate, click, type, screenshot.",
51
+ repository: "https://github.com/microsoft/playwright-mcp",
52
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@playwright/mcp@latest"), env: [], pending: [] }] },
53
+ { key: "context7", title: "Context7", source: "curated", publisher: "Upstash",
54
+ description: "Up-to-date library documentation and code examples for the versions you actually use.",
55
+ repository: "https://github.com/upstash/context7",
56
+ installs: [
57
+ { kind: "http", url: "https://mcp.context7.com/mcp", headers: [{ name: "CONTEXT7_API_KEY", required: false, secret: true, description: "optional API key for higher limits" }] },
58
+ { kind: "stdio", runtime: "npx", command: "npx", args: npx("@upstash/context7-mcp"), env: [{ name: "CONTEXT7_API_KEY", required: false, secret: true, description: "optional API key for higher limits" }], pending: [] },
59
+ ] },
60
+ { key: "brave-search", title: "Brave Search", source: "curated", publisher: "Brave",
61
+ description: "Web, news, image and local search through the Brave Search API.",
62
+ repository: "https://github.com/brave/brave-search-mcp-server",
63
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@brave/brave-search-mcp-server"), env: [{ name: "BRAVE_API_KEY", required: true, secret: true, description: "Brave Search API key" }], pending: [] }] },
64
+ { key: "firecrawl", title: "Firecrawl", source: "curated", publisher: "Firecrawl (Mendable)",
65
+ description: "Scrape, crawl and extract structured data from websites.",
66
+ repository: "https://github.com/mendableai/firecrawl-mcp-server",
67
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("firecrawl-mcp"), env: [{ name: "FIRECRAWL_API_KEY", required: true, secret: true, description: "Firecrawl API key" }], pending: [] }] },
68
+ { key: "tavily", title: "Tavily", source: "curated", publisher: "Tavily",
69
+ description: "Search and extract built for agents: web search, page extraction, site crawl.",
70
+ repository: "https://github.com/tavily-ai/tavily-mcp",
71
+ installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("tavily-mcp"), env: [{ name: "TAVILY_API_KEY", required: true, secret: true, description: "Tavily API key" }], pending: [] }] },
72
+ { key: "exa", title: "Exa", source: "curated", publisher: "Exa",
73
+ description: "Neural web search and page contents for AI; the remote works without a key at a low rate.",
74
+ repository: "https://github.com/exa-labs/exa-mcp-server",
75
+ installs: [{ kind: "http", url: "https://mcp.exa.ai/mcp", headers: [] }] },
76
+ { key: "deepwiki", title: "DeepWiki", source: "curated", publisher: "Cognition (Devin)",
77
+ description: "Ask questions about any public GitHub repository, answered from its generated wiki. No key.",
78
+ homepage: "https://docs.devin.ai/work-with-devin/deepwiki-mcp",
79
+ installs: [{ kind: "http", url: "https://mcp.deepwiki.com/mcp", headers: [] }] },
80
+ { key: "cloudflare-docs", title: "Cloudflare Docs", source: "curated", publisher: "Cloudflare",
81
+ description: "Search Cloudflare's developer documentation. No key.",
82
+ repository: "https://github.com/cloudflare/mcp-server-cloudflare",
83
+ installs: [{ kind: "http", url: "https://docs.mcp.cloudflare.com/mcp", headers: [] }] },
84
+ ];
@@ -0,0 +1,289 @@
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
+ }
Binary file