rovecode 0.4.0-beta.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,398 @@
1
+ /** Append-only JSONL session tree (ADR-004).
2
+ * Entries form a tree by (id, parentId); a leaf pointer selects the active path.
3
+ * One serde path (JSON) for every backend. Corruption is detected, classified, and reported. */
4
+
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, renameSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import type { ImagePart, Message, RunEvent, TextPart } from "./types.ts";
9
+ import { hydrateImageParts, sidecarImageParts } from "./session-images.ts";
10
+
11
+ export type Entry = Message | ({ id: string; kind: "event"; parentId: string | null; createdAt: number; event: RunEvent });
12
+
13
+ /** Port #34 image sidecars (session-images.ts): `<session>/attachments/<sha256>.<ext>`. The store's
14
+ * sidecar writer references them from entries.jsonl in ONE form, the session-relative
15
+ * `attachments/<file>`, resolved to an absolute path in memory only. Any other persisted path —
16
+ * whatever wrote it — never resolves to a readable file: a non-canonical relative path stays
17
+ * relative (F2) and an absolute path is dropped at load (F3), so the part lowers to a "file
18
+ * unavailable" placeholder and no line in entries.jsonl can point a hydrated part at a file
19
+ * outside `<session>/attachments/`.
20
+ * `rovecode export --json` copies entries.jsonl ALONE — the attachments directory travels with the
21
+ * session directory, not with the export (the JSONL stays a small, verbatim-copyable record). */
22
+
23
+ export type CorruptionKind =
24
+ | "orphan-entry" // parentId points at nothing
25
+ | "cycle" // ancestry loop
26
+ | "duplicate-id"
27
+ | "malformed-json"
28
+ | "unknown-shape"
29
+ | "chain-broken"; // prevHash disagrees with the parent entry's hash (tree/chain fork)
30
+
31
+ export interface Corruption { kind: CorruptionKind; entryId?: string; line: number; detail: string }
32
+
33
+ /** `leaf` (optional, port #2): durable active-leaf pointer. Absent = legacy = last entry wins. */
34
+ export interface SessionMeta { id: string; createdAt: number; goal?: string; model?: string; leaf?: string }
35
+
36
+ /** Hash chain: each entry carries sha256(prevHash + canonical(entry)). Tamper-evident replay.
37
+ * Accepts the wrapped envelope too — the chain hashes the full wrapper (hash field empty). */
38
+ export function chainHash(prev: string, entry: Entry | object): string {
39
+ const canon = JSON.stringify(sortKeys(entry));
40
+ return createHash("sha256").update(prev + canon).digest("hex");
41
+ }
42
+
43
+ function sortKeys(v: unknown): unknown {
44
+ if (Array.isArray(v)) return v.map(sortKeys);
45
+ if (v && typeof v === "object") {
46
+ return Object.fromEntries(
47
+ Object.entries(v as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)).map(([k, x]) => [k, sortKeys(x)])
48
+ );
49
+ }
50
+ return v;
51
+ }
52
+
53
+ interface Wrapped { id: string; parentId: string | null; createdAt: number; prevHash: string; hash: string; entry: Entry }
54
+
55
+ /** Shape of a loaded entry (ADR-004: classify foreign/corrupt lines, never crash on them):
56
+ * "event" = kind "event"; "message" = a role plus a parts ARRAY whose members are objects with a
57
+ * string kind; undefined = anything else (null, a scalar, `parts: [null]`, `parts: "x"`, `{}`) —
58
+ * reload reports it as unknown-shape, path()/turnPoints() skip it, hydration leaves it alone. */
59
+ export function entryShape(e: unknown): "message" | "event" | undefined {
60
+ if (!e || typeof e !== "object") return undefined;
61
+ if ((e as { kind?: unknown }).kind === "event") return "event";
62
+ const m = e as { role?: unknown; parts?: unknown };
63
+ if (!("role" in m) || !Array.isArray(m.parts)) return undefined;
64
+ return m.parts.every((p: unknown) => !!p && typeof p === "object" && typeof (p as { kind?: unknown }).kind === "string") ? "message" : undefined;
65
+ }
66
+
67
+ /** Event entry (kind "event") vs message — tolerant of foreign/corrupt entry shapes. */
68
+ function isEventWrapped(w: Wrapped): boolean { return entryShape(w.entry) === "event"; }
69
+
70
+ /** Single-line preview of a message's text parts; ≤80 chars, "" when no text. */
71
+ function previewText(m: Message): string {
72
+ const joined = m.parts.filter((p): p is TextPart => p.kind === "text").map((p) => p.text).join(" ");
73
+ const one = joined.replace(/\s+/g, " ").trim();
74
+ return one.length > 80 ? one.slice(0, 79) + "…" : one;
75
+ }
76
+
77
+ export interface SessionSummary {
78
+ id: string;
79
+ createdAt: number;
80
+ updatedAt: number; // max entry createdAt, else meta createdAt
81
+ entryCount: number;
82
+ preview: string; // first user-message text, single-line, ≤80 chars, "" if none
83
+ }
84
+
85
+ /** Scan rootDir for session dirs; skip (never throw on) corrupt/foreign dirs. Sorted updatedAt desc. */
86
+ export function listSessions(rootDir: string): SessionSummary[] {
87
+ const out: SessionSummary[] = [];
88
+ let names: string[];
89
+ try { names = readdirSync(rootDir); } catch { return out; }
90
+ for (const name of names) {
91
+ try {
92
+ const raw: unknown = JSON.parse(readFileSync(join(rootDir, name, "meta.json"), "utf8"));
93
+ if (!raw || typeof raw !== "object") continue;
94
+ const meta = raw as SessionMeta;
95
+ if (typeof meta.id !== "string" || typeof meta.createdAt !== "number") continue;
96
+ let updatedAt = meta.createdAt; let entryCount = 0; let preview = "";
97
+ const entriesPath = join(rootDir, name, "entries.jsonl");
98
+ if (existsSync(entriesPath)) {
99
+ for (const line of readFileSync(entriesPath, "utf8").split("\n")) {
100
+ if (!line.trim()) continue;
101
+ let parsed: unknown;
102
+ try { parsed = JSON.parse(line); } catch { continue; }
103
+ if (!parsed || typeof parsed !== "object") continue;
104
+ const w = parsed as Wrapped;
105
+ entryCount++;
106
+ if (typeof w.createdAt === "number" && w.createdAt > updatedAt) updatedAt = w.createdAt;
107
+ const e: unknown = w.entry;
108
+ if (!preview && entryShape(e) === "message" && (e as Message).role === "user") {
109
+ preview = previewText(e as Message);
110
+ }
111
+ }
112
+ }
113
+ // identity = the DIRECTORY name, never meta.id — a copied/tampered meta.json must not
114
+ // redirect resume to another path (critic finding: "../../escaped" traversal)
115
+ out.push({ id: name, createdAt: meta.createdAt, updatedAt, entryCount, preview });
116
+ } catch { /* foreign or corrupt dir: skip, never throw */ }
117
+ }
118
+ return out.sort((a, b) => b.updatedAt - a.updatedAt);
119
+ }
120
+
121
+ /** The session `rovecode --continue` (or `--resume` with no id) reopens: the most recently updated one that
122
+ * HOLDS something. A session with no entries is not a place to continue from — and while the store used to
123
+ * leave one behind at every start (see the constructor note), a directory holding only meta.json can still
124
+ * exist from before that change, so the count is checked rather than assumed. */
125
+ export function newestSession(rootDir: string): SessionSummary | undefined {
126
+ return listSessions(rootDir).find((s) => s.entryCount > 0);
127
+ }
128
+
129
+ export interface TurnPoint {
130
+ entryId: string; // the user message's wrapped-entry id
131
+ index: number; // 1-based position among user turns on the active path
132
+ text: string; // single-line preview ≤80 chars (overlay label ONLY)
133
+ /** untruncated message text — the edit-and-resubmit prefill (pi sessions.md:113) */
134
+ fullText: string;
135
+ parentId: string | null; // the entry's parent (rewind target: leaf moves HERE)
136
+ branches: number; // children of parentId in the whole tree MINUS the active-path child (0 = linear)
137
+ }
138
+
139
+ export class SessionStore {
140
+ private readonly dir: string;
141
+ private leaf = "root";
142
+ private prevHash = "";
143
+ private cache: Wrapped[] = [];
144
+ private meta: SessionMeta;
145
+ /** true once meta.json carries a leaf field — appends then keep it in step. */
146
+ private leafPersisted = false;
147
+ /** port #34: image parts waiting for the next user message (stageAttachments) */
148
+ private staged: ImagePart[] = [];
149
+
150
+ /** Opening a store touches nothing on disk. The directory and meta.json appear with the FIRST entry
151
+ * (append, appendEvent, branch) — see materialize(). Constructing used to write both immediately, so
152
+ * every start that never got a prompt (`rovecode --help` paths that boot a runtime, `rovecode context`,
153
+ * a TUI opened and closed, every test) left `<sessions>/<uuid>/meta.json` behind: dozens of empty
154
+ * directories that listSessions offered as sessions with nothing in them. Deleting them afterwards was
155
+ * the other option, and it races: process B prunes the directory process A has just opened and not
156
+ * yet written to. Not creating it has no such window. An existing session (meta.json + entries) opens
157
+ * exactly as before; nothing is ever deleted. */
158
+ constructor(rootDir: string, public readonly id: string) {
159
+ this.dir = join(rootDir, id);
160
+ this.meta = { id, createdAt: Date.now() };
161
+ this.reload();
162
+ }
163
+
164
+ private get file() { return join(this.dir, "entries.jsonl"); }
165
+
166
+ /** Make the session real on disk: the directory plus meta.json (once). Called on every write path,
167
+ * before the write. Idempotent and cheap (a mkdir on an existing directory and one stat), so it also
168
+ * means a session whose empty directory was swept away by hand recovers on its next entry instead of
169
+ * throwing ENOENT from appendFileSync. `createdAt` is the construction time, written whenever the first
170
+ * entry lands — the session began when it was opened, not when someone first spoke. */
171
+ private materialize(): void {
172
+ mkdirSync(this.dir, { recursive: true });
173
+ const metaP = join(this.dir, "meta.json");
174
+ if (!existsSync(metaP)) writeFileSync(metaP, JSON.stringify(this.meta, null, 2));
175
+ }
176
+
177
+ /** Replay JSONL → cache; detect corruption instead of crashing (pi reducer pattern).
178
+ * Restores a persisted leaf (durable branch) when meta.json names an existing entry;
179
+ * missing/invalid leaf falls back to the last tree-linked, well-shaped entry (LOW-A below). */
180
+ reload(): Corruption[] {
181
+ this.cache = [];
182
+ this.leaf = "root"; this.prevHash = ""; // a replay that finds no leaf (no file, only foreign lines) is a fresh root
183
+ const seen = new Set<string>();
184
+ const corrupt: Corruption[] = [];
185
+ let persistedLeaf: string | undefined;
186
+ this.leafPersisted = false;
187
+ try {
188
+ const raw: unknown = JSON.parse(readFileSync(join(this.dir, "meta.json"), "utf8"));
189
+ if (raw && typeof raw === "object") {
190
+ const m = raw as SessionMeta;
191
+ if (typeof m.id === "string" && typeof m.createdAt === "number") this.meta = m;
192
+ if (typeof m.leaf === "string") { persistedLeaf = m.leaf; this.leafPersisted = true; }
193
+ }
194
+ } catch { /* unreadable meta behaves as legacy (no persisted leaf) */ }
195
+ if (!existsSync(this.file)) return corrupt;
196
+ const lines = readFileSync(this.file, "utf8").split("\n").filter(Boolean);
197
+ const byLine = new Map<string, Wrapped>(); // ids seen so far (first occurrence wins)
198
+ // LOW-A (#34): the fallback leaf = the last entry that is BOTH tree-linked (a root, or its parent seen
199
+ // above it) AND a message/event. As cache.at(-1) it was whatever id-bearing line came last, so a
200
+ // foreign `{"id":"ghost","parentId":"nope",…}` hijacked the active path (messages() shrank to the
201
+ // ghost, the next append parented on it) and a bare `{"id":"bare"}` emptied it and re-rooted the next
202
+ // append — silently: only `rovecode trace` shows these findings, the constructor discards them. Such
203
+ // lines stay in the cache for chain/reporting and keep their orphan-entry / unknown-shape findings.
204
+ let tail: Wrapped | undefined;
205
+ lines.forEach((line, i) => {
206
+ let w: Wrapped;
207
+ try {
208
+ w = JSON.parse(line) as Wrapped;
209
+ } catch {
210
+ corrupt.push({ kind: "malformed-json", line: i, detail: `unparseable line ${i}` });
211
+ return;
212
+ }
213
+ // F1: a line that parses but is not an entry object (null, a number, a string) is reported, never dereferenced
214
+ if (!w || typeof w !== "object") { corrupt.push({ kind: "unknown-shape", line: i, detail: "line is not an entry object" }); return; }
215
+ // F4: an object line with no string id (`{"entry":null}`) is not a tree node either — reported and
216
+ // skipped; cached, it became the leaf (cache.at(-1)) with id undefined, emptied messages() and
217
+ // re-rooted the next append (the loop parents on history.at(-1) ?? null)
218
+ if (typeof w.id !== "string") { corrupt.push({ kind: "unknown-shape", line: i, detail: "entry line has no string id" }); return; }
219
+ if (seen.has(w.id)) corrupt.push({ kind: "duplicate-id", entryId: w.id, line: i, detail: "duplicate id" });
220
+ seen.add(w.id);
221
+ const orphan = w.parentId !== null && !seen.has(w.parentId);
222
+ if (orphan) corrupt.push({ kind: "orphan-entry", entryId: w.id, line: i, detail: `parent ${w.parentId} missing` });
223
+ // chain linkage: prevHash must equal the PARENT's hash ("" for roots). A leaf moved
224
+ // mid-run used to fork the hash chain away from the parent pointer — detect it.
225
+ // Missing parents are skipped here (already reported as orphan-entry above).
226
+ const expected = w.parentId === null ? "" : byLine.get(w.parentId)?.hash;
227
+ if (expected !== undefined && w.prevHash !== expected) {
228
+ corrupt.push({ kind: "chain-broken", entryId: w.id, line: i, detail: `prevHash disagrees with parent ${w.parentId ?? "(root)"}` });
229
+ }
230
+ if (!byLine.has(w.id)) byLine.set(w.id, w);
231
+ // F1: a foreign/corrupt entry (null, a scalar, parts:[null], …) is reported and kept in the tree
232
+ // for chain purposes; path()/turnPoints() skip it and hydrateImages leaves it untouched
233
+ const shape = entryShape(w.entry);
234
+ if (shape === undefined) corrupt.push({ kind: "unknown-shape", entryId: w.id, line: i, detail: "entry is neither a message nor an event" });
235
+ w.entry = this.hydrateImages(w.entry); // session-relative sidecar paths → absolute (in memory only)
236
+ this.cache.push(w);
237
+ if (!orphan && shape !== undefined) tail = w;
238
+ });
239
+ // cycle check over ancestry
240
+ const byId = new Map(this.cache.map((w) => [w.id, w]));
241
+ for (const w of this.cache) {
242
+ const anc = new Set<string>(); let cur: Wrapped | undefined = w;
243
+ while (cur && cur.parentId !== null) {
244
+ if (anc.has(cur.id)) { corrupt.push({ kind: "cycle", entryId: w.id, line: -1, detail: "ancestry cycle" }); break; }
245
+ anc.add(cur.id); cur = byId.get(cur.parentId);
246
+ }
247
+ }
248
+ if (tail) { this.leaf = tail.id; this.prevHash = tail.hash; }
249
+ if (persistedLeaf !== undefined) {
250
+ const w = byId.get(persistedLeaf);
251
+ if (w) { this.leaf = w.id; this.prevHash = w.hash; }
252
+ }
253
+ return corrupt;
254
+ }
255
+
256
+ append(entry: Entry): void {
257
+ // A supplied parentId can lag the leaf (the loop snapshots history at run start;
258
+ // /new and /rewind may move the leaf mid-run). The chain must follow the PARENT
259
+ // pointer, never the moved leaf, or hash chain and tree silently disagree.
260
+ this.materialize();
261
+ const supplied = (entry as { parentId?: string | null }).parentId;
262
+ const parentId = supplied !== undefined ? supplied : this.leaf;
263
+ let prevHash = this.prevHash;
264
+ if (parentId !== this.leaf) {
265
+ prevHash = parentId === null ? "" : (this.cache.find((c) => c.id === parentId)?.hash ?? this.prevHash);
266
+ }
267
+ // port #34: staged attachments ride on this user message — folded into the caller's parts
268
+ // array IN PLACE, because the loop persists the very object it keeps in its history
269
+ // (loop.ts:116-122); that is what puts the image on the wire this run without a loop change
270
+ if (this.staged.length > 0 && "role" in entry && entry.role === "user") {
271
+ entry.parts.push(...this.staged);
272
+ this.staged = [];
273
+ }
274
+ const persisted = this.sidecarImages(entry); // inline bytes → sidecar files; entries.jsonl stays small
275
+ const w: Wrapped = {
276
+ id: entry.id, parentId,
277
+ createdAt: entry.createdAt ?? Date.now(),
278
+ prevHash,
279
+ hash: "",
280
+ entry: persisted,
281
+ };
282
+ w.hash = chainHash(prevHash, w);
283
+ appendFileSync(this.file, JSON.stringify(w) + "\n");
284
+ this.cache.push(persisted === entry ? w : { ...w, entry: this.hydrateImages(persisted) });
285
+ this.leaf = w.id; this.prevHash = w.hash;
286
+ if (this.leafPersisted) this.persistLeaf(); // keep the durable leaf in step after a branch
287
+ }
288
+
289
+ /** Port #34 TUI attach path (`/attach <path>` → next submit): image parts staged here are
290
+ * appended to the parts of the NEXT user message that lands in append(), then cleared.
291
+ * System/assistant/tool entries in between (a pending mode switch, tool results) leave the
292
+ * stage untouched. Replaces any earlier stage; `[]` clears it. */
293
+ stageAttachments(parts: readonly ImagePart[]): void { this.staged = [...parts]; }
294
+
295
+ get stagedAttachments(): readonly ImagePart[] { return this.staged; }
296
+
297
+ /** The on-disk form (session-images.ts sidecarImageParts): inline image bytes → sidecar files +
298
+ * session-relative paths. Same object when there is nothing to do or the shape is foreign (F1). */
299
+ private sidecarImages(entry: Entry): Entry {
300
+ if (entryShape(entry) !== "message" || !("role" in entry)) return entry;
301
+ const parts = sidecarImageParts(this.dir, entry.parts);
302
+ return parts === undefined ? entry : { ...entry, parts };
303
+ }
304
+
305
+ /** The in-memory form (session-images.ts hydrateImageParts): canonical sidecar paths → absolute
306
+ * under this session's dir; a persisted absolute path is dropped (F3), any other relative one
307
+ * stays put (F2). Same object when there is nothing to do or the shape is foreign (F1). */
308
+ private hydrateImages(entry: Entry): Entry {
309
+ if (entryShape(entry) !== "message" || !("role" in entry)) return entry;
310
+ const parts = hydrateImageParts(this.dir, entry.parts);
311
+ return parts === undefined ? entry : { ...entry, parts };
312
+ }
313
+
314
+ /** Persist a RunEvent as an ANNOTATION of the current leaf (port #25: the loop's compaction
315
+ * marker). The entry hangs off the leaf without becoming one — the loop parents its next
316
+ * message on the last real message (history.at(-1)), so a chain-linked event would turn
317
+ * into a dead sibling the moment that message lands. Hash-chained like any entry; path()
318
+ * folds it back in right after the message it annotates; messages() never sees it. */
319
+ appendEvent(event: RunEvent): Entry {
320
+ this.materialize();
321
+ const parentId = this.leaf === "root" ? null : this.leaf;
322
+ const entry: Entry = { id: randomUUID(), kind: "event", parentId, createdAt: Date.now(), event };
323
+ const w: Wrapped = { id: entry.id, parentId, createdAt: entry.createdAt, prevHash: this.prevHash, hash: "", entry };
324
+ w.hash = chainHash(this.prevHash, w);
325
+ appendFileSync(this.file, JSON.stringify(w) + "\n");
326
+ this.cache.push(w);
327
+ return entry;
328
+ }
329
+
330
+ /** Active path = root → leaf (omp buildSessionContext); unknown-shape entries (reload F1) are skipped. */
331
+ path(): Entry[] { return this.wrappedPath().filter((w) => entryShape(w.entry) !== undefined).map((w) => w.entry); }
332
+
333
+ private wrappedPath(): Wrapped[] {
334
+ const byId = new Map(this.cache.map((w) => [w.id, w]));
335
+ const out: Wrapped[] = [];
336
+ let cur = byId.get(this.leaf);
337
+ while (cur) { out.unshift(cur); cur = cur.parentId ? byId.get(cur.parentId) : undefined; }
338
+ // event annotations (appendEvent) are children of path entries but never parents: fold each
339
+ // in right after the entry it annotates, file order. Chain-linked events (a supplied
340
+ // parentId via append, e.g. the export fixture) are already on the path and stay put.
341
+ const onPath = new Set(out.map((w) => w.id));
342
+ const notes = new Map<string | null, Wrapped[]>();
343
+ for (const w of this.cache) {
344
+ if (onPath.has(w.id) || !isEventWrapped(w)) continue;
345
+ if (w.parentId !== null && !onPath.has(w.parentId)) continue;
346
+ const list = notes.get(w.parentId) ?? []; list.push(w); notes.set(w.parentId, list);
347
+ }
348
+ if (notes.size === 0) return out;
349
+ const folded: Wrapped[] = [...(notes.get(null) ?? [])];
350
+ for (const w of out) folded.push(w, ...(notes.get(w.id) ?? []));
351
+ return folded;
352
+ }
353
+
354
+ /** User turns along the ACTIVE path only, root→leaf order. */
355
+ turnPoints(): TurnPoint[] {
356
+ const children = new Map<string | null, number>();
357
+ // event annotations hang off entries without forking them: not a branch (port #25)
358
+ for (const w of this.cache) if (!isEventWrapped(w)) children.set(w.parentId, (children.get(w.parentId) ?? 0) + 1);
359
+ const out: TurnPoint[] = [];
360
+ for (const w of this.wrappedPath()) {
361
+ const e = w.entry;
362
+ if (entryShape(e) !== "message" || !("role" in e) || e.role !== "user") continue; // F1: foreign shapes skipped
363
+ out.push({
364
+ entryId: w.id,
365
+ index: out.length + 1,
366
+ text: previewText(e),
367
+ fullText: e.parts.filter((p): p is TextPart => p.kind === "text").map((p) => p.text).join(""),
368
+ parentId: w.parentId,
369
+ branches: (children.get(w.parentId) ?? 1) - 1,
370
+ });
371
+ }
372
+ return out;
373
+ }
374
+
375
+ /** Branch: move leaf back to an earlier entry without deleting anything.
376
+ * Durable (port #2): the leaf survives restarts via meta.json. Unknown id → false. */
377
+ branch(entryId: string): boolean {
378
+ const target = this.cache.find((w) => w.id === entryId);
379
+ if (!target) return false;
380
+ this.leaf = target.id;
381
+ this.prevHash = target.hash;
382
+ this.persistLeaf();
383
+ return true;
384
+ }
385
+
386
+ /** Atomically rewrite meta.json carrying the active leaf (write tmp + rename). */
387
+ private persistLeaf(): void {
388
+ this.materialize();
389
+ this.meta = { ...this.meta, leaf: this.leaf };
390
+ const metaP = join(this.dir, "meta.json");
391
+ const tmp = metaP + ".tmp";
392
+ writeFileSync(tmp, JSON.stringify(this.meta, null, 2));
393
+ renameSync(tmp, metaP);
394
+ this.leafPersisted = true;
395
+ }
396
+
397
+ messages(): Message[] { return this.path().filter((e): e is Message => "role" in e); }
398
+ }
@@ -0,0 +1,98 @@
1
+ /** Persisted preferences — the answers you should only have to give once.
2
+ *
3
+ * Until this file existed, the permission level lived in the session: `/yolo` and `/accept-edits`
4
+ * died with the terminal, so every launch started back at "ask before every write and every
5
+ * command". That is the difference between a dial and a nag.
6
+ *
7
+ * Two scopes, the providers.json idiom exactly (providers/provider-config.ts): `~/.rovecode/
8
+ * settings.json` is you, `<cwd>/.rovecode/settings.json` is this repository, and the project file
9
+ * wins — "in THIS checkout, stop asking" is a different sentence from "stop asking anywhere".
10
+ *
11
+ * Precedence, highest first: an explicit CLI flag → ROVECODE_PERMISSION → project → user → "ask".
12
+ * A flag is about this run, an env var about this shell, a file about this place; the narrower the
13
+ * intent, the louder it speaks.
14
+ *
15
+ * A missing, unreadable or malformed file reads as "no preference" — never an error. A settings
16
+ * file is a convenience; losing it must never stop the agent from starting. */
17
+
18
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { dirname, join } from "node:path";
20
+ import { rovecodeHome } from "../providers/auth.ts";
21
+ import { THINKING_EFFORTS, type PermissionLevel, type ThinkingEffort } from "./types.ts";
22
+
23
+ export type SettingsScope = "user" | "project";
24
+
25
+ export interface Settings {
26
+ /** how much the human is asked — see types.ts PermissionLevel */
27
+ permission?: PermissionLevel;
28
+ /** how hard the model thinks before answering */
29
+ effort?: ThinkingEffort;
30
+ /** the terminal bell (BEL) when a run ends or a card needs you — default true; `false` turns it off.
31
+ * A terminal that beeps when you did not ask is worse than silence, so this is one key, in the file
32
+ * you already have, rather than a flag you have to remember every launch (sextant-renderer.ts ring). */
33
+ bell?: boolean;
34
+ /** the check the loop runs after the agent's last edit before its reply counts as done (core/verify.ts):
35
+ * one shell command, or a list run in order; `false` turns the gate off AND stops any inference. The
36
+ * one source that needs no guessing — a project should set this rather than let a `test` script be
37
+ * inferred, because a gate that runs the wrong command once is turned off forever. */
38
+ verify?: string | string[] | false;
39
+ }
40
+
41
+ const FILE = "settings.json";
42
+
43
+ export function settingsPath(scope: SettingsScope, cwd: string): string {
44
+ return scope === "user" ? join(rovecodeHome(), FILE) : join(cwd, ".rovecode", FILE);
45
+ }
46
+
47
+ const PERMISSIONS: readonly string[] = ["ask", "accept-edits", "auto"];
48
+
49
+ /** Only keys we recognize survive, and only with values we recognize: a hand-edited
50
+ * `"permission": "yes"` must not become a truthy something downstream. */
51
+ function sanitize(raw: unknown): Settings {
52
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
53
+ const r = raw as Record<string, unknown>;
54
+ const out: Settings = {};
55
+ if (typeof r.permission === "string" && PERMISSIONS.includes(r.permission)) out.permission = r.permission as PermissionLevel;
56
+ if (typeof r.effort === "string" && (THINKING_EFFORTS as readonly string[]).includes(r.effort)) out.effort = r.effort as ThinkingEffort;
57
+ if (typeof r.bell === "boolean") out.bell = r.bell; // "off"/"no" are not false: a string is ignored, the bell stays on
58
+ // verify: a non-empty command string, a list of them (empty strings dropped, at most 8, each under 500 chars),
59
+ // or `false`. Not a table with the other keys on purpose: the shapes differ (two enums, a boolean, this).
60
+ if (r.verify === false) out.verify = false;
61
+ else if (typeof r.verify === "string" && r.verify.trim() !== "" && r.verify.length <= 500) out.verify = r.verify.trim();
62
+ else if (Array.isArray(r.verify)) {
63
+ const cmds = r.verify.filter((v): v is string => typeof v === "string" && v.trim() !== "" && v.length <= 500).map((v) => v.trim()).slice(0, 8);
64
+ if (cmds.length > 0) out.verify = cmds;
65
+ }
66
+ return out;
67
+ }
68
+
69
+ function readOne(path: string): Settings {
70
+ try { return sanitize(JSON.parse(readFileSync(path, "utf8"))); } catch { return {}; }
71
+ }
72
+
73
+ /** user then project, project winning key by key — a repo may pin the permission level while the
74
+ * thinking effort stays whatever you chose globally */
75
+ export function loadSettings(cwd: string): Settings {
76
+ return { ...readOne(settingsPath("user", cwd)), ...readOne(settingsPath("project", cwd)) };
77
+ }
78
+
79
+ /** Merge one key into a scope's file, leaving the rest of it (and the other scope) alone. */
80
+ export function saveSetting<K extends keyof Settings>(key: K, value: Settings[K], scope: SettingsScope, cwd: string): string {
81
+ const path = settingsPath(scope, cwd);
82
+ const next: Settings = { ...readOne(path), [key]: value };
83
+ mkdirSync(dirname(path), { recursive: true });
84
+ writeFileSync(path, JSON.stringify(next, null, 2) + "\n");
85
+ return path;
86
+ }
87
+
88
+ /** The level this run starts at. `flag` is the CLI's answer (undefined when it said nothing);
89
+ * the env var is next; then the files; then the deny-default "ask". */
90
+ export function resolvePermission(cwd: string, flag: PermissionLevel | undefined, env: { ROVECODE_PERMISSION?: string | undefined; ROVECODE_YOLO?: string | undefined; ROVECODE_ACCEPT_EDITS?: string | undefined }): PermissionLevel {
91
+ if (flag !== undefined) return flag;
92
+ const named = (env.ROVECODE_PERMISSION ?? "").trim().toLowerCase();
93
+ if (PERMISSIONS.includes(named)) return named as PermissionLevel;
94
+ // the older single-purpose switches keep working; auto wins because it is the wider of the two
95
+ if (env.ROVECODE_YOLO === "1") return "auto";
96
+ if (env.ROVECODE_ACCEPT_EDITS === "1") return "accept-edits";
97
+ return loadSettings(cwd).permission ?? "ask";
98
+ }