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,431 @@
1
+ /** MCP client with LAZY tool disclosure (port #3).
2
+ *
3
+ * Design goal: idle token cost ~0. pi rejected MCP because tool schemas bloat
4
+ * every prompt (a Playwright server costs ~13.7k tokens idle). Rovecode's answer:
5
+ * exactly TWO house tools are ever advertised — mcp_list and mcp_call (tools.ts).
6
+ * Real server schemas stay here, fetched on demand and cached with a TTL.
7
+ *
8
+ * Config loading lives in config.ts; re-exported here as the public surface. */
9
+
10
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
11
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
12
+ import { isRecord, message, type McpServerConfig } from "./config.ts";
13
+
14
+ export { loadMcpConfig, type McpServerConfig } from "./config.ts";
15
+
16
+ // ---------- the SDK, loaded when the first server connects ----------
17
+
18
+ /** The MCP SDK is ~200 ms of module evaluation (zod schemas for every protocol message). Requiring this
19
+ * file used to pay that inside createRuntime, before the terminal had painted anything — two servers in
20
+ * ~/.rovecode/mcp.json made the first frame ~250 ms late. Now this module imports only types, so the
21
+ * manager exists (server names, tool registration) for free and the SDK loads on the async connect path. */
22
+ interface Sdk {
23
+ Client: typeof import("@modelcontextprotocol/sdk/client/index.js").Client;
24
+ StdioClientTransport: typeof import("@modelcontextprotocol/sdk/client/stdio.js").StdioClientTransport;
25
+ getDefaultEnvironment: typeof import("@modelcontextprotocol/sdk/client/stdio.js").getDefaultEnvironment;
26
+ StreamableHTTPClientTransport: typeof import("@modelcontextprotocol/sdk/client/streamableHttp.js").StreamableHTTPClientTransport;
27
+ }
28
+ let sdkPromise: Promise<Sdk> | null = null;
29
+ function sdk(): Promise<Sdk> {
30
+ sdkPromise ??= Promise.all([
31
+ import("@modelcontextprotocol/sdk/client/index.js"),
32
+ import("@modelcontextprotocol/sdk/client/stdio.js"),
33
+ import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
34
+ ]).then(([index, stdio, http]) => ({
35
+ Client: index.Client,
36
+ StdioClientTransport: stdio.StdioClientTransport,
37
+ getDefaultEnvironment: stdio.getDefaultEnvironment,
38
+ StreamableHTTPClientTransport: http.StreamableHTTPClientTransport,
39
+ }));
40
+ return sdkPromise;
41
+ }
42
+
43
+ /** one turn of the event loop — a timer, not a microtask, so painters and input get to run in between */
44
+ const yieldToLoop = (): Promise<void> => new Promise((r) => setTimeout(r, 0));
45
+
46
+ /** One call's signal, linked to the caller's and thrown away with the call.
47
+ *
48
+ * The SDK does `options.signal.addEventListener("abort", …)` per request (shared/protocol.js:709) and
49
+ * never removes it. Every surface passes the RUN's signal, which lives for the whole run, so the
50
+ * listeners pile up on one emitter: at eleven, Node fires MaxListenersExceededWarning and prints the
51
+ * emitter it is complaining about — a whole Writable, pages of it — on stderr, straight over the TUI's
52
+ * alternate screen. Observed on a Playwright MCP session, twelve calls in.
53
+ *
54
+ * A per-call controller ends that: the SDK's listener belongs to a signal nobody keeps, and the link
55
+ * back to the run's signal is removed in the caller's finally. Aborting still works in both directions
56
+ * — the run aborts the call; the call finishing does not touch the run. */
57
+ function perCallSignal(outer?: AbortSignal): { signal: AbortSignal; dispose: () => void } {
58
+ const ac = new AbortController();
59
+ if (outer === undefined) return { signal: ac.signal, dispose: () => {} };
60
+ if (outer.aborted) { ac.abort(outer.reason); return { signal: ac.signal, dispose: () => {} }; }
61
+ const forward = () => ac.abort(outer.reason);
62
+ outer.addEventListener("abort", forward, { once: true });
63
+ return { signal: ac.signal, dispose: () => outer.removeEventListener("abort", forward) };
64
+ }
65
+
66
+ /** Commands that fetch the server’s package before running it, so a first launch is a download. */
67
+ const PACKAGE_RUNNERS = new Set(["npx", "npx.cmd", "uvx", "uvx.exe", "pipx", "pipx.exe", "bunx", "bunx.exe"]);
68
+ /** the budget a runner-launched server gets: enough for a cold fetch on a slow line */
69
+ export const RUNNER_CONNECT_MS = 90_000;
70
+
71
+ /** is this server launched through a package runner rather than an installed binary? */
72
+ export function isPackageRunner(config: Pick<McpServerConfig, "transport" | "command">): boolean {
73
+ if (config.transport !== "stdio" || config.command === undefined) return false;
74
+ const base = config.command.split(/[\\/]/).pop() ?? config.command;
75
+ return PACKAGE_RUNNERS.has(base.toLowerCase());
76
+ }
77
+
78
+ /** What a timeout MEANS, said in terms the reader can act on. "Request timed out" is true and useless. */
79
+ export function runnerTimeoutMessage(config: Pick<McpServerConfig, "name" | "transport" | "command">, budgetMs: number): string {
80
+ const head = `connect to MCP server "${config.name}" timed out after ${Math.round(budgetMs / 1000)}s`;
81
+ if (!isPackageRunner(config)) return head;
82
+ const runner = (config.command ?? "the runner").split(/[\\/]/).pop();
83
+ return `${head} — ${runner} downloads the server's package on first use. Try again (the download is cached), `
84
+ + `or install it once so it starts without the network: rovecode mcp add ${config.name} --local --force`;
85
+ }
86
+
87
+ // ---------- manager ----------
88
+
89
+ /** Pagination guard: no sane server needs 50 tool-list pages; beyond this we
90
+ * assume a misbehaving server and stop instead of looping forever. */
91
+ const MAX_LIST_PAGES = 50;
92
+ /** Tool-result cap fed back into the conversation, mirroring the bash tool's
93
+ * 10k output cap (coding/hashline.ts runOnce). */
94
+ const OUTPUT_MAX = 10_000;
95
+
96
+ interface CachedTool {
97
+ name: string;
98
+ description: string;
99
+ inputSchema: Record<string, unknown>;
100
+ }
101
+
102
+ interface ServerEntry {
103
+ config: McpServerConfig;
104
+ client: Client | null;
105
+ lastError?: string;
106
+ }
107
+
108
+ export interface McpManagerOptions {
109
+ /** tool index cache lifetime (default 60s) */
110
+ toolTtlMs?: number;
111
+ /** per-server connect budget (default 10s) */
112
+ connectTimeoutMs?: number;
113
+ /** per-call budget; progress notifications reset it (default 120s) */
114
+ callTimeoutMs?: number;
115
+ /** seam for tests/extensions: return a Transport for a config, or undefined for the default */
116
+ transportFactory?: (config: McpServerConfig) => Transport | undefined | Promise<Transport | undefined>;
117
+ }
118
+
119
+ export class McpManager {
120
+ private readonly servers = new Map<string, ServerEntry>();
121
+ private readonly toolCache = new Map<string, { at: number; tools: CachedTool[] }>();
122
+ private readonly ttl: number;
123
+ private readonly connectTimeout: number;
124
+ private readonly callTimeout: number;
125
+ private readonly transportFactory: McpManagerOptions["transportFactory"];
126
+
127
+ constructor(configs: McpServerConfig[], options: McpManagerOptions = {}) {
128
+ this.ttl = options.toolTtlMs ?? 60_000;
129
+ this.connectTimeout = options.connectTimeoutMs ?? 10_000;
130
+ this.callTimeout = options.callTimeoutMs ?? 120_000;
131
+ this.transportFactory = options.transportFactory;
132
+ for (const config of configs) {
133
+ if (!this.servers.has(config.name)) this.servers.set(config.name, { config, client: null });
134
+ }
135
+ }
136
+
137
+ serverNames(): string[] {
138
+ return [...this.servers.keys()];
139
+ }
140
+
141
+ connectedNames(): string[] {
142
+ return [...this.servers.values()].filter((e) => e.client !== null).map((e) => e.config.name);
143
+ }
144
+
145
+ /** Connect every enabled server. Lazy-friendly: failures never throw — they
146
+ * come back in `failed` and the server simply stays unavailable.
147
+ *
148
+ * The servers wait in parallel (an `npx` server spends ~2 s resolving its package and booting node;
149
+ * two of them must not take 4 s), but they are STARTED one per event-loop turn. Spawning a stdio
150
+ * server is the one synchronous cost here — cross-spawn's PATH walk plus a cmd.exe wrapper on
151
+ * Windows, ~40 ms each — and back to back those stalls added up into one long freeze of a renderer
152
+ * that repaints every 40 ms. Staggered, no single frame loses more than one spawn. */
153
+ async connect(): Promise<{ connected: string[]; failed: { name: string; error: string }[] }> {
154
+ const failed: { name: string; error: string }[] = [];
155
+ const pending = [...this.servers.values()].filter((e) => e.config.enabled !== false && e.client === null);
156
+ const attempts: Promise<void>[] = [];
157
+ for (const entry of pending) {
158
+ if (attempts.length > 0) await yieldToLoop();
159
+ attempts.push((async () => {
160
+ try {
161
+ entry.client = await this.open(entry.config);
162
+ delete entry.lastError;
163
+ } catch (err) {
164
+ entry.lastError = message(err);
165
+ failed.push({ name: entry.config.name, error: entry.lastError });
166
+ }
167
+ })());
168
+ }
169
+ await Promise.all(attempts);
170
+ return { connected: this.connectedNames(), failed };
171
+ }
172
+
173
+ private async open(config: McpServerConfig): Promise<Client> {
174
+ const transport = await this.buildTransport(config);
175
+ const client = new (await sdk()).Client({ name: "rovecode", version: "0.1.0" });
176
+ // A package runner downloads before it runs, and a download is not a hang. Measured on this machine:
177
+ // a first-ever `uvx mcp-server-time` took over 10 s and lost to the default budget — the user saw
178
+ // "Request timed out" for a server that was working perfectly, and would have seen it again on the
179
+ // next start because nothing had finished caching. `npx -y` is the same shape (7 s cold, 47 MB).
180
+ const budget = isPackageRunner(config) ? Math.max(this.connectTimeout, RUNNER_CONNECT_MS) : this.connectTimeout;
181
+ try {
182
+ await withTimeout(
183
+ client.connect(transport, { timeout: budget }),
184
+ budget + 2_000,
185
+ runnerTimeoutMessage(config, budget),
186
+ );
187
+ } catch (err) {
188
+ await client.close().catch(() => {});
189
+ throw err;
190
+ }
191
+ return client;
192
+ }
193
+
194
+ private async buildTransport(config: McpServerConfig): Promise<Transport> {
195
+ if (this.transportFactory) {
196
+ const custom = await this.transportFactory(config);
197
+ if (custom) return custom;
198
+ }
199
+ const { StreamableHTTPClientTransport, StdioClientTransport, getDefaultEnvironment } = await sdk();
200
+ if (config.transport === "http") {
201
+ if (config.url === undefined) throw new Error(`http server "${config.name}" has no url`);
202
+ // headers (auth tokens etc.) ride on every request via fetch's RequestInit
203
+ const opts = config.headers ? { requestInit: { headers: config.headers } } : undefined;
204
+ return new StreamableHTTPClientTransport(new URL(config.url), opts);
205
+ }
206
+ if (config.command === undefined) throw new Error(`stdio server "${config.name}" has no command`);
207
+ return new StdioClientTransport({
208
+ command: config.command,
209
+ args: config.args ?? [],
210
+ env: { ...getDefaultEnvironment(), ...(config.env ?? {}) },
211
+ stderr: "ignore",
212
+ });
213
+ }
214
+
215
+ /** Fetch (and cache) a server's tools, following list pagination. Bounded: a
216
+ * misbehaving server (repeated cursor, endless pages) throws instead of
217
+ * hanging the agent; the abort signal cancels between and inside pages. */
218
+ private async fetchTools(server: string, force = false, signal?: AbortSignal): Promise<CachedTool[]> {
219
+ const entry = this.servers.get(server);
220
+ if (!entry) throw new Error(`unknown MCP server "${server}"`);
221
+ const client = entry.client;
222
+ if (!client) throw new Error(`MCP server "${server}" is not connected${entry.lastError ? ` (${entry.lastError})` : ""}`);
223
+ const cached = this.toolCache.get(server);
224
+ const now = Date.now();
225
+ if (!force && cached && now - cached.at < this.ttl) return cached.tools;
226
+
227
+ const tools: CachedTool[] = [];
228
+ const seenCursors = new Set<string>();
229
+ let cursor: string | undefined;
230
+ let pages = 0;
231
+ do {
232
+ if (signal?.aborted) throw new Error(`tool listing on "${server}" aborted`);
233
+ const per = perCallSignal(signal); // same leak: listTools is a request like any other
234
+ let res;
235
+ try {
236
+ res = await client.listTools(cursor === undefined ? undefined : { cursor }, { timeout: this.connectTimeout, signal: per.signal });
237
+ } finally { per.dispose(); }
238
+ pages += 1;
239
+ for (const t of res.tools) {
240
+ tools.push({
241
+ name: t.name,
242
+ description: typeof t.description === "string" ? t.description : "",
243
+ inputSchema: t.inputSchema,
244
+ });
245
+ }
246
+ cursor = typeof res.nextCursor === "string" ? res.nextCursor : undefined;
247
+ if (cursor !== undefined) {
248
+ if (seenCursors.has(cursor)) {
249
+ throw new Error(`tool listing on "${server}" stopped: server repeated pagination cursor ${JSON.stringify(cursor)} (partial: ${tools.length} tools in ${pages} pages)`);
250
+ }
251
+ seenCursors.add(cursor);
252
+ if (pages >= MAX_LIST_PAGES) {
253
+ throw new Error(`tool listing on "${server}" stopped after ${MAX_LIST_PAGES} pages without a final page (partial: ${tools.length} tools)`);
254
+ }
255
+ }
256
+ } while (cursor !== undefined);
257
+ this.toolCache.set(server, { at: now, tools });
258
+ return tools;
259
+ }
260
+
261
+ /** Compact index across all connected servers. Cached per server with a TTL;
262
+ * pass refresh=true to bypass the cache. One broken server never hides the rest. */
263
+ async listTools(refresh = false, signal?: AbortSignal): Promise<{ server: string; name: string; description: string }[]> {
264
+ const out: { server: string; name: string; description: string }[] = [];
265
+ for (const [name, entry] of this.servers) {
266
+ if (entry.client === null) continue;
267
+ try {
268
+ for (const t of await this.fetchTools(name, refresh, signal)) {
269
+ out.push({ server: name, name: t.name, description: t.description });
270
+ }
271
+ } catch {
272
+ /* isolate per-server list failures */
273
+ }
274
+ }
275
+ return out;
276
+ }
277
+
278
+ /** Full JSON input schema for one tool, on demand (the lazy-disclosure payoff). */
279
+ async toolSchema(server: string, tool: string, signal?: AbortSignal): Promise<object | undefined> {
280
+ try {
281
+ let found = (await this.fetchTools(server, false, signal)).find((t) => t.name === tool);
282
+ if (!found) found = (await this.fetchTools(server, true, signal)).find((t) => t.name === tool);
283
+ return found?.inputSchema;
284
+ } catch {
285
+ return undefined;
286
+ }
287
+ }
288
+
289
+ /** Execute a tool. Never throws: unknown server/tool, transport errors and
290
+ * aborts all come back as { ok: false }. `onProgress` surfaces server progress
291
+ * notifications (and makes the call-timeout reset on each one). */
292
+ async callTool(server: string, tool: string, args: unknown, signal?: AbortSignal, onProgress?: (note: string) => void): Promise<{ ok: boolean; output: string }> {
293
+ const entry = this.servers.get(server);
294
+ if (!entry) {
295
+ const known = this.serverNames();
296
+ return { ok: false, output: `unknown MCP server "${server}". Known servers: ${known.length > 0 ? known.join(", ") : "(none configured)"}` };
297
+ }
298
+ const client = entry.client;
299
+ if (!client) return { ok: false, output: `MCP server "${server}" is not connected${entry.lastError ? `: ${entry.lastError}` : ""}` };
300
+ if (args !== undefined && args !== null && !isRecord(args)) {
301
+ return { ok: false, output: `args for ${server}/${tool} must be a JSON object (got ${Array.isArray(args) ? "array" : typeof args})` };
302
+ }
303
+
304
+ let known: CachedTool[];
305
+ try {
306
+ known = await this.fetchTools(server, false, signal);
307
+ } catch (err) {
308
+ return { ok: false, output: `failed to list tools on "${server}": ${message(err)}` };
309
+ }
310
+ if (!known.some((t) => t.name === tool)) {
311
+ try {
312
+ known = await this.fetchTools(server, true, signal); // maybe stale cache — refresh once
313
+ } catch {
314
+ /* keep the stale list for the error message */
315
+ }
316
+ if (!known.some((t) => t.name === tool)) {
317
+ const available = known.map((t) => t.name).join(", ");
318
+ return { ok: false, output: `unknown tool "${tool}" on server "${server}". Available: ${available.length > 0 ? available : "(none)"}` };
319
+ }
320
+ }
321
+
322
+ const per = perCallSignal(signal);
323
+ try {
324
+ // an onprogress handler makes the SDK request a progress token, which is what
325
+ // arms resetTimeoutOnProgress — without it that option is a no-op.
326
+ const result = await client.callTool(
327
+ { name: tool, arguments: (args ?? undefined) as Record<string, unknown> | undefined },
328
+ undefined,
329
+ {
330
+ signal: per.signal,
331
+ timeout: this.callTimeout,
332
+ resetTimeoutOnProgress: true,
333
+ onprogress: (p) => onProgress?.(
334
+ typeof p.message === "string" && p.message.length > 0
335
+ ? p.message
336
+ : `progress ${p.progress}${typeof p.total === "number" ? `/${p.total}` : ""}`,
337
+ ),
338
+ },
339
+ );
340
+ const text = renderContent(result.content, result.structuredContent);
341
+ if (result.isError === true) return { ok: false, output: text.length > 0 ? text : `tool "${tool}" reported an error` };
342
+ return { ok: true, output: text };
343
+ } catch (err) {
344
+ return { ok: false, output: `mcp call ${server}/${tool} failed: ${message(err)}` };
345
+ } finally {
346
+ per.dispose();
347
+ }
348
+ }
349
+
350
+ /** Bring the manager's server list in line with `configs`, without touching what is already running.
351
+ *
352
+ * This is what lets `market install mcp:<id>` be usable in the session that installed it, instead of
353
+ * ending in "restart rovecode". A server the manager already knows is left exactly as it is —
354
+ * connected, cached tools and all — because re-adding it would drop a working connection to change
355
+ * nothing. A server that has disappeared from the files is closed and forgotten. New names are added
356
+ * disconnected; `connect()` then picks them up, since it only attempts entries whose client is null.
357
+ *
358
+ * Config changes to an EXISTING name are deliberately not applied here: swapping the command under a
359
+ * live connection is a different operation with its own failure modes, and pretending otherwise would
360
+ * make this quietly unreliable. Removing and re-adding does it, and says what it is doing. */
361
+ async sync(configs: readonly McpServerConfig[]): Promise<{ added: string[]; removed: string[] }> {
362
+ const wanted = new Map(configs.map((c) => [c.name, c]));
363
+ const added: string[] = [];
364
+ const removed: string[] = [];
365
+ for (const [name, entry] of [...this.servers]) {
366
+ if (wanted.has(name)) continue;
367
+ removed.push(name);
368
+ this.servers.delete(name);
369
+ this.toolCache.delete(name);
370
+ if (entry.client) await entry.client.close().catch(() => {});
371
+ }
372
+ for (const [name, config] of wanted) {
373
+ if (this.servers.has(name)) continue;
374
+ this.servers.set(name, { config, client: null });
375
+ added.push(name);
376
+ }
377
+ return { added, removed };
378
+ }
379
+
380
+ /** Close all clients (errors swallowed) and drop caches. Configs are kept, so
381
+ * connect() can be called again. */
382
+ async close(): Promise<void> {
383
+ const closing: Promise<unknown>[] = [];
384
+ for (const entry of this.servers.values()) {
385
+ if (entry.client) {
386
+ closing.push(entry.client.close().catch(() => {}));
387
+ entry.client = null;
388
+ }
389
+ }
390
+ this.toolCache.clear();
391
+ await Promise.all(closing);
392
+ }
393
+ }
394
+
395
+ // ---------- helpers ----------
396
+
397
+ /** Flatten an MCP content array to text; non-text parts become markers. Falls
398
+ * back to structuredContent JSON when there is no text at all. Capped at
399
+ * OUTPUT_MAX chars so a multi-MB result cannot flood the conversation. */
400
+ function renderContent(content: unknown, structured: unknown): string {
401
+ const parts: string[] = [];
402
+ if (Array.isArray(content)) {
403
+ for (const item of content) {
404
+ if (!isRecord(item)) continue;
405
+ if (item.type === "text" && typeof item.text === "string") parts.push(item.text);
406
+ else if (typeof item.type === "string") parts.push(`[${item.type} content]`);
407
+ }
408
+ }
409
+ if (parts.length === 0 && structured !== undefined) {
410
+ try {
411
+ parts.push(JSON.stringify(structured));
412
+ } catch {
413
+ /* unserializable structured content */
414
+ }
415
+ }
416
+ const text = parts.join("\n");
417
+ if (text.length <= OUTPUT_MAX) return text;
418
+ return `${text.slice(0, OUTPUT_MAX)}\n[mcp output truncated: showing ${OUTPUT_MAX} of ${text.length} chars]`;
419
+ }
420
+
421
+ async function withTimeout<T>(p: Promise<T>, ms: number, note: string): Promise<T> {
422
+ let timer: ReturnType<typeof setTimeout> | undefined;
423
+ const gate = new Promise<never>((_, reject) => {
424
+ timer = setTimeout(() => reject(new Error(note)), ms);
425
+ });
426
+ try {
427
+ return await Promise.race([p, gate]);
428
+ } finally {
429
+ if (timer !== undefined) clearTimeout(timer);
430
+ }
431
+ }
@@ -0,0 +1,239 @@
1
+ /** MCP server configuration loading (port #3).
2
+ * Merges `.rovecode/mcp.json` (ours) with `.mcp.json` (harvested, claude-code
3
+ * format `{ mcpServers: { name: {command,args,env,url,type} } }`); ours wins
4
+ * on a name clash. With a `home`, the USER file `<home>/mcp.json` is the lowest
5
+ * layer under both (that is where `rovecode mcp add` writes by default).
6
+ * `${NAME}` in args, env, headers or url is filled from the environment — so a
7
+ * project file can name a secret without holding it; an unset name skips the
8
+ * entry with a warning rather than launching a server with an empty key.
9
+ * Malformed files/entries are skipped with a warning — this loader never throws. */
10
+
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { createHash } from "node:crypto";
13
+ import { join } from "node:path";
14
+
15
+ /** sha256 (hex) of a file's bytes — the trust key for a project file (mcp/trust.ts); undefined when absent */
16
+ export function fileDigest(file: string): string | undefined {
17
+ try { return createHash("sha256").update(readFileSync(file)).digest("hex"); } catch { return undefined; }
18
+ }
19
+
20
+ /** the one line an untrusted project file gets — names the file, the count, the review and the approval command */
21
+ export function untrustedNote(file: string, count: number): string {
22
+ return `${file}: not trusted on this machine — its ${count} MCP server${count === 1 ? "" : "s"} stay off (they would run commands from this repo). Review: rovecode mcp show · approve: rovecode mcp trust`;
23
+ }
24
+
25
+ /** where each scope's file lives; the market's `add`/`remove` write exactly these */
26
+ export function mcpConfigFiles(cwd: string, home?: string): { user?: string; harvest: string; project: string } {
27
+ return { ...(home !== undefined ? { user: join(home, "mcp.json") } : {}), harvest: join(cwd, ".mcp.json"), project: join(cwd, ".rovecode", "mcp.json") };
28
+ }
29
+
30
+ const VAR_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
31
+ /** an unfilled market placeholder: `<directory the server may touch>`. Angle brackets around prose, which
32
+ * is not something a real argument, URL or header value looks like. */
33
+ const PLACEHOLDER_REF = /<[^<>]*[a-z][^<>]*>/g;
34
+ /** `${NAME}` → env value; every missing name lands in `missing` (the caller decides that it is fatal) */
35
+ export function expandVars(text: string, env: Record<string, string | undefined>, missing: string[]): string {
36
+ return text.replace(VAR_REF, (_m, name: string) => { const v = env[name]; if (v === undefined) { missing.push(name); return ""; } return v; });
37
+ }
38
+
39
+ export interface McpServerConfig {
40
+ name: string;
41
+ transport: "stdio" | "http";
42
+ command?: string;
43
+ args?: string[];
44
+ env?: Record<string, string>;
45
+ url?: string;
46
+ /** extra HTTP headers (auth tokens etc.) for http transports */
47
+ headers?: Record<string, string>;
48
+ enabled?: boolean;
49
+ }
50
+
51
+ /** Shared narrow helper (also used by the manager in client.ts). */
52
+ export function isRecord(v: unknown): v is Record<string, unknown> {
53
+ return typeof v === "object" && v !== null && !Array.isArray(v);
54
+ }
55
+
56
+ /** Shared error-to-string helper (also used by the manager in client.ts). */
57
+ export function message(err: unknown): string {
58
+ return err instanceof Error ? err.message : String(err);
59
+ }
60
+
61
+ /** Normalize one server entry (claude-code `.mcp.json` style or ours). Returns
62
+ * undefined (and records a warning) when the entry cannot produce a usable config. */
63
+ export function normalizeEntry(name: string, raw: unknown, file: string, warnings: string[], envSource: Record<string, string | undefined> = process.env, opts: { allowPlaceholders?: boolean } = {}): McpServerConfig | undefined {
64
+ if (!isRecord(raw)) {
65
+ warnings.push(`${file}: server "${name}" is not an object; skipped`);
66
+ return undefined;
67
+ }
68
+ const missing: string[] = [];
69
+ const fill = (s: string): string => expandVars(s, envSource, missing);
70
+ const command = typeof raw.command === "string" && raw.command.length > 0 ? raw.command : undefined;
71
+ const url = typeof raw.url === "string" && raw.url.length > 0 ? fill(raw.url) : undefined;
72
+ const declared = typeof raw.transport === "string" ? raw.transport : typeof raw.type === "string" ? raw.type : undefined;
73
+
74
+ let transport: "stdio" | "http" | undefined;
75
+ if (declared === "stdio") transport = "stdio";
76
+ else if (declared === "http" || declared === "streamable-http" || declared === "streamable_http") transport = "http";
77
+ else if (declared === "sse") {
78
+ warnings.push(`${file}: server "${name}" uses legacy sse transport (unsupported); skipped`);
79
+ return undefined;
80
+ } else if (declared !== undefined) {
81
+ warnings.push(`${file}: server "${name}" has unknown transport "${declared}"; skipped`);
82
+ return undefined;
83
+ } else transport = url !== undefined ? "http" : command !== undefined ? "stdio" : undefined;
84
+
85
+ if (transport === undefined) {
86
+ warnings.push(`${file}: server "${name}" has neither command nor url; skipped`);
87
+ return undefined;
88
+ }
89
+ if (transport === "stdio" && command === undefined) {
90
+ warnings.push(`${file}: stdio server "${name}" is missing command; skipped`);
91
+ return undefined;
92
+ }
93
+ if (transport === "http") {
94
+ if (url === undefined) {
95
+ warnings.push(`${file}: http server "${name}" is missing url; skipped`);
96
+ return undefined;
97
+ }
98
+ try {
99
+ new URL(url);
100
+ } catch {
101
+ warnings.push(`${file}: http server "${name}" has invalid url "${url}"; skipped`);
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ let args: string[] | undefined;
107
+ if (raw.args !== undefined) {
108
+ if (Array.isArray(raw.args) && raw.args.every((x): x is string => typeof x === "string")) {
109
+ args = raw.args.map(fill);
110
+ } else {
111
+ warnings.push(`${file}: server "${name}" has non-string args; skipped`);
112
+ return undefined;
113
+ }
114
+ }
115
+ let env: Record<string, string> | undefined;
116
+ if (isRecord(raw.env)) {
117
+ env = {};
118
+ for (const [k, v] of Object.entries(raw.env)) if (typeof v === "string") env[k] = fill(v);
119
+ }
120
+ let headers: Record<string, string> | undefined;
121
+ if (isRecord(raw.headers)) {
122
+ headers = {};
123
+ for (const [k, v] of Object.entries(raw.headers)) if (typeof v === "string") headers[k] = fill(v);
124
+ }
125
+ if (missing.length > 0) {
126
+ // an empty key would launch a server that fails on its first call; naming the variable is the fix
127
+ warnings.push(`${file}: server "${name}" needs ${[...new Set(missing)].map((m) => `\${${m}}`).join(", ")} set in the environment; skipped`);
128
+ return undefined;
129
+ }
130
+ // The other half of the same rule, for the values a market install could not fill: a required argument
131
+ // that only the human knows (which directory the filesystem server may touch) is written into the file
132
+ // as `<directory the server may touch>`, and a server still carrying one is not launched. Launching it
133
+ // means npx starts, the server rejects its own arguments, and the failure surfaces as a connect error
134
+ // several layers from the thing that is actually wrong — an editable line in a file rovecode named.
135
+ const holes = placeholderHoles({ args, env, headers, url });
136
+ if (holes.length > 0 && opts.allowPlaceholders !== true) {
137
+ warnings.push(`${file}: server "${name}" still has ${holes.join(", ")} to fill in; skipped (edit that line and it will connect)`);
138
+ return undefined;
139
+ }
140
+
141
+ const out: McpServerConfig = { name, transport };
142
+ if (command !== undefined) out.command = command;
143
+ if (args !== undefined) out.args = args;
144
+ if (env !== undefined) out.env = env;
145
+ if (url !== undefined) out.url = url;
146
+ if (headers !== undefined) out.headers = headers;
147
+ if (typeof raw.enabled === "boolean") out.enabled = raw.enabled;
148
+ return out;
149
+ }
150
+
151
+ /** Parse one config file. Accepts the claude-code map form
152
+ * `{ mcpServers: { name: {...} } }` and, additionally (ours), a
153
+ * `{ servers: McpServerConfig[] }` array form. */
154
+ /** The `<…>` placeholders an entry still carries, in file order, each once. Empty = launchable as far as
155
+ * arguments go. Shared by the loader (which refuses to launch while any remain) and the listing surfaces
156
+ * (which must SHOW such an entry, marked, rather than pretend it was never written). */
157
+ export function placeholderHoles(s: { args?: string[]; env?: Record<string, string>; headers?: Record<string, string>; url?: string }): string[] {
158
+ return [...new Set([...(s.args ?? []), ...Object.values(s.env ?? {}), ...Object.values(s.headers ?? {}), ...(s.url !== undefined ? [s.url] : [])]
159
+ .flatMap((v) => [...v.matchAll(PLACEHOLDER_REF)].map((m) => m[0])))];
160
+ }
161
+
162
+ /** `opts.allowPlaceholders`: keep entries that still carry a `<…>` hole (a LISTING wants to show them, marked);
163
+ * the loader leaves it off and such an entry is skipped with a warning naming the hole. */
164
+ export function parseConfigFile(path: string, warnings: string[], envSource: Record<string, string | undefined> = process.env, opts: { allowPlaceholders?: boolean } = {}): McpServerConfig[] {
165
+ if (!existsSync(path)) return [];
166
+ let text: string;
167
+ try {
168
+ text = readFileSync(path, "utf8");
169
+ } catch (err) {
170
+ warnings.push(`${path}: unreadable (${message(err)}); file skipped`);
171
+ return [];
172
+ }
173
+ let json: unknown;
174
+ try {
175
+ json = JSON.parse(text);
176
+ } catch (err) {
177
+ warnings.push(`${path}: invalid JSON (${message(err)}); file skipped`);
178
+ return [];
179
+ }
180
+ if (!isRecord(json)) {
181
+ warnings.push(`${path}: root is not an object; file skipped`);
182
+ return [];
183
+ }
184
+ const out: McpServerConfig[] = [];
185
+ if (json.mcpServers !== undefined) {
186
+ if (isRecord(json.mcpServers)) {
187
+ for (const [name, raw] of Object.entries(json.mcpServers)) {
188
+ const cfg = normalizeEntry(name, raw, path, warnings, envSource, opts);
189
+ if (cfg) out.push(cfg);
190
+ }
191
+ } else warnings.push(`${path}: "mcpServers" is not an object; ignored`);
192
+ }
193
+ if (json.servers !== undefined) {
194
+ if (Array.isArray(json.servers)) {
195
+ for (const raw of json.servers) {
196
+ const name = isRecord(raw) && typeof raw.name === "string" && raw.name.length > 0 ? raw.name : undefined;
197
+ if (name === undefined) {
198
+ warnings.push(`${path}: servers[] entry without a name; skipped`);
199
+ continue;
200
+ }
201
+ const cfg = normalizeEntry(name, raw, path, warnings, envSource, opts);
202
+ if (cfg) out.push(cfg);
203
+ }
204
+ } else warnings.push(`${path}: "servers" is not an array; ignored`);
205
+ }
206
+ return out;
207
+ }
208
+
209
+ export interface LoadMcpOptions {
210
+ /** the user home; without it the user file is not read (existing callers and tests) */
211
+ home?: string;
212
+ /** what `${NAME}` is filled from (default process.env) */
213
+ env?: Record<string, string | undefined>;
214
+ /** the trust gate for PROJECT files (mcp/trust.ts trustedPredicate). Without it project files load as
215
+ * they always did; with it an unapproved file contributes nothing and leaves one warning. The user
216
+ * file is never gated — it is yours. */
217
+ trusted?: (file: string, digest: string) => boolean;
218
+ }
219
+
220
+ /** Merge the user file (`<home>/mcp.json`, only when a home is given) under `.mcp.json` (harvest)
221
+ * under `.rovecode/mcp.json` (ours). The most local wins on a name clash. Pass a `warnings` array
222
+ * to collect human-readable skip reasons. */
223
+ export function loadMcpConfig(cwd: string, warnings: string[] = [], opts: LoadMcpOptions = {}): McpServerConfig[] {
224
+ const files = mcpConfigFiles(cwd, opts.home);
225
+ const byName = new Map<string, McpServerConfig>();
226
+ const anySet = new Proxy({}, { get: () => "set" }) as Record<string, string>; // counting entries, not launching them
227
+ for (const [scope, path] of [["user", files.user], ["harvest", files.harvest], ["project", files.project]] as const) {
228
+ if (path === undefined) continue;
229
+ if (scope !== "user" && opts.trusted !== undefined) {
230
+ const digest = fileDigest(path);
231
+ if (digest !== undefined && !opts.trusted(path, digest)) {
232
+ warnings.push(untrustedNote(path, parseConfigFile(path, [], anySet).length));
233
+ continue; // absence, not "loaded but marked"
234
+ }
235
+ }
236
+ for (const c of parseConfigFile(path, warnings, opts.env)) byName.set(c.name, c); // later layers win
237
+ }
238
+ return [...byName.values()];
239
+ }