@pentoshi/clai 2.0.61 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/README.md +262 -368
  2. package/dist/agent/context-manager.d.ts +5 -0
  3. package/dist/agent/context-manager.js +14 -2
  4. package/dist/agent/context-manager.js.map +1 -1
  5. package/dist/agent/events.d.ts +4 -1
  6. package/dist/agent/runner.js +210 -91
  7. package/dist/agent/runner.js.map +1 -1
  8. package/dist/agent/tool-call-parser.d.ts +15 -0
  9. package/dist/agent/tool-call-parser.js +171 -2
  10. package/dist/agent/tool-call-parser.js.map +1 -1
  11. package/dist/app/adapters/agent-event-adapter.d.ts +29 -0
  12. package/dist/app/adapters/agent-event-adapter.js +143 -0
  13. package/dist/app/adapters/agent-event-adapter.js.map +1 -0
  14. package/dist/app/adapters/current-agent-adapter.d.ts +7 -0
  15. package/dist/app/adapters/current-agent-adapter.js +27 -0
  16. package/dist/app/adapters/current-agent-adapter.js.map +1 -0
  17. package/dist/app/adapters/current-jobs-adapter.d.ts +4 -0
  18. package/dist/app/adapters/current-jobs-adapter.js +13 -0
  19. package/dist/app/adapters/current-jobs-adapter.js.map +1 -0
  20. package/dist/app/adapters/current-store-adapter.d.ts +3 -0
  21. package/dist/app/adapters/current-store-adapter.js +22 -0
  22. package/dist/app/adapters/current-store-adapter.js.map +1 -0
  23. package/dist/app/adapters/current-terminal-adapter.d.ts +9 -0
  24. package/dist/app/adapters/current-terminal-adapter.js +31 -0
  25. package/dist/app/adapters/current-terminal-adapter.js.map +1 -0
  26. package/dist/app/adapters/current-updates-adapter.d.ts +7 -0
  27. package/dist/app/adapters/current-updates-adapter.js +31 -0
  28. package/dist/app/adapters/current-updates-adapter.js.map +1 -0
  29. package/dist/app/adapters/in-memory-clipboard-adapter.d.ts +10 -0
  30. package/dist/app/adapters/in-memory-clipboard-adapter.js +20 -0
  31. package/dist/app/adapters/in-memory-clipboard-adapter.js.map +1 -0
  32. package/dist/app/commands/command.d.ts +24 -0
  33. package/dist/app/commands/command.js +4 -0
  34. package/dist/app/commands/command.js.map +1 -0
  35. package/dist/app/commands/registry.d.ts +45 -0
  36. package/dist/app/commands/registry.js +164 -0
  37. package/dist/app/commands/registry.js.map +1 -0
  38. package/dist/app/controllers/disposable.d.ts +15 -0
  39. package/dist/app/controllers/disposable.js +38 -0
  40. package/dist/app/controllers/disposable.js.map +1 -0
  41. package/dist/app/controllers/job-controller.d.ts +22 -0
  42. package/dist/app/controllers/job-controller.js +35 -0
  43. package/dist/app/controllers/job-controller.js.map +1 -0
  44. package/dist/app/controllers/plan-controller.d.ts +31 -0
  45. package/dist/app/controllers/plan-controller.js +69 -0
  46. package/dist/app/controllers/plan-controller.js.map +1 -0
  47. package/dist/app/controllers/session-controller.d.ts +167 -0
  48. package/dist/app/controllers/session-controller.js +467 -0
  49. package/dist/app/controllers/session-controller.js.map +1 -0
  50. package/dist/app/controllers/turn-controller.d.ts +52 -0
  51. package/dist/app/controllers/turn-controller.js +118 -0
  52. package/dist/app/controllers/turn-controller.js.map +1 -0
  53. package/dist/app/events/app-event.d.ts +118 -0
  54. package/dist/app/events/app-event.js +25 -0
  55. package/dist/app/events/app-event.js.map +1 -0
  56. package/dist/app/events/event-buffer.d.ts +51 -0
  57. package/dist/app/events/event-buffer.js +109 -0
  58. package/dist/app/events/event-buffer.js.map +1 -0
  59. package/dist/app/events/sequencer.d.ts +32 -0
  60. package/dist/app/events/sequencer.js +55 -0
  61. package/dist/app/events/sequencer.js.map +1 -0
  62. package/dist/app/ports/agent-port.d.ts +29 -0
  63. package/dist/app/ports/agent-port.js +2 -0
  64. package/dist/app/ports/agent-port.js.map +1 -0
  65. package/dist/app/ports/clipboard-port.d.ts +9 -0
  66. package/dist/app/ports/clipboard-port.js +2 -0
  67. package/dist/app/ports/clipboard-port.js.map +1 -0
  68. package/dist/app/ports/confirm-port.d.ts +17 -0
  69. package/dist/app/ports/confirm-port.js +2 -0
  70. package/dist/app/ports/confirm-port.js.map +1 -0
  71. package/dist/app/ports/jobs-port.d.ts +18 -0
  72. package/dist/app/ports/jobs-port.js +2 -0
  73. package/dist/app/ports/jobs-port.js.map +1 -0
  74. package/dist/app/ports/persistence-port.d.ts +21 -0
  75. package/dist/app/ports/persistence-port.js +2 -0
  76. package/dist/app/ports/persistence-port.js.map +1 -0
  77. package/dist/app/ports/secret-port.d.ts +12 -0
  78. package/dist/app/ports/secret-port.js +2 -0
  79. package/dist/app/ports/secret-port.js.map +1 -0
  80. package/dist/app/ports/terminal-port.d.ts +18 -0
  81. package/dist/app/ports/terminal-port.js +2 -0
  82. package/dist/app/ports/terminal-port.js.map +1 -0
  83. package/dist/app/ports/updates-port.d.ts +12 -0
  84. package/dist/app/ports/updates-port.js +2 -0
  85. package/dist/app/ports/updates-port.js.map +1 -0
  86. package/dist/commands/doctor.js +9 -0
  87. package/dist/commands/doctor.js.map +1 -1
  88. package/dist/commands/update.js +1 -1
  89. package/dist/commands/update.js.map +1 -1
  90. package/dist/index.js +9 -6
  91. package/dist/index.js.map +1 -1
  92. package/dist/llm/router.js +4 -2
  93. package/dist/llm/router.js.map +1 -1
  94. package/dist/prompts/index.d.ts +1 -1
  95. package/dist/prompts/index.js +5 -3
  96. package/dist/prompts/index.js.map +1 -1
  97. package/dist/repl/slash-commands.js +1 -1
  98. package/dist/repl/slash-commands.js.map +1 -1
  99. package/dist/repl.js +35 -107
  100. package/dist/repl.js.map +1 -1
  101. package/dist/store/config.js +3 -1
  102. package/dist/store/config.js.map +1 -1
  103. package/dist/store/history.d.ts +26 -2
  104. package/dist/store/history.js +383 -72
  105. package/dist/store/history.js.map +1 -1
  106. package/dist/store/logs.js +3 -3
  107. package/dist/store/logs.js.map +1 -1
  108. package/dist/store/paths.d.ts +6 -0
  109. package/dist/store/paths.js +38 -0
  110. package/dist/store/paths.js.map +1 -0
  111. package/dist/store/plan.js +8 -5
  112. package/dist/store/plan.js.map +1 -1
  113. package/dist/tools/jobs.js +2 -2
  114. package/dist/tools/jobs.js.map +1 -1
  115. package/dist/tools/registry.js +117 -46
  116. package/dist/tools/registry.js.map +1 -1
  117. package/dist/tools/shell.js +2 -2
  118. package/dist/tools/shell.js.map +1 -1
  119. package/dist/tui/state.js +20 -17
  120. package/dist/tui/state.js.map +1 -1
  121. package/dist/tui-v2/actions/action-id.d.ts +14 -0
  122. package/dist/tui-v2/actions/action-id.js +109 -0
  123. package/dist/tui-v2/actions/action-id.js.map +1 -0
  124. package/dist/tui-v2/actions/action-router.d.ts +18 -0
  125. package/dist/tui-v2/actions/action-router.js +62 -0
  126. package/dist/tui-v2/actions/action-router.js.map +1 -0
  127. package/dist/tui-v2/actions/chord-from-key.d.ts +26 -0
  128. package/dist/tui-v2/actions/chord-from-key.js +45 -0
  129. package/dist/tui-v2/actions/chord-from-key.js.map +1 -0
  130. package/dist/tui-v2/actions/keymap.d.ts +24 -0
  131. package/dist/tui-v2/actions/keymap.js +134 -0
  132. package/dist/tui-v2/actions/keymap.js.map +1 -0
  133. package/dist/tui-v2/app/App.d.ts +13 -0
  134. package/dist/tui-v2/app/App.js +320 -0
  135. package/dist/tui-v2/app/App.js.map +1 -0
  136. package/dist/tui-v2/app/command-handlers.d.ts +8 -0
  137. package/dist/tui-v2/app/command-handlers.js +66 -0
  138. package/dist/tui-v2/app/command-handlers.js.map +1 -0
  139. package/dist/tui-v2/app/commands/config-commands.d.ts +14 -0
  140. package/dist/tui-v2/app/commands/config-commands.js +161 -0
  141. package/dist/tui-v2/app/commands/config-commands.js.map +1 -0
  142. package/dist/tui-v2/app/commands/key-commands.d.ts +9 -0
  143. package/dist/tui-v2/app/commands/key-commands.js +263 -0
  144. package/dist/tui-v2/app/commands/key-commands.js.map +1 -0
  145. package/dist/tui-v2/app/commands/picker-commands.d.ts +24 -0
  146. package/dist/tui-v2/app/commands/picker-commands.js +423 -0
  147. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -0
  148. package/dist/tui-v2/app/commands/session-commands.d.ts +19 -0
  149. package/dist/tui-v2/app/commands/session-commands.js +162 -0
  150. package/dist/tui-v2/app/commands/session-commands.js.map +1 -0
  151. package/dist/tui-v2/app/plan-lifecycle.d.ts +16 -0
  152. package/dist/tui-v2/app/plan-lifecycle.js +59 -0
  153. package/dist/tui-v2/app/plan-lifecycle.js.map +1 -0
  154. package/dist/tui-v2/app/providers.d.ts +17 -0
  155. package/dist/tui-v2/app/providers.js +32 -0
  156. package/dist/tui-v2/app/providers.js.map +1 -0
  157. package/dist/tui-v2/bootstrap/capabilities.d.ts +35 -0
  158. package/dist/tui-v2/bootstrap/capabilities.js +93 -0
  159. package/dist/tui-v2/bootstrap/capabilities.js.map +1 -0
  160. package/dist/tui-v2/bootstrap/composition-root.d.ts +85 -0
  161. package/dist/tui-v2/bootstrap/composition-root.js +131 -0
  162. package/dist/tui-v2/bootstrap/composition-root.js.map +1 -0
  163. package/dist/tui-v2/bootstrap/disable-native-selection.d.ts +11 -0
  164. package/dist/tui-v2/bootstrap/disable-native-selection.js +13 -0
  165. package/dist/tui-v2/bootstrap/disable-native-selection.js.map +1 -0
  166. package/dist/tui-v2/bootstrap/lifecycle.d.ts +62 -0
  167. package/dist/tui-v2/bootstrap/lifecycle.js +123 -0
  168. package/dist/tui-v2/bootstrap/lifecycle.js.map +1 -0
  169. package/dist/tui-v2/bootstrap/osc52-clipboard.d.ts +28 -0
  170. package/dist/tui-v2/bootstrap/osc52-clipboard.js +44 -0
  171. package/dist/tui-v2/bootstrap/osc52-clipboard.js.map +1 -0
  172. package/dist/tui-v2/bootstrap/overlay-ports.d.ts +12 -0
  173. package/dist/{tui/confirm.js → tui-v2/bootstrap/overlay-ports.js} +17 -25
  174. package/dist/tui-v2/bootstrap/overlay-ports.js.map +1 -0
  175. package/dist/tui-v2/bootstrap/pager-export.d.ts +24 -0
  176. package/dist/tui-v2/bootstrap/pager-export.js +87 -0
  177. package/dist/tui-v2/bootstrap/pager-export.js.map +1 -0
  178. package/dist/tui-v2/bootstrap/start-tui-v2.d.ts +15 -0
  179. package/dist/tui-v2/bootstrap/start-tui-v2.js +95 -0
  180. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -0
  181. package/dist/tui-v2/bootstrap/ui-selection.d.ts +30 -0
  182. package/dist/tui-v2/bootstrap/ui-selection.js +59 -0
  183. package/dist/tui-v2/bootstrap/ui-selection.js.map +1 -0
  184. package/dist/tui-v2/components/completion/completion-menu.d.ts +23 -0
  185. package/dist/tui-v2/components/completion/completion-menu.js +84 -0
  186. package/dist/tui-v2/components/completion/completion-menu.js.map +1 -0
  187. package/dist/tui-v2/components/jobs/jobs-panel.d.ts +16 -0
  188. package/dist/tui-v2/components/jobs/jobs-panel.js +92 -0
  189. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -0
  190. package/dist/tui-v2/components/modal/confirm-modal.d.ts +21 -0
  191. package/dist/tui-v2/components/modal/confirm-modal.js +71 -0
  192. package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -0
  193. package/dist/tui-v2/components/modal/prompt-actions-modal.d.ts +15 -0
  194. package/dist/tui-v2/components/modal/prompt-actions-modal.js +80 -0
  195. package/dist/tui-v2/components/modal/prompt-actions-modal.js.map +1 -0
  196. package/dist/tui-v2/components/modal/secret-modal.d.ts +24 -0
  197. package/dist/tui-v2/components/modal/secret-modal.js +50 -0
  198. package/dist/tui-v2/components/modal/secret-modal.js.map +1 -0
  199. package/dist/tui-v2/components/overlay/overlay-host.d.ts +17 -0
  200. package/dist/tui-v2/components/overlay/overlay-host.js +28 -0
  201. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -0
  202. package/dist/tui-v2/components/pager/pager.d.ts +20 -0
  203. package/dist/tui-v2/components/pager/pager.js +396 -0
  204. package/dist/tui-v2/components/pager/pager.js.map +1 -0
  205. package/dist/tui-v2/components/picker/picker.d.ts +17 -0
  206. package/dist/tui-v2/components/picker/picker.js +222 -0
  207. package/dist/tui-v2/components/picker/picker.js.map +1 -0
  208. package/dist/tui-v2/components/plan/plan-view.d.ts +20 -0
  209. package/dist/tui-v2/components/plan/plan-view.js +116 -0
  210. package/dist/tui-v2/components/plan/plan-view.js.map +1 -0
  211. package/dist/tui-v2/components/queue/queue-panel.d.ts +23 -0
  212. package/dist/tui-v2/components/queue/queue-panel.js +58 -0
  213. package/dist/tui-v2/components/queue/queue-panel.js.map +1 -0
  214. package/dist/tui-v2/components/status/status-line.d.ts +27 -0
  215. package/dist/tui-v2/components/status/status-line.js +158 -0
  216. package/dist/tui-v2/components/status/status-line.js.map +1 -0
  217. package/dist/tui-v2/components/toast/toast-host.d.ts +15 -0
  218. package/dist/tui-v2/components/toast/toast-host.js +48 -0
  219. package/dist/tui-v2/components/toast/toast-host.js.map +1 -0
  220. package/dist/tui-v2/components/transcript/assistant-message.d.ts +20 -0
  221. package/dist/tui-v2/components/transcript/assistant-message.js +35 -0
  222. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -0
  223. package/dist/tui-v2/components/transcript/compacted-row.d.ts +19 -0
  224. package/dist/tui-v2/components/transcript/compacted-row.js +94 -0
  225. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -0
  226. package/dist/tui-v2/components/transcript/intro-card.d.ts +19 -0
  227. package/dist/tui-v2/components/transcript/intro-card.js +50 -0
  228. package/dist/tui-v2/components/transcript/intro-card.js.map +1 -0
  229. package/dist/tui-v2/components/transcript/linkable-text.d.ts +21 -0
  230. package/dist/tui-v2/components/transcript/linkable-text.js +46 -0
  231. package/dist/tui-v2/components/transcript/linkable-text.js.map +1 -0
  232. package/dist/tui-v2/components/transcript/notice-row.d.ts +14 -0
  233. package/dist/tui-v2/components/transcript/notice-row.js +40 -0
  234. package/dist/tui-v2/components/transcript/notice-row.js.map +1 -0
  235. package/dist/tui-v2/components/transcript/search-bar.d.ts +19 -0
  236. package/dist/tui-v2/components/transcript/search-bar.js +7 -0
  237. package/dist/tui-v2/components/transcript/search-bar.js.map +1 -0
  238. package/dist/tui-v2/components/transcript/thinking-block.d.ts +22 -0
  239. package/dist/tui-v2/components/transcript/thinking-block.js +27 -0
  240. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -0
  241. package/dist/tui-v2/components/transcript/tool-card.d.ts +26 -0
  242. package/dist/tui-v2/components/transcript/tool-card.js +175 -0
  243. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -0
  244. package/dist/tui-v2/components/transcript/transcript-row.d.ts +25 -0
  245. package/dist/tui-v2/components/transcript/transcript-row.js +30 -0
  246. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -0
  247. package/dist/tui-v2/components/transcript/transcript-scroll-port.d.ts +52 -0
  248. package/dist/tui-v2/components/transcript/transcript-scroll-port.js +86 -0
  249. package/dist/tui-v2/components/transcript/transcript-scroll-port.js.map +1 -0
  250. package/dist/tui-v2/components/transcript/transcript-view.d.ts +24 -0
  251. package/dist/tui-v2/components/transcript/transcript-view.js +403 -0
  252. package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -0
  253. package/dist/tui-v2/components/transcript/use-click-without-drag.d.ts +13 -0
  254. package/dist/tui-v2/components/transcript/use-click-without-drag.js +35 -0
  255. package/dist/tui-v2/components/transcript/use-click-without-drag.js.map +1 -0
  256. package/dist/tui-v2/components/transcript/use-native-selection-copy.d.ts +10 -0
  257. package/dist/tui-v2/components/transcript/use-native-selection-copy.js +37 -0
  258. package/dist/tui-v2/components/transcript/use-native-selection-copy.js.map +1 -0
  259. package/dist/tui-v2/components/transcript/use-transcript-selection.d.ts +23 -0
  260. package/dist/tui-v2/components/transcript/use-transcript-selection.js +136 -0
  261. package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -0
  262. package/dist/tui-v2/components/transcript/user-message.d.ts +10 -0
  263. package/dist/tui-v2/components/transcript/user-message.js +23 -0
  264. package/dist/tui-v2/components/transcript/user-message.js.map +1 -0
  265. package/dist/tui-v2/composer/arrow-intent.d.ts +23 -0
  266. package/dist/tui-v2/composer/arrow-intent.js +35 -0
  267. package/dist/tui-v2/composer/arrow-intent.js.map +1 -0
  268. package/dist/tui-v2/composer/completion.d.ts +37 -0
  269. package/dist/tui-v2/composer/completion.js +67 -0
  270. package/dist/tui-v2/composer/completion.js.map +1 -0
  271. package/dist/tui-v2/composer/composer-editor.d.ts +33 -0
  272. package/dist/tui-v2/composer/composer-editor.js +620 -0
  273. package/dist/tui-v2/composer/composer-editor.js.map +1 -0
  274. package/dist/tui-v2/composer/composer-height.d.ts +21 -0
  275. package/dist/tui-v2/composer/composer-height.js +29 -0
  276. package/dist/tui-v2/composer/composer-height.js.map +1 -0
  277. package/dist/tui-v2/composer/composer-meta.d.ts +9 -0
  278. package/dist/tui-v2/composer/composer-meta.js +38 -0
  279. package/dist/tui-v2/composer/composer-meta.js.map +1 -0
  280. package/dist/tui-v2/composer/history-nav.d.ts +15 -0
  281. package/dist/tui-v2/composer/history-nav.js +16 -0
  282. package/dist/tui-v2/composer/history-nav.js.map +1 -0
  283. package/dist/tui-v2/composer/newline-hint.d.ts +13 -0
  284. package/dist/tui-v2/composer/newline-hint.js +13 -0
  285. package/dist/tui-v2/composer/newline-hint.js.map +1 -0
  286. package/dist/tui-v2/composer/paste-placeholder.d.ts +35 -0
  287. package/dist/tui-v2/composer/paste-placeholder.js +55 -0
  288. package/dist/tui-v2/composer/paste-placeholder.js.map +1 -0
  289. package/dist/tui-v2/composer/prompt-history.d.ts +22 -0
  290. package/dist/tui-v2/composer/prompt-history.js +51 -0
  291. package/dist/tui-v2/composer/prompt-history.js.map +1 -0
  292. package/dist/tui-v2/composer/secret-buffer.d.ts +20 -0
  293. package/dist/tui-v2/composer/secret-buffer.js +42 -0
  294. package/dist/tui-v2/composer/secret-buffer.js.map +1 -0
  295. package/dist/tui-v2/composer/textarea-keybindings.d.ts +22 -0
  296. package/dist/tui-v2/composer/textarea-keybindings.js +27 -0
  297. package/dist/tui-v2/composer/textarea-keybindings.js.map +1 -0
  298. package/dist/tui-v2/controllers/focus-controller.d.ts +30 -0
  299. package/dist/tui-v2/controllers/focus-controller.js +80 -0
  300. package/dist/tui-v2/controllers/focus-controller.js.map +1 -0
  301. package/dist/tui-v2/controllers/overlay-controller.d.ts +99 -0
  302. package/dist/tui-v2/controllers/overlay-controller.js +195 -0
  303. package/dist/tui-v2/controllers/overlay-controller.js.map +1 -0
  304. package/dist/tui-v2/controllers/selection-controller.d.ts +76 -0
  305. package/dist/tui-v2/controllers/selection-controller.js +238 -0
  306. package/dist/tui-v2/controllers/selection-controller.js.map +1 -0
  307. package/dist/tui-v2/controllers/toast-controller.d.ts +35 -0
  308. package/dist/tui-v2/controllers/toast-controller.js +88 -0
  309. package/dist/tui-v2/controllers/toast-controller.js.map +1 -0
  310. package/dist/tui-v2/layout/compute-layout.d.ts +59 -0
  311. package/dist/tui-v2/layout/compute-layout.js +170 -0
  312. package/dist/tui-v2/layout/compute-layout.js.map +1 -0
  313. package/dist/tui-v2/rendering/ansi-to-styled.d.ts +17 -0
  314. package/dist/tui-v2/rendering/ansi-to-styled.js +217 -0
  315. package/dist/tui-v2/rendering/ansi-to-styled.js.map +1 -0
  316. package/dist/tui-v2/rendering/batch-sections.d.ts +45 -0
  317. package/dist/tui-v2/rendering/batch-sections.js +109 -0
  318. package/dist/tui-v2/rendering/batch-sections.js.map +1 -0
  319. package/dist/tui-v2/rendering/link-detector.d.ts +16 -0
  320. package/dist/tui-v2/rendering/link-detector.js +43 -0
  321. package/dist/tui-v2/rendering/link-detector.js.map +1 -0
  322. package/dist/tui-v2/rendering/open-tool-output.d.ts +28 -0
  323. package/dist/tui-v2/rendering/open-tool-output.js +138 -0
  324. package/dist/tui-v2/rendering/open-tool-output.js.map +1 -0
  325. package/dist/tui-v2/rendering/picker-filter.d.ts +22 -0
  326. package/dist/tui-v2/rendering/picker-filter.js +68 -0
  327. package/dist/tui-v2/rendering/picker-filter.js.map +1 -0
  328. package/dist/tui-v2/rendering/plan-view.d.ts +32 -0
  329. package/dist/tui-v2/rendering/plan-view.js +196 -0
  330. package/dist/tui-v2/rendering/plan-view.js.map +1 -0
  331. package/dist/tui-v2/rendering/render-markdown-lines.d.ts +29 -0
  332. package/dist/tui-v2/rendering/render-markdown-lines.js +46 -0
  333. package/dist/tui-v2/rendering/render-markdown-lines.js.map +1 -0
  334. package/dist/tui-v2/rendering/sanitize-display.d.ts +13 -0
  335. package/dist/tui-v2/rendering/sanitize-display.js +24 -0
  336. package/dist/tui-v2/rendering/sanitize-display.js.map +1 -0
  337. package/dist/tui-v2/rendering/theme.d.ts +67 -0
  338. package/dist/tui-v2/rendering/theme.js +74 -0
  339. package/dist/tui-v2/rendering/theme.js.map +1 -0
  340. package/dist/tui-v2/rendering/tool-presenter.d.ts +39 -0
  341. package/dist/tui-v2/rendering/tool-presenter.js +207 -0
  342. package/dist/tui-v2/rendering/tool-presenter.js.map +1 -0
  343. package/dist/tui-v2/rendering/transcript-export.d.ts +11 -0
  344. package/dist/tui-v2/rendering/transcript-export.js +18 -0
  345. package/dist/tui-v2/rendering/transcript-export.js.map +1 -0
  346. package/dist/tui-v2/rendering/transcript-semantic.d.ts +17 -0
  347. package/dist/tui-v2/rendering/transcript-semantic.js +67 -0
  348. package/dist/tui-v2/rendering/transcript-semantic.js.map +1 -0
  349. package/dist/tui-v2/state/pager-search.d.ts +22 -0
  350. package/dist/tui-v2/state/pager-search.js +78 -0
  351. package/dist/tui-v2/state/pager-search.js.map +1 -0
  352. package/dist/tui-v2/state/semantic-document.d.ts +34 -0
  353. package/dist/tui-v2/state/semantic-document.js +201 -0
  354. package/dist/tui-v2/state/semantic-document.js.map +1 -0
  355. package/dist/tui-v2/state/transcript-compaction.d.ts +24 -0
  356. package/dist/tui-v2/state/transcript-compaction.js +79 -0
  357. package/dist/tui-v2/state/transcript-compaction.js.map +1 -0
  358. package/dist/tui-v2/state/transcript-hydrate.d.ts +27 -0
  359. package/dist/tui-v2/state/transcript-hydrate.js +272 -0
  360. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -0
  361. package/dist/tui-v2/state/transcript-reducer.d.ts +10 -0
  362. package/dist/tui-v2/state/transcript-reducer.js +281 -0
  363. package/dist/tui-v2/state/transcript-reducer.js.map +1 -0
  364. package/dist/tui-v2/state/transcript-search.d.ts +17 -0
  365. package/dist/tui-v2/state/transcript-search.js +38 -0
  366. package/dist/tui-v2/state/transcript-search.js.map +1 -0
  367. package/dist/tui-v2/state/transcript-store.d.ts +32 -0
  368. package/dist/tui-v2/state/transcript-store.js +66 -0
  369. package/dist/tui-v2/state/transcript-store.js.map +1 -0
  370. package/dist/tui-v2/state/transcript-struct.d.ts +15 -0
  371. package/dist/tui-v2/state/transcript-struct.js +133 -0
  372. package/dist/tui-v2/state/transcript-struct.js.map +1 -0
  373. package/dist/tui-v2/state/transcript-types.d.ts +76 -0
  374. package/dist/tui-v2/state/transcript-types.js +65 -0
  375. package/dist/tui-v2/state/transcript-types.js.map +1 -0
  376. package/dist/tui-v2/state/use-overlay.d.ts +3 -0
  377. package/dist/tui-v2/state/use-overlay.js +6 -0
  378. package/dist/tui-v2/state/use-overlay.js.map +1 -0
  379. package/dist/tui-v2/state/use-plan.d.ts +4 -0
  380. package/dist/tui-v2/state/use-plan.js +6 -0
  381. package/dist/tui-v2/state/use-plan.js.map +1 -0
  382. package/dist/tui-v2/state/use-session-state.d.ts +2 -0
  383. package/dist/tui-v2/state/use-session-state.js +7 -0
  384. package/dist/tui-v2/state/use-session-state.js.map +1 -0
  385. package/dist/tui-v2/state/use-toast.d.ts +3 -0
  386. package/dist/tui-v2/state/use-toast.js +15 -0
  387. package/dist/tui-v2/state/use-toast.js.map +1 -0
  388. package/dist/tui-v2/state/use-transcript-store.d.ts +10 -0
  389. package/dist/tui-v2/state/use-transcript-store.js +12 -0
  390. package/dist/tui-v2/state/use-transcript-store.js.map +1 -0
  391. package/dist/ui/intro-card.js +3 -3
  392. package/dist/ui/intro-card.js.map +1 -1
  393. package/dist/ui/intro-header.d.ts +23 -0
  394. package/dist/ui/intro-header.js +232 -0
  395. package/dist/ui/intro-header.js.map +1 -0
  396. package/dist/ui/markdown.js +0 -1
  397. package/dist/ui/markdown.js.map +1 -1
  398. package/dist/ui/mentions.d.ts +9 -2
  399. package/dist/ui/mentions.js +102 -4
  400. package/dist/ui/mentions.js.map +1 -1
  401. package/dist/ui/output-pane.js +1 -1
  402. package/dist/ui/output-pane.js.map +1 -1
  403. package/dist/ui/plan-pane.d.ts +8 -0
  404. package/dist/ui/plan-pane.js +38 -0
  405. package/dist/ui/plan-pane.js.map +1 -1
  406. package/dist/ui/wordmark.d.ts +5 -0
  407. package/dist/ui/wordmark.js +6 -1
  408. package/dist/ui/wordmark.js.map +1 -1
  409. package/package.json +14 -10
  410. package/dist/tui/App.d.ts +0 -12
  411. package/dist/tui/App.js +0 -2174
  412. package/dist/tui/App.js.map +0 -1
  413. package/dist/tui/components/ConfirmModal.d.ts +0 -9
  414. package/dist/tui/components/ConfirmModal.js +0 -55
  415. package/dist/tui/components/ConfirmModal.js.map +0 -1
  416. package/dist/tui/components/JobsPanel.d.ts +0 -11
  417. package/dist/tui/components/JobsPanel.js +0 -55
  418. package/dist/tui/components/JobsPanel.js.map +0 -1
  419. package/dist/tui/components/Pager.d.ts +0 -14
  420. package/dist/tui/components/Pager.js +0 -56
  421. package/dist/tui/components/Pager.js.map +0 -1
  422. package/dist/tui/components/PickerPanel.d.ts +0 -15
  423. package/dist/tui/components/PickerPanel.js +0 -126
  424. package/dist/tui/components/PickerPanel.js.map +0 -1
  425. package/dist/tui/components/PlanSidebar.d.ts +0 -6
  426. package/dist/tui/components/PlanSidebar.js +0 -99
  427. package/dist/tui/components/PlanSidebar.js.map +0 -1
  428. package/dist/tui/components/SecretInputPanel.d.ts +0 -7
  429. package/dist/tui/components/SecretInputPanel.js +0 -38
  430. package/dist/tui/components/SecretInputPanel.js.map +0 -1
  431. package/dist/tui/confirm.d.ts +0 -16
  432. package/dist/tui/confirm.js.map +0 -1
  433. package/dist/tui/default.d.ts +0 -6
  434. package/dist/tui/default.js +0 -11
  435. package/dist/tui/default.js.map +0 -1
  436. package/dist/tui/hooks/useAgentRunner.d.ts +0 -73
  437. package/dist/tui/hooks/useAgentRunner.js +0 -277
  438. package/dist/tui/hooks/useAgentRunner.js.map +0 -1
  439. package/dist/tui/hooks/useJobs.d.ts +0 -7
  440. package/dist/tui/hooks/useJobs.js +0 -39
  441. package/dist/tui/hooks/useJobs.js.map +0 -1
  442. package/dist/tui/hooks/useSpinner.d.ts +0 -5
  443. package/dist/tui/hooks/useSpinner.js +0 -21
  444. package/dist/tui/hooks/useSpinner.js.map +0 -1
  445. package/dist/tui/hooks/useTerminalSize.d.ts +0 -9
  446. package/dist/tui/hooks/useTerminalSize.js +0 -26
  447. package/dist/tui/hooks/useTerminalSize.js.map +0 -1
  448. package/dist/tui/index.d.ts +0 -10
  449. package/dist/tui/index.js +0 -37
  450. package/dist/tui/index.js.map +0 -1
  451. package/dist/tui/key-intent.d.ts +0 -36
  452. package/dist/tui/key-intent.js +0 -54
  453. package/dist/tui/key-intent.js.map +0 -1
  454. package/dist/tui/mouse.d.ts +0 -8
  455. package/dist/tui/mouse.js +0 -34
  456. package/dist/tui/mouse.js.map +0 -1
  457. package/dist/tui/plan-sidebar-lines.d.ts +0 -2
  458. package/dist/tui/plan-sidebar-lines.js +0 -140
  459. package/dist/tui/plan-sidebar-lines.js.map +0 -1
  460. package/dist/tui/render-lines.d.ts +0 -22
  461. package/dist/tui/render-lines.js +0 -611
  462. package/dist/tui/render-lines.js.map +0 -1
@@ -23,6 +23,27 @@ export interface ToolCallRecord {
23
23
  exitCode?: number | undefined;
24
24
  output: string;
25
25
  }
26
+ /** Keep the newest version of each session id. */
27
+ export declare function dedupeHistoryById(records: readonly HistoryRecord[]): HistoryRecord[];
28
+ export declare function sortHistoryByUpdatedDesc(records: readonly HistoryRecord[]): HistoryRecord[];
29
+ /**
30
+ * Apply retention by *recency* (not file order). Pruned sessions are returned
31
+ * separately so callers can archive them — never silently destroy history.
32
+ * limit <= 0 means unlimited (keep everything).
33
+ */
34
+ export declare function partitionByRetention(records: readonly HistoryRecord[], limit: number): {
35
+ kept: HistoryRecord[];
36
+ pruned: HistoryRecord[];
37
+ };
38
+ /**
39
+ * Scan leftover write temps + the archive for sessions missing from the
40
+ * active file and merge them back. Fixes history that was pruned by the old
41
+ * slice(-200) retention or left in .tmp after a crashed rename.
42
+ */
43
+ export declare function recoverOrphanedHistory(): Promise<{
44
+ recovered: number;
45
+ sources: string[];
46
+ }>;
26
47
  export declare function saveSession(messages: ChatMessage[], name?: string | undefined, transcript?: TranscriptItem[] | undefined): Promise<HistoryRecord>;
27
48
  export declare function upsertSession(id: string, messages: ChatMessage[], name?: string | undefined, transcript?: TranscriptItem[] | undefined): Promise<HistoryRecord>;
28
49
  export declare function saveToolCall(sessionId: string, call: ToolCall, result: ToolResult): Promise<ToolCallRecord>;
@@ -30,11 +51,14 @@ export declare function listSessions(limit?: number): Promise<HistoryRecord[]>;
30
51
  export declare function getSession(sessionId: string): Promise<HistoryRecord | undefined>;
31
52
  export declare function getHistoryPath(): string;
32
53
  /**
33
- * Clear all stored history. Returns whether the operation succeeded.
34
- * Used by the /privacy slash command and `clai privacy clear-history`.
54
+ * Clear active history after archiving a full snapshot. Never unrecoverably
55
+ * destroys chats a timestamped copy is written under history-backups/ and
56
+ * the previous active file is moved into history-archive.jsonl.
35
57
  */
36
58
  export declare function clearAllHistory(): Promise<{
37
59
  cleared: boolean;
38
60
  detail: string;
39
61
  }>;
40
62
  export declare function getJsonlHistoryPath(): string;
63
+ export declare function getHistoryArchivePath(): string;
64
+ export declare function getHistoryBackupDir(): string;
@@ -1,13 +1,30 @@
1
- import { mkdir, readFile, rm, writeFile, rename } from "node:fs/promises";
1
+ import { appendFile, copyFile, mkdir, readdir, readFile, rm, writeFile, rename, } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { homedir } from "node:os";
4
3
  import { redactSecrets } from "../llm/provider.js";
5
4
  import { getConfig } from "./config.js";
6
5
  import { safeCwd } from "../os/cwd.js";
7
6
  import { fixOwner, fixOwnerSync, handlePermissionError, safeExists } from "../os/permissions.js";
8
- const historyDir = join(homedir(), ".clai");
9
- const dbFile = join(historyDir, "history.db");
10
- const jsonlFile = join(historyDir, "history.jsonl");
7
+ import { getHistoryDir } from "./paths.js";
8
+ /** Live paths so CLAI_DATA_DIR / CLAI_HISTORY_DIR always apply (and tests work). */
9
+ function historyDirPath() {
10
+ return getHistoryDir();
11
+ }
12
+ function dbFilePath() {
13
+ return join(historyDirPath(), "history.db");
14
+ }
15
+ function jsonlFilePath() {
16
+ return join(historyDirPath(), "history.jsonl");
17
+ }
18
+ /** Sessions pruned by retention land here — never hard-deleted on autosave. */
19
+ function archiveFilePath() {
20
+ return join(historyDirPath(), "history-archive.jsonl");
21
+ }
22
+ /** Rolling pre-write snapshots of the active history file. */
23
+ function backupDirPath() {
24
+ return join(historyDirPath(), "history-backups");
25
+ }
26
+ /** Max rolling backups kept under history-backups/. */
27
+ const MAX_HISTORY_BACKUPS = 12;
11
28
  // We keep this string here (not as a literal) so the bundler doesn't try
12
29
  // to statically resolve a module that may not be installed. If a user has
13
30
  // `better-sqlite3` available (eg they explicitly added it for a richer
@@ -25,12 +42,12 @@ async function loadDatabase() {
25
42
  if (sqliteUnavailable)
26
43
  return undefined;
27
44
  try {
28
- await mkdir(historyDir, { recursive: true });
29
- await fixOwner(historyDir);
45
+ await mkdir(historyDirPath(), { recursive: true });
46
+ await fixOwner(historyDirPath());
30
47
  const imported = (await import(sqliteModuleName));
31
48
  const Ctor = imported.default ?? imported;
32
- cachedDb = new Ctor(dbFile);
33
- fixOwnerSync(dbFile);
49
+ cachedDb = new Ctor(dbFilePath());
50
+ fixOwnerSync(dbFilePath());
34
51
  cachedDb.exec(`
35
52
  CREATE TABLE IF NOT EXISTS sessions (
36
53
  id TEXT PRIMARY KEY,
@@ -123,10 +140,13 @@ async function appendJsonl(record) {
123
140
  * file and then persist back only its own record, wiping every other session.
124
141
  */
125
142
  let jsonlWriteChain = Promise.resolve();
143
+ /** Ensures orphan .tmp / archive recovery runs at most once per process. */
144
+ let recoveryAttempted = false;
126
145
  function mutateJsonl(update) {
127
146
  const run = jsonlWriteChain.then(async () => {
128
147
  try {
129
- const current = await readJsonlRecords();
148
+ await ensureHistoryRecovered();
149
+ const current = await readJsonlRecordsFrom(jsonlFilePath());
130
150
  const next = update(current);
131
151
  await writeJsonlAtomic(next);
132
152
  }
@@ -138,49 +158,276 @@ function mutateJsonl(update) {
138
158
  jsonlWriteChain = run.then(() => undefined, () => undefined);
139
159
  return run;
140
160
  }
141
- /** Read and parse all valid JSONL records, silently skipping malformed lines. */
142
- async function readJsonlRecords() {
143
- if (!(await safeExists(jsonlFile)))
161
+ function updatedAtMs(record) {
162
+ const t = Date.parse(record.updatedAt || record.createdAt || "");
163
+ return Number.isFinite(t) ? t : 0;
164
+ }
165
+ /** Keep the newest version of each session id. */
166
+ export function dedupeHistoryById(records) {
167
+ const byId = new Map();
168
+ for (const record of records) {
169
+ if (!record?.id)
170
+ continue;
171
+ const prev = byId.get(record.id);
172
+ if (!prev || updatedAtMs(record) >= updatedAtMs(prev)) {
173
+ byId.set(record.id, record);
174
+ }
175
+ }
176
+ return [...byId.values()];
177
+ }
178
+ export function sortHistoryByUpdatedDesc(records) {
179
+ return [...records].sort((a, b) => updatedAtMs(b) - updatedAtMs(a));
180
+ }
181
+ /**
182
+ * Apply retention by *recency* (not file order). Pruned sessions are returned
183
+ * separately so callers can archive them — never silently destroy history.
184
+ * limit <= 0 means unlimited (keep everything).
185
+ */
186
+ export function partitionByRetention(records, limit) {
187
+ const unique = sortHistoryByUpdatedDesc(dedupeHistoryById(records));
188
+ if (!limit || limit <= 0 || unique.length <= limit) {
189
+ return { kept: unique, pruned: [] };
190
+ }
191
+ return {
192
+ kept: unique.slice(0, limit),
193
+ pruned: unique.slice(limit),
194
+ };
195
+ }
196
+ /** Read and parse all valid JSONL records from any path. */
197
+ async function readJsonlRecordsFrom(path) {
198
+ if (!(await safeExists(path)))
144
199
  return [];
145
- const raw = await readFile(jsonlFile, "utf8");
146
- return raw
147
- .split("\n")
148
- .filter(Boolean)
149
- .map((line) => {
150
- try {
151
- return JSON.parse(line);
200
+ try {
201
+ const raw = await readFile(path, "utf8");
202
+ return raw
203
+ .split("\n")
204
+ .filter(Boolean)
205
+ .map((line) => {
206
+ try {
207
+ return JSON.parse(line);
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ })
213
+ .filter((record) => record !== null);
214
+ }
215
+ catch (err) {
216
+ if (err && err.code === "EACCES")
217
+ handlePermissionError(err);
218
+ return [];
219
+ }
220
+ }
221
+ async function appendRecordsToFile(path, records) {
222
+ if (records.length === 0)
223
+ return;
224
+ await mkdir(historyDirPath(), { recursive: true });
225
+ await fixOwner(historyDirPath());
226
+ const chunk = `${records.map((r) => JSON.stringify(r)).join("\n")}\n`;
227
+ // Append is best-effort durable; archive growth is unbounded by design so
228
+ // retention never permanently loses a conversation.
229
+ await appendFile(path, chunk, { mode: 0o600 });
230
+ await fixOwner(path).catch(() => undefined);
231
+ }
232
+ async function backupActiveHistory() {
233
+ if (!(await safeExists(jsonlFilePath())))
234
+ return;
235
+ try {
236
+ await mkdir(backupDirPath(), { recursive: true });
237
+ await fixOwner(backupDirPath());
238
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
239
+ const dest = join(backupDirPath(), `history-${stamp}.jsonl`);
240
+ await copyFile(jsonlFilePath(), dest);
241
+ await fixOwner(dest).catch(() => undefined);
242
+ // Keep only the newest N backups.
243
+ const names = (await readdir(backupDirPath()))
244
+ .filter((n) => n.startsWith("history-") && n.endsWith(".jsonl"))
245
+ .sort()
246
+ .reverse();
247
+ for (const old of names.slice(MAX_HISTORY_BACKUPS)) {
248
+ await rm(join(backupDirPath(), old), { force: true }).catch(() => undefined);
152
249
  }
153
- catch {
154
- return null;
250
+ }
251
+ catch {
252
+ // Backup is best-effort; never block the autosave path.
253
+ }
254
+ }
255
+ /**
256
+ * Scan leftover write temps + the archive for sessions missing from the
257
+ * active file and merge them back. Fixes history that was pruned by the old
258
+ * slice(-200) retention or left in .tmp after a crashed rename.
259
+ */
260
+ export async function recoverOrphanedHistory() {
261
+ const sources = [];
262
+ const extras = [];
263
+ try {
264
+ const names = await readdir(historyDirPath());
265
+ for (const name of names) {
266
+ // Live write temps: history.jsonl.<pid>.<stamp>.tmp
267
+ if (name.startsWith("history.jsonl.") &&
268
+ name.endsWith(".tmp")) {
269
+ const path = join(historyDirPath(), name);
270
+ const rows = await readJsonlRecordsFrom(path);
271
+ if (rows.length === 0) {
272
+ // Empty crash leftovers — safe to remove.
273
+ await rm(path, { force: true }).catch(() => undefined);
274
+ continue;
275
+ }
276
+ extras.push(...rows);
277
+ sources.push(name);
278
+ }
279
+ }
280
+ }
281
+ catch {
282
+ /* dir may not exist yet */
283
+ }
284
+ // Also fold in archive (sessions previously pruned).
285
+ if (await safeExists(archiveFilePath())) {
286
+ const archived = await readJsonlRecordsFrom(archiveFilePath());
287
+ if (archived.length > 0) {
288
+ extras.push(...archived);
289
+ sources.push("history-archive.jsonl");
290
+ }
291
+ }
292
+ // Rolling backups (last-resort recovery of wiped active files).
293
+ try {
294
+ if (await safeExists(backupDirPath())) {
295
+ const backups = (await readdir(backupDirPath()))
296
+ .filter((n) => n.startsWith("history-") && n.endsWith(".jsonl"))
297
+ .sort()
298
+ .reverse()
299
+ .slice(0, 3);
300
+ for (const name of backups) {
301
+ const rows = await readJsonlRecordsFrom(join(backupDirPath(), name));
302
+ if (rows.length > 0) {
303
+ extras.push(...rows);
304
+ sources.push(`history-backups/${name}`);
305
+ }
306
+ }
155
307
  }
156
- })
157
- .filter((record) => record !== null);
308
+ }
309
+ catch {
310
+ /* ignore */
311
+ }
312
+ if (extras.length === 0)
313
+ return { recovered: 0, sources: [] };
314
+ const active = await readJsonlRecordsFrom(jsonlFilePath());
315
+ const beforeIds = new Set(active.map((r) => r.id));
316
+ const merged = dedupeHistoryById([...active, ...extras]);
317
+ const newCount = merged.filter((r) => !beforeIds.has(r.id)).length;
318
+ if (newCount === 0 && merged.length <= active.length) {
319
+ return { recovered: 0, sources };
320
+ }
321
+ // Write WITHOUT applying retention so recovery cannot re-prune.
322
+ await mkdir(historyDirPath(), { recursive: true });
323
+ await fixOwner(historyDirPath());
324
+ if (await safeExists(jsonlFilePath()))
325
+ await backupActiveHistory();
326
+ const sorted = sortHistoryByUpdatedDesc(merged);
327
+ // Stable chronological file order (oldest first) for append-friendly diffs.
328
+ sorted.reverse();
329
+ const body = sorted.length
330
+ ? `${sorted.map((item) => JSON.stringify(item)).join("\n")}\n`
331
+ : "";
332
+ const tmpFile = `${jsonlFilePath()}.recover.${process.pid}.${Date.now().toString(36)}.tmp`;
333
+ await writeFile(tmpFile, body, { mode: 0o600 });
334
+ try {
335
+ await rename(tmpFile, jsonlFilePath());
336
+ }
337
+ catch (err) {
338
+ await rm(tmpFile, { force: true }).catch(() => undefined);
339
+ throw err;
340
+ }
341
+ await fixOwner(jsonlFilePath());
342
+ // Successful recovery: drop non-empty orphan temps we already merged.
343
+ for (const name of sources) {
344
+ if (name.startsWith("history.jsonl.") && name.endsWith(".tmp")) {
345
+ await rm(join(historyDirPath(), name), { force: true }).catch(() => undefined);
346
+ }
347
+ }
348
+ return { recovered: newCount, sources };
349
+ }
350
+ async function ensureHistoryRecovered() {
351
+ if (recoveryAttempted)
352
+ return;
353
+ recoveryAttempted = true;
354
+ try {
355
+ await recoverOrphanedHistory();
356
+ }
357
+ catch {
358
+ // Never block history reads/writes on recovery failure.
359
+ }
158
360
  }
159
361
  /**
160
- * Apply retention and write the whole file atomically: write a temp file and
161
- * rename it over the target. `rename` is atomic on the same filesystem, so a
162
- * concurrent reader sees either the old file or the new one — never a partial.
362
+ * Apply retention (archive pruned sessions) and write the active file
363
+ * atomically. Never hard-deletes pruned chats they go to history-archive.jsonl.
163
364
  */
164
365
  async function writeJsonlAtomic(records) {
165
- await mkdir(historyDir, { recursive: true });
166
- await fixOwner(historyDir);
366
+ await mkdir(historyDirPath(), { recursive: true });
367
+ await fixOwner(historyDirPath());
167
368
  const limit = getConfig().historyRetentionLimit;
168
- const kept = limit && limit > 0 ? records.slice(-limit) : records;
169
- const body = kept.length
170
- ? `${kept.map((item) => JSON.stringify(item)).join("\n")}\n`
369
+ const { kept, pruned } = partitionByRetention(records, limit);
370
+ if (pruned.length > 0) {
371
+ // Durable archive before the active file shrinks.
372
+ await appendRecordsToFile(archiveFilePath(), pruned);
373
+ }
374
+ // If we would shrink (or replace) the on-disk set, snapshot first.
375
+ if (await safeExists(jsonlFilePath())) {
376
+ const existing = await readJsonlRecordsFrom(jsonlFilePath());
377
+ if (kept.length < existing.length || pruned.length > 0) {
378
+ await backupActiveHistory();
379
+ }
380
+ }
381
+ // Refuse to write an empty file over a non-empty one unless the caller
382
+ // intentionally has zero records (e.g. clear after archiving).
383
+ if (kept.length === 0 && (await safeExists(jsonlFilePath()))) {
384
+ const existing = await readJsonlRecordsFrom(jsonlFilePath());
385
+ if (existing.length > 0 && records.length > 0) {
386
+ // Something went wrong in partitioning — keep the safer set.
387
+ const safe = sortHistoryByUpdatedDesc(dedupeHistoryById(existing));
388
+ safe.reverse();
389
+ const body = `${safe.map((item) => JSON.stringify(item)).join("\n")}\n`;
390
+ const tmpFile = `${jsonlFilePath()}.${process.pid}.${Date.now().toString(36)}.tmp`;
391
+ await writeFile(tmpFile, body, { mode: 0o600 });
392
+ try {
393
+ await rename(tmpFile, jsonlFilePath());
394
+ }
395
+ catch (err) {
396
+ await rm(tmpFile, { force: true }).catch(() => undefined);
397
+ throw err;
398
+ }
399
+ await fixOwner(jsonlFilePath());
400
+ return;
401
+ }
402
+ }
403
+ // File order: oldest → newest (matches classic append style).
404
+ const ordered = sortHistoryByUpdatedDesc(kept);
405
+ ordered.reverse();
406
+ const body = ordered.length
407
+ ? `${ordered.map((item) => JSON.stringify(item)).join("\n")}\n`
171
408
  : "";
172
- const tmpFile = `${jsonlFile}.${process.pid}.${Date.now().toString(36)}.${Math.random()
409
+ const tmpFile = `${jsonlFilePath()}.${process.pid}.${Date.now().toString(36)}.${Math.random()
173
410
  .toString(36)
174
411
  .slice(2, 8)}.tmp`;
175
412
  await writeFile(tmpFile, body, { mode: 0o600 });
176
413
  try {
177
- await rename(tmpFile, jsonlFile);
414
+ await rename(tmpFile, jsonlFilePath());
178
415
  }
179
416
  catch (err) {
180
- await rm(tmpFile, { force: true }).catch(() => undefined);
417
+ // Keep the temp file on rename failure so recovery can pick it up —
418
+ // only remove empty temps.
419
+ try {
420
+ const st = await readFile(tmpFile).catch(() => null);
421
+ if (!st || st.length === 0) {
422
+ await rm(tmpFile, { force: true }).catch(() => undefined);
423
+ }
424
+ }
425
+ catch {
426
+ /* ignore */
427
+ }
181
428
  throw err;
182
429
  }
183
- await fixOwner(jsonlFile);
430
+ await fixOwner(jsonlFilePath());
184
431
  }
185
432
  export async function saveSession(messages, name, transcript) {
186
433
  // Auto-derive a readable name from the first user message if none provided
@@ -248,6 +495,20 @@ async function enforceSqliteRetention(db) {
248
495
  const limit = getConfig().historyRetentionLimit;
249
496
  if (!limit || limit <= 0)
250
497
  return;
498
+ // Archive rows about to be deleted, then delete — never hard-drop without
499
+ // a JSONL archive copy.
500
+ try {
501
+ const doomed = db
502
+ .prepare(`SELECT id, name, created_at, updated_at, cwd, messages_json FROM sessions
503
+ WHERE id NOT IN (SELECT id FROM sessions ORDER BY updated_at DESC LIMIT ?)`)
504
+ .all(Math.floor(limit));
505
+ const records = doomed.map(rowToSession);
506
+ if (records.length > 0)
507
+ await appendRecordsToFile(archiveFilePath(), records);
508
+ }
509
+ catch {
510
+ /* archive best-effort */
511
+ }
251
512
  db.exec(`DELETE FROM sessions WHERE id NOT IN (SELECT id FROM sessions ORDER BY updated_at DESC LIMIT ${Math.floor(limit)});`);
252
513
  }
253
514
  async function upsertJsonl(record) {
@@ -292,24 +553,12 @@ function rowToSession(row) {
292
553
  };
293
554
  }
294
555
  async function listJsonlSessions(limit) {
295
- if (!(await safeExists(jsonlFile)))
296
- return [];
556
+ await ensureHistoryRecovered();
297
557
  try {
298
- const raw = await readFile(jsonlFile, "utf8");
299
- return raw
300
- .split("\n")
301
- .filter(Boolean)
302
- .map((line) => {
303
- try {
304
- return JSON.parse(line);
305
- }
306
- catch {
307
- return null;
308
- }
309
- })
310
- .filter((record) => record !== null)
311
- .slice(-limit)
312
- .reverse();
558
+ const records = sortHistoryByUpdatedDesc(dedupeHistoryById(await readJsonlRecordsFrom(jsonlFilePath())));
559
+ if (!limit || limit <= 0)
560
+ return records;
561
+ return records.slice(0, limit);
313
562
  }
314
563
  catch (err) {
315
564
  if (err && err.code === "EACCES") {
@@ -318,34 +567,85 @@ async function listJsonlSessions(limit) {
318
567
  return [];
319
568
  }
320
569
  }
570
+ /**
571
+ * Merge SQLite + JSONL sources so enabling better-sqlite3 never hides an
572
+ * existing JSONL history (or vice versa). Prefer the newer updatedAt per id.
573
+ */
574
+ function mergeSessionLists(...lists) {
575
+ return sortHistoryByUpdatedDesc(dedupeHistoryById(lists.flat()));
576
+ }
321
577
  export async function listSessions(limit = 20) {
578
+ await ensureHistoryRecovered();
579
+ const fromJsonl = await listJsonlSessions(0); // full set, already sorted
322
580
  const db = await loadDatabase();
323
- if (!db)
324
- return listJsonlSessions(limit);
325
- const rows = db
326
- .prepare("SELECT id, name, created_at, updated_at, cwd, messages_json FROM sessions ORDER BY updated_at DESC LIMIT ?")
327
- .all(limit);
328
- return rows.map(rowToSession);
581
+ let fromDb = [];
582
+ if (db) {
583
+ try {
584
+ const rows = db
585
+ .prepare("SELECT id, name, created_at, updated_at, cwd, messages_json FROM sessions ORDER BY updated_at DESC")
586
+ .all();
587
+ fromDb = rows.map(rowToSession);
588
+ }
589
+ catch {
590
+ fromDb = [];
591
+ }
592
+ }
593
+ // Active + SQLite only. Archive/pruned sessions are merged back into the
594
+ // active file by recoverOrphanedHistory() (called on /history open), so
595
+ // they reappear there rather than staying invisible forever.
596
+ const merged = mergeSessionLists(fromJsonl, fromDb);
597
+ if (!limit || limit <= 0)
598
+ return merged;
599
+ return merged.slice(0, limit);
329
600
  }
330
601
  export async function getSession(sessionId) {
602
+ await ensureHistoryRecovered();
331
603
  const db = await loadDatabase();
332
- if (!db) {
333
- return (await listJsonlSessions(Number.MAX_SAFE_INTEGER)).find((session) => session.id === sessionId);
604
+ let fromDb;
605
+ if (db) {
606
+ const row = db
607
+ .prepare("SELECT id, name, created_at, updated_at, cwd, messages_json FROM sessions WHERE id = ?")
608
+ .get(sessionId);
609
+ if (row)
610
+ fromDb = rowToSession(row);
334
611
  }
335
- const row = db
336
- .prepare("SELECT id, name, created_at, updated_at, cwd, messages_json FROM sessions WHERE id = ?")
337
- .get(sessionId);
338
- return row ? rowToSession(row) : undefined;
612
+ const fromJsonl = (await readJsonlRecordsFrom(jsonlFilePath())).find((session) => session.id === sessionId);
613
+ // Also check archive for sessions pruned from the active set.
614
+ const fromArchive = fromJsonl
615
+ ? undefined
616
+ : (await readJsonlRecordsFrom(archiveFilePath())).find((s) => s.id === sessionId);
617
+ const candidates = [fromDb, fromJsonl, fromArchive].filter((r) => Boolean(r));
618
+ if (candidates.length === 0)
619
+ return undefined;
620
+ return sortHistoryByUpdatedDesc(candidates)[0];
339
621
  }
340
622
  export function getHistoryPath() {
341
- return sqliteUnavailable ? jsonlFile : dbFile;
623
+ // Prefer JSONL as the durable path users can inspect/backup; SQLite is
624
+ // optional acceleration when better-sqlite3 is installed.
625
+ return jsonlFilePath();
342
626
  }
343
627
  /**
344
- * Clear all stored history. Returns whether the operation succeeded.
345
- * Used by the /privacy slash command and `clai privacy clear-history`.
628
+ * Clear active history after archiving a full snapshot. Never unrecoverably
629
+ * destroys chats a timestamped copy is written under history-backups/ and
630
+ * the previous active file is moved into history-archive.jsonl.
346
631
  */
347
632
  export async function clearAllHistory() {
348
633
  let detail = "";
634
+ await ensureHistoryRecovered();
635
+ // Snapshot + move aside. Do NOT write into history-archive.jsonl here —
636
+ // that file is auto-reimported by recoverOrphanedHistory, which would
637
+ // immediately undo a clear. Intentional clears go to history-cleared-*.jsonl
638
+ // (and rolling backups) only.
639
+ try {
640
+ const snapshot = await readJsonlRecordsFrom(jsonlFilePath());
641
+ if (snapshot.length > 0) {
642
+ await backupActiveHistory();
643
+ detail += `backed up ${snapshot.length} session(s); `;
644
+ }
645
+ }
646
+ catch (error) {
647
+ detail += `backup error: ${error instanceof Error ? error.message : String(error)}; `;
648
+ }
349
649
  try {
350
650
  const db = await loadDatabase();
351
651
  if (db) {
@@ -356,10 +656,15 @@ export async function clearAllHistory() {
356
656
  catch (error) {
357
657
  detail += `sqlite error: ${error instanceof Error ? error.message : String(error)}; `;
358
658
  }
359
- if (await safeExists(jsonlFile)) {
659
+ if (await safeExists(jsonlFilePath())) {
360
660
  try {
361
- await rm(jsonlFile, { force: true });
362
- detail += "jsonl removed";
661
+ // Move aside rather than unlink so crash mid-clear still leaves a file.
662
+ const clearedCopy = join(historyDirPath(), `history-cleared-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
663
+ await rename(jsonlFilePath(), clearedCopy).catch(async () => {
664
+ await copyFile(jsonlFilePath(), clearedCopy).catch(() => undefined);
665
+ await rm(jsonlFilePath(), { force: true });
666
+ });
667
+ detail += `jsonl moved to ${clearedCopy} (recoverable)`;
363
668
  }
364
669
  catch (error) {
365
670
  detail += `jsonl error: ${error instanceof Error ? error.message : String(error)}`;
@@ -378,6 +683,12 @@ export async function clearAllHistory() {
378
683
  return { cleared: true, detail: detail.trim() };
379
684
  }
380
685
  export function getJsonlHistoryPath() {
381
- return jsonlFile;
686
+ return jsonlFilePath();
687
+ }
688
+ export function getHistoryArchivePath() {
689
+ return archiveFilePath();
690
+ }
691
+ export function getHistoryBackupDir() {
692
+ return backupDirPath();
382
693
  }
383
694
  //# sourceMappingURL=history.js.map