rovecode 0.4.0-beta.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,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
+ }