rovecode 0.3.3 → 0.4.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. package/CHANGELOG.md +527 -0
  2. package/README.md +46 -48
  3. package/bin/rovecode.js +24 -0
  4. package/dist/cli/app-j6gn14w3.js +2 -0
  5. package/dist/cli/ask-user-cwstt8fz.js +2 -0
  6. package/dist/cli/auth-login-9bbp9915.js +2 -0
  7. package/dist/cli/auth-m8p9grty.js +2 -0
  8. package/dist/cli/bench-16zqdms5.js +9 -0
  9. package/dist/cli/catalog-1xchffa4.js +2 -0
  10. package/dist/cli/cli-1n1zb64f.js +2 -0
  11. package/dist/cli/client-2t9gjkck.js +2 -0
  12. package/dist/cli/commands-exafvm2b.js +2 -0
  13. package/dist/cli/connect-6zde0kn3.js +2 -0
  14. package/dist/cli/context-cmd-5t43wgqt.js +2 -0
  15. package/dist/cli/context-report-kt01pw8y.js +2 -0
  16. package/dist/cli/count-remote-ap7x3vh6.js +2 -0
  17. package/dist/cli/design-ne5zszyh.js +2 -0
  18. package/dist/cli/dispatch-2r5myxye.js +2 -0
  19. package/dist/cli/doctor-ws4fh4tn.js +3 -0
  20. package/dist/cli/executor-bdrjn634.js +2 -0
  21. package/dist/cli/export-1mxb9g5p.js +2 -0
  22. package/dist/cli/files-g104xghh.js +2 -0
  23. package/dist/cli/gauntlet-07xrjpj7.js +2 -0
  24. package/dist/cli/gauntlet-runner-xvy64436.js +10 -0
  25. package/dist/cli/gauntlet-wave3-jm91yt5w.js +5 -0
  26. package/dist/cli/gauntlet-wave4-r13py7p1.js +14 -0
  27. package/dist/cli/hashline-znvrat11.js +2 -0
  28. package/dist/cli/http-xafw6fsh.js +143 -0
  29. package/dist/cli/index-1sgjm25y.js +2 -0
  30. package/dist/cli/init-g2m0tn4m.js +51 -0
  31. package/dist/cli/install-avaqjjqq.js +2 -0
  32. package/dist/cli/loop-mmpfft01.js +2 -0
  33. package/dist/cli/main-0904f6ps.js +5 -0
  34. package/dist/cli/main-0ab9fc26.js +9 -0
  35. package/dist/cli/main-0jys2ccn.js +3 -0
  36. package/dist/cli/main-0mtcdbs7.js +3 -0
  37. package/dist/cli/main-0z1w2zsg.js +3 -0
  38. package/dist/cli/main-1dchs7xv.js +18 -0
  39. package/dist/cli/main-1ereejm1.js +3 -0
  40. package/dist/cli/main-1k1kw6b5.js +3 -0
  41. package/dist/cli/main-27y4sm2k.js +38 -0
  42. package/dist/cli/main-2wwjex5j.js +58 -0
  43. package/dist/cli/main-2yeveeve.js +6 -0
  44. package/dist/cli/main-2yfck9b5.js +3 -0
  45. package/dist/cli/main-2zmzgkwh.js +3 -0
  46. package/dist/cli/main-351pz3z7.js +7 -0
  47. package/dist/cli/main-3gjqfh7a.js +6 -0
  48. package/dist/cli/main-3nf3kgve.js +3 -0
  49. package/dist/cli/main-3pjrb2hd.js +3 -0
  50. package/dist/cli/main-3rxcvgna.js +19 -0
  51. package/dist/cli/main-4b3jgy66.js +19 -0
  52. package/dist/cli/main-4wndhjdc.js +7 -0
  53. package/dist/cli/main-4xcmvxnk.js +3 -0
  54. package/dist/cli/main-5tbz0wbz.js +4 -0
  55. package/dist/cli/main-5ywnwthm.js +3 -0
  56. package/dist/cli/main-6b62vkz0.js +14 -0
  57. package/dist/cli/main-6dnk69vp.js +3 -0
  58. package/dist/cli/main-6genrmhs.js +136 -0
  59. package/dist/cli/main-73g7eff4.js +15 -0
  60. package/dist/cli/main-7c5thhjd.js +5 -0
  61. package/dist/cli/main-7rn6bqje.js +3 -0
  62. package/dist/cli/main-80haw7qk.js +4 -0
  63. package/dist/cli/main-875s60s2.js +4 -0
  64. package/dist/cli/main-8kjxbpw4.js +8 -0
  65. package/dist/cli/main-90ds1z4e.js +10 -0
  66. package/dist/cli/main-9etavkew.js +3 -0
  67. package/dist/cli/main-a9njrkk1.js +3 -0
  68. package/dist/cli/main-aecrjq2d.js +12 -0
  69. package/dist/cli/main-ck9asesq.js +9 -0
  70. package/dist/cli/main-cta9racd.js +4 -0
  71. package/dist/cli/main-ddv7j2ag.js +3 -0
  72. package/dist/cli/main-dfreez27.js +10 -0
  73. package/dist/cli/main-f7rw7des.js +3 -0
  74. package/dist/cli/main-ggcn7rd7.js +5 -0
  75. package/dist/cli/main-gzkmycnv.js +3 -0
  76. package/dist/cli/main-hq51jg8v.js +18 -0
  77. package/dist/cli/main-jft389w9.js +8 -0
  78. package/dist/cli/main-k1eqkg83.js +3 -0
  79. package/dist/cli/main-k2y8a2aw.js +9 -0
  80. package/dist/cli/main-kcpbykxz.js +4 -0
  81. package/dist/cli/main-kd488vje.js +22 -0
  82. package/dist/cli/main-kh32yvgk.js +5 -0
  83. package/dist/cli/main-kqxnqjnv.js +25 -0
  84. package/dist/cli/main-kyn0xnsg.js +3 -0
  85. package/dist/cli/main-m1kk6fp5.js +21 -0
  86. package/dist/cli/main-mv40pcr2.js +4 -0
  87. package/dist/cli/main-n0t3973w.js +3 -0
  88. package/dist/cli/main-nqveez48.js +4 -0
  89. package/dist/cli/main-pknhvrmj.js +3 -0
  90. package/dist/cli/main-pn1w7a7j.js +3 -0
  91. package/dist/cli/main-prxxs70n.js +4 -0
  92. package/dist/cli/main-q3vsesf9.js +3 -0
  93. package/dist/cli/main-qsevpgsv.js +3 -0
  94. package/dist/cli/main-rdgdw24b.js +25 -0
  95. package/dist/cli/main-rfth4tbm.js +16 -0
  96. package/dist/cli/main-rg0wn0xf.js +5 -0
  97. package/dist/cli/main-sdmxhtv8.js +4 -0
  98. package/dist/cli/main-skbp13js.js +18 -0
  99. package/dist/cli/main-t4xnd213.js +7 -0
  100. package/dist/cli/main-vqak588n.js +4 -0
  101. package/dist/cli/main-w2n1303f.js +9 -0
  102. package/dist/cli/main-wbrdspr2.js +5 -0
  103. package/dist/cli/main-wsrg79c1.js +7 -0
  104. package/dist/cli/main-x4r0fne4.js +5 -0
  105. package/dist/cli/main-xea2f3tn.js +6 -0
  106. package/dist/cli/main-xg704a3c.js +3 -0
  107. package/dist/cli/main-xvnrabfp.js +16 -0
  108. package/dist/cli/main-xy53xf0r.js +4 -0
  109. package/dist/cli/main-y1fqy60y.js +3 -0
  110. package/dist/cli/main-yn8cd281.js +34 -0
  111. package/dist/cli/main-yr0ksc0h.js +4 -0
  112. package/dist/cli/main-z2ex2vyf.js +4 -0
  113. package/dist/cli/main-z3aayzvq.js +3 -0
  114. package/dist/cli/main-zaqh35jg.js +3 -0
  115. package/dist/cli/main-zc2e8e46.js +4 -0
  116. package/dist/cli/main-zzrfw6cf.js +13 -0
  117. package/dist/cli/main.js +280 -0
  118. package/dist/cli/market-cmd-e14kmx9n.js +5 -0
  119. package/dist/cli/mcp-login-wq7ktdek.js +2 -0
  120. package/dist/cli/mcp-market-cmd-9mg3jecy.js +2 -0
  121. package/dist/cli/notify-b7qc0cjb.js +2 -0
  122. package/dist/cli/oauth-z8whcgfx.js +2 -0
  123. package/dist/cli/output-b3ewj3ps.js +16 -0
  124. package/dist/cli/profiles-6mr5he5e.js +2 -0
  125. package/dist/cli/provider-config-g7j42q8x.js +2 -0
  126. package/dist/cli/provider-jr1y8vvm.js +2 -0
  127. package/dist/cli/registry-s8yk86g0.js +2 -0
  128. package/dist/cli/registry-t6p8d4mn.js +2 -0
  129. package/dist/cli/repl-bajwe1mh.js +11 -0
  130. package/dist/cli/resume-rwn9nz7y.js +2 -0
  131. package/dist/cli/run-flags-nah7ndpt.js +2 -0
  132. package/dist/cli/runtime-n7gafzhb.js +2 -0
  133. package/dist/cli/sandbox-config-emdy18x4.js +2 -0
  134. package/dist/cli/server-b0nvs2bn.js +5 -0
  135. package/dist/cli/session-arg-y75wd4kj.js +2 -0
  136. package/dist/cli/session-j62evmjq.js +2 -0
  137. package/dist/cli/sessions-cmd-tsnwz0ns.js +7 -0
  138. package/dist/cli/settings-df10wfez.js +2 -0
  139. package/dist/cli/setup-jzvv72fg.js +2 -0
  140. package/dist/cli/sextant-smoke-37m81ke6.js +5 -0
  141. package/dist/cli/skills-cmd-gjxnxnhx.js +2 -0
  142. package/dist/cli/smoke-p7748apt.js +8 -0
  143. package/dist/cli/start-chat-s4st3mm0.js +12 -0
  144. package/dist/cli/stream-gmeyewds.js +2 -0
  145. package/dist/cli/task-gh0kkp3n.js +2 -0
  146. package/dist/cli/tasks-z1kfpe8e.js +2 -0
  147. package/dist/cli/thinking-0eqkrz6t.js +2 -0
  148. package/dist/cli/todo-5brcrt9m.js +2 -0
  149. package/dist/cli/tools-7pzm0vj9.js +2 -0
  150. package/dist/cli/tools-s635p6s8.js +2 -0
  151. package/dist/cli/trust-cmd-cjav8zgm.js +2 -0
  152. package/dist/cli/update-check-pt31bm2f.js +2 -0
  153. package/dist/cli/update-cmd-tk131s9t.js +2 -0
  154. package/dist/cli/voice-56nabd8d.js +2 -0
  155. package/dist/cli/webfetch-xd8q596m.js +2 -0
  156. package/dist/cli/websearch-5hkf98k1.js +2 -0
  157. package/dist/cli/workflow-cmd-cy3cvzjp.js +4 -0
  158. package/dist/cli/workspace-q10g5z3e.js +2 -0
  159. package/dist/lib/index.js +62 -0
  160. package/dist/lib/models-index.json +1 -0
  161. package/dist/lib/plugins.js +55 -0
  162. package/dist/lib/providers.js +17 -0
  163. package/dist/lib/public-api.js +20 -0
  164. package/dist/lib/sdk.js +360 -0
  165. package/package.json +84 -72
  166. package/bin/rovecode.ts +0 -21
  167. package/src/acp/server.ts +0 -374
  168. package/src/cli/auth-login.ts +0 -122
  169. package/src/cli/connect.ts +0 -244
  170. package/src/cli/context-cmd.ts +0 -199
  171. package/src/cli/dispatch.ts +0 -82
  172. package/src/cli/doctor.ts +0 -362
  173. package/src/cli/export.ts +0 -276
  174. package/src/cli/help.ts +0 -293
  175. package/src/cli/is-tui-invocation.ts +0 -8
  176. package/src/cli/main.ts +0 -583
  177. package/src/cli/market-cmd.ts +0 -658
  178. package/src/cli/mcp-login.ts +0 -141
  179. package/src/cli/mcp-market-cmd.ts +0 -302
  180. package/src/cli/output.ts +0 -382
  181. package/src/cli/repl.ts +0 -250
  182. package/src/cli/repomap-root.ts +0 -14
  183. package/src/cli/resume.ts +0 -57
  184. package/src/cli/run-flags.ts +0 -43
  185. package/src/cli/run-limits.ts +0 -78
  186. package/src/cli/runtime.ts +0 -931
  187. package/src/cli/session-arg.ts +0 -30
  188. package/src/cli/sessions-cmd.ts +0 -145
  189. package/src/cli/setup.ts +0 -153
  190. package/src/cli/skills-cmd.ts +0 -194
  191. package/src/cli/start-chat.ts +0 -65
  192. package/src/cli/trust-cmd.ts +0 -52
  193. package/src/coding/bash.ts +0 -148
  194. package/src/coding/checkpoints.ts +0 -327
  195. package/src/coding/diff.ts +0 -138
  196. package/src/coding/files.ts +0 -341
  197. package/src/coding/hashline.ts +0 -274
  198. package/src/coding/lsp-gate.ts +0 -254
  199. package/src/coding/lsp-servers.ts +0 -147
  200. package/src/coding/lsp.ts +0 -283
  201. package/src/coding/repomap-cache.ts +0 -99
  202. package/src/coding/repomap-files.ts +0 -192
  203. package/src/coding/repomap.ts +0 -481
  204. package/src/core/agents.ts +0 -255
  205. package/src/core/compaction.ts +0 -259
  206. package/src/core/config.ts +0 -289
  207. package/src/core/context-report.ts +0 -228
  208. package/src/core/context.ts +0 -60
  209. package/src/core/count-remote.ts +0 -107
  210. package/src/core/execpolicy-rules.ts +0 -196
  211. package/src/core/execpolicy.ts +0 -385
  212. package/src/core/executor.ts +0 -454
  213. package/src/core/guardrails.ts +0 -400
  214. package/src/core/hooks.ts +0 -411
  215. package/src/core/images.ts +0 -230
  216. package/src/core/intro.ts +0 -266
  217. package/src/core/loop.ts +0 -567
  218. package/src/core/modes.ts +0 -372
  219. package/src/core/orchestrator.ts +0 -245
  220. package/src/core/proc-group.ts +0 -48
  221. package/src/core/project-trust.ts +0 -98
  222. package/src/core/reflection.ts +0 -165
  223. package/src/core/sandbox-config.ts +0 -186
  224. package/src/core/session-id.ts +0 -24
  225. package/src/core/session-images.ts +0 -73
  226. package/src/core/session-ops.ts +0 -183
  227. package/src/core/session-text.ts +0 -29
  228. package/src/core/session.ts +0 -469
  229. package/src/core/settings.ts +0 -170
  230. package/src/core/tasks.ts +0 -646
  231. package/src/core/token-scale.ts +0 -108
  232. package/src/core/tools.ts +0 -309
  233. package/src/core/trust.ts +0 -104
  234. package/src/core/types.ts +0 -330
  235. package/src/core/update-check.ts +0 -171
  236. package/src/core/usage.ts +0 -204
  237. package/src/core/validate.ts +0 -121
  238. package/src/core/verify-gate.ts +0 -159
  239. package/src/core/verify.ts +0 -236
  240. package/src/core/voice.ts +0 -158
  241. package/src/core/win-job.ts +0 -183
  242. package/src/core/workspace.ts +0 -184
  243. package/src/design/audit.ts +0 -797
  244. package/src/design/direction.ts +0 -190
  245. package/src/design/rules.ts +0 -157
  246. package/src/eval/bench.ts +0 -150
  247. package/src/eval/gauntlet-runner.ts +0 -215
  248. package/src/eval/gauntlet-support.ts +0 -84
  249. package/src/eval/gauntlet-wave3.ts +0 -269
  250. package/src/eval/gauntlet-wave4.ts +0 -217
  251. package/src/eval/gauntlet.ts +0 -253
  252. package/src/index.ts +0 -17
  253. package/src/lanes/agy.ts +0 -95
  254. package/src/lanes/approval.ts +0 -24
  255. package/src/lanes/claude.ts +0 -129
  256. package/src/lanes/codex.ts +0 -127
  257. package/src/lanes/events.ts +0 -130
  258. package/src/lanes/job.ts +0 -142
  259. package/src/lanes/opencode.ts +0 -122
  260. package/src/lanes/process.ts +0 -184
  261. package/src/lanes/progress.ts +0 -183
  262. package/src/lanes/registry.ts +0 -178
  263. package/src/lanes/runner.ts +0 -124
  264. package/src/lanes/types.ts +0 -112
  265. package/src/market/catalogs/mcp-docs.json +0 -111
  266. package/src/market/catalogs/plugins.json +0 -111
  267. package/src/market/catalogs/skills.json +0 -478
  268. package/src/market/clone.ts +0 -72
  269. package/src/market/context-cost.ts +0 -121
  270. package/src/market/digest.ts +0 -106
  271. package/src/market/index.ts +0 -22
  272. package/src/market/install.ts +0 -578
  273. package/src/market/manifest.ts +0 -187
  274. package/src/market/prereq.ts +0 -145
  275. package/src/market/registry.ts +0 -363
  276. package/src/market/resolve.ts +0 -111
  277. package/src/market/types.ts +0 -236
  278. package/src/market/validate.ts +0 -227
  279. package/src/mcp/client.ts +0 -449
  280. package/src/mcp/config.ts +0 -252
  281. package/src/mcp/local-package.ts +0 -211
  282. package/src/mcp/market-catalog.ts +0 -84
  283. package/src/mcp/market-install.ts +0 -289
  284. package/src/mcp/market.ts +0 -362
  285. package/src/mcp/oauth.ts +0 -251
  286. package/src/mcp/prompts-resources.ts +0 -249
  287. package/src/mcp/shared.ts +0 -149
  288. package/src/mcp/status.ts +0 -67
  289. package/src/mcp/tools.ts +0 -275
  290. package/src/mcp/transport.ts +0 -122
  291. package/src/mcp/trust.ts +0 -25
  292. package/src/memory/blocks.ts +0 -278
  293. package/src/memory/recall.ts +0 -355
  294. package/src/memory/scope.ts +0 -182
  295. package/src/memory/store.ts +0 -105
  296. package/src/memory/tools.ts +0 -99
  297. package/src/plugins/cli.ts +0 -119
  298. package/src/plugins/discover.ts +0 -108
  299. package/src/plugins/index.ts +0 -50
  300. package/src/plugins/install.ts +0 -184
  301. package/src/plugins/load.ts +0 -124
  302. package/src/plugins/manifest.ts +0 -92
  303. package/src/plugins/state.ts +0 -83
  304. package/src/providers/auth.ts +0 -408
  305. package/src/providers/cache.ts +0 -223
  306. package/src/providers/catalog-local.ts +0 -160
  307. package/src/providers/catalog.ts +0 -421
  308. package/src/providers/middleware-context.ts +0 -86
  309. package/src/providers/middleware.ts +0 -373
  310. package/src/providers/model-list.ts +0 -23
  311. package/src/providers/oauth/common.ts +0 -105
  312. package/src/providers/oauth/device-code.ts +0 -107
  313. package/src/providers/oauth/github-copilot.ts +0 -146
  314. package/src/providers/oauth/loopback.ts +0 -158
  315. package/src/providers/oauth/openai.ts +0 -163
  316. package/src/providers/oauth/openrouter.ts +0 -89
  317. package/src/providers/oauth/pkce.ts +0 -45
  318. package/src/providers/oauth/registry.ts +0 -39
  319. package/src/providers/oauth/seam.ts +0 -89
  320. package/src/providers/profile-glm53.ts +0 -111
  321. package/src/providers/profile-sonnet5-persona.ts +0 -65
  322. package/src/providers/profile-sonnet5-voice.ts +0 -23
  323. package/src/providers/profiles.ts +0 -156
  324. package/src/providers/provider-config.ts +0 -311
  325. package/src/providers/registry.ts +0 -333
  326. package/src/providers/responses.ts +0 -209
  327. package/src/providers/retry.ts +0 -234
  328. package/src/providers/router.ts +0 -294
  329. package/src/providers/sse.ts +0 -26
  330. package/src/providers/stream-errors.ts +0 -117
  331. package/src/providers/stream.ts +0 -566
  332. package/src/providers/thinking.ts +0 -189
  333. package/src/providers/wire-messages.ts +0 -129
  334. package/src/providers/wire-responses.ts +0 -79
  335. package/src/providers/wire-select.ts +0 -53
  336. package/src/server/http.ts +0 -291
  337. package/src/server/openapi.ts +0 -246
  338. package/src/sextant/card-hits.ts +0 -102
  339. package/src/sextant/card-keys.ts +0 -55
  340. package/src/sextant/context-source.ts +0 -157
  341. package/src/sextant/crew-cards.ts +0 -350
  342. package/src/sextant/draw-agents.ts +0 -273
  343. package/src/sextant/draw-code.ts +0 -388
  344. package/src/sextant/draw-context.ts +0 -222
  345. package/src/sextant/draw-frame.ts +0 -164
  346. package/src/sextant/draw-market.ts +0 -573
  347. package/src/sextant/draw-messages.ts +0 -386
  348. package/src/sextant/draw-pet.ts +0 -230
  349. package/src/sextant/draw-plan.ts +0 -187
  350. package/src/sextant/draw-tabs.ts +0 -85
  351. package/src/sextant/draw-util.ts +0 -65
  352. package/src/sextant/draw-wizard.ts +0 -378
  353. package/src/sextant/engine.ts +0 -230
  354. package/src/sextant/frame-hits.ts +0 -25
  355. package/src/sextant/frame.ts +0 -101
  356. package/src/sextant/git-status.ts +0 -197
  357. package/src/sextant/grid.ts +0 -59
  358. package/src/sextant/input.ts +0 -119
  359. package/src/sextant/keys.ts +0 -521
  360. package/src/sextant/layout.ts +0 -86
  361. package/src/sextant/local-commands.ts +0 -169
  362. package/src/sextant/market-source.ts +0 -287
  363. package/src/sextant/mentions.ts +0 -200
  364. package/src/sextant/message-hits.ts +0 -26
  365. package/src/sextant/model.ts +0 -387
  366. package/src/sextant/overlays.ts +0 -456
  367. package/src/sextant/panel-hits.ts +0 -38
  368. package/src/sextant/pet.ts +0 -399
  369. package/src/sextant/screen.ts +0 -324
  370. package/src/sextant/scroll-hits.ts +0 -66
  371. package/src/sextant/scrollbar.ts +0 -82
  372. package/src/sextant/sextant-bridge.ts +0 -174
  373. package/src/sextant/sextant-cards.ts +0 -142
  374. package/src/sextant/sextant-diff-base.ts +0 -63
  375. package/src/sextant/sextant-files.ts +0 -154
  376. package/src/sextant/sextant-frame-loop.ts +0 -335
  377. package/src/sextant/sextant-renderer.ts +0 -574
  378. package/src/sextant/sextant-repo.ts +0 -140
  379. package/src/sextant/theme.ts +0 -66
  380. package/src/sextant/tool-rows.ts +0 -189
  381. package/src/sextant/types.ts +0 -493
  382. package/src/skills/index.ts +0 -387
  383. package/src/skills/pack.ts +0 -220
  384. package/src/skills/spec.ts +0 -162
  385. package/src/skills/tools.ts +0 -69
  386. package/src/skills/versioned.ts +0 -227
  387. package/src/telemetry/otel-export.ts +0 -122
  388. package/src/telemetry/otel-lanes.ts +0 -89
  389. package/src/telemetry/otel-logs.ts +0 -131
  390. package/src/telemetry/otel-metrics.ts +0 -136
  391. package/src/telemetry/otel.ts +0 -397
  392. package/src/telemetry/otlp.ts +0 -76
  393. package/src/tools/ask-user.ts +0 -156
  394. package/src/tools/bash-bg.ts +0 -94
  395. package/src/tools/bash-jobs.ts +0 -237
  396. package/src/tools/design.ts +0 -151
  397. package/src/tools/evalcell.ts +0 -338
  398. package/src/tools/html-text.ts +0 -139
  399. package/src/tools/provider.ts +0 -149
  400. package/src/tools/task.ts +0 -250
  401. package/src/tools/todo.ts +0 -320
  402. package/src/tools/webfetch.ts +0 -332
  403. package/src/tools/websearch.ts +0 -359
  404. package/src/tui/agents-cmd.ts +0 -41
  405. package/src/tui/app.ts +0 -749
  406. package/src/tui/attach.ts +0 -127
  407. package/src/tui/boot-notes.ts +0 -41
  408. package/src/tui/builtin-prompts.ts +0 -59
  409. package/src/tui/checkpoints-cmd.ts +0 -70
  410. package/src/tui/clipboard-image.ts +0 -81
  411. package/src/tui/clipboard.ts +0 -78
  412. package/src/tui/commands.ts +0 -283
  413. package/src/tui/config-view.ts +0 -53
  414. package/src/tui/context-cmds.ts +0 -282
  415. package/src/tui/cost.ts +0 -108
  416. package/src/tui/crash-guard.ts +0 -173
  417. package/src/tui/focus-terminal.ts +0 -34
  418. package/src/tui/git-cmds.ts +0 -273
  419. package/src/tui/git-plain.ts +0 -58
  420. package/src/tui/info-cmd.ts +0 -150
  421. package/src/tui/input-plain.ts +0 -76
  422. package/src/tui/mcp-cmd.ts +0 -128
  423. package/src/tui/memory-note.ts +0 -77
  424. package/src/tui/modes-cmd.ts +0 -45
  425. package/src/tui/notify-seq.ts +0 -100
  426. package/src/tui/notify.ts +0 -318
  427. package/src/tui/overlays.ts +0 -97
  428. package/src/tui/pi-renderer.ts +0 -428
  429. package/src/tui/providers-cmd.ts +0 -377
  430. package/src/tui/reasoning-view.ts +0 -56
  431. package/src/tui/renderer.ts +0 -128
  432. package/src/tui/replay-marker.ts +0 -29
  433. package/src/tui/session-cmd.ts +0 -148
  434. package/src/tui/session-manage.ts +0 -95
  435. package/src/tui/sextant-attach.ts +0 -102
  436. package/src/tui/sextant-io.ts +0 -202
  437. package/src/tui/sextant-smoke.ts +0 -110
  438. package/src/tui/shell-cmd.ts +0 -158
  439. package/src/tui/smoke.ts +0 -72
  440. package/src/tui/staged-terminal.ts +0 -50
  441. package/src/tui/startup.ts +0 -12
  442. package/src/tui/theme.ts +0 -59
  443. package/src/tui/todo-label.ts +0 -7
  444. package/src/tui/trust-card.ts +0 -107
  445. package/src/tui/tui-commands.ts +0 -87
  446. package/tsconfig.json +0 -30
  447. package/vendor/pi-tui/LICENSE +0 -21
  448. package/vendor/pi-tui/PATCHES.md +0 -12
  449. package/vendor/pi-tui/PROVENANCE.md +0 -12
  450. package/vendor/pi-tui/README.upstream.md +0 -854
  451. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  452. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  453. package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
  454. package/vendor/pi-tui/src/autocomplete.ts +0 -827
  455. package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
  456. package/vendor/pi-tui/src/components/box.ts +0 -138
  457. package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
  458. package/vendor/pi-tui/src/components/editor.ts +0 -2364
  459. package/vendor/pi-tui/src/components/h-stack.ts +0 -45
  460. package/vendor/pi-tui/src/components/image.ts +0 -128
  461. package/vendor/pi-tui/src/components/input.ts +0 -448
  462. package/vendor/pi-tui/src/components/loader.ts +0 -93
  463. package/vendor/pi-tui/src/components/markdown.ts +0 -1016
  464. package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
  465. package/vendor/pi-tui/src/components/select-list.ts +0 -230
  466. package/vendor/pi-tui/src/components/settings-list.ts +0 -277
  467. package/vendor/pi-tui/src/components/spacer.ts +0 -29
  468. package/vendor/pi-tui/src/components/stack.ts +0 -155
  469. package/vendor/pi-tui/src/components/text.ts +0 -108
  470. package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
  471. package/vendor/pi-tui/src/components/v-stack.ts +0 -34
  472. package/vendor/pi-tui/src/editor-component.ts +0 -75
  473. package/vendor/pi-tui/src/fuzzy.ts +0 -138
  474. package/vendor/pi-tui/src/index.ts +0 -149
  475. package/vendor/pi-tui/src/keybindings.ts +0 -321
  476. package/vendor/pi-tui/src/keys.ts +0 -1402
  477. package/vendor/pi-tui/src/kill-ring.ts +0 -47
  478. package/vendor/pi-tui/src/latex.ts +0 -1381
  479. package/vendor/pi-tui/src/layout-node.ts +0 -52
  480. package/vendor/pi-tui/src/layout.ts +0 -411
  481. package/vendor/pi-tui/src/native-modifiers.ts +0 -60
  482. package/vendor/pi-tui/src/native-module-path.ts +0 -32
  483. package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
  484. package/vendor/pi-tui/src/terminal-colors.ts +0 -74
  485. package/vendor/pi-tui/src/terminal-image.ts +0 -701
  486. package/vendor/pi-tui/src/terminal.ts +0 -554
  487. package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
  488. package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
  489. package/vendor/pi-tui/src/tui.ts +0 -1264
  490. package/vendor/pi-tui/src/undo-stack.ts +0 -29
  491. package/vendor/pi-tui/src/utils.ts +0 -1327
  492. package/vendor/pi-tui/src/word-navigation.ts +0 -118
  493. package/vendor/pi-tui/test/test-themes.ts +0 -39
  494. package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
  495. /package/{src/providers → dist/cli}/models-index.json +0 -0
@@ -1,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
- }