@cloudcli-ai/cloudcli 1.37.2 → 1.37.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 (328) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-CJntskwA.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-CIfH4uMN.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-_IpEkukg.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-BttxrpgZ.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-g51dSRr1.js} +1 -1
  7. package/dist/assets/channel-C5xU9jMw.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-D_-LbQUa.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-BtlHzwaO.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-UzkpvpAT.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-BKwx5Gag.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-D_KEBH5f.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-rK2DGyto.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CeBl_N9i.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-BiQLWVIr.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-C1knVGV5.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-CtKbJkvF.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DjX8y40Z.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-DM6YeRba.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-wZJ-vy5B.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-BzP0ejkH.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-BTL_j23J.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-Cf1Norgu.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-DPiUEKr-.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-CTcTpiz5.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-DPMSdYm0.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-7yvD6uiF.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CfgeDoV6.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-B27ng5zu.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-CcC6b_7t.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-BX5kPGrO.js} +1 -1
  34. package/dist/assets/index-BTDHQ6Id.css +32 -0
  35. package/dist/assets/index-sFfz8iBR.js +4931 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-CUP-SlN4.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-BTTDhOpX.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-ARXcc61m.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-BieMpqMA.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-61vxjRnP.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-nhnwkfQx.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-Cm6x5vT1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-CcYBq_9H.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-CfvIjfzo.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-SgYoWPNb.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-D2Mu5E_f.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Azwe_k4Q.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-DN1Ju3PA.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-Bj4qC34S.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-Co4PdklG.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-nOvL9dRC.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-CCK3wMid.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-BC29RNFm.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  58. package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-pZlZCtEo.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-De8k2-zM.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-Bk8ykH0w.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +7 -3
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.routes.js +2 -2
  65. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  66. package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
  67. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  68. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  69. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  70. package/dist-server/server/modules/database/connection.js +1 -1
  71. package/dist-server/server/modules/database/connection.js.map +1 -1
  72. package/dist-server/server/modules/database/index.js +5 -0
  73. package/dist-server/server/modules/database/index.js.map +1 -1
  74. package/dist-server/server/modules/database/migrations.js +32 -1
  75. package/dist-server/server/modules/database/migrations.js.map +1 -1
  76. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  77. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  78. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  79. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  80. package/dist-server/server/modules/database/repositories/sessions.db.js +137 -5
  81. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  83. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  84. package/dist-server/server/modules/database/schema.js +92 -0
  85. package/dist-server/server/modules/database/schema.js.map +1 -1
  86. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  87. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  88. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  89. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  90. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  91. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  92. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  93. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  94. package/dist-server/server/modules/providers/index.js +3 -0
  95. package/dist-server/server/modules/providers/index.js.map +1 -1
  96. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +20 -1
  97. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  98. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  99. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  100. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +35 -6
  101. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  102. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +168 -38
  103. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  104. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
  105. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  106. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  107. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  108. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  109. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  110. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  111. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  112. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  113. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  114. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +16 -0
  115. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  116. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +60 -49
  117. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  118. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  119. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  120. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1565 -432
  121. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  122. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  123. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  124. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  125. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  127. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +258 -6
  128. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  129. package/dist-server/server/modules/providers/provider.routes.js +24 -0
  130. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  131. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
  132. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  133. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  134. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  135. package/dist-server/server/modules/providers/services/session-history-cache.service.js +87 -0
  136. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  137. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  138. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  139. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  140. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  141. package/dist-server/server/modules/providers/services/sessions.service.js +179 -9
  142. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  143. package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
  144. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  145. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  146. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  147. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +465 -39
  148. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  149. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  150. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  151. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  152. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  153. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  154. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  155. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
  156. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  157. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  158. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  159. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +304 -15
  160. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  161. package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
  162. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  163. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  164. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  165. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  166. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  167. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
  168. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  169. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  170. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  171. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  172. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  173. package/dist-server/server/modules/providers/tests/sessions.service.test.js +45 -1
  174. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  175. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  176. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  177. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  178. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  179. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  180. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  181. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
  182. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  183. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  184. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  185. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
  186. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  187. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  188. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  189. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  190. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  191. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  192. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  193. package/dist-server/server/modules/user/user.module.js +10 -1
  194. package/dist-server/server/modules/user/user.module.js.map +1 -1
  195. package/dist-server/server/modules/user/user.routes.js +44 -0
  196. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  197. package/dist-server/server/modules/user/user.service.js +75 -0
  198. package/dist-server/server/modules/user/user.service.js.map +1 -1
  199. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  200. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  201. package/dist-server/server/modules/websocket/index.js +7 -0
  202. package/dist-server/server/modules/websocket/index.js.map +1 -1
  203. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +10 -44
  204. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  205. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  206. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  207. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +166 -9
  208. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  209. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  210. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  211. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -3
  212. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  213. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  214. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  215. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  216. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  217. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  218. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  219. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +80 -0
  220. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  221. package/dist-server/server/shared/claude-cli-path.js +22 -5
  222. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  223. package/dist-server/server/shared/index.js +3 -0
  224. package/dist-server/server/shared/index.js.map +1 -0
  225. package/dist-server/server/shared/message-unification.js +376 -0
  226. package/dist-server/server/shared/message-unification.js.map +1 -0
  227. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  228. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  229. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  230. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  231. package/dist-server/server/shared/utils.js +34 -0
  232. package/dist-server/server/shared/utils.js.map +1 -1
  233. package/package.json +19 -16
  234. package/server/index.ts +11 -4
  235. package/server/modules/agent/agent.routes.ts +2 -2
  236. package/server/modules/agent/tests/agent.routes.test.ts +33 -1
  237. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  238. package/server/modules/database/connection.ts +2 -1
  239. package/server/modules/database/index.ts +10 -0
  240. package/server/modules/database/migrations.ts +37 -0
  241. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  242. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  243. package/server/modules/database/repositories/sessions.db.ts +196 -3
  244. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  245. package/server/modules/database/schema.ts +96 -0
  246. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  247. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  248. package/server/modules/projects/services/project-delete.service.ts +13 -1
  249. package/server/modules/projects/services/project-star.service.ts +2 -2
  250. package/server/modules/providers/index.ts +4 -0
  251. package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
  252. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  253. package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
  254. package/server/modules/providers/list/claude/claude-runtime.provider.js +188 -39
  255. package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
  256. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  257. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  258. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  259. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  260. package/server/modules/providers/list/codex/codex-models.provider.ts +16 -0
  261. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
  262. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  263. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
  264. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  265. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  266. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  267. package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
  268. package/server/modules/providers/provider.routes.ts +31 -0
  269. package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
  270. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  271. package/server/modules/providers/services/session-history-cache.service.ts +139 -0
  272. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  273. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  274. package/server/modules/providers/services/sessions.service.ts +201 -9
  275. package/server/modules/providers/tests/claude-auth.test.ts +58 -0
  276. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  277. package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
  278. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  279. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  280. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  281. package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
  282. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  283. package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
  284. package/server/modules/providers/tests/opencode-models.test.ts +166 -8
  285. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  286. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  287. package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
  288. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  289. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  290. package/server/modules/providers/tests/sessions.service.test.ts +67 -1
  291. package/server/modules/providers/tests/skills.test.ts +21 -5
  292. package/server/modules/scheduled-messages/index.ts +8 -0
  293. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  294. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
  295. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  296. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
  297. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  298. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  299. package/server/modules/user/tests/user.service.test.ts +90 -0
  300. package/server/modules/user/user.module.ts +10 -1
  301. package/server/modules/user/user.routes.ts +44 -0
  302. package/server/modules/user/user.service.ts +105 -0
  303. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  304. package/server/modules/websocket/index.ts +8 -0
  305. package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
  306. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  307. package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
  308. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  309. package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
  310. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  311. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  312. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  313. package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
  314. package/server/shared/claude-cli-path.ts +24 -6
  315. package/server/shared/index.ts +3 -0
  316. package/server/shared/interfaces.ts +62 -0
  317. package/server/shared/message-unification.ts +449 -0
  318. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  319. package/server/shared/tests/message-unification.test.ts +218 -0
  320. package/server/shared/types.ts +123 -2
  321. package/server/shared/utils.ts +40 -0
  322. package/dist/assets/channel-_falYbU1.js +0 -1
  323. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  324. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  325. package/dist/assets/index-CzPLBvXT.css +0 -32
  326. package/dist/assets/index-DFkBBuSA.js +0 -1549
  327. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  328. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -13,6 +13,8 @@ type SessionRow = {
13
13
  model: string | null;
14
14
  /** Reasoning effort this session runs with; NULL until the app records one. */
15
15
  effort: string | null;
16
+ /** The app session this one was branched from; NULL unless it is a fork. */
17
+ forked_from_session_id: string | null;
16
18
  isArchived: number;
17
19
  created_at: string;
18
20
  updated_at: string;
@@ -24,7 +26,7 @@ type RecentSessionsPage = {
24
26
  };
25
27
 
26
28
  const SESSION_ROW_COLUMNS =
27
- 'session_id, provider, provider_session_id, project_path, jsonl_path, custom_name, model, effort, isArchived, created_at, updated_at';
29
+ 'session_id, provider, provider_session_id, project_path, jsonl_path, custom_name, model, effort, forked_from_session_id, isArchived, created_at, updated_at';
28
30
 
29
31
  const SQLITE_UTC_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
30
32
 
@@ -111,7 +113,7 @@ export const sessionsDb = {
111
113
  updated_at = COALESCE(?, CURRENT_TIMESTAMP),
112
114
  project_path = ?,
113
115
  jsonl_path = ?,
114
- isArchived = 0,
116
+ isArchived = CASE WHEN ? IS NULL OR julianday(?) > julianday(updated_at) THEN 0 ELSE isArchived END,
115
117
  custom_name = CASE
116
118
  WHEN session_id <> provider_session_id AND custom_name IS NOT NULL THEN custom_name
117
119
  ELSE COALESCE(?, custom_name)
@@ -122,6 +124,8 @@ export const sessionsDb = {
122
124
  updatedAtValue,
123
125
  normalizedProjectPath,
124
126
  jsonlPath ?? null,
127
+ updatedAtValue,
128
+ updatedAtValue,
125
129
  customName ?? null,
126
130
  existing.session_id
127
131
  );
@@ -141,7 +145,7 @@ export const sessionsDb = {
141
145
  updated_at = excluded.updated_at,
142
146
  project_path = excluded.project_path,
143
147
  jsonl_path = excluded.jsonl_path,
144
- isArchived = 0,
148
+ isArchived = CASE WHEN ? IS NULL OR julianday(excluded.updated_at) > julianday(sessions.updated_at) THEN 0 ELSE sessions.isArchived END,
145
149
  custom_name = CASE
146
150
  WHEN sessions.session_id <> sessions.provider_session_id AND sessions.custom_name IS NOT NULL
147
151
  THEN sessions.custom_name
@@ -155,6 +159,7 @@ export const sessionsDb = {
155
159
  normalizedProjectPath,
156
160
  jsonlPath ?? null,
157
161
  createdAtValue,
162
+ updatedAtValue,
158
163
  updatedAtValue
159
164
  );
160
165
 
@@ -189,6 +194,55 @@ export const sessionsDb = {
189
194
  return sessionId;
190
195
  },
191
196
 
197
+ /**
198
+ * Inserts a session that already has its provider artifact on disk.
199
+ *
200
+ * Unlike `createAppSession` this writes `provider_session_id` and
201
+ * `jsonl_path` immediately, because a fork's transcript file exists before
202
+ * the row does — and the filesystem watcher would otherwise index it as an
203
+ * unrelated session under its own id.
204
+ */
205
+ createForkedSession(input: {
206
+ sessionId: string;
207
+ provider: string;
208
+ projectPath: string;
209
+ customName: string | null;
210
+ providerSessionId: string;
211
+ jsonlPath: string;
212
+ forkedFromSessionId: string;
213
+ model: string | null;
214
+ effort: string | null;
215
+ }): string {
216
+ const db = getConnection();
217
+ const normalizedProjectPath = normalizeProjectPathForProvider(input.provider, input.projectPath);
218
+
219
+ projectsDb.createProjectPath(normalizedProjectPath);
220
+
221
+ // The watcher may already have created a row for the new transcript. Its
222
+ // id is the provider-native one, which is what this row claims, so replace
223
+ // it rather than leaving two sidebar entries for one conversation.
224
+ db.transaction(() => {
225
+ db.prepare('DELETE FROM sessions WHERE session_id = ? AND session_id <> ?')
226
+ .run(input.providerSessionId, input.sessionId);
227
+ db.prepare(
228
+ `INSERT INTO sessions (session_id, provider, provider_session_id, custom_name, project_path, jsonl_path, model, effort, forked_from_session_id, isArchived, created_at, updated_at)
229
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`
230
+ ).run(
231
+ input.sessionId,
232
+ input.provider,
233
+ input.providerSessionId,
234
+ input.customName,
235
+ normalizedProjectPath,
236
+ input.jsonlPath,
237
+ input.model,
238
+ input.effort,
239
+ input.forkedFromSessionId,
240
+ );
241
+ })();
242
+
243
+ return input.sessionId;
244
+ },
245
+
192
246
  /**
193
247
  * Records the provider-native session id for one app-allocated session.
194
248
  *
@@ -235,6 +289,125 @@ export const sessionsDb = {
235
289
  merge();
236
290
  },
237
291
 
292
+ /**
293
+ * Moves one session onto a different provider session and transcript.
294
+ *
295
+ * Only editing a message on a provider that has to branch to rewind (Codex)
296
+ * does this — an ordinary run keeps the same provider session for its whole
297
+ * life. `assignProviderSessionId` cannot be used for it: that one keeps the
298
+ * existing `jsonl_path` on purpose, so a session repointed with it would
299
+ * claim the new thread while still reading the old transcript.
300
+ *
301
+ * The watcher may already have indexed the new transcript under its own id.
302
+ * That row is the same conversation this one is about to become, so it is
303
+ * replaced rather than left behind as a second sidebar entry.
304
+ */
305
+ repointSessionToProviderSession(
306
+ sessionId: string,
307
+ input: { providerSessionId: string; jsonlPath: string },
308
+ ): void {
309
+ const db = getConnection();
310
+
311
+ db.transaction(() => {
312
+ db.prepare('DELETE FROM sessions WHERE session_id = ? AND session_id <> ?')
313
+ .run(input.providerSessionId, sessionId);
314
+ db.prepare(
315
+ `UPDATE sessions SET
316
+ provider_session_id = ?,
317
+ jsonl_path = ?,
318
+ updated_at = CURRENT_TIMESTAMP
319
+ WHERE session_id = ?`
320
+ ).run(input.providerSessionId, input.jsonlPath, sessionId);
321
+ })();
322
+ },
323
+
324
+ /**
325
+ * Detaches a session from its provider session so the next run starts a new
326
+ * one.
327
+ *
328
+ * Used when an edit replaces the very first prompt: there is no conversation
329
+ * left to branch from, so the session starts over instead.
330
+ */
331
+ detachProviderSession(sessionId: string): void {
332
+ const db = getConnection();
333
+ db.prepare(
334
+ `UPDATE sessions SET
335
+ provider_session_id = NULL,
336
+ jsonl_path = NULL,
337
+ updated_at = CURRENT_TIMESTAMP
338
+ WHERE session_id = ?`
339
+ ).run(sessionId);
340
+ },
341
+
342
+ /**
343
+ * Records that a session has left a provider session behind for good.
344
+ *
345
+ * The transcript stays on disk, which is deliberate — the abandoned attempt
346
+ * is recoverable — but the indexer must not offer it back, and on a session
347
+ * discovered from disk (whose app id *is* the provider id) rediscovering it
348
+ * would repoint the row at the conversation the user edited away from.
349
+ */
350
+ markProviderSessionSuperseded(input: {
351
+ providerSessionId: string;
352
+ provider: string;
353
+ sessionId: string;
354
+ jsonlPath: string | null;
355
+ }): void {
356
+ const db = getConnection();
357
+ db.prepare(
358
+ `INSERT INTO superseded_provider_sessions (provider_session_id, provider, session_id, jsonl_path)
359
+ VALUES (?, ?, ?, ?)
360
+ ON CONFLICT(provider_session_id, provider) DO UPDATE SET
361
+ session_id = excluded.session_id,
362
+ jsonl_path = excluded.jsonl_path,
363
+ created_at = CURRENT_TIMESTAMP`
364
+ ).run(input.providerSessionId, input.provider, input.sessionId, input.jsonlPath);
365
+ },
366
+
367
+ isProviderSessionSuperseded(providerSessionId: string, provider: string): boolean {
368
+ const db = getConnection();
369
+ const row = db
370
+ .prepare(
371
+ `SELECT 1 AS found FROM superseded_provider_sessions
372
+ WHERE provider_session_id = ? AND provider = ?
373
+ LIMIT 1`
374
+ )
375
+ .get(providerSessionId, provider) as { found: number } | undefined;
376
+
377
+ return Boolean(row);
378
+ },
379
+
380
+ /**
381
+ * Transcripts one session has left behind, for the caller that deletes a
382
+ * conversation from disk.
383
+ *
384
+ * A conversation edited more than once has lived in more than one file, and
385
+ * the session row only ever points at the newest.
386
+ */
387
+ getSupersededTranscriptPaths(sessionId: string): string[] {
388
+ const db = getConnection();
389
+ const rows = db
390
+ .prepare(
391
+ `SELECT jsonl_path FROM superseded_provider_sessions
392
+ WHERE session_id = ? AND jsonl_path IS NOT NULL`
393
+ )
394
+ .all(sessionId) as Array<{ jsonl_path: string }>;
395
+
396
+ return rows.map((row) => row.jsonl_path);
397
+ },
398
+
399
+ /**
400
+ * Forgets what a session left behind, once the session itself is gone.
401
+ *
402
+ * Without this the record outlives the row it was written for and keeps the
403
+ * indexer refusing a transcript that no longer belongs to anything — a
404
+ * conversation invisible to the app and impossible to delete through it.
405
+ */
406
+ clearSupersededProviderSessions(sessionId: string): void {
407
+ const db = getConnection();
408
+ db.prepare('DELETE FROM superseded_provider_sessions WHERE session_id = ?').run(sessionId);
409
+ },
410
+
238
411
  /**
239
412
  * Records the model one session runs with.
240
413
  *
@@ -521,4 +694,24 @@ export const sessionsDb = {
521
694
  const db = getConnection();
522
695
  return db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId).changes > 0;
523
696
  },
697
+
698
+ /**
699
+ * Lists every indexed session that claims a transcript file on disk.
700
+ *
701
+ * Only rows with a `jsonl_path` are returned, which deliberately excludes
702
+ * app-created sessions still waiting for their first provider write and
703
+ * OpenCode rows (whose transcripts all live inside one shared sqlite file).
704
+ * Used by the session synchronizer to find rows whose transcript has been
705
+ * deleted underneath the index.
706
+ */
707
+ getSessionsWithTranscriptPath(): Array<{ session_id: string; jsonl_path: string }> {
708
+ const db = getConnection();
709
+ return db
710
+ .prepare(
711
+ `SELECT session_id, jsonl_path
712
+ FROM sessions
713
+ WHERE jsonl_path IS NOT NULL AND jsonl_path <> ''`
714
+ )
715
+ .all() as Array<{ session_id: string; jsonl_path: string }>;
716
+ },
524
717
  };
@@ -0,0 +1,80 @@
1
+ import { getConnection } from '@/modules/database/connection.js';
2
+
3
+ type PreferenceRow = {
4
+ preference_key: string;
5
+ preference_value: string;
6
+ };
7
+
8
+ /**
9
+ * Decodes one stored preference, dropping any row whose JSON no longer parses
10
+ * rather than failing the whole read. A single corrupted value must not cost
11
+ * the user every other setting they have.
12
+ */
13
+ function decodePreference(row: PreferenceRow): [string, unknown] | null {
14
+ try {
15
+ return [row.preference_key, JSON.parse(row.preference_value) as unknown];
16
+ } catch {
17
+ console.warn(`[UserPreferences] Dropping unreadable value for "${row.preference_key}"`);
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export const userPreferencesDb = {
23
+ /**
24
+ * Returns every preference the user has ever set, as one object.
25
+ *
26
+ * The client fetches this once on start-up, so absent keys simply mean "the
27
+ * user never changed this" and the client applies its own default.
28
+ */
29
+ getPreferences(userId: number): Record<string, unknown> {
30
+ const db = getConnection();
31
+ const rows = db
32
+ .prepare(
33
+ `SELECT preference_key, preference_value
34
+ FROM user_preferences
35
+ WHERE user_id = ?`
36
+ )
37
+ .all(userId) as PreferenceRow[];
38
+
39
+ const preferences: Record<string, unknown> = {};
40
+ for (const row of rows) {
41
+ const decoded = decodePreference(row);
42
+ if (decoded) {
43
+ preferences[decoded[0]] = decoded[1];
44
+ }
45
+ }
46
+
47
+ return preferences;
48
+ },
49
+
50
+ /**
51
+ * Merge-patches preferences: keys present in `updates` are written, keys
52
+ * absent are left alone, and a key given as `undefined` is deleted.
53
+ *
54
+ * Runs in one transaction so a multi-key save from the settings dialog can
55
+ * never be observed half-applied.
56
+ */
57
+ savePreferences(userId: number, updates: Record<string, unknown>): void {
58
+ const db = getConnection();
59
+ const upsert = db.prepare(
60
+ `INSERT INTO user_preferences (user_id, preference_key, preference_value, updated_at)
61
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
62
+ ON CONFLICT(user_id, preference_key) DO UPDATE SET
63
+ preference_value = excluded.preference_value,
64
+ updated_at = CURRENT_TIMESTAMP`
65
+ );
66
+ const remove = db.prepare(
67
+ 'DELETE FROM user_preferences WHERE user_id = ? AND preference_key = ?'
68
+ );
69
+
70
+ db.transaction(() => {
71
+ for (const [key, value] of Object.entries(updates)) {
72
+ if (value === undefined) {
73
+ remove.run(userId, key);
74
+ continue;
75
+ }
76
+ upsert.run(userId, key, JSON.stringify(value));
77
+ }
78
+ })();
79
+ },
80
+ };
@@ -96,6 +96,30 @@ CREATE TABLE IF NOT EXISTS projects (
96
96
  );
97
97
  `;
98
98
 
99
+ export const SCHEDULED_MESSAGES_TABLE_SCHEMA_SQL = `
100
+ CREATE TABLE IF NOT EXISTS scheduled_messages (
101
+ id TEXT PRIMARY KEY,
102
+ user_id INTEGER NOT NULL,
103
+ session_id TEXT NOT NULL,
104
+ content TEXT NOT NULL,
105
+ -- Composer preferences (model, effort, permission mode, attachments) as
106
+ -- they were when the message was scheduled, so it runs the way the user
107
+ -- set it up rather than however the session is configured hours later.
108
+ options TEXT NOT NULL DEFAULT '{}',
109
+ -- UTC. The client sends an absolute instant so the schedule does not move
110
+ -- when the user changes time zone between scheduling and firing.
111
+ scheduled_for DATETIME NOT NULL,
112
+ -- pending | sent | failed | cancelled
113
+ status TEXT NOT NULL DEFAULT 'pending',
114
+ -- Why a failed one failed, shown next to it in the composer.
115
+ failure_reason TEXT,
116
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
117
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
118
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
119
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
120
+ );
121
+ `;
122
+
99
123
  export const SESSIONS_TABLE_SCHEMA_SQL = `
100
124
  CREATE TABLE IF NOT EXISTS sessions (
101
125
  session_id TEXT NOT NULL,
@@ -114,6 +138,10 @@ CREATE TABLE IF NOT EXISTS sessions (
114
138
  -- restores its exact runtime configuration instead of provider defaults.
115
139
  model TEXT,
116
140
  effort TEXT,
141
+ -- The app session this one was branched from, NULL for sessions created
142
+ -- normally. Informational only: a fork is a fully independent provider
143
+ -- session, and deleting the source does not affect it.
144
+ forked_from_session_id TEXT,
117
145
  isArchived BOOLEAN DEFAULT 0,
118
146
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
119
147
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -160,6 +188,68 @@ CREATE TABLE IF NOT EXISTS provider_models (
160
188
  );
161
189
  `;
162
190
 
191
+ /**
192
+ * Per-user application preferences that used to live in browser localStorage.
193
+ *
194
+ * One row per (user, key); `preference_value` is always a JSON document so a
195
+ * key can hold a scalar (`"dark"`), a flag (`false`) or a whole settings blob
196
+ * without the schema having to know which. Keeping them server-side is what
197
+ * makes a preference follow the user from one device to another.
198
+ */
199
+ export const USER_PREFERENCES_TABLE_SCHEMA_SQL = `
200
+ CREATE TABLE IF NOT EXISTS user_preferences (
201
+ user_id INTEGER NOT NULL,
202
+ preference_key TEXT NOT NULL,
203
+ preference_value TEXT NOT NULL,
204
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
205
+ PRIMARY KEY (user_id, preference_key),
206
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
207
+ );
208
+ `;
209
+
210
+ /**
211
+ * Unsent composer text and queued messages, per user and per chat scope.
212
+ *
213
+ * `draft_scope` is a session id, or `project:<projectId>` for a chat that has
214
+ * not been sent yet and therefore has no session. Storing this server-side is
215
+ * what lets a message typed on a laptop be finished on a phone.
216
+ */
217
+ export const SESSION_DRAFTS_TABLE_SCHEMA_SQL = `
218
+ CREATE TABLE IF NOT EXISTS session_drafts (
219
+ user_id INTEGER NOT NULL,
220
+ draft_scope TEXT NOT NULL,
221
+ draft_text TEXT NOT NULL DEFAULT '',
222
+ queued_message TEXT,
223
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
224
+ PRIMARY KEY (user_id, draft_scope),
225
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
226
+ );
227
+ `;
228
+
229
+ /**
230
+ * Provider sessions an app session has moved off, and must never move back to.
231
+ *
232
+ * Editing a message on a provider that cannot resume a transcript partway
233
+ * (Codex) is done by branching: the conversation is copied up to the edited
234
+ * turn and the app session follows the copy. The original transcript is left
235
+ * on disk untouched — nothing is deleted — but it is no longer the session's,
236
+ * and the indexer would otherwise rediscover it on its next full scan and hand
237
+ * the session back to the version the user edited away from.
238
+ */
239
+ export const SUPERSEDED_PROVIDER_SESSIONS_TABLE_SCHEMA_SQL = `
240
+ CREATE TABLE IF NOT EXISTS superseded_provider_sessions (
241
+ provider_session_id TEXT NOT NULL,
242
+ provider TEXT NOT NULL,
243
+ session_id TEXT NOT NULL,
244
+ -- The transcript the session left behind. Recorded because the session row
245
+ -- stops pointing at it, and "delete this conversation from disk" has to
246
+ -- reach every file the conversation ever lived in.
247
+ jsonl_path TEXT,
248
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
249
+ PRIMARY KEY (provider_session_id, provider)
250
+ );
251
+ `;
252
+
163
253
  export const INIT_SCHEMA_SQL = `
164
254
  -- Initialize authentication database
165
255
  PRAGMA foreign_keys = ON;
@@ -207,4 +297,10 @@ ${APP_CONFIG_TABLE_SCHEMA_SQL}
207
297
  ${PROVIDER_MODELS_TABLE_SCHEMA_SQL}
208
298
  CREATE INDEX IF NOT EXISTS idx_provider_models_provider_order
209
299
  ON provider_models(provider, sort_order, id);
300
+
301
+ ${USER_PREFERENCES_TABLE_SCHEMA_SQL}
302
+
303
+ ${SESSION_DRAFTS_TABLE_SCHEMA_SQL}
304
+
305
+ ${SUPERSEDED_PROVIDER_SESSIONS_TABLE_SCHEMA_SQL}
210
306
  `;
@@ -72,6 +72,58 @@ test('createSession reactivates archived rows when the session becomes active ag
72
72
  });
73
73
  });
74
74
 
75
+ test("createSession leaves an archived row archived when the transcript has not changed", async () => {
76
+ await withIsolatedDatabase(() => {
77
+ const createdAt = "2026-07-18T09:00:00.000Z";
78
+ const updatedAt = "2026-07-18T10:00:00.000Z";
79
+ const jsonlPath = "/transcripts/session-untouched.jsonl";
80
+
81
+ sessionsDb.createSession("session-untouched", "claude", "/workspace/demo-project", "A Name", createdAt, updatedAt, jsonlPath);
82
+ sessionsDb.updateSessionIsArchived("session-untouched", true);
83
+
84
+ // A full rescan re-indexes every transcript created since the last scan,
85
+ // changed or not, and hands over the timestamps the file still carries.
86
+ sessionsDb.createSession("session-untouched", "claude", "/workspace/demo-project", "A Name", createdAt, updatedAt, jsonlPath);
87
+
88
+ assert.equal(sessionsDb.getSessionById("session-untouched")?.isArchived, 1);
89
+ assert.equal(sessionsDb.getArchivedSessions().length, 1);
90
+ assert.equal(sessionsDb.getAllSessions().length, 0);
91
+
92
+ // Actually writing to the session again still brings it back.
93
+ sessionsDb.createSession("session-untouched", "claude", "/workspace/demo-project", "A Name", createdAt, "2026-07-18T11:00:00.000Z", jsonlPath);
94
+
95
+ assert.equal(sessionsDb.getSessionById("session-untouched")?.isArchived, 0);
96
+ });
97
+ });
98
+
99
+ test("the upsert path counts an omitted timestamp as activity", async () => {
100
+ await withIsolatedDatabase(() => {
101
+ // An app-created row carries no provider id, so indexing it takes the
102
+ // INSERT ... ON CONFLICT branch rather than the UPDATE above. Its
103
+ // updated_at is CURRENT_TIMESTAMP, which resolves to whole seconds, so a
104
+ // call in the same second is not *newer* -- the omitted timestamp itself
105
+ // has to be what reactivates the row.
106
+ sessionsDb.createAppSession("session-legacy", "claude", "/workspace/demo-project");
107
+ sessionsDb.updateSessionIsArchived("session-legacy", true);
108
+
109
+ sessionsDb.createSession("session-legacy", "claude", "/workspace/demo-project", "Indexed Name");
110
+
111
+ assert.equal(sessionsDb.getSessionById("session-legacy")?.isArchived, 0);
112
+ });
113
+ });
114
+
115
+ test("the upsert path leaves an archived row alone for a transcript older than it", async () => {
116
+ await withIsolatedDatabase(() => {
117
+ sessionsDb.createAppSession("session-stale", "claude", "/workspace/demo-project");
118
+ sessionsDb.updateSessionIsArchived("session-stale", true);
119
+
120
+ sessionsDb.createSession("session-stale", "claude", "/workspace/demo-project", "Indexed Name", "2026-07-18T09:00:00.000Z", "2026-07-18T10:00:00.000Z", "/transcripts/session-stale.jsonl");
121
+
122
+ assert.equal(sessionsDb.getSessionById("session-stale")?.isArchived, 1);
123
+ });
124
+ });
125
+
126
+
75
127
  test('repository reads normalize SQLite UTC timestamps to ISO strings', async () => {
76
128
  await withIsolatedDatabase(() => {
77
129
  sessionsDb.createAppSession('session-timezone', 'claude', '/workspace/demo-project');
@@ -0,0 +1,135 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import {
8
+ closeConnection,
9
+ getConnection,
10
+ initializeDatabase,
11
+ sessionDraftsDb,
12
+ userPreferencesDb,
13
+ } from '@/modules/database/index.js';
14
+
15
+ const USER_ID = 1;
16
+
17
+ async function withDatabase(runTest: () => void | Promise<void>): Promise<void> {
18
+ const previousDatabasePath = process.env.DATABASE_PATH;
19
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'user-prefs-db-'));
20
+ const databasePath = path.join(tempDirectory, 'auth.db');
21
+
22
+ closeConnection();
23
+ process.env.DATABASE_PATH = databasePath;
24
+ await writeFile(databasePath, '');
25
+ await initializeDatabase();
26
+
27
+ // Both tables cascade from users(id), so a row has to exist to write against.
28
+ getConnection()
29
+ .prepare('INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)')
30
+ .run(USER_ID, 'tester', 'hash');
31
+
32
+ try {
33
+ await runTest();
34
+ } finally {
35
+ closeConnection();
36
+ if (previousDatabasePath === undefined) {
37
+ delete process.env.DATABASE_PATH;
38
+ } else {
39
+ process.env.DATABASE_PATH = previousDatabasePath;
40
+ }
41
+ await rm(tempDirectory, { recursive: true, force: true });
42
+ }
43
+ }
44
+
45
+ test('preferences round-trip every JSON shape a setting can take', async () => {
46
+ await withDatabase(() => {
47
+ userPreferencesDb.savePreferences(USER_ID, {
48
+ theme: 'dark',
49
+ tasksEnabled: false,
50
+ claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
51
+ });
52
+
53
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
54
+ theme: 'dark',
55
+ tasksEnabled: false,
56
+ claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
57
+ });
58
+ });
59
+ });
60
+
61
+ test('saving preferences merge-patches instead of replacing the whole set', async () => {
62
+ await withDatabase(() => {
63
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
64
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'light' });
65
+
66
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
67
+ theme: 'light',
68
+ userLanguage: 'de',
69
+ });
70
+ });
71
+ });
72
+
73
+ test('a preference set to undefined is removed', async () => {
74
+ await withDatabase(() => {
75
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
76
+ userPreferencesDb.savePreferences(USER_ID, { userLanguage: undefined });
77
+
78
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
79
+ });
80
+ });
81
+
82
+ test('one unreadable preference value does not cost the user the others', async () => {
83
+ await withDatabase(() => {
84
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'dark' });
85
+ getConnection()
86
+ .prepare(
87
+ `INSERT INTO user_preferences (user_id, preference_key, preference_value)
88
+ VALUES (?, ?, ?)`
89
+ )
90
+ .run(USER_ID, 'broken', 'not json');
91
+
92
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
93
+ });
94
+ });
95
+
96
+ test('drafts round-trip text and queued message per scope', async () => {
97
+ await withDatabase(() => {
98
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'half typed', queuedMessage: null });
99
+ sessionDraftsDb.saveDraft(USER_ID, 'project:p1', {
100
+ text: '',
101
+ queuedMessage: { content: 'run it' },
102
+ });
103
+
104
+ const drafts = sessionDraftsDb.getDrafts(USER_ID);
105
+ const byScope = new Map(drafts.map((draft) => [draft.scope, draft]));
106
+
107
+ assert.equal(byScope.get('session-a')?.text, 'half typed');
108
+ assert.equal(byScope.get('session-a')?.queuedMessage, null);
109
+ assert.equal(byScope.get('project:p1')?.text, '');
110
+ assert.deepEqual(byScope.get('project:p1')?.queuedMessage, { content: 'run it' });
111
+ });
112
+ });
113
+
114
+ test('saving an empty draft deletes the row rather than keeping a blank one', async () => {
115
+ await withDatabase(() => {
116
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'typed', queuedMessage: null });
117
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: '', queuedMessage: null });
118
+
119
+ assert.deepEqual(sessionDraftsDb.getDrafts(USER_ID), []);
120
+ });
121
+ });
122
+
123
+ test('deleteDraft removes only the named scope', async () => {
124
+ await withDatabase(() => {
125
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'a', queuedMessage: null });
126
+ sessionDraftsDb.saveDraft(USER_ID, 'session-b', { text: 'b', queuedMessage: null });
127
+
128
+ sessionDraftsDb.deleteDraft(USER_ID, 'session-a');
129
+
130
+ assert.deepEqual(
131
+ sessionDraftsDb.getDrafts(USER_ID).map((draft) => draft.scope),
132
+ ['session-b'],
133
+ );
134
+ });
135
+ });
@@ -43,11 +43,23 @@ async function unlinkJsonlIfExists(filePath: string): Promise<void> {
43
43
  */
44
44
  export async function deleteSessionJsonlFilesForProjectPath(projectPath: string): Promise<void> {
45
45
  const sessions = sessionsDb.getSessionsByProjectPathIncludingArchived(projectPath);
46
- const paths = uniqueJsonlPathsFromSessions(sessions);
46
+ // Including the transcripts a session has left behind: editing a message on
47
+ // a provider that rewinds by branching moves the session onto a copy, and
48
+ // the row stops pointing at the file the replaced turns are still in.
49
+ const paths = uniqueJsonlPathsFromSessions([
50
+ ...sessions,
51
+ ...sessions.flatMap((session) => sessionsDb
52
+ .getSupersededTranscriptPaths(session.session_id)
53
+ .map((jsonl_path) => ({ jsonl_path }))),
54
+ ]);
47
55
 
48
56
  for (const filePath of paths) {
49
57
  await unlinkJsonlIfExists(filePath);
50
58
  }
59
+
60
+ for (const session of sessions) {
61
+ sessionsDb.clearSupersededProviderSessions(session.session_id);
62
+ }
51
63
  }
52
64
 
53
65
  /**