@cloudcli-ai/cloudcli 1.37.1 → 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 (402) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/abnfDiagram-N423BO3Z-CJntskwA.js +1 -0
  3. package/dist/assets/arc-CIfH4uMN.js +1 -0
  4. package/dist/assets/architectureDiagram-T3A2C74G-_IpEkukg.js +36 -0
  5. package/dist/assets/blockDiagram-VBNYF7ZC-BttxrpgZ.js +132 -0
  6. package/dist/assets/c4Diagram-5PPSVZJV-g51dSRr1.js +10 -0
  7. package/dist/assets/channel-C5xU9jMw.js +1 -0
  8. package/dist/assets/chunk-2GRJ4B5K-D_-LbQUa.js +1 -0
  9. package/dist/assets/chunk-2Q5K7J3B-BtlHzwaO.js +1 -0
  10. package/dist/assets/chunk-5RXB4S5H-UzkpvpAT.js +231 -0
  11. package/dist/assets/chunk-5VM5RSS4-BKwx5Gag.js +15 -0
  12. package/dist/assets/chunk-6Q2QTUOP-D_KEBH5f.js +88 -0
  13. package/dist/assets/chunk-GF5L2VYU-rK2DGyto.js +206 -0
  14. package/dist/assets/chunk-JWPE2WC7-CeBl_N9i.js +1 -0
  15. package/dist/assets/chunk-KBJHAD2P-BiQLWVIr.js +1 -0
  16. package/dist/assets/chunk-RYQCIY6F-C1knVGV5.js +1 -0
  17. package/dist/assets/chunk-XXDRQBXY-CtKbJkvF.js +1 -0
  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-DjX8y40Z.js +1 -0
  21. package/dist/assets/cynefin-VYW2F7L2-DM6YeRba.js +178 -0
  22. package/dist/assets/cynefinDiagram-MW4NZA55-wZJ-vy5B.js +62 -0
  23. package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
  24. package/dist/assets/dagre-VZM6K2ZE-BzP0ejkH.js +4 -0
  25. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  26. package/dist/assets/diagram-7IWD3JNH-BTL_j23J.js +30 -0
  27. package/dist/assets/diagram-B4RE2ZJO-Cf1Norgu.js +3 -0
  28. package/dist/assets/diagram-LBJQPF4R-DPiUEKr-.js +24 -0
  29. package/dist/assets/diagram-Q27KOJAE-CTcTpiz5.js +24 -0
  30. package/dist/assets/diagram-UB23O5K3-DPMSdYm0.js +41 -0
  31. package/dist/assets/ebnfDiagram-BXEA7PRR-7yvD6uiF.js +1 -0
  32. package/dist/assets/erDiagram-JOGREHBK-CfgeDoV6.js +85 -0
  33. package/dist/assets/flowDiagram-UKHOOZJN-B27ng5zu.js +156 -0
  34. package/dist/assets/ganttDiagram-PKOTCBZU-CcC6b_7t.js +292 -0
  35. package/dist/assets/gitGraphDiagram-DS77QQ5N-BX5kPGrO.js +106 -0
  36. package/dist/assets/graph-Cx-EqisW.js +1 -0
  37. package/dist/assets/index-BTDHQ6Id.css +32 -0
  38. package/dist/assets/index-sFfz8iBR.js +4931 -0
  39. package/dist/assets/infoDiagram-6WML65LV-CUP-SlN4.js +2 -0
  40. package/dist/assets/init-Gi6I4Gst.js +1 -0
  41. package/dist/assets/ishikawaDiagram-WSZJBQD7-BTTDhOpX.js +70 -0
  42. package/dist/assets/journeyDiagram-NVQOT4AX-ARXcc61m.js +139 -0
  43. package/dist/assets/kanban-definition-27J2QSJJ-BieMpqMA.js +89 -0
  44. package/dist/assets/layout-D6BstSzF.js +1 -0
  45. package/dist/assets/linear-61vxjRnP.js +1 -0
  46. package/dist/assets/map-CV9uif0g.js +1 -0
  47. package/dist/assets/mermaid.core-nhnwkfQx.js +312 -0
  48. package/dist/assets/mindmap-definition-FAOFIHXS-Cm6x5vT1.js +96 -0
  49. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  50. package/dist/assets/pegDiagram-VL7TDLO6-CcYBq_9H.js +1 -0
  51. package/dist/assets/pieDiagram-7S7Q4E2Y-CfvIjfzo.js +39 -0
  52. package/dist/assets/quadrantDiagram-CIZ2JOQS-SgYoWPNb.js +7 -0
  53. package/dist/assets/railroadDiagram-AXF67PYL-D2Mu5E_f.js +1 -0
  54. package/dist/assets/requirementDiagram-LRYGKXZP-Azwe_k4Q.js +84 -0
  55. package/dist/assets/sankeyDiagram-W5VNT64P-DN1Ju3PA.js +40 -0
  56. package/dist/assets/sequenceDiagram-SI44F4Z6-Bj4qC34S.js +162 -0
  57. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  58. package/dist/assets/sizeCapture-X5ZJPWSS-Co4PdklG.js +1 -0
  59. package/dist/assets/stateDiagram-OKZ733FA-nOvL9dRC.js +1 -0
  60. package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
  61. package/dist/assets/swimlanes-SLNWSIFB-CCK3wMid.js +2 -0
  62. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
  63. package/dist/assets/timeline-definition-Z64GVDOM-BC29RNFm.js +120 -0
  64. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  65. package/dist/assets/vennDiagram-T6HMQDX7-pZlZCtEo.js +34 -0
  66. package/dist/assets/wardleyDiagram-T6FBY63Y-De8k2-zM.js +78 -0
  67. package/dist/assets/xychartDiagram-ELKLHX3M-Bk8ykH0w.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/dist-server/server/index.js +8 -5
  70. package/dist-server/server/index.js.map +1 -1
  71. package/dist-server/server/modules/agent/agent.module.js +2 -1
  72. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  73. package/dist-server/server/modules/agent/agent.routes.js +2 -2
  74. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  75. package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
  76. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  77. package/dist-server/server/modules/auth/auth.middleware.js +1 -1
  78. package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
  79. package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
  80. package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
  81. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  82. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  83. package/dist-server/server/modules/browser-use/browser-use.service.js +3 -6
  84. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  85. package/dist-server/server/modules/browser-use/index.js +2 -0
  86. package/dist-server/server/modules/browser-use/index.js.map +1 -1
  87. package/dist-server/server/modules/cli/cli.js +9 -3
  88. package/dist-server/server/modules/cli/cli.js.map +1 -1
  89. package/dist-server/server/modules/cli/cli.module.js +0 -1
  90. package/dist-server/server/modules/cli/cli.module.js.map +1 -1
  91. package/dist-server/server/modules/cli/cli.service.js +0 -18
  92. package/dist-server/server/modules/cli/cli.service.js.map +1 -1
  93. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
  94. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
  95. package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
  96. package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -1
  97. package/dist-server/server/modules/database/connection.js +1 -1
  98. package/dist-server/server/modules/database/connection.js.map +1 -1
  99. package/dist-server/server/modules/database/index.js +5 -0
  100. package/dist-server/server/modules/database/index.js.map +1 -1
  101. package/dist-server/server/modules/database/migrations.js +32 -1
  102. package/dist-server/server/modules/database/migrations.js.map +1 -1
  103. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  104. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  105. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  106. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  107. package/dist-server/server/modules/database/repositories/sessions.db.js +137 -5
  108. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  109. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  110. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  111. package/dist-server/server/modules/database/schema.js +92 -0
  112. package/dist-server/server/modules/database/schema.js.map +1 -1
  113. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  114. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  115. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  116. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  117. package/dist-server/server/modules/file-tree/file-tree.service.js +18 -10
  118. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  119. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +62 -11
  120. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  121. package/dist-server/server/modules/git/git-branch.service.js +15 -0
  122. package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
  123. package/dist-server/server/modules/git/git.routes.js +15 -7
  124. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  125. package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
  126. package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
  127. package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
  128. package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
  129. package/dist-server/server/modules/notifications/index.js +3 -1
  130. package/dist-server/server/modules/notifications/index.js.map +1 -1
  131. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
  132. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  133. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  134. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  135. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  136. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  137. package/dist-server/server/modules/providers/index.js +3 -0
  138. package/dist-server/server/modules/providers/index.js.map +1 -1
  139. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +20 -1
  140. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  141. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  142. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  143. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +35 -6
  144. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  145. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +422 -86
  146. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  147. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
  148. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  149. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  150. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  151. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  152. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  153. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  155. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  156. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  157. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +16 -0
  158. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  159. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +60 -49
  160. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  161. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  162. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  163. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1565 -432
  164. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  165. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  166. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  167. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  168. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  169. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  170. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +258 -6
  171. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  172. package/dist-server/server/modules/providers/provider.routes.js +29 -0
  173. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  174. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
  175. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  176. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  177. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  178. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +63 -7
  179. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  180. package/dist-server/server/modules/providers/services/session-history-cache.service.js +87 -0
  181. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  182. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  183. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  184. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  185. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  186. package/dist-server/server/modules/providers/services/sessions.service.js +179 -9
  187. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  188. package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
  189. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  190. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  191. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  192. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +465 -39
  193. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  194. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  195. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  196. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  197. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  198. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  199. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  200. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
  201. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  202. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  203. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  204. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +304 -15
  205. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  206. package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
  207. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  208. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  209. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  210. package/dist-server/server/modules/providers/tests/provider.routes.test.js +35 -0
  211. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  212. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  213. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  214. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
  215. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  216. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  217. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  218. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  219. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  220. package/dist-server/server/modules/providers/tests/sessions.service.test.js +45 -1
  221. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  222. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  223. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  224. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  225. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  226. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  227. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  228. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
  229. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  230. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  231. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  232. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
  233. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  234. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  235. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  236. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  237. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  238. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  239. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  240. package/dist-server/server/modules/user/user.module.js +10 -1
  241. package/dist-server/server/modules/user/user.module.js.map +1 -1
  242. package/dist-server/server/modules/user/user.routes.js +44 -0
  243. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  244. package/dist-server/server/modules/user/user.service.js +75 -0
  245. package/dist-server/server/modules/user/user.service.js.map +1 -1
  246. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  247. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  248. package/dist-server/server/modules/websocket/index.js +7 -0
  249. package/dist-server/server/modules/websocket/index.js.map +1 -1
  250. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +10 -44
  251. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  252. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  253. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  254. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +166 -9
  255. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  256. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  257. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  258. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -3
  259. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  260. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  261. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  262. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  263. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  264. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  265. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  266. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +80 -0
  267. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  268. package/dist-server/server/shared/claude-cli-path.js +22 -5
  269. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  270. package/dist-server/server/shared/index.js +3 -0
  271. package/dist-server/server/shared/index.js.map +1 -0
  272. package/dist-server/server/shared/message-unification.js +376 -0
  273. package/dist-server/server/shared/message-unification.js.map +1 -0
  274. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  275. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  276. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  277. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  278. package/dist-server/server/shared/utils.js +42 -0
  279. package/dist-server/server/shared/utils.js.map +1 -1
  280. package/package.json +23 -16
  281. package/scripts/promote-dist-server.mjs +50 -0
  282. package/server/index.ts +12 -6
  283. package/server/modules/agent/agent.module.ts +2 -1
  284. package/server/modules/agent/agent.routes.ts +2 -2
  285. package/server/modules/agent/tests/agent.routes.test.ts +33 -1
  286. package/server/modules/auth/auth.middleware.ts +2 -2
  287. package/server/modules/browser-use/browser-use-runtime.ts +10 -0
  288. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  289. package/server/modules/browser-use/browser-use.service.ts +5 -8
  290. package/server/modules/browser-use/index.ts +3 -0
  291. package/server/modules/cli/cli.module.ts +0 -1
  292. package/server/modules/cli/cli.service.ts +0 -20
  293. package/server/modules/cli/cli.ts +9 -3
  294. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
  295. package/server/modules/cli/tests/cli.service.test.ts +0 -3
  296. package/server/modules/database/connection.ts +2 -1
  297. package/server/modules/database/index.ts +10 -0
  298. package/server/modules/database/migrations.ts +37 -0
  299. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  300. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  301. package/server/modules/database/repositories/sessions.db.ts +196 -3
  302. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  303. package/server/modules/database/schema.ts +96 -0
  304. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  305. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  306. package/server/modules/file-tree/file-tree.service.ts +21 -10
  307. package/server/modules/file-tree/tests/file-tree.service.test.ts +65 -11
  308. package/server/modules/git/git-branch.service.ts +43 -0
  309. package/server/modules/git/git.routes.ts +16 -9
  310. package/server/modules/git/tests/git-branch.service.test.ts +60 -0
  311. package/server/modules/git/tests/git-init.routes.test.ts +83 -0
  312. package/server/modules/notifications/index.ts +2 -0
  313. package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
  314. package/server/modules/projects/services/project-delete.service.ts +13 -1
  315. package/server/modules/projects/services/project-star.service.ts +2 -2
  316. package/server/modules/providers/index.ts +4 -0
  317. package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
  318. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  319. package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
  320. package/server/modules/providers/list/claude/claude-runtime.provider.js +463 -93
  321. package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
  322. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  323. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  324. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  325. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  326. package/server/modules/providers/list/codex/codex-models.provider.ts +16 -0
  327. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
  328. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  329. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
  330. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  331. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  332. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  333. package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
  334. package/server/modules/providers/provider.routes.ts +36 -0
  335. package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
  336. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  337. package/server/modules/providers/services/session-conversations-search.service.ts +94 -7
  338. package/server/modules/providers/services/session-history-cache.service.ts +139 -0
  339. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  340. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  341. package/server/modules/providers/services/sessions.service.ts +201 -9
  342. package/server/modules/providers/tests/claude-auth.test.ts +58 -0
  343. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  344. package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
  345. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  346. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  347. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  348. package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
  349. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  350. package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
  351. package/server/modules/providers/tests/opencode-models.test.ts +166 -8
  352. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  353. package/server/modules/providers/tests/provider.routes.test.ts +61 -0
  354. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  355. package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
  356. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  357. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  358. package/server/modules/providers/tests/sessions.service.test.ts +67 -1
  359. package/server/modules/providers/tests/skills.test.ts +21 -5
  360. package/server/modules/scheduled-messages/index.ts +8 -0
  361. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  362. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
  363. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  364. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
  365. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  366. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  367. package/server/modules/user/tests/user.service.test.ts +90 -0
  368. package/server/modules/user/user.module.ts +10 -1
  369. package/server/modules/user/user.routes.ts +44 -0
  370. package/server/modules/user/user.service.ts +105 -0
  371. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  372. package/server/modules/websocket/index.ts +8 -0
  373. package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
  374. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  375. package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
  376. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  377. package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
  378. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  379. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  380. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  381. package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
  382. package/server/shared/claude-cli-path.ts +24 -6
  383. package/server/shared/index.ts +3 -0
  384. package/server/shared/interfaces.ts +62 -0
  385. package/server/shared/message-unification.ts +449 -0
  386. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  387. package/server/shared/tests/message-unification.test.ts +218 -0
  388. package/server/shared/types.ts +123 -3
  389. package/server/shared/utils.ts +50 -0
  390. package/server/tsconfig.json +6 -2
  391. package/README.de.md +0 -257
  392. package/README.ja.md +0 -249
  393. package/README.ko.md +0 -249
  394. package/README.ru.md +0 -257
  395. package/README.tr.md +0 -258
  396. package/README.zh-CN.md +0 -249
  397. package/README.zh-TW.md +0 -249
  398. package/dist/assets/index-D3jfzuKN.css +0 -32
  399. package/dist/assets/index-DTMWjJj8.js +0 -1550
  400. package/dist-server/server/constants/config.js +0 -6
  401. package/dist-server/server/constants/config.js.map +0 -1
  402. package/server/constants/config.ts +0 -5
@@ -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
+ });
@@ -11,9 +11,13 @@ import type {
11
11
  } from '@/shared/types.js';
12
12
  import { AppError, FORBIDDEN_WORKSPACE_PATHS, normalizeProjectPath } from '@/shared/utils.js';
13
13
 
14
+ const HARD_EXCLUDED_DIRECTORY_NAMES = new Set([
15
+ 'node_modules', '.git', '.svn', '.hg',
16
+ ]);
17
+
14
18
  const IGNORED_DIRECTORY_NAMES = new Set([
15
- 'node_modules', 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
16
- '.git', '.svn', '.hg',
19
+ ...HARD_EXCLUDED_DIRECTORY_NAMES,
20
+ 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
17
21
  '__pycache__', '.pytest_cache', '.mypy_cache', '.tox', 'venv', '.venv',
18
22
  'target', 'vendor',
19
23
  '.gradle', '.idea', 'coverage', '.nyc_output',
@@ -36,6 +40,15 @@ const MAXIMUM_FILE_TREE_ENTRIES = 10_000;
36
40
 
37
41
  type FileTreeEntryFilter = (entryPath: string, isDirectory: boolean) => boolean;
38
42
 
43
+ function includeEntryByHardExclusions(entryPath: string, isDirectory: boolean): boolean {
44
+ return !isDirectory || !HARD_EXCLUDED_DIRECTORY_NAMES.has(path.basename(entryPath));
45
+ }
46
+
47
+ function includeEntryByFallbackDirectoryNames(entryPath: string, isDirectory: boolean): boolean {
48
+ return includeEntryByHardExclusions(entryPath, isDirectory)
49
+ && (!isDirectory || !IGNORED_DIRECTORY_NAMES.has(path.basename(entryPath)));
50
+ }
51
+
39
52
  function createFileTreeError(message: string, statusCode: number, code: string): AppError {
40
53
  return new AppError(message, { statusCode, code });
41
54
  }
@@ -146,6 +159,8 @@ function createGitignoreEntryFilter(
146
159
  const gitignore = ignore().add(gitignoreContent);
147
160
 
148
161
  return (entryPath, isDirectory) => {
162
+ if (!includeEntryByHardExclusions(entryPath, isDirectory)) return false;
163
+
149
164
  const relativePath = path.relative(projectRoot, entryPath).split(path.sep).join('/');
150
165
  const matchPath = isDirectory ? `${relativePath}/` : relativePath;
151
166
  return !gitignore.ignores(matchPath);
@@ -191,17 +206,13 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
191
206
  try {
192
207
  for await (const entry of fileSystem.openDirectory(directoryPath)) {
193
208
  const isDirectory = entry.isDirectory();
194
- if (isDirectory && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
195
- continue;
196
- }
197
209
  if (!includeEntry(path.join(directoryPath, entry.name), isDirectory)) {
198
210
  continue;
199
211
  }
200
212
 
201
- // Charged after ignore filtering so generated directories and
202
- // .gitignore exclusions never consume the budget. Every recursive
203
- // branch shares one counter, so the cap applies to the whole tree
204
- // rather than per directory.
213
+ // Charged after visibility filtering so ignored entries never consume
214
+ // the budget. Every recursive branch shares one counter, so the cap
215
+ // applies to the whole tree rather than per directory.
205
216
  if (remainingEntries.value <= 0) {
206
217
  throw createFileTreeError(
207
218
  `Project file tree exceeds the ${MAXIMUM_FILE_TREE_ENTRIES.toLocaleString()} entry limit. Choose a narrower project directory or add ignore rules.`,
@@ -233,7 +244,7 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
233
244
  directoryPath: string,
234
245
  maximumDepth: number,
235
246
  currentDepth = 0,
236
- includeEntry: FileTreeEntryFilter = () => true,
247
+ includeEntry: FileTreeEntryFilter = includeEntryByFallbackDirectoryNames,
237
248
  remainingEntries = { value: MAXIMUM_FILE_TREE_ENTRIES },
238
249
  ): Promise<FileTreeNode[]> {
239
250
  const visibleEntries = await collectVisibleEntries(directoryPath, includeEntry, remainingEntries);
@@ -87,37 +87,67 @@ function createDependencies(
87
87
  };
88
88
  }
89
89
 
90
- test('listProjectFiles builds a sorted tree and skips generated directories', async () => {
90
+ test('listProjectFiles applies gitignore alongside hard directory exclusions', async () => {
91
91
  const projectRoot = path.resolve('file-tree-test-project');
92
+ const documentationDirectory = path.join(projectRoot, 'docs');
93
+ const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
94
+ const gitDirectory = path.join(projectRoot, '.git');
95
+ const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
92
96
  const sourceDirectory = path.join(projectRoot, 'src');
97
+ const readDirectories: string[] = [];
93
98
  const fileSystem = createFakeFileSystem({
94
99
  access: async () => undefined,
100
+ readTextFile: async (filePath) => {
101
+ assert.equal(filePath, path.join(projectRoot, '.gitignore'));
102
+ return '*.log';
103
+ },
95
104
  openDirectory: createDirectoryReader((directoryPath) => {
105
+ readDirectories.push(directoryPath);
96
106
  if (directoryPath === projectRoot) {
97
107
  return [
108
+ createDirectoryEntry('.git', true),
98
109
  createDirectoryEntry('node_modules', true),
99
110
  createDirectoryEntry('README.md', false),
111
+ createDirectoryEntry('docs', true),
100
112
  createDirectoryEntry('src', true),
101
113
  ];
102
114
  }
115
+ if (directoryPath === documentationDirectory) {
116
+ return [createDirectoryEntry('build', true)];
117
+ }
118
+ if (directoryPath === buildDocumentationDirectory) {
119
+ return [createDirectoryEntry('foo.md', false)];
120
+ }
103
121
  if (directoryPath === sourceDirectory) {
104
122
  return [createDirectoryEntry('index.ts', false)];
105
123
  }
106
124
  return [];
107
125
  }),
108
- lstat: async (candidatePath) => createStats(candidatePath === sourceDirectory, 0o754),
126
+ lstat: async (candidatePath) => createStats(
127
+ candidatePath === documentationDirectory
128
+ || candidatePath === buildDocumentationDirectory
129
+ || candidatePath === sourceDirectory
130
+ || candidatePath === gitDirectory
131
+ || candidatePath === nodeModulesDirectory,
132
+ 0o754,
133
+ ),
109
134
  });
110
135
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
111
136
 
112
- const tree = await service.listProjectFiles('project-1');
137
+ const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
113
138
 
114
- assert.deepEqual(tree.map((entry) => entry.name), ['src', 'README.md']);
115
- const sourceEntry = tree[0];
139
+ assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'src', 'README.md']);
140
+ const documentationEntry = tree[0];
141
+ assert.deepEqual(documentationEntry?.children?.map((entry) => entry.name), ['build']);
142
+ assert.deepEqual(documentationEntry?.children?.[0]?.children?.map((entry) => entry.name), ['foo.md']);
143
+ const sourceEntry = tree[1];
116
144
  assert.ok(sourceEntry);
117
145
  assert.equal(sourceEntry.type, 'directory');
118
146
  assert.equal(sourceEntry.permissions, '754');
119
147
  assert.equal(sourceEntry.permissionsRwx, 'rwxr-xr--');
120
148
  assert.deepEqual(sourceEntry.children?.map((entry) => entry.name), ['index.ts']);
149
+ assert.equal(readDirectories.includes(gitDirectory), false);
150
+ assert.equal(readDirectories.includes(nodeModulesDirectory), false);
121
151
  });
122
152
 
123
153
  test('listProjectFiles excludes gitignored entries only when requested', async () => {
@@ -167,23 +197,47 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
167
197
  assert.equal(readDirectories.includes(cacheDirectory), false);
168
198
  });
169
199
 
170
- test('listProjectFiles returns the normal tree when no gitignore exists', async () => {
200
+ test('listProjectFiles falls back to conventional directory names when no gitignore exists', async () => {
171
201
  const projectRoot = path.resolve('file-tree-test-project');
202
+ const documentationDirectory = path.join(projectRoot, 'docs');
203
+ const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
204
+ const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
205
+ const readDirectories: string[] = [];
172
206
  const fileSystem = createFakeFileSystem({
173
207
  access: async () => undefined,
174
208
  readTextFile: async () => {
175
209
  throw Object.assign(new Error('missing'), { code: 'ENOENT' });
176
210
  },
177
- openDirectory: createDirectoryReader((directoryPath) => directoryPath === projectRoot
178
- ? [createDirectoryEntry('debug.log', false)]
179
- : []),
180
- lstat: async () => createStats(false, 0o644),
211
+ openDirectory: createDirectoryReader((directoryPath) => {
212
+ readDirectories.push(directoryPath);
213
+ if (directoryPath === projectRoot) {
214
+ return [
215
+ createDirectoryEntry('debug.log', false),
216
+ createDirectoryEntry('docs', true),
217
+ createDirectoryEntry('node_modules', true),
218
+ ];
219
+ }
220
+ if (directoryPath === documentationDirectory) {
221
+ return [
222
+ createDirectoryEntry('build', true),
223
+ createDirectoryEntry('guide.md', false),
224
+ ];
225
+ }
226
+ if (directoryPath === buildDocumentationDirectory) {
227
+ return [createDirectoryEntry('generated.md', false)];
228
+ }
229
+ return [];
230
+ }),
231
+ lstat: async (candidatePath) => createStats(candidatePath === documentationDirectory, 0o644),
181
232
  });
182
233
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
183
234
 
184
235
  const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
185
236
 
186
- assert.deepEqual(tree.map((entry) => entry.name), ['debug.log']);
237
+ assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'debug.log']);
238
+ assert.deepEqual(tree[0]?.children?.map((entry) => entry.name), ['guide.md']);
239
+ assert.equal(readDirectories.includes(nodeModulesDirectory), false);
240
+ assert.equal(readDirectories.includes(buildDocumentationDirectory), false);
187
241
  });
188
242
 
189
243
  test('listProjectFiles rejects a tree that exceeds the server entry limit', async () => {
@@ -0,0 +1,43 @@
1
+ import { AppError } from '@/shared/utils.js';
2
+
3
+ type GitCommandResult = {
4
+ stdout: string;
5
+ stderr: string;
6
+ };
7
+
8
+ type GitCommandRunner = (
9
+ command: string,
10
+ args: string[],
11
+ options: { cwd: string },
12
+ ) => Promise<GitCommandResult>;
13
+
14
+ type DeleteLocalBranchInput = {
15
+ projectPath: string;
16
+ branch: string;
17
+ force: boolean;
18
+ runCommand: GitCommandRunner;
19
+ };
20
+
21
+ /** Used by the Git routes module to safely delete a non-current local branch. */
22
+ export async function deleteLocalBranch(input: DeleteLocalBranchInput): Promise<string> {
23
+ const { stdout: currentBranch } = await input.runCommand(
24
+ 'git',
25
+ ['branch', '--show-current'],
26
+ { cwd: input.projectPath },
27
+ );
28
+
29
+ if (currentBranch.trim() === input.branch) {
30
+ throw new AppError('Cannot delete the currently checked-out branch', {
31
+ code: 'GIT_CURRENT_BRANCH_DELETE',
32
+ statusCode: 400,
33
+ });
34
+ }
35
+
36
+ const deleteFlag = input.force ? '-D' : '-d';
37
+ const { stdout } = await input.runCommand(
38
+ 'git',
39
+ ['branch', deleteFlag, '--', input.branch],
40
+ { cwd: input.projectPath },
41
+ );
42
+ return stdout;
43
+ }
@@ -8,6 +8,7 @@ import { AppError } from '@/shared/utils.js';
8
8
 
9
9
  // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
10
10
  import { parseGitLogWithStats, parseGitStatusOutput } from './git-parsing.service.js';
11
+ import { deleteLocalBranch } from './git-branch.service.js';
11
12
 
12
13
  type GitRouterDependencies = {
13
14
  fileSystem: typeof import('node:fs/promises');
@@ -839,26 +840,32 @@ router.post('/create-branch', async (req, res) => {
839
840
 
840
841
  // Delete a local branch
841
842
  router.post('/delete-branch', async (req, res) => {
842
- const { project, branch } = req.body;
843
+ const { project, branch, force = false } = req.body;
843
844
 
844
845
  if (!project || !branch) {
845
846
  return res.status(400).json({ error: 'Project id and branch name are required' });
846
847
  }
847
848
 
849
+ if (typeof force !== 'boolean') {
850
+ return res.status(400).json({ error: 'force must be a boolean' });
851
+ }
852
+
848
853
  try {
849
854
  const projectPath = await getActualProjectPath(project);
850
855
  await validateGitRepository(projectPath);
851
-
852
- // Safety: cannot delete the currently checked-out branch
853
- const { stdout: currentBranch } = await spawnAsync('git', ['branch', '--show-current'], { cwd: projectPath });
854
- if (currentBranch.trim() === branch) {
855
- return res.status(400).json({ error: 'Cannot delete the currently checked-out branch' });
856
- }
857
-
858
- const { stdout } = await spawnAsync('git', ['branch', '-d', branch], { cwd: projectPath });
856
+ validateBranchName(branch);
857
+ const stdout = await deleteLocalBranch({
858
+ projectPath,
859
+ branch,
860
+ force,
861
+ runCommand: spawnAsync,
862
+ });
859
863
  res.json({ success: true, output: stdout });
860
864
  } catch (error) {
861
865
  console.error('Git delete branch error:', error);
866
+ if (error instanceof AppError) {
867
+ return res.status(error.statusCode).json({ error: error.message });
868
+ }
862
869
  res.status(500).json({ error: error.message });
863
870
  }
864
871
  });
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { deleteLocalBranch } from '../git-branch.service.js';
5
+
6
+ test('deleteLocalBranch uses safe deletion by default', async () => {
7
+ const commands: string[][] = [];
8
+ const output = await deleteLocalBranch({
9
+ projectPath: '/workspace/repo',
10
+ branch: 'feature/safe-delete',
11
+ force: false,
12
+ runCommand: async (_command, args) => {
13
+ commands.push(args);
14
+ return {
15
+ stdout: args.includes('--show-current') ? 'main\n' : 'Deleted branch feature/safe-delete.\n',
16
+ stderr: '',
17
+ };
18
+ },
19
+ });
20
+
21
+ assert.equal(output, 'Deleted branch feature/safe-delete.\n');
22
+ assert.deepEqual(commands, [
23
+ ['branch', '--show-current'],
24
+ ['branch', '-d', '--', 'feature/safe-delete'],
25
+ ]);
26
+ });
27
+
28
+ test('deleteLocalBranch uses force deletion only when requested', async () => {
29
+ const commands: string[][] = [];
30
+ await deleteLocalBranch({
31
+ projectPath: '/workspace/repo',
32
+ branch: 'feature/unmerged',
33
+ force: true,
34
+ runCommand: async (_command, args) => {
35
+ commands.push(args);
36
+ return { stdout: args.includes('--show-current') ? 'main\n' : '', stderr: '' };
37
+ },
38
+ });
39
+
40
+ assert.deepEqual(commands, [
41
+ ['branch', '--show-current'],
42
+ ['branch', '-D', '--', 'feature/unmerged'],
43
+ ]);
44
+ });
45
+
46
+ test('deleteLocalBranch refuses to delete the current branch even when forced', async () => {
47
+ await assert.rejects(
48
+ deleteLocalBranch({
49
+ projectPath: '/workspace/repo',
50
+ branch: 'feature/current',
51
+ force: true,
52
+ runCommand: async () => ({ stdout: 'feature/current\n', stderr: '' }),
53
+ }),
54
+ (error: unknown) => {
55
+ assert.equal((error as { code?: string }).code, 'GIT_CURRENT_BRANCH_DELETE');
56
+ assert.equal((error as { statusCode?: number }).statusCode, 400);
57
+ return true;
58
+ },
59
+ );
60
+ });