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,278 +0,0 @@
1
- /** Bounded markdown memory blocks (hermes pattern): MEMORY.md + USER.md with hard char caps.
2
- * The snapshot is frozen at boot (constructor) so the system prompt stays cache-stable and
3
- * byte-identical across the turns of a run; tools mutate live state + disk only — an edit is in the
4
- * prompt from the NEXT run. A threat-scan neutralizes injection lines in the rendered view — raw text
5
- * on disk is never rewritten by the scan.
6
- *
7
- * Scoping (2026-09-07): the two blocks may live in DIFFERENT directories (`BlockDirs` — MEMORY under
8
- * the project's .rovecode, USER under the user home; memory/scope.ts resolves them), because a USER
9
- * block written inside a repository is the wrong file in the wrong place. A plain string keeps both in
10
- * one dir. Directories are created lazily by the first commit (the ledger's write), so constructing a
11
- * store over the user scope never touches the home directory.
12
- *
13
- * A block file is REPO DATA — `<cwd>/.rovecode/memory/MEMORY.md` is committable — so the boot read is
14
- * bounded twice: at most READ_CAP_BYTES are read from disk (a longer file marks the block read-cut and
15
- * every commit/rollback is refused — the store never rewrites a file it could not read whole), and the
16
- * PROMPT view is cut at the block's char cap (line boundary) behind a visible `[truncated: …]` marker,
17
- * so the prompt cannot balloon whatever the file holds; the live text (/memory, the tools) is what was
18
- * read. A block may also be WITHHELD (`BlockStoreOptions.withhold`): read as usual, kept out of the
19
- * prompt — memory/scope.ts withholds a project MEMORY block this machine has not approved. */
20
-
21
- import { closeSync, existsSync, openSync, readSync } from "node:fs";
22
- import { join } from "node:path";
23
- import { TRUST_HINT } from "../core/trust.ts";
24
- import { VersionLedger } from "../skills/versioned.ts";
25
-
26
- export type BlockName = "memory" | "user";
27
-
28
- /** the directory each block's file (+ its `.versions.jsonl` ledger sidecar) lives in */
29
- export type BlockDirs = Record<BlockName, string>;
30
-
31
- /** the block file names — the ONE place that spells them (memory/scope.ts copies them by these names) */
32
- export const BLOCK_FILES: Readonly<Record<BlockName, string>> = { memory: "MEMORY.md", user: "USER.md" };
33
-
34
- export interface BlockCaps { memory: number; user: number }
35
-
36
- export const defaultCaps: BlockCaps = { memory: 2_200, user: 1_375 };
37
-
38
- /** the most a block file is read at boot (bytes); past it the block is read-cut: shown cut, never rewritten */
39
- export const READ_CAP_BYTES = 64 * 1024;
40
-
41
- export interface BlockStoreOptions {
42
- /** blocks read from disk but KEPT OUT of the prompt (a project MEMORY block this machine has not
43
- * approved); live text and /memory see them as usual, and every WRITE to them is refused — appending
44
- * our own note to a stranger's file would launder its content into the next prompt */
45
- withhold?: readonly BlockName[];
46
- /** called after a write THIS store performed landed on disk (commit or rollback), with the block and its
47
- * file. memory/scope.ts uses it to keep the project block's trust digest current: the narrow self-trust —
48
- * a file we just wrote, never "the file looks close enough". Never called for a refused write. */
49
- onCommit?: (block: BlockName, file: string) => void;
50
- }
51
-
52
- export interface BlockEditResult {
53
- ok: boolean;
54
- reason?: string;
55
- /** present on cap failures: current chars / cap */
56
- current?: number;
57
- limit?: number;
58
- /** present on version conflicts: ledger numbers for logs/data surfaces only —
59
- * the model-facing `reason` stays generic (ouroboros: no ledger internals). */
60
- conflict?: { baseVersion: number; currentVersion: number };
61
- }
62
-
63
- /** Model-facing conflict reason — deliberately free of ledger internals. */
64
- export const CONFLICT_REASON = "memory changed since it was read — re-read and retry";
65
-
66
- const THREAT = /(?:ignore previous|disregard above|system prompt)/i;
67
-
68
- function scan(text: string): string {
69
- return text.split("\n").map((l) => (THREAT.test(l) ? "[BLOCKED]" : l)).join("\n");
70
- }
71
-
72
- /** `text` cut to at most `cap` chars at a line boundary (a hard cut when the last newline sits in the first half) */
73
- export function cutAtCap(text: string, cap: number): string {
74
- if (text.length <= cap) return text;
75
- let cut = text.lastIndexOf("\n", cap);
76
- if (cut < cap / 2) cut = cap;
77
- return text.slice(0, cut);
78
- }
79
-
80
- /** at most `cap + 1` bytes of `p` (existence checked by the caller): the text and whether the file went on past the cap */
81
- function readBounded(p: string, cap: number): { text: string; cut: boolean } {
82
- const fd = openSync(p, "r");
83
- try {
84
- const buf = Buffer.alloc(cap + 1);
85
- let n = 0;
86
- while (n < buf.length) { const got = readSync(fd, buf, n, buf.length - n, n); if (got <= 0) break; n += got; }
87
- return { text: buf.subarray(0, Math.min(n, cap)).toString("utf8"), cut: n > cap };
88
- } finally { closeSync(fd); }
89
- }
90
-
91
- function occurrences(haystack: string, needle: string): number {
92
- if (!needle) return 0;
93
- let n = 0;
94
- let i = 0;
95
- while ((i = haystack.indexOf(needle, i)) !== -1) { n++; i += needle.length; }
96
- return n;
97
- }
98
-
99
- export class BlockStore {
100
- private live: Record<BlockName, string> = { memory: "", user: "" };
101
- private readonly frozen: Record<BlockName, string>;
102
- /** port #16: versioned edits — every commit lands through a sidecar ledger
103
- * (<file>.versions.jsonl), enabling one-call rollback + drift detection. */
104
- private readonly ledgers: Record<BlockName, VersionLedger>;
105
- /** Optimistic-concurrency baseline (prime baselineState, refinement.ts:735-749):
106
- * the version THIS store last saw. Passing ledger.version() to commit would
107
- * compare the guard to itself and silently drop a concurrent store's edit. */
108
- private readonly baseVersions: Record<BlockName, number>;
109
-
110
- private readonly dirs: BlockDirs;
111
- /** the block's file went on past READ_CAP_BYTES at boot — shown cut, never rewritten */
112
- private readonly readCut: Record<BlockName, boolean> = { memory: false, user: false };
113
- private readonly withheld: ReadonlySet<BlockName>;
114
- private readonly onCommit: ((block: BlockName, file: string) => void) | undefined;
115
-
116
- /** `dir`: one directory for both blocks, or a per-block map (the scoped layout). Nothing is created here. */
117
- constructor(
118
- dir: string | BlockDirs,
119
- private readonly caps: BlockCaps = defaultCaps,
120
- opts: BlockStoreOptions = {},
121
- ) {
122
- // no mkdir here: reads tolerate a missing directory and the ledger creates it on the first commit
123
- // (skills/versioned.ts). Creating `<session>/memory` at construction was what made every session
124
- // directory non-empty before anyone had said anything (core/session.ts materialize()).
125
- this.dirs = typeof dir === "string" ? { memory: dir, user: dir } : { memory: dir.memory, user: dir.user };
126
- this.withheld = new Set(opts.withhold ?? []);
127
- this.onCommit = opts.onCommit;
128
- this.live.memory = this.read("memory");
129
- this.live.user = this.read("user");
130
- // snapshot is the threat-scanned view of what boot loaded
131
- this.frozen = { memory: scan(this.live.memory), user: scan(this.live.user) };
132
- this.ledgers = {
133
- memory: new VersionLedger(this.file("memory")),
134
- user: new VersionLedger(this.file("user")),
135
- };
136
- this.baseVersions = { memory: this.ledgers.memory.version(), user: this.ledgers.user.version() };
137
- }
138
-
139
- /** the bounded boot read (header): at most READ_CAP_BYTES; a longer file marks the block read-cut */
140
- private read(block: BlockName): string {
141
- const p = this.file(block);
142
- if (!existsSync(p)) return "";
143
- const r = readBounded(p, READ_CAP_BYTES);
144
- this.readCut[block] = r.cut;
145
- return r.text;
146
- }
147
-
148
- private file(block: BlockName): string {
149
- return join(this.dirs[block], BLOCK_FILES[block]);
150
- }
151
-
152
- /** the block's file path (where edits land) — for notes, /memory and tests. */
153
- path(block: BlockName): string { return this.file(block); }
154
-
155
- add(block: BlockName, text: string): BlockEditResult {
156
- const t = text.trim();
157
- if (!t) return { ok: false, reason: "empty text" };
158
- const next = this.live[block] ? this.live[block] + "\n" + t : t;
159
- return this.commit(block, next);
160
- }
161
-
162
- replace(block: BlockName, oldText: string, newText: string): BlockEditResult {
163
- const found = this.locate(block, oldText);
164
- if (!found.ok) return found;
165
- const t = newText.trim();
166
- const next = t ? this.live[block].replace(oldText, t) : this.live[block].replace(oldText, "");
167
- return this.commit(block, next.trim());
168
- }
169
-
170
- remove(block: BlockName, oldText: string): BlockEditResult {
171
- const found = this.locate(block, oldText);
172
- if (!found.ok) return found;
173
- const next = this.live[block].replace(oldText, "").replace(/\n{3,}/g, "\n\n").trim();
174
- return this.commit(block, next);
175
- }
176
-
177
- /** oldText must match exactly once: 0 → not found, >1 → ambiguous. */
178
- private locate(block: BlockName, oldText: string): BlockEditResult {
179
- if (!oldText) return { ok: false, reason: "oldText required" };
180
- const n = occurrences(this.live[block], oldText);
181
- if (n === 0) return { ok: false, reason: "oldText not found" };
182
- if (n > 1) return { ok: false, reason: `oldText matches ${n} times; must match exactly once` };
183
- return { ok: true };
184
- }
185
-
186
- /** Char-cap guard shared by commit() and rollback(); runs BEFORE any ledger write. A read-cut block
187
- * (header) refuses every write: the store never rewrites a file it could not read whole. */
188
- private capCheck(block: BlockName, next: string): BlockEditResult | null {
189
- const limit = this.caps[block];
190
- if (this.withheld.has(block)) {
191
- return { ok: false, reason: `${BLOCK_FILES[block]} came with this repository and is not approved on this machine — it is not in the prompt and cannot be written to (${TRUST_HINT})`, current: this.live[block].length, limit };
192
- }
193
- if (this.readCut[block]) {
194
- return { ok: false, reason: `${BLOCK_FILES[block]} on disk is longer than the ${READ_CAP_BYTES}-byte read cap — trim it by hand before editing`, current: this.live[block].length, limit };
195
- }
196
- if (next.length <= limit) return null;
197
- return {
198
- ok: false, reason: `block would exceed cap (${next.length} > ${limit} chars)`,
199
- current: this.live[block].length, limit,
200
- };
201
- }
202
-
203
- private commit(block: BlockName, next: string): BlockEditResult {
204
- const capped = this.capCheck(block, next);
205
- if (capped) return capped;
206
- // ledger-first write (port #16): version record appended, then the target
207
- // is written atomically by the ledger — never writeFileSync directly. The
208
- // baseline is what THIS store last saw, so a concurrent store's commit is
209
- // caught here instead of being silently overwritten.
210
- const r = this.ledgers[block].commit(next, "memory_edit", this.baseVersions[block]);
211
- if (!r.ok) {
212
- // conflict: another store/process advanced the block. Re-read reality so
213
- // the next edit builds on the merged state; the reason stays generic —
214
- // ledger numbers ride the structured field only.
215
- this.live[block] = this.ledgers[block].read();
216
- this.baseVersions[block] = r.currentVersion;
217
- return {
218
- ok: false, reason: CONFLICT_REASON,
219
- conflict: { baseVersion: r.baseVersion, currentVersion: r.currentVersion },
220
- };
221
- }
222
- this.baseVersions[block] = r.edit.version;
223
- this.live[block] = next;
224
- this.onCommit?.(block, this.file(block)); // the write landed and it was ours (memory/scope.ts self-trust)
225
- return { ok: true, current: next.length, limit: this.caps[block] };
226
- }
227
-
228
- /** port #16: one-call rollback of a block to any retained version.
229
- * Restored content re-enters through the same cap guard as commits. */
230
- rollback(block: BlockName, toVersion: number): BlockEditResult {
231
- const restored = this.ledgers[block].contentAt(toVersion);
232
- const capped = restored !== undefined ? this.capCheck(block, restored) : null;
233
- if (capped) return capped;
234
- const r = this.ledgers[block].rollback(toVersion, "memory_rollback");
235
- if (!r.ok) return { ok: false, reason: r.message };
236
- this.baseVersions[block] = r.edit.version;
237
- this.live[block] = this.ledgers[block].read();
238
- this.onCommit?.(block, this.file(block)); // a rollback rewrites the file too — same self-trust
239
- return { ok: true, current: this.live[block].length, limit: this.caps[block] };
240
- }
241
-
242
- /** port #16: version history access for status/debug surfaces. */
243
- ledger(block: BlockName): VersionLedger { return this.ledgers[block]; }
244
-
245
- /** the prompt text of one block: the boot snapshot, cut at the block's cap (header) behind a visible marker */
246
- private promptView(block: BlockName): string {
247
- const text = this.frozen[block], cap = this.caps[block];
248
- if (text.length <= cap && !this.readCut[block]) return text;
249
- const size = this.readCut[block] ? `more than ${READ_CAP_BYTES} bytes` : `${this.live[block].length} chars`;
250
- return `${cutAtCap(text, cap)}\n[truncated: ${BLOCK_FILES[block]} holds ${size}; the first ${cap} chars are shown — trim the file]`;
251
- }
252
-
253
- /** Boot snapshot (threat-scanned, cut at the cap; withheld blocks absent). Tool writes never change it. */
254
- renderForPrompt(): string {
255
- const parts: string[] = [];
256
- if (this.frozen.memory && !this.withheld.has("memory")) parts.push(`# Memory\n${this.promptView("memory")}`);
257
- if (this.frozen.user && !this.withheld.has("user")) parts.push(`# User\n${this.promptView("user")}`);
258
- return parts.join("\n\n");
259
- }
260
-
261
- /** Live (post-snapshot) raw text — for reads/debug, not for the prompt. */
262
- liveText(block: BlockName): string { return this.live[block]; }
263
-
264
- /** true when the block's live text no longer matches the boot snapshot the prompt shows
265
- * (an edit this run — the prompt picks it up on the next run). */
266
- edited(block: BlockName): boolean { return scan(this.live[block]) !== this.frozen[block]; }
267
-
268
- cap(block: BlockName): number { return this.caps[block]; }
269
-
270
- /** the block's boot read against its cap — null when it fits; `readCut` = the file went past READ_CAP_BYTES */
271
- overCap(block: BlockName): { chars: number; cap: number; readCut: boolean } | null {
272
- if (this.live[block].length <= this.caps[block] && !this.readCut[block]) return null;
273
- return { chars: this.live[block].length, cap: this.caps[block], readCut: this.readCut[block] };
274
- }
275
-
276
- /** true when the block is read (live, /memory, memory_edit) but kept out of the prompt (an unapproved project file) */
277
- isWithheld(block: BlockName): boolean { return this.withheld.has(block); }
278
- }
@@ -1,355 +0,0 @@
1
- /** Cross-session recall (port #17): tokenized full-text search over
2
- * `.rovecode/sessions/<id>/entries.jsonl` message text, exposed as a `recall` tool.
3
- *
4
- * Ported from hermes-agent's session search (MIT), adapted from SQLite FTS5 to an
5
- * in-memory inverted index over the JSONL session tree:
6
- * - Index = tokenized message content, maintained incrementally; hermes keeps an
7
- * external-content FTS5 table in sync via insert triggers + high-water/progress
8
- * markers so only unindexed rows are (re)indexed (hermes_state_common.py:637-684,
9
- * hermes_state_search.py:280-346 fts_rebuild_step). Here the incremental unit is
10
- * the session FILE, keyed by mtime (+size guard): only changed files re-index.
11
- * - Ranking tiers = hermes's routing: exact tokenized FTS5 match is the primary
12
- * path, substring (trigram) matching is the fallback tier
13
- * (hermes_state_search.py:1467-1489 _describe_search_path, 1846-1885 routing);
14
- * within a tier hermes orders by BM25 `ORDER BY rank` with timestamp tiebreaks
15
- * (hermes_state_search.py:1791-1798). Here: exact-term count desc, then weighted
16
- * term frequency, then recency. Query terms are implicitly ANDed, matching FTS5
17
- * (tools/session_search_tool.py:807-809).
18
- * - Partial matches need terms >=3 chars, mirroring trigram eligibility
19
- * (hermes_state_search.py:1322-1337 _trigram_eligible_tokens).
20
- * - Hits are snippet + metadata only, never full content
21
- * (hermes_state_search.py:1694-1701, 1827-1844); preview = 120-char window
22
- * starting 40 before the first match, the LIKE-fallback snippet shape
23
- * (hermes_state_search.py:1585-1595).
24
- * - Result budget: limit clamped like hermes's max(1, min(limit, 10))
25
- * (tools/session_search_tool.py:1040-1046).
26
- * - NO LLM anywhere in the search path (tools/session_search_tool.py:25-33 — the
27
- * historical "summary mode" was removed upstream); summarization is an optional
28
- * injected fn here, off by default.
29
- * - Trust boundary: previews get the same per-line injection neutralization as
30
- * BlockStore (blocks.ts:24-27) — threat lines render as [BLOCKED]; disk is never
31
- * rewritten. The query echo in tool output is bounded to MAX_QUERY_CHARS.
32
- */
33
-
34
- import { readdirSync, readFileSync, statSync } from "node:fs";
35
- import { join } from "node:path";
36
- import type { Tool, ToolContext, ToolOutput } from "../core/types.ts";
37
-
38
- /** Ranked hit — the bar's exact shape. Preview only; full text stays on disk. */
39
- export interface RecallHit { sessionId: string; entryId: string; preview: string; timestamp: number }
40
-
41
- /** Optional post-search summarizer (LLM or otherwise) — injected, never required. */
42
- export type SummarizeFn = (query: string, hits: RecallHit[]) => Promise<string> | string;
43
-
44
- export interface RefreshStats { scanned: number; indexed: number; removed: number }
45
-
46
- const MAX_RESULTS = 10; // hermes limit ceiling (session_search_tool.py:1046)
47
- const DEFAULT_RESULTS = 5;
48
- const PREVIEW_WINDOW = 120; // hermes LIKE-fallback snippet width (hermes_state_search.py:1587)
49
- const PREVIEW_LEAD = 40; // window starts 40 chars before the match (same line)
50
- const MIN_PARTIAL_TERM = 3; // trigram eligibility (hermes_state_search.py:1322-1337)
51
- const MAX_QUERY_CHARS = 512; // bounded adversarial input (hermes MAX_FTS5_QUERY_CHARS, hermes_state_search.py:1199-1201)
52
- const EXACT_WEIGHT = 2;
53
- const PARTIAL_WEIGHT = 1;
54
-
55
- /** unicode61-style tokenization (hermes's base FTS5 index): case-fold, split on
56
- * anything that is not a letter or digit. */
57
- export function tokenize(text: string): string[] {
58
- return text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((t) => t.length > 0);
59
- }
60
-
61
- interface Doc {
62
- sessionId: string;
63
- entryId: string;
64
- text: string;
65
- timestamp: number;
66
- tokens: Map<string, number>; // token -> tf (kept for O(tokens) removal on re-index)
67
- }
68
-
69
- interface FileState { mtimeMs: number; size: number; docKeys: string[] }
70
-
71
- /** One session-entry line, already filtered to message text. */
72
- function parseLine(line: string): { entryId: string; text: string; timestamp: number } | null {
73
- let raw: unknown;
74
- try { raw = JSON.parse(line); } catch { return null; } // malformed lines: skip, never throw (session.ts reload pattern)
75
- if (!raw || typeof raw !== "object") return null;
76
- const w = raw as { id?: unknown; createdAt?: unknown; entry?: unknown };
77
- if (typeof w.id !== "string") return null;
78
- const e = w.entry;
79
- // events (kind:"event") and non-message shapes are not "message text" — skip
80
- if (!e || typeof e !== "object" || !("role" in e)) return null;
81
- const parts = (e as { parts?: unknown }).parts;
82
- if (!Array.isArray(parts)) return null;
83
- const texts: string[] = [];
84
- for (const p of parts) {
85
- if (p && typeof p === "object"
86
- && (p as { kind?: unknown }).kind === "text"
87
- && typeof (p as { text?: unknown }).text === "string") {
88
- texts.push((p as { text: string }).text);
89
- }
90
- }
91
- const text = texts.join(" ").trim();
92
- if (!text) return null; // tool_call/tool_result-only messages carry no text parts
93
- return { entryId: w.id, text, timestamp: typeof w.createdAt === "number" ? w.createdAt : 0 };
94
- }
95
-
96
- /** Recalled transcript text sits at the SAME trust level as BlockStore markdown:
97
- * text a past model/user wrote, re-entering a live context. Local copy of the
98
- * blocks.ts:24-27 neutralization — any line matching the injection pattern
99
- * renders as [BLOCKED]; the raw text on disk is never rewritten. */
100
- const THREAT = /(?:ignore previous|disregard above|system prompt)/i;
101
- function neutralize(text: string): string {
102
- return text.split("\n").map((l) => (THREAT.test(l) ? "[BLOCKED]" : l)).join("\n");
103
- }
104
-
105
- /** Slicing by UTF-16 unit can strand half of a surrogate pair at either cut —
106
- * drop a leading low / trailing high orphan so emitted text stays well-formed. */
107
- function trimOrphanSurrogates(s: string): string {
108
- const head = s.charCodeAt(0); // NaN on empty: both range checks are false
109
- if (head >= 0xdc00 && head <= 0xdfff) s = s.slice(1);
110
- const tail = s.charCodeAt(s.length - 1);
111
- if (tail >= 0xd800 && tail <= 0xdbff) s = s.slice(0, -1);
112
- return s;
113
- }
114
-
115
- /** Single-line preview: 120-char window starting 40 before the first matched term
116
- * (hermes_state_search.py:1585-1595). Ellipses mark clipping. */
117
- function makePreview(text: string, terms: string[]): string {
118
- const flat = text.replace(/\s+/g, " ").trim();
119
- const lower = flat.toLowerCase();
120
- let pos = -1;
121
- for (const t of terms) {
122
- const i = lower.indexOf(t);
123
- if (i !== -1 && (pos === -1 || i < pos)) pos = i;
124
- }
125
- const start = pos === -1 ? 0 : Math.max(0, pos - PREVIEW_LEAD);
126
- const clip = trimOrphanSurrogates(flat.slice(start, start + PREVIEW_WINDOW));
127
- return (start > 0 ? "…" : "") + clip + (start + PREVIEW_WINDOW < flat.length ? "…" : "");
128
- }
129
-
130
- /** Inverted index over every session's entries.jsonl. Incremental: a file is
131
- * re-read only when its mtime (or size — appends always grow JSONL) changed;
132
- * deleted session dirs drop out. Session identity = the DIRECTORY name, matching
133
- * session.ts listSessions (a tampered meta.json must not redirect recall). */
134
- export class RecallIndex {
135
- private docs = new Map<string, Doc>(); // docKey -> doc
136
- private postings = new Map<string, Map<string, number>>(); // token -> docKey -> tf
137
- private files = new Map<string, FileState>(); // sessionId -> file state
138
-
139
- constructor(private readonly root: string) {}
140
-
141
- /** Stat every session file; (re)index changed/new ones, drop vanished ones. */
142
- refresh(): RefreshStats {
143
- const stats: RefreshStats = { scanned: 0, indexed: 0, removed: 0 };
144
- let names: string[] = [];
145
- try { names = readdirSync(this.root); } catch { /* missing root = empty index */ }
146
- const live = new Set<string>();
147
- for (const name of names) {
148
- const file = join(this.root, name, "entries.jsonl");
149
- let st: { mtimeMs: number; size: number };
150
- try { const s = statSync(file); st = { mtimeMs: s.mtimeMs, size: s.size }; }
151
- catch { continue; } // foreign dir / no entries yet: skip, never throw
152
- live.add(name);
153
- stats.scanned++;
154
- const prev = this.files.get(name);
155
- if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) continue; // unchanged: keyed by mtime
156
- this.dropSession(name);
157
- this.indexFile(name, file, st);
158
- stats.indexed++;
159
- }
160
- for (const name of [...this.files.keys()]) {
161
- if (!live.has(name)) { this.dropSession(name); stats.removed++; }
162
- }
163
- return stats;
164
- }
165
-
166
- private dropSession(sessionId: string): void {
167
- const prev = this.files.get(sessionId);
168
- if (!prev) return;
169
- for (const key of prev.docKeys) {
170
- const doc = this.docs.get(key);
171
- if (doc) {
172
- for (const token of doc.tokens.keys()) {
173
- const posting = this.postings.get(token);
174
- if (posting) { posting.delete(key); if (posting.size === 0) this.postings.delete(token); }
175
- }
176
- }
177
- this.docs.delete(key);
178
- }
179
- this.files.delete(sessionId);
180
- }
181
-
182
- private indexFile(sessionId: string, file: string, st: { mtimeMs: number; size: number }): void {
183
- const docKeys: string[] = [];
184
- let content = "";
185
- try { content = readFileSync(file, "utf8"); } catch { /* raced deletion: index as empty */ }
186
- for (const line of content.split("\n")) {
187
- if (!line.trim()) continue;
188
- const parsed = parseLine(line);
189
- if (!parsed) continue;
190
- // length-prefixed doc key: immune to separator-content collisions — session
191
- // "s" + entry "0000e1" and session "s0000" + entry "e1" must stay distinct.
192
- // The previous separator was the literal 4-char string "0000" (bytes 0x30
193
- // 0x30 0x30 0x30 — an intended U+0000 NUL escape written without the
194
- // backslash-u), so exactly that pair collided and the second doc was
195
- // silently dropped by the duplicate-key guard below.
196
- const key = `${sessionId.length}:${sessionId}:${parsed.entryId}`;
197
- if (this.docs.has(key)) continue; // duplicate-id guard (session.ts corruption class)
198
- const tokens = new Map<string, number>();
199
- for (const t of tokenize(parsed.text)) tokens.set(t, (tokens.get(t) ?? 0) + 1);
200
- if (tokens.size === 0) continue;
201
- this.docs.set(key, { sessionId, entryId: parsed.entryId, text: parsed.text, timestamp: parsed.timestamp, tokens });
202
- docKeys.push(key);
203
- for (const [token, tf] of tokens) {
204
- let posting = this.postings.get(token);
205
- if (!posting) { posting = new Map(); this.postings.set(token, posting); }
206
- posting.set(key, tf);
207
- }
208
- }
209
- this.files.set(sessionId, { mtimeMs: st.mtimeMs, size: st.size, docKeys });
210
- }
211
-
212
- /** Ranked search. Terms are ANDed (FTS5 implicit AND). Tiering: docs with more
213
- * exact-term matches ALWAYS outrank docs matching only partially (hermes's
214
- * exact-FTS5-before-trigram routing as a rank, not a fallback); within a tier,
215
- * weighted tf desc, then timestamp desc, then key for determinism. */
216
- search(query: string, limit: number, excludeSession?: string): RecallHit[] {
217
- this.refresh();
218
- const terms = [...new Set(tokenize(query.slice(0, MAX_QUERY_CHARS)))];
219
- if (terms.length === 0) return [];
220
-
221
- let cands: Map<string, { exact: number; weighted: number }> | null = null;
222
- for (const term of terms) {
223
- const matched = new Map<string, { ex: number; part: number }>();
224
- const exact = this.postings.get(term);
225
- if (exact) for (const [key, tf] of exact) matched.set(key, { ex: tf, part: 0 });
226
- if (term.length >= MIN_PARTIAL_TERM) { // substring tier, trigram-eligible terms only
227
- for (const [token, posting] of this.postings) {
228
- if (token === term || !token.includes(term)) continue;
229
- for (const [key, tf] of posting) {
230
- const m = matched.get(key) ?? { ex: 0, part: 0 };
231
- m.part += tf;
232
- matched.set(key, m);
233
- }
234
- }
235
- }
236
- const next = new Map<string, { exact: number; weighted: number }>();
237
- for (const [key, m] of matched) {
238
- const prev = cands === null ? { exact: 0, weighted: 0 } : cands.get(key);
239
- if (prev === undefined) continue; // AND: term missing from doc drops it
240
- next.set(key, {
241
- exact: prev.exact + (m.ex > 0 ? 1 : 0),
242
- weighted: prev.weighted + EXACT_WEIGHT * m.ex + PARTIAL_WEIGHT * m.part,
243
- });
244
- }
245
- cands = next;
246
- if (cands.size === 0) return [];
247
- }
248
-
249
- const ranked = [...(cands ?? new Map<string, { exact: number; weighted: number }>())]
250
- .flatMap(([key, cand]) => {
251
- const doc = this.docs.get(key);
252
- // exclude the live session: recall is CROSS-session (hermes skips the
253
- // current lineage, session_search_tool.py:852-860)
254
- return doc && doc.sessionId !== excludeSession ? [{ key, cand, doc }] : [];
255
- })
256
- .sort((a, b) =>
257
- b.cand.exact - a.cand.exact
258
- || b.cand.weighted - a.cand.weighted
259
- || b.doc.timestamp - a.doc.timestamp
260
- || (a.key < b.key ? -1 : 1));
261
-
262
- return ranked.slice(0, Math.max(0, limit)).map(({ doc }) => ({
263
- sessionId: doc.sessionId,
264
- entryId: doc.entryId,
265
- // neutralized HERE so every consumer — tool output, data.hits, the injected
266
- // summarizer — sees the scanned view, never the verbatim transcript line
267
- preview: neutralize(makePreview(doc.text, terms)),
268
- timestamp: doc.timestamp,
269
- }));
270
- }
271
- }
272
-
273
- export interface RecallToolOptions {
274
- /** injected summarizer; absent = raw hits only (upstream removed its LLM summary mode) */
275
- summarize?: SummarizeFn;
276
- /** result-budget ceiling; defaults to hermes's 10 */
277
- maxResults?: number;
278
- }
279
-
280
- /** Build the `recall` tool over a sessions root (one lazy index per tool instance).
281
- *
282
- * kind "read", NOT "memory": recall only READS session files from disk — it never
283
- * mutates memory. core/tools.ts actionFor() maps "read" -> "file.read" but
284
- * "memory" -> "memory.write"; gating a pure read behind a write action would let
285
- * memory-write policies silently grant history reads AND lock recall out of
286
- * read-only rule sets. With no `path` arg, describeResource() falls back to the
287
- * tool name, so policy can target `file.read recall` precisely; deny-by-default
288
- * still applies when no rule matches (core/tools.ts evaluatePermissions). */
289
- export function recallTool(sessionsRoot: string, opts: RecallToolOptions = {}): Tool {
290
- const index = new RecallIndex(sessionsRoot);
291
- const ceiling = Math.max(1, opts.maxResults ?? MAX_RESULTS);
292
- return {
293
- schema: {
294
- name: "recall",
295
- description:
296
- "Search past session transcripts (cross-session recall). Full-text over prior conversation " +
297
- "message text — no LLM. Terms are ANDed; exact word matches rank above partial (substring) " +
298
- `matches. Returns up to ${ceiling} hits: sessionId, entryId, timestamp, and a short preview. ` +
299
- "Use for questions about past conversations: 'what did we decide about X', 'where did we leave Y'.",
300
- args: {
301
- type: "object",
302
- properties: {
303
- query: { type: "string", description: "words to find in past sessions" },
304
- limit: { type: "integer", description: `max hits (default ${Math.min(DEFAULT_RESULTS, ceiling)}, max ${ceiling})` },
305
- },
306
- required: ["query"],
307
- },
308
- },
309
- kind: "read",
310
- sequential: false, // pure read: safe to run concurrently with sibling reads
311
- async execute(args: unknown, ctx: ToolContext): Promise<ToolOutput> {
312
- // Defense in depth: keep ONLY schema args (query, limit) — smuggled keys,
313
- // notably `path`, must never influence behavior. core/tools.ts
314
- // describeResource() prefers an args `path` over the tool-name fallback, so
315
- // {query, path:"/x"} re-aims a `file.read recall` deny rule at "/x"; policy
316
- // runs BEFORE execute, so this strip cannot repair that gate — the
317
- // authoritative fix belongs in describeResource (validate against the tool
318
- // schema). Residual gap documented in recall.test.ts. A fresh object (not
319
- // deletes on `args`) because the registry reuses the caller's object for
320
- // loop-guard identity and onToolResult after execute.
321
- const raw = (args && typeof args === "object" ? args : {}) as Record<string, unknown>;
322
- const a: { query?: unknown; limit?: unknown } = { query: raw.query, limit: raw.limit };
323
- if (typeof a.query !== "string" || a.query.trim().length === 0) {
324
- return { ok: false, output: "recall failed: query must be a non-empty string" };
325
- }
326
- // the echoed query is bounded like the searched one: a 200k-char query must
327
- // not reflect 200k chars into tool output (truncation marked with an ellipsis)
328
- const trimmed = a.query.trim();
329
- const echo = trimmed.length > MAX_QUERY_CHARS
330
- ? trimOrphanSurrogates(trimmed.slice(0, MAX_QUERY_CHARS)) + "…" : trimmed;
331
- // hermes limit clamp: max(1, min(limit, ceiling)) (session_search_tool.py:1040-1046)
332
- let limit = Math.min(DEFAULT_RESULTS, ceiling);
333
- if (typeof a.limit === "number" && Number.isFinite(a.limit)) limit = Math.trunc(a.limit);
334
- limit = Math.max(1, Math.min(limit, ceiling));
335
-
336
- const hits = index.search(a.query, limit, ctx.sessionId);
337
- if (hits.length === 0) {
338
- // actionable empty message, hermes session_search_tool.py:806-810
339
- return { ok: true, output: `recall: no matches for "${echo}" — terms are ANDed; try fewer or broader terms`, data: { hits } };
340
- }
341
- const lines = hits.map((h) =>
342
- `- [${h.sessionId}] entry ${h.entryId} @ ${h.timestamp > 0 ? new Date(h.timestamp).toISOString() : "unknown time"}\n ${h.preview}`);
343
- let output = `recall: ${hits.length} hit(s) for "${echo}"\n` + lines.join("\n");
344
- if (opts.summarize) {
345
- try {
346
- const summary = await opts.summarize(a.query, hits);
347
- if (summary) output += `\n\nsummary: ${summary}`;
348
- } catch (e) {
349
- output += `\n\n(summarize step failed: ${e instanceof Error ? e.message : String(e)}; hits above are unaffected)`;
350
- }
351
- }
352
- return { ok: true, output, data: { hits } };
353
- },
354
- };
355
- }