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,382 @@
1
+ /** PORT #35 — machine-readable output for `rovecode run`: --output text | json | ndjson.
2
+ *
3
+ * Ported shape decisions (pi @ 853a80d, packages/coding-agent/src):
4
+ * - modes/print-mode.ts:108-111 — `--mode json` writes ONE JSON line per session event
5
+ * (`JSON.stringify(toJsonEvent(event)) + "\n"` through writeRawStdout); the RPC protocol
6
+ * frames its output identically (modes/rpc/rpc-mode.ts:60-62, :355-356). rovecode's ndjson mode
7
+ * emits every RunEvent VERBATIM that way (no reformatting: the loop's events are already
8
+ * deltas — pi's toJsonEvent only strips its cumulative partials, json-event.ts:40-45) and
9
+ * closes with one {type:"result", …} line.
10
+ * - core/output-guard.ts:45-70 takeOverStdout — outside the interactive TUI process.stdout.write
11
+ * is redirected to stderr and only the raw writer bound BEFORE the takeover reaches fd 1
12
+ * (installed for every appMode !== "interactive", main.ts:633-636). guardStdout below is that
13
+ * guard; Bun's console.log does not route through process.stdout.write, so console.log/info/
14
+ * debug are redirected too. stdout purity is structural, not a discipline. cmdRun installs it
15
+ * right after parseOutputMode — BEFORE bootRuntime, whose session_open hooks may print — and
16
+ * hands the sink the raw writer it bound first (fix-wave 4 MED-C); the sink's own install below
17
+ * serves embedders that pass process.stdout itself.
18
+ * - print-mode.ts:139-156 — text mode prints only the final assistant text; error/aborted →
19
+ * message on stderr, exit 1 (:145-147); signals exit 128+n (:57-61: 143 SIGTERM, 129 SIGHUP).
20
+ * rovecode keeps its text mode byte-identical to the pre-port cmdRun (progress lines + summary on
21
+ * stdout) and maps an ABORTED run to 130 by the same 128+signal convention: the sink's signal
22
+ * aborts on the first SIGINT and rides into LoopDeps.signal (port #21), so Ctrl-C ends the run
23
+ * "stopped" with a well-formed result instead of a hard kill (Windows: exit 0xC000013A, no output).
24
+ *
25
+ * Result schema (json: the ONLY stdout line; ndjson: the LAST line, with type:"result"):
26
+ * status "done" | "stopped" | "error" | "budget" — run_end.status ("error" when the loop
27
+ * ended without a run_end)
28
+ * summary run_end.summary: the final assistant text, or the error text
29
+ * sessionId run_start.sessionId (null if never seen) — the store under .rovecode/sessions
30
+ * model { provider, model } requested
31
+ * origin { provider, model } that SERVED the last turn (router fallback may differ), or null
32
+ * usage { input, output, cacheRead, cacheWrite } summed over the run's assistant messages
33
+ * costUsd number, or null when any usage-bearing turn has no catalog pricing (mock, unknown
34
+ * models) — an honest unknown, never a silent lower bound (core/usage.ts costUsd)
35
+ * toolCalls [{ tool, ok, ms? }] in event order, one entry per ISSUED call — keyed per
36
+ * `<turn>:<callId>`, so a call id a provider reuses across turns (the SSE adapter's
37
+ * `tc<idx>` fallback, providers/stream.ts) is one entry PER TURN, the telemetry/otel.ts
38
+ * rovecode.tool_calls count (LOW-B, #39); ms absent for calls that never executed
39
+ * (permission_denied / truncated / not_found → ok:false)
40
+ * durationMs sink construction → finish
41
+ * exitCode the process exit code below
42
+ * Exit codes: 0 done · 1 error / budget / no run_end · 2 usage error (bad --output value; also
43
+ * the startup-error class, port #27) · 130 stopped (the run was aborted: SIGINT). Text mode keeps
44
+ * its 0 done / 1 otherwise, plus 130 for the (newly reachable) aborted run. */
45
+
46
+ import { format } from "node:util";
47
+ import type { Message, ModelRef, RunEvent, RunOutstanding, StreamFn } from "../core/types.ts";
48
+ import { outstandingClause, type LoopDeps } from "../core/loop.ts";
49
+ import type { Runtime } from "./runtime.ts";
50
+ import { costUsd, type PricingRow } from "../core/usage.ts";
51
+ import { ModelCatalog } from "../providers/catalog.ts";
52
+ import { VALUE_FLAGS } from "./dispatch.ts";
53
+
54
+ export type OutputMode = "text" | "json" | "ndjson";
55
+ export const OUTPUT_MODES: readonly OutputMode[] = ["text", "json", "ndjson"];
56
+ export type RunEndStatus = Extract<RunEvent, { type: "run_end" }>["status"];
57
+ type RunEnd = { status: RunEndStatus; summary: string; outstanding?: RunOutstanding };
58
+
59
+ export interface RunResult {
60
+ status: RunEndStatus;
61
+ summary: string;
62
+ sessionId: string | null;
63
+ model: { provider: string; model: string };
64
+ origin: { provider: string; model: string } | null;
65
+ usage: { input: number; output: number; cacheRead: number; cacheWrite: number };
66
+ costUsd: number | null;
67
+ toolCalls: { tool: string; ok: boolean; ms?: number }[];
68
+ durationMs: number;
69
+ exitCode: number;
70
+ }
71
+
72
+ export interface Writer { write(chunk: string): unknown }
73
+
74
+ /** ModelCatalog.lookup's shape — tests inject fixed pricing. */
75
+ export interface PricingSource { lookup(provider: string, model: string): { pricing?: PricingRow } | undefined }
76
+
77
+ export interface OutputSinkOptions {
78
+ stdout: Writer;
79
+ stderr: Writer;
80
+ /** requested model → result.model, and the pricing fallback for origin-less messages */
81
+ model: ModelRef;
82
+ /** live view of the run's session store; read at finish (usage, origin, tool names) */
83
+ messages: () => Message[];
84
+ catalog?: PricingSource;
85
+ /** SIGINT hookup seam; returns the uninstaller. Default: process.once("SIGINT"). */
86
+ onInterrupt?: (handler: () => void) => () => void;
87
+ }
88
+
89
+ export interface OutputSink {
90
+ readonly mode: OutputMode;
91
+ /** aborts on the first SIGINT — thread into LoopDeps.signal so the run ends "stopped" (130) */
92
+ readonly signal: AbortSignal;
93
+ onEvent(ev: RunEvent): void;
94
+ /** After the loop settles: writes the text summary / the json result / the ndjson result line
95
+ * and returns the exit code. No `end` = the loop ended without run_end → status "error", 1. */
96
+ finish(end?: RunEnd): number;
97
+ /** Uninstalls the stdout guard (installed for json/ndjson over the REAL process.stdout; a no-op
98
+ * otherwise) so console.log / process.stdout.write reach fd 1 again — for embedders and tests.
99
+ * cmdRun never calls it: hooks.close()/mcp.close() run AFTER finish and may still print, so the
100
+ * guard must hold until process.exit. */
101
+ close(): void;
102
+ }
103
+
104
+ // ---------- argv ----------
105
+
106
+ const USAGE = "--output <mode>: text (default) | json | ndjson";
107
+
108
+ const usageExit = (msg: string): never => {
109
+ process.stderr.write(`error: ${msg} — ${USAGE}\n`);
110
+ return process.exit(2);
111
+ };
112
+
113
+ function isOutputMode(v: string): v is OutputMode {
114
+ return (OUTPUT_MODES as readonly string[]).includes(v);
115
+ }
116
+
117
+ /** `--output <mode>` or `--output=<mode>`, anywhere in argv (dispatch.ts VALUE_FLAGS keeps the
118
+ * value from being taken for the command). Missing or unknown value → one-line stderr usage
119
+ * error, exit 2 (the usage/startup-error class; `fail` is injectable for tests). Last one wins.
120
+ * cmdRun calls this FIRST — before bootRuntime — so a usage error leaves no trace: no
121
+ * .rovecode/sessions/<id> (meta.json, memory dir), no sandbox probe, no MCP children to reap. */
122
+ export function parseOutputMode(argv: readonly string[], fail: (msg: string) => never = usageExit): OutputMode {
123
+ const args = argv.slice(2);
124
+ let mode: string | undefined;
125
+ for (let i = 0; i < args.length; i++) {
126
+ const a = args[i]!;
127
+ if (a === "--output") {
128
+ const v = args[++i];
129
+ if (v === undefined || v.startsWith("-")) return fail("--output needs a value");
130
+ mode = v;
131
+ } else if (a.startsWith("--output=")) {
132
+ mode = a.slice("--output=".length);
133
+ }
134
+ }
135
+ if (mode === undefined) return "text";
136
+ return isOutputMode(mode) ? mode : fail(`unknown --output mode "${mode}"`);
137
+ }
138
+
139
+ /** The one-shot prompt words for cmdRun. parseCli's `rest` keeps a POST-command value flag's
140
+ * value (its contract — owners drop their own, like cmdAuth's --key and export.ts's --out), so
141
+ * `rovecode run "hi" --output json` arrives as rest ["hi", "json"]. The token that followed --output
142
+ * is removed by POSITION, never by value (a prompt may legitimately contain the word "json"). A
143
+ * pre-command --output value never enters rest; a dangling --output is parseOutputMode's error. */
144
+ export function runPromptWords(cli: { cmd: string; rest: string[] }, argv: readonly string[]): string[] {
145
+ const words = cli.cmd === "run" ? [...cli.rest] : [cli.cmd, ...cli.rest]; // bare prompt keeps cmd as word 0
146
+ const args = argv.slice(2);
147
+ const isFlag = (a: string) => a.startsWith("-");
148
+ const cmdIdx = args.findIndex((a, i) => !isFlag(a) && !(i > 0 && VALUE_FLAGS.has(args[i - 1]!)));
149
+ if (cmdIdx === -1) return words;
150
+ // words = the non-flag tokens from `from` on; a value's word index = the non-flag tokens before it.
151
+ // EVERY value flag's value, not only --output's: `rovecode run "hi" --max-turns 1` used to send the prompt
152
+ // "hi 1" — the ceiling's number rode into the words because only --output dropped its own.
153
+ const from = cli.cmd === "run" ? cmdIdx + 1 : cmdIdx;
154
+ const drop: number[] = [];
155
+ args.forEach((a, oi) => {
156
+ const value = args[oi + 1];
157
+ if (!VALUE_FLAGS.has(a) || oi < cmdIdx || value === undefined || isFlag(value)) return;
158
+ drop.push(args.slice(from, oi + 1).filter((t) => !isFlag(t)).length);
159
+ });
160
+ for (const k of drop.reverse()) words.splice(k, 1); // descending: earlier indexes stay valid
161
+ return words;
162
+ }
163
+
164
+ // ---------- piped stdin (`git diff | rovecode run "review this"`) ----------
165
+
166
+ /** What a pipe on stdin handed us, or "" — never from a terminal, and never waited on forever.
167
+ *
168
+ * The one hazard: stdin that is not a TTY and not a pipe anybody writes to — a child spawned with an
169
+ * inherited-but-idle handle, an agent's own bash tool running `rovecode run`. `readFileSync(0)` there
170
+ * blocks until something closes the handle, which may be never. So the first byte gets a deadline
171
+ * (default 3 s): nothing by then means nothing is coming, a note says so, and the run proceeds with the
172
+ * prompt alone. A producer that HAS started is read to EOF however long it takes — the deadline is on the
173
+ * first byte, not the whole stream. Bounded at `maxChars` (1 MB of text) with a note, because a 200 MB log
174
+ * piped in by accident should not become one 200 MB prompt. */
175
+ export async function readPipedStdin(
176
+ stdin: NodeJS.ReadableStream & { isTTY?: boolean; pause?: () => unknown; resume?: () => unknown },
177
+ opts: { firstByteMs?: number; maxChars?: number; note?: (line: string) => void } = {},
178
+ ): Promise<string> {
179
+ if (stdin.isTTY === true) return "";
180
+ const firstByteMs = opts.firstByteMs ?? 3_000;
181
+ const maxChars = opts.maxChars ?? 1_000_000;
182
+ return new Promise<string>((resolve) => {
183
+ const chunks: Buffer[] = [];
184
+ let got = false, done = false;
185
+ const finish = (): void => {
186
+ if (done) return;
187
+ done = true; clearTimeout(timer);
188
+ const text = Buffer.concat(chunks).toString("utf8");
189
+ if (text.length > maxChars) opts.note?.(`stdin: ${text.length.toLocaleString("en-US")} characters piped in — kept the first ${maxChars.toLocaleString("en-US")}`);
190
+ resolve(text.slice(0, maxChars));
191
+ };
192
+ const timer = setTimeout(() => {
193
+ if (got || done) return;
194
+ opts.note?.(`stdin: not a terminal, but nothing arrived in ${firstByteMs / 1000} s — ignored (pipe your input, or pass --no-stdin)`);
195
+ try { stdin.pause?.(); } catch { /* nothing to pause */ }
196
+ finish();
197
+ }, firstByteMs);
198
+ stdin.on("data", (c: Buffer | string) => { got = true; chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)); });
199
+ stdin.on("end", finish);
200
+ stdin.on("close", finish);
201
+ stdin.on("error", finish);
202
+ try { stdin.resume?.(); } catch { finish(); }
203
+ });
204
+ }
205
+
206
+ /** The prompt with the piped text under it as a fenced block. The fence grows until it cannot occur inside the
207
+ * text (a diff of a markdown file carries ``` of its own). No words → "Here is the input:" introduces the
208
+ * block, so the model is never handed a bare fence. Empty pipe → the prompt unchanged. */
209
+ export function withPipedInput(prompt: string, piped: string): string {
210
+ const text = piped.replace(/\r\n?/g, "\n").replace(/\n+$/, "");
211
+ if (text.length === 0) return prompt;
212
+ let fence = "```";
213
+ while (text.includes(fence)) fence += "`";
214
+ const head = prompt.trim().length > 0 ? prompt.trim() : "Here is the input:";
215
+ return `${head}\n\n${fence}\n${text}\n${fence}`;
216
+ }
217
+
218
+ // ---------- stdout guard (pi core/output-guard.ts:45-70) ----------
219
+
220
+ /** Redirect every stray stdout writer — console.log/info/debug (Bun writes them natively, not via
221
+ * process.stdout.write) and process.stdout.write itself — to `stderr`, so only a writer bound
222
+ * BEFORE the guard can reach fd 1. Returns the restorer. */
223
+ export function guardStdout(stderr: Writer): { restore(): void } {
224
+ const saved = { log: console.log, info: console.info, debug: console.debug, write: process.stdout.write };
225
+ const toErr = (...args: unknown[]): void => { stderr.write(`${format(...args)}\n`); };
226
+ console.log = toErr; console.info = toErr; console.debug = toErr;
227
+ process.stdout.write = ((chunk: string | Uint8Array): boolean => {
228
+ stderr.write(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
229
+ return true;
230
+ }) as typeof process.stdout.write;
231
+ return {
232
+ restore() {
233
+ console.log = saved.log; console.info = saved.info; console.debug = saved.debug;
234
+ process.stdout.write = saved.write;
235
+ },
236
+ };
237
+ }
238
+
239
+ // ---------- exit codes ----------
240
+
241
+ /** 0 done · 130 stopped (loop.ts yields "stopped" ONLY on abort — :133/:219/:302) · 1 otherwise. */
242
+ export function exitCodeFor(status: RunEndStatus | undefined): number {
243
+ if (status === "done") return 0;
244
+ if (status === "stopped") return 130;
245
+ return 1;
246
+ }
247
+
248
+ // ---------- sink ----------
249
+
250
+ const installSigint = (handler: () => void): (() => void) => {
251
+ // once: the first Ctrl-C aborts the run (a well-formed result follows); the second is the
252
+ // platform default again (hard exit), so a signal-deaf tool cannot hold the terminal hostage
253
+ try { process.once("SIGINT", handler); } catch { return () => {}; }
254
+ return () => { try { process.off("SIGINT", handler); } catch { /* already gone */ } };
255
+ };
256
+
257
+ /** one issued call; `id` = the provider's call id (the store's tool_call part id — the name fallback) */
258
+ type CallRecord = { id: string; tool?: string; ok: boolean; ms?: number };
259
+
260
+ export function createOutputSink(mode: OutputMode, opts: OutputSinkOptions): OutputSink {
261
+ const t0 = Date.now();
262
+ // bind the raw writer BEFORE guarding: the guard turns process.stdout.write into a stderr relay
263
+ const real = opts.stdout === process.stdout;
264
+ const out: Writer = real ? { write: process.stdout.write.bind(process.stdout) } : opts.stdout;
265
+ const guard = mode !== "text" && real ? guardStdout(opts.stderr) : null;
266
+ const ac = new AbortController();
267
+ const uninstall = (opts.onInterrupt ?? installSigint)(() => ac.abort());
268
+ let sessionId: string | null = null;
269
+ let baseline = 0; // store length at run_start — only THIS run's messages are accounted
270
+ let turn = 0; // the issuing turn: a turn's tool events follow its turn_end (loop.ts:232 → :295)
271
+ // issued calls by `<turn>:<callId>` in event order (the telemetry/otel.ts toolKey idiom): a call id a
272
+ // provider reuses across turns is one call PER TURN, never a merge — keyed by callId alone, `same`
273
+ // issued by 3 turns was ONE toolCall while rovecode.tool_calls said 3 (LOW-B, #39)
274
+ const calls = new Map<string, CallRecord>();
275
+ const call = (id: string): CallRecord => {
276
+ const key = `${turn}:${id}`;
277
+ const c = calls.get(key) ?? { id, ok: false };
278
+ calls.set(key, c);
279
+ return c;
280
+ };
281
+ // human progress: stdout in text mode (byte-identical to the pre-port console.log lines),
282
+ // stderr in json mode (a terminal user still sees progress; stdout stays the one object),
283
+ // nothing extra in ndjson mode (the event stream IS the progress)
284
+ const human = (line: string): void => {
285
+ if (mode === "text") out.write(`${line}\n`);
286
+ else if (mode === "json") opts.stderr.write(`${line}\n`);
287
+ };
288
+ return {
289
+ mode, signal: ac.signal,
290
+ onEvent(ev) {
291
+ if (mode === "ndjson") out.write(`${JSON.stringify(ev)}\n`);
292
+ if (ev.type === "run_start") { sessionId = ev.sessionId; baseline = opts.messages().length; }
293
+ else if (ev.type === "turn_start") turn = ev.turn;
294
+ else if (ev.type === "tool_execution_start") {
295
+ call(ev.callId).tool = ev.tool;
296
+ human(`→ ${ev.tool} ${String(JSON.stringify(ev.args)).slice(0, 100)}`);
297
+ } else if (ev.type === "tool_execution_end") {
298
+ Object.assign(call(ev.callId), { ok: ev.ok, ms: ev.durationMs });
299
+ human(`← ${ev.ok ? "ok" : "FAIL"} ${ev.output.slice(0, 200).replace(/\n/g, " ⏎ ")}`);
300
+ } else if (ev.type === "tool_call_failed") {
301
+ call(ev.callId).ok = false;
302
+ } else if (ev.type === "verify") {
303
+ // the verify gate, in the same two-line shape as a tool call: what runs, then how it ended
304
+ human(ev.state === "running" ? `→ verify ${ev.command.slice(0, 100)}` : `← ${ev.state === "passed" ? "ok" : "FAIL"} verify ${ev.detail ?? ev.state}`);
305
+ }
306
+ },
307
+ finish(end) {
308
+ uninstall();
309
+ const exitCode = exitCodeFor(end?.status);
310
+ if (mode === "text") {
311
+ if (end) out.write(`\n${end.summary}\n`);
312
+ // "done" is the model's silence, not a verdict: one clause says what the transcript says was left
313
+ // (a failed call never recovered, an unanswered question, open todos). A run that only answered a
314
+ // question carries no `outstanding` at all and prints exactly the bytes it always did.
315
+ if (end?.status === "done" && end.outstanding) {
316
+ const clause = outstandingClause(end.outstanding);
317
+ if (clause !== null) out.write(`done · ${clause}\n`);
318
+ }
319
+ return exitCode;
320
+ }
321
+ const result = summarize(
322
+ end ?? { status: "error", summary: "stream ended without run_end" }, exitCode, sessionId, opts.model,
323
+ opts.messages().slice(baseline), [...calls.values()], opts.catalog ?? new ModelCatalog(), Date.now() - t0,
324
+ );
325
+ out.write(`${JSON.stringify(mode === "json" ? result : { type: "result", ...result })}\n`);
326
+ return exitCode;
327
+ },
328
+ close() { guard?.restore(); },
329
+ };
330
+ }
331
+
332
+ // ---------- run deps ----------
333
+
334
+ /** cmdRun's LoopDeps, built in one place so the wiring is unit-testable (LOW-2): the runtime's
335
+ * registry/store/tools, guard (port #4), cwd (port #26) and hooks (port #29), with the sink's
336
+ * SIGINT signal (port #21) — the fields agentLoop reads. Same object literal cmdRun used to inline,
337
+ * evaluated at the same argument position (tools listed at call time). */
338
+ export function buildRunDeps(rt: Pick<Runtime, "registry" | "store" | "guard" | "planReminder" | "cwd" | "hooks">, stream: StreamFn, sink: Pick<OutputSink, "signal">): LoopDeps {
339
+ return { stream, registry: rt.registry, store: rt.store, tools: rt.registry.list().map((t) => t.schema), guard: rt.guard, planReminder: rt.planReminder, cwd: rt.cwd, signal: sink.signal, hooks: rt.hooks };
340
+ }
341
+
342
+ function summarize(
343
+ end: RunEnd, exitCode: number, sessionId: string | null, model: ModelRef, msgs: Message[],
344
+ calls: CallRecord[], catalog: PricingSource, durationMs: number,
345
+ ): RunResult {
346
+ const assistants = msgs.filter((m) => m.role === "assistant");
347
+ const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
348
+ let cost: number | null = 0;
349
+ for (const m of assistants) {
350
+ const u = m.usage;
351
+ if (!u) continue;
352
+ const n = { input: u.input, output: u.output, cacheRead: u.cacheRead ?? 0, cacheWrite: u.cacheWrite ?? 0 };
353
+ usage.input += n.input; usage.output += n.output; usage.cacheRead += n.cacheRead; usage.cacheWrite += n.cacheWrite;
354
+ if (cost === null || (n.input === 0 && n.output === 0 && n.cacheRead === 0 && n.cacheWrite === 0)) continue;
355
+ // priced PER MESSAGE at the model that served it (Message.origin — the tui/cost.ts idiom)
356
+ const o = m.origin ?? model;
357
+ const pricing = catalog.lookup(o.provider, o.model)?.pricing;
358
+ const c = pricing ? costUsd(n, pricing) : undefined;
359
+ cost = c === undefined ? null : cost + c;
360
+ }
361
+ // names for calls that never started (denied/truncated/not_found) come from the tool_call parts, paired
362
+ // by OCCURRENCE — the n-th record with a call id ↔ the n-th part carrying it (one part per issued call),
363
+ // so a reused id names each turn's own call rather than the last part that mentioned the id
364
+ const names = new Map<string, string[]>();
365
+ for (const m of assistants) for (const p of m.parts) if (p.kind === "tool_call") names.set(p.id, [...(names.get(p.id) ?? []), p.tool]);
366
+ const nth = new Map<string, number>();
367
+ const served = assistants.at(-1)?.origin;
368
+ return {
369
+ status: end.status, summary: end.summary, sessionId,
370
+ // what "done" left behind (core/loop.ts assessOutstanding): failed tool calls in the last turn, an
371
+ // unanswered ask_user, successful edit/write count, open todos, whether the finish check asked once
372
+ ...(end.outstanding ? { outstanding: end.outstanding } : {}),
373
+ model: { provider: model.provider, model: model.model },
374
+ origin: served ? { provider: served.provider, model: served.model } : null,
375
+ usage, costUsd: cost,
376
+ toolCalls: calls.map((c) => {
377
+ const n = nth.get(c.id) ?? 0; nth.set(c.id, n + 1);
378
+ return { tool: c.tool ?? names.get(c.id)?.[n] ?? "unknown", ok: c.ok, ...(c.ms !== undefined ? { ms: c.ms } : {}) };
379
+ }),
380
+ durationMs, exitCode,
381
+ };
382
+ }
@@ -0,0 +1,172 @@
1
+ /** Interactive agent chat (omp/claude-code style): persistent session, streaming
2
+ * output, y/n/a approvals, slash commands. Bare `rovecode` drops here. */
3
+
4
+ import readline from "node:readline";
5
+ import { agentLoop } from "../core/loop.ts";
6
+ import { resetTurnFailureCount } from "../memory/tools.ts";
7
+ import type { ApprovalFn, RunEvent } from "../core/types.ts";
8
+ import type { AskFn } from "../tools/ask-user.ts";
9
+ import { bootRuntime, NO_PROVIDER_HINT } from "./runtime.ts";
10
+ import { modeSwitchNote } from "../core/voice.ts";
11
+ import { SandboxConfigError, describeSandbox } from "../core/sandbox-config.ts";
12
+
13
+ export interface ReplState {
14
+ yolo: boolean;
15
+ provider: string;
16
+ model: string;
17
+ turns: number;
18
+ tokensIn: number;
19
+ tokensOut: number;
20
+ }
21
+
22
+ function ask(rl: readline.Interface, q: string): Promise<string> {
23
+ return new Promise((res) => rl.question(q, (a) => res(a.trim().toLowerCase())));
24
+ }
25
+
26
+ /** port #33: the `--plain` asker behind ask_user — `--plain` HAS a human (the y/n/a approvals prove
27
+ * it), headless surfaces leave the tool unbound and it fails closed. Numbered options plus free text
28
+ * when allowed: a number picks, other text is the typed answer, an empty line declines (null). Piped
29
+ * stdin is fine — it just consumes the next line. The run's abort resolves null AND is handed to
30
+ * rl.question itself (WIRE-1 LOW): an aborted question's callback is DISARMED, so the user's next
31
+ * line is a normal `line` event again instead of being swallowed by the dead callback. `out` is
32
+ * console.log; tests capture it. */
33
+ export function readlineAsker(rl: readline.Interface, out: (line: string) => void = console.log): AskFn {
34
+ return (q, signal) => new Promise((resolve) => {
35
+ const options = q.options ?? [];
36
+ const free = q.allowFreeText !== false;
37
+ out(`\n question: ${q.question}`);
38
+ options.forEach((o, i) => out(` ${i + 1}) ${o}`));
39
+ const hint = [options.length > 0 ? `1-${options.length}` : "", free ? "text" : ""].filter(Boolean).join(" or ");
40
+ const onAbort = (): void => { resolve(null); };
41
+ signal.addEventListener("abort", onAbort, { once: true });
42
+ rl.question(` answer [${hint}; empty = decline]: `, { signal }, (line) => {
43
+ signal.removeEventListener("abort", onAbort);
44
+ const a = line.trim();
45
+ const n = Number(a);
46
+ if (a === "") resolve(null);
47
+ else if (Number.isInteger(n) && n >= 1 && n <= options.length) resolve({ choice: n - 1, label: options[n - 1] });
48
+ else if (free) resolve({ text: a });
49
+ else { out(" (not one of the options — declined)"); resolve(null); }
50
+ });
51
+ });
52
+ }
53
+
54
+ export async function runRepl( /* eslint-disable-line complexity */
55
+ opts: { yolo?: boolean; model?: string } = {},
56
+ ): Promise<void> {
57
+ // port #27: sandbox misconfig / unavailable configured rung → one-line startup error, exit 2
58
+ const rt = await bootRuntime().catch((e: unknown): never => {
59
+ if (e instanceof SandboxConfigError) { console.error(`error: ${e.message}`); process.exit(2); }
60
+ throw e;
61
+ });
62
+
63
+ const state: ReplState = {
64
+ yolo: opts.yolo ?? process.env.ROVECODE_YOLO === "1",
65
+ provider: "mock",
66
+ model: opts.model ?? process.env.ROVECODE_MODEL ?? "",
67
+ turns: 0, tokensIn: 0, tokensOut: 0,
68
+ };
69
+
70
+ // the registry is live: `rovecode provider add …` / `rovecode auth set …` from another terminal is
71
+ // picked up on the next line — no restart and no ad-hoc base-url prompt here
72
+ const stream = rt.stream ?? undefined;
73
+ const adoptDefault = (): void => {
74
+ const d = rt.providers.defaultRef();
75
+ if (d !== null) { state.provider = d.provider; state.model = state.model || d.model || "gpt-4o-mini"; }
76
+ };
77
+ adoptDefault();
78
+ if (state.provider === "mock") console.log(NO_PROVIDER_HINT);
79
+ rt.hooks.onWarning((w) => console.error(`hooks: ${w}`)); // port #29: load + runtime hook notes → stderr (cmdRun idiom)
80
+
81
+ console.log(`◆ rovecode here — plain chat with ${state.provider}/${state.model}`);
82
+ console.log(`session ${rt.sessionId.slice(0, 8)} in ${rt.cwd}`);
83
+ console.log(modeSwitchNote(state.yolo));
84
+ console.log(`commands: /exit /new /yolo /model <provider/model> /status /skills /memory`);
85
+
86
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "rovecode> " });
87
+
88
+ rt.setAskUser(readlineAsker(rl)); // port #33: ask_user over the same readline (readlineAsker above)
89
+
90
+ const approval: ApprovalFn = async (req) => {
91
+ const argPreview = JSON.stringify(req.revisedArgs).slice(0, 140);
92
+ console.log(`\n approval needed: ${req.tool} ${argPreview}`);
93
+ const a = await ask(rl, " allow? [y]es / [a]lways / [n]o: ");
94
+ return a === "a" ? "always" : a === "n" || a === "" ? "deny" : "once";
95
+ };
96
+
97
+ rl.prompt();
98
+
99
+ // port #21: one AbortController per run — Ctrl+C mid-run aborts the in-flight
100
+ // fetch/tools for real (abort first, then return() settles the generator); idle
101
+ // Ctrl+C keeps its old meaning (close the repl)
102
+ let running: { ac: AbortController; gen: AsyncGenerator<RunEvent> } | null = null;
103
+ rl.on("SIGINT", () => {
104
+ if (running) { running.ac.abort(); void running.gen.return(undefined as never); console.log("\n [interrupted]"); }
105
+ else rl.close();
106
+ });
107
+
108
+ rl.on("line", async (line) => {
109
+ const text = line.trim();
110
+ if (!text) { rl.prompt(); return; }
111
+ if (text === "/exit" || text === "/quit") { rl.close(); return; }
112
+ if (text === "/yolo") { state.yolo = !state.yolo; console.log(modeSwitchNote(state.yolo)); rl.prompt(); return; }
113
+ if (text === "/status") { console.log(`provider=${state.provider} model=${state.model} turns=${state.turns} tokens=${state.tokensIn}in/${state.tokensOut}out\nsandbox: ${describeSandbox(rt.sandbox)}`); rl.prompt(); return; }
114
+ if (text === "/skills") { for (const s of rt.skillStore.list()) console.log(` ${s.name.padEnd(20)} ${s.description}`); rl.prompt(); return; }
115
+ if (text === "/memory") { console.log(rt.blockStore.renderForPrompt() || "(empty)"); rl.prompt(); return; }
116
+ if (text.startsWith("/model ")) {
117
+ const ref = rt.providers.resolveSelector(text.slice(7), state.provider);
118
+ if ("error" in ref) console.log(ref.error); else { state.provider = ref.provider; state.model = ref.model; console.log(`model → ${ref.provider}/${ref.model}`); }
119
+ rl.prompt(); return;
120
+ }
121
+ if (text === "/new") { rt.store.branch(rt.store.messages()[0]?.id ?? ""); console.log("branched to session start"); rl.prompt(); return; }
122
+
123
+ const reason = rt.noProviderReason(); // live — a provider added since boot is picked up here
124
+ if (!stream || reason !== null) { console.log(reason ?? "no provider stream"); rl.prompt(); return; }
125
+ if (state.provider === "mock") adoptDefault();
126
+
127
+ const def = rt.buildDef({ provider: state.provider, model: state.model });
128
+
129
+ const ac = new AbortController();
130
+ rt.tasks.bindRun(ac.signal); // port #26: Ctrl-C (ac.abort above) also cancels the background tasks this run started
131
+ // port #29: hooks ride the deps like every surface; port #26: the runtime's ONE steering queue (not a
132
+ // fresh one) so background-task completion notes — and any hook-pushed steer — reach the next turn
133
+ const gen = agentLoop(def, text, {}, rt.buildCfg(state.yolo, approval), { stream, registry: rt.registry, store: rt.store, tools: rt.registry.list().map((t) => t.schema), guard: rt.guard, planReminder: rt.planReminder, cwd: rt.cwd, signal: ac.signal, hooks: rt.hooks }, rt.steering);
134
+ running = { ac, gen };
135
+ try {
136
+ let live = "";
137
+ for await (const ev of gen) {
138
+ if (ev.type === "turn_start") { resetTurnFailureCount(); state.turns++; }
139
+ if (ev.type === "message_update") { process.stdout.write(ev.delta); live += ev.delta; }
140
+ if (ev.type === "tool_execution_start") { console.log(`\n → ${ev.tool} ${JSON.stringify(ev.args).slice(0, 120)}`); }
141
+ if (ev.type === "tool_execution_end") { console.log(` ← ${ev.ok ? "ok" : "FAIL"} ${ev.output.slice(0, 160).replace(/\n/g, " ⏎ ")}`); }
142
+ if (ev.type === "run_end") {
143
+ if (!live.trim()) console.log(ev.summary);
144
+ else console.log();
145
+ if (ev.status !== "done") console.log(` [${ev.status}]`);
146
+ }
147
+ }
148
+ for (const n of rt.drainRouterNotes()) console.log(` [${n}]`); // port #14 fallback advances
149
+ for (const m of rt.store.messages()) if (m.usage) { state.tokensIn += m.usage.input; state.tokensOut += m.usage.output; }
150
+ } catch (e) {
151
+ console.log(`error: ${e instanceof Error ? e.message : String(e)}`);
152
+ } finally {
153
+ running = null;
154
+ }
155
+ rl.prompt();
156
+ });
157
+
158
+ // every quit path lands here (Ctrl+D, /exit, /quit, idle Ctrl+C → rl.close()), so this is the ONE exit
159
+ rl.on("close", async () => {
160
+ // port #29: a run still in flight dies with the surface (abort, then let its generator settle) BEFORE
161
+ // session_close fires once — after in-flight on_event taps drained (hooks.close() waits for them)
162
+ if (running) { running.ac.abort(); await running.gen.return(undefined as never).catch(() => {}); }
163
+ // port #26: quitting leaves no background children — their runs (and subprocess trees) die
164
+ // now and settle, bounded, before the process goes (same policy as cmdRun's exit())
165
+ rt.tasks.cancelAll();
166
+ await rt.tasks.drain(2_000);
167
+ await rt.hooks.close().catch(() => {});
168
+ void rt.mcp?.close().catch(() => {}); // kill MCP child processes (TUI does the same in app.ts)
169
+ console.log(`\nbye — session ${rt.sessionId.slice(0, 8)} saved (${state.turns} turns, ${state.tokensIn}in/${state.tokensOut}out tokens)`);
170
+ process.exit(0);
171
+ });
172
+ }
@@ -0,0 +1,32 @@
1
+ /** Which session `rovecode` reopens at boot, from argv.
2
+ *
3
+ * Three spellings, one rule — an explicit id always wins:
4
+ * rovecode --resume <id> that session (a unique prefix resolves in the TUI, session-cmd.ts resolveBootSession)
5
+ * rovecode --resume the newest session that holds something
6
+ * rovecode --continue the same — the flag every CLI that got this right converged on
7
+ * `--resume` followed by another flag (`rovecode --resume --yolo`) is `--resume` with no id, never "resume the
8
+ * session named --yolo". With nothing to continue from, the answer is undefined and the TUI starts fresh, as
9
+ * it always did; that is the honest outcome, not an error. */
10
+
11
+ import { newestSession } from "../core/session.ts";
12
+
13
+ export interface ResumeRequest {
14
+ /** the id or prefix the user named */
15
+ id?: string;
16
+ /** `--continue`, or `--resume` with no id: reopen the newest non-empty session */
17
+ newest: boolean;
18
+ }
19
+
20
+ export function parseResume(argv: readonly string[]): ResumeRequest {
21
+ const ix = argv.indexOf("--resume");
22
+ const arg = ix !== -1 ? argv[ix + 1] : undefined;
23
+ const id = arg !== undefined && !arg.startsWith("-") ? arg : undefined;
24
+ return { ...(id !== undefined ? { id } : {}), newest: id === undefined && (ix !== -1 || argv.includes("--continue")) };
25
+ }
26
+
27
+ /** The session id to boot with, or undefined for a fresh one. */
28
+ export function resolveResume(argv: readonly string[], sessionsDir: string): string | undefined {
29
+ const r = parseResume(argv);
30
+ if (r.id !== undefined) return r.id;
31
+ return r.newest ? newestSession(sessionsDir)?.id : undefined;
32
+ }