rovecode 0.3.3 → 0.4.0-beta.3

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 (495) hide show
  1. package/CHANGELOG.md +527 -0
  2. package/README.md +46 -48
  3. package/bin/rovecode.js +24 -0
  4. package/dist/cli/app-j6gn14w3.js +2 -0
  5. package/dist/cli/ask-user-cwstt8fz.js +2 -0
  6. package/dist/cli/auth-login-9bbp9915.js +2 -0
  7. package/dist/cli/auth-m8p9grty.js +2 -0
  8. package/dist/cli/bench-16zqdms5.js +9 -0
  9. package/dist/cli/catalog-1xchffa4.js +2 -0
  10. package/dist/cli/cli-1n1zb64f.js +2 -0
  11. package/dist/cli/client-2t9gjkck.js +2 -0
  12. package/dist/cli/commands-exafvm2b.js +2 -0
  13. package/dist/cli/connect-6zde0kn3.js +2 -0
  14. package/dist/cli/context-cmd-5t43wgqt.js +2 -0
  15. package/dist/cli/context-report-kt01pw8y.js +2 -0
  16. package/dist/cli/count-remote-ap7x3vh6.js +2 -0
  17. package/dist/cli/design-ne5zszyh.js +2 -0
  18. package/dist/cli/dispatch-2r5myxye.js +2 -0
  19. package/dist/cli/doctor-ws4fh4tn.js +3 -0
  20. package/dist/cli/executor-bdrjn634.js +2 -0
  21. package/dist/cli/export-1mxb9g5p.js +2 -0
  22. package/dist/cli/files-g104xghh.js +2 -0
  23. package/dist/cli/gauntlet-07xrjpj7.js +2 -0
  24. package/dist/cli/gauntlet-runner-xvy64436.js +10 -0
  25. package/dist/cli/gauntlet-wave3-jm91yt5w.js +5 -0
  26. package/dist/cli/gauntlet-wave4-r13py7p1.js +14 -0
  27. package/dist/cli/hashline-znvrat11.js +2 -0
  28. package/dist/cli/http-xafw6fsh.js +143 -0
  29. package/dist/cli/index-1sgjm25y.js +2 -0
  30. package/dist/cli/init-g2m0tn4m.js +51 -0
  31. package/dist/cli/install-avaqjjqq.js +2 -0
  32. package/dist/cli/loop-mmpfft01.js +2 -0
  33. package/dist/cli/main-0904f6ps.js +5 -0
  34. package/dist/cli/main-0ab9fc26.js +9 -0
  35. package/dist/cli/main-0jys2ccn.js +3 -0
  36. package/dist/cli/main-0mtcdbs7.js +3 -0
  37. package/dist/cli/main-0z1w2zsg.js +3 -0
  38. package/dist/cli/main-1dchs7xv.js +18 -0
  39. package/dist/cli/main-1ereejm1.js +3 -0
  40. package/dist/cli/main-1k1kw6b5.js +3 -0
  41. package/dist/cli/main-27y4sm2k.js +38 -0
  42. package/dist/cli/main-2wwjex5j.js +58 -0
  43. package/dist/cli/main-2yeveeve.js +6 -0
  44. package/dist/cli/main-2yfck9b5.js +3 -0
  45. package/dist/cli/main-2zmzgkwh.js +3 -0
  46. package/dist/cli/main-351pz3z7.js +7 -0
  47. package/dist/cli/main-3gjqfh7a.js +6 -0
  48. package/dist/cli/main-3nf3kgve.js +3 -0
  49. package/dist/cli/main-3pjrb2hd.js +3 -0
  50. package/dist/cli/main-3rxcvgna.js +19 -0
  51. package/dist/cli/main-4b3jgy66.js +19 -0
  52. package/dist/cli/main-4wndhjdc.js +7 -0
  53. package/dist/cli/main-4xcmvxnk.js +3 -0
  54. package/dist/cli/main-5tbz0wbz.js +4 -0
  55. package/dist/cli/main-5ywnwthm.js +3 -0
  56. package/dist/cli/main-6b62vkz0.js +14 -0
  57. package/dist/cli/main-6dnk69vp.js +3 -0
  58. package/dist/cli/main-6genrmhs.js +136 -0
  59. package/dist/cli/main-73g7eff4.js +15 -0
  60. package/dist/cli/main-7c5thhjd.js +5 -0
  61. package/dist/cli/main-7rn6bqje.js +3 -0
  62. package/dist/cli/main-80haw7qk.js +4 -0
  63. package/dist/cli/main-875s60s2.js +4 -0
  64. package/dist/cli/main-8kjxbpw4.js +8 -0
  65. package/dist/cli/main-90ds1z4e.js +10 -0
  66. package/dist/cli/main-9etavkew.js +3 -0
  67. package/dist/cli/main-a9njrkk1.js +3 -0
  68. package/dist/cli/main-aecrjq2d.js +12 -0
  69. package/dist/cli/main-ck9asesq.js +9 -0
  70. package/dist/cli/main-cta9racd.js +4 -0
  71. package/dist/cli/main-ddv7j2ag.js +3 -0
  72. package/dist/cli/main-dfreez27.js +10 -0
  73. package/dist/cli/main-f7rw7des.js +3 -0
  74. package/dist/cli/main-ggcn7rd7.js +5 -0
  75. package/dist/cli/main-gzkmycnv.js +3 -0
  76. package/dist/cli/main-hq51jg8v.js +18 -0
  77. package/dist/cli/main-jft389w9.js +8 -0
  78. package/dist/cli/main-k1eqkg83.js +3 -0
  79. package/dist/cli/main-k2y8a2aw.js +9 -0
  80. package/dist/cli/main-kcpbykxz.js +4 -0
  81. package/dist/cli/main-kd488vje.js +22 -0
  82. package/dist/cli/main-kh32yvgk.js +5 -0
  83. package/dist/cli/main-kqxnqjnv.js +25 -0
  84. package/dist/cli/main-kyn0xnsg.js +3 -0
  85. package/dist/cli/main-m1kk6fp5.js +21 -0
  86. package/dist/cli/main-mv40pcr2.js +4 -0
  87. package/dist/cli/main-n0t3973w.js +3 -0
  88. package/dist/cli/main-nqveez48.js +4 -0
  89. package/dist/cli/main-pknhvrmj.js +3 -0
  90. package/dist/cli/main-pn1w7a7j.js +3 -0
  91. package/dist/cli/main-prxxs70n.js +4 -0
  92. package/dist/cli/main-q3vsesf9.js +3 -0
  93. package/dist/cli/main-qsevpgsv.js +3 -0
  94. package/dist/cli/main-rdgdw24b.js +25 -0
  95. package/dist/cli/main-rfth4tbm.js +16 -0
  96. package/dist/cli/main-rg0wn0xf.js +5 -0
  97. package/dist/cli/main-sdmxhtv8.js +4 -0
  98. package/dist/cli/main-skbp13js.js +18 -0
  99. package/dist/cli/main-t4xnd213.js +7 -0
  100. package/dist/cli/main-vqak588n.js +4 -0
  101. package/dist/cli/main-w2n1303f.js +9 -0
  102. package/dist/cli/main-wbrdspr2.js +5 -0
  103. package/dist/cli/main-wsrg79c1.js +7 -0
  104. package/dist/cli/main-x4r0fne4.js +5 -0
  105. package/dist/cli/main-xea2f3tn.js +6 -0
  106. package/dist/cli/main-xg704a3c.js +3 -0
  107. package/dist/cli/main-xvnrabfp.js +16 -0
  108. package/dist/cli/main-xy53xf0r.js +4 -0
  109. package/dist/cli/main-y1fqy60y.js +3 -0
  110. package/dist/cli/main-yn8cd281.js +34 -0
  111. package/dist/cli/main-yr0ksc0h.js +4 -0
  112. package/dist/cli/main-z2ex2vyf.js +4 -0
  113. package/dist/cli/main-z3aayzvq.js +3 -0
  114. package/dist/cli/main-zaqh35jg.js +3 -0
  115. package/dist/cli/main-zc2e8e46.js +4 -0
  116. package/dist/cli/main-zzrfw6cf.js +13 -0
  117. package/dist/cli/main.js +280 -0
  118. package/dist/cli/market-cmd-e14kmx9n.js +5 -0
  119. package/dist/cli/mcp-login-wq7ktdek.js +2 -0
  120. package/dist/cli/mcp-market-cmd-9mg3jecy.js +2 -0
  121. package/dist/cli/notify-b7qc0cjb.js +2 -0
  122. package/dist/cli/oauth-z8whcgfx.js +2 -0
  123. package/dist/cli/output-b3ewj3ps.js +16 -0
  124. package/dist/cli/profiles-6mr5he5e.js +2 -0
  125. package/dist/cli/provider-config-g7j42q8x.js +2 -0
  126. package/dist/cli/provider-jr1y8vvm.js +2 -0
  127. package/dist/cli/registry-s8yk86g0.js +2 -0
  128. package/dist/cli/registry-t6p8d4mn.js +2 -0
  129. package/dist/cli/repl-bajwe1mh.js +11 -0
  130. package/dist/cli/resume-rwn9nz7y.js +2 -0
  131. package/dist/cli/run-flags-nah7ndpt.js +2 -0
  132. package/dist/cli/runtime-n7gafzhb.js +2 -0
  133. package/dist/cli/sandbox-config-emdy18x4.js +2 -0
  134. package/dist/cli/server-b0nvs2bn.js +5 -0
  135. package/dist/cli/session-arg-y75wd4kj.js +2 -0
  136. package/dist/cli/session-j62evmjq.js +2 -0
  137. package/dist/cli/sessions-cmd-tsnwz0ns.js +7 -0
  138. package/dist/cli/settings-df10wfez.js +2 -0
  139. package/dist/cli/setup-jzvv72fg.js +2 -0
  140. package/dist/cli/sextant-smoke-37m81ke6.js +5 -0
  141. package/dist/cli/skills-cmd-gjxnxnhx.js +2 -0
  142. package/dist/cli/smoke-p7748apt.js +8 -0
  143. package/dist/cli/start-chat-s4st3mm0.js +12 -0
  144. package/dist/cli/stream-gmeyewds.js +2 -0
  145. package/dist/cli/task-gh0kkp3n.js +2 -0
  146. package/dist/cli/tasks-z1kfpe8e.js +2 -0
  147. package/dist/cli/thinking-0eqkrz6t.js +2 -0
  148. package/dist/cli/todo-5brcrt9m.js +2 -0
  149. package/dist/cli/tools-7pzm0vj9.js +2 -0
  150. package/dist/cli/tools-s635p6s8.js +2 -0
  151. package/dist/cli/trust-cmd-cjav8zgm.js +2 -0
  152. package/dist/cli/update-check-pt31bm2f.js +2 -0
  153. package/dist/cli/update-cmd-tk131s9t.js +2 -0
  154. package/dist/cli/voice-56nabd8d.js +2 -0
  155. package/dist/cli/webfetch-xd8q596m.js +2 -0
  156. package/dist/cli/websearch-5hkf98k1.js +2 -0
  157. package/dist/cli/workflow-cmd-cy3cvzjp.js +4 -0
  158. package/dist/cli/workspace-q10g5z3e.js +2 -0
  159. package/dist/lib/index.js +62 -0
  160. package/dist/lib/models-index.json +1 -0
  161. package/dist/lib/plugins.js +55 -0
  162. package/dist/lib/providers.js +17 -0
  163. package/dist/lib/public-api.js +20 -0
  164. package/dist/lib/sdk.js +360 -0
  165. package/package.json +84 -72
  166. package/bin/rovecode.ts +0 -21
  167. package/src/acp/server.ts +0 -374
  168. package/src/cli/auth-login.ts +0 -122
  169. package/src/cli/connect.ts +0 -244
  170. package/src/cli/context-cmd.ts +0 -199
  171. package/src/cli/dispatch.ts +0 -82
  172. package/src/cli/doctor.ts +0 -362
  173. package/src/cli/export.ts +0 -276
  174. package/src/cli/help.ts +0 -293
  175. package/src/cli/is-tui-invocation.ts +0 -8
  176. package/src/cli/main.ts +0 -583
  177. package/src/cli/market-cmd.ts +0 -658
  178. package/src/cli/mcp-login.ts +0 -141
  179. package/src/cli/mcp-market-cmd.ts +0 -302
  180. package/src/cli/output.ts +0 -382
  181. package/src/cli/repl.ts +0 -250
  182. package/src/cli/repomap-root.ts +0 -14
  183. package/src/cli/resume.ts +0 -57
  184. package/src/cli/run-flags.ts +0 -43
  185. package/src/cli/run-limits.ts +0 -78
  186. package/src/cli/runtime.ts +0 -931
  187. package/src/cli/session-arg.ts +0 -30
  188. package/src/cli/sessions-cmd.ts +0 -145
  189. package/src/cli/setup.ts +0 -153
  190. package/src/cli/skills-cmd.ts +0 -194
  191. package/src/cli/start-chat.ts +0 -65
  192. package/src/cli/trust-cmd.ts +0 -52
  193. package/src/coding/bash.ts +0 -148
  194. package/src/coding/checkpoints.ts +0 -327
  195. package/src/coding/diff.ts +0 -138
  196. package/src/coding/files.ts +0 -341
  197. package/src/coding/hashline.ts +0 -274
  198. package/src/coding/lsp-gate.ts +0 -254
  199. package/src/coding/lsp-servers.ts +0 -147
  200. package/src/coding/lsp.ts +0 -283
  201. package/src/coding/repomap-cache.ts +0 -99
  202. package/src/coding/repomap-files.ts +0 -192
  203. package/src/coding/repomap.ts +0 -481
  204. package/src/core/agents.ts +0 -255
  205. package/src/core/compaction.ts +0 -259
  206. package/src/core/config.ts +0 -289
  207. package/src/core/context-report.ts +0 -228
  208. package/src/core/context.ts +0 -60
  209. package/src/core/count-remote.ts +0 -107
  210. package/src/core/execpolicy-rules.ts +0 -196
  211. package/src/core/execpolicy.ts +0 -385
  212. package/src/core/executor.ts +0 -454
  213. package/src/core/guardrails.ts +0 -400
  214. package/src/core/hooks.ts +0 -411
  215. package/src/core/images.ts +0 -230
  216. package/src/core/intro.ts +0 -266
  217. package/src/core/loop.ts +0 -567
  218. package/src/core/modes.ts +0 -372
  219. package/src/core/orchestrator.ts +0 -245
  220. package/src/core/proc-group.ts +0 -48
  221. package/src/core/project-trust.ts +0 -98
  222. package/src/core/reflection.ts +0 -165
  223. package/src/core/sandbox-config.ts +0 -186
  224. package/src/core/session-id.ts +0 -24
  225. package/src/core/session-images.ts +0 -73
  226. package/src/core/session-ops.ts +0 -183
  227. package/src/core/session-text.ts +0 -29
  228. package/src/core/session.ts +0 -469
  229. package/src/core/settings.ts +0 -170
  230. package/src/core/tasks.ts +0 -646
  231. package/src/core/token-scale.ts +0 -108
  232. package/src/core/tools.ts +0 -309
  233. package/src/core/trust.ts +0 -104
  234. package/src/core/types.ts +0 -330
  235. package/src/core/update-check.ts +0 -171
  236. package/src/core/usage.ts +0 -204
  237. package/src/core/validate.ts +0 -121
  238. package/src/core/verify-gate.ts +0 -159
  239. package/src/core/verify.ts +0 -236
  240. package/src/core/voice.ts +0 -158
  241. package/src/core/win-job.ts +0 -183
  242. package/src/core/workspace.ts +0 -184
  243. package/src/design/audit.ts +0 -797
  244. package/src/design/direction.ts +0 -190
  245. package/src/design/rules.ts +0 -157
  246. package/src/eval/bench.ts +0 -150
  247. package/src/eval/gauntlet-runner.ts +0 -215
  248. package/src/eval/gauntlet-support.ts +0 -84
  249. package/src/eval/gauntlet-wave3.ts +0 -269
  250. package/src/eval/gauntlet-wave4.ts +0 -217
  251. package/src/eval/gauntlet.ts +0 -253
  252. package/src/index.ts +0 -17
  253. package/src/lanes/agy.ts +0 -95
  254. package/src/lanes/approval.ts +0 -24
  255. package/src/lanes/claude.ts +0 -129
  256. package/src/lanes/codex.ts +0 -127
  257. package/src/lanes/events.ts +0 -130
  258. package/src/lanes/job.ts +0 -142
  259. package/src/lanes/opencode.ts +0 -122
  260. package/src/lanes/process.ts +0 -184
  261. package/src/lanes/progress.ts +0 -183
  262. package/src/lanes/registry.ts +0 -178
  263. package/src/lanes/runner.ts +0 -124
  264. package/src/lanes/types.ts +0 -112
  265. package/src/market/catalogs/mcp-docs.json +0 -111
  266. package/src/market/catalogs/plugins.json +0 -111
  267. package/src/market/catalogs/skills.json +0 -478
  268. package/src/market/clone.ts +0 -72
  269. package/src/market/context-cost.ts +0 -121
  270. package/src/market/digest.ts +0 -106
  271. package/src/market/index.ts +0 -22
  272. package/src/market/install.ts +0 -578
  273. package/src/market/manifest.ts +0 -187
  274. package/src/market/prereq.ts +0 -145
  275. package/src/market/registry.ts +0 -363
  276. package/src/market/resolve.ts +0 -111
  277. package/src/market/types.ts +0 -236
  278. package/src/market/validate.ts +0 -227
  279. package/src/mcp/client.ts +0 -449
  280. package/src/mcp/config.ts +0 -252
  281. package/src/mcp/local-package.ts +0 -211
  282. package/src/mcp/market-catalog.ts +0 -84
  283. package/src/mcp/market-install.ts +0 -289
  284. package/src/mcp/market.ts +0 -362
  285. package/src/mcp/oauth.ts +0 -251
  286. package/src/mcp/prompts-resources.ts +0 -249
  287. package/src/mcp/shared.ts +0 -149
  288. package/src/mcp/status.ts +0 -67
  289. package/src/mcp/tools.ts +0 -275
  290. package/src/mcp/transport.ts +0 -122
  291. package/src/mcp/trust.ts +0 -25
  292. package/src/memory/blocks.ts +0 -278
  293. package/src/memory/recall.ts +0 -355
  294. package/src/memory/scope.ts +0 -182
  295. package/src/memory/store.ts +0 -105
  296. package/src/memory/tools.ts +0 -99
  297. package/src/plugins/cli.ts +0 -119
  298. package/src/plugins/discover.ts +0 -108
  299. package/src/plugins/index.ts +0 -50
  300. package/src/plugins/install.ts +0 -184
  301. package/src/plugins/load.ts +0 -124
  302. package/src/plugins/manifest.ts +0 -92
  303. package/src/plugins/state.ts +0 -83
  304. package/src/providers/auth.ts +0 -408
  305. package/src/providers/cache.ts +0 -223
  306. package/src/providers/catalog-local.ts +0 -160
  307. package/src/providers/catalog.ts +0 -421
  308. package/src/providers/middleware-context.ts +0 -86
  309. package/src/providers/middleware.ts +0 -373
  310. package/src/providers/model-list.ts +0 -23
  311. package/src/providers/oauth/common.ts +0 -105
  312. package/src/providers/oauth/device-code.ts +0 -107
  313. package/src/providers/oauth/github-copilot.ts +0 -146
  314. package/src/providers/oauth/loopback.ts +0 -158
  315. package/src/providers/oauth/openai.ts +0 -163
  316. package/src/providers/oauth/openrouter.ts +0 -89
  317. package/src/providers/oauth/pkce.ts +0 -45
  318. package/src/providers/oauth/registry.ts +0 -39
  319. package/src/providers/oauth/seam.ts +0 -89
  320. package/src/providers/profile-glm53.ts +0 -111
  321. package/src/providers/profile-sonnet5-persona.ts +0 -65
  322. package/src/providers/profile-sonnet5-voice.ts +0 -23
  323. package/src/providers/profiles.ts +0 -156
  324. package/src/providers/provider-config.ts +0 -311
  325. package/src/providers/registry.ts +0 -333
  326. package/src/providers/responses.ts +0 -209
  327. package/src/providers/retry.ts +0 -234
  328. package/src/providers/router.ts +0 -294
  329. package/src/providers/sse.ts +0 -26
  330. package/src/providers/stream-errors.ts +0 -117
  331. package/src/providers/stream.ts +0 -566
  332. package/src/providers/thinking.ts +0 -189
  333. package/src/providers/wire-messages.ts +0 -129
  334. package/src/providers/wire-responses.ts +0 -79
  335. package/src/providers/wire-select.ts +0 -53
  336. package/src/server/http.ts +0 -291
  337. package/src/server/openapi.ts +0 -246
  338. package/src/sextant/card-hits.ts +0 -102
  339. package/src/sextant/card-keys.ts +0 -55
  340. package/src/sextant/context-source.ts +0 -157
  341. package/src/sextant/crew-cards.ts +0 -350
  342. package/src/sextant/draw-agents.ts +0 -273
  343. package/src/sextant/draw-code.ts +0 -388
  344. package/src/sextant/draw-context.ts +0 -222
  345. package/src/sextant/draw-frame.ts +0 -164
  346. package/src/sextant/draw-market.ts +0 -573
  347. package/src/sextant/draw-messages.ts +0 -386
  348. package/src/sextant/draw-pet.ts +0 -230
  349. package/src/sextant/draw-plan.ts +0 -187
  350. package/src/sextant/draw-tabs.ts +0 -85
  351. package/src/sextant/draw-util.ts +0 -65
  352. package/src/sextant/draw-wizard.ts +0 -378
  353. package/src/sextant/engine.ts +0 -230
  354. package/src/sextant/frame-hits.ts +0 -25
  355. package/src/sextant/frame.ts +0 -101
  356. package/src/sextant/git-status.ts +0 -197
  357. package/src/sextant/grid.ts +0 -59
  358. package/src/sextant/input.ts +0 -119
  359. package/src/sextant/keys.ts +0 -521
  360. package/src/sextant/layout.ts +0 -86
  361. package/src/sextant/local-commands.ts +0 -169
  362. package/src/sextant/market-source.ts +0 -287
  363. package/src/sextant/mentions.ts +0 -200
  364. package/src/sextant/message-hits.ts +0 -26
  365. package/src/sextant/model.ts +0 -387
  366. package/src/sextant/overlays.ts +0 -456
  367. package/src/sextant/panel-hits.ts +0 -38
  368. package/src/sextant/pet.ts +0 -399
  369. package/src/sextant/screen.ts +0 -324
  370. package/src/sextant/scroll-hits.ts +0 -66
  371. package/src/sextant/scrollbar.ts +0 -82
  372. package/src/sextant/sextant-bridge.ts +0 -174
  373. package/src/sextant/sextant-cards.ts +0 -142
  374. package/src/sextant/sextant-diff-base.ts +0 -63
  375. package/src/sextant/sextant-files.ts +0 -154
  376. package/src/sextant/sextant-frame-loop.ts +0 -335
  377. package/src/sextant/sextant-renderer.ts +0 -574
  378. package/src/sextant/sextant-repo.ts +0 -140
  379. package/src/sextant/theme.ts +0 -66
  380. package/src/sextant/tool-rows.ts +0 -189
  381. package/src/sextant/types.ts +0 -493
  382. package/src/skills/index.ts +0 -387
  383. package/src/skills/pack.ts +0 -220
  384. package/src/skills/spec.ts +0 -162
  385. package/src/skills/tools.ts +0 -69
  386. package/src/skills/versioned.ts +0 -227
  387. package/src/telemetry/otel-export.ts +0 -122
  388. package/src/telemetry/otel-lanes.ts +0 -89
  389. package/src/telemetry/otel-logs.ts +0 -131
  390. package/src/telemetry/otel-metrics.ts +0 -136
  391. package/src/telemetry/otel.ts +0 -397
  392. package/src/telemetry/otlp.ts +0 -76
  393. package/src/tools/ask-user.ts +0 -156
  394. package/src/tools/bash-bg.ts +0 -94
  395. package/src/tools/bash-jobs.ts +0 -237
  396. package/src/tools/design.ts +0 -151
  397. package/src/tools/evalcell.ts +0 -338
  398. package/src/tools/html-text.ts +0 -139
  399. package/src/tools/provider.ts +0 -149
  400. package/src/tools/task.ts +0 -250
  401. package/src/tools/todo.ts +0 -320
  402. package/src/tools/webfetch.ts +0 -332
  403. package/src/tools/websearch.ts +0 -359
  404. package/src/tui/agents-cmd.ts +0 -41
  405. package/src/tui/app.ts +0 -749
  406. package/src/tui/attach.ts +0 -127
  407. package/src/tui/boot-notes.ts +0 -41
  408. package/src/tui/builtin-prompts.ts +0 -59
  409. package/src/tui/checkpoints-cmd.ts +0 -70
  410. package/src/tui/clipboard-image.ts +0 -81
  411. package/src/tui/clipboard.ts +0 -78
  412. package/src/tui/commands.ts +0 -283
  413. package/src/tui/config-view.ts +0 -53
  414. package/src/tui/context-cmds.ts +0 -282
  415. package/src/tui/cost.ts +0 -108
  416. package/src/tui/crash-guard.ts +0 -173
  417. package/src/tui/focus-terminal.ts +0 -34
  418. package/src/tui/git-cmds.ts +0 -273
  419. package/src/tui/git-plain.ts +0 -58
  420. package/src/tui/info-cmd.ts +0 -150
  421. package/src/tui/input-plain.ts +0 -76
  422. package/src/tui/mcp-cmd.ts +0 -128
  423. package/src/tui/memory-note.ts +0 -77
  424. package/src/tui/modes-cmd.ts +0 -45
  425. package/src/tui/notify-seq.ts +0 -100
  426. package/src/tui/notify.ts +0 -318
  427. package/src/tui/overlays.ts +0 -97
  428. package/src/tui/pi-renderer.ts +0 -428
  429. package/src/tui/providers-cmd.ts +0 -377
  430. package/src/tui/reasoning-view.ts +0 -56
  431. package/src/tui/renderer.ts +0 -128
  432. package/src/tui/replay-marker.ts +0 -29
  433. package/src/tui/session-cmd.ts +0 -148
  434. package/src/tui/session-manage.ts +0 -95
  435. package/src/tui/sextant-attach.ts +0 -102
  436. package/src/tui/sextant-io.ts +0 -202
  437. package/src/tui/sextant-smoke.ts +0 -110
  438. package/src/tui/shell-cmd.ts +0 -158
  439. package/src/tui/smoke.ts +0 -72
  440. package/src/tui/staged-terminal.ts +0 -50
  441. package/src/tui/startup.ts +0 -12
  442. package/src/tui/theme.ts +0 -59
  443. package/src/tui/todo-label.ts +0 -7
  444. package/src/tui/trust-card.ts +0 -107
  445. package/src/tui/tui-commands.ts +0 -87
  446. package/tsconfig.json +0 -30
  447. package/vendor/pi-tui/LICENSE +0 -21
  448. package/vendor/pi-tui/PATCHES.md +0 -12
  449. package/vendor/pi-tui/PROVENANCE.md +0 -12
  450. package/vendor/pi-tui/README.upstream.md +0 -854
  451. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  452. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  453. package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
  454. package/vendor/pi-tui/src/autocomplete.ts +0 -827
  455. package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
  456. package/vendor/pi-tui/src/components/box.ts +0 -138
  457. package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
  458. package/vendor/pi-tui/src/components/editor.ts +0 -2364
  459. package/vendor/pi-tui/src/components/h-stack.ts +0 -45
  460. package/vendor/pi-tui/src/components/image.ts +0 -128
  461. package/vendor/pi-tui/src/components/input.ts +0 -448
  462. package/vendor/pi-tui/src/components/loader.ts +0 -93
  463. package/vendor/pi-tui/src/components/markdown.ts +0 -1016
  464. package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
  465. package/vendor/pi-tui/src/components/select-list.ts +0 -230
  466. package/vendor/pi-tui/src/components/settings-list.ts +0 -277
  467. package/vendor/pi-tui/src/components/spacer.ts +0 -29
  468. package/vendor/pi-tui/src/components/stack.ts +0 -155
  469. package/vendor/pi-tui/src/components/text.ts +0 -108
  470. package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
  471. package/vendor/pi-tui/src/components/v-stack.ts +0 -34
  472. package/vendor/pi-tui/src/editor-component.ts +0 -75
  473. package/vendor/pi-tui/src/fuzzy.ts +0 -138
  474. package/vendor/pi-tui/src/index.ts +0 -149
  475. package/vendor/pi-tui/src/keybindings.ts +0 -321
  476. package/vendor/pi-tui/src/keys.ts +0 -1402
  477. package/vendor/pi-tui/src/kill-ring.ts +0 -47
  478. package/vendor/pi-tui/src/latex.ts +0 -1381
  479. package/vendor/pi-tui/src/layout-node.ts +0 -52
  480. package/vendor/pi-tui/src/layout.ts +0 -411
  481. package/vendor/pi-tui/src/native-modifiers.ts +0 -60
  482. package/vendor/pi-tui/src/native-module-path.ts +0 -32
  483. package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
  484. package/vendor/pi-tui/src/terminal-colors.ts +0 -74
  485. package/vendor/pi-tui/src/terminal-image.ts +0 -701
  486. package/vendor/pi-tui/src/terminal.ts +0 -554
  487. package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
  488. package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
  489. package/vendor/pi-tui/src/tui.ts +0 -1264
  490. package/vendor/pi-tui/src/undo-stack.ts +0 -29
  491. package/vendor/pi-tui/src/utils.ts +0 -1327
  492. package/vendor/pi-tui/src/word-navigation.ts +0 -118
  493. package/vendor/pi-tui/test/test-themes.ts +0 -39
  494. package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
  495. /package/{src/providers → dist/cli}/models-index.json +0 -0
@@ -1,469 +0,0 @@
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, statSync } 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
- import { oneLineTitle, previewText } from "./session-text.ts";
11
-
12
- export type Entry = Message | ({ id: string; kind: "event"; parentId: string | null; createdAt: number; event: RunEvent });
13
-
14
- /** Port #34 image sidecars (session-images.ts): `<session>/attachments/<sha256>.<ext>`. The store's
15
- * sidecar writer references them from entries.jsonl in ONE form, the session-relative
16
- * `attachments/<file>`, resolved to an absolute path in memory only. Any other persisted path —
17
- * whatever wrote it — never resolves to a readable file: a non-canonical relative path stays
18
- * relative (F2) and an absolute path is dropped at load (F3), so the part lowers to a "file
19
- * unavailable" placeholder and no line in entries.jsonl can point a hydrated part at a file
20
- * outside `<session>/attachments/`.
21
- * `rovecode export --json` copies entries.jsonl ALONE — the attachments directory travels with the
22
- * session directory, not with the export (the JSONL stays a small, verbatim-copyable record). */
23
-
24
- export type CorruptionKind =
25
- | "orphan-entry" // parentId points at nothing
26
- | "cycle" // ancestry loop
27
- | "duplicate-id"
28
- | "malformed-json"
29
- | "unknown-shape"
30
- | "chain-broken"; // prevHash disagrees with the parent entry's hash (tree/chain fork)
31
-
32
- export interface Corruption { kind: CorruptionKind; entryId?: string; line: number; detail: string }
33
-
34
- /** `leaf` (optional, port #2): durable active-leaf pointer. Absent = legacy = last entry wins.
35
- * `title` / `forkedFrom` (aion port #84, 2026-09-07): a user-given name (`rovecode sessions rename`, one-lined on
36
- * write AND on read — session-text.ts) and the id a fork was copied from. Written only through patchMeta. */
37
- export interface SessionMeta { id: string; createdAt: number; goal?: string; model?: string; leaf?: string; title?: string; forkedFrom?: string }
38
-
39
- /** Hash chain: each entry carries sha256(prevHash + canonical(entry)). Tamper-evident replay.
40
- * Accepts the wrapped envelope too — the chain hashes the full wrapper (hash field empty). */
41
- export function chainHash(prev: string, entry: Entry | object): string {
42
- const canon = JSON.stringify(sortKeys(entry));
43
- return createHash("sha256").update(prev + canon).digest("hex");
44
- }
45
-
46
- function sortKeys(v: unknown): unknown {
47
- if (Array.isArray(v)) return v.map(sortKeys);
48
- if (v && typeof v === "object") {
49
- return Object.fromEntries(
50
- Object.entries(v as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)).map(([k, x]) => [k, sortKeys(x)])
51
- );
52
- }
53
- return v;
54
- }
55
-
56
- interface Wrapped { id: string; parentId: string | null; createdAt: number; prevHash: string; hash: string; entry: Entry }
57
-
58
- /** Shape of a loaded entry (ADR-004: classify foreign/corrupt lines, never crash on them):
59
- * "event" = kind "event"; "message" = a role plus a parts ARRAY whose members are objects with a
60
- * string kind; undefined = anything else (null, a scalar, `parts: [null]`, `parts: "x"`, `{}`) —
61
- * reload reports it as unknown-shape, path()/turnPoints() skip it, hydration leaves it alone. */
62
- export function entryShape(e: unknown): "message" | "event" | undefined {
63
- if (!e || typeof e !== "object") return undefined;
64
- if ((e as { kind?: unknown }).kind === "event") return "event";
65
- const m = e as { role?: unknown; parts?: unknown };
66
- if (!("role" in m) || !Array.isArray(m.parts)) return undefined;
67
- return m.parts.every((p: unknown) => !!p && typeof p === "object" && typeof (p as { kind?: unknown }).kind === "string") ? "message" : undefined;
68
- }
69
-
70
- /** Event entry (kind "event") vs message — tolerant of foreign/corrupt entry shapes. */
71
- function isEventWrapped(w: Wrapped): boolean { return entryShape(w.entry) === "event"; }
72
-
73
- export interface SessionSummary {
74
- id: string;
75
- createdAt: number;
76
- updatedAt: number; // max entry createdAt, else meta createdAt
77
- entryCount: number;
78
- preview: string; // first user-message text, single-line, ≤80 chars, "" if none
79
- /** user messages recorded in the session file (every branch) */
80
- turns: number;
81
- /** the user-given title (meta.json `title`, one-lined on read) — the key is present ONLY when one is set */
82
- title?: string;
83
- }
84
-
85
- export interface ScanOptions {
86
- /** read the details (meta + entries) of at most this many sessions, newest file first; the rest are not opened */
87
- limit?: number;
88
- /** also list the hollow directories (a meta.json and no entry ever written) as sessions with entryCount 0 */
89
- includeHollow?: boolean;
90
- }
91
-
92
- export interface SessionScan {
93
- /** the sessions that hold something, newest first (plus the hollow ones when asked) */
94
- sessions: SessionSummary[];
95
- /** directory names that have a meta.json but no entries.jsonl (or an empty one) — nothing was ever written
96
- * to them. Counted from the stat alone, never opened; `rovecode sessions` prints the count so the question of
97
- * deleting them stays askable (Berkay has not decided; 35 of 36 on his machine, 2026-09-07). */
98
- hollow: string[];
99
- }
100
-
101
- /** Scan rootDir WITHOUT opening every file (2026-09-07): one readdir, one stat of entries.jsonl per directory, and
102
- * meta.json + entries are read only for the directories that hold something — and only the newest `limit` of those
103
- * when a limit is given (`--continue` needs one session, the picker twenty; the file's mtime orders the candidates
104
- * before anything is opened, so a limit trusts the filesystem clock rather than the entries' own timestamps). The
105
- * scan used to read the whole entries.jsonl of every directory to answer "which is newest": 36 directories on one
106
- * machine, 35 of them empty, for one answer. A directory with neither file is foreign and skipped silently as
107
- * before; corrupt files never throw. Sorted updatedAt desc. */
108
- export function scanSessions(rootDir: string, opts: ScanOptions = {}): SessionScan {
109
- const hollow: string[] = [];
110
- const candidates: { name: string; mtime: number }[] = [];
111
- let names: string[];
112
- try { names = readdirSync(rootDir); } catch { return { sessions: [], hollow }; }
113
- for (const name of names) {
114
- let size = -1, mtime = 0;
115
- try { const st = statSync(join(rootDir, name, "entries.jsonl")); if (st.isFile()) { size = st.size; mtime = st.mtimeMs; } } catch { /* no entries file */ }
116
- if (size > 0) { candidates.push({ name, mtime }); continue; }
117
- try { if (statSync(join(rootDir, name, "meta.json")).isFile()) hollow.push(name); } catch { /* neither file: foreign, skipped */ }
118
- }
119
- candidates.sort((a, b) => b.mtime - a.mtime);
120
- const chosen = opts.limit !== undefined ? candidates.slice(0, Math.max(0, opts.limit)) : candidates;
121
- const sessions: SessionSummary[] = [];
122
- for (const { name } of chosen) { const s = summarize(rootDir, name); if (s) sessions.push(s); }
123
- if (opts.includeHollow) for (const name of hollow) { const s = summarize(rootDir, name); if (s) sessions.push(s); }
124
- return { sessions: sessions.sort((a, b) => b.updatedAt - a.updatedAt), hollow };
125
- }
126
-
127
- /** one directory's summary: meta.json (identity = the DIRECTORY name, never meta.id — a copied/tampered meta.json
128
- * must not redirect resume to another path) plus a pass over entries.jsonl; undefined for a corrupt or foreign dir */
129
- function summarize(rootDir: string, name: string): SessionSummary | undefined {
130
- try {
131
- const raw: unknown = JSON.parse(readFileSync(join(rootDir, name, "meta.json"), "utf8"));
132
- if (!raw || typeof raw !== "object") return undefined;
133
- const meta = raw as SessionMeta;
134
- if (typeof meta.id !== "string" || typeof meta.createdAt !== "number") return undefined;
135
- let updatedAt = meta.createdAt; let entryCount = 0; let preview = ""; let turns = 0;
136
- const entriesPath = join(rootDir, name, "entries.jsonl");
137
- if (existsSync(entriesPath)) {
138
- for (const line of readFileSync(entriesPath, "utf8").split("\n")) {
139
- if (!line.trim()) continue;
140
- let parsed: unknown;
141
- try { parsed = JSON.parse(line); } catch { continue; }
142
- if (!parsed || typeof parsed !== "object") continue;
143
- const w = parsed as Wrapped;
144
- entryCount++;
145
- if (typeof w.createdAt === "number" && w.createdAt > updatedAt) updatedAt = w.createdAt;
146
- const e: unknown = w.entry;
147
- if (entryShape(e) === "message" && (e as Message).role === "user") {
148
- turns++;
149
- if (!preview) preview = previewText(e as Message);
150
- }
151
- }
152
- }
153
- const title = typeof meta.title === "string" ? oneLineTitle(meta.title) : undefined; // one-lined on READ; "" → no key
154
- return { id: name, createdAt: meta.createdAt, updatedAt, entryCount, preview, turns, ...(title !== undefined ? { title } : {}) };
155
- } catch { return undefined; /* foreign or corrupt dir: skip, never throw */ }
156
- }
157
-
158
- /** The sessions that hold something, newest first — scanSessions without the hollow count. `opts.includeHollow`
159
- * lists the hollow directories too (the TUI picker and the CLI table do not; an exact id still opens one). */
160
- export function listSessions(rootDir: string, opts: ScanOptions = {}): SessionSummary[] {
161
- return scanSessions(rootDir, opts).sessions;
162
- }
163
-
164
- /** The session `rovecode --continue` (or `--resume` with no id) reopens: the most recently updated one that
165
- * HOLDS something. Costs one readdir, one stat per directory and ONE read (the newest file); a newest file made
166
- * only of unparseable lines falls back to the full list rather than answering "nothing". */
167
- export function newestSession(rootDir: string): SessionSummary | undefined {
168
- const first = scanSessions(rootDir, { limit: 1 }).sessions[0];
169
- if (first !== undefined && first.entryCount > 0) return first;
170
- return listSessions(rootDir).find((s) => s.entryCount > 0);
171
- }
172
-
173
- export interface TurnPoint {
174
- entryId: string; // the user message's wrapped-entry id
175
- index: number; // 1-based position among user turns on the active path
176
- text: string; // single-line preview ≤80 chars (overlay label ONLY)
177
- /** untruncated message text — the edit-and-resubmit prefill (pi sessions.md:113) */
178
- fullText: string;
179
- parentId: string | null; // the entry's parent (rewind target: leaf moves HERE)
180
- branches: number; // children of parentId in the whole tree MINUS the active-path child (0 = linear)
181
- }
182
-
183
- export class SessionStore {
184
- private readonly dir: string;
185
- private leaf = "root";
186
- private prevHash = "";
187
- private cache: Wrapped[] = [];
188
- private meta: SessionMeta;
189
- /** true once meta.json carries a leaf field — appends then keep it in step. */
190
- private leafPersisted = false;
191
- /** port #34: image parts waiting for the next user message (stageAttachments) */
192
- private staged: ImagePart[] = [];
193
-
194
- /** Opening a store touches nothing on disk. The directory and meta.json appear with the FIRST entry
195
- * (append, appendEvent, branch) — see materialize(). Constructing used to write both immediately, so
196
- * every start that never got a prompt (`rovecode --help` paths that boot a runtime, `rovecode context`,
197
- * a TUI opened and closed, every test) left `<sessions>/<uuid>/meta.json` behind: dozens of empty
198
- * directories that listSessions offered as sessions with nothing in them. Deleting them afterwards was
199
- * the other option, and it races: process B prunes the directory process A has just opened and not
200
- * yet written to. Not creating it has no such window. An existing session (meta.json + entries) opens
201
- * exactly as before; nothing is ever deleted. */
202
- constructor(rootDir: string, public readonly id: string) {
203
- this.dir = join(rootDir, id);
204
- this.meta = { id, createdAt: Date.now() };
205
- this.reload();
206
- }
207
-
208
- private get file() { return join(this.dir, "entries.jsonl"); }
209
-
210
- /** Make the session real on disk: the directory plus meta.json (once). Called on every write path,
211
- * before the write. Idempotent and cheap (a mkdir on an existing directory and one stat), so it also
212
- * means a session whose empty directory was swept away by hand recovers on its next entry instead of
213
- * throwing ENOENT from appendFileSync. `createdAt` is the construction time, written whenever the first
214
- * entry lands — the session began when it was opened, not when someone first spoke. */
215
- private materialize(): void {
216
- mkdirSync(this.dir, { recursive: true });
217
- const metaP = join(this.dir, "meta.json");
218
- if (!existsSync(metaP)) writeFileSync(metaP, JSON.stringify(this.meta, null, 2));
219
- }
220
-
221
- /** Replay JSONL → cache; detect corruption instead of crashing (pi reducer pattern).
222
- * Restores a persisted leaf (durable branch) when meta.json names an existing entry;
223
- * missing/invalid leaf falls back to the last tree-linked, well-shaped entry (LOW-A below). */
224
- reload(): Corruption[] {
225
- this.cache = [];
226
- this.leaf = "root"; this.prevHash = ""; // a replay that finds no leaf (no file, only foreign lines) is a fresh root
227
- const seen = new Set<string>();
228
- const corrupt: Corruption[] = [];
229
- let persistedLeaf: string | undefined;
230
- this.leafPersisted = false;
231
- try {
232
- const raw: unknown = JSON.parse(readFileSync(join(this.dir, "meta.json"), "utf8"));
233
- if (raw && typeof raw === "object") {
234
- const m = raw as SessionMeta;
235
- if (typeof m.id === "string" && typeof m.createdAt === "number") this.meta = m;
236
- if (typeof m.leaf === "string") { persistedLeaf = m.leaf; this.leafPersisted = true; }
237
- }
238
- } catch { /* unreadable meta behaves as legacy (no persisted leaf) */ }
239
- if (!existsSync(this.file)) return corrupt;
240
- const lines = readFileSync(this.file, "utf8").split("\n").filter(Boolean);
241
- const byLine = new Map<string, Wrapped>(); // ids seen so far (first occurrence wins)
242
- // LOW-A (#34): the fallback leaf = the last entry that is BOTH tree-linked (a root, or its parent seen
243
- // above it) AND a message/event. As cache.at(-1) it was whatever id-bearing line came last, so a
244
- // foreign `{"id":"ghost","parentId":"nope",…}` hijacked the active path (messages() shrank to the
245
- // ghost, the next append parented on it) and a bare `{"id":"bare"}` emptied it and re-rooted the next
246
- // append — silently: only `rovecode trace` shows these findings, the constructor discards them. Such
247
- // lines stay in the cache for chain/reporting and keep their orphan-entry / unknown-shape findings.
248
- let tail: Wrapped | undefined;
249
- lines.forEach((line, i) => {
250
- let w: Wrapped;
251
- try {
252
- w = JSON.parse(line) as Wrapped;
253
- } catch {
254
- corrupt.push({ kind: "malformed-json", line: i, detail: `unparseable line ${i}` });
255
- return;
256
- }
257
- // F1: a line that parses but is not an entry object (null, a number, a string) is reported, never dereferenced
258
- if (!w || typeof w !== "object") { corrupt.push({ kind: "unknown-shape", line: i, detail: "line is not an entry object" }); return; }
259
- // F4: an object line with no string id (`{"entry":null}`) is not a tree node either — reported and
260
- // skipped; cached, it became the leaf (cache.at(-1)) with id undefined, emptied messages() and
261
- // re-rooted the next append (the loop parents on history.at(-1) ?? null)
262
- if (typeof w.id !== "string") { corrupt.push({ kind: "unknown-shape", line: i, detail: "entry line has no string id" }); return; }
263
- if (seen.has(w.id)) corrupt.push({ kind: "duplicate-id", entryId: w.id, line: i, detail: "duplicate id" });
264
- seen.add(w.id);
265
- const orphan = w.parentId !== null && !seen.has(w.parentId);
266
- if (orphan) corrupt.push({ kind: "orphan-entry", entryId: w.id, line: i, detail: `parent ${w.parentId} missing` });
267
- // chain linkage: prevHash must equal the PARENT's hash ("" for roots). A leaf moved
268
- // mid-run used to fork the hash chain away from the parent pointer — detect it.
269
- // Missing parents are skipped here (already reported as orphan-entry above).
270
- const expected = w.parentId === null ? "" : byLine.get(w.parentId)?.hash;
271
- if (expected !== undefined && w.prevHash !== expected) {
272
- corrupt.push({ kind: "chain-broken", entryId: w.id, line: i, detail: `prevHash disagrees with parent ${w.parentId ?? "(root)"}` });
273
- }
274
- if (!byLine.has(w.id)) byLine.set(w.id, w);
275
- // F1: a foreign/corrupt entry (null, a scalar, parts:[null], …) is reported and kept in the tree
276
- // for chain purposes; path()/turnPoints() skip it and hydrateImages leaves it untouched
277
- const shape = entryShape(w.entry);
278
- if (shape === undefined) corrupt.push({ kind: "unknown-shape", entryId: w.id, line: i, detail: "entry is neither a message nor an event" });
279
- w.entry = this.hydrateImages(w.entry); // session-relative sidecar paths → absolute (in memory only)
280
- this.cache.push(w);
281
- if (!orphan && shape !== undefined) tail = w;
282
- });
283
- // cycle check over ancestry
284
- const byId = new Map(this.cache.map((w) => [w.id, w]));
285
- for (const w of this.cache) {
286
- const anc = new Set<string>(); let cur: Wrapped | undefined = w;
287
- while (cur && cur.parentId !== null) {
288
- if (anc.has(cur.id)) { corrupt.push({ kind: "cycle", entryId: w.id, line: -1, detail: "ancestry cycle" }); break; }
289
- anc.add(cur.id); cur = byId.get(cur.parentId);
290
- }
291
- }
292
- if (tail) { this.leaf = tail.id; this.prevHash = tail.hash; }
293
- if (persistedLeaf !== undefined) {
294
- const w = byId.get(persistedLeaf);
295
- if (w) { this.leaf = w.id; this.prevHash = w.hash; }
296
- }
297
- return corrupt;
298
- }
299
-
300
- append(entry: Entry): void {
301
- // A supplied parentId can lag the leaf (the loop snapshots history at run start;
302
- // /new and /rewind may move the leaf mid-run). The chain must follow the PARENT
303
- // pointer, never the moved leaf, or hash chain and tree silently disagree.
304
- this.materialize();
305
- const supplied = (entry as { parentId?: string | null }).parentId;
306
- const parentId = supplied !== undefined ? supplied : this.leaf;
307
- let prevHash = this.prevHash;
308
- if (parentId !== this.leaf) {
309
- prevHash = parentId === null ? "" : (this.cache.find((c) => c.id === parentId)?.hash ?? this.prevHash);
310
- }
311
- // port #34: staged attachments ride on this user message — folded into the caller's parts
312
- // array IN PLACE, because the loop persists the very object it keeps in its history
313
- // (loop.ts:116-122); that is what puts the image on the wire this run without a loop change
314
- if (this.staged.length > 0 && "role" in entry && entry.role === "user") {
315
- entry.parts.push(...this.staged);
316
- this.staged = [];
317
- }
318
- const persisted = this.sidecarImages(entry); // inline bytes → sidecar files; entries.jsonl stays small
319
- const w: Wrapped = {
320
- id: entry.id, parentId,
321
- createdAt: entry.createdAt ?? Date.now(),
322
- prevHash,
323
- hash: "",
324
- entry: persisted,
325
- };
326
- w.hash = chainHash(prevHash, w);
327
- appendFileSync(this.file, JSON.stringify(w) + "\n");
328
- this.cache.push(persisted === entry ? w : { ...w, entry: this.hydrateImages(persisted) });
329
- this.leaf = w.id; this.prevHash = w.hash;
330
- if (this.leafPersisted) this.persistLeaf(); // keep the durable leaf in step after a branch
331
- }
332
-
333
- /** Port #34 TUI attach path (`/attach <path>` → next submit): image parts staged here are
334
- * appended to the parts of the NEXT user message that lands in append(), then cleared.
335
- * System/assistant/tool entries in between (a pending mode switch, tool results) leave the
336
- * stage untouched. Replaces any earlier stage; `[]` clears it. */
337
- stageAttachments(parts: readonly ImagePart[]): void { this.staged = [...parts]; }
338
-
339
- get stagedAttachments(): readonly ImagePart[] { return this.staged; }
340
-
341
- /** The on-disk form (session-images.ts sidecarImageParts): inline image bytes → sidecar files +
342
- * session-relative paths. Same object when there is nothing to do or the shape is foreign (F1). */
343
- private sidecarImages(entry: Entry): Entry {
344
- if (entryShape(entry) !== "message" || !("role" in entry)) return entry;
345
- const parts = sidecarImageParts(this.dir, entry.parts);
346
- return parts === undefined ? entry : { ...entry, parts };
347
- }
348
-
349
- /** The in-memory form (session-images.ts hydrateImageParts): canonical sidecar paths → absolute
350
- * under this session's dir; a persisted absolute path is dropped (F3), any other relative one
351
- * stays put (F2). Same object when there is nothing to do or the shape is foreign (F1). */
352
- private hydrateImages(entry: Entry): Entry {
353
- if (entryShape(entry) !== "message" || !("role" in entry)) return entry;
354
- const parts = hydrateImageParts(this.dir, entry.parts);
355
- return parts === undefined ? entry : { ...entry, parts };
356
- }
357
-
358
- /** Persist a RunEvent as an ANNOTATION of the current leaf (port #25: the loop's compaction
359
- * marker). The entry hangs off the leaf without becoming one — the loop parents its next
360
- * message on the last real message (history.at(-1)), so a chain-linked event would turn
361
- * into a dead sibling the moment that message lands. Hash-chained like any entry; path()
362
- * folds it back in right after the message it annotates; messages() never sees it. */
363
- appendEvent(event: RunEvent): Entry {
364
- this.materialize();
365
- const parentId = this.leaf === "root" ? null : this.leaf;
366
- const entry: Entry = { id: randomUUID(), kind: "event", parentId, createdAt: Date.now(), event };
367
- const w: Wrapped = { id: entry.id, parentId, createdAt: entry.createdAt, prevHash: this.prevHash, hash: "", entry };
368
- w.hash = chainHash(this.prevHash, w);
369
- appendFileSync(this.file, JSON.stringify(w) + "\n");
370
- this.cache.push(w);
371
- return entry;
372
- }
373
-
374
- /** Active path = root → leaf (omp buildSessionContext); unknown-shape entries (reload F1) are skipped. */
375
- path(): Entry[] { return this.wrappedPath().filter((w) => entryShape(w.entry) !== undefined).map((w) => w.entry); }
376
-
377
- private wrappedPath(): Wrapped[] {
378
- const byId = new Map(this.cache.map((w) => [w.id, w]));
379
- const out: Wrapped[] = [];
380
- let cur = byId.get(this.leaf);
381
- // A tampered or corrupt file can carry an ancestry cycle (reload() already reports it as `cycle`);
382
- // walking it here looped forever and died with `RangeError: Out of memory`, so ANY surface that
383
- // resumed such a session — run --resume, trace, export, context — crashed instead of reporting the
384
- // corruption. Found by the gauntlet's session-tamper case (eval/gauntlet-wave4.ts), 2026-09-07. The
385
- // walk now stops at the first id it has already seen: the path truncates there, the finding stays on
386
- // reload()'s list, and nothing hangs.
387
- const seen = new Set<string>();
388
- while (cur && !seen.has(cur.id)) { seen.add(cur.id); out.unshift(cur); cur = cur.parentId ? byId.get(cur.parentId) : undefined; }
389
- // event annotations (appendEvent) are children of path entries but never parents: fold each
390
- // in right after the entry it annotates, file order. Chain-linked events (a supplied
391
- // parentId via append, e.g. the export fixture) are already on the path and stay put.
392
- const onPath = new Set(out.map((w) => w.id));
393
- const notes = new Map<string | null, Wrapped[]>();
394
- for (const w of this.cache) {
395
- if (onPath.has(w.id) || !isEventWrapped(w)) continue;
396
- if (w.parentId !== null && !onPath.has(w.parentId)) continue;
397
- const list = notes.get(w.parentId) ?? []; list.push(w); notes.set(w.parentId, list);
398
- }
399
- if (notes.size === 0) return out;
400
- const folded: Wrapped[] = [...(notes.get(null) ?? [])];
401
- for (const w of out) folded.push(w, ...(notes.get(w.id) ?? []));
402
- return folded;
403
- }
404
-
405
- /** User turns along the ACTIVE path only, root→leaf order. */
406
- turnPoints(): TurnPoint[] {
407
- const children = new Map<string | null, number>();
408
- // event annotations hang off entries without forking them: not a branch (port #25)
409
- for (const w of this.cache) if (!isEventWrapped(w)) children.set(w.parentId, (children.get(w.parentId) ?? 0) + 1);
410
- const out: TurnPoint[] = [];
411
- for (const w of this.wrappedPath()) {
412
- const e = w.entry;
413
- if (entryShape(e) !== "message" || !("role" in e) || e.role !== "user") continue; // F1: foreign shapes skipped
414
- out.push({
415
- entryId: w.id,
416
- index: out.length + 1,
417
- text: previewText(e),
418
- fullText: e.parts.filter((p): p is TextPart => p.kind === "text").map((p) => p.text).join(""),
419
- parentId: w.parentId,
420
- branches: (children.get(w.parentId) ?? 1) - 1,
421
- });
422
- }
423
- return out;
424
- }
425
-
426
- /** Branch: move leaf back to an earlier entry without deleting anything.
427
- * Durable (port #2): the leaf survives restarts via meta.json. Unknown id → false. */
428
- branch(entryId: string): boolean {
429
- const target = this.cache.find((w) => w.id === entryId);
430
- if (!target) return false;
431
- this.leaf = target.id;
432
- this.prevHash = target.hash;
433
- this.persistLeaf();
434
- return true;
435
- }
436
-
437
- /** Atomically rewrite meta.json carrying the active leaf (write tmp + rename). */
438
- private persistLeaf(): void {
439
- this.materialize();
440
- this.meta = { ...this.meta, leaf: this.leaf };
441
- this.writeMeta(this.meta);
442
- this.leafPersisted = true;
443
- }
444
-
445
- /** The ONE meta writer for everything that is not the leaf (aion port #84): `title`, `forkedFrom`, `createdAt`.
446
- * Reads the file as it is on disk so unknown keys and their order survive byte-for-byte, applies the patch, and
447
- * forces `id` = the directory name (a meta.json copied from another session self-heals). The entries file is
448
- * untouched. Materialises the directory first, so a fork that copied meta.json in already exists. */
449
- patchMeta(patch: Partial<Pick<SessionMeta, "title" | "forkedFrom" | "createdAt">>): void {
450
- this.materialize();
451
- let current: Record<string, unknown> = {};
452
- try {
453
- const raw: unknown = JSON.parse(readFileSync(join(this.dir, "meta.json"), "utf8"));
454
- if (raw && typeof raw === "object" && !Array.isArray(raw)) current = raw as Record<string, unknown>;
455
- } catch { /* unreadable: rebuild from what this store knows */ current = { ...this.meta }; }
456
- const next = { ...current, ...patch, id: this.id } as Record<string, unknown>;
457
- this.writeMeta(next);
458
- this.meta = { ...this.meta, ...patch, id: this.id };
459
- }
460
-
461
- private writeMeta(meta: object): void {
462
- const metaP = join(this.dir, "meta.json");
463
- const tmp = metaP + ".tmp";
464
- writeFileSync(tmp, JSON.stringify(meta, null, 2));
465
- renameSync(tmp, metaP);
466
- }
467
-
468
- messages(): Message[] { return this.path().filter((e): e is Message => "role" in e); }
469
- }
@@ -1,170 +0,0 @@
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 { isTrustedFile, untrustedFileNote } from "./trust.ts";
22
- import { THINKING_EFFORTS, type PermissionLevel, type ThinkingEffort } from "./types.ts";
23
-
24
- export type SettingsScope = "user" | "project";
25
-
26
- /** The keys whose value is something rovecode EXECUTES — a shell string, an argv table, an argv. From the PROJECT file
27
- * they are honoured only once that file is trusted on this machine (core/trust.ts): a cloned repo must not decide what
28
- * we run. The user file is the person's own and is never gated. ONE list, enforced inside loadSettings itself, so a key
29
- * added later joins the gate by being named here — not by every consumer remembering to ask. `verify: false` is a
30
- * refusal, not a command, and stays honoured from any file. */
31
- export const COMMAND_KEYS = ["verify", "lsp", "notify_command"] as const;
32
- export type CommandKey = (typeof COMMAND_KEYS)[number];
33
-
34
- export interface Settings {
35
- /** how much the human is asked — see types.ts PermissionLevel */
36
- permission?: PermissionLevel;
37
- /** how hard the model thinks before answering */
38
- effort?: ThinkingEffort;
39
- /** terminal notifications when a run ends or a card needs you — default true; `false` turns them off.
40
- * A terminal that beeps when you did not ask is worse than silence, so this is one key, in the file
41
- * you already have, rather than a flag you have to remember every launch. Since 2026-09-07 the signal
42
- * fires only while the terminal is UNFOCUSED (tui/notify.ts; `notify_when: "always"` restores every run). */
43
- bell?: boolean;
44
- /** the notification method (tui/notify.ts): auto (default — an OSC 9 toast on Ghostty / iTerm2 / kitty / Warp /
45
- * WezTerm, the bell everywhere else) | bell | osc9 | osc777. ROVECODE_NOTIFY overrides it (off = `bell: false`). */
46
- notify?: "auto" | "bell" | "osc9" | "osc777";
47
- /** when to notify: unfocused (default — the terminal must report focus, which Windows Terminal ≥ 1.14, Ghostty,
48
- * kitty, WezTerm, iTerm2, xterm and VTE do) | always. ROVECODE_NOTIFY_WHEN overrides it. */
49
- notify_when?: "unfocused" | "always";
50
- /** a desktop hook run on every notification, under the same gate: argv as a JSON string array
51
- * (`["notify-send","rovecode"]`) or whitespace-split words, the JSON payload appended as its last argument —
52
- * never a shell. TRUST-REQUIRED from the project file: a repo-supplied command applies only once that file is
53
- * approved in the digest store (mcp/trust.ts); the user file and ROVECODE_NOTIFY_COMMAND always may. */
54
- notify_command?: string;
55
- /** the check the loop runs after the agent's last edit before its reply counts as done (core/verify.ts):
56
- * one shell command, or a list run in order; `false` turns the gate off AND stops any inference. The
57
- * one source that needs no guessing — a project should set this rather than let a `test` script be
58
- * inferred, because a gate that runs the wrong command once is turned off forever. */
59
- verify?: string | string[] | false;
60
- /** the LSP server table (coding/lsp-servers.ts): `ext[,ext]=argv;…`, `ext=off`, or `off` — merged over the
61
- * built-in typescript-language-server entry; ROVECODE_LSP overrides it. Kept as written: a malformed table is
62
- * named at boot and in `rovecode doctor` (coding/lsp-gate.ts lspAvailabilityNotes) instead of being dropped here
63
- * in silence; the gate applies its valid entries. */
64
- lsp?: string;
65
- }
66
-
67
- const FILE = "settings.json";
68
-
69
- export function settingsPath(scope: SettingsScope, cwd: string): string {
70
- return scope === "user" ? join(rovecodeHome(), FILE) : join(cwd, ".rovecode", FILE);
71
- }
72
-
73
- const PERMISSIONS: readonly string[] = ["ask", "accept-edits", "auto"];
74
-
75
- /** Only keys we recognize survive, and only with values we recognize: a hand-edited
76
- * `"permission": "yes"` must not become a truthy something downstream. */
77
- function sanitize(raw: unknown): Settings {
78
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
79
- const r = raw as Record<string, unknown>;
80
- const out: Settings = {};
81
- if (typeof r.permission === "string" && PERMISSIONS.includes(r.permission)) out.permission = r.permission as PermissionLevel;
82
- if (typeof r.effort === "string" && (THINKING_EFFORTS as readonly string[]).includes(r.effort)) out.effort = r.effort as ThinkingEffort;
83
- if (typeof r.bell === "boolean") out.bell = r.bell; // "off"/"no" are not false: a string is ignored, the bell stays on
84
- if (typeof r.notify === "string" && ["auto", "bell", "osc9", "osc777"].includes(r.notify)) out.notify = r.notify as Settings["notify"];
85
- if (r.notify_when === "unfocused" || r.notify_when === "always") out.notify_when = r.notify_when;
86
- // notify_command: one non-blank string (the argv grammar lives in tui/notify-seq.ts parseArgv); blank means "not set"
87
- if (typeof r.notify_command === "string" && r.notify_command.trim() !== "" && r.notify_command.length <= 2_000) out.notify_command = r.notify_command;
88
- // verify: a non-empty command string, a list of them (empty strings dropped, at most 8, each under 500 chars),
89
- // or `false`. Not a table with the other keys on purpose: the shapes differ (two enums, a boolean, this).
90
- if (r.verify === false) out.verify = false;
91
- else if (typeof r.verify === "string" && r.verify.trim() !== "" && r.verify.length <= 500) out.verify = r.verify.trim();
92
- else if (Array.isArray(r.verify)) {
93
- const cmds = r.verify.filter((v): v is string => typeof v === "string" && v.trim() !== "" && v.length <= 500).map((v) => v.trim()).slice(0, 8);
94
- if (cmds.length > 0) out.verify = cmds;
95
- }
96
- // lsp: one string (a table can be a whole line; 2 000 chars is room for a dozen servers with quoted paths); blank
97
- // means "not set" — the knob's own grammar treats blank as the defaults anyway. Not validated here: see the key's doc.
98
- if (typeof r.lsp === "string" && r.lsp.trim() !== "" && r.lsp.length <= 2_000) out.lsp = r.lsp;
99
- return out;
100
- }
101
-
102
- function readOne(path: string): Settings {
103
- try { return sanitize(JSON.parse(readFileSync(path, "utf8"))); } catch { return {}; }
104
- }
105
-
106
- /** ONE settings file, sanitized and UNGATED — what the file says, for `rovecode trust show` (a person deciding to
107
- * approve a file needs to see what it would do) and for a reader that must know WHICH file said something */
108
- export function readSettingsFile(path: string): Settings { return readOne(path); }
109
-
110
- /** the command-bearing keys an untrusted project file carries (`verify: false` excepted — a refusal is not a command) */
111
- function commandKeysIn(s: Settings): CommandKey[] {
112
- return COMMAND_KEYS.filter((k) => s[k] !== undefined && !(k === "verify" && s.verify === false));
113
- }
114
-
115
- export interface ScopedSettings {
116
- user: Settings;
117
- /** the project file with its command-bearing keys REMOVED when the file is not trusted on this machine */
118
- project: Settings;
119
- projectPath: string;
120
- /** the command-bearing keys the project file carried and lost to the gate — empty when trusted or none */
121
- dropped: CommandKey[];
122
- }
123
-
124
- /** the two scopes apart, the project layer already gated (the trust store is keyed by `projectPath`) */
125
- export function loadSettingsScoped(cwd: string, home: string = rovecodeHome()): ScopedSettings {
126
- const projectPath = settingsPath("project", cwd);
127
- const raw = readOne(projectPath);
128
- const carried = commandKeysIn(raw);
129
- if (carried.length === 0 || isTrustedFile(home, projectPath)) return { user: readOne(settingsPath("user", cwd)), project: raw, projectPath, dropped: [] };
130
- const project: Settings = { ...raw };
131
- for (const k of carried) delete project[k];
132
- return { user: readOne(settingsPath("user", cwd)), project, projectPath, dropped: carried };
133
- }
134
-
135
- /** user then project, project winning key by key — a repo may pin the permission level while the thinking effort
136
- * stays whatever you chose globally. The project's command-bearing keys (COMMAND_KEYS) are here ONLY when that file is
137
- * trusted: every consumer — verify, lsp, notify — is safe by construction, none of them has to ask. */
138
- export function loadSettings(cwd: string): Settings {
139
- const s = loadSettingsScoped(cwd);
140
- return { ...s.user, ...s.project };
141
- }
142
-
143
- /** the one boot / doctor line when the gate dropped something from the project file; [] otherwise */
144
- export function settingsTrustNotes(cwd: string, home: string = rovecodeHome()): string[] {
145
- const s = loadSettingsScoped(cwd, home);
146
- if (s.dropped.length === 0) return [];
147
- const keys = s.dropped.join(", ");
148
- return [untrustedFileNote(s.projectPath, `its ${keys} key${s.dropped.length === 1 ? " is" : "s are"} ignored (a repo file would decide what we run)`)];
149
- }
150
-
151
- /** Merge one key into a scope's file, leaving the rest of it (and the other scope) alone. */
152
- export function saveSetting<K extends keyof Settings>(key: K, value: Settings[K], scope: SettingsScope, cwd: string): string {
153
- const path = settingsPath(scope, cwd);
154
- const next: Settings = { ...readOne(path), [key]: value };
155
- mkdirSync(dirname(path), { recursive: true });
156
- writeFileSync(path, JSON.stringify(next, null, 2) + "\n");
157
- return path;
158
- }
159
-
160
- /** The level this run starts at. `flag` is the CLI's answer (undefined when it said nothing);
161
- * the env var is next; then the files; then the deny-default "ask". */
162
- export function resolvePermission(cwd: string, flag: PermissionLevel | undefined, env: { ROVECODE_PERMISSION?: string | undefined; ROVECODE_YOLO?: string | undefined; ROVECODE_ACCEPT_EDITS?: string | undefined }): PermissionLevel {
163
- if (flag !== undefined) return flag;
164
- const named = (env.ROVECODE_PERMISSION ?? "").trim().toLowerCase();
165
- if (PERMISSIONS.includes(named)) return named as PermissionLevel;
166
- // the older single-purpose switches keep working; auto wins because it is the wider of the two
167
- if (env.ROVECODE_YOLO === "1") return "auto";
168
- if (env.ROVECODE_ACCEPT_EDITS === "1") return "accept-edits";
169
- return loadSettings(cwd).permission ?? "ask";
170
- }