rovecode 0.4.0-beta.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -69
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -37
- 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 -512
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-dybnr56b.js +0 -2
- package/dist/cli/ask-user-p8hq4xgj.js +0 -2
- package/dist/cli/auth-login-ewpgw5sm.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-xv3ypwev.js +0 -9
- package/dist/cli/catalog-737wb2s0.js +0 -2
- package/dist/cli/cli-arhg40m0.js +0 -2
- package/dist/cli/client-cf2pxx8q.js +0 -2
- package/dist/cli/commands-3p7e4xxs.js +0 -2
- package/dist/cli/connect-3q93d7cb.js +0 -2
- package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
- package/dist/cli/context-report-hbw9zfes.js +0 -2
- package/dist/cli/count-remote-mby98cd0.js +0 -2
- package/dist/cli/design-122y0axd.js +0 -2
- package/dist/cli/dispatch-b4egzvvh.js +0 -2
- package/dist/cli/doctor-x4jkv72e.js +0 -3
- package/dist/cli/executor-ftvg6tsy.js +0 -2
- package/dist/cli/export-pdgdhkch.js +0 -2
- package/dist/cli/files-cez9a96p.js +0 -2
- package/dist/cli/gauntlet-r3xxaszc.js +0 -2
- package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
- package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
- package/dist/cli/hashline-ewg5hbe3.js +0 -2
- package/dist/cli/http-n0kehsk8.js +0 -5
- package/dist/cli/index-z5qt1s76.js +0 -2
- package/dist/cli/install-80mp63kx.js +0 -2
- package/dist/cli/loop-12twjcat.js +0 -2
- package/dist/cli/main-01pv9206.js +0 -4
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-1ztz6fkj.js +0 -10
- package/dist/cli/main-23q7cmww.js +0 -9
- package/dist/cli/main-2rzbexn2.js +0 -3
- package/dist/cli/main-2wyax8k9.js +0 -9
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2z3dek0b.js +0 -3
- package/dist/cli/main-2zgsknth.js +0 -3
- package/dist/cli/main-45ejth3a.js +0 -4
- package/dist/cli/main-45rn3trk.js +0 -22
- package/dist/cli/main-4p4e2w7x.js +0 -4
- package/dist/cli/main-4y0tnfpa.js +0 -16
- package/dist/cli/main-5py0rkmc.js +0 -4
- package/dist/cli/main-6dtqmbt6.js +0 -7
- package/dist/cli/main-6h9x282m.js +0 -4
- package/dist/cli/main-6vjeds42.js +0 -3
- package/dist/cli/main-78gq4bt9.js +0 -6
- package/dist/cli/main-7jd5vh3x.js +0 -4
- package/dist/cli/main-7kt6r53y.js +0 -4
- package/dist/cli/main-8c1tbazx.js +0 -58
- package/dist/cli/main-9a9rnh47.js +0 -19
- package/dist/cli/main-9ht36z12.js +0 -3
- package/dist/cli/main-a2yfvcy9.js +0 -7
- package/dist/cli/main-a3f51n0x.js +0 -5
- package/dist/cli/main-b8zq261k.js +0 -3
- package/dist/cli/main-bxtvnf6d.js +0 -13
- package/dist/cli/main-edxc3yzt.js +0 -4
- package/dist/cli/main-evgz4mp5.js +0 -21
- package/dist/cli/main-f33fc5je.js +0 -9
- package/dist/cli/main-fvnpq46y.js +0 -12
- package/dist/cli/main-gbbty4d4.js +0 -3
- package/dist/cli/main-gth53dnt.js +0 -25
- package/dist/cli/main-hqbz10aw.js +0 -9
- package/dist/cli/main-hrrvcfan.js +0 -38
- package/dist/cli/main-hzwtsb2m.js +0 -5
- package/dist/cli/main-j7ttv0sd.js +0 -34
- package/dist/cli/main-jak598k9.js +0 -5
- package/dist/cli/main-kba6zeyd.js +0 -6
- package/dist/cli/main-kwwsz6rq.js +0 -3
- package/dist/cli/main-m8vm17zq.js +0 -3
- package/dist/cli/main-mg4f96e1.js +0 -3
- package/dist/cli/main-mg9b20ac.js +0 -18
- package/dist/cli/main-mgb9ccnx.js +0 -3
- package/dist/cli/main-mjt2p7aj.js +0 -3
- package/dist/cli/main-n6qrdbmy.js +0 -3
- package/dist/cli/main-na7wse0x.js +0 -5
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-ntqef02r.js +0 -10
- package/dist/cli/main-nvc3yjay.js +0 -136
- package/dist/cli/main-p0cfn6nr.js +0 -16
- package/dist/cli/main-qj2djy17.js +0 -19
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-qvarybsp.js +0 -3
- package/dist/cli/main-rebtt91r.js +0 -5
- package/dist/cli/main-rpg7h8mb.js +0 -3
- package/dist/cli/main-rsy72qmw.js +0 -15
- package/dist/cli/main-rvetps99.js +0 -18
- package/dist/cli/main-s4bb0jav.js +0 -3
- package/dist/cli/main-s9v8k74e.js +0 -3
- package/dist/cli/main-tjvwmscs.js +0 -3
- package/dist/cli/main-tkgarpjj.js +0 -4
- package/dist/cli/main-v8y60bb2.js +0 -3
- package/dist/cli/main-vhrrq337.js +0 -3
- package/dist/cli/main-vp2dfb7s.js +0 -4
- package/dist/cli/main-vqbr22sz.js +0 -8
- package/dist/cli/main-vxnwe5xx.js +0 -18
- package/dist/cli/main-wgph00xf.js +0 -5
- package/dist/cli/main-wk2csfnj.js +0 -5
- package/dist/cli/main-wm997zjx.js +0 -3
- package/dist/cli/main-wpkyraxh.js +0 -3
- package/dist/cli/main-wqt32p5x.js +0 -4
- package/dist/cli/main-x9ct6y1a.js +0 -3
- package/dist/cli/main-xfekqh9m.js +0 -7
- package/dist/cli/main-xt9zc3n6.js +0 -7
- package/dist/cli/main-xx2z3zh5.js +0 -4
- package/dist/cli/main-y5c82rxr.js +0 -3
- package/dist/cli/main-yrjt2sqt.js +0 -14
- package/dist/cli/main-ys6zj3yr.js +0 -3
- package/dist/cli/main-ywbxshqc.js +0 -8
- package/dist/cli/main-z13755t8.js +0 -25
- package/dist/cli/main-zc7pyrbj.js +0 -4
- package/dist/cli/main.js +0 -279
- package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
- package/dist/cli/mcp-login-bthtfpt7.js +0 -2
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
- package/dist/cli/notify-54v5z9dz.js +0 -2
- package/dist/cli/oauth-g5gme95c.js +0 -2
- package/dist/cli/output-satndjap.js +0 -16
- package/dist/cli/profiles-sfhpbq3m.js +0 -2
- package/dist/cli/provider-config-hv3xtdt4.js +0 -2
- package/dist/cli/provider-kwzq6g84.js +0 -2
- package/dist/cli/registry-fh0hdnyn.js +0 -2
- package/dist/cli/registry-y1y8e94r.js +0 -2
- package/dist/cli/repl-t4z03mqq.js +0 -11
- package/dist/cli/resume-fqt4chg8.js +0 -2
- package/dist/cli/run-flags-rysbag9t.js +0 -2
- package/dist/cli/runtime-j19fjbsa.js +0 -2
- package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
- package/dist/cli/server-r0b6bksk.js +0 -5
- package/dist/cli/session-arg-txmn5g4x.js +0 -2
- package/dist/cli/session-ed250d9j.js +0 -2
- package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
- package/dist/cli/settings-y9rzcqx8.js +0 -2
- package/dist/cli/setup-jmbr11j0.js +0 -2
- package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
- package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
- package/dist/cli/smoke-1bg937kx.js +0 -8
- package/dist/cli/start-chat-p01cdks3.js +0 -12
- package/dist/cli/stream-4wmyaypz.js +0 -2
- package/dist/cli/task-eg4s093s.js +0 -2
- package/dist/cli/tasks-12v9rr9k.js +0 -2
- package/dist/cli/thinking-a5ngvqyh.js +0 -2
- package/dist/cli/todo-1wxpcecx.js +0 -2
- package/dist/cli/tools-2ftsya7w.js +0 -2
- package/dist/cli/tools-x1tj4fxm.js +0 -2
- package/dist/cli/trust-cmd-hccxehzb.js +0 -2
- package/dist/cli/update-check-ygt3vd7m.js +0 -2
- package/dist/cli/update-cmd-v23qhr8c.js +0 -2
- package/dist/cli/voice-g1gtck92.js +0 -2
- package/dist/cli/webfetch-0nnrjgb5.js +0 -2
- package/dist/cli/websearch-f0vr2p7d.js +0 -2
- package/dist/cli/workspace-9rq1w4ta.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -6
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/rovecode.exe +0 -4
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Model profiles: per-model-family tuning that the provider layer and the prompt assembler share.
|
|
2
|
+
*
|
|
3
|
+
* One profile answers three questions for one model family, in one place:
|
|
4
|
+
* 1. prompt — a behavioral section appended to the interactive system prompt (cli/runtime.ts
|
|
5
|
+
* buildDef). A model that was not tuned for this harness gets the operating contract spelled
|
|
6
|
+
* out: the read → edit hash protocol, act by default, parallel calls, verify before "done", …
|
|
7
|
+
* The GLM-5.3 profile's section has THREE parts: a Claude Sonnet 5 PERSONA (profile-sonnet5-
|
|
8
|
+
* persona.ts — the role Berkay asked for, carrying everything Anthropic publishes about how
|
|
9
|
+
* Sonnet 5 behaves and how Claude talks; a closed role that stays Claude Sonnet 5 even when asked
|
|
10
|
+
* directly which model it is — /status and /cost still report the real model, so the harness stays
|
|
11
|
+
* honest at the system level), then real captured Sonnet 5 VOICE examples (profile-sonnet5-voice.ts),
|
|
12
|
+
* then the harness WORKING AGREEMENT (profile-glm53.ts — tool protocol, permissions, reporting;
|
|
13
|
+
* names no model or vendor).
|
|
14
|
+
* 2. wire — extra request fields the model's endpoint wants. OpenAI-compatible adapters only
|
|
15
|
+
* (stream.ts spreads profileWire() into the /chat/completions body); the Anthropic adapter
|
|
16
|
+
* has its own dial (anthropicThinking) and no profile today — a GLM behind an Anthropic-protocol
|
|
17
|
+
* gateway gets the prompt section and nothing on the wire.
|
|
18
|
+
* 3. effort — the endpoint's vocabulary for rovecode's ONE effort dial (off|low|medium|high).
|
|
19
|
+
*
|
|
20
|
+
* Two resolvers, on purpose:
|
|
21
|
+
* - profileFor (the PROMPT): ROVECODE_PROFILE=off → none for any model; ROVECODE_PROFILE=<id> → that
|
|
22
|
+
* profile's section for EVERY model (A/B a contract on a model it was not written for; or pick the
|
|
23
|
+
* persona-free `glm-5.3-plain` variant for a GLM); unset → the first profile whose matches() accepts
|
|
24
|
+
* the model id.
|
|
25
|
+
* - wireProfileFor (the WIRE + effort words): the off-words still switch it off, but a forced id is
|
|
26
|
+
* ignored — request fields follow the model id only. An endpoint rejects fields it does not know
|
|
27
|
+
* (OpenAI has no `thinking`/`tool_stream`, and "max" is not an OpenAI effort word), so forcing the
|
|
28
|
+
* GLM contract onto gpt-5 must not turn every request into a 400.
|
|
29
|
+
* Both are provider-agnostic: the same GLM served by kaesra ("zai-org/glm-5.3-flash"), zai ("glm-5.3")
|
|
30
|
+
* or openrouter ("z-ai/glm-5.3", "z-ai/glm-5.3:free") gets one profile.
|
|
31
|
+
*
|
|
32
|
+
* Prompt override: <cwd>/.rovecode/profiles/<id>.md, else <ROVECODE_HOME>/profiles/<id>.md, REPLACES
|
|
33
|
+
* the whole built-in section (persona + agreement for glm-5.3) — iterate on the text without a rebuild;
|
|
34
|
+
* an EMPTY file drops the section and keeps the wire tuning (buildDef appends nothing, not even the
|
|
35
|
+
* separator). Read at every buildDef: once per run start, never per turn, so the system prefix stays
|
|
36
|
+
* byte-stable within a run (prompt cache).
|
|
37
|
+
*
|
|
38
|
+
* Not a port — rovecode-native. GLM-5.3 facts are Z.ai's model docs (docs.z.ai, glm-5.3 /
|
|
39
|
+
* glm-5.3-flash pages, 2026-08): thinking cannot be disabled (`thinking.type` accepts "enabled" only,
|
|
40
|
+
* `clear_thinking: false` recommended for agents), `reasoning_effort` low|high|max with max the default
|
|
41
|
+
* and the recommendation for coding, temperature 1 / top_p 0.95 suggested, `tool_stream: true` for
|
|
42
|
+
* streamed tool-call arguments. The word "medium" is not in GLM's vocabulary (an out-of-enum value
|
|
43
|
+
* silently means max), so rovecode's four levels map onto the three it has. */
|
|
44
|
+
|
|
45
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
46
|
+
import { join } from "node:path";
|
|
47
|
+
import type { ModelRef, ThinkingEffort } from "../core/types.ts";
|
|
48
|
+
import { rovecodeHome } from "./auth.ts";
|
|
49
|
+
import { GLM_53_AGENT_CONTRACT } from "./profile-glm53.ts";
|
|
50
|
+
import { SONNET_5_PERSONA } from "./profile-sonnet5-persona.ts";
|
|
51
|
+
import { SONNET_5_VOICE } from "./profile-sonnet5-voice.ts";
|
|
52
|
+
|
|
53
|
+
export { GLM_53_AGENT_CONTRACT, SONNET_5_PERSONA, SONNET_5_VOICE };
|
|
54
|
+
|
|
55
|
+
export interface ModelProfile {
|
|
56
|
+
/** stable id: ROVECODE_PROFILE value, override file name (<id>.md), the live gauntlet's banner */
|
|
57
|
+
id: string;
|
|
58
|
+
/** does this model id belong to the family? provider is deliberately not consulted */
|
|
59
|
+
matches(model: ModelRef): boolean;
|
|
60
|
+
/** built-in prompt section (markdown, `# ` headers like the base prompt's own sections) */
|
|
61
|
+
promptSection: string;
|
|
62
|
+
/** extra OpenAI-compatible request fields; `streaming` = the SSE adapter is asking */
|
|
63
|
+
wire(ctx: { streaming: boolean }): Record<string, unknown>;
|
|
64
|
+
/** the endpoint's word for rovecode's level; null = send no reasoning field (endpoint default) */
|
|
65
|
+
reasoningEffort(effort: ThinkingEffort | undefined): string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** GLM-5.3 and GLM-5.3-Flash under any vendor prefix, date suffix or `:variant`/`:tag` suffix:
|
|
69
|
+
* glm-5.3, glm-5.3-flash, zai-org/glm-5.3-flash, z-ai/glm-5.3-20260816, z-ai/glm-5.3:free,
|
|
70
|
+
* glm-5.3:latest — not glm-5.2, not glm-5.32, not glm-5.3x. */
|
|
71
|
+
export const GLM_53_MODEL_RE = /(^|[/:])glm-5\.3(?=[-:]|$)/i;
|
|
72
|
+
|
|
73
|
+
const glm53Wire = ({ streaming }: { streaming: boolean }): Record<string, unknown> => ({
|
|
74
|
+
thinking: { type: "enabled", clear_thinking: false },
|
|
75
|
+
temperature: 1,
|
|
76
|
+
top_p: 0.95,
|
|
77
|
+
...(streaming ? { tool_stream: true } : {}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// off = leave the endpoint's default alone (max: Z.ai's coding recommendation); low is the fast
|
|
81
|
+
// path; medium has no GLM word and rounds UP (a rovecode "medium" asks for more than "low" —
|
|
82
|
+
// rounding down would hand the two lower levels the same behavior)
|
|
83
|
+
const glm53Effort = (e: ThinkingEffort | undefined): string | null =>
|
|
84
|
+
e === undefined || e === "auto" || e === "off" ? null : e === "low" ? "low" : e === "medium" ? "high" : "max";
|
|
85
|
+
|
|
86
|
+
/** The default for the GLM-5.3 family: the Sonnet 5 persona, real Sonnet 5 voice examples, then the
|
|
87
|
+
* working agreement. */
|
|
88
|
+
export const GLM_53_PROFILE: ModelProfile = {
|
|
89
|
+
id: "glm-5.3",
|
|
90
|
+
matches: (m) => GLM_53_MODEL_RE.test(m.model),
|
|
91
|
+
promptSection: `${SONNET_5_PERSONA}\n\n${SONNET_5_VOICE}\n\n${GLM_53_AGENT_CONTRACT}`,
|
|
92
|
+
wire: glm53Wire,
|
|
93
|
+
reasoningEffort: glm53Effort,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The persona-free variant — the working agreement alone, same wire tuning. Opt-in only
|
|
97
|
+
* (ROVECODE_PROFILE=glm-5.3-plain): matches no model id by itself. */
|
|
98
|
+
export const GLM_53_PLAIN_PROFILE: ModelProfile = {
|
|
99
|
+
id: "glm-5.3-plain",
|
|
100
|
+
matches: () => false,
|
|
101
|
+
promptSection: GLM_53_AGENT_CONTRACT,
|
|
102
|
+
wire: glm53Wire,
|
|
103
|
+
reasoningEffort: glm53Effort,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const PROFILES: readonly ModelProfile[] = [GLM_53_PROFILE, GLM_53_PLAIN_PROFILE];
|
|
107
|
+
|
|
108
|
+
const OFF_WORDS = new Set(["off", "0", "false", "none", "no"]);
|
|
109
|
+
|
|
110
|
+
function forcedId(env: Record<string, string | undefined>): string {
|
|
111
|
+
return (env["ROVECODE_PROFILE"] ?? "").trim().toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The profile whose PROMPT section a model runs under, honoring ROVECODE_PROFILE (off | <id>). An
|
|
115
|
+
* unknown id → null, so a typo means "no profile", never a silently different one (profileHint names it). */
|
|
116
|
+
export function profileFor(model: ModelRef, env: Record<string, string | undefined> = process.env): ModelProfile | null {
|
|
117
|
+
const forced = forcedId(env);
|
|
118
|
+
if (OFF_WORDS.has(forced)) return null;
|
|
119
|
+
if (forced.length > 0) return PROFILES.find((p) => p.id === forced) ?? null;
|
|
120
|
+
return PROFILES.find((p) => p.matches(model)) ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The profile whose WIRE fields and effort words a request carries: by model id only. The off-words
|
|
124
|
+
* still disable it; a forced id does not reach the wire (see the header). */
|
|
125
|
+
export function wireProfileFor(model: ModelRef, env: Record<string, string | undefined> = process.env): ModelProfile | null {
|
|
126
|
+
if (OFF_WORDS.has(forcedId(env))) return null;
|
|
127
|
+
return PROFILES.find((p) => p.matches(model)) ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** ROVECODE_PROFILE names something that is not a profile — the one-line note the live gauntlet prints
|
|
131
|
+
* (`rovecode gauntlet --live`); the other surfaces run silently without a profile. */
|
|
132
|
+
export function profileHint(env: Record<string, string | undefined> = process.env): string | null {
|
|
133
|
+
const forced = forcedId(env);
|
|
134
|
+
if (forced.length === 0 || OFF_WORDS.has(forced) || PROFILES.some((p) => p.id === forced)) return null;
|
|
135
|
+
return `ROVECODE_PROFILE=${forced} names no profile (known: ${PROFILES.map((p) => p.id).join(", ")}, off) — running without one`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Override file paths, most specific first (project, then user scope). */
|
|
139
|
+
export function profileOverridePaths(profile: ModelProfile, cwd: string, home: string = rovecodeHome()): string[] {
|
|
140
|
+
return [join(cwd, ".rovecode", "profiles", `${profile.id}.md`), join(home, "profiles", `${profile.id}.md`)];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The section buildDef appends: the first override file that exists (trimmed; "" disables the
|
|
144
|
+
* section), else the built-in text. An unreadable file falls back to the built-in text. */
|
|
145
|
+
export function profilePromptSection(profile: ModelProfile, cwd: string, home: string = rovecodeHome()): string {
|
|
146
|
+
for (const p of profileOverridePaths(profile, cwd, home)) {
|
|
147
|
+
if (!existsSync(p)) continue;
|
|
148
|
+
try { return readFileSync(p, "utf8").trim(); } catch { return profile.promptSection; }
|
|
149
|
+
}
|
|
150
|
+
return profile.promptSection;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The request fields a model's endpoint wants beside the standard body; {} without a wire profile. */
|
|
154
|
+
export function profileWire(model: ModelRef, streaming: boolean, env: Record<string, string | undefined> = process.env): Record<string, unknown> {
|
|
155
|
+
return wireProfileFor(model, env)?.wire({ streaming }) ?? {};
|
|
156
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/** Provider configuration — the DATA half of the provider layer (no adapters, no network).
|
|
2
|
+
*
|
|
3
|
+
* Sources, merged per provider id in this order (later wins):
|
|
4
|
+
* 1. BUILTIN_PROVIDERS (the table below) scope "builtin"
|
|
5
|
+
* 2. ~/.rovecode/providers.json (ROVECODE_HOME-aware) scope "user"
|
|
6
|
+
* 3. <cwd>/.rovecode/providers.json scope "project"
|
|
7
|
+
* 4. ROVECODE_BASE_URL + ROVECODE_API_KEY scope "env" — the "custom" provider, always the default
|
|
8
|
+
*
|
|
9
|
+
* File shape (both scopes):
|
|
10
|
+
* { "default"?: "provider/model",
|
|
11
|
+
* "providers"?: { "<id>": { "baseUrl", "protocol"?, "keyEnv"?, "defaultModel"?, "models"?, "headers"?, "noKey"? } } }
|
|
12
|
+
*
|
|
13
|
+
* Secrets never live in providers.json. A provider's key is the stored credential (auth.ts,
|
|
14
|
+
* `rovecode auth set <id>`, masked prompt) else process.env[keyEnv ?? keyNameFor(id)]; `noKey: true`
|
|
15
|
+
* marks local servers (ollama, vllm) that need none. The `default` selector is "provider/model"
|
|
16
|
+
* split on the FIRST slash (router.ts grammar) — a bare provider id selects its defaultModel.
|
|
17
|
+
*
|
|
18
|
+
* Hot reload: ProviderConfig.snapshot() re-reads when the mtime or size of any source file changed
|
|
19
|
+
* (both providers.json + credentials.json), checked at most once per `throttleMs`. A provider added
|
|
20
|
+
* from another terminal — or by the agent through provider_edit — is live on the next model call;
|
|
21
|
+
* no restart. resolveProvider() (stream.ts) is the one-shot form of the same merge, so the CLI and
|
|
22
|
+
* a long-running TUI always agree on what "the default provider" is. */
|
|
23
|
+
|
|
24
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import { credentialsPath, keyNameFor, loadCredentials, rovecodeHome, type StoredCredential } from "./auth.ts";
|
|
27
|
+
|
|
28
|
+
export type ProviderProtocol = "openai" | "anthropic";
|
|
29
|
+
export type ProviderScope = "builtin" | "user" | "project" | "env";
|
|
30
|
+
/** where a provider's key came from: stored credential, env var, the ROVECODE_API_KEY pair, or nowhere */
|
|
31
|
+
export type KeySource = "stored" | "env" | "inline" | "none";
|
|
32
|
+
|
|
33
|
+
export interface ProviderSpec {
|
|
34
|
+
id: string;
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
protocol: ProviderProtocol;
|
|
37
|
+
/** env var consulted when no credential is stored; default keyNameFor(id) (models.dev name or <ID>_API_KEY) */
|
|
38
|
+
keyEnv?: string;
|
|
39
|
+
defaultModel?: string;
|
|
40
|
+
/** static model list for `/models` when the endpoint has no /models route */
|
|
41
|
+
models?: string[];
|
|
42
|
+
/** extra request headers (proxies, org ids) — sent on every call to this provider */
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
/** local servers: no key required, the provider counts as configured without one */
|
|
45
|
+
noKey?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResolvedProvider extends ProviderSpec {
|
|
49
|
+
keyEnv: string;
|
|
50
|
+
scope: ProviderScope;
|
|
51
|
+
apiKey: string | null;
|
|
52
|
+
keySource: KeySource;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const BUILTIN_PROVIDERS: readonly ProviderSpec[] = [
|
|
56
|
+
{ id: "kaesra", baseUrl: "https://api.kaesra.tech/v1", protocol: "openai", keyEnv: "KAESRA_API_KEY", defaultModel: "zai-org/glm-5.3-flash" },
|
|
57
|
+
{ id: "openai", baseUrl: "https://api.openai.com/v1", protocol: "openai", keyEnv: "OPENAI_API_KEY" },
|
|
58
|
+
{ id: "anthropic", baseUrl: "https://api.anthropic.com/v1", protocol: "anthropic", keyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5" },
|
|
59
|
+
{ id: "deepseek", baseUrl: "https://api.deepseek.com/v1", protocol: "openai", keyEnv: "DEEPSEEK_API_KEY" },
|
|
60
|
+
{ id: "groq", baseUrl: "https://api.groq.com/openai/v1", protocol: "openai", keyEnv: "GROQ_API_KEY" },
|
|
61
|
+
{ id: "openrouter", baseUrl: "https://openrouter.ai/api/v1", protocol: "openai", keyEnv: "OPENROUTER_API_KEY" },
|
|
62
|
+
{ id: "ollama", baseUrl: "http://127.0.0.1:11434/v1", protocol: "openai", keyEnv: "OLLAMA_API_KEY" },
|
|
63
|
+
{ id: "lmstudio", baseUrl: "http://127.0.0.1:1234/v1", protocol: "openai", keyEnv: "LMSTUDIO_API_KEY" },
|
|
64
|
+
{ id: "together", baseUrl: "https://api.together.xyz/v1", protocol: "openai", keyEnv: "TOGETHER_API_KEY" },
|
|
65
|
+
{ id: "mistral", baseUrl: "https://api.mistral.ai/v1", protocol: "openai", keyEnv: "MISTRAL_API_KEY" },
|
|
66
|
+
{ id: "cerebras", baseUrl: "https://api.cerebras.ai/v1", protocol: "openai", keyEnv: "CEREBRAS_API_KEY" },
|
|
67
|
+
{ id: "fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", protocol: "openai", keyEnv: "FIREWORKS_API_KEY" },
|
|
68
|
+
{ id: "perplexity", baseUrl: "https://api.perplexity.ai", protocol: "openai", keyEnv: "PERPLEXITY_API_KEY" },
|
|
69
|
+
{ id: "xai", baseUrl: "https://api.x.ai/v1", protocol: "openai", keyEnv: "XAI_API_KEY" },
|
|
70
|
+
{ id: "moondream", baseUrl: "https://api.moondream.ai/v1", protocol: "openai", keyEnv: "MOONDREAM_API_KEY" },
|
|
71
|
+
{ id: "vllm", baseUrl: "http://127.0.0.1:8000/v1", protocol: "openai", keyEnv: "VLLM_API_KEY" },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// ---------- files ----------
|
|
75
|
+
|
|
76
|
+
/** one providers.json entry as written: baseUrl required, protocol may be omitted (inferred on read) */
|
|
77
|
+
export interface ProviderEntry extends Partial<Omit<ProviderSpec, "id" | "baseUrl">> { baseUrl: string }
|
|
78
|
+
|
|
79
|
+
export interface ProvidersFile {
|
|
80
|
+
default?: string;
|
|
81
|
+
providers?: Record<string, ProviderEntry>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type FileScope = "user" | "project";
|
|
85
|
+
|
|
86
|
+
export function userProvidersPath(): string { return join(rovecodeHome(), "providers.json"); }
|
|
87
|
+
export function projectProvidersPath(cwd: string): string { return join(cwd, ".rovecode", "providers.json"); }
|
|
88
|
+
export function providersPathFor(scope: FileScope, cwd: string): string {
|
|
89
|
+
return scope === "project" ? projectProvidersPath(cwd) : userProvidersPath();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** ids are lowercase slugs — they name env vars (keyNameFor) and appear in "provider/model" selectors */
|
|
93
|
+
export const PROVIDER_ID_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/;
|
|
94
|
+
const ENV_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
95
|
+
|
|
96
|
+
export function inferProtocol(baseUrl: string): ProviderProtocol {
|
|
97
|
+
return baseUrl.includes("anthropic.com") ? "anthropic" : "openai";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Validate one provider entry (file entry or CLI/tool input) into a spec. Pure; never throws. */
|
|
101
|
+
export function validateSpec(id: string, raw: unknown): { spec: ProviderSpec } | { error: string } {
|
|
102
|
+
if (!PROVIDER_ID_RE.test(id)) return { error: `provider id "${id}" must be a lowercase slug (a-z 0-9 _ . -), at most 40 chars` };
|
|
103
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { error: `provider "${id}": entry must be an object` };
|
|
104
|
+
const r = raw as Record<string, unknown>;
|
|
105
|
+
const baseUrl = typeof r["baseUrl"] === "string" ? r["baseUrl"].trim().replace(/\/+$/, "") : "";
|
|
106
|
+
if (!/^https?:\/\/[^\s/]+/.test(baseUrl)) return { error: `provider "${id}": baseUrl must be an http(s) URL` };
|
|
107
|
+
const protocol = r["protocol"] === undefined ? inferProtocol(baseUrl) : r["protocol"];
|
|
108
|
+
if (protocol !== "openai" && protocol !== "anthropic") return { error: `provider "${id}": protocol must be "openai" or "anthropic"` };
|
|
109
|
+
const spec: ProviderSpec = { id, baseUrl, protocol };
|
|
110
|
+
if (r["keyEnv"] !== undefined) {
|
|
111
|
+
if (typeof r["keyEnv"] !== "string" || !ENV_NAME_RE.test(r["keyEnv"])) return { error: `provider "${id}": keyEnv must be an ENV_VAR_NAME` };
|
|
112
|
+
spec.keyEnv = r["keyEnv"];
|
|
113
|
+
}
|
|
114
|
+
if (r["defaultModel"] !== undefined) {
|
|
115
|
+
if (typeof r["defaultModel"] !== "string" || r["defaultModel"].trim().length === 0) return { error: `provider "${id}": defaultModel must be a non-empty string` };
|
|
116
|
+
spec.defaultModel = r["defaultModel"].trim();
|
|
117
|
+
}
|
|
118
|
+
if (r["models"] !== undefined) {
|
|
119
|
+
if (!Array.isArray(r["models"]) || !r["models"].every((m) => typeof m === "string" && m.trim().length > 0)) return { error: `provider "${id}": models must be a list of model ids` };
|
|
120
|
+
spec.models = (r["models"] as string[]).map((m) => m.trim());
|
|
121
|
+
}
|
|
122
|
+
if (r["headers"] !== undefined) {
|
|
123
|
+
const h = r["headers"];
|
|
124
|
+
if (typeof h !== "object" || h === null || Array.isArray(h) || !Object.values(h as Record<string, unknown>).every((v) => typeof v === "string")) return { error: `provider "${id}": headers must be an object of strings` };
|
|
125
|
+
spec.headers = { ...(h as Record<string, string>) };
|
|
126
|
+
}
|
|
127
|
+
if (r["noKey"] !== undefined) {
|
|
128
|
+
if (typeof r["noKey"] !== "boolean") return { error: `provider "${id}": noKey must be true or false` };
|
|
129
|
+
if (r["noKey"]) spec.noKey = true;
|
|
130
|
+
}
|
|
131
|
+
return { spec };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Read one providers.json: a missing file is {} silently; a corrupt file or a malformed entry is
|
|
135
|
+
* dropped with a warning, never fatal (auth.ts loadCredentials idiom). */
|
|
136
|
+
export function readProvidersFile(path: string): { data: ProvidersFile; warnings: string[] } {
|
|
137
|
+
let text: string;
|
|
138
|
+
try { text = readFileSync(path, "utf8"); } catch { return { data: {}, warnings: [] }; }
|
|
139
|
+
let parsed: unknown;
|
|
140
|
+
try { parsed = JSON.parse(text); } catch { return { data: {}, warnings: [`${path}: invalid JSON — ignored`] }; }
|
|
141
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { data: {}, warnings: [`${path}: expected an object — ignored`] };
|
|
142
|
+
const o = parsed as Record<string, unknown>;
|
|
143
|
+
const data: ProvidersFile = {};
|
|
144
|
+
const warnings: string[] = [];
|
|
145
|
+
if (o["default"] !== undefined) {
|
|
146
|
+
if (typeof o["default"] === "string" && o["default"].trim().length > 0) data.default = o["default"].trim();
|
|
147
|
+
else warnings.push(`${path}: "default" must be "provider/model" — ignored`);
|
|
148
|
+
}
|
|
149
|
+
if (o["providers"] !== undefined) {
|
|
150
|
+
const p = o["providers"];
|
|
151
|
+
if (typeof p !== "object" || p === null || Array.isArray(p)) warnings.push(`${path}: "providers" must be an object — ignored`);
|
|
152
|
+
else {
|
|
153
|
+
data.providers = {};
|
|
154
|
+
for (const [id, raw] of Object.entries(p as Record<string, unknown>)) {
|
|
155
|
+
const v = validateSpec(id, raw);
|
|
156
|
+
if ("error" in v) { warnings.push(`${path}: ${v.error} — ignored`); continue; }
|
|
157
|
+
const { id: _id, ...rest } = v.spec;
|
|
158
|
+
data.providers[id] = rest;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { data, warnings };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function writeProvidersFile(path: string, data: ProvidersFile): void {
|
|
166
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
167
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------- merge ----------
|
|
171
|
+
|
|
172
|
+
export interface ProviderSnapshot {
|
|
173
|
+
/** builtins in table order, then file-only providers in file order, then the env "custom" provider */
|
|
174
|
+
providers: ResolvedProvider[];
|
|
175
|
+
/** the `default` selector — project file beats user file */
|
|
176
|
+
defaultSelector?: string;
|
|
177
|
+
defaultScope?: FileScope;
|
|
178
|
+
warnings: string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resolveKey(spec: ProviderSpec, scope: ProviderScope, env: NodeJS.ProcessEnv, creds: Record<string, StoredCredential>): ResolvedProvider {
|
|
182
|
+
const keyEnv = spec.keyEnv ?? keyNameFor(spec.id);
|
|
183
|
+
const stored = creds[spec.id]?.key;
|
|
184
|
+
if (stored !== undefined && stored.trim().length > 0) return { ...spec, keyEnv, scope, apiKey: stored, keySource: "stored" };
|
|
185
|
+
const fromEnv = env[keyEnv];
|
|
186
|
+
if (fromEnv !== undefined && fromEnv.trim().length > 0) return { ...spec, keyEnv, scope, apiKey: fromEnv, keySource: "env" };
|
|
187
|
+
return { ...spec, keyEnv, scope, apiKey: null, keySource: "none" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The ROVECODE_BASE_URL/ROVECODE_API_KEY pair (ROVECODE_API_KEY falls back to OPENAI_API_KEY, as before). */
|
|
191
|
+
export function envCustomProvider(env: NodeJS.ProcessEnv): ResolvedProvider | null {
|
|
192
|
+
const base = env["ROVECODE_BASE_URL"];
|
|
193
|
+
const key = env["ROVECODE_API_KEY"] ?? env["OPENAI_API_KEY"];
|
|
194
|
+
if (!base || !key) return null;
|
|
195
|
+
return {
|
|
196
|
+
id: "custom", baseUrl: base, protocol: inferProtocol(base), keyEnv: "ROVECODE_API_KEY", scope: "env", apiKey: key, keySource: "inline",
|
|
197
|
+
...(env["ROVECODE_MODEL"] ? { defaultModel: env["ROVECODE_MODEL"] } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function isConfigured(p: ResolvedProvider): boolean {
|
|
202
|
+
return p.apiKey !== null || p.noKey === true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function buildSnapshot(cwd: string, env: NodeJS.ProcessEnv = process.env): ProviderSnapshot {
|
|
206
|
+
const warnings: string[] = [];
|
|
207
|
+
const order: string[] = [];
|
|
208
|
+
const specs = new Map<string, { spec: ProviderSpec; scope: ProviderScope }>();
|
|
209
|
+
for (const b of BUILTIN_PROVIDERS) { specs.set(b.id, { spec: b, scope: "builtin" }); order.push(b.id); }
|
|
210
|
+
let defaultSelector: string | undefined;
|
|
211
|
+
let defaultScope: FileScope | undefined;
|
|
212
|
+
const layer = (path: string, scope: FileScope): void => {
|
|
213
|
+
const { data, warnings: w } = readProvidersFile(path);
|
|
214
|
+
warnings.push(...w);
|
|
215
|
+
for (const [id, rest] of Object.entries(data.providers ?? {})) {
|
|
216
|
+
const prev = specs.get(id);
|
|
217
|
+
specs.set(id, { spec: { ...(prev?.spec ?? {}), ...rest, id } as ProviderSpec, scope });
|
|
218
|
+
if (prev === undefined) order.push(id);
|
|
219
|
+
}
|
|
220
|
+
if (data.default !== undefined) { defaultSelector = data.default; defaultScope = scope; }
|
|
221
|
+
};
|
|
222
|
+
layer(userProvidersPath(), "user");
|
|
223
|
+
layer(projectProvidersPath(cwd), "project");
|
|
224
|
+
const creds = loadCredentials();
|
|
225
|
+
const providers = order.map((id) => { const e = specs.get(id)!; return resolveKey(e.spec, e.scope, env, creds); });
|
|
226
|
+
const custom = envCustomProvider(env);
|
|
227
|
+
if (custom) providers.push(custom);
|
|
228
|
+
return { providers, ...(defaultSelector !== undefined ? { defaultSelector, defaultScope } : {}), warnings };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------- selection ----------
|
|
232
|
+
|
|
233
|
+
export interface Selection { provider: string; model?: string }
|
|
234
|
+
|
|
235
|
+
/** "provider/model" split on the FIRST slash so model ids keep their own slashes; a bare id is a provider. */
|
|
236
|
+
export function parseSelector(selector: string): Selection {
|
|
237
|
+
const s = selector.trim();
|
|
238
|
+
const i = s.indexOf("/");
|
|
239
|
+
if (i <= 0) return { provider: s };
|
|
240
|
+
const model = s.slice(i + 1).trim();
|
|
241
|
+
return model.length > 0 ? { provider: s.slice(0, i), model } : { provider: s.slice(0, i) };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface DefaultPick { provider: ResolvedProvider; model?: string; via: "env-pair" | "default" | "stored" | "env-key" | "no-key" }
|
|
245
|
+
|
|
246
|
+
/** The default provider (+ its model, when one is known WITHOUT env ROVECODE_MODEL — callers layer
|
|
247
|
+
* that on top): env pair > file `default` (when that provider is configured) > first stored
|
|
248
|
+
* credential in order > first env key in order > first keyless file provider > null. The first
|
|
249
|
+
* three ranks are resolveProvider()'s historical precedence, pinned by test/unit/auth.test.ts. */
|
|
250
|
+
export function pickDefault(snap: ProviderSnapshot): DefaultPick | null {
|
|
251
|
+
const custom = snap.providers.find((p) => p.scope === "env");
|
|
252
|
+
if (custom) return { provider: custom, model: custom.defaultModel, via: "env-pair" };
|
|
253
|
+
if (snap.defaultSelector !== undefined) {
|
|
254
|
+
const sel = parseSelector(snap.defaultSelector);
|
|
255
|
+
const p = snap.providers.find((x) => x.id === sel.provider);
|
|
256
|
+
if (p && isConfigured(p)) return { provider: p, model: sel.model ?? p.defaultModel, via: "default" };
|
|
257
|
+
}
|
|
258
|
+
const stored = snap.providers.find((p) => p.keySource === "stored");
|
|
259
|
+
if (stored) return { provider: stored, model: stored.defaultModel, via: "stored" };
|
|
260
|
+
const fromEnv = snap.providers.find((p) => p.keySource === "env");
|
|
261
|
+
if (fromEnv) return { provider: fromEnv, model: fromEnv.defaultModel, via: "env-key" };
|
|
262
|
+
const noKey = snap.providers.find((p) => p.noKey === true);
|
|
263
|
+
if (noKey) return { provider: noKey, model: noKey.defaultModel, via: "no-key" };
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------- live view (hot reload) ----------
|
|
268
|
+
|
|
269
|
+
export class ProviderConfig {
|
|
270
|
+
private snap: ProviderSnapshot | null = null;
|
|
271
|
+
private stamp = "";
|
|
272
|
+
private checkedAt = -Infinity;
|
|
273
|
+
private readonly listeners = new Set<(snap: ProviderSnapshot) => void>();
|
|
274
|
+
|
|
275
|
+
constructor(
|
|
276
|
+
private readonly cwd: string,
|
|
277
|
+
private readonly env: NodeJS.ProcessEnv = process.env,
|
|
278
|
+
private readonly throttleMs = 250,
|
|
279
|
+
) {}
|
|
280
|
+
|
|
281
|
+
/** the files whose mtime/size drive reloads */
|
|
282
|
+
sourcePaths(): string[] { return [userProvidersPath(), projectProvidersPath(this.cwd), credentialsPath()]; }
|
|
283
|
+
|
|
284
|
+
private stampNow(): string {
|
|
285
|
+
return this.sourcePaths().map((p) => { try { const s = statSync(p); return `${s.mtimeMs}:${s.size}`; } catch { return "-"; } }).join("|");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Current merged view. Re-reads when a source file changed (checked at most every throttleMs);
|
|
289
|
+
* listeners fire on every rebuild after the first. */
|
|
290
|
+
snapshot(): ProviderSnapshot {
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
if (this.snap !== null && now - this.checkedAt < this.throttleMs) return this.snap;
|
|
293
|
+
this.checkedAt = now;
|
|
294
|
+
const stamp = this.stampNow();
|
|
295
|
+
if (this.snap === null || stamp !== this.stamp) {
|
|
296
|
+
const first = this.snap === null;
|
|
297
|
+
this.stamp = stamp;
|
|
298
|
+
this.snap = buildSnapshot(this.cwd, this.env);
|
|
299
|
+
if (!first) for (const cb of this.listeners) cb(this.snap);
|
|
300
|
+
}
|
|
301
|
+
return this.snap;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Force a re-read on the next snapshot() (in-process writers call this right after writing). */
|
|
305
|
+
invalidate(): void { this.checkedAt = -Infinity; this.stamp = ""; }
|
|
306
|
+
|
|
307
|
+
onChange(cb: (snap: ProviderSnapshot) => void): () => void {
|
|
308
|
+
this.listeners.add(cb);
|
|
309
|
+
return () => { this.listeners.delete(cb); };
|
|
310
|
+
}
|
|
311
|
+
}
|