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.
- package/README.md +57 -72
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -38
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -527
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-j6gn14w3.js +0 -2
- package/dist/cli/ask-user-cwstt8fz.js +0 -2
- package/dist/cli/auth-login-9bbp9915.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-16zqdms5.js +0 -9
- package/dist/cli/catalog-1xchffa4.js +0 -2
- package/dist/cli/cli-1n1zb64f.js +0 -2
- package/dist/cli/client-2t9gjkck.js +0 -2
- package/dist/cli/commands-exafvm2b.js +0 -2
- package/dist/cli/connect-6zde0kn3.js +0 -2
- package/dist/cli/context-cmd-5t43wgqt.js +0 -2
- package/dist/cli/context-report-kt01pw8y.js +0 -2
- package/dist/cli/count-remote-ap7x3vh6.js +0 -2
- package/dist/cli/design-ne5zszyh.js +0 -2
- package/dist/cli/dispatch-2r5myxye.js +0 -2
- package/dist/cli/doctor-ws4fh4tn.js +0 -3
- package/dist/cli/executor-bdrjn634.js +0 -2
- package/dist/cli/export-1mxb9g5p.js +0 -2
- package/dist/cli/files-g104xghh.js +0 -2
- package/dist/cli/gauntlet-07xrjpj7.js +0 -2
- package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
- package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
- package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
- package/dist/cli/hashline-znvrat11.js +0 -2
- package/dist/cli/http-xafw6fsh.js +0 -143
- package/dist/cli/index-1sgjm25y.js +0 -2
- package/dist/cli/init-g2m0tn4m.js +0 -51
- package/dist/cli/install-avaqjjqq.js +0 -2
- package/dist/cli/loop-mmpfft01.js +0 -2
- package/dist/cli/main-0904f6ps.js +0 -5
- package/dist/cli/main-0ab9fc26.js +0 -9
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-0mtcdbs7.js +0 -3
- package/dist/cli/main-0z1w2zsg.js +0 -3
- package/dist/cli/main-1dchs7xv.js +0 -18
- package/dist/cli/main-1ereejm1.js +0 -3
- package/dist/cli/main-1k1kw6b5.js +0 -3
- package/dist/cli/main-27y4sm2k.js +0 -38
- package/dist/cli/main-2wwjex5j.js +0 -58
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2yfck9b5.js +0 -3
- package/dist/cli/main-2zmzgkwh.js +0 -3
- package/dist/cli/main-351pz3z7.js +0 -7
- package/dist/cli/main-3gjqfh7a.js +0 -6
- package/dist/cli/main-3nf3kgve.js +0 -3
- package/dist/cli/main-3pjrb2hd.js +0 -3
- package/dist/cli/main-3rxcvgna.js +0 -19
- package/dist/cli/main-4b3jgy66.js +0 -19
- package/dist/cli/main-4wndhjdc.js +0 -7
- package/dist/cli/main-4xcmvxnk.js +0 -3
- package/dist/cli/main-5tbz0wbz.js +0 -4
- package/dist/cli/main-5ywnwthm.js +0 -3
- package/dist/cli/main-6b62vkz0.js +0 -14
- package/dist/cli/main-6dnk69vp.js +0 -3
- package/dist/cli/main-6genrmhs.js +0 -136
- package/dist/cli/main-73g7eff4.js +0 -15
- package/dist/cli/main-7c5thhjd.js +0 -5
- package/dist/cli/main-7rn6bqje.js +0 -3
- package/dist/cli/main-80haw7qk.js +0 -4
- package/dist/cli/main-875s60s2.js +0 -4
- package/dist/cli/main-8kjxbpw4.js +0 -8
- package/dist/cli/main-90ds1z4e.js +0 -10
- package/dist/cli/main-9etavkew.js +0 -3
- package/dist/cli/main-a9njrkk1.js +0 -3
- package/dist/cli/main-aecrjq2d.js +0 -12
- package/dist/cli/main-ck9asesq.js +0 -9
- package/dist/cli/main-cta9racd.js +0 -4
- package/dist/cli/main-ddv7j2ag.js +0 -3
- package/dist/cli/main-dfreez27.js +0 -10
- package/dist/cli/main-f7rw7des.js +0 -3
- package/dist/cli/main-ggcn7rd7.js +0 -5
- package/dist/cli/main-gzkmycnv.js +0 -3
- package/dist/cli/main-hq51jg8v.js +0 -18
- package/dist/cli/main-jft389w9.js +0 -8
- package/dist/cli/main-k1eqkg83.js +0 -3
- package/dist/cli/main-k2y8a2aw.js +0 -9
- package/dist/cli/main-kcpbykxz.js +0 -4
- package/dist/cli/main-kd488vje.js +0 -22
- package/dist/cli/main-kh32yvgk.js +0 -5
- package/dist/cli/main-kqxnqjnv.js +0 -25
- package/dist/cli/main-kyn0xnsg.js +0 -3
- package/dist/cli/main-m1kk6fp5.js +0 -21
- package/dist/cli/main-mv40pcr2.js +0 -4
- package/dist/cli/main-n0t3973w.js +0 -3
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-pknhvrmj.js +0 -3
- package/dist/cli/main-pn1w7a7j.js +0 -3
- package/dist/cli/main-prxxs70n.js +0 -4
- package/dist/cli/main-q3vsesf9.js +0 -3
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-rdgdw24b.js +0 -25
- package/dist/cli/main-rfth4tbm.js +0 -16
- package/dist/cli/main-rg0wn0xf.js +0 -5
- package/dist/cli/main-sdmxhtv8.js +0 -4
- package/dist/cli/main-skbp13js.js +0 -18
- package/dist/cli/main-t4xnd213.js +0 -7
- package/dist/cli/main-vqak588n.js +0 -4
- package/dist/cli/main-w2n1303f.js +0 -9
- package/dist/cli/main-wbrdspr2.js +0 -5
- package/dist/cli/main-wsrg79c1.js +0 -7
- package/dist/cli/main-x4r0fne4.js +0 -5
- package/dist/cli/main-xea2f3tn.js +0 -6
- package/dist/cli/main-xg704a3c.js +0 -3
- package/dist/cli/main-xvnrabfp.js +0 -16
- package/dist/cli/main-xy53xf0r.js +0 -4
- package/dist/cli/main-y1fqy60y.js +0 -3
- package/dist/cli/main-yn8cd281.js +0 -34
- package/dist/cli/main-yr0ksc0h.js +0 -4
- package/dist/cli/main-z2ex2vyf.js +0 -4
- package/dist/cli/main-z3aayzvq.js +0 -3
- package/dist/cli/main-zaqh35jg.js +0 -3
- package/dist/cli/main-zc2e8e46.js +0 -4
- package/dist/cli/main-zzrfw6cf.js +0 -13
- package/dist/cli/main.js +0 -280
- package/dist/cli/market-cmd-e14kmx9n.js +0 -5
- package/dist/cli/mcp-login-wq7ktdek.js +0 -2
- package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
- package/dist/cli/notify-b7qc0cjb.js +0 -2
- package/dist/cli/oauth-z8whcgfx.js +0 -2
- package/dist/cli/output-b3ewj3ps.js +0 -16
- package/dist/cli/profiles-6mr5he5e.js +0 -2
- package/dist/cli/provider-config-g7j42q8x.js +0 -2
- package/dist/cli/provider-jr1y8vvm.js +0 -2
- package/dist/cli/registry-s8yk86g0.js +0 -2
- package/dist/cli/registry-t6p8d4mn.js +0 -2
- package/dist/cli/repl-bajwe1mh.js +0 -11
- package/dist/cli/resume-rwn9nz7y.js +0 -2
- package/dist/cli/run-flags-nah7ndpt.js +0 -2
- package/dist/cli/runtime-n7gafzhb.js +0 -2
- package/dist/cli/sandbox-config-emdy18x4.js +0 -2
- package/dist/cli/server-b0nvs2bn.js +0 -5
- package/dist/cli/session-arg-y75wd4kj.js +0 -2
- package/dist/cli/session-j62evmjq.js +0 -2
- package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
- package/dist/cli/settings-df10wfez.js +0 -2
- package/dist/cli/setup-jzvv72fg.js +0 -2
- package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
- package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
- package/dist/cli/smoke-p7748apt.js +0 -8
- package/dist/cli/start-chat-s4st3mm0.js +0 -12
- package/dist/cli/stream-gmeyewds.js +0 -2
- package/dist/cli/task-gh0kkp3n.js +0 -2
- package/dist/cli/tasks-z1kfpe8e.js +0 -2
- package/dist/cli/thinking-0eqkrz6t.js +0 -2
- package/dist/cli/todo-5brcrt9m.js +0 -2
- package/dist/cli/tools-7pzm0vj9.js +0 -2
- package/dist/cli/tools-s635p6s8.js +0 -2
- package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
- package/dist/cli/update-check-pt31bm2f.js +0 -2
- package/dist/cli/update-cmd-tk131s9t.js +0 -2
- package/dist/cli/voice-56nabd8d.js +0 -2
- package/dist/cli/webfetch-xd8q596m.js +0 -2
- package/dist/cli/websearch-5hkf98k1.js +0 -2
- package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
- package/dist/cli/workspace-q10g5z3e.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -55
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/lib/sdk.js +0 -360
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Install-once for MCP servers the market would otherwise launch with `npx -y <package>`.
|
|
2
|
+
*
|
|
3
|
+
* Why this exists, in numbers (Windows 11, node 24, npm 11, the memory and filesystem servers, medians of
|
|
4
|
+
* 3–5 runs through McpManager, measured 2026-09-06): a warm `npx -y` takes 1.8–2.1 s from spawn to the
|
|
5
|
+
* initialize handshake; the same package installed once and launched with `node <its bin>` takes
|
|
6
|
+
* 0.35–0.47 s. The difference is npx itself — `npx --version` alone is 0.6 s, and every warm launch still
|
|
7
|
+
* asks registry.npmjs.org to revalidate the package (0.3–1.3 s, and no network means a timeout path). A
|
|
8
|
+
* cold cache is 7 s and a 47 MB download, repeated whenever upstream publishes, because `-y <package>`
|
|
9
|
+
* means "latest". The one-time install is ~6 s and ~30 MB for two servers sharing one SDK copy.
|
|
10
|
+
*
|
|
11
|
+
* What it is NOT: silent, or the only path. The human is asked, in the plan they approve, and "no" leaves
|
|
12
|
+
* the npx line exactly as it is today. Installing code is a bigger act than writing a config line, and
|
|
13
|
+
* the plan says so in those words. The record in installed.json carries the package name, the version
|
|
14
|
+
* that landed and npm's integrity hash from the lockfile — an install that is recorded is auditable; an
|
|
15
|
+
* `npx -y` that runs whatever "latest" is at every start is not.
|
|
16
|
+
*
|
|
17
|
+
* Where: one shared prefix, `~/.rovecode/mcp/`, with its own package.json so npm never mistakes a parent
|
|
18
|
+
* folder for the project. N servers share one node_modules and one SDK copy. The launch line is `node`
|
|
19
|
+
* plus the ABSOLUTE path of the package's bin — never the .cmd shim (it means cmd.exe and its quoting),
|
|
20
|
+
* and the path survives a space because it is one argv entry, not a shell string. */
|
|
21
|
+
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { join, resolve } from "node:path";
|
|
24
|
+
import type { McpServerConfig } from "./config.ts";
|
|
25
|
+
import type { MarketInstall } from "./market.ts";
|
|
26
|
+
|
|
27
|
+
export type Spawn = (cmd: string[], cwd: string) => Promise<{ code: number; stderr: string }>;
|
|
28
|
+
|
|
29
|
+
/** the package an `npx …` launch line runs, taken apart */
|
|
30
|
+
export interface NpxPackage {
|
|
31
|
+
/** the package name, `@scope/name` or `name` */
|
|
32
|
+
name: string;
|
|
33
|
+
/** what npx was given: name, or `name@version` when the catalog pinned one */
|
|
34
|
+
spec: string;
|
|
35
|
+
/** the pinned version, when the spec carried one */
|
|
36
|
+
version?: string;
|
|
37
|
+
/** the arguments the SERVER receives — everything after the package spec */
|
|
38
|
+
rest: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const NPX_FLAGS_NO_VALUE = new Set(["-y", "--yes", "-q", "--quiet", "--no-install", "--prefer-offline", "--prefer-online"]);
|
|
42
|
+
/** `@scope/name`, `name`, optionally `@version` — a registry package and nothing else (no git URL, no tarball, no path) */
|
|
43
|
+
const PACKAGE_SPEC = /^(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)(?:@([^@\s/]+))?$/;
|
|
44
|
+
|
|
45
|
+
/** The package a stdio `npx` launch runs, or undefined when the line is not a plain `npx [flags] <package>
|
|
46
|
+
* [args]` — an `npx -p x cmd`, a git URL or a tarball is left to npx, since we cannot say what we would be
|
|
47
|
+
* installing. Undefined means "no offer", never an error: the npx line still works. */
|
|
48
|
+
export function npxPackage(install: MarketInstall): NpxPackage | undefined {
|
|
49
|
+
if (install.kind !== "stdio" || install.runtime !== "npx" || install.command !== "npx") return undefined;
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < install.args.length && install.args[i]!.startsWith("-")) {
|
|
52
|
+
if (!NPX_FLAGS_NO_VALUE.has(install.args[i]!)) return undefined; // a flag with a value (-p, --package, -c): not our shape
|
|
53
|
+
i += 1;
|
|
54
|
+
}
|
|
55
|
+
const spec = install.args[i];
|
|
56
|
+
if (spec === undefined) return undefined;
|
|
57
|
+
const m = PACKAGE_SPEC.exec(spec);
|
|
58
|
+
if (!m) return undefined;
|
|
59
|
+
const out: NpxPackage = { name: m[1]!, spec, rest: install.args.slice(i + 1) };
|
|
60
|
+
if (m[2] !== undefined) out.version = m[2];
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** the shared prefix every install-once server lives in */
|
|
65
|
+
export function localPrefix(home: string): string { return join(home, "mcp"); }
|
|
66
|
+
|
|
67
|
+
/** what landed on disk, read back after npm finished — the record's source of truth */
|
|
68
|
+
export interface LocalPackage {
|
|
69
|
+
name: string;
|
|
70
|
+
version: string;
|
|
71
|
+
/** ABSOLUTE path of the package's bin script — what `node` runs */
|
|
72
|
+
bin: string;
|
|
73
|
+
/** npm's integrity hash for the tarball, from package-lock.json */
|
|
74
|
+
integrity?: string;
|
|
75
|
+
/** the tarball URL npm resolved, from package-lock.json */
|
|
76
|
+
resolved?: string;
|
|
77
|
+
/** what could not be recorded and why — an empty list is the normal case. Written into installed.json
|
|
78
|
+
* as it is, so a record with a hole says where the hole is instead of leaving a field silently absent. */
|
|
79
|
+
missing: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface InstallLocalDeps {
|
|
83
|
+
/** how `npm install` runs — tests inject one that writes a fake node_modules and never touches the network */
|
|
84
|
+
spawn?: Spawn;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const defaultSpawn: Spawn = async (cmd, cwd) => {
|
|
88
|
+
const p = Bun.spawn(cmd, { cwd, stdout: "ignore", stderr: "pipe", stdin: "ignore" });
|
|
89
|
+
return { code: await p.exited, stderr: await new Response(p.stderr).text() };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** the exact argv `installLocalPackage` runs — exported so the plan can show it verbatim */
|
|
93
|
+
export function npmInstallArgv(pkg: NpxPackage, prefix: string): string[] {
|
|
94
|
+
return ["npm", "install", "--prefix", prefix, "--save", "--no-fund", "--no-audit", "--loglevel=error", pkg.spec];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Run `npm install` for one package into the shared prefix, then read back what landed. Never throws for
|
|
98
|
+
* an npm failure — the caller shows the error and nothing has been written to mcp.json yet. */
|
|
99
|
+
export async function installLocalPackage(pkg: NpxPackage, prefix: string, deps: InstallLocalDeps = {}): Promise<{ ok: true; pkg: LocalPackage } | { ok: false; error: string }> {
|
|
100
|
+
const spawn = deps.spawn ?? defaultSpawn;
|
|
101
|
+
try {
|
|
102
|
+
mkdirSync(prefix, { recursive: true });
|
|
103
|
+
// a package.json of its own, so npm treats the prefix as the project: without one it can walk up and
|
|
104
|
+
// install into whatever package.json it finds above ~/.rovecode
|
|
105
|
+
const manifest = join(prefix, "package.json");
|
|
106
|
+
if (!existsSync(manifest)) {
|
|
107
|
+
writeFileSync(manifest, JSON.stringify({ name: "rovecode-mcp-servers", private: true, description: "MCP servers installed once by rovecode's market — launched with node, not npx (docs/mcp-market.md)" }, null, 2) + "\n");
|
|
108
|
+
}
|
|
109
|
+
const r = await spawn(npmInstallArgv(pkg, prefix), prefix);
|
|
110
|
+
if (r.code !== 0) return { ok: false, error: `npm install ${pkg.spec} failed (exit ${r.code})${r.stderr.trim() ? `: ${r.stderr.trim().split("\n").slice(-3).join(" · ")}` : ""}` };
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return { ok: false, error: `npm install ${pkg.spec} could not run: ${e instanceof Error ? e.message : String(e)}` };
|
|
113
|
+
}
|
|
114
|
+
return readLocalPackage(pkg.name, prefix);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What is on disk for one package under the prefix: version and bin from its package.json, integrity from
|
|
118
|
+
* the lockfile. `ok: false` when the package or its bin is not there — then there is nothing to launch. */
|
|
119
|
+
export function readLocalPackage(name: string, prefix: string): { ok: true; pkg: LocalPackage } | { ok: false; error: string } {
|
|
120
|
+
const dir = join(prefix, "node_modules", ...name.split("/"));
|
|
121
|
+
const manifestPath = join(dir, "package.json");
|
|
122
|
+
if (!existsSync(manifestPath)) return { ok: false, error: `npm reported success but ${manifestPath} is not there` };
|
|
123
|
+
let manifest: Record<string, unknown>;
|
|
124
|
+
try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>; }
|
|
125
|
+
catch (e) { return { ok: false, error: `${manifestPath}: ${e instanceof Error ? e.message : String(e)}` }; }
|
|
126
|
+
const version = typeof manifest.version === "string" ? manifest.version : undefined;
|
|
127
|
+
if (version === undefined) return { ok: false, error: `${manifestPath} states no version` };
|
|
128
|
+
const binRel = binOf(name, manifest.bin);
|
|
129
|
+
if (binRel === undefined) return { ok: false, error: `${name} declares no bin — there is nothing for node to run; npx would have failed the same way` };
|
|
130
|
+
const bin = resolve(dir, binRel);
|
|
131
|
+
if (!existsSync(bin)) return { ok: false, error: `${name}'s bin ${bin} is not on disk` };
|
|
132
|
+
const missing: string[] = [];
|
|
133
|
+
const lock = lockEntry(prefix, name, missing);
|
|
134
|
+
const pkg: LocalPackage = { name, version, bin, missing };
|
|
135
|
+
if (lock?.integrity !== undefined) pkg.integrity = lock.integrity;
|
|
136
|
+
if (lock?.resolved !== undefined) pkg.resolved = lock.resolved;
|
|
137
|
+
return { ok: true, pkg };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** the bin a package declares: a string, or a map — the entry named like the package, else the only one */
|
|
141
|
+
function binOf(name: string, bin: unknown): string | undefined {
|
|
142
|
+
if (typeof bin === "string") return bin;
|
|
143
|
+
if (typeof bin !== "object" || bin === null) return undefined;
|
|
144
|
+
const entries = Object.entries(bin as Record<string, unknown>).filter((e): e is [string, string] => typeof e[1] === "string");
|
|
145
|
+
if (entries.length === 0) return undefined;
|
|
146
|
+
const short = name.split("/").pop()!;
|
|
147
|
+
return (entries.find(([k]) => k === short) ?? entries[0]!)[1];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** package-lock.json (lockfileVersion 2/3) → the entry for `node_modules/<name>`. Every way it can fall
|
|
151
|
+
* short is written into `missing` in words, because a silent undefined here becomes a record that looks
|
|
152
|
+
* complete and is not. */
|
|
153
|
+
function lockEntry(prefix: string, name: string, missing: string[]): { integrity?: string; resolved?: string } | undefined {
|
|
154
|
+
const lockPath = join(prefix, "package-lock.json");
|
|
155
|
+
if (!existsSync(lockPath)) { missing.push(`integrity: ${lockPath} was not written by npm`); return undefined; }
|
|
156
|
+
let lock: unknown;
|
|
157
|
+
try { lock = JSON.parse(readFileSync(lockPath, "utf8")); }
|
|
158
|
+
catch { missing.push(`integrity: ${lockPath} is not valid JSON`); return undefined; }
|
|
159
|
+
const packages = typeof lock === "object" && lock !== null ? (lock as { packages?: unknown }).packages : undefined;
|
|
160
|
+
if (typeof packages !== "object" || packages === null) { missing.push(`integrity: ${lockPath} has no "packages" map (lockfileVersion 1?)`); return undefined; }
|
|
161
|
+
const entry = (packages as Record<string, unknown>)[`node_modules/${name}`];
|
|
162
|
+
if (typeof entry !== "object" || entry === null) { missing.push(`integrity: ${lockPath} has no entry for node_modules/${name}`); return undefined; }
|
|
163
|
+
const e = entry as { integrity?: unknown; resolved?: unknown };
|
|
164
|
+
const out: { integrity?: string; resolved?: string } = {};
|
|
165
|
+
if (typeof e.integrity === "string") out.integrity = e.integrity; else missing.push(`integrity: the lockfile entry for ${name} carries no integrity field`);
|
|
166
|
+
if (typeof e.resolved === "string") out.resolved = e.resolved;
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** the mcp.json launch line for an installed package: `node` + the bin's absolute path + the server's own
|
|
171
|
+
* arguments. One argv entry per item — a path with a space in it is still one argument, because nothing
|
|
172
|
+
* here goes through a shell (mcp/client.ts spawns with shell: false). */
|
|
173
|
+
export function localLaunch(pkg: LocalPackage, rest: string[]): { command: "node"; args: string[] } {
|
|
174
|
+
return { command: "node", args: [pkg.bin, ...rest] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** the launch line as the plan can show it BEFORE the install: the bin's exact filename is read from the
|
|
178
|
+
* package after npm has put it there, so the preview names the folder and marks the file as pending */
|
|
179
|
+
export function plannedLaunchLabel(pkg: NpxPackage, prefix: string): string {
|
|
180
|
+
return ["node", `${join(prefix, "node_modules", ...pkg.name.split("/"))}${process.platform === "win32" ? "\\" : "/"}<its bin, read after the install>`, ...pkg.rest].join(" ");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** the plan rows that say what installing once means — every word the human should read before the yes */
|
|
184
|
+
export function localPlanLines(pkg: NpxPackage, prefix: string): string[] {
|
|
185
|
+
return [
|
|
186
|
+
` installs ${npmInstallArgv(pkg, prefix).join(" ")}`,
|
|
187
|
+
` rovecode runs a package manager for you here. npm downloads ${pkg.spec} and everything it depends`,
|
|
188
|
+
` on and puts their CODE on this machine, under ${prefix} — typically 20–30 MB and a few seconds, once.`,
|
|
189
|
+
` In return the server starts in ~0.4 s instead of ~2 s and needs no network to start.`,
|
|
190
|
+
` Requires npm (it comes with Node.js, as npx does).`,
|
|
191
|
+
` records package name, version and npm's integrity hash in installed.json — what ran is on record`,
|
|
192
|
+
` (an npx line runs whatever "latest" is at every start, and records nothing)`,
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** a configured server that still launches through npx — the rows `mcp list` may offer the install-once line for */
|
|
197
|
+
export function launchesViaNpx(server: McpServerConfig): boolean {
|
|
198
|
+
return server.transport === "stdio" && server.command === "npx" && server.enabled !== false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** the one-line offer `mcp list` prints under the rows when some of them start through npx. Says what
|
|
202
|
+
* changes, what it costs and that nothing happens until the human runs the command — no config is
|
|
203
|
+
* rewritten by a listing. */
|
|
204
|
+
export function npxOfferLine(names: string[]): string | undefined {
|
|
205
|
+
if (names.length === 0) return undefined;
|
|
206
|
+
const n = names.length;
|
|
207
|
+
// `<catalog name>`, not the server name the row shows: `mcp add` takes the name you installed it by (a
|
|
208
|
+
// registry server is `io.github.acme/widgets`, its row is `widgets`), and a renamed one needs its `--as` back
|
|
209
|
+
return `${n} server${n === 1 ? "" : "s"} start${n === 1 ? "s" : ""} through npx, which re-resolves the package at every start (~2 s each): ${names.join(", ")}. `
|
|
210
|
+
+ `To start in ~0.4 s, reinstall with \`rovecode mcp add <catalog name> --local --force\` (the name you installed it by; add \`--as <server name>\` if you renamed it; installs the package once, ~25 MB). Nothing changes until you do.`;
|
|
211
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** The curated half of the MCP market: servers we have looked at, from publishers we can name, with
|
|
2
|
+
* the exact launch line spelled out. Offline by design — `rovecode mcp search` answers from this list
|
|
3
|
+
* before it asks the registry, and /mcp with an empty query shows only this list. Rules for an entry:
|
|
4
|
+
* the publisher is the org that owns the package or the host; secrets travel by environment variable
|
|
5
|
+
* (or a header for a remote), never as an argument; anything OAuth-only is left out because our http
|
|
6
|
+
* transport carries a header, not a browser flow. Keep it short — this is a shelf, not a mirror. */
|
|
7
|
+
|
|
8
|
+
import type { MarketEntry } from "./market.ts";
|
|
9
|
+
|
|
10
|
+
const npx = (pkg: string, ...rest: string[]): string[] => ["-y", pkg, ...rest];
|
|
11
|
+
|
|
12
|
+
export const CURATED: readonly MarketEntry[] = [
|
|
13
|
+
{ key: "filesystem", title: "Filesystem", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
|
|
14
|
+
description: "Read, write, search and move files under the directories you name.",
|
|
15
|
+
repository: "https://github.com/modelcontextprotocol/servers",
|
|
16
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-filesystem"), env: [], pending: ["<directory the server may touch>"] }] },
|
|
17
|
+
{ key: "memory", title: "Memory", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
|
|
18
|
+
description: "A knowledge graph the model can remember across sessions with (entities, relations, observations).",
|
|
19
|
+
repository: "https://github.com/modelcontextprotocol/servers",
|
|
20
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-memory"), env: [], pending: [] }] },
|
|
21
|
+
{ key: "sequential-thinking", title: "Sequential Thinking", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
|
|
22
|
+
description: "A scratchpad tool for step-by-step reasoning with revisions and branches.",
|
|
23
|
+
repository: "https://github.com/modelcontextprotocol/servers",
|
|
24
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-sequential-thinking"), env: [], pending: [] }] },
|
|
25
|
+
{ key: "everything", title: "Everything (test server)", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
|
|
26
|
+
description: "Exercises every MCP feature — prompts, resources, sampling, progress. For testing a client, not for work.",
|
|
27
|
+
repository: "https://github.com/modelcontextprotocol/servers",
|
|
28
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@modelcontextprotocol/server-everything"), env: [], pending: [] }] },
|
|
29
|
+
{ key: "fetch", title: "Fetch", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
|
|
30
|
+
description: "Fetch a URL and hand the page back as markdown (Python; needs uv).",
|
|
31
|
+
repository: "https://github.com/modelcontextprotocol/servers",
|
|
32
|
+
installs: [{ kind: "stdio", runtime: "uvx", command: "uvx", args: ["mcp-server-fetch"], env: [], pending: [] }] },
|
|
33
|
+
{ key: "git", title: "Git", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
|
|
34
|
+
description: "Read and search git repositories: log, diff, show, status (Python; needs uv).",
|
|
35
|
+
repository: "https://github.com/modelcontextprotocol/servers",
|
|
36
|
+
installs: [{ kind: "stdio", runtime: "uvx", command: "uvx", args: ["mcp-server-git"], env: [], pending: [] }] },
|
|
37
|
+
{ key: "time", title: "Time", source: "curated", publisher: "modelcontextprotocol (Anthropic)",
|
|
38
|
+
description: "Current time and timezone conversion (Python; needs uv).",
|
|
39
|
+
repository: "https://github.com/modelcontextprotocol/servers",
|
|
40
|
+
installs: [{ kind: "stdio", runtime: "uvx", command: "uvx", args: ["mcp-server-time"], env: [], pending: [] }] },
|
|
41
|
+
{ key: "github", title: "GitHub", source: "curated", publisher: "GitHub",
|
|
42
|
+
description: "Issues, pull requests, code search and Actions on GitHub — remote with a personal access token, or the official image via docker.",
|
|
43
|
+
repository: "https://github.com/github/github-mcp-server",
|
|
44
|
+
installs: [
|
|
45
|
+
{ kind: "http", url: "https://api.githubcopilot.com/mcp/", headers: [{ name: "Authorization", template: "Bearer {GITHUB_PAT}", required: true, secret: true, description: "a GitHub personal access token" }] },
|
|
46
|
+
{ kind: "stdio", runtime: "docker", command: "docker", args: ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
|
|
47
|
+
env: [{ name: "GITHUB_PERSONAL_ACCESS_TOKEN", required: true, secret: true, description: "a GitHub personal access token" }], pending: [] },
|
|
48
|
+
] },
|
|
49
|
+
{ key: "playwright", title: "Playwright", source: "curated", publisher: "Microsoft",
|
|
50
|
+
description: "Drive a real browser through accessibility snapshots: navigate, click, type, screenshot.",
|
|
51
|
+
repository: "https://github.com/microsoft/playwright-mcp",
|
|
52
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@playwright/mcp@latest"), env: [], pending: [] }] },
|
|
53
|
+
{ key: "context7", title: "Context7", source: "curated", publisher: "Upstash",
|
|
54
|
+
description: "Up-to-date library documentation and code examples for the versions you actually use.",
|
|
55
|
+
repository: "https://github.com/upstash/context7",
|
|
56
|
+
installs: [
|
|
57
|
+
{ kind: "http", url: "https://mcp.context7.com/mcp", headers: [{ name: "CONTEXT7_API_KEY", required: false, secret: true, description: "optional API key for higher limits" }] },
|
|
58
|
+
{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@upstash/context7-mcp"), env: [{ name: "CONTEXT7_API_KEY", required: false, secret: true, description: "optional API key for higher limits" }], pending: [] },
|
|
59
|
+
] },
|
|
60
|
+
{ key: "brave-search", title: "Brave Search", source: "curated", publisher: "Brave",
|
|
61
|
+
description: "Web, news, image and local search through the Brave Search API.",
|
|
62
|
+
repository: "https://github.com/brave/brave-search-mcp-server",
|
|
63
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("@brave/brave-search-mcp-server"), env: [{ name: "BRAVE_API_KEY", required: true, secret: true, description: "Brave Search API key" }], pending: [] }] },
|
|
64
|
+
{ key: "firecrawl", title: "Firecrawl", source: "curated", publisher: "Firecrawl (Mendable)",
|
|
65
|
+
description: "Scrape, crawl and extract structured data from websites.",
|
|
66
|
+
repository: "https://github.com/mendableai/firecrawl-mcp-server",
|
|
67
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("firecrawl-mcp"), env: [{ name: "FIRECRAWL_API_KEY", required: true, secret: true, description: "Firecrawl API key" }], pending: [] }] },
|
|
68
|
+
{ key: "tavily", title: "Tavily", source: "curated", publisher: "Tavily",
|
|
69
|
+
description: "Search and extract built for agents: web search, page extraction, site crawl.",
|
|
70
|
+
repository: "https://github.com/tavily-ai/tavily-mcp",
|
|
71
|
+
installs: [{ kind: "stdio", runtime: "npx", command: "npx", args: npx("tavily-mcp"), env: [{ name: "TAVILY_API_KEY", required: true, secret: true, description: "Tavily API key" }], pending: [] }] },
|
|
72
|
+
{ key: "exa", title: "Exa", source: "curated", publisher: "Exa",
|
|
73
|
+
description: "Neural web search and page contents for AI; the remote works without a key at a low rate.",
|
|
74
|
+
repository: "https://github.com/exa-labs/exa-mcp-server",
|
|
75
|
+
installs: [{ kind: "http", url: "https://mcp.exa.ai/mcp", headers: [] }] },
|
|
76
|
+
{ key: "deepwiki", title: "DeepWiki", source: "curated", publisher: "Cognition (Devin)",
|
|
77
|
+
description: "Ask questions about any public GitHub repository, answered from its generated wiki. No key.",
|
|
78
|
+
homepage: "https://docs.devin.ai/work-with-devin/deepwiki-mcp",
|
|
79
|
+
installs: [{ kind: "http", url: "https://mcp.deepwiki.com/mcp", headers: [] }] },
|
|
80
|
+
{ key: "cloudflare-docs", title: "Cloudflare Docs", source: "curated", publisher: "Cloudflare",
|
|
81
|
+
description: "Search Cloudflare's developer documentation. No key.",
|
|
82
|
+
repository: "https://github.com/cloudflare/mcp-server-cloudflare",
|
|
83
|
+
installs: [{ kind: "http", url: "https://docs.mcp.cloudflare.com/mcp", headers: [] }] },
|
|
84
|
+
];
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/** From a market entry to a line in mcp.json — in three visible steps, so no install is silent:
|
|
2
|
+
* planInstall picks the launch form and lists what must be asked; describePlan renders EXACTLY what
|
|
3
|
+
* will be written (command + args or URL, source, publisher, version, the env NAMES, the file) for the
|
|
4
|
+
* human to read before answering; fillPlan + writeServer put it on disk. Secrets: asked by name through
|
|
5
|
+
* the caller's masked prompt, written as values only into the USER file (~/.rovecode/mcp.json, 0o600
|
|
6
|
+
* where the OS honours it) — a PROJECT file gets `${NAME}` and the loader fills it from the environment
|
|
7
|
+
* at launch (config.ts expandVars), so a token never lands in a repo. Never on the command line: a
|
|
8
|
+
* stdio server receives them through `env`, docker through `-e NAME`. */
|
|
9
|
+
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { isRecord, mcpConfigFiles, normalizeEntry, parseConfigFile, placeholderHoles, type McpServerConfig } from "./config.ts";
|
|
13
|
+
import type { EnvSpec, MarketEntry, MarketInstall } from "./market.ts";
|
|
14
|
+
import { installLabel } from "./market.ts";
|
|
15
|
+
import { mcpTrustStatus, trustMcpFile } from "./trust.ts";
|
|
16
|
+
import { localPlanLines, localPrefix, npxPackage, plannedLaunchLabel, type NpxPackage } from "./local-package.ts";
|
|
17
|
+
|
|
18
|
+
export type McpScope = "user" | "project";
|
|
19
|
+
const SERVER_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
20
|
+
const PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_-]*)\}/g;
|
|
21
|
+
|
|
22
|
+
export interface InstallPlan {
|
|
23
|
+
entry: MarketEntry;
|
|
24
|
+
install: MarketInstall;
|
|
25
|
+
scope: McpScope;
|
|
26
|
+
/** the mcp.json this lands in */
|
|
27
|
+
file: string;
|
|
28
|
+
/** the server's name in that file (= the tool prefix the model sees) */
|
|
29
|
+
name: string;
|
|
30
|
+
/** what has to be asked, in order; `secret` ones go through the masked prompt */
|
|
31
|
+
asks: EnvSpec[];
|
|
32
|
+
/** required arguments nobody can fill for the human (a directory, a database URL) */
|
|
33
|
+
pending: string[];
|
|
34
|
+
/** where a header placeholder maps back: variable name → header it belongs to */
|
|
35
|
+
headerVars: Record<string, string>;
|
|
36
|
+
/** install ONCE (mcp/local-package.ts): the npx package this line would run, and the shared prefix npm
|
|
37
|
+
* puts it in. Set only when the human asked for it — the default plan is today's npx line. The launch
|
|
38
|
+
* line written to the file is then `node <bin>`, known after npm has run, so fillPlan takes it as an
|
|
39
|
+
* argument instead of reading it from `install`. */
|
|
40
|
+
local?: { pkg: NpxPackage; prefix: string };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** the server name a registry key gets in mcp.json: its last path segment, lowercased, unsafe runs → "-" */
|
|
44
|
+
export function defaultServerName(key: string): string {
|
|
45
|
+
const tail = key.split("/").pop() ?? key;
|
|
46
|
+
const name = tail.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").slice(0, 64);
|
|
47
|
+
return name.length > 0 ? name : "server";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** env-variable-safe spelling of a header name: `X-Api-Key` → `X_API_KEY` */
|
|
51
|
+
function headerVar(name: string): string { return name.toUpperCase().replace(/[^A-Z0-9_]/g, "_").replace(/^[0-9]/, "_$&"); }
|
|
52
|
+
|
|
53
|
+
export interface PlanOptions {
|
|
54
|
+
scope: McpScope; cwd: string; home: string;
|
|
55
|
+
/** which of entry.installs (default: the first) */
|
|
56
|
+
pick?: number;
|
|
57
|
+
/** override the mcp.json name */
|
|
58
|
+
name?: string;
|
|
59
|
+
/** install the npx package once and launch it with node (the human said yes to the offer). An error when
|
|
60
|
+
* the chosen form is not a plain `npx <package>` line — silently falling back to npx would write a plan
|
|
61
|
+
* the human did not approve. */
|
|
62
|
+
local?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function planInstall(entry: MarketEntry, opts: PlanOptions): InstallPlan | { error: string } {
|
|
66
|
+
if (entry.installs.length === 0) return { error: `${entry.key} lists nothing rovecode can launch or connect to (no stdio package, no streamable-http remote)` };
|
|
67
|
+
const ix = opts.pick ?? 0;
|
|
68
|
+
const install = entry.installs[ix];
|
|
69
|
+
if (!install) return { error: `${entry.key} has ${entry.installs.length} install form(s); --pick ${ix} is out of range` };
|
|
70
|
+
const name = opts.name ?? (entry.source === "curated" ? entry.key : defaultServerName(entry.key));
|
|
71
|
+
if (!SERVER_NAME.test(name)) return { error: `"${name}" is not a usable server name (lowercase letters, digits, . _ -)` };
|
|
72
|
+
const files = mcpConfigFiles(opts.cwd, opts.home);
|
|
73
|
+
const file = opts.scope === "project" ? files.project : files.user!;
|
|
74
|
+
let local: InstallPlan["local"];
|
|
75
|
+
if (opts.local === true) {
|
|
76
|
+
// The launch line install-once writes is THIS machine's absolute path under its ROVECODE_HOME. A project
|
|
77
|
+
// file is shared with everyone who clones the repo, so that line would be a server none of them can start
|
|
78
|
+
// — refused with the way out, rather than written and discovered on someone else's machine.
|
|
79
|
+
if (opts.scope === "project") return { error: `${entry.key}: install-once writes this machine's absolute path (node <home>/mcp/…), and a project file is shared with every clone — install it in user scope (drop --project) or keep the npx line` };
|
|
80
|
+
const pkg = npxPackage(install);
|
|
81
|
+
if (pkg === undefined) return { error: `${entry.key} cannot be installed once: its launch line is not a plain \`npx <package>\` (${installLabel(install)}) — drop --local to write it as it is` };
|
|
82
|
+
local = { pkg, prefix: localPrefix(opts.home) };
|
|
83
|
+
}
|
|
84
|
+
const asks: EnvSpec[] = [], headerVars: Record<string, string> = {};
|
|
85
|
+
if (install.kind === "stdio") {
|
|
86
|
+
for (const e of install.env) {
|
|
87
|
+
// a filled default is not a question; an optional plain value without one is left out and named in the preview
|
|
88
|
+
if (e.default !== undefined) continue;
|
|
89
|
+
if (e.secret || e.required) asks.push(e);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
for (const h of install.headers) {
|
|
93
|
+
const vars = [...(h.template ?? "").matchAll(PLACEHOLDER)].map((m) => m[1]!);
|
|
94
|
+
if (h.template !== undefined && vars.length === 0) continue; // a literal header, nothing to ask
|
|
95
|
+
for (const v of vars.length ? vars : [headerVar(h.name)]) {
|
|
96
|
+
headerVars[v] = h.name;
|
|
97
|
+
const spec: EnvSpec = { name: v, required: h.required, secret: h.secret };
|
|
98
|
+
if (h.description) spec.description = h.description;
|
|
99
|
+
asks.push(spec);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { entry, install, scope: opts.scope, file, name, asks, pending: install.kind === "stdio" ? install.pending : [], headerVars, ...(local ? { local } : {}) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One `pending` fragment → the argv words it contributes. A fragment is literal words followed by one
|
|
107
|
+
* `<hole>` — "--root <path>" or "<directory the server may touch>" — so the flag survives whether or not
|
|
108
|
+
* anybody answered, and the hole is either the answer or the placeholder itself. Written into the file
|
|
109
|
+
* unanswered it is not a silent failure: mcp/config.ts refuses to launch a server that still carries one
|
|
110
|
+
* and names the line. Split on the hole rather than on whitespace, because a hole is usually a sentence. */
|
|
111
|
+
export function pendingWords(fragment: string, answer?: string): string[] {
|
|
112
|
+
const m = /^(.*?)\s*(<[^<>]*>)\s*$/.exec(fragment);
|
|
113
|
+
const filled = answer !== undefined && answer.length > 0;
|
|
114
|
+
if (!m) return [filled ? answer : fragment]; // no hole at all: an older catalog's bare hint
|
|
115
|
+
const lead = m[1]!.length > 0 ? m[1]!.split(/\s+/) : [];
|
|
116
|
+
return [...lead, filled ? answer : m[2]!];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** the raw mcp.json entry, with answers in place. Secrets: a value in the USER file, `${NAME}` in a
|
|
120
|
+
* PROJECT file (and `${NAME}` whenever the answer is empty, so a later `export NAME=…` completes it).
|
|
121
|
+
* `launch` replaces the entry's own command + args — the install-once path passes `node <bin>` here once
|
|
122
|
+
* npm has put the bin on disk (local-package.ts localLaunch); a plan without `local` never sets it. */
|
|
123
|
+
export function fillPlan(plan: InstallPlan, answers: Record<string, string>, launch?: { command: string; args: string[] }): Record<string, unknown> {
|
|
124
|
+
const ref = (spec: EnvSpec): string | undefined => {
|
|
125
|
+
const v = answers[spec.name];
|
|
126
|
+
if (v !== undefined && v.length > 0 && !(spec.secret && plan.scope === "project")) return v;
|
|
127
|
+
if (v === undefined || v.length === 0) { if (!spec.required && !(v !== undefined && spec.secret)) return undefined; }
|
|
128
|
+
return `\${${spec.name}}`;
|
|
129
|
+
};
|
|
130
|
+
const { install } = plan;
|
|
131
|
+
if (install.kind === "stdio") {
|
|
132
|
+
const env: Record<string, string> = {};
|
|
133
|
+
for (const e of install.env) {
|
|
134
|
+
if (e.default !== undefined) { env[e.name] = e.default; continue; }
|
|
135
|
+
const v = ref(e);
|
|
136
|
+
if (v !== undefined) env[e.name] = v;
|
|
137
|
+
}
|
|
138
|
+
// `pending` is the entry's required positional arguments — the filesystem server's directory, say.
|
|
139
|
+
// They go into args either as the human's answer or, when nobody could be asked (the TUI has no
|
|
140
|
+
// prompt, `--yes` did not stop), as the placeholder itself. Dropping them, which is what this did
|
|
141
|
+
// first, wrote a server that could never start and a note pointing at a line that was not there.
|
|
142
|
+
const positional = install.pending.flatMap((p) => pendingWords(p, answers[p]));
|
|
143
|
+
const line = launch ?? { command: install.command, args: install.args };
|
|
144
|
+
return { command: line.command, args: [...line.args, ...positional], ...(Object.keys(env).length ? { env } : {}) };
|
|
145
|
+
}
|
|
146
|
+
const headers: Record<string, string> = {};
|
|
147
|
+
for (const h of install.headers) {
|
|
148
|
+
if (h.template !== undefined && !PLACEHOLDER.test(h.template)) { headers[h.name] = h.template; PLACEHOLDER.lastIndex = 0; continue; }
|
|
149
|
+
PLACEHOLDER.lastIndex = 0;
|
|
150
|
+
const vars = Object.entries(plan.headerVars).filter(([, hn]) => hn === h.name).map(([v]) => v);
|
|
151
|
+
let value = h.template ?? `{${vars[0] ?? headerVar(h.name)}}`, complete = true;
|
|
152
|
+
for (const v of vars) {
|
|
153
|
+
const spec = plan.asks.find((a) => a.name === v)!;
|
|
154
|
+
const r = ref(spec);
|
|
155
|
+
if (r === undefined) { complete = false; break; }
|
|
156
|
+
value = value.split(`{${v}}`).join(r);
|
|
157
|
+
}
|
|
158
|
+
if (complete) headers[h.name] = value;
|
|
159
|
+
}
|
|
160
|
+
return { type: "http", url: install.url, ...(Object.keys(headers).length ? { headers } : {}) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** the asks whose `${NAME}` the filled entry actually carries — what the closing note tells the human to set.
|
|
164
|
+
* An optional ask nobody answered is left out of the entry (fillPlan), so naming it would send them to set a
|
|
165
|
+
* variable nothing reads. */
|
|
166
|
+
export function namesWritten(plan: InstallPlan, raw: Record<string, unknown>): string[] {
|
|
167
|
+
const text = JSON.stringify(raw);
|
|
168
|
+
return plan.asks.filter((a) => text.includes(`\${${a.name}}`)).map((a) => a.name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** the confirmation text — everything the human must see before anything is written. `asking` says how
|
|
172
|
+
* the plan's questions get answered: "prompt" (the CLI asks, secrets masked) or "env" (the TUI has no
|
|
173
|
+
* masked input, so every asked value is written as `${NAME}` and read from the environment at launch) */
|
|
174
|
+
export function describePlan(plan: InstallPlan, asking: "prompt" | "env" = "prompt"): string[] {
|
|
175
|
+
const { entry, install } = plan;
|
|
176
|
+
const asked = (secret: boolean): string => asking === "env" ? "(${NAME} — from your environment)" : secret ? "(asked, masked, never shown)" : "(asked)";
|
|
177
|
+
const lines = [
|
|
178
|
+
`${entry.title ?? entry.key}${entry.version ? ` ${entry.version}` : ""}${entry.status ? ` [${entry.status}]` : ""}`,
|
|
179
|
+
` source ${entry.source === "curated" ? "curated list (built into rovecode)" : "MCP registry (registry.modelcontextprotocol.io)"}`,
|
|
180
|
+
` publisher ${entry.publisher ?? "unknown"}`,
|
|
181
|
+
];
|
|
182
|
+
if (entry.repository) lines.push(` repo ${entry.repository}`);
|
|
183
|
+
// install-once: the launch line the file will hold is `node <bin>`, and the rows under it say — in so many
|
|
184
|
+
// words — that a package manager runs and code lands on this machine. That is the plan being approved.
|
|
185
|
+
if (plan.local) lines.push(` runs ${plannedLaunchLabel(plan.local.pkg, plan.local.prefix)}`, ...localPlanLines(plan.local.pkg, plan.local.prefix));
|
|
186
|
+
else lines.push(install.kind === "stdio" ? ` runs ${installLabel(install)}` : ` connects ${install.url}`);
|
|
187
|
+
const envNames = install.kind === "stdio" ? install.env : [];
|
|
188
|
+
for (const e of envNames) {
|
|
189
|
+
const how = e.default !== undefined ? `= ${e.default}` : plan.asks.includes(e) ? asked(e.secret).replace("NAME", e.name) : "(optional, left unset)";
|
|
190
|
+
lines.push(` env ${e.name} ${how}${e.required ? "" : " optional"}`);
|
|
191
|
+
}
|
|
192
|
+
if (install.kind === "http") for (const h of install.headers) {
|
|
193
|
+
const vars = Object.entries(plan.headerVars).filter(([, hn]) => hn === h.name).map(([v]) => v);
|
|
194
|
+
lines.push(` header ${h.name}: ${vars.length ? `${h.template ?? `{${vars[0]}}`} ← ${vars.join(", ")} ${asked(h.secret).replace("NAME", vars[0]!)}` : h.template ?? ""}${h.required ? "" : " optional"}`);
|
|
195
|
+
}
|
|
196
|
+
for (const p of plan.pending) lines.push(` needs ${p} — ${asking === "env" ? "written as the placeholder; fill it in and the server connects" : "asked here; unanswered it is written as the placeholder"}`);
|
|
197
|
+
lines.push(` writes ${plan.file} as "${plan.name}"${asking === "prompt" && plan.scope === "project" && plan.asks.some((a) => a.secret) ? " (secrets stay out of this file: ${NAME} is read from your environment)" : ""}`);
|
|
198
|
+
return lines;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ------------------------------------------------------------------ the file
|
|
202
|
+
|
|
203
|
+
interface FileShape { json: Record<string, unknown>; servers: Record<string, unknown> }
|
|
204
|
+
function readShape(file: string): FileShape {
|
|
205
|
+
if (!existsSync(file)) return { json: {}, servers: {} };
|
|
206
|
+
const json: unknown = JSON.parse(readFileSync(file, "utf8")); // a broken file is the human's to fix; we do not overwrite it
|
|
207
|
+
if (!isRecord(json)) throw new Error(`${file}: root is not an object`);
|
|
208
|
+
const servers = isRecord(json.mcpServers) ? json.mcpServers : {};
|
|
209
|
+
return { json, servers };
|
|
210
|
+
}
|
|
211
|
+
function writeShape(file: string, shape: FileShape, secret: boolean): void {
|
|
212
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
213
|
+
writeFileSync(file, JSON.stringify({ ...shape.json, mcpServers: shape.servers }, null, 2) + "\n", { mode: 0o600 });
|
|
214
|
+
if (secret && process.platform !== "win32") chmodSync(file, 0o600);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** add or replace one server; the entry is normalized first (every `${NAME}` counted as set) so the
|
|
218
|
+
* runtime is guaranteed to accept what was written. Returns the loader's view of it. `trustHome`: the
|
|
219
|
+
* human just approved this exact content on a card/prompt, so a PROJECT file is recorded as trusted in
|
|
220
|
+
* that home's store right after the write (mcp/trust.ts) — the "trust my own" half of the gate. */
|
|
221
|
+
export function writeServer(file: string, name: string, raw: Record<string, unknown>, opts: { replace?: boolean; trustHome?: string } = {}): McpServerConfig & { trusted?: boolean } {
|
|
222
|
+
const warnings: string[] = [];
|
|
223
|
+
// allowPlaceholders: this validates the SHAPE of an entry the human just approved, and an unanswered
|
|
224
|
+
// `pending` hole is part of that entry by design — writing it is how the human gets a line to edit. The
|
|
225
|
+
// loader (parseConfigFile) applies the same check without the exemption, so the server is named and
|
|
226
|
+
// skipped until it is filled rather than launched into an argument error.
|
|
227
|
+
const cfg = normalizeEntry(name, raw, file, warnings, new Proxy({}, { get: () => "set" }) as Record<string, string>, { allowPlaceholders: true });
|
|
228
|
+
if (!cfg) throw new Error(warnings.join("; ") || `${name}: not a valid server entry`);
|
|
229
|
+
const shape = readShape(file);
|
|
230
|
+
if (shape.servers[name] !== undefined && !opts.replace) throw new Error(`${file} already has a server named "${name}" — remove it first, or add --force`);
|
|
231
|
+
// the human approved THIS entry. The rest of the file is approved only if it already was (or there was
|
|
232
|
+
// no file): adding to a cloned, unapproved file must not quietly bless the strangers already in it.
|
|
233
|
+
const mayTrust = opts.trustHome !== undefined && (!existsSync(file) || Object.keys(shape.servers).filter((k) => k !== name).length === 0 || mcpTrustStatus(opts.trustHome, file) === "trusted");
|
|
234
|
+
shape.servers[name] = raw;
|
|
235
|
+
const secret = JSON.stringify(raw).includes("env") || JSON.stringify(raw).includes("headers");
|
|
236
|
+
writeShape(file, shape, secret);
|
|
237
|
+
if (opts.trustHome === undefined) return cfg;
|
|
238
|
+
if (mayTrust) trustMcpFile(opts.trustHome, file);
|
|
239
|
+
return { ...cfg, trusted: mayTrust };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** delete one server; with `trustHome` the file's new bytes stay approved when the file was approved
|
|
243
|
+
* before (a removal is the human's edit too) — an unapproved file stays unapproved */
|
|
244
|
+
export function removeServer(file: string, name: string, opts: { trustHome?: string } = {}): boolean {
|
|
245
|
+
if (!existsSync(file)) return false;
|
|
246
|
+
const wasTrusted = opts.trustHome !== undefined && mcpTrustStatus(opts.trustHome, file) === "trusted";
|
|
247
|
+
const shape = readShape(file);
|
|
248
|
+
if (shape.servers[name] === undefined) return false;
|
|
249
|
+
delete shape.servers[name];
|
|
250
|
+
writeShape(file, shape, false);
|
|
251
|
+
if (wasTrusted) trustMcpFile(opts.trustHome!, file);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** one configured server on one line, NAMES of env/headers only — never their values (they may be keys) */
|
|
256
|
+
export function serverLine(s: McpServerConfig): string {
|
|
257
|
+
const what = s.transport === "stdio" ? [s.command, ...(s.args ?? [])].join(" ") : s.url ?? "";
|
|
258
|
+
const env = s.env && Object.keys(s.env).length ? ` env ${Object.keys(s.env).join(", ")}` : "";
|
|
259
|
+
const headers = s.headers && Object.keys(s.headers).length ? ` headers ${Object.keys(s.headers).join(", ")}` : "";
|
|
260
|
+
// an entry the loader will skip until a hand edits it says so on its own line — it is configured, not launchable
|
|
261
|
+
const holes = placeholderHoles(s);
|
|
262
|
+
const fill = holes.length ? ` (fill in ${holes.join(", ")})` : "";
|
|
263
|
+
return `${s.name.padEnd(24)} ${s.transport.padEnd(5)} ${what}${env}${headers}${fill}${s.enabled === false ? " (disabled)" : ""}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** every configured server with the scope it comes from, most local last — what the files SAY, which is
|
|
267
|
+
* more than what the runtime would load: an entry still carrying a `<…>` placeholder is kept (serverLine
|
|
268
|
+
* marks it), because the person who was told "fill in the directory after the install" and typed the
|
|
269
|
+
* command the docs point at must not be told they have nothing. The loader's own view (skipping such an
|
|
270
|
+
* entry with a warning) is loadMcpConfig. `warnings` collects what parsing had to say — an unreadable
|
|
271
|
+
* file, invalid JSON, a nameless entry — for the caller to show; it used to be discarded here. */
|
|
272
|
+
export function configuredServers(cwd: string, home: string, warnings: string[] = []): { scope: McpScope | "harvest"; file: string; server: McpServerConfig }[] {
|
|
273
|
+
const files = mcpConfigFiles(cwd, home);
|
|
274
|
+
const all = new Proxy({}, { get: () => "set" }) as Record<string, string>; // list what is configured, not what is launchable right now
|
|
275
|
+
const out: { scope: McpScope | "harvest"; file: string; server: McpServerConfig }[] = [];
|
|
276
|
+
for (const [scope, file] of [["user", files.user!], ["harvest", files.harvest], ["project", files.project]] as const) {
|
|
277
|
+
for (const server of parseConfigFile(file, warnings, all, { allowPlaceholders: true })) out.push({ scope, file, server });
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The `pending` fragments of a plan whose placeholder is STILL in the written entry — nobody answered for
|
|
283
|
+
* them at the prompt (no terminal, or an empty answer). What the closing line of an install must name, and
|
|
284
|
+
* only that: a question answered at the prompt must not leave the install telling you to go and edit a line
|
|
285
|
+
* that now holds your answer. */
|
|
286
|
+
export function unfilledPending(plan: InstallPlan, raw: Record<string, unknown>): string[] {
|
|
287
|
+
const args = Array.isArray(raw.args) ? (raw.args as unknown[]) : [];
|
|
288
|
+
return plan.pending.filter((p) => pendingWords(p).some((w) => /^<.*>$/.test(w) && args.includes(w)));
|
|
289
|
+
}
|
|
Binary file
|