rovecode 0.4.0-beta.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,658 @@
1
+ /** `rovecode market …` — one command for all three kinds. search · info · install · remove · list ·
2
+ * update · sources, each with `--json` so a script or another surface reads the same data the terminal
3
+ * shows. A pure function over injected cwd/home/streams/prompts: tests drive it without a process, a
4
+ * network or a TTY.
5
+ *
6
+ * The rule install lives by is the MCP market's, unchanged: the human SEES the plan — the exact command,
7
+ * the folder, the file list, the source, the publisher, what it will ask for — then says yes, on a
8
+ * terminal through y/N or in a script through `--yes`. Without a TTY and without --yes nothing is
9
+ * written. Secrets are asked by NAME through the masked prompt: never echoed, never on the command line,
10
+ * never into a project file (there they are written as `${NAME}` and read from the environment).
11
+ *
12
+ * `rovecode mcp …` keeps working and is not deprecated here — it is the MCP-only door into the same
13
+ * code; `market` is the door that also sees skills and plugins. */
14
+
15
+ import { readSecret, rovecodeHome } from "../providers/auth.ts";
16
+ import { itemLine, qualify, type MarketItem, type MarketKind, type MarketRow, type MarketScope } from "../market/types.ts";
17
+ import { allItems, searchMarket, type RegistryDeps } from "../market/registry.ts";
18
+ import { resolveTarget } from "../market/resolve.ts";
19
+ import { installedState, needsNetwork, planInstall, removeItem, runInstall, withInstalled, type PlanOptions, type RunDeps } from "../market/install.ts";
20
+ import { npxPackage, type NpxPackage } from "../mcp/local-package.ts";
21
+ import type { PrereqEnv } from "../market/prereq.ts";
22
+ import { originLine, readManifest, recordFor } from "../market/manifest.ts";
23
+ import { verifyDigest, verifyLine } from "../market/digest.ts";
24
+ import { disposeCloneCache } from "../plugins/install.ts";
25
+ import { reportLines, validateCatalog } from "../market/validate.ts";
26
+ import { readFileSync } from "node:fs";
27
+ import { createInterface } from "node:readline";
28
+
29
+ export interface MarketCliDeps {
30
+ cwd?: string;
31
+ home?: string;
32
+ out?: (line: string) => void;
33
+ err?: (line: string) => void;
34
+ /** source access (offline / fixture fetch / catalog paths) */
35
+ registry?: RegistryDeps;
36
+ /** how a clone runs, and force */
37
+ run?: RunDeps;
38
+ /** one masked line (default readSecret) */
39
+ secret?: (prompt: string) => Promise<string>;
40
+ /** one plain line (default: readline on stdin) */
41
+ plain?: (prompt: string) => Promise<string>;
42
+ /** default process.stdin.isTTY === true; a pipe is never consumed by a prompt */
43
+ tty?: boolean;
44
+ /** PATH lookup for the plan's prerequisite row — tests inject a fixed environment */
45
+ prereqEnv?: PrereqEnv;
46
+ /** the model to scale the plan's token estimate for; tests inject, production reads the configured one */
47
+ model?: { provider: string; model: string };
48
+ }
49
+
50
+ export const MARKET_USAGE = [
51
+ "usage: rovecode market <command>",
52
+ " search [query] [--kind mcp|skill|plugin] every source at once: the curated MCP shelf, the MCP registry,",
53
+ " rovecode's skill and plugin catalogs (skills/plugins work offline)",
54
+ " info <id> one item in full: publisher, version, what it installs, what it asks",
55
+ " docs <id> the item's own documentation, as the catalog carries it",
56
+ " install <id|kind:id|git-url|npm-package> [--project] [--as <name>] [--pick N] [--ref <branch|tag|commit>] [--yes] [--force]",
57
+ " shows the plan, asks (masked) for keys by name, then writes",
58
+ " --local / --no-local: an npx server installed ONCE (npm, ~25 MB, starts in 0.4 s not 2 s)",
59
+ " or the npx line as it is; without either, a terminal asks and --yes keeps npx",
60
+ " --dry-run shows the plan and stops; nothing is fetched or written",
61
+ " remove <id|kind:id> [--project] undo an install of any kind",
62
+ " list [--all] [--kind mcp|skill|plugin] what is installed here (--all: the whole market, with badges)",
63
+ " update [id] [--all] [--yes] what is out of date; with an id or --all: plan, approve, reinstall",
64
+ " --all --yes skips plugins (new code): name one, or pass --yes-plugins",
65
+ " sources [probe] where rows come from right now; really asks the registry (--offline to skip)",
66
+ " verify [id] re-hash what is installed and say what has changed since",
67
+ " validate <path|url> [--kind skill|plugin] check a catalog you wrote before anyone trusts it: what would",
68
+ " load, what would be dropped, and which fields will not survive",
69
+ "every command takes --json · --offline skips the network entirely",
70
+ "an id is a bare slug inside its kind (filesystem); say mcp:filesystem when two kinds share a name",
71
+ ];
72
+
73
+ /** Flags every subcommand reads. */
74
+ const COMMON_FLAGS: ReadonlySet<string> = new Set(["--json", "--offline"]);
75
+ /** Flags that take a value — the token after them is never a positional. */
76
+ const VALUE_FLAGS: ReadonlySet<string> = new Set(["--as", "--pick", "--kind", "--ref"]);
77
+ /** What each subcommand reads, beyond COMMON_FLAGS. A flag not listed for the subcommand it was given to is
78
+ * a usage error (exit 2), whether or not another subcommand knows it — `list --kind mcp` used to be
79
+ * accepted and ignored. Pinned by test/unit/market-cmd-flags.test.ts, one case per subcommand. */
80
+ export const SUBCOMMAND_FLAGS: Readonly<Record<string, ReadonlySet<string>>> = {
81
+ search: new Set(["--kind"]),
82
+ info: new Set(),
83
+ docs: new Set(),
84
+ install: new Set(["--project", "--as", "--pick", "--ref", "--yes", "--force", "--dry-run", "--local", "--no-local"]),
85
+ remove: new Set(["--project", "--yes"]),
86
+ list: new Set(["--all", "--kind"]),
87
+ update: new Set(["--all", "--yes", "--yes-plugins", "--dry-run"]),
88
+ sources: new Set(),
89
+ verify: new Set(),
90
+ validate: new Set(["--kind"]),
91
+ help: new Set(),
92
+ };
93
+ /** every flag some subcommand takes — only to word the error: "does not take" vs "unknown flag" */
94
+ const KNOWN_FLAGS: ReadonlySet<string> = new Set([...COMMON_FLAGS, ...Object.values(SUBCOMMAND_FLAGS).flatMap((s) => [...s])]);
95
+
96
+ /** C0/C1 control characters, minus the three that are legitimately part of a text file (tab, newline,
97
+ * carriage return). A catalog body is UNTRUSTED text from a third party: printed raw it can clear the
98
+ * screen, retitle the window, or hide itself with ESC[8m. The TUI already strips this (sextant/
99
+ * market-source.ts, and again in screen.ts); stdout had no such pass. */
100
+ const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
101
+ /** what `market docs` is allowed to put on a terminal */
102
+ export const safeForTerminal = (text: string): string => text.replace(/\r\n?/g, "\n").replace(CONTROL, "");
103
+
104
+ /** a size a person reads, from a byte count */
105
+ const kb = (bytes: number): string => (bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`);
106
+
107
+ const jsonOut = (deps: MarketCliDeps, value: unknown): void => (deps.out ?? console.log)(JSON.stringify(value, null, 2));
108
+
109
+ function badge(row: MarketRow): string {
110
+ // the publisher's status rides along with the install state rather than replacing it: "installed" and
111
+ // "archived" are both true at once and a reader needs both — the second is why they might remove it
112
+ const status = row.status ? ` [${row.status}]` : "";
113
+ if (!row.installed) return status;
114
+ if (row.installed.updateAvailable) return `${status} [installed ${row.installed.version ?? "?"} · update ${"available"}]`;
115
+ if (row.installed.trusted === false) return `${status} [installed · NOT approved on this machine]`;
116
+ return `${status} [installed]`;
117
+ }
118
+
119
+ /** everything about one item, in the order a person asks it */
120
+ export function infoLines(item: MarketItem, cwd: string, home: string): string[] {
121
+ const lines = [
122
+ `${item.title}${item.version ? ` ${item.version}` : ""}${item.status ? ` [${item.status}]` : ""}`,
123
+ ` id ${qualify(item)}`,
124
+ ` kind ${item.kind === "mcp" ? "MCP server — rovecode launches it and the model gets its tools" : item.kind === "skill" ? "skill — instructions the model reads; files only, nothing runs" : "plugin — a folder of code rovecode loads and runs"}`,
125
+ ` publisher ${item.publisher}`,
126
+ ` source ${item.source === "curated" ? "curated list (built into rovecode)" : item.source === "registry" ? "MCP registry (registry.modelcontextprotocol.io)" : "rovecode catalog (in the repository)"}`,
127
+ ` ${item.description}`,
128
+ ];
129
+ if (item.license) lines.push(` licence ${item.license}`);
130
+ lines.push(item.docs
131
+ ? ` docs ${kb(item.docs.bytes)} from ${item.docs.source}${item.docs.truncated ? " (truncated)" : ""} — rovecode market docs ${qualify(item)}`
132
+ : ` docs none${item.repository ? ` — try ${item.repository}` : ""}`);
133
+ if (item.repository) lines.push(` repo ${item.repository}`);
134
+ if (item.homepage) lines.push(` home ${item.homepage}`);
135
+ if (item.tags.length) lines.push(` tags ${item.tags.join(", ")}`);
136
+ const { install } = item;
137
+ if (install.kind === "mcp") {
138
+ for (const i of install.entry.installs) lines.push(i.kind === "stdio" ? ` runs ${i.command} ${i.args.join(" ")}` : ` connects ${i.url}`);
139
+ } else if (install.kind === "plugin") {
140
+ lines.push(` installs ${install.git ? `git clone ${install.source}` : `copy of ${install.source}`}${install.subfolder ? ` (subfolder ${install.subfolder})` : ""}`);
141
+ } else {
142
+ lines.push(install.source ? ` installs git clone ${install.source.git}${install.source.subfolder ? ` (${install.source.subfolder})` : ""}` : ` installs ${(install.files ?? []).length} file(s) from the catalog`);
143
+ }
144
+ for (const e of item.env) lines.push(` needs ${e.name}${e.secret ? " (secret, asked masked)" : ""}${e.required ? "" : " (optional)"}${e.description ? ` — ${e.description}` : ""}`);
145
+ const state = installedState(item, cwd, home);
146
+ lines.push(state ? ` installed ${state.path} (${state.scope}${state.version ? `, ${state.version}` : ""}${state.trusted === false ? ", NOT approved here" : ""})` : ` installed no`);
147
+ return lines;
148
+ }
149
+
150
+ /** ask for the plan's variables; null = the human said no or there is no way to ask */
151
+ async function askFor(plan: { asks: { name: string; secret: boolean; description?: string }[]; pending?: string[] }, deps: { secret: (p: string) => Promise<string>; plain: (p: string) => Promise<string>; tty: boolean; err: (l: string) => void }): Promise<Record<string, string>> {
152
+ const answers: Record<string, string> = {};
153
+ for (const a of plan.asks) {
154
+ if (!deps.tty) { deps.err(`${a.name} is not set — it will be written as \${${a.name}} and read from your environment`); continue; }
155
+ const label = `${a.name}${a.description ? ` (${a.description})` : ""}: `;
156
+ const v = (await (a.secret ? deps.secret(label) : deps.plain(label))).trim();
157
+ if (v.length > 0) answers[a.name] = v;
158
+ }
159
+ // a `pending` value is a required argument only the human knows (the directory the filesystem server may
160
+ // touch). It is never secret, and it is keyed by the placeholder text, which is what fillPlan reads.
161
+ for (const p of plan.pending ?? []) {
162
+ if (!deps.tty) continue; // written as the placeholder; the loader names it rather than launching
163
+ const v = (await deps.plain(`${p}: `)).trim();
164
+ if (v.length > 0) answers[p] = v;
165
+ }
166
+ return answers;
167
+ }
168
+
169
+ /** the npx package the chosen install form would run — the thing the install-once offer is about */
170
+ function offeredPackage(item: MarketItem, opts: PlanOptions): NpxPackage | undefined {
171
+ if (item.install.kind !== "mcp") return undefined;
172
+ // a project file is shared with every clone; install-once writes this machine's absolute path — no offer there
173
+ if (opts.scope === "project") return undefined;
174
+ const form = item.install.entry.installs[opts.pick ?? 0];
175
+ return form === undefined ? undefined : npxPackage(form);
176
+ }
177
+
178
+ /** the question, in the human's terms: what it costs, what they get, and that "no" changes nothing */
179
+ function offerLines(pkg: NpxPackage): string[] {
180
+ return [
181
+ `${pkg.spec} would start through npx: ~2 s at every start, re-resolving the package (and asking the npm registry) each time.`,
182
+ `Install it once instead? npm puts the package's code under ~/.rovecode/mcp — typically 20–30 MB and a few seconds, one time;`,
183
+ `it then starts in ~0.4 s and needs no network to start. No keeps the npx line exactly as it is today.`,
184
+ ];
185
+ }
186
+
187
+ /** One item, the whole ceremony: plan → show → ask → write. Shared by `install` and `update`, so an
188
+ * update can never become a quieter install that skips the preview. Returns the process exit code. */
189
+ async function installOne(item: MarketItem, opts: PlanOptions, ctx: {
190
+ out: (l: string) => void; err: (l: string) => void; json: boolean; yes: boolean; tty: boolean;
191
+ secret: (p: string) => Promise<string>; plain: (p: string) => Promise<string>;
192
+ run: RunDeps; verb: string; dryRun?: boolean;
193
+ /** --json with several items (`update --all`): each item's document goes here instead of stdout, and the
194
+ * caller prints ONE document around them. Absent = this call owns stdout (`install`, `update <id>`). */
195
+ collect?: (doc: unknown) => void;
196
+ }): Promise<number> {
197
+ const doc = (d: unknown): void => { if (ctx.collect) ctx.collect(d); else jsonOut({ out: ctx.out }, d); };
198
+ // The install-once offer, BEFORE the plan is drawn, so the plan the human then reads is the one that will
199
+ // run. Asked only where a person can answer (a terminal, no --yes, no --json, no --dry-run) and only when
200
+ // nothing decided it already (--local / --no-local, or an update keeping what the record says). Every
201
+ // other path keeps today's npx line: the offer is never silent and never the only way.
202
+ let local = opts.local;
203
+ const offer = offeredPackage(item, opts);
204
+ if (offer !== undefined && local === undefined && !ctx.yes && !ctx.json && !ctx.dryRun && ctx.tty) {
205
+ for (const l of offerLines(offer)) ctx.out(l);
206
+ const a = (await ctx.plain(`install ${offer.spec} once? [y/N] `)).trim().toLowerCase();
207
+ local = a === "y" || a === "yes";
208
+ }
209
+ const plan = planInstall(item, local === undefined ? opts : { ...opts, local });
210
+ if ("error" in plan) { ctx.err(plan.error); if (ctx.json) doc({ ok: false, error: plan.error, id: qualify(item) }); return 1; }
211
+ // In --json mode the plan travels as FIELDS, not as prose printed above the JSON. It used to be both,
212
+ // which meant `market install --json` emitted human lines and then an object on the same stream and
213
+ // nothing could parse the result — a flag whose whole promise is "a script reads what the terminal
214
+ // shows" has to produce one document. The same lines are still there, inside `preview`.
215
+ if (!ctx.json) {
216
+ for (const l of plan.preview) ctx.out(l);
217
+ if (plan.replaces) ctx.out(` replaces ${plan.replaces}`);
218
+ for (const p of plan.pending) ctx.out(` fill in ${p} — after the install, in ${plan.target}`);
219
+ }
220
+ // --dry-run stops HERE: after the plan is complete and before anything is asked for. It is a success,
221
+ // not a refusal — the question was "what would this do", and it has been answered. It also overrides
222
+ // --yes rather than arguing with it: between "show me" and "go ahead", the one that writes nothing wins.
223
+ //
224
+ // What it does NOT do is fetch. A git-sourced skill is not cloned here, so this says what would be
225
+ // written and where, never what is inside the repository — and the sentence below says so rather than
226
+ // letting the silence imply a stronger check than happened.
227
+ if (ctx.dryRun) {
228
+ if (ctx.json) { doc({ dryRun: true, item, target: plan.target, scope: plan.scope,
229
+ preview: plan.preview, asks: plan.asks, pending: plan.pending, ...(plan.replaces ? { replaces: plan.replaces } : {}) }); return 0; }
230
+ ctx.out(`nothing written — --dry-run. ${needsNetwork(item.install) ? "The source was not fetched, so this is the plan, not its contents." : "This is the whole plan."}`);
231
+ return 0;
232
+ }
233
+ if (!ctx.yes) {
234
+ // --json never prompts, terminal or not. A y/N is a question for a person, and in --json mode the
235
+ // preview that would let a person answer it is inside the document rather than on the screen — so
236
+ // asking would mean asking someone to approve a plan they were not shown. The plan is returned with
237
+ // `needsApproval` and exit 1; rerun with --yes, or --dry-run if reading it was the whole point.
238
+ if (ctx.json) {
239
+ doc({ ok: false, needsApproval: true, item, target: plan.target, scope: plan.scope,
240
+ preview: plan.preview, asks: plan.asks, pending: plan.pending, ...(plan.replaces ? { replaces: plan.replaces } : {}) });
241
+ ctx.err(`nothing written: pass --yes to accept this plan, or --dry-run to read it`);
242
+ return 1;
243
+ }
244
+ if (!ctx.tty) { ctx.err(`nothing written: rerun on a terminal, or pass --yes to accept this plan in a script`); return 1; }
245
+ const answer = (await ctx.plain(`${ctx.verb} this? [y/N] `)).trim().toLowerCase();
246
+ if (answer !== "y" && answer !== "yes") { ctx.out("nothing written"); return 1; }
247
+ }
248
+ const answers = await askFor(plan, {
249
+ secret: ctx.secret, plain: ctx.plain,
250
+ // a project scope never takes a typed secret: it is written as ${NAME}
251
+ tty: ctx.tty && !(opts.scope === "project" && plan.asks.some((a) => a.secret)), err: ctx.err,
252
+ });
253
+ const outcome = await runInstall(plan, answers, local === undefined ? opts : { ...opts, local }, ctx.run);
254
+ if (!outcome.ok) { ctx.err(outcome.error); if (ctx.json) doc(outcome); return 1; }
255
+ if (ctx.json) { doc(outcome); return 0; }
256
+ ctx.out(`${ctx.verb === "update" ? "updated" : "installed"} ${qualify(item)} → ${outcome.target}${outcome.trusted === true ? " (trusted as written)" : ""}`);
257
+ if (outcome.package) {
258
+ ctx.out(` package ${outcome.package.name} ${outcome.package.version} → ${outcome.package.prefix}${outcome.package.integrity !== undefined ? " (integrity recorded in installed.json)" : ""}`);
259
+ // a hole in the record is said, not smoothed over: the reader decides whether it matters
260
+ if (outcome.package.missing) ctx.err(` record incomplete: ${outcome.package.missing.join("; ")}`);
261
+ }
262
+ if (outcome.trusted === false) ctx.err(`that file already held entries you have not approved, so it is NOT trusted yet — rovecode mcp trust`);
263
+ if (outcome.envNames.length) ctx.err(`set ${outcome.envNames.join(", ")} in your environment before the restart`);
264
+ if (outcome.next) ctx.out(outcome.next);
265
+ return 0;
266
+ }
267
+
268
+ export async function cmdMarket(args: string[], deps: MarketCliDeps = {}): Promise<number> {
269
+ // The clone cache lives exactly as long as ONE command: `update --all` out of a monorepo clones it once
270
+ // instead of once per item. The cache OWNS every directory in it (plugins/install.ts), so disposing it
271
+ // here is not tidiness — without this finally the clones outlive the process in the temp directory.
272
+ const cloneCache = new Map<string, string>();
273
+ try { return await runMarket(args, { ...deps, run: { ...deps.run, cloneCache } }); }
274
+ catch (e) { (deps.err ?? ((l: string) => console.error(l)))(`market: ${e instanceof Error ? e.message : String(e)}`); return 1; }
275
+ finally { disposeCloneCache(cloneCache); }
276
+ }
277
+
278
+ async function runMarket(args: string[], deps: MarketCliDeps): Promise<number> {
279
+ const out = deps.out ?? console.log;
280
+ const err = deps.err ?? ((l: string) => console.error(l));
281
+ const cwd = deps.cwd ?? process.cwd();
282
+ const home = deps.home ?? rovecodeHome();
283
+ const json = args.includes("--json");
284
+ const offline = args.includes("--offline");
285
+ const scope: MarketScope = args.includes("--project") ? "project" : "user";
286
+ const registry: RegistryDeps = { ...deps.registry, ...(offline ? { offline: true } : {}) };
287
+ const flag = (name: string): string | undefined => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
288
+ const positional = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && VALUE_FLAGS.has(args[i - 1]!)));
289
+ // A usage error is exit 2 and prose on stderr. In --json mode it is ALSO a document on stdout: "one
290
+ // document, every subcommand, every exit code" (docs/market.md) — a script that passed a flag `list`
291
+ // does not take must be able to read why, not just see an empty stdout and a number.
292
+ const usage = (msg: string): 2 => {
293
+ err(msg); err(MARKET_USAGE.join("\n"));
294
+ if (json) jsonOut(deps, { ok: false, error: msg, usage: MARKET_USAGE });
295
+ return 2;
296
+ };
297
+
298
+ const sub = positional[0];
299
+ if (sub === undefined) return usage("market needs a command");
300
+ const allowed = SUBCOMMAND_FLAGS[sub];
301
+ if (allowed === undefined) return usage(`unknown command "${sub}"`);
302
+ // Per SUBCOMMAND, not against the union of every flag any subcommand takes: that global set accepted
303
+ // `list --kind mcp` and then ignored it, so a caller who asked for MCP servers got a skill row and no
304
+ // error to notice. A flag a subcommand does not read is refused, in the same words for every one.
305
+ for (const a of args) {
306
+ if (!a.startsWith("--")) continue;
307
+ if (COMMON_FLAGS.has(a) || allowed.has(a)) continue;
308
+ return usage(KNOWN_FLAGS.has(a) ? `market ${sub} does not take ${a}` : `unknown flag ${a}`);
309
+ }
310
+ // `help` is the one subcommand whose stdout IS the usage text, --json or not: it is for a person
311
+ if (sub === "help") { out(MARKET_USAGE.join("\n")); return 0; }
312
+ const kindFilter = (): MarketKind | undefined | 2 => {
313
+ const kind = flag("--kind");
314
+ if (kind === undefined) return undefined;
315
+ return ["mcp", "skill", "plugin"].includes(kind) ? (kind as MarketKind) : usage(`--kind takes mcp, skill or plugin`);
316
+ };
317
+
318
+ // ---------------- search
319
+ if (sub === "search") {
320
+ const kind = kindFilter();
321
+ if (kind === 2) return 2;
322
+ const query = positional.slice(1).join(" ");
323
+ const r = await searchMarket(query, registry);
324
+ const items = kind ? r.items.filter((i) => i.kind === kind) : r.items;
325
+ const rows = withInstalled(items, cwd, home);
326
+ // exit code carries the same signal as the text form: a script piping --json must not read "no
327
+ // matches" as success when the human-readable run would have said otherwise
328
+ if (json) { jsonOut(deps, { items: rows, sources: r.sources, notes: r.notes }); return rows.length === 0 ? 1 : 0; }
329
+ for (const n of r.notes) err(`market: ${n}`);
330
+ if (rows.length === 0) {
331
+ const dead = Object.entries(r.sources).filter(([, s]) => !s.ok);
332
+ err(query ? `nothing matches "${query}"` : "the market is empty");
333
+ for (const [name, s] of dead) if (!s.ok) err(` ${name}: ${s.reason}`);
334
+ return 1;
335
+ }
336
+ for (const row of rows) out(`${itemLine(row)}${badge(row)}`);
337
+ return 0;
338
+ }
339
+
340
+ // ---------------- sources
341
+ if (sub === "sources") {
342
+ // `sources` exists to answer "is the registry up?", and it was the one command that never asked: it
343
+ // ran the empty query, which by design never leaves the machine, and then reported the registry as
344
+ // "not consulted". A real probe with a real term is the whole job.
345
+ const probe = positional.slice(1).join(" ") || "mcp";
346
+ const r = offline ? await allItems(registry) : await searchMarket(probe, registry);
347
+ if (json) { jsonOut(deps, { sources: r.sources, notes: r.notes, count: r.items.length, probe: offline ? null : probe, offline }); return Object.values(r.sources).every((x) => x.ok) ? 0 : 1; }
348
+ if (offline) out(`--offline: the registry was not asked`);
349
+ else out(`probed with "${probe}"`);
350
+ for (const [name, s] of Object.entries(r.sources)) {
351
+ const how = !s.ok ? `FAILED — ${s.reason}`
352
+ : s.from === "live" ? "answered just now"
353
+ : s.from === "cache" ? `from the cache${s.ageMs ? ` (${Math.round(s.ageMs / 1000)}s old)` : ""}`
354
+ : s.from === "skipped" ? `not consulted — ${s.why}`
355
+ : "built in / on disk";
356
+ out(`${name.padEnd(14)} ${how}`);
357
+ }
358
+ out(`${String(r.items.length).padStart(14)} items visible right now`);
359
+ return Object.values(r.sources).every((s) => s.ok) ? 0 : 1;
360
+ }
361
+
362
+ // ---------------- verify
363
+ if (sub === "verify") {
364
+ // `--ref` pins what was ASKED for. This is what ARRIVED, checked again now. It detects drift; it does
365
+ // not prove provenance, and nothing here claims otherwise — nobody in this space signs anything yet.
366
+ const which = positional[1];
367
+ const rows: { id: string; result: ReturnType<typeof verifyDigest> }[] = [];
368
+ for (const scope of ["user", "project"] as const) {
369
+ for (const record of readManifest(scope, cwd, home)) {
370
+ const id = `${record.kind}:${record.id}`;
371
+ if (which !== undefined && which !== id && which !== record.id) continue;
372
+ rows.push({ id, result: record.kind === "mcp"
373
+ ? { state: "not-applicable", why: "an MCP entry is a line inside a shared mcp.json, not a folder of its own" }
374
+ : verifyDigest(record.digest, record.target) });
375
+ }
376
+ }
377
+ // the same exit rule as the text path below, including the one that is easy to lose here: naming an id
378
+ // that has no record is a 1. An empty array with a 0 reads as "checked it, all fine", which is the
379
+ // opposite of what happened — nothing was checked, because nothing was found.
380
+ if (json) {
381
+ jsonOut(deps, rows);
382
+ if (rows.length === 0) return which !== undefined ? 1 : 0;
383
+ return rows.some((r) => r.result.state === "changed" || r.result.state === "missing") ? 1 : 0;
384
+ }
385
+ if (rows.length === 0) {
386
+ out(which !== undefined ? `nothing recorded for "${which}"` : "nothing installed through the market yet");
387
+ return which !== undefined ? 1 : 0;
388
+ }
389
+ for (const r of rows) out(verifyLine(r.id, r.result));
390
+ const bad = rows.filter((r) => r.result.state === "changed" || r.result.state === "missing").length;
391
+ if (bad > 0) err(`${bad} item${bad === 1 ? " is" : "s are"} not what was installed — reinstall with \`market install <id> --force\`, or keep the edit`);
392
+ return bad > 0 ? 1 : 0;
393
+ }
394
+
395
+ // ---------------- list
396
+ if (sub === "list") {
397
+ const all = args.includes("--all");
398
+ const kind = kindFilter();
399
+ if (kind === 2) return 2;
400
+ const r = await allItems(registry);
401
+ const rows = withInstalled(r.items, cwd, home).filter((row) => (all || row.installed) && (kind === undefined || row.kind === kind));
402
+ // where each installed row came from: the catalog row, the clone URL, the commit. The disk still says
403
+ // WHETHER it is installed; the manifest says where it came from, and says so honestly when it cannot.
404
+ const withOrigin = rows.map((row) => row.installed
405
+ ? { ...row, origin: recordFor(row, row.installed.scope, cwd, home) ?? null }
406
+ : row);
407
+ if (json) { jsonOut(deps, withOrigin); return 0; }
408
+ if (rows.length === 0) {
409
+ const what = kind === undefined ? "" : `${kind === "mcp" ? "MCP server" : kind} `;
410
+ out(all ? (kind === undefined ? "the market is empty" : `the market has no ${what}items`)
411
+ : `${kind === undefined ? "nothing " : `no ${what}`}installed here yet — \`rovecode market search${kind ? ` --kind ${kind}` : ""}\` to look around`);
412
+ return 0;
413
+ }
414
+ for (const row of rows) {
415
+ out(`${itemLine(row)}${badge(row)}`);
416
+ if (row.installed) out(` from ${originLine(recordFor(row, row.installed.scope, cwd, home))}`);
417
+ }
418
+ return 0;
419
+ }
420
+
421
+ // ---------------- update
422
+ if (sub === "update") {
423
+ const r = await allItems(registry);
424
+ const installed = withInstalled(r.items, cwd, home).filter((row) => row.installed);
425
+ // "stale" is either a version that differs, or a version nobody can compare — a skill without one in
426
+ // its SKILL.md is NOT silently skipped, it is offered as a reinstall and says why
427
+ const stale = installed.filter((row) => row.installed!.updateAvailable === true || row.version === undefined || row.installed!.version === undefined);
428
+ const which = positional[1];
429
+ const all = args.includes("--all");
430
+
431
+ if (which === undefined && !all) { // the dry list
432
+ if (json) { jsonOut(deps, stale); return 0; }
433
+ if (stale.length === 0) { out("everything installed is at the catalog's version"); return 0; }
434
+ for (const row of stale) {
435
+ const from = row.installed!.version, to = row.version;
436
+ out(from !== undefined && to !== undefined
437
+ ? `${qualify(row)} ${from} → ${to}`
438
+ : `${qualify(row)} version unknown (${from === undefined ? "nothing on disk says one" : "the catalog states none"}) — updating reinstalls it`);
439
+ }
440
+ out(`rovecode market update <id> · rovecode market update --all`);
441
+ return 0;
442
+ }
443
+
444
+ // `--all --yes` must not silently re-clone every PLUGIN from whatever its source's HEAD says today and
445
+ // re-record project trust for the result: that is running new code with no question asked. A skill is
446
+ // text and a server entry is config, so those go; plugins need `--yes-plugins` or a named update.
447
+ const skipPlugins = all && args.includes("--yes") && !args.includes("--yes-plugins");
448
+ let targets = stale;
449
+ if (which !== undefined) {
450
+ const pick = await resolveTarget(which, registry);
451
+ if (!pick.ok) { err(pick.error); if (json) jsonOut(deps, { ok: false, error: pick.error, candidates: pick.ambiguous ?? [] }); return pick.ambiguous ? 2 : 1; }
452
+ const row = installed.find((x) => x.id === pick.item.id && x.kind === pick.item.kind);
453
+ if (row === undefined) {
454
+ const msg = `${qualify(pick.item)} is not installed here — rovecode market install ${qualify(pick.item)}`;
455
+ err(msg); if (json) jsonOut(deps, { ok: false, error: msg, id: qualify(pick.item), installed: false });
456
+ return 1;
457
+ }
458
+ targets = [row];
459
+ }
460
+ const skipped = skipPlugins ? targets.filter((r) => r.kind === "plugin") : [];
461
+ if (skipped.length) targets = targets.filter((r) => r.kind !== "plugin");
462
+ // In --json mode the whole update is ONE document, however many items it touched: each item's own
463
+ // outcome (the same object `install --json` prints) is collected under `results`, the plugins that
464
+ // `--all --yes` set aside under `skipped`. It used to print one document per item and, with nothing
465
+ // to do, a sentence — so `update --all --yes --json` was parseable only when exactly one item was stale.
466
+ const results: unknown[] = [];
467
+ const wrap = (ok: boolean): void => { if (json) jsonOut(deps, { ok, results, skipped: skipped.map(qualify) }); };
468
+ if (targets.length === 0 && skipped.length === 0) { if (json) wrap(true); else out("everything installed is at the catalog's version"); return 0; }
469
+
470
+ const ctx = {
471
+ out, err, json, yes: args.includes("--yes"), tty: deps.tty ?? process.stdin.isTTY === true,
472
+ secret: deps.secret ?? readSecret, plain: deps.plain ?? defaultPlain, verb: "update", dryRun: args.includes("--dry-run"),
473
+ run: { ...deps.run, force: true, ...(offline ? { offline: true } : {}) },
474
+ ...(json ? { collect: (doc: unknown) => { results.push(doc); } } : {}),
475
+ };
476
+ let worst = 0;
477
+ if (skipped.length && !json) {
478
+ out(`${skipped.length} plugin${skipped.length > 1 ? "s" : ""} skipped — a plugin update runs new code: ${skipped.map(qualify).join(", ")}`);
479
+ out(` rovecode market update <id> --yes · or --yes-plugins to take them all`);
480
+ }
481
+ for (const row of targets) {
482
+ // an item is updated in the scope it is installed in, not the flag's default. An MCP server that was
483
+ // installed ONCE stays installed once (the record says so); one on an npx line stays on npx — an update
484
+ // never asks the install-once question, because it must not change how the server starts.
485
+ const keepLocal = row.kind === "mcp" ? recordFor(row, row.installed!.scope, cwd, home)?.package !== undefined : undefined;
486
+ const opts: PlanOptions = { scope: row.installed!.scope, cwd, home, ...(deps.prereqEnv !== undefined ? { prereqEnv: deps.prereqEnv } : {}),
487
+ ...(keepLocal !== undefined ? { local: keepLocal } : {}) };
488
+ const code = await installOne(row, opts, ctx);
489
+ if (code !== 0) worst = code;
490
+ }
491
+ wrap(worst === 0);
492
+ return worst;
493
+ }
494
+
495
+ const target = positional[1];
496
+ if (["info", "install", "remove", "docs"].includes(sub) && target === undefined) return usage(`market ${sub} needs a name`);
497
+
498
+ // ---------------- info
499
+ if (sub === "info") {
500
+ // `install` may reasonably treat an unknown dashed word as an npm package — the human is naming a
501
+ // package to install. `info` and `docs` must NOT: a typo would come back as a confident record for a
502
+ // server nobody has ever published ("runs npx -y totally-bogus-name") with exit 0, and there would be
503
+ // no way left to ask "does this exist?".
504
+ const r = await resolveTarget(target!, registry);
505
+ if (r.ok && r.item.source === "catalog" && r.item.publisher.startsWith("unknown (") && r.item.kind === "mcp") {
506
+ err(`"${target}" is not in the catalog or the registry — \`market install\` would treat it as an npm package, but there is nothing here to describe`);
507
+ if (json) jsonOut(deps, { error: "not found", id: target });
508
+ return 1;
509
+ }
510
+ if (!r.ok) { err(r.error); if (json) jsonOut(deps, { error: r.error, candidates: r.ambiguous ?? [] }); return r.ambiguous ? 2 : 1; }
511
+ if (json) { jsonOut(deps, { ...r.item, installed: installedState(r.item, cwd, home) }); return 0; }
512
+ for (const l of infoLines(r.item, cwd, home)) out(l);
513
+ return 0;
514
+ }
515
+
516
+ // ---------------- docs
517
+ if (sub === "docs") {
518
+ const r = await resolveTarget(target!, registry);
519
+ if (r.ok && r.item.source === "catalog" && r.item.publisher.startsWith("unknown (") && r.item.kind === "mcp") {
520
+ err(`"${target}" is not in the catalog or the registry — nothing here has documentation`);
521
+ if (json) jsonOut(deps, { error: "not found", id: target });
522
+ return 1;
523
+ }
524
+ if (!r.ok) { err(r.error); if (json) jsonOut(deps, { error: r.error, candidates: r.ambiguous ?? [] }); return r.ambiguous ? 2 : 1; }
525
+ const d = r.item.docs;
526
+ if (!d || d.body === undefined) {
527
+ // never silently empty: say where the documentation would be if the reader wants to go looking
528
+ const where = r.item.repository ?? r.item.homepage;
529
+ err(`${qualify(r.item)} carries no documentation in the catalog${where ? ` — the publisher's own is at ${where}` : ""}`);
530
+ if (json) jsonOut(deps, { id: qualify(r.item), docs: null, ...(where ? { repository: where } : {}) });
531
+ return 1;
532
+ }
533
+ if (json) { jsonOut(deps, { id: qualify(r.item), docs: d }); return 0; }
534
+ out(safeForTerminal(d.body));
535
+ if (d.truncated) err(`— truncated: ${kb(d.bytes)} upstream, read the rest at ${d.source}`);
536
+ return 0;
537
+ }
538
+
539
+ // ---------------- validate
540
+ if (sub === "validate") {
541
+ if (target === undefined) return usage("usage: rovecode market validate <path|url> [--kind skill|plugin]");
542
+ const kindFlag = flag("--kind");
543
+ if (kindFlag !== undefined && kindFlag !== "skill" && kindFlag !== "plugin") return usage("validate takes --kind skill or --kind plugin");
544
+
545
+ let text: string;
546
+ const isUrl = /^https?:\/\//i.test(target);
547
+ // an unreadable path or a failed fetch is a document too: the report a script asked for says why there is none
548
+ const unreadable = (msg: string): 1 => { err(msg); if (json) jsonOut(deps, { ok: false, error: msg, source: target }); return 1; };
549
+ if (isUrl) {
550
+ // a URL is the network, so --offline means it: the flag says "skips the network entirely"
551
+ if (offline) return usage(`--offline and a URL cannot both be meant — give a local path, or drop --offline`);
552
+ try {
553
+ const res = await fetch(target, { headers: { "user-agent": "rovecode-market-validate" } });
554
+ if (!res.ok) return unreadable(`${target}: HTTP ${res.status}`);
555
+ text = await res.text();
556
+ } catch (e) { return unreadable(`${target}: ${e instanceof Error ? e.message : String(e)}`); }
557
+ } else {
558
+ try { text = readFileSync(target, "utf8"); }
559
+ catch (e) { return unreadable(`${target}: ${e instanceof Error ? e.message : String(e)}`); }
560
+ }
561
+
562
+ const report = validateCatalog(text, {
563
+ ...(kindFlag ? { kind: kindFlag as "skill" | "plugin" } : {}),
564
+ filename: target,
565
+ });
566
+ if (json) { jsonOut(deps, report); return report.ok ? 0 : 1; }
567
+ for (const line of reportLines(report, target)) out(line);
568
+ return report.ok ? 0 : 1;
569
+ }
570
+
571
+ // ---------------- remove
572
+ if (sub === "remove") {
573
+ // every exit from here is a document in --json mode, including the ones that only used to write to
574
+ // stderr. A script that asks to remove something it already removed gets an answer it can read, not
575
+ // an empty stdout and a number
576
+ const r = await resolveTarget(target!, registry);
577
+ if (!r.ok) { err(r.error); if (json) jsonOut(deps, { ok: false, error: r.error, candidates: r.ambiguous ?? [] }); return r.ambiguous ? 2 : 1; }
578
+ // install writes nothing without a yes; remove deleted a folder in silence. Same rule both ways.
579
+ const state = installedState(r.item, cwd, home, args.includes("--project") ? "project" : undefined);
580
+ if (state === undefined) {
581
+ const msg = `${qualify(r.item)} is not installed here`;
582
+ err(msg); if (json) jsonOut(deps, { ok: false, error: msg, id: qualify(r.item), installed: false });
583
+ return 1;
584
+ }
585
+ const tty = deps.tty ?? process.stdin.isTTY === true;
586
+ if (!args.includes("--yes")) {
587
+ // --json never prompts, for the same reason install does not: the line that tells you WHAT you are
588
+ // about to delete belongs in the document, and a y/N without it is a question nobody can answer
589
+ if (json) {
590
+ const msg = `nothing removed: pass --yes to confirm`;
591
+ jsonOut(deps, { ok: false, needsApproval: true, id: qualify(r.item), path: state.path, scope: state.scope });
592
+ err(msg); return 1;
593
+ }
594
+ if (!tty) { err(`nothing removed: ${qualify(r.item)} lives at ${state.path} — rerun on a terminal, or pass --yes`); return 1; }
595
+ out(`${qualify(r.item)} ${state.path}${state.scope === "project" ? " (this repo)" : ""}`);
596
+ const answer = (await (deps.plain ?? defaultPlain)("remove this? [y/N] ")).trim().toLowerCase();
597
+ if (answer !== "y" && answer !== "yes") { out("nothing removed"); return 1; }
598
+ }
599
+ const done = removeItem(r.item, cwd, home, args.includes("--project") ? "project" : undefined);
600
+ if (!done.ok) { err(done.error); if (json) jsonOut(deps, { ok: false, error: done.error, id: qualify(r.item) }); return 1; }
601
+ if (json) { jsonOut(deps, { ok: true, removed: qualify(r.item), path: done.path }); return 0; }
602
+ out(`removed ${qualify(r.item)} from ${done.path}`);
603
+ return 0;
604
+ }
605
+
606
+ // ---------------- install
607
+ if (sub === "install") {
608
+ const r = await resolveTarget(target!, registry);
609
+ if (!r.ok) {
610
+ err(r.error);
611
+ if (json) jsonOut(deps, { error: r.error, candidates: r.ambiguous ?? [] });
612
+ else for (const c of r.ambiguous ?? []) err(` ${qualify(c)} ${c.description}`);
613
+ return r.ambiguous ? 2 : 1;
614
+ }
615
+ const pickRaw = flag("--pick");
616
+ const pick = pickRaw === undefined ? undefined : Number(pickRaw);
617
+ if (pick !== undefined && !Number.isInteger(pick)) return usage(`--pick takes a number`);
618
+ const asName = flag("--as");
619
+ // the configured default model, so the estimate is scaled to the tokenizer the person actually runs.
620
+ // Nothing configured → undefined, and the line says the numbers are unscaled rather than guessing.
621
+ const model = deps.model ?? (await defaultModelRef());
622
+ const ref = flag("--ref");
623
+ if (ref !== undefined && ref.trim() === "") return usage(`--ref needs a branch, tag or commit`);
624
+ // --local / --no-local decide the install-once question up front; neither → it is asked on a terminal
625
+ const local = args.includes("--local") ? true : args.includes("--no-local") ? false : undefined;
626
+ const opts = { scope, cwd, home, ...(pick !== undefined ? { pick } : {}), ...(asName !== undefined ? { as: asName } : {}),
627
+ ...(ref !== undefined ? { ref } : {}), ...(deps.prereqEnv !== undefined ? { prereqEnv: deps.prereqEnv } : {}),
628
+ ...(model !== undefined ? { model } : {}), ...(local !== undefined ? { local } : {}) };
629
+ return installOne(r.item, opts, {
630
+ out, err, json, yes: args.includes("--yes"), tty: deps.tty ?? process.stdin.isTTY === true,
631
+ secret: deps.secret ?? readSecret, plain: deps.plain ?? defaultPlain, verb: "install", dryRun: args.includes("--dry-run"),
632
+ // --force was accepted, documented, and read by nobody: the plan said "replaces …" and the write
633
+ // then refused with "already exists (use --force to replace)" — asking for the flag the user passed.
634
+ run: { ...deps.run, ...(args.includes("--force") ? { force: true } : {}), ...(offline ? { offline: true } : {}) },
635
+ });
636
+ }
637
+
638
+ // every name in SUBCOMMAND_FLAGS is handled above; an unknown one was refused before the branches
639
+ return usage(`unknown command "${sub}"`);
640
+ }
641
+
642
+ /** The configured default provider/model, or undefined. Imported lazily: `market search` has no business
643
+ * loading the provider stack, and this is only wanted while drawing an install plan. Never throws — a
644
+ * broken provider config must not stop an install. */
645
+ async function defaultModelRef(): Promise<{ provider: string; model: string } | undefined> {
646
+ try {
647
+ const { resolveProvider } = await import("../providers/stream.ts");
648
+ const cfg = resolveProvider();
649
+ if (!cfg) return undefined;
650
+ const model = process.env.ROVECODE_MODEL ?? cfg.defaultModel;
651
+ return model ? { provider: cfg.id, model } : undefined;
652
+ } catch { return undefined; }
653
+ }
654
+
655
+ async function defaultPlain(prompt: string): Promise<string> {
656
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
657
+ try { return await new Promise<string>((res) => rl.question(prompt, res)); } finally { rl.close(); }
658
+ }