rovecode 0.4.0-beta.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (428) hide show
  1. package/README.md +67 -69
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -37
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -512
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-dybnr56b.js +0 -2
  272. package/dist/cli/ask-user-p8hq4xgj.js +0 -2
  273. package/dist/cli/auth-login-ewpgw5sm.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-xv3ypwev.js +0 -9
  276. package/dist/cli/catalog-737wb2s0.js +0 -2
  277. package/dist/cli/cli-arhg40m0.js +0 -2
  278. package/dist/cli/client-cf2pxx8q.js +0 -2
  279. package/dist/cli/commands-3p7e4xxs.js +0 -2
  280. package/dist/cli/connect-3q93d7cb.js +0 -2
  281. package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
  282. package/dist/cli/context-report-hbw9zfes.js +0 -2
  283. package/dist/cli/count-remote-mby98cd0.js +0 -2
  284. package/dist/cli/design-122y0axd.js +0 -2
  285. package/dist/cli/dispatch-b4egzvvh.js +0 -2
  286. package/dist/cli/doctor-x4jkv72e.js +0 -3
  287. package/dist/cli/executor-ftvg6tsy.js +0 -2
  288. package/dist/cli/export-pdgdhkch.js +0 -2
  289. package/dist/cli/files-cez9a96p.js +0 -2
  290. package/dist/cli/gauntlet-r3xxaszc.js +0 -2
  291. package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
  292. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
  293. package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
  294. package/dist/cli/hashline-ewg5hbe3.js +0 -2
  295. package/dist/cli/http-n0kehsk8.js +0 -5
  296. package/dist/cli/index-z5qt1s76.js +0 -2
  297. package/dist/cli/install-80mp63kx.js +0 -2
  298. package/dist/cli/loop-12twjcat.js +0 -2
  299. package/dist/cli/main-01pv9206.js +0 -4
  300. package/dist/cli/main-0jys2ccn.js +0 -3
  301. package/dist/cli/main-1ztz6fkj.js +0 -10
  302. package/dist/cli/main-23q7cmww.js +0 -9
  303. package/dist/cli/main-2rzbexn2.js +0 -3
  304. package/dist/cli/main-2wyax8k9.js +0 -9
  305. package/dist/cli/main-2yeveeve.js +0 -6
  306. package/dist/cli/main-2z3dek0b.js +0 -3
  307. package/dist/cli/main-2zgsknth.js +0 -3
  308. package/dist/cli/main-45ejth3a.js +0 -4
  309. package/dist/cli/main-45rn3trk.js +0 -22
  310. package/dist/cli/main-4p4e2w7x.js +0 -4
  311. package/dist/cli/main-4y0tnfpa.js +0 -16
  312. package/dist/cli/main-5py0rkmc.js +0 -4
  313. package/dist/cli/main-6dtqmbt6.js +0 -7
  314. package/dist/cli/main-6h9x282m.js +0 -4
  315. package/dist/cli/main-6vjeds42.js +0 -3
  316. package/dist/cli/main-78gq4bt9.js +0 -6
  317. package/dist/cli/main-7jd5vh3x.js +0 -4
  318. package/dist/cli/main-7kt6r53y.js +0 -4
  319. package/dist/cli/main-8c1tbazx.js +0 -58
  320. package/dist/cli/main-9a9rnh47.js +0 -19
  321. package/dist/cli/main-9ht36z12.js +0 -3
  322. package/dist/cli/main-a2yfvcy9.js +0 -7
  323. package/dist/cli/main-a3f51n0x.js +0 -5
  324. package/dist/cli/main-b8zq261k.js +0 -3
  325. package/dist/cli/main-bxtvnf6d.js +0 -13
  326. package/dist/cli/main-edxc3yzt.js +0 -4
  327. package/dist/cli/main-evgz4mp5.js +0 -21
  328. package/dist/cli/main-f33fc5je.js +0 -9
  329. package/dist/cli/main-fvnpq46y.js +0 -12
  330. package/dist/cli/main-gbbty4d4.js +0 -3
  331. package/dist/cli/main-gth53dnt.js +0 -25
  332. package/dist/cli/main-hqbz10aw.js +0 -9
  333. package/dist/cli/main-hrrvcfan.js +0 -38
  334. package/dist/cli/main-hzwtsb2m.js +0 -5
  335. package/dist/cli/main-j7ttv0sd.js +0 -34
  336. package/dist/cli/main-jak598k9.js +0 -5
  337. package/dist/cli/main-kba6zeyd.js +0 -6
  338. package/dist/cli/main-kwwsz6rq.js +0 -3
  339. package/dist/cli/main-m8vm17zq.js +0 -3
  340. package/dist/cli/main-mg4f96e1.js +0 -3
  341. package/dist/cli/main-mg9b20ac.js +0 -18
  342. package/dist/cli/main-mgb9ccnx.js +0 -3
  343. package/dist/cli/main-mjt2p7aj.js +0 -3
  344. package/dist/cli/main-n6qrdbmy.js +0 -3
  345. package/dist/cli/main-na7wse0x.js +0 -5
  346. package/dist/cli/main-nqveez48.js +0 -4
  347. package/dist/cli/main-ntqef02r.js +0 -10
  348. package/dist/cli/main-nvc3yjay.js +0 -136
  349. package/dist/cli/main-p0cfn6nr.js +0 -16
  350. package/dist/cli/main-qj2djy17.js +0 -19
  351. package/dist/cli/main-qsevpgsv.js +0 -3
  352. package/dist/cli/main-qvarybsp.js +0 -3
  353. package/dist/cli/main-rebtt91r.js +0 -5
  354. package/dist/cli/main-rpg7h8mb.js +0 -3
  355. package/dist/cli/main-rsy72qmw.js +0 -15
  356. package/dist/cli/main-rvetps99.js +0 -18
  357. package/dist/cli/main-s4bb0jav.js +0 -3
  358. package/dist/cli/main-s9v8k74e.js +0 -3
  359. package/dist/cli/main-tjvwmscs.js +0 -3
  360. package/dist/cli/main-tkgarpjj.js +0 -4
  361. package/dist/cli/main-v8y60bb2.js +0 -3
  362. package/dist/cli/main-vhrrq337.js +0 -3
  363. package/dist/cli/main-vp2dfb7s.js +0 -4
  364. package/dist/cli/main-vqbr22sz.js +0 -8
  365. package/dist/cli/main-vxnwe5xx.js +0 -18
  366. package/dist/cli/main-wgph00xf.js +0 -5
  367. package/dist/cli/main-wk2csfnj.js +0 -5
  368. package/dist/cli/main-wm997zjx.js +0 -3
  369. package/dist/cli/main-wpkyraxh.js +0 -3
  370. package/dist/cli/main-wqt32p5x.js +0 -4
  371. package/dist/cli/main-x9ct6y1a.js +0 -3
  372. package/dist/cli/main-xfekqh9m.js +0 -7
  373. package/dist/cli/main-xt9zc3n6.js +0 -7
  374. package/dist/cli/main-xx2z3zh5.js +0 -4
  375. package/dist/cli/main-y5c82rxr.js +0 -3
  376. package/dist/cli/main-yrjt2sqt.js +0 -14
  377. package/dist/cli/main-ys6zj3yr.js +0 -3
  378. package/dist/cli/main-ywbxshqc.js +0 -8
  379. package/dist/cli/main-z13755t8.js +0 -25
  380. package/dist/cli/main-zc7pyrbj.js +0 -4
  381. package/dist/cli/main.js +0 -279
  382. package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
  383. package/dist/cli/mcp-login-bthtfpt7.js +0 -2
  384. package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
  385. package/dist/cli/notify-54v5z9dz.js +0 -2
  386. package/dist/cli/oauth-g5gme95c.js +0 -2
  387. package/dist/cli/output-satndjap.js +0 -16
  388. package/dist/cli/profiles-sfhpbq3m.js +0 -2
  389. package/dist/cli/provider-config-hv3xtdt4.js +0 -2
  390. package/dist/cli/provider-kwzq6g84.js +0 -2
  391. package/dist/cli/registry-fh0hdnyn.js +0 -2
  392. package/dist/cli/registry-y1y8e94r.js +0 -2
  393. package/dist/cli/repl-t4z03mqq.js +0 -11
  394. package/dist/cli/resume-fqt4chg8.js +0 -2
  395. package/dist/cli/run-flags-rysbag9t.js +0 -2
  396. package/dist/cli/runtime-j19fjbsa.js +0 -2
  397. package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
  398. package/dist/cli/server-r0b6bksk.js +0 -5
  399. package/dist/cli/session-arg-txmn5g4x.js +0 -2
  400. package/dist/cli/session-ed250d9j.js +0 -2
  401. package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
  402. package/dist/cli/settings-y9rzcqx8.js +0 -2
  403. package/dist/cli/setup-jmbr11j0.js +0 -2
  404. package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
  405. package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
  406. package/dist/cli/smoke-1bg937kx.js +0 -8
  407. package/dist/cli/start-chat-p01cdks3.js +0 -12
  408. package/dist/cli/stream-4wmyaypz.js +0 -2
  409. package/dist/cli/task-eg4s093s.js +0 -2
  410. package/dist/cli/tasks-12v9rr9k.js +0 -2
  411. package/dist/cli/thinking-a5ngvqyh.js +0 -2
  412. package/dist/cli/todo-1wxpcecx.js +0 -2
  413. package/dist/cli/tools-2ftsya7w.js +0 -2
  414. package/dist/cli/tools-x1tj4fxm.js +0 -2
  415. package/dist/cli/trust-cmd-hccxehzb.js +0 -2
  416. package/dist/cli/update-check-ygt3vd7m.js +0 -2
  417. package/dist/cli/update-cmd-v23qhr8c.js +0 -2
  418. package/dist/cli/voice-g1gtck92.js +0 -2
  419. package/dist/cli/webfetch-0nnrjgb5.js +0 -2
  420. package/dist/cli/websearch-f0vr2p7d.js +0 -2
  421. package/dist/cli/workspace-9rq1w4ta.js +0 -2
  422. package/dist/lib/index.js +0 -62
  423. package/dist/lib/models-index.json +0 -1
  424. package/dist/lib/plugins.js +0 -6
  425. package/dist/lib/providers.js +0 -17
  426. package/dist/lib/public-api.js +0 -20
  427. package/dist/rovecode.exe +0 -4
  428. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,569 @@
1
+ /** Providers: StreamFn adapters (ADR-003 seam). Errors cross as stopReason "error", never throws.
2
+ *
3
+ * Wire protocols supported:
4
+ * - OpenAI-compatible /chat/completions (JSON + SSE streaming), with tools
5
+ * - Anthropic /messages (x-api-key), with tools
6
+ * - Any custom base URL + key (ROVECODE_BASE_URL/ROVECODE_API_KEY or explicit config)
7
+ * Model catalogs are FETCHED from the endpoint (/v1/models) — never hard-coded (pi pattern).
8
+ * Error-turn shaping (abort vs error; HTTP status + Retry-After side-channel, port #23) lives in stream-errors.ts.
9
+ * Message lowering (harness parts → wire content, incl. port #34 image blocks) lives in wire-messages.ts. */
10
+
11
+ import type { StreamFn, Message, AssistantTurn, StreamEvent, ModelRef, StopReason } from "../core/types.ts";
12
+ import { partsText } from "../core/loop.ts";
13
+ import { applyAnthropicCacheBoundaries } from "./cache.ts";
14
+ import { normalizeUsage } from "../core/usage.ts";
15
+ import { BUILTIN_PROVIDERS, buildSnapshot, pickDefault } from "./provider-config.ts";
16
+ import { failedTurn, fetchFirstByte, httpErrorTurn } from "./stream-errors.ts";
17
+ import { supportsImages } from "./catalog.ts";
18
+ import { profileWire } from "./profiles.ts";
19
+ import { anthropicThinking, thinkingBudget, thinkingPlan, type AnthropicThinkingShape } from "./thinking.ts";
20
+ import { toOpenAiMessages, toAnthropicMessages, toOpenAiToolSchemas, asToolSchema, type WireOptions } from "./wire-messages.ts";
21
+
22
+ export { toOpenAiMessages, toAnthropicMessages, toOpenAiToolSchemas } from "./wire-messages.ts";
23
+
24
+ /** port #34: image parts go on the wire as image blocks unless the models.dev catalog says the
25
+ * model has no image input (then wire-messages.ts substitutes a text placeholder); an unknown
26
+ * model is given the image (catalog.ts supportsImages). */
27
+ const wireOptions = (model: ModelRef): WireOptions => ({ vision: supportsImages(model) !== false });
28
+
29
+ export interface ProviderConfig {
30
+ id: string; // provider id, e.g. "kaesra"
31
+ baseUrl: string; // e.g. https://api.kaesra.tech/v1
32
+ apiKey: string;
33
+ protocol: "openai" | "anthropic";
34
+ defaultModel?: string;
35
+ /** extra request headers (proxies, org ids) — providers.json `headers`; the protocol's own auth headers win */
36
+ headers?: Record<string, string>;
37
+ }
38
+
39
+ /** What a wire adapter needs: endpoint root, key, optional extra headers (ProviderConfig.headers). */
40
+ export interface AdapterOptions {
41
+ baseUrl: string;
42
+ apiKey: string;
43
+ headers?: Record<string, string>;
44
+ }
45
+
46
+ export interface ModelCatalogEntry {
47
+ id: string;
48
+ ownedBy?: string;
49
+ type?: string;
50
+ }
51
+
52
+ // ---------- catalog (fetched, cached) ----------
53
+
54
+ const catalogCache = new Map<string, { models: ModelCatalogEntry[]; fetchedAt: number }>();
55
+ const CATALOG_TTL_MS = 5 * 60_000;
56
+
57
+ export async function fetchModels(cfg: ProviderConfig, force = false): Promise<ModelCatalogEntry[]> {
58
+ const hit = catalogCache.get(cfg.id);
59
+ if (!force && hit && Date.now() - hit.fetchedAt < CATALOG_TTL_MS) return hit.models;
60
+ const url = cfg.baseUrl.replace(/\/$/, "") + "/models";
61
+ let models: ModelCatalogEntry[] = [];
62
+ try {
63
+ const res = await fetch(url, { headers: authHeaders(cfg) });
64
+ if (res.ok) {
65
+ const json = (await res.json()) as { data?: { id: string; owned_by?: string; type?: string }[] };
66
+ models = (json.data ?? []).map((m) => ({ id: m.id, ownedBy: m.owned_by, type: m.type }));
67
+ }
68
+ } catch { /* errors are empty catalog — provider seam never throws */ }
69
+ catalogCache.set(cfg.id, { models, fetchedAt: Date.now() });
70
+ return models;
71
+ }
72
+
73
+ function authHeaders(cfg: ProviderConfig): Record<string, string> {
74
+ return {
75
+ ...(cfg.headers ?? {}),
76
+ ...(cfg.protocol === "anthropic"
77
+ ? { "x-api-key": cfg.apiKey, "anthropic-version": "2023-06-01" }
78
+ : { authorization: `Bearer ${cfg.apiKey}` }),
79
+ };
80
+ }
81
+
82
+ // ---------- factories ----------
83
+
84
+ export function providerStream(cfg: ProviderConfig): StreamFn {
85
+ const opts: AdapterOptions = { baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, ...(cfg.headers !== undefined ? { headers: cfg.headers } : {}) };
86
+ return cfg.protocol === "anthropic" ? anthropicStream(opts) : openaiCompatStream(opts);
87
+ }
88
+
89
+ /** The streaming twin of providerStream: text_delta as the model writes, the same final turn.
90
+ * This is what every surface gets by default — the one-shot JSON adapters above are the fallback. */
91
+ export function providerStreaming(cfg: ProviderConfig): StreamFn {
92
+ const opts: AdapterOptions = { baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, ...(cfg.headers !== undefined ? { headers: cfg.headers } : {}) };
93
+ return cfg.protocol === "anthropic" ? anthropicStreaming(opts) : openaiCompatStreaming(opts);
94
+ }
95
+
96
+ /** Streaming is ON unless ROVECODE_STREAM says otherwise: `off`/`json`/`0`/`false`/`none` fall back to
97
+ * the one-shot JSON adapters (a proxy with no SSE route, a recording harness that wants one body).
98
+ * `sse` is still accepted — it used to be the opt-IN spelling and some scripts still set it. */
99
+ export function wantsStreaming(env: { ROVECODE_STREAM?: string | undefined }): boolean {
100
+ const v = (env.ROVECODE_STREAM ?? "").trim().toLowerCase();
101
+ return !(v === "off" || v === "json" || v === "0" || v === "false" || v === "none");
102
+ }
103
+
104
+ /** Streaming requests ask for an UNCOMPRESSED body. Measured against the Anthropic endpoint: with the
105
+ * default `accept-encoding: gzip, deflate, br`, the decompressor holds the whole SSE stream and every
106
+ * event lands in one burst at the end (first text delta at 11.1 s of an 11.1 s response); with
107
+ * `identity` the same prompt starts writing at 1.4 s. Streaming is the point of these two adapters, so
108
+ * the extra bytes are the right trade — the JSON adapters below keep compression. */
109
+ const SSE_HEADERS = { "accept-encoding": "identity" } as const;
110
+
111
+ /** A proxy that ignores `stream: true` and answers with one JSON body would otherwise leave the SSE
112
+ * reader with nothing to parse and the turn empty. Content-type decides: an explicit application/json
113
+ * is read whole through the same parser the one-shot adapters use (no deltas — there is nothing to
114
+ * stream), anything else is treated as an event stream. */
115
+ const isJsonBody = (res: Response): boolean => (res.headers.get("content-type") ?? "").includes("application/json");
116
+
117
+ /** Thinking, per protocol.
118
+ *
119
+ * Anthropic has TWO request shapes and no model exposes both reliably — measured against the live
120
+ * API on 2026-09-03:
121
+ *
122
+ * model output_config.effort thinking.type=enabled
123
+ * claude-opus-5 200 400 "not supported for this model"
124
+ * claude-sonnet-5 200 400
125
+ * claude-opus-4-5 200 200
126
+ * claude-sonnet-4-5 400 "does not support…" 200 → [thinking, text]
127
+ * claude-haiku-4-5 400 200
128
+ *
129
+ * So the shape is a property of the model, and guessing wrong is a hard 400, not a downgrade. There
130
+ * is no capability field to read (the /models list carries ids only), and a hard-coded table would
131
+ * rot with the next release — this file's rule is that catalogs are fetched, never hard-coded. So:
132
+ * try the newer shape, and when the endpoint says that shape is unsupported, flip and retry ONCE,
133
+ * remembering the answer per model id for the rest of the process. One wasted round trip the first
134
+ * time a model is used, never again.
135
+ *
136
+ * `off` is not "send nothing": opus-5 thinks by DEFAULT (measured — a bare request streams
137
+ * thinking_delta), so off has to say so with thinking.type=disabled.
138
+ *
139
+ * Extended thinking and tool use: the thinking blocks are NOT echoed back on the next turn, and
140
+ * measurement says they need not be — a tool_result turn that omits them answers 200 on both shapes
141
+ * and both model generations. */
142
+ // the dial itself (budgets, both Anthropic shapes, every OpenAI-compatible dialect) lives in thinking.ts;
143
+ // re-exported so the adapters' callers and tests keep one import
144
+ export { anthropicThinking, thinkingBudget, type AnthropicThinkingShape };
145
+
146
+ /** what the endpoint said when the shape was wrong — both spellings it uses. The message must ALSO name
147
+ * the dial (thinking/effort/budget): "not supported for this model" alone is how the API refuses other
148
+ * things too (a tool feature, an image), and flipping the shape on those would cost every later request
149
+ * a wasted round trip. */
150
+ const WRONG_SHAPE = /not supported for this model|does not support the effort parameter/i;
151
+ const ABOUT_THINKING = /thinking|effort|budget_tokens|output_config/i;
152
+ const wrongShape = (text: string): boolean => WRONG_SHAPE.test(text) && ABOUT_THINKING.test(text);
153
+
154
+ /** per provider+model, learned from a 400. Process-lifetime: a model's shape does not change under us.
155
+ * Keyed with the provider so a gateway that fronts the same model id differently cannot poison the
156
+ * direct endpoint's memory (or the other way round). Exported for tests only. */
157
+ export const shapeByModel = new Map<string, AnthropicThinkingShape>();
158
+ export const shapeKey = (model: ModelRef): string => `${model.provider}/${model.model}`;
159
+
160
+ /** the answer needs room BESIDE the thinking budget — never less than the caller asked for */
161
+ export function anthropicMaxTokens(model: ModelRef): number {
162
+ const base = model.maxTokens ?? 8192; // buildDef normally sets maxTokens from the catalog; 8192 is the floor every current Claude accepts
163
+ const budget = thinkingBudget(model.effort);
164
+ return budget === null ? base : Math.max(base, budget + 4096);
165
+ }
166
+
167
+ /** the shape the next request for this model will use (for /effort and `model show` to say the truth) */
168
+ export function anthropicShapeFor(model: ModelRef): AnthropicThinkingShape { return shapeByModel.get(shapeKey(model)) ?? "effort"; }
169
+
170
+ /** POST to Anthropic, learning the model's thinking shape from a wrong-shape 400 and retrying ONCE.
171
+ * `build` is called per attempt because the shape changes the body. A failure that is NOT about the
172
+ * shape is returned as-is — the adapters turn it into an error turn (httpErrorTurn). Bounded: at most
173
+ * two sends per request, and the flipped shape is remembered only when the retry was not the same
174
+ * refusal — two wrong-shape 400s in a row (a model that takes neither) leave no memory, so the next
175
+ * request does not oscillate between two guaranteed failures. */
176
+ async function anthropicPost(url: string, headers: Record<string, string>, model: ModelRef, build: (extra: Record<string, unknown>) => unknown, signal: AbortSignal | undefined): Promise<Response> {
177
+ const wanted = model.effort;
178
+ const key = shapeKey(model);
179
+ let shape: AnthropicThinkingShape = shapeByModel.get(key) ?? "effort";
180
+ const send = (): Promise<Response> => fetchFirstByte(url, { method: "POST", headers, body: JSON.stringify(build(anthropicThinking(wanted, shape))), ...(signal ? { signal } : {}) });
181
+ const res = await send();
182
+ // nothing to learn when the request carried no level (auto/off/unset use one shape-free field), or when it worked
183
+ if (res.ok || wanted === undefined || wanted === "auto" || wanted === "off" || res.status !== 400) return res;
184
+ const text = await res.clone().text().catch(() => "");
185
+ if (!wrongShape(text)) return res;
186
+ shape = shape === "effort" ? "budget" : "effort";
187
+ const retry = await send();
188
+ if (retry.status === 400 && wrongShape(await retry.clone().text().catch(() => ""))) shapeByModel.delete(key);
189
+ else shapeByModel.set(key, shape);
190
+ return retry;
191
+ }
192
+
193
+ /** OpenAI-compatible endpoints name the dial in a dozen vocabularies (thinking.ts): the plan's fields are
194
+ * spread into the body after the profile's own wire fields (profiles.ts), so the dial wins a clash. */
195
+ function reasoningEffort(model: ModelRef): Record<string, unknown> {
196
+ return thinkingPlan(model, "openai").fields;
197
+ }
198
+
199
+ export function openaiCompatStream(opts: AdapterOptions): StreamFn {
200
+ return async function* (model: ModelRef, messages: Message[], options?: { signal?: AbortSignal; tools?: unknown[] }): AsyncGenerator<StreamEvent> {
201
+ let turn: AssistantTurn;
202
+ try {
203
+ const res = await fetchFirstByte(opts.baseUrl.replace(/\/$/, "") + "/chat/completions", {
204
+ method: "POST",
205
+ headers: { ...(opts.headers ?? {}), "content-type": "application/json", authorization: `Bearer ${opts.apiKey}` },
206
+ body: JSON.stringify({
207
+ model: model.model,
208
+ messages: toOpenAiMessages(messages, wireOptions(model)),
209
+ ...(options?.tools?.length ? { tools: toOpenAiToolSchemas(options.tools) } : {}),
210
+ stream: false,
211
+ ...profileWire(model, false), // per-family endpoint fields (providers/profiles.ts); the effort word below wins a clash
212
+ ...reasoningEffort(model),
213
+ ...(model.maxTokens ? { max_tokens: model.maxTokens } : {}),
214
+ }),
215
+ signal: options?.signal,
216
+ });
217
+ if (!res.ok) {
218
+ turn = await httpErrorTurn(res); // status + Retry-After recorded for withRetry (stream-errors.ts)
219
+ } else {
220
+ turn = parseOpenAiResponse(await res.json());
221
+ }
222
+ } catch (e) {
223
+ turn = failedTurn(e, options?.signal);
224
+ }
225
+ yield { type: "turn", turn };
226
+ };
227
+ }
228
+
229
+ /** Streaming variant: emits text_delta events as they arrive, then the final turn. */
230
+ export function openaiCompatStreaming(opts: AdapterOptions): StreamFn {
231
+ return async function* (model: ModelRef, messages: Message[], options?: { signal?: AbortSignal; tools?: unknown[] }): AsyncGenerator<StreamEvent> {
232
+ let turn: AssistantTurn;
233
+ let buffer = "";
234
+ const toolArgs = new Map<number, { id: string; name: string; args: string }>();
235
+ try {
236
+ const res = await fetchFirstByte(opts.baseUrl.replace(/\/$/, "") + "/chat/completions", {
237
+ method: "POST",
238
+ headers: { ...(opts.headers ?? {}), ...SSE_HEADERS, "content-type": "application/json", authorization: `Bearer ${opts.apiKey}` },
239
+ body: JSON.stringify({
240
+ model: model.model,
241
+ messages: toOpenAiMessages(messages, wireOptions(model)),
242
+ ...(options?.tools?.length ? { tools: toOpenAiToolSchemas(options.tools) } : {}),
243
+ stream: true,
244
+ // ask for the final usage chunk — without it most OpenAI-compat SSE streams omit usage
245
+ stream_options: { include_usage: true },
246
+ ...profileWire(model, true), // per-family endpoint fields, streaming variant (tool_stream for GLM)
247
+ ...reasoningEffort(model),
248
+ ...(model.maxTokens ? { max_tokens: model.maxTokens } : {}),
249
+ }),
250
+ signal: options?.signal,
251
+ });
252
+ if (!res.ok || !res.body) {
253
+ turn = await httpErrorTurn(res); // status + Retry-After recorded for withRetry (stream-errors.ts)
254
+ yield { type: "turn", turn };
255
+ return;
256
+ }
257
+ if (isJsonBody(res)) { yield { type: "turn", turn: parseOpenAiResponse(await res.json()) }; return; }
258
+ let finish: StopReason = "end_turn";
259
+ let usage: AssistantTurn["usage"] = { input: 0, output: 0 };
260
+ for await (const line of sseLines(res.body)) {
261
+ const ev = JSON.parse(line) as {
262
+ choices?: { delta?: { content?: string | null; reasoning_content?: string | null; tool_calls?: { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] }; finish_reason?: string | null }[];
263
+ usage?: unknown;
264
+ };
265
+ const c = ev.choices?.[0];
266
+ if (c?.delta?.content) { buffer += c.delta.content; yield { type: "text_delta", text: c.delta.content }; }
267
+ // reasoning slices (GLM / DeepSeek-style `reasoning_content`): surfaced exactly like Anthropic's
268
+ // thinking_delta — the live status line counts them, the turn's parts never carry them
269
+ if (c?.delta?.reasoning_content) yield { type: "reasoning_delta", text: c.delta.reasoning_content };
270
+ for (const tc of c?.delta?.tool_calls ?? []) {
271
+ const idx = tc.index ?? 0;
272
+ const cur = toolArgs.get(idx) ?? { id: tc.id ?? `tc${idx}`, name: tc.function?.name ?? "", args: "" };
273
+ if (tc.id) cur.id = tc.id;
274
+ if (tc.function?.name) cur.name += tc.function.name;
275
+ if (tc.function?.arguments) cur.args += tc.function.arguments;
276
+ toolArgs.set(idx, cur);
277
+ }
278
+ if (c?.finish_reason) finish = c.finish_reason === "tool_calls" ? "tool_use" : c.finish_reason === "length" ? "length" : "end_turn";
279
+ if (ev.usage) {
280
+ // same normalization as the JSON adapters (parseOpenAiResponse/parseAnthropicResponse):
281
+ // cached_tokens subtracted from the inclusive prompt count, cacheRead/Write carried
282
+ const u = normalizeUsage(ev.usage);
283
+ usage = { input: u.input, output: u.output, cacheRead: u.cacheRead || undefined, cacheWrite: u.cacheWrite || undefined };
284
+ }
285
+ }
286
+ const parts: AssistantTurn["parts"] = [];
287
+ if (buffer) parts.push({ kind: "text", text: buffer });
288
+ for (const [, tc] of [...toolArgs].sort((a, b) => a[0] - b[0])) {
289
+ let args: unknown = {};
290
+ try { args = JSON.parse(tc.args || "{}"); } catch { args = { _raw: tc.args }; }
291
+ parts.push({ kind: "tool_call", id: tc.id, tool: tc.name, args });
292
+ }
293
+ turn = { parts, stopReason: finish, usage };
294
+ } catch (e) {
295
+ turn = failedTurn(e, options?.signal, buffer); // mid-stream abort: the deltas already streamed survive as the turn's text
296
+ }
297
+ yield { type: "turn", turn };
298
+ };
299
+ }
300
+
301
+ /** Streaming variant of the Anthropic adapter: emits text_delta as the model writes, then the final
302
+ * turn. The Messages SSE stream is a sequence of numbered content blocks — `content_block_start`
303
+ * opens one (text or tool_use), `content_block_delta` carries `text_delta` for prose and
304
+ * `input_json_delta` for a tool call's arguments (streamed as JSON text, one fragment at a time),
305
+ * `content_block_stop` closes it. Usage arrives in two halves: the input side on `message_start`,
306
+ * the output count on the final `message_delta` — they are merged so /cost sees the same shape the
307
+ * JSON adapter produces. Block ORDER is preserved: parts are emitted by block index, so a text
308
+ * block before a tool_use stays before it. */
309
+ export function anthropicStreaming(opts: AdapterOptions): StreamFn {
310
+ return async function* (model: ModelRef, messages: Message[], options?: { signal?: AbortSignal; tools?: unknown[] }): AsyncGenerator<StreamEvent> {
311
+ let turn: AssistantTurn;
312
+ let buffer = "";
313
+ /** open + finished blocks by wire index; `json` accumulates an input_json_delta */
314
+ const blocks = new Map<number, { kind: "text"; text: string } | { kind: "tool"; id: string; name: string; json: string } | { kind: "thinking" }>();
315
+ try {
316
+ const system = messages.filter((m) => m.role === "system").map((m) => partsText(m.parts)).join("\n");
317
+ const rest: Record<string, unknown> = {};
318
+ if (options?.tools?.length) rest.tools = options.tools.map((t) => { const sc = asToolSchema(t); return { name: sc.name, description: sc.description, input_schema: sc.args }; });
319
+ if (system) rest.system = system;
320
+ const body = (extra: Record<string, unknown>): Record<string, unknown> => ({
321
+ model: model.model,
322
+ max_tokens: anthropicMaxTokens(model),
323
+ messages: toAnthropicMessages(messages, wireOptions(model)),
324
+ stream: true,
325
+ ...rest,
326
+ ...extra,
327
+ });
328
+ const res = await anthropicPost(
329
+ opts.baseUrl.replace(/\/$/, "") + "/messages",
330
+ { ...(opts.headers ?? {}), ...SSE_HEADERS, "content-type": "application/json", "x-api-key": opts.apiKey, "anthropic-version": "2023-06-01" },
331
+ model,
332
+ (extra) => applyAnthropicCacheBoundaries(body(extra)),
333
+ options?.signal,
334
+ );
335
+ if (!res.ok || !res.body) {
336
+ turn = await httpErrorTurn(res); // status + Retry-After recorded for withRetry (stream-errors.ts)
337
+ yield { type: "turn", turn };
338
+ return;
339
+ }
340
+ if (isJsonBody(res)) { yield { type: "turn", turn: parseAnthropicResponse(await res.json()) }; return; }
341
+ let stop: StopReason = "end_turn";
342
+ let usage: AssistantTurn["usage"] = { input: 0, output: 0 };
343
+ for await (const line of sseLines(res.body)) {
344
+ const ev = JSON.parse(line) as {
345
+ type?: string;
346
+ index?: number;
347
+ message?: { usage?: unknown };
348
+ content_block?: { type?: string; id?: string; name?: string };
349
+ delta?: { type?: string; text?: string; partial_json?: string; thinking?: string; stop_reason?: string | null };
350
+ usage?: unknown;
351
+ error?: { type?: string; message?: string };
352
+ };
353
+ switch (ev.type) {
354
+ case "message_start": {
355
+ const u = normalizeUsage(ev.message?.usage);
356
+ usage = { input: u.input, output: u.output, cacheRead: u.cacheRead || undefined, cacheWrite: u.cacheWrite || undefined };
357
+ break;
358
+ }
359
+ case "content_block_start": {
360
+ const i = ev.index ?? 0;
361
+ if (ev.content_block?.type === "tool_use") blocks.set(i, { kind: "tool", id: ev.content_block.id ?? `tc${i}`, name: ev.content_block.name ?? "unknown", json: "" });
362
+ else if (ev.content_block?.type === "thinking" || ev.content_block?.type === "redacted_thinking") blocks.set(i, { kind: "thinking" });
363
+ else blocks.set(i, { kind: "text", text: "" });
364
+ break;
365
+ }
366
+ case "content_block_delta": {
367
+ const i = ev.index ?? 0;
368
+ const b = blocks.get(i);
369
+ if (ev.delta?.type === "text_delta" && ev.delta.text !== undefined) {
370
+ buffer += ev.delta.text;
371
+ if (b?.kind === "text") b.text += ev.delta.text;
372
+ else blocks.set(i, { kind: "text", text: ev.delta.text }); // a delta without its start
373
+ yield { type: "text_delta", text: ev.delta.text };
374
+ } else if (ev.delta?.type === "input_json_delta" && ev.delta.partial_json !== undefined && b?.kind === "tool") {
375
+ b.json += ev.delta.partial_json;
376
+ } else if (ev.delta?.type === "thinking_delta" && ev.delta.thinking !== undefined) {
377
+ // the model is reasoning: surface the slice so the TUI can prove work is happening, keep
378
+ // nothing — thinking is not the answer (signature_delta carries no tokens and is skipped)
379
+ yield { type: "reasoning_delta", text: ev.delta.thinking };
380
+ }
381
+ break;
382
+ }
383
+ case "message_delta": {
384
+ if (ev.delta?.stop_reason === "max_tokens") stop = "length";
385
+ // the output count only exists here; the input side stays as message_start reported it
386
+ if (ev.usage !== undefined) {
387
+ const u = normalizeUsage(ev.usage);
388
+ usage = { ...usage, output: u.output || usage.output, cacheRead: usage.cacheRead ?? (u.cacheRead || undefined), cacheWrite: usage.cacheWrite ?? (u.cacheWrite || undefined) };
389
+ }
390
+ break;
391
+ }
392
+ // a mid-stream `error` event ends the turn with what was already streamed
393
+ case "error":
394
+ throw new Error(ev.error?.message ?? "anthropic stream error");
395
+ default:
396
+ break;
397
+ }
398
+ }
399
+ const parts: AssistantTurn["parts"] = [];
400
+ for (const [, b] of [...blocks].sort((a, z) => a[0] - z[0])) {
401
+ if (b.kind === "thinking") continue; // reasoning never becomes a part
402
+ if (b.kind === "text") { if (b.text) parts.push({ kind: "text", text: b.text }); continue; }
403
+ let args: unknown = {};
404
+ try { args = JSON.parse(b.json || "{}"); } catch { args = { _raw: b.json }; }
405
+ parts.push({ kind: "tool_call", id: b.id, tool: b.name, args });
406
+ stop = "tool_use";
407
+ }
408
+ turn = { parts, stopReason: stop, usage };
409
+ } catch (e) {
410
+ turn = failedTurn(e, options?.signal, buffer); // mid-stream abort: the deltas already streamed survive as the turn's text
411
+ }
412
+ yield { type: "turn", turn };
413
+ };
414
+ }
415
+
416
+ /** Anthropic Messages protocol adapter. */
417
+ export function anthropicStream(opts: AdapterOptions): StreamFn {
418
+ return async function* (model: ModelRef, messages: Message[], options?: { signal?: AbortSignal; tools?: unknown[] }): AsyncGenerator<StreamEvent> {
419
+ let turn: AssistantTurn;
420
+ try {
421
+ const system = messages.filter((m) => m.role === "system").map((m) => partsText(m.parts)).join("\n");
422
+ const rest: Record<string, unknown> = {};
423
+ if (options?.tools?.length) rest.tools = options.tools.map((t) => { const sc = asToolSchema(t); return { name: sc.name, description: sc.description, input_schema: sc.args }; });
424
+ if (system) rest.system = system;
425
+ const body = (extra: Record<string, unknown>): Record<string, unknown> => ({
426
+ model: model.model,
427
+ max_tokens: anthropicMaxTokens(model),
428
+ messages: toAnthropicMessages(messages, wireOptions(model)),
429
+ ...rest,
430
+ ...extra,
431
+ });
432
+ // port #5: place prompt-cache breakpoints on the stable prefix (hermes pattern)
433
+ const res = await anthropicPost(
434
+ opts.baseUrl.replace(/\/$/, "") + "/messages",
435
+ { ...(opts.headers ?? {}), "content-type": "application/json", "x-api-key": opts.apiKey, "anthropic-version": "2023-06-01" },
436
+ model,
437
+ (extra) => applyAnthropicCacheBoundaries(body(extra)),
438
+ options?.signal,
439
+ );
440
+ if (!res.ok) {
441
+ turn = await httpErrorTurn(res); // status + Retry-After recorded for withRetry (stream-errors.ts)
442
+ } else {
443
+ turn = parseAnthropicResponse(await res.json());
444
+ }
445
+ } catch (e) {
446
+ turn = failedTurn(e, options?.signal);
447
+ }
448
+ yield { type: "turn", turn };
449
+ };
450
+ }
451
+
452
+ // ---------- parsing ----------
453
+
454
+ function parseOpenAiResponse(json: unknown): AssistantTurn {
455
+ const j = json as {
456
+ choices: { message: { content: string | null; tool_calls?: { id: string; function: { name: string; arguments: string } }[] }; finish_reason: string | null }[];
457
+ usage?: { prompt_tokens: number; completion_tokens: number };
458
+ };
459
+ const c = j.choices?.[0];
460
+ const parts: AssistantTurn["parts"] = [];
461
+ if (c?.message.content) parts.push({ kind: "text", text: c.message.content });
462
+ for (const tc of c?.message.tool_calls ?? []) {
463
+ let args: unknown = {};
464
+ try { args = JSON.parse(tc.function.arguments || "{}"); } catch { args = { _raw: tc.function.arguments }; }
465
+ parts.push({ kind: "tool_call", id: tc.id, tool: tc.function.name, args });
466
+ }
467
+ const stop = (c?.message.tool_calls?.length ?? 0) > 0
468
+ ? "tool_use"
469
+ : c?.finish_reason === "length" ? "length" : "end_turn";
470
+ const u = normalizeUsage(j.usage);
471
+ return { parts, stopReason: stop, usage: { input: u.input, output: u.output, cacheRead: u.cacheRead || undefined, cacheWrite: u.cacheWrite || undefined } };
472
+ }
473
+
474
+ function parseAnthropicResponse(json: unknown): AssistantTurn {
475
+ const j = json as {
476
+ content: { type: string; text?: string; id?: string; name?: string; input?: unknown }[];
477
+ stop_reason: string | null;
478
+ usage: { input_tokens: number; output_tokens: number };
479
+ };
480
+ const parts: AssistantTurn["parts"] = [];
481
+ for (const b of j.content ?? []) {
482
+ if (b.type === "text" && b.text) parts.push({ kind: "text", text: b.text });
483
+ if (b.type === "tool_use" && b.id) parts.push({ kind: "tool_call", id: b.id, tool: b.name ?? "unknown", args: b.input ?? {} });
484
+ }
485
+ const stop = (j.content ?? []).some((b) => b.type === "tool_use") ? "tool_use" : j.stop_reason === "max_tokens" ? "length" : "end_turn";
486
+ const u = normalizeUsage(j.usage);
487
+ return { parts, stopReason: stop, usage: { input: u.input, output: u.output, cacheRead: u.cacheRead || undefined, cacheWrite: u.cacheWrite || undefined } };
488
+ }
489
+
490
+ // ---------- SSE ----------
491
+
492
+ async function* sseLines(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {
493
+ const reader = body.getReader();
494
+ const dec = new TextDecoder();
495
+ let buf = "";
496
+ while (true) {
497
+ const { done, value } = await reader.read();
498
+ if (done) break;
499
+ buf += dec.decode(value, { stream: true });
500
+ const lines = buf.split("\n");
501
+ buf = lines.pop() ?? "";
502
+ for (const l of lines) {
503
+ const t = l.trim();
504
+ if (t.startsWith("data:")) {
505
+ const payload = t.slice(5).trim();
506
+ if (payload && payload !== "[DONE]") yield payload;
507
+ }
508
+ }
509
+ }
510
+ }
511
+
512
+ // ---------- mock (test seam) ----------
513
+
514
+ export interface MockScript { turns: AssistantTurn[] }
515
+
516
+ export function mockStream(script: MockScript): StreamFn {
517
+ let i = 0;
518
+ return async function* (_model: ModelRef, _messages: Message[]): AsyncGenerator<StreamEvent> {
519
+ const turn = script.turns[Math.min(i, script.turns.length - 1)]!;
520
+ i++;
521
+ yield { type: "turn", turn };
522
+ };
523
+ }
524
+
525
+ export function textTurn(text: string): AssistantTurn {
526
+ return { parts: [{ kind: "text", text }], stopReason: "end_turn", usage: { input: 0, output: 1 } };
527
+ }
528
+
529
+ export function toolTurn(calls: { id: string; tool: string; args: unknown }[]): AssistantTurn {
530
+ return { parts: calls.map((c) => ({ kind: "tool_call" as const, ...c })), stopReason: "tool_use", usage: { input: 0, output: 1 } };
531
+ }
532
+
533
+ // ---------- registry: named providers from env/config ----------
534
+ // The provider table and the providers.json merge live in provider-config.ts (data) and the live
535
+ // dispatcher in registry.ts. resolveProvider() is the ONE-SHOT default lookup the CLI uses at boot.
536
+
537
+ /** Resolve the default provider config: ROVECODE_BASE_URL/ROVECODE_API_KEY override, else the
538
+ * providers.json `default` selector (when that provider has a key), else stored credential
539
+ * (`rovecode auth set`, port #37) in provider order, else a named env key, else null.
540
+ *
541
+ * Precedence follows opencode provider.ts @ ebece6e: stored api keys are merged AFTER env
542
+ * (provider.ts:1578-1602, later mergeProvider patch wins) so a stored credential beats a
543
+ * named env key; the explicit pair keeps its documented "always wins" rank, like opencode's
544
+ * config source re-applied last (provider.ts:1643-1651). provider-config.ts pickDefault is
545
+ * the single implementation; the live registry shares it. */
546
+ export function resolveProvider(overrides: { baseUrl?: string; apiKey?: string; id?: string; protocol?: "openai" | "anthropic"; defaultModel?: string } = {}): ProviderConfig | null {
547
+ const base = overrides.baseUrl ?? process.env.ROVECODE_BASE_URL;
548
+ const key = overrides.apiKey ?? process.env.ROVECODE_API_KEY ?? process.env.OPENAI_API_KEY;
549
+ if (base && key) {
550
+ const looksAnthropic = overrides.protocol === "anthropic" || base.includes("anthropic.com");
551
+ const defaultModel = overrides.defaultModel ?? process.env.ROVECODE_MODEL;
552
+ return { id: overrides.id ?? "custom", baseUrl: base, apiKey: key, protocol: looksAnthropic ? "anthropic" : "openai", ...(defaultModel !== undefined ? { defaultModel } : {}) };
553
+ }
554
+ const pick = pickDefault(buildSnapshot(process.cwd()));
555
+ if (pick === null) return null;
556
+ const p = pick.provider;
557
+ return {
558
+ id: p.id, baseUrl: p.baseUrl, apiKey: p.apiKey ?? "", protocol: p.protocol,
559
+ ...(pick.model !== undefined ? { defaultModel: pick.model } : {}),
560
+ ...(p.headers !== undefined ? { headers: p.headers } : {}),
561
+ };
562
+ }
563
+
564
+ export function listBuiltinProviders(): { id: string; baseUrl: string; protocol: string; envKey: string; configured: boolean }[] {
565
+ return BUILTIN_PROVIDERS.map((p) => {
566
+ const envKey = p.keyEnv ?? `${p.id.toUpperCase()}_API_KEY`;
567
+ return { id: p.id, baseUrl: p.baseUrl, protocol: p.protocol, envKey, configured: Boolean(process.env[envKey]) };
568
+ });
569
+ }