rovecode 0.3.2 → 0.4.0-beta.2

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 (491) hide show
  1. package/CHANGELOG.md +512 -0
  2. package/README.md +33 -45
  3. package/bin/rovecode.js +24 -0
  4. package/dist/cli/app-dybnr56b.js +2 -0
  5. package/dist/cli/ask-user-p8hq4xgj.js +2 -0
  6. package/dist/cli/auth-login-ewpgw5sm.js +2 -0
  7. package/dist/cli/auth-m8p9grty.js +2 -0
  8. package/dist/cli/bench-xv3ypwev.js +9 -0
  9. package/dist/cli/catalog-737wb2s0.js +2 -0
  10. package/dist/cli/cli-arhg40m0.js +2 -0
  11. package/dist/cli/client-cf2pxx8q.js +2 -0
  12. package/dist/cli/commands-3p7e4xxs.js +2 -0
  13. package/dist/cli/connect-3q93d7cb.js +2 -0
  14. package/dist/cli/context-cmd-eqxmxhzq.js +2 -0
  15. package/dist/cli/context-report-hbw9zfes.js +2 -0
  16. package/dist/cli/count-remote-mby98cd0.js +2 -0
  17. package/dist/cli/design-122y0axd.js +2 -0
  18. package/dist/cli/dispatch-b4egzvvh.js +2 -0
  19. package/dist/cli/doctor-x4jkv72e.js +3 -0
  20. package/dist/cli/executor-ftvg6tsy.js +2 -0
  21. package/dist/cli/export-pdgdhkch.js +2 -0
  22. package/dist/cli/files-cez9a96p.js +2 -0
  23. package/dist/cli/gauntlet-r3xxaszc.js +2 -0
  24. package/dist/cli/gauntlet-runner-r515m7kk.js +10 -0
  25. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +5 -0
  26. package/dist/cli/gauntlet-wave4-acs9s60q.js +14 -0
  27. package/dist/cli/hashline-ewg5hbe3.js +2 -0
  28. package/dist/cli/http-n0kehsk8.js +5 -0
  29. package/dist/cli/index-z5qt1s76.js +2 -0
  30. package/dist/cli/install-80mp63kx.js +2 -0
  31. package/dist/cli/loop-12twjcat.js +2 -0
  32. package/dist/cli/main-01pv9206.js +4 -0
  33. package/dist/cli/main-0jys2ccn.js +3 -0
  34. package/dist/cli/main-1ztz6fkj.js +10 -0
  35. package/dist/cli/main-23q7cmww.js +9 -0
  36. package/dist/cli/main-2rzbexn2.js +3 -0
  37. package/dist/cli/main-2wyax8k9.js +9 -0
  38. package/dist/cli/main-2yeveeve.js +6 -0
  39. package/dist/cli/main-2z3dek0b.js +3 -0
  40. package/dist/cli/main-2zgsknth.js +3 -0
  41. package/dist/cli/main-45ejth3a.js +4 -0
  42. package/dist/cli/main-45rn3trk.js +22 -0
  43. package/dist/cli/main-4p4e2w7x.js +4 -0
  44. package/dist/cli/main-4y0tnfpa.js +16 -0
  45. package/dist/cli/main-5py0rkmc.js +4 -0
  46. package/dist/cli/main-6dtqmbt6.js +7 -0
  47. package/dist/cli/main-6h9x282m.js +4 -0
  48. package/dist/cli/main-6vjeds42.js +3 -0
  49. package/dist/cli/main-78gq4bt9.js +6 -0
  50. package/dist/cli/main-7jd5vh3x.js +4 -0
  51. package/dist/cli/main-7kt6r53y.js +4 -0
  52. package/dist/cli/main-8c1tbazx.js +58 -0
  53. package/dist/cli/main-9a9rnh47.js +19 -0
  54. package/dist/cli/main-9ht36z12.js +3 -0
  55. package/dist/cli/main-a2yfvcy9.js +7 -0
  56. package/dist/cli/main-a3f51n0x.js +5 -0
  57. package/dist/cli/main-b8zq261k.js +3 -0
  58. package/dist/cli/main-bxtvnf6d.js +13 -0
  59. package/dist/cli/main-edxc3yzt.js +4 -0
  60. package/dist/cli/main-evgz4mp5.js +21 -0
  61. package/dist/cli/main-f33fc5je.js +9 -0
  62. package/dist/cli/main-fvnpq46y.js +12 -0
  63. package/dist/cli/main-gbbty4d4.js +3 -0
  64. package/dist/cli/main-gth53dnt.js +25 -0
  65. package/dist/cli/main-hqbz10aw.js +9 -0
  66. package/dist/cli/main-hrrvcfan.js +38 -0
  67. package/dist/cli/main-hzwtsb2m.js +5 -0
  68. package/dist/cli/main-j7ttv0sd.js +34 -0
  69. package/dist/cli/main-jak598k9.js +5 -0
  70. package/dist/cli/main-kba6zeyd.js +6 -0
  71. package/dist/cli/main-kwwsz6rq.js +3 -0
  72. package/dist/cli/main-m8vm17zq.js +3 -0
  73. package/dist/cli/main-mg4f96e1.js +3 -0
  74. package/dist/cli/main-mg9b20ac.js +18 -0
  75. package/dist/cli/main-mgb9ccnx.js +3 -0
  76. package/dist/cli/main-mjt2p7aj.js +3 -0
  77. package/dist/cli/main-n6qrdbmy.js +3 -0
  78. package/dist/cli/main-na7wse0x.js +5 -0
  79. package/dist/cli/main-nqveez48.js +4 -0
  80. package/dist/cli/main-ntqef02r.js +10 -0
  81. package/dist/cli/main-nvc3yjay.js +136 -0
  82. package/dist/cli/main-p0cfn6nr.js +16 -0
  83. package/dist/cli/main-qj2djy17.js +19 -0
  84. package/dist/cli/main-qsevpgsv.js +3 -0
  85. package/dist/cli/main-qvarybsp.js +3 -0
  86. package/dist/cli/main-rebtt91r.js +5 -0
  87. package/dist/cli/main-rpg7h8mb.js +3 -0
  88. package/dist/cli/main-rsy72qmw.js +15 -0
  89. package/dist/cli/main-rvetps99.js +18 -0
  90. package/dist/cli/main-s4bb0jav.js +3 -0
  91. package/dist/cli/main-s9v8k74e.js +3 -0
  92. package/dist/cli/main-tjvwmscs.js +3 -0
  93. package/dist/cli/main-tkgarpjj.js +4 -0
  94. package/dist/cli/main-v8y60bb2.js +3 -0
  95. package/dist/cli/main-vhrrq337.js +3 -0
  96. package/dist/cli/main-vp2dfb7s.js +4 -0
  97. package/dist/cli/main-vqbr22sz.js +8 -0
  98. package/dist/cli/main-vxnwe5xx.js +18 -0
  99. package/dist/cli/main-wgph00xf.js +5 -0
  100. package/dist/cli/main-wk2csfnj.js +5 -0
  101. package/dist/cli/main-wm997zjx.js +3 -0
  102. package/dist/cli/main-wpkyraxh.js +3 -0
  103. package/dist/cli/main-wqt32p5x.js +4 -0
  104. package/dist/cli/main-x9ct6y1a.js +3 -0
  105. package/dist/cli/main-xfekqh9m.js +7 -0
  106. package/dist/cli/main-xt9zc3n6.js +7 -0
  107. package/dist/cli/main-xx2z3zh5.js +4 -0
  108. package/dist/cli/main-y5c82rxr.js +3 -0
  109. package/dist/cli/main-yrjt2sqt.js +14 -0
  110. package/dist/cli/main-ys6zj3yr.js +3 -0
  111. package/dist/cli/main-ywbxshqc.js +8 -0
  112. package/dist/cli/main-z13755t8.js +25 -0
  113. package/dist/cli/main-zc7pyrbj.js +4 -0
  114. package/dist/cli/main.js +279 -0
  115. package/dist/cli/market-cmd-bm5xvn9f.js +5 -0
  116. package/dist/cli/mcp-login-bthtfpt7.js +2 -0
  117. package/dist/cli/mcp-market-cmd-mbeshfyd.js +2 -0
  118. package/dist/cli/notify-54v5z9dz.js +2 -0
  119. package/dist/cli/oauth-g5gme95c.js +2 -0
  120. package/dist/cli/output-satndjap.js +16 -0
  121. package/dist/cli/profiles-sfhpbq3m.js +2 -0
  122. package/dist/cli/provider-config-hv3xtdt4.js +2 -0
  123. package/dist/cli/provider-kwzq6g84.js +2 -0
  124. package/dist/cli/registry-fh0hdnyn.js +2 -0
  125. package/dist/cli/registry-y1y8e94r.js +2 -0
  126. package/dist/cli/repl-t4z03mqq.js +11 -0
  127. package/dist/cli/resume-fqt4chg8.js +2 -0
  128. package/dist/cli/run-flags-rysbag9t.js +2 -0
  129. package/dist/cli/runtime-j19fjbsa.js +2 -0
  130. package/dist/cli/sandbox-config-g4qxd7y5.js +2 -0
  131. package/dist/cli/server-r0b6bksk.js +5 -0
  132. package/dist/cli/session-arg-txmn5g4x.js +2 -0
  133. package/dist/cli/session-ed250d9j.js +2 -0
  134. package/dist/cli/sessions-cmd-adw7svfn.js +7 -0
  135. package/dist/cli/settings-y9rzcqx8.js +2 -0
  136. package/dist/cli/setup-jmbr11j0.js +2 -0
  137. package/dist/cli/sextant-smoke-tcth0vea.js +5 -0
  138. package/dist/cli/skills-cmd-zbdy99v6.js +2 -0
  139. package/dist/cli/smoke-1bg937kx.js +8 -0
  140. package/dist/cli/start-chat-p01cdks3.js +12 -0
  141. package/dist/cli/stream-4wmyaypz.js +2 -0
  142. package/dist/cli/task-eg4s093s.js +2 -0
  143. package/dist/cli/tasks-12v9rr9k.js +2 -0
  144. package/dist/cli/thinking-a5ngvqyh.js +2 -0
  145. package/dist/cli/todo-1wxpcecx.js +2 -0
  146. package/dist/cli/tools-2ftsya7w.js +2 -0
  147. package/dist/cli/tools-x1tj4fxm.js +2 -0
  148. package/dist/cli/trust-cmd-hccxehzb.js +2 -0
  149. package/dist/cli/update-check-ygt3vd7m.js +2 -0
  150. package/dist/cli/update-cmd-v23qhr8c.js +2 -0
  151. package/dist/cli/voice-g1gtck92.js +2 -0
  152. package/dist/cli/webfetch-0nnrjgb5.js +2 -0
  153. package/dist/cli/websearch-f0vr2p7d.js +2 -0
  154. package/dist/cli/workspace-9rq1w4ta.js +2 -0
  155. package/dist/lib/index.js +62 -0
  156. package/dist/lib/models-index.json +1 -0
  157. package/dist/lib/plugins.js +6 -0
  158. package/dist/lib/providers.js +17 -0
  159. package/dist/lib/public-api.js +20 -0
  160. package/dist/rovecode.exe +4 -0
  161. package/package.json +38 -11
  162. package/bin/rovecode.ts +0 -21
  163. package/src/acp/server.ts +0 -374
  164. package/src/cli/auth-login.ts +0 -122
  165. package/src/cli/connect.ts +0 -244
  166. package/src/cli/context-cmd.ts +0 -199
  167. package/src/cli/dispatch.ts +0 -82
  168. package/src/cli/doctor.ts +0 -362
  169. package/src/cli/export.ts +0 -276
  170. package/src/cli/help.ts +0 -293
  171. package/src/cli/is-tui-invocation.ts +0 -8
  172. package/src/cli/main.ts +0 -583
  173. package/src/cli/market-cmd.ts +0 -658
  174. package/src/cli/mcp-login.ts +0 -141
  175. package/src/cli/mcp-market-cmd.ts +0 -302
  176. package/src/cli/output.ts +0 -382
  177. package/src/cli/repl.ts +0 -250
  178. package/src/cli/repomap-root.ts +0 -14
  179. package/src/cli/resume.ts +0 -57
  180. package/src/cli/run-flags.ts +0 -43
  181. package/src/cli/run-limits.ts +0 -78
  182. package/src/cli/runtime.ts +0 -931
  183. package/src/cli/session-arg.ts +0 -30
  184. package/src/cli/sessions-cmd.ts +0 -145
  185. package/src/cli/setup.ts +0 -153
  186. package/src/cli/skills-cmd.ts +0 -194
  187. package/src/cli/start-chat.ts +0 -65
  188. package/src/cli/trust-cmd.ts +0 -52
  189. package/src/coding/bash.ts +0 -148
  190. package/src/coding/checkpoints.ts +0 -327
  191. package/src/coding/diff.ts +0 -138
  192. package/src/coding/files.ts +0 -341
  193. package/src/coding/hashline.ts +0 -274
  194. package/src/coding/lsp-gate.ts +0 -254
  195. package/src/coding/lsp-servers.ts +0 -147
  196. package/src/coding/lsp.ts +0 -283
  197. package/src/coding/repomap-cache.ts +0 -99
  198. package/src/coding/repomap-files.ts +0 -192
  199. package/src/coding/repomap.ts +0 -481
  200. package/src/core/agents.ts +0 -255
  201. package/src/core/compaction.ts +0 -259
  202. package/src/core/config.ts +0 -289
  203. package/src/core/context-report.ts +0 -228
  204. package/src/core/context.ts +0 -60
  205. package/src/core/count-remote.ts +0 -107
  206. package/src/core/execpolicy-rules.ts +0 -196
  207. package/src/core/execpolicy.ts +0 -385
  208. package/src/core/executor.ts +0 -454
  209. package/src/core/guardrails.ts +0 -400
  210. package/src/core/hooks.ts +0 -411
  211. package/src/core/images.ts +0 -230
  212. package/src/core/intro.ts +0 -266
  213. package/src/core/loop.ts +0 -567
  214. package/src/core/modes.ts +0 -372
  215. package/src/core/orchestrator.ts +0 -245
  216. package/src/core/proc-group.ts +0 -48
  217. package/src/core/project-trust.ts +0 -98
  218. package/src/core/reflection.ts +0 -165
  219. package/src/core/sandbox-config.ts +0 -186
  220. package/src/core/session-id.ts +0 -24
  221. package/src/core/session-images.ts +0 -73
  222. package/src/core/session-ops.ts +0 -183
  223. package/src/core/session-text.ts +0 -29
  224. package/src/core/session.ts +0 -469
  225. package/src/core/settings.ts +0 -170
  226. package/src/core/tasks.ts +0 -646
  227. package/src/core/token-scale.ts +0 -108
  228. package/src/core/tools.ts +0 -309
  229. package/src/core/trust.ts +0 -104
  230. package/src/core/types.ts +0 -330
  231. package/src/core/update-check.ts +0 -171
  232. package/src/core/usage.ts +0 -204
  233. package/src/core/validate.ts +0 -121
  234. package/src/core/verify-gate.ts +0 -159
  235. package/src/core/verify.ts +0 -236
  236. package/src/core/voice.ts +0 -158
  237. package/src/core/win-job.ts +0 -183
  238. package/src/core/workspace.ts +0 -184
  239. package/src/design/audit.ts +0 -797
  240. package/src/design/direction.ts +0 -190
  241. package/src/design/rules.ts +0 -157
  242. package/src/eval/bench.ts +0 -150
  243. package/src/eval/gauntlet-runner.ts +0 -215
  244. package/src/eval/gauntlet-support.ts +0 -84
  245. package/src/eval/gauntlet-wave3.ts +0 -269
  246. package/src/eval/gauntlet-wave4.ts +0 -217
  247. package/src/eval/gauntlet.ts +0 -253
  248. package/src/index.ts +0 -17
  249. package/src/lanes/agy.ts +0 -95
  250. package/src/lanes/approval.ts +0 -24
  251. package/src/lanes/claude.ts +0 -129
  252. package/src/lanes/codex.ts +0 -127
  253. package/src/lanes/events.ts +0 -130
  254. package/src/lanes/job.ts +0 -142
  255. package/src/lanes/opencode.ts +0 -122
  256. package/src/lanes/process.ts +0 -184
  257. package/src/lanes/progress.ts +0 -183
  258. package/src/lanes/registry.ts +0 -178
  259. package/src/lanes/runner.ts +0 -124
  260. package/src/lanes/types.ts +0 -112
  261. package/src/market/catalogs/mcp-docs.json +0 -111
  262. package/src/market/catalogs/plugins.json +0 -111
  263. package/src/market/catalogs/skills.json +0 -478
  264. package/src/market/clone.ts +0 -72
  265. package/src/market/context-cost.ts +0 -121
  266. package/src/market/digest.ts +0 -106
  267. package/src/market/index.ts +0 -22
  268. package/src/market/install.ts +0 -578
  269. package/src/market/manifest.ts +0 -187
  270. package/src/market/prereq.ts +0 -145
  271. package/src/market/registry.ts +0 -363
  272. package/src/market/resolve.ts +0 -111
  273. package/src/market/types.ts +0 -236
  274. package/src/market/validate.ts +0 -227
  275. package/src/mcp/client.ts +0 -449
  276. package/src/mcp/config.ts +0 -252
  277. package/src/mcp/local-package.ts +0 -211
  278. package/src/mcp/market-catalog.ts +0 -84
  279. package/src/mcp/market-install.ts +0 -289
  280. package/src/mcp/market.ts +0 -362
  281. package/src/mcp/oauth.ts +0 -251
  282. package/src/mcp/prompts-resources.ts +0 -249
  283. package/src/mcp/shared.ts +0 -149
  284. package/src/mcp/status.ts +0 -67
  285. package/src/mcp/tools.ts +0 -275
  286. package/src/mcp/transport.ts +0 -122
  287. package/src/mcp/trust.ts +0 -25
  288. package/src/memory/blocks.ts +0 -278
  289. package/src/memory/recall.ts +0 -355
  290. package/src/memory/scope.ts +0 -182
  291. package/src/memory/store.ts +0 -105
  292. package/src/memory/tools.ts +0 -99
  293. package/src/plugins/cli.ts +0 -119
  294. package/src/plugins/discover.ts +0 -108
  295. package/src/plugins/index.ts +0 -50
  296. package/src/plugins/install.ts +0 -184
  297. package/src/plugins/load.ts +0 -124
  298. package/src/plugins/manifest.ts +0 -92
  299. package/src/plugins/state.ts +0 -83
  300. package/src/providers/auth.ts +0 -408
  301. package/src/providers/cache.ts +0 -223
  302. package/src/providers/catalog-local.ts +0 -160
  303. package/src/providers/catalog.ts +0 -421
  304. package/src/providers/middleware-context.ts +0 -86
  305. package/src/providers/middleware.ts +0 -373
  306. package/src/providers/model-list.ts +0 -23
  307. package/src/providers/oauth/common.ts +0 -105
  308. package/src/providers/oauth/device-code.ts +0 -107
  309. package/src/providers/oauth/github-copilot.ts +0 -146
  310. package/src/providers/oauth/loopback.ts +0 -158
  311. package/src/providers/oauth/openai.ts +0 -163
  312. package/src/providers/oauth/openrouter.ts +0 -89
  313. package/src/providers/oauth/pkce.ts +0 -45
  314. package/src/providers/oauth/registry.ts +0 -39
  315. package/src/providers/oauth/seam.ts +0 -89
  316. package/src/providers/profile-glm53.ts +0 -111
  317. package/src/providers/profile-sonnet5-persona.ts +0 -65
  318. package/src/providers/profile-sonnet5-voice.ts +0 -23
  319. package/src/providers/profiles.ts +0 -156
  320. package/src/providers/provider-config.ts +0 -311
  321. package/src/providers/registry.ts +0 -333
  322. package/src/providers/responses.ts +0 -209
  323. package/src/providers/retry.ts +0 -234
  324. package/src/providers/router.ts +0 -294
  325. package/src/providers/sse.ts +0 -26
  326. package/src/providers/stream-errors.ts +0 -117
  327. package/src/providers/stream.ts +0 -566
  328. package/src/providers/thinking.ts +0 -189
  329. package/src/providers/wire-messages.ts +0 -129
  330. package/src/providers/wire-responses.ts +0 -79
  331. package/src/providers/wire-select.ts +0 -53
  332. package/src/server/http.ts +0 -291
  333. package/src/server/openapi.ts +0 -246
  334. package/src/sextant/card-hits.ts +0 -102
  335. package/src/sextant/card-keys.ts +0 -55
  336. package/src/sextant/context-source.ts +0 -157
  337. package/src/sextant/crew-cards.ts +0 -350
  338. package/src/sextant/draw-agents.ts +0 -273
  339. package/src/sextant/draw-code.ts +0 -388
  340. package/src/sextant/draw-context.ts +0 -222
  341. package/src/sextant/draw-frame.ts +0 -164
  342. package/src/sextant/draw-market.ts +0 -573
  343. package/src/sextant/draw-messages.ts +0 -386
  344. package/src/sextant/draw-pet.ts +0 -230
  345. package/src/sextant/draw-plan.ts +0 -187
  346. package/src/sextant/draw-tabs.ts +0 -85
  347. package/src/sextant/draw-util.ts +0 -65
  348. package/src/sextant/draw-wizard.ts +0 -378
  349. package/src/sextant/engine.ts +0 -230
  350. package/src/sextant/frame-hits.ts +0 -25
  351. package/src/sextant/frame.ts +0 -101
  352. package/src/sextant/git-status.ts +0 -197
  353. package/src/sextant/grid.ts +0 -59
  354. package/src/sextant/input.ts +0 -119
  355. package/src/sextant/keys.ts +0 -521
  356. package/src/sextant/layout.ts +0 -86
  357. package/src/sextant/local-commands.ts +0 -169
  358. package/src/sextant/market-source.ts +0 -287
  359. package/src/sextant/mentions.ts +0 -200
  360. package/src/sextant/message-hits.ts +0 -26
  361. package/src/sextant/model.ts +0 -387
  362. package/src/sextant/overlays.ts +0 -456
  363. package/src/sextant/panel-hits.ts +0 -38
  364. package/src/sextant/pet.ts +0 -399
  365. package/src/sextant/screen.ts +0 -324
  366. package/src/sextant/scroll-hits.ts +0 -66
  367. package/src/sextant/scrollbar.ts +0 -82
  368. package/src/sextant/sextant-bridge.ts +0 -174
  369. package/src/sextant/sextant-cards.ts +0 -142
  370. package/src/sextant/sextant-diff-base.ts +0 -63
  371. package/src/sextant/sextant-files.ts +0 -154
  372. package/src/sextant/sextant-frame-loop.ts +0 -335
  373. package/src/sextant/sextant-renderer.ts +0 -574
  374. package/src/sextant/sextant-repo.ts +0 -140
  375. package/src/sextant/theme.ts +0 -66
  376. package/src/sextant/tool-rows.ts +0 -189
  377. package/src/sextant/types.ts +0 -493
  378. package/src/skills/index.ts +0 -387
  379. package/src/skills/pack.ts +0 -220
  380. package/src/skills/spec.ts +0 -162
  381. package/src/skills/tools.ts +0 -69
  382. package/src/skills/versioned.ts +0 -227
  383. package/src/telemetry/otel-export.ts +0 -122
  384. package/src/telemetry/otel-lanes.ts +0 -89
  385. package/src/telemetry/otel-logs.ts +0 -131
  386. package/src/telemetry/otel-metrics.ts +0 -136
  387. package/src/telemetry/otel.ts +0 -397
  388. package/src/telemetry/otlp.ts +0 -76
  389. package/src/tools/ask-user.ts +0 -156
  390. package/src/tools/bash-bg.ts +0 -94
  391. package/src/tools/bash-jobs.ts +0 -237
  392. package/src/tools/design.ts +0 -151
  393. package/src/tools/evalcell.ts +0 -338
  394. package/src/tools/html-text.ts +0 -139
  395. package/src/tools/provider.ts +0 -149
  396. package/src/tools/task.ts +0 -250
  397. package/src/tools/todo.ts +0 -320
  398. package/src/tools/webfetch.ts +0 -332
  399. package/src/tools/websearch.ts +0 -359
  400. package/src/tui/agents-cmd.ts +0 -41
  401. package/src/tui/app.ts +0 -749
  402. package/src/tui/attach.ts +0 -127
  403. package/src/tui/boot-notes.ts +0 -41
  404. package/src/tui/builtin-prompts.ts +0 -59
  405. package/src/tui/checkpoints-cmd.ts +0 -70
  406. package/src/tui/clipboard-image.ts +0 -81
  407. package/src/tui/clipboard.ts +0 -78
  408. package/src/tui/commands.ts +0 -283
  409. package/src/tui/config-view.ts +0 -53
  410. package/src/tui/context-cmds.ts +0 -282
  411. package/src/tui/cost.ts +0 -108
  412. package/src/tui/crash-guard.ts +0 -173
  413. package/src/tui/focus-terminal.ts +0 -34
  414. package/src/tui/git-cmds.ts +0 -273
  415. package/src/tui/git-plain.ts +0 -58
  416. package/src/tui/info-cmd.ts +0 -150
  417. package/src/tui/input-plain.ts +0 -76
  418. package/src/tui/mcp-cmd.ts +0 -128
  419. package/src/tui/memory-note.ts +0 -77
  420. package/src/tui/modes-cmd.ts +0 -45
  421. package/src/tui/notify-seq.ts +0 -100
  422. package/src/tui/notify.ts +0 -318
  423. package/src/tui/overlays.ts +0 -97
  424. package/src/tui/pi-renderer.ts +0 -428
  425. package/src/tui/providers-cmd.ts +0 -377
  426. package/src/tui/reasoning-view.ts +0 -56
  427. package/src/tui/renderer.ts +0 -128
  428. package/src/tui/replay-marker.ts +0 -29
  429. package/src/tui/session-cmd.ts +0 -148
  430. package/src/tui/session-manage.ts +0 -95
  431. package/src/tui/sextant-attach.ts +0 -102
  432. package/src/tui/sextant-io.ts +0 -202
  433. package/src/tui/sextant-smoke.ts +0 -110
  434. package/src/tui/shell-cmd.ts +0 -158
  435. package/src/tui/smoke.ts +0 -72
  436. package/src/tui/staged-terminal.ts +0 -50
  437. package/src/tui/startup.ts +0 -12
  438. package/src/tui/theme.ts +0 -59
  439. package/src/tui/todo-label.ts +0 -7
  440. package/src/tui/trust-card.ts +0 -107
  441. package/src/tui/tui-commands.ts +0 -87
  442. package/tsconfig.json +0 -30
  443. package/vendor/pi-tui/LICENSE +0 -21
  444. package/vendor/pi-tui/PATCHES.md +0 -12
  445. package/vendor/pi-tui/PROVENANCE.md +0 -12
  446. package/vendor/pi-tui/README.upstream.md +0 -854
  447. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  448. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  449. package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
  450. package/vendor/pi-tui/src/autocomplete.ts +0 -827
  451. package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
  452. package/vendor/pi-tui/src/components/box.ts +0 -138
  453. package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
  454. package/vendor/pi-tui/src/components/editor.ts +0 -2364
  455. package/vendor/pi-tui/src/components/h-stack.ts +0 -45
  456. package/vendor/pi-tui/src/components/image.ts +0 -128
  457. package/vendor/pi-tui/src/components/input.ts +0 -448
  458. package/vendor/pi-tui/src/components/loader.ts +0 -93
  459. package/vendor/pi-tui/src/components/markdown.ts +0 -1016
  460. package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
  461. package/vendor/pi-tui/src/components/select-list.ts +0 -230
  462. package/vendor/pi-tui/src/components/settings-list.ts +0 -277
  463. package/vendor/pi-tui/src/components/spacer.ts +0 -29
  464. package/vendor/pi-tui/src/components/stack.ts +0 -155
  465. package/vendor/pi-tui/src/components/text.ts +0 -108
  466. package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
  467. package/vendor/pi-tui/src/components/v-stack.ts +0 -34
  468. package/vendor/pi-tui/src/editor-component.ts +0 -75
  469. package/vendor/pi-tui/src/fuzzy.ts +0 -138
  470. package/vendor/pi-tui/src/index.ts +0 -149
  471. package/vendor/pi-tui/src/keybindings.ts +0 -321
  472. package/vendor/pi-tui/src/keys.ts +0 -1402
  473. package/vendor/pi-tui/src/kill-ring.ts +0 -47
  474. package/vendor/pi-tui/src/latex.ts +0 -1381
  475. package/vendor/pi-tui/src/layout-node.ts +0 -52
  476. package/vendor/pi-tui/src/layout.ts +0 -411
  477. package/vendor/pi-tui/src/native-modifiers.ts +0 -60
  478. package/vendor/pi-tui/src/native-module-path.ts +0 -32
  479. package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
  480. package/vendor/pi-tui/src/terminal-colors.ts +0 -74
  481. package/vendor/pi-tui/src/terminal-image.ts +0 -701
  482. package/vendor/pi-tui/src/terminal.ts +0 -554
  483. package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
  484. package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
  485. package/vendor/pi-tui/src/tui.ts +0 -1264
  486. package/vendor/pi-tui/src/undo-stack.ts +0 -29
  487. package/vendor/pi-tui/src/utils.ts +0 -1327
  488. package/vendor/pi-tui/src/word-navigation.ts +0 -118
  489. package/vendor/pi-tui/test/test-themes.ts +0 -39
  490. package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
  491. /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
- }