@cloudcli-ai/cloudcli 1.29.5 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.de.md +1 -1
  2. package/README.ja.md +1 -1
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/README.ru.md +1 -1
  6. package/README.tr.md +252 -0
  7. package/README.zh-CN.md +1 -1
  8. package/dist/assets/index-DF8TTVHl.css +32 -0
  9. package/dist/assets/index-DbZAcc9z.js +1262 -0
  10. package/dist/index.html +2 -2
  11. package/dist-server/server/claude-sdk.js +13 -8
  12. package/dist-server/server/claude-sdk.js.map +1 -1
  13. package/dist-server/server/cursor-cli.js +7 -7
  14. package/dist-server/server/cursor-cli.js.map +1 -1
  15. package/dist-server/server/gemini-cli.js +5 -5
  16. package/dist-server/server/gemini-cli.js.map +1 -1
  17. package/dist-server/server/gemini-response-handler.js +2 -2
  18. package/dist-server/server/gemini-response-handler.js.map +1 -1
  19. package/dist-server/server/index.js +139 -977
  20. package/dist-server/server/index.js.map +1 -1
  21. package/dist-server/server/middleware/auth.js +1 -1
  22. package/dist-server/server/middleware/auth.js.map +1 -1
  23. package/dist-server/server/modules/database/connection.js +125 -0
  24. package/dist-server/server/modules/database/connection.js.map +1 -0
  25. package/dist-server/server/modules/database/index.js +13 -0
  26. package/dist-server/server/modules/database/index.js.map +1 -0
  27. package/dist-server/server/modules/database/init-db.js +18 -0
  28. package/dist-server/server/modules/database/init-db.js.map +1 -0
  29. package/dist-server/server/modules/database/migrations.js +370 -0
  30. package/dist-server/server/modules/database/migrations.js.map +1 -0
  31. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  32. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  33. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  34. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  35. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  36. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  37. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  38. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  39. package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
  40. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  41. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
  42. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
  43. package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
  44. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  45. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  46. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  47. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  48. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  49. package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
  50. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  51. package/dist-server/server/modules/database/repositories/users.js +88 -0
  52. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  53. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  54. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  55. package/dist-server/server/modules/database/schema.js +143 -0
  56. package/dist-server/server/modules/database/schema.js.map +1 -0
  57. package/dist-server/server/modules/projects/index.js +4 -0
  58. package/dist-server/server/modules/projects/index.js.map +1 -0
  59. package/dist-server/server/modules/projects/projects.routes.js +188 -0
  60. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  61. package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
  62. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  63. package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
  64. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  65. package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
  66. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  67. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  68. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  69. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
  70. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  71. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
  72. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  73. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
  74. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  75. package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
  76. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  77. package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
  78. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  79. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
  80. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  81. package/dist-server/server/modules/providers/index.js +4 -0
  82. package/dist-server/server/modules/providers/index.js.map +1 -0
  83. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +103 -0
  84. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -0
  85. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js +103 -0
  86. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js.map +1 -0
  87. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
  88. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  89. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +403 -0
  90. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -0
  91. package/dist-server/server/modules/providers/list/claude/claude.provider.js +15 -0
  92. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -0
  93. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +84 -0
  94. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -0
  95. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js +107 -0
  96. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js.map +1 -0
  97. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
  98. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  99. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +494 -0
  100. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -0
  101. package/dist-server/server/modules/providers/list/codex/codex.provider.js +15 -0
  102. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -0
  103. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +118 -0
  104. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -0
  105. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js +80 -0
  106. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js.map +1 -0
  107. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
  108. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  109. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +369 -0
  110. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -0
  111. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +15 -0
  112. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -0
  113. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +131 -0
  114. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -0
  115. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +82 -0
  116. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +1 -0
  117. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
  118. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
  119. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +474 -0
  120. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -0
  121. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +15 -0
  122. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -0
  123. package/dist-server/server/modules/providers/provider.registry.js +31 -0
  124. package/dist-server/server/modules/providers/provider.registry.js.map +1 -0
  125. package/dist-server/server/modules/providers/provider.routes.js +331 -0
  126. package/dist-server/server/modules/providers/provider.routes.js.map +1 -0
  127. package/dist-server/server/modules/providers/services/mcp.service.js +69 -0
  128. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -0
  129. package/dist-server/server/modules/providers/services/provider-auth.service.js +25 -0
  130. package/dist-server/server/modules/providers/services/provider-auth.service.js.map +1 -0
  131. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
  132. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  133. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
  134. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  135. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
  136. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  137. package/dist-server/server/modules/providers/services/sessions.service.js +104 -0
  138. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -0
  139. package/dist-server/server/modules/providers/shared/base/abstract.provider.js +14 -0
  140. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -0
  141. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js +102 -0
  142. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js.map +1 -0
  143. package/dist-server/server/modules/providers/tests/mcp.test.js +250 -0
  144. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -0
  145. package/dist-server/server/modules/websocket/index.js +3 -0
  146. package/dist-server/server/modules/websocket/index.js.map +1 -0
  147. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
  148. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  149. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  150. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  151. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
  152. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  153. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
  154. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  155. package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
  156. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  157. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  158. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  159. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  160. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  161. package/dist-server/server/openai-codex.js +5 -5
  162. package/dist-server/server/openai-codex.js.map +1 -1
  163. package/dist-server/server/routes/agent.js +10 -17
  164. package/dist-server/server/routes/agent.js.map +1 -1
  165. package/dist-server/server/routes/auth.js +3 -1
  166. package/dist-server/server/routes/auth.js.map +1 -1
  167. package/dist-server/server/routes/commands.js +1 -45
  168. package/dist-server/server/routes/commands.js.map +1 -1
  169. package/dist-server/server/routes/cursor.js +9 -489
  170. package/dist-server/server/routes/cursor.js.map +1 -1
  171. package/dist-server/server/routes/gemini.js +2 -2
  172. package/dist-server/server/routes/gemini.js.map +1 -1
  173. package/dist-server/server/routes/git.js +33 -29
  174. package/dist-server/server/routes/git.js.map +1 -1
  175. package/dist-server/server/routes/mcp-utils.js +1 -18
  176. package/dist-server/server/routes/mcp-utils.js.map +1 -1
  177. package/dist-server/server/routes/settings.js +11 -1
  178. package/dist-server/server/routes/settings.js.map +1 -1
  179. package/dist-server/server/routes/taskmaster.js +97 -569
  180. package/dist-server/server/routes/taskmaster.js.map +1 -1
  181. package/dist-server/server/routes/user.js +1 -1
  182. package/dist-server/server/routes/user.js.map +1 -1
  183. package/dist-server/server/services/notification-orchestrator.js +2 -2
  184. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  185. package/dist-server/server/services/vapid-keys.js +2 -1
  186. package/dist-server/server/services/vapid-keys.js.map +1 -1
  187. package/dist-server/server/shared/interfaces.js +2 -0
  188. package/dist-server/server/shared/interfaces.js.map +1 -0
  189. package/dist-server/server/shared/types.js +2 -0
  190. package/dist-server/server/shared/types.js.map +1 -0
  191. package/dist-server/server/shared/utils.js +551 -0
  192. package/dist-server/server/shared/utils.js.map +1 -0
  193. package/dist-server/server/utils/mcp-detector.js +0 -49
  194. package/dist-server/server/utils/mcp-detector.js.map +1 -1
  195. package/dist-server/server/utils/taskmaster-websocket.js +18 -12
  196. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
  197. package/dist-server/shared/modelConstants.js +3 -1
  198. package/dist-server/shared/modelConstants.js.map +1 -1
  199. package/package.json +7 -2
  200. package/server/claude-sdk.js +14 -8
  201. package/server/cursor-cli.js +7 -7
  202. package/server/gemini-cli.js +5 -5
  203. package/server/gemini-response-handler.js +2 -2
  204. package/server/index.js +173 -1035
  205. package/server/middleware/auth.js +1 -1
  206. package/server/modules/database/connection.ts +143 -0
  207. package/server/modules/database/index.ts +12 -0
  208. package/server/modules/database/init-db.ts +17 -0
  209. package/server/modules/database/migrations.ts +442 -0
  210. package/server/modules/database/repositories/api-keys.ts +119 -0
  211. package/server/modules/database/repositories/app-config.ts +53 -0
  212. package/server/modules/database/repositories/credentials.ts +106 -0
  213. package/server/modules/database/repositories/github-tokens.ts +100 -0
  214. package/server/modules/database/repositories/notification-preferences.ts +103 -0
  215. package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
  216. package/server/modules/database/repositories/projects.db.ts +183 -0
  217. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  218. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  219. package/server/modules/database/repositories/sessions.db.ts +174 -0
  220. package/server/modules/database/repositories/users.ts +140 -0
  221. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  222. package/server/modules/database/schema.ts +152 -0
  223. package/server/modules/projects/index.ts +6 -0
  224. package/server/modules/projects/projects.routes.ts +247 -0
  225. package/server/modules/projects/services/project-clone.service.ts +321 -0
  226. package/server/modules/projects/services/project-delete.service.ts +75 -0
  227. package/server/modules/projects/services/project-management.service.ts +150 -0
  228. package/server/modules/projects/services/project-star.service.ts +78 -0
  229. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
  230. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
  231. package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
  232. package/server/modules/projects/tests/project-management.service.test.ts +117 -0
  233. package/server/modules/projects/tests/project-star.service.test.ts +123 -0
  234. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
  235. package/server/modules/providers/index.ts +4 -0
  236. package/server/modules/providers/list/claude/claude-auth.provider.ts +123 -0
  237. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -0
  238. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
  239. package/server/modules/providers/list/claude/claude-sessions.provider.ts +475 -0
  240. package/server/modules/providers/list/claude/claude.provider.ts +17 -0
  241. package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -0
  242. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -0
  243. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
  244. package/server/modules/providers/list/codex/codex-sessions.provider.ts +564 -0
  245. package/server/modules/providers/list/codex/codex.provider.ts +17 -0
  246. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -0
  247. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -0
  248. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
  249. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -0
  250. package/server/modules/providers/list/cursor/cursor.provider.ts +17 -0
  251. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +151 -0
  252. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -0
  253. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
  254. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +541 -0
  255. package/server/modules/providers/list/gemini/gemini.provider.ts +17 -0
  256. package/server/modules/providers/provider.registry.ts +36 -0
  257. package/server/modules/providers/provider.routes.ts +425 -0
  258. package/server/modules/providers/services/mcp.service.ts +94 -0
  259. package/server/modules/providers/services/provider-auth.service.ts +26 -0
  260. package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
  261. package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
  262. package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
  263. package/server/modules/providers/services/sessions.service.ts +130 -0
  264. package/server/modules/providers/shared/base/abstract.provider.ts +27 -0
  265. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -0
  266. package/server/modules/providers/tests/mcp.test.ts +293 -0
  267. package/server/modules/websocket/README.md +267 -0
  268. package/server/modules/websocket/index.ts +2 -0
  269. package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
  270. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  271. package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
  272. package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
  273. package/server/modules/websocket/services/websocket-server.service.ts +58 -0
  274. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  275. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  276. package/server/openai-codex.js +5 -5
  277. package/server/routes/agent.js +11 -16
  278. package/server/routes/auth.js +4 -2
  279. package/server/routes/commands.js +1 -50
  280. package/server/routes/cursor.js +15 -527
  281. package/server/routes/gemini.js +3 -2
  282. package/server/routes/git.js +33 -28
  283. package/server/routes/mcp-utils.js +2 -19
  284. package/server/routes/settings.js +11 -1
  285. package/server/routes/taskmaster.js +107 -602
  286. package/server/routes/user.js +1 -1
  287. package/server/services/notification-orchestrator.js +3 -2
  288. package/server/services/vapid-keys.js +2 -1
  289. package/server/shared/interfaces.ts +92 -0
  290. package/server/shared/types.ts +327 -0
  291. package/server/shared/utils.ts +669 -0
  292. package/server/tsconfig.json +6 -3
  293. package/server/utils/mcp-detector.js +0 -51
  294. package/server/utils/taskmaster-websocket.js +19 -13
  295. package/shared/modelConstants.js +3 -1
  296. package/dist/assets/index-BBAE1OJ_.css +0 -32
  297. package/dist/assets/index-Ci1GOyV7.js +0 -1262
  298. package/dist-server/server/database/db.js +0 -535
  299. package/dist-server/server/database/db.js.map +0 -1
  300. package/dist-server/server/database/schema.js +0 -97
  301. package/dist-server/server/database/schema.js.map +0 -1
  302. package/dist-server/server/projects.js +0 -2325
  303. package/dist-server/server/projects.js.map +0 -1
  304. package/dist-server/server/providers/claude/adapter.js +0 -266
  305. package/dist-server/server/providers/claude/adapter.js.map +0 -1
  306. package/dist-server/server/providers/claude/status.js +0 -121
  307. package/dist-server/server/providers/claude/status.js.map +0 -1
  308. package/dist-server/server/providers/codex/adapter.js +0 -232
  309. package/dist-server/server/providers/codex/adapter.js.map +0 -1
  310. package/dist-server/server/providers/codex/status.js +0 -70
  311. package/dist-server/server/providers/codex/status.js.map +0 -1
  312. package/dist-server/server/providers/cursor/adapter.js +0 -342
  313. package/dist-server/server/providers/cursor/adapter.js.map +0 -1
  314. package/dist-server/server/providers/cursor/status.js +0 -118
  315. package/dist-server/server/providers/cursor/status.js.map +0 -1
  316. package/dist-server/server/providers/gemini/adapter.js +0 -174
  317. package/dist-server/server/providers/gemini/adapter.js.map +0 -1
  318. package/dist-server/server/providers/gemini/status.js +0 -104
  319. package/dist-server/server/providers/gemini/status.js.map +0 -1
  320. package/dist-server/server/providers/registry.js +0 -58
  321. package/dist-server/server/providers/registry.js.map +0 -1
  322. package/dist-server/server/providers/types.js +0 -117
  323. package/dist-server/server/providers/types.js.map +0 -1
  324. package/dist-server/server/providers/utils.js +0 -28
  325. package/dist-server/server/providers/utils.js.map +0 -1
  326. package/dist-server/server/routes/cli-auth.js +0 -25
  327. package/dist-server/server/routes/cli-auth.js.map +0 -1
  328. package/dist-server/server/routes/codex.js +0 -294
  329. package/dist-server/server/routes/codex.js.map +0 -1
  330. package/dist-server/server/routes/mcp.js +0 -480
  331. package/dist-server/server/routes/mcp.js.map +0 -1
  332. package/dist-server/server/routes/messages.js +0 -56
  333. package/dist-server/server/routes/messages.js.map +0 -1
  334. package/dist-server/server/routes/projects.js +0 -505
  335. package/dist-server/server/routes/projects.js.map +0 -1
  336. package/server/database/db.js +0 -593
  337. package/server/database/schema.js +0 -102
  338. package/server/projects.js +0 -2555
  339. package/server/providers/claude/adapter.js +0 -278
  340. package/server/providers/claude/status.js +0 -136
  341. package/server/providers/codex/adapter.js +0 -248
  342. package/server/providers/codex/status.js +0 -78
  343. package/server/providers/cursor/adapter.js +0 -348
  344. package/server/providers/cursor/status.js +0 -128
  345. package/server/providers/gemini/adapter.js +0 -186
  346. package/server/providers/gemini/status.js +0 -111
  347. package/server/providers/registry.js +0 -67
  348. package/server/providers/types.js +0 -132
  349. package/server/providers/utils.js +0 -29
  350. package/server/routes/cli-auth.js +0 -27
  351. package/server/routes/codex.js +0 -329
  352. package/server/routes/mcp.js +0 -552
  353. package/server/routes/messages.js +0 -61
  354. package/server/routes/projects.js +0 -548
package/server/index.js CHANGED
@@ -1,251 +1,123 @@
1
1
  #!/usr/bin/env node
2
2
  // Load environment variables before other imports execute
3
3
  import './load-env.js';
4
- import fs from 'fs';
4
+ import fs, { promises as fsPromises } from 'fs';
5
5
  import path from 'path';
6
- import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
7
-
8
- const __dirname = getModuleDir(import.meta.url);
9
- // The server source runs from /server, while the compiled output runs from /dist-server/server.
10
- // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
11
- const APP_ROOT = findAppRoot(__dirname);
12
- const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
13
-
14
- import { c } from './utils/colors.js';
15
-
16
- console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
17
-
18
- import express from 'express';
19
- import { WebSocketServer, WebSocket } from 'ws';
20
6
  import os from 'os';
21
7
  import http from 'http';
22
- import cors from 'cors';
23
- import { promises as fsPromises } from 'fs';
24
8
  import { spawn } from 'child_process';
25
- import pty from 'node-pty';
26
- import fetch from 'node-fetch';
9
+
10
+ import express from 'express';
11
+ import cors from 'cors';
27
12
  import mime from 'mime-types';
28
13
 
29
- import { getProjects, getSessions, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js';
30
- import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
31
- import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
32
- import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
33
- import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
14
+ import { AppError, WORKSPACES_ROOT, validateWorkspacePath } from '@/shared/utils.js';
15
+ import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
16
+ import { createWebSocketServer } from '@/modules/websocket/index.js';
17
+
18
+ import { getConnectableHost } from '../shared/networkHosts.js';
19
+
20
+ import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
21
+ import {
22
+ queryClaudeSDK,
23
+ abortClaudeSDKSession,
24
+ isClaudeSDKSessionActive,
25
+ getActiveClaudeSDKSessions,
26
+ resolveToolApproval,
27
+ getPendingApprovalsForSession,
28
+ reconnectSessionWriter,
29
+ } from './claude-sdk.js';
30
+ import {
31
+ spawnCursor,
32
+ abortCursorSession,
33
+ isCursorSessionActive,
34
+ getActiveCursorSessions,
35
+ } from './cursor-cli.js';
36
+ import {
37
+ queryCodex,
38
+ abortCodexSession,
39
+ isCodexSessionActive,
40
+ getActiveCodexSessions,
41
+ } from './openai-codex.js';
42
+ import {
43
+ spawnGemini,
44
+ abortGeminiSession,
45
+ isGeminiSessionActive,
46
+ getActiveGeminiSessions,
47
+ } from './gemini-cli.js';
34
48
  import sessionManager from './sessionManager.js';
49
+ import {
50
+ stripAnsiSequences,
51
+ normalizeDetectedUrl,
52
+ extractUrlsFromText,
53
+ shouldAutoOpenUrlFromOutput,
54
+ } from './utils/url-detection.js';
35
55
  import gitRoutes from './routes/git.js';
36
56
  import authRoutes from './routes/auth.js';
37
- import mcpRoutes from './routes/mcp.js';
38
57
  import cursorRoutes from './routes/cursor.js';
39
58
  import taskmasterRoutes from './routes/taskmaster.js';
40
59
  import mcpUtilsRoutes from './routes/mcp-utils.js';
41
60
  import commandsRoutes from './routes/commands.js';
42
61
  import settingsRoutes from './routes/settings.js';
43
62
  import agentRoutes from './routes/agent.js';
44
- import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes/projects.js';
45
- import cliAuthRoutes from './routes/cli-auth.js';
63
+ import projectModuleRoutes from './modules/projects/projects.routes.js';
46
64
  import userRoutes from './routes/user.js';
47
- import codexRoutes from './routes/codex.js';
48
65
  import geminiRoutes from './routes/gemini.js';
49
66
  import pluginsRoutes from './routes/plugins.js';
50
- import messagesRoutes from './routes/messages.js';
51
- import { createNormalizedMessage } from './providers/types.js';
67
+ import providerRoutes from './modules/providers/provider.routes.js';
52
68
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
53
- import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
69
+ import { initializeDatabase, projectsDb } from './modules/database/index.js';
54
70
  import { configureWebPush } from './services/vapid-keys.js';
55
71
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
56
72
  import { IS_PLATFORM } from './constants/config.js';
57
- import { getConnectableHost } from '../shared/networkHosts.js';
58
-
59
- const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
60
-
61
- // File system watchers for provider project/session folders
62
- const PROVIDER_WATCH_PATHS = [
63
- { provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
64
- { provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
65
- { provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') },
66
- { provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
67
- { provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
68
- ];
69
- const WATCHER_IGNORED_PATTERNS = [
70
- '**/node_modules/**',
71
- '**/.git/**',
72
- '**/dist/**',
73
- '**/build/**',
74
- '**/*.tmp',
75
- '**/*.swp',
76
- '**/.DS_Store'
77
- ];
78
- const WATCHER_DEBOUNCE_MS = 300;
79
- let projectsWatchers = [];
80
- let projectsWatcherDebounceTimer = null;
81
- const connectedClients = new Set();
82
- let isGetProjectsRunning = false; // Flag to prevent reentrant calls
83
-
84
- // Broadcast progress to all connected WebSocket clients
85
- function broadcastProgress(progress) {
86
- const message = JSON.stringify({
87
- type: 'loading_progress',
88
- ...progress
89
- });
90
- connectedClients.forEach(client => {
91
- if (client.readyState === WebSocket.OPEN) {
92
- client.send(message);
93
- }
94
- });
95
- }
96
-
97
- // Setup file system watchers for Claude, Cursor, and Codex project/session folders
98
- async function setupProjectsWatcher() {
99
- const chokidar = (await import('chokidar')).default;
100
-
101
- if (projectsWatcherDebounceTimer) {
102
- clearTimeout(projectsWatcherDebounceTimer);
103
- projectsWatcherDebounceTimer = null;
104
- }
105
-
106
- await Promise.all(
107
- projectsWatchers.map(async (watcher) => {
108
- try {
109
- await watcher.close();
110
- } catch (error) {
111
- console.error('[WARN] Failed to close watcher:', error);
112
- }
113
- })
114
- );
115
- projectsWatchers = [];
116
-
117
- const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
118
- if (projectsWatcherDebounceTimer) {
119
- clearTimeout(projectsWatcherDebounceTimer);
120
- }
121
-
122
- projectsWatcherDebounceTimer = setTimeout(async () => {
123
- // Prevent reentrant calls
124
- if (isGetProjectsRunning) {
125
- return;
126
- }
127
-
128
- try {
129
- isGetProjectsRunning = true;
130
-
131
- // Clear project directory cache when files change
132
- clearProjectDirectoryCache();
133
-
134
- // Get updated projects list
135
- const updatedProjects = await getProjects(broadcastProgress);
136
-
137
- // Notify all connected clients about the project changes
138
- const updateMessage = JSON.stringify({
139
- type: 'projects_updated',
140
- projects: updatedProjects,
141
- timestamp: new Date().toISOString(),
142
- changeType: eventType,
143
- changedFile: path.relative(rootPath, filePath),
144
- watchProvider: provider
145
- });
146
-
147
- connectedClients.forEach(client => {
148
- if (client.readyState === WebSocket.OPEN) {
149
- client.send(updateMessage);
150
- }
151
- });
152
-
153
- } catch (error) {
154
- console.error('[ERROR] Error handling project changes:', error);
155
- } finally {
156
- isGetProjectsRunning = false;
157
- }
158
- }, WATCHER_DEBOUNCE_MS);
159
- };
160
-
161
- for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
162
- try {
163
- // chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
164
- // Ensure provider folders exist before creating the watcher so watching stays active.
165
- await fsPromises.mkdir(rootPath, { recursive: true });
166
-
167
- // Initialize chokidar watcher with optimized settings
168
- const watcher = chokidar.watch(rootPath, {
169
- ignored: WATCHER_IGNORED_PATTERNS,
170
- persistent: true,
171
- ignoreInitial: true, // Don't fire events for existing files on startup
172
- followSymlinks: false,
173
- depth: 10, // Reasonable depth limit
174
- awaitWriteFinish: {
175
- stabilityThreshold: 100, // Wait 100ms for file to stabilize
176
- pollInterval: 50
177
- }
178
- });
179
-
180
- // Set up event listeners
181
- watcher
182
- .on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
183
- .on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
184
- .on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
185
- .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
186
- .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
187
- .on('error', (error) => {
188
- console.error(`[ERROR] ${provider} watcher error:`, error);
189
- })
190
- .on('ready', () => {
191
- });
192
-
193
- projectsWatchers.push(watcher);
194
- } catch (error) {
195
- console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
196
- }
197
- }
73
+ import { c } from './utils/colors.js';
198
74
 
199
- if (projectsWatchers.length === 0) {
200
- console.error('[ERROR] Failed to setup any provider watchers');
201
- }
202
- }
75
+ const __dirname = getModuleDir(import.meta.url);
76
+ // The server source runs from /server, while the compiled output runs from /dist-server/server.
77
+ // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
78
+ const APP_ROOT = findAppRoot(__dirname);
79
+ const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
203
80
 
81
+ console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
204
82
 
205
83
  const app = express();
206
84
  const server = http.createServer(app);
207
85
 
208
- const ptySessionsMap = new Map();
209
- const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
210
- const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
211
- import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
212
-
213
- // Single WebSocket server that handles both paths
214
- const wss = new WebSocketServer({
215
- server,
216
- verifyClient: (info) => {
217
- console.log('WebSocket connection attempt to:', info.req.url);
218
-
219
- // Platform mode: always allow connection
220
- if (IS_PLATFORM) {
221
- const user = authenticateWebSocket(null); // Will return first user
222
- if (!user) {
223
- console.log('[WARN] Platform mode: No user found in database');
224
- return false;
225
- }
226
- info.req.user = user;
227
- console.log('[OK] Platform mode WebSocket authenticated for user:', user.username);
228
- return true;
229
- }
230
-
231
- // Normal mode: verify token
232
- // Extract token from query parameters or headers
233
- const url = new URL(info.req.url, 'http://localhost');
234
- const token = url.searchParams.get('token') ||
235
- info.req.headers.authorization?.split(' ')[1];
236
-
237
- // Verify token
238
- const user = authenticateWebSocket(token);
239
- if (!user) {
240
- console.log('[WARN] WebSocket authentication failed');
241
- return false;
242
- }
243
-
244
- // Store user info in the request for later use
245
- info.req.user = user;
246
- console.log('[OK] WebSocket authenticated for user:', user.username);
247
- return true;
248
- }
86
+ // Single WebSocket server that handles chat, shell, and plugin proxy paths.
87
+ const wss = createWebSocketServer(server, {
88
+ verifyClient: {
89
+ isPlatform: IS_PLATFORM,
90
+ authenticateWebSocket,
91
+ },
92
+ chat: {
93
+ queryClaudeSDK,
94
+ spawnCursor,
95
+ queryCodex,
96
+ spawnGemini,
97
+ abortClaudeSDKSession,
98
+ abortCursorSession,
99
+ abortCodexSession,
100
+ abortGeminiSession,
101
+ resolveToolApproval,
102
+ isClaudeSDKSessionActive,
103
+ isCursorSessionActive,
104
+ isCodexSessionActive,
105
+ isGeminiSessionActive,
106
+ reconnectSessionWriter,
107
+ getPendingApprovalsForSession,
108
+ getActiveClaudeSDKSessions,
109
+ getActiveCursorSessions,
110
+ getActiveCodexSessions,
111
+ getActiveGeminiSessions,
112
+ },
113
+ shell: {
114
+ getSessionById: (sessionId) => sessionManager.getSession(sessionId),
115
+ stripAnsiSequences,
116
+ normalizeDetectedUrl,
117
+ extractUrlsFromText,
118
+ shouldAutoOpenUrlFromOutput,
119
+ },
120
+ getPluginPort,
249
121
  });
250
122
 
251
123
  // Make WebSocket server available to routes
@@ -281,14 +153,11 @@ app.use('/api', validateApiKey);
281
153
  app.use('/api/auth', authRoutes);
282
154
 
283
155
  // Projects API Routes (protected)
284
- app.use('/api/projects', authenticateToken, projectsRoutes);
156
+ app.use('/api/projects', authenticateToken, projectModuleRoutes);
285
157
 
286
158
  // Git API Routes (protected)
287
159
  app.use('/api/git', authenticateToken, gitRoutes);
288
160
 
289
- // MCP API Routes (protected)
290
- app.use('/api/mcp', authenticateToken, mcpRoutes);
291
-
292
161
  // Cursor API Routes (protected)
293
162
  app.use('/api/cursor', authenticateToken, cursorRoutes);
294
163
 
@@ -304,23 +173,17 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
304
173
  // Settings API Routes (protected)
305
174
  app.use('/api/settings', authenticateToken, settingsRoutes);
306
175
 
307
- // CLI Authentication API Routes (protected)
308
- app.use('/api/cli', authenticateToken, cliAuthRoutes);
309
-
310
176
  // User API Routes (protected)
311
177
  app.use('/api/user', authenticateToken, userRoutes);
312
178
 
313
- // Codex API Routes (protected)
314
- app.use('/api/codex', authenticateToken, codexRoutes);
315
-
316
179
  // Gemini API Routes (protected)
317
180
  app.use('/api/gemini', authenticateToken, geminiRoutes);
318
181
 
319
182
  // Plugins API Routes (protected)
320
183
  app.use('/api/plugins', authenticateToken, pluginsRoutes);
321
184
 
322
- // Unified session messages route (protected)
323
- app.use('/api/sessions', authenticateToken, messagesRoutes);
185
+ // Unified provider MCP routes (protected)
186
+ app.use('/api/providers', authenticateToken, providerRoutes);
324
187
 
325
188
  // Agent API Routes (uses API key authentication)
326
189
  app.use('/api/agent', agentRoutes);
@@ -422,155 +285,6 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
422
285
  }
423
286
  });
424
287
 
425
- app.get('/api/projects', authenticateToken, async (req, res) => {
426
- try {
427
- const projects = await getProjects(broadcastProgress);
428
- res.json(projects);
429
- } catch (error) {
430
- res.status(500).json({ error: error.message });
431
- }
432
- });
433
-
434
- app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
435
- try {
436
- const { limit = 5, offset = 0 } = req.query;
437
- const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
438
- applyCustomSessionNames(result.sessions, 'claude');
439
- res.json(result);
440
- } catch (error) {
441
- res.status(500).json({ error: error.message });
442
- }
443
- });
444
-
445
- // Rename project endpoint
446
- app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
447
- try {
448
- const { displayName } = req.body;
449
- await renameProject(req.params.projectName, displayName);
450
- res.json({ success: true });
451
- } catch (error) {
452
- res.status(500).json({ error: error.message });
453
- }
454
- });
455
-
456
- // Delete session endpoint
457
- app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
458
- try {
459
- const { projectName, sessionId } = req.params;
460
- console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
461
- await deleteSession(projectName, sessionId);
462
- sessionNamesDb.deleteName(sessionId, 'claude');
463
- console.log(`[API] Session ${sessionId} deleted successfully`);
464
- res.json({ success: true });
465
- } catch (error) {
466
- console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
467
- res.status(500).json({ error: error.message });
468
- }
469
- });
470
-
471
- // Rename session endpoint
472
- app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
473
- try {
474
- const { sessionId } = req.params;
475
- const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
476
- if (!safeSessionId || safeSessionId !== String(sessionId)) {
477
- return res.status(400).json({ error: 'Invalid sessionId' });
478
- }
479
- const { summary, provider } = req.body;
480
- if (!summary || typeof summary !== 'string' || summary.trim() === '') {
481
- return res.status(400).json({ error: 'Summary is required' });
482
- }
483
- if (summary.trim().length > 500) {
484
- return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
485
- }
486
- if (!provider || !VALID_PROVIDERS.includes(provider)) {
487
- return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
488
- }
489
- sessionNamesDb.setName(safeSessionId, provider, summary.trim());
490
- res.json({ success: true });
491
- } catch (error) {
492
- console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
493
- res.status(500).json({ error: error.message });
494
- }
495
- });
496
-
497
- // Delete project endpoint
498
- // force=true to allow removal even when sessions exist
499
- // deleteData=true to also delete session/memory files on disk (destructive)
500
- app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
501
- try {
502
- const { projectName } = req.params;
503
- const force = req.query.force === 'true';
504
- const deleteData = req.query.deleteData === 'true';
505
- await deleteProject(projectName, force, deleteData);
506
- res.json({ success: true });
507
- } catch (error) {
508
- res.status(500).json({ error: error.message });
509
- }
510
- });
511
-
512
- // Create project endpoint
513
- app.post('/api/projects/create', authenticateToken, async (req, res) => {
514
- try {
515
- const { path: projectPath } = req.body;
516
-
517
- if (!projectPath || !projectPath.trim()) {
518
- return res.status(400).json({ error: 'Project path is required' });
519
- }
520
-
521
- const project = await addProjectManually(projectPath.trim());
522
- res.json({ success: true, project });
523
- } catch (error) {
524
- console.error('Error creating project:', error);
525
- res.status(500).json({ error: error.message });
526
- }
527
- });
528
-
529
- // Search conversations content (SSE streaming)
530
- app.get('/api/search/conversations', authenticateToken, async (req, res) => {
531
- const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
532
- const parsedLimit = Number.parseInt(String(req.query.limit), 10);
533
- const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100));
534
-
535
- if (query.length < 2) {
536
- return res.status(400).json({ error: 'Query must be at least 2 characters' });
537
- }
538
-
539
- res.writeHead(200, {
540
- 'Content-Type': 'text/event-stream',
541
- 'Cache-Control': 'no-cache',
542
- 'Connection': 'keep-alive',
543
- 'X-Accel-Buffering': 'no',
544
- });
545
-
546
- let closed = false;
547
- const abortController = new AbortController();
548
- req.on('close', () => { closed = true; abortController.abort(); });
549
-
550
- try {
551
- await searchConversations(query, limit, ({ projectResult, totalMatches, scannedProjects, totalProjects }) => {
552
- if (closed) return;
553
- if (projectResult) {
554
- res.write(`event: result\ndata: ${JSON.stringify({ projectResult, totalMatches, scannedProjects, totalProjects })}\n\n`);
555
- } else {
556
- res.write(`event: progress\ndata: ${JSON.stringify({ totalMatches, scannedProjects, totalProjects })}\n\n`);
557
- }
558
- }, abortController.signal);
559
- if (!closed) {
560
- res.write(`event: done\ndata: {}\n\n`);
561
- }
562
- } catch (error) {
563
- console.error('Error searching conversations:', error);
564
- if (!closed) {
565
- res.write(`event: error\ndata: ${JSON.stringify({ error: 'Search failed' })}\n\n`);
566
- }
567
- } finally {
568
- if (!closed) {
569
- res.end();
570
- }
571
- }
572
- });
573
-
574
288
  const expandWorkspacePath = (inputPath) => {
575
289
  if (!inputPath) return inputPath;
576
290
  if (inputPath === '~') {
@@ -704,9 +418,9 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
704
418
  });
705
419
 
706
420
  // Read file content endpoint
707
- app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
421
+ app.get('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
708
422
  try {
709
- const { projectName } = req.params;
423
+ const { projectId } = req.params;
710
424
  const { filePath } = req.query;
711
425
 
712
426
 
@@ -715,7 +429,9 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
715
429
  return res.status(400).json({ error: 'Invalid file path' });
716
430
  }
717
431
 
718
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
432
+ // Resolve the absolute project root via the DB-backed helper; the
433
+ // caller passes the DB-assigned `projectId`, not a folder name.
434
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
719
435
  if (!projectRoot) {
720
436
  return res.status(404).json({ error: 'Project not found' });
721
437
  }
@@ -744,9 +460,9 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
744
460
  });
745
461
 
746
462
  // Serve raw file bytes for previews and downloads.
747
- app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
463
+ app.get('/api/projects/:projectId/files/content', authenticateToken, async (req, res) => {
748
464
  try {
749
- const { projectName } = req.params;
465
+ const { projectId } = req.params;
750
466
  const { path: filePath } = req.query;
751
467
 
752
468
 
@@ -755,7 +471,8 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
755
471
  return res.status(400).json({ error: 'Invalid file path' });
756
472
  }
757
473
 
758
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
474
+ // Projects are now addressed by DB `projectId`, resolved to their path here.
475
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
759
476
  if (!projectRoot) {
760
477
  return res.status(404).json({ error: 'Project not found' });
761
478
  }
@@ -801,9 +518,9 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
801
518
  });
802
519
 
803
520
  // Save file content endpoint
804
- app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
521
+ app.put('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
805
522
  try {
806
- const { projectName } = req.params;
523
+ const { projectId } = req.params;
807
524
  const { filePath, content } = req.body;
808
525
 
809
526
 
@@ -816,7 +533,8 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
816
533
  return res.status(400).json({ error: 'Content is required' });
817
534
  }
818
535
 
819
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
536
+ // Projects are now addressed by DB `projectId`, resolved to their path here.
537
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
820
538
  if (!projectRoot) {
821
539
  return res.status(404).json({ error: 'Project not found' });
822
540
  }
@@ -850,19 +568,16 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
850
568
  }
851
569
  });
852
570
 
853
- app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
571
+ app.get('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
854
572
  try {
855
573
 
856
574
  // Using fsPromises from import
857
575
 
858
- // Use extractProjectDirectory to get the actual project path
859
- let actualPath;
860
- try {
861
- actualPath = await extractProjectDirectory(req.params.projectName);
862
- } catch (error) {
863
- console.error('Error extracting project directory:', error);
864
- // Fallback to simple dash replacement
865
- actualPath = req.params.projectName.replace(/-/g, '/');
576
+ // Resolve the project's absolute path through the DB (projectId is the
577
+ // primary key of the `projects` table after the identifier migration).
578
+ const actualPath = await projectsDb.getProjectPathById(req.params.projectId);
579
+ if (!actualPath) {
580
+ return res.status(404).json({ error: 'Project not found' });
866
581
  }
867
582
 
868
583
  // Check if path exists
@@ -927,10 +642,10 @@ function validateFilename(name) {
927
642
  return { valid: true };
928
643
  }
929
644
 
930
- // POST /api/projects/:projectName/files/create - Create new file or directory
931
- app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => {
645
+ // POST /api/projects/:projectId/files/create - Create new file or directory
646
+ app.post('/api/projects/:projectId/files/create', authenticateToken, async (req, res) => {
932
647
  try {
933
- const { projectName } = req.params;
648
+ const { projectId } = req.params;
934
649
  const { path: parentPath, type, name } = req.body;
935
650
 
936
651
  // Validate input
@@ -947,8 +662,8 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
947
662
  return res.status(400).json({ error: nameValidation.error });
948
663
  }
949
664
 
950
- // Get project root
951
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
665
+ // Resolve the project directory through the DB using the new projectId.
666
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
952
667
  if (!projectRoot) {
953
668
  return res.status(404).json({ error: 'Project not found' });
954
669
  }
@@ -1004,10 +719,10 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
1004
719
  }
1005
720
  });
1006
721
 
1007
- // PUT /api/projects/:projectName/files/rename - Rename file or directory
1008
- app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => {
722
+ // PUT /api/projects/:projectId/files/rename - Rename file or directory
723
+ app.put('/api/projects/:projectId/files/rename', authenticateToken, async (req, res) => {
1009
724
  try {
1010
- const { projectName } = req.params;
725
+ const { projectId } = req.params;
1011
726
  const { oldPath, newName } = req.body;
1012
727
 
1013
728
  // Validate input
@@ -1020,8 +735,8 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
1020
735
  return res.status(400).json({ error: nameValidation.error });
1021
736
  }
1022
737
 
1023
- // Get project root
1024
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
738
+ // Resolve the project directory through the DB using the new projectId.
739
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
1025
740
  if (!projectRoot) {
1026
741
  return res.status(404).json({ error: 'Project not found' });
1027
742
  }
@@ -1081,10 +796,10 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
1081
796
  }
1082
797
  });
1083
798
 
1084
- // DELETE /api/projects/:projectName/files - Delete file or directory
1085
- app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
799
+ // DELETE /api/projects/:projectId/files - Delete file or directory
800
+ app.delete('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
1086
801
  try {
1087
- const { projectName } = req.params;
802
+ const { projectId } = req.params;
1088
803
  const { path: targetPath, type } = req.body;
1089
804
 
1090
805
  // Validate input
@@ -1092,8 +807,8 @@ app.delete('/api/projects/:projectName/files', authenticateToken, async (req, re
1092
807
  return res.status(400).json({ error: 'Path is required' });
1093
808
  }
1094
809
 
1095
- // Get project root
1096
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
810
+ // Resolve the project directory through the DB using the new projectId.
811
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
1097
812
  if (!projectRoot) {
1098
813
  return res.status(404).json({ error: 'Project not found' });
1099
814
  }
@@ -1146,7 +861,7 @@ app.delete('/api/projects/:projectName/files', authenticateToken, async (req, re
1146
861
  }
1147
862
  });
1148
863
 
1149
- // POST /api/projects/:projectName/files/upload - Upload files
864
+ // POST /api/projects/:projectId/files/upload - Upload files
1150
865
  // Dynamic import of multer for file uploads
1151
866
  const uploadFilesHandler = async (req, res) => {
1152
867
  // Dynamic import of multer
@@ -1185,7 +900,7 @@ const uploadFilesHandler = async (req, res) => {
1185
900
  }
1186
901
 
1187
902
  try {
1188
- const { projectName } = req.params;
903
+ const { projectId } = req.params;
1189
904
  const { targetPath, relativePaths } = req.body;
1190
905
 
1191
906
  // Parse relative paths if provided (for folder uploads)
@@ -1199,7 +914,7 @@ const uploadFilesHandler = async (req, res) => {
1199
914
  }
1200
915
 
1201
916
  console.log('[DEBUG] File upload request:', {
1202
- projectName,
917
+ projectId,
1203
918
  targetPath: JSON.stringify(targetPath),
1204
919
  targetPathType: typeof targetPath,
1205
920
  filesCount: req.files?.length,
@@ -1210,8 +925,8 @@ const uploadFilesHandler = async (req, res) => {
1210
925
  return res.status(400).json({ error: 'No files provided' });
1211
926
  }
1212
927
 
1213
- // Get project root
1214
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
928
+ // Resolve the project directory through the DB using the new projectId.
929
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
1215
930
  if (!projectRoot) {
1216
931
  return res.status(404).json({ error: 'Project not found' });
1217
932
  }
@@ -1308,615 +1023,12 @@ const uploadFilesHandler = async (req, res) => {
1308
1023
  });
1309
1024
  };
1310
1025
 
1311
- app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
1312
-
1313
- /**
1314
- * Proxy an authenticated client WebSocket to a plugin's internal WS server.
1315
- * Auth is enforced by verifyClient before this function is reached.
1316
- */
1317
- function handlePluginWsProxy(clientWs, pathname) {
1318
- const pluginName = pathname.replace('/plugin-ws/', '');
1319
- if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) {
1320
- clientWs.close(4400, 'Invalid plugin name');
1321
- return;
1322
- }
1323
-
1324
- const port = getPluginPort(pluginName);
1325
- if (!port) {
1326
- clientWs.close(4404, 'Plugin not running');
1327
- return;
1328
- }
1329
-
1330
- const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1331
-
1332
- upstream.on('open', () => {
1333
- console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
1334
- });
1335
-
1336
- // Relay messages bidirectionally
1337
- upstream.on('message', (data) => {
1338
- if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data);
1339
- });
1340
- clientWs.on('message', (data) => {
1341
- if (upstream.readyState === WebSocket.OPEN) upstream.send(data);
1342
- });
1343
-
1344
- // Propagate close in both directions
1345
- upstream.on('close', () => { if (clientWs.readyState === WebSocket.OPEN) clientWs.close(); });
1346
- clientWs.on('close', () => { if (upstream.readyState === WebSocket.OPEN) upstream.close(); });
1347
-
1348
- upstream.on('error', (err) => {
1349
- console.error(`[Plugins] WS proxy error for "${pluginName}":`, err.message);
1350
- if (clientWs.readyState === WebSocket.OPEN) clientWs.close(4502, 'Upstream error');
1351
- });
1352
- clientWs.on('error', () => {
1353
- if (upstream.readyState === WebSocket.OPEN) upstream.close();
1354
- });
1355
- }
1356
-
1357
- // WebSocket connection handler that routes based on URL path
1358
- wss.on('connection', (ws, request) => {
1359
- const url = request.url;
1360
- console.log('[INFO] Client connected to:', url);
1361
-
1362
- // Parse URL to get pathname without query parameters
1363
- const urlObj = new URL(url, 'http://localhost');
1364
- const pathname = urlObj.pathname;
1365
-
1366
- if (pathname === '/shell') {
1367
- handleShellConnection(ws);
1368
- } else if (pathname === '/ws') {
1369
- handleChatConnection(ws, request);
1370
- } else if (pathname.startsWith('/plugin-ws/')) {
1371
- handlePluginWsProxy(ws, pathname);
1372
- } else {
1373
- console.log('[WARN] Unknown WebSocket path:', pathname);
1374
- ws.close();
1375
- }
1376
- });
1377
-
1378
- /**
1379
- * WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
1380
- *
1381
- * Provider files use `createNormalizedMessage()` from `providers/types.js` and
1382
- * adapter `normalizeMessage()` to produce unified NormalizedMessage events.
1383
- * The writer simply serialises and sends.
1384
- */
1385
- class WebSocketWriter {
1386
- constructor(ws, userId = null) {
1387
- this.ws = ws;
1388
- this.sessionId = null;
1389
- this.userId = userId;
1390
- this.isWebSocketWriter = true; // Marker for transport detection
1391
- }
1392
-
1393
- send(data) {
1394
- if (this.ws.readyState === 1) { // WebSocket.OPEN
1395
- this.ws.send(JSON.stringify(data));
1396
- }
1397
- }
1398
-
1399
- updateWebSocket(newRawWs) {
1400
- this.ws = newRawWs;
1401
- }
1402
-
1403
- setSessionId(sessionId) {
1404
- this.sessionId = sessionId;
1405
- }
1406
-
1407
- getSessionId() {
1408
- return this.sessionId;
1409
- }
1410
- }
1411
-
1412
- // Handle chat WebSocket connections
1413
- function handleChatConnection(ws, request) {
1414
- console.log('[INFO] Chat WebSocket connected');
1415
-
1416
- // Add to connected clients for project updates
1417
- connectedClients.add(ws);
1418
-
1419
- // Wrap WebSocket with writer for consistent interface with SSEStreamWriter
1420
- const writer = new WebSocketWriter(ws, request?.user?.id ?? request?.user?.userId ?? null);
1421
-
1422
- ws.on('message', async (message) => {
1423
- try {
1424
- const data = JSON.parse(message);
1425
-
1426
- if (data.type === 'claude-command') {
1427
- console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
1428
- console.log('📁 Project:', data.options?.projectPath || 'Unknown');
1429
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1430
-
1431
- // Use Claude Agents SDK
1432
- await queryClaudeSDK(data.command, data.options, writer);
1433
- } else if (data.type === 'cursor-command') {
1434
- console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
1435
- console.log('📁 Project:', data.options?.cwd || 'Unknown');
1436
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1437
- console.log('🤖 Model:', data.options?.model || 'default');
1438
- await spawnCursor(data.command, data.options, writer);
1439
- } else if (data.type === 'codex-command') {
1440
- console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
1441
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1442
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1443
- console.log('🤖 Model:', data.options?.model || 'default');
1444
- await queryCodex(data.command, data.options, writer);
1445
- } else if (data.type === 'gemini-command') {
1446
- console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
1447
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1448
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1449
- console.log('🤖 Model:', data.options?.model || 'default');
1450
- await spawnGemini(data.command, data.options, writer);
1451
- } else if (data.type === 'cursor-resume') {
1452
- // Backward compatibility: treat as cursor-command with resume and no prompt
1453
- console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
1454
- await spawnCursor('', {
1455
- sessionId: data.sessionId,
1456
- resume: true,
1457
- cwd: data.options?.cwd
1458
- }, writer);
1459
- } else if (data.type === 'abort-session') {
1460
- console.log('[DEBUG] Abort session request:', data.sessionId);
1461
- const provider = data.provider || 'claude';
1462
- let success;
1463
-
1464
- if (provider === 'cursor') {
1465
- success = abortCursorSession(data.sessionId);
1466
- } else if (provider === 'codex') {
1467
- success = abortCodexSession(data.sessionId);
1468
- } else if (provider === 'gemini') {
1469
- success = abortGeminiSession(data.sessionId);
1470
- } else {
1471
- // Use Claude Agents SDK
1472
- success = await abortClaudeSDKSession(data.sessionId);
1473
- }
1026
+ app.post('/api/projects/:projectId/files/upload', authenticateToken, uploadFilesHandler);
1474
1027
 
1475
- writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider }));
1476
- } else if (data.type === 'claude-permission-response') {
1477
- // Relay UI approval decisions back into the SDK control flow.
1478
- // This does not persist permissions; it only resolves the in-flight request,
1479
- // introduced so the SDK can resume once the user clicks Allow/Deny.
1480
- if (data.requestId) {
1481
- resolveToolApproval(data.requestId, {
1482
- allow: Boolean(data.allow),
1483
- updatedInput: data.updatedInput,
1484
- message: data.message,
1485
- rememberEntry: data.rememberEntry
1486
- });
1487
- }
1488
- } else if (data.type === 'cursor-abort') {
1489
- console.log('[DEBUG] Abort Cursor session:', data.sessionId);
1490
- const success = abortCursorSession(data.sessionId);
1491
- writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider: 'cursor' }));
1492
- } else if (data.type === 'check-session-status') {
1493
- // Check if a specific session is currently processing
1494
- const provider = data.provider || 'claude';
1495
- const sessionId = data.sessionId;
1496
- let isActive;
1497
-
1498
- if (provider === 'cursor') {
1499
- isActive = isCursorSessionActive(sessionId);
1500
- } else if (provider === 'codex') {
1501
- isActive = isCodexSessionActive(sessionId);
1502
- } else if (provider === 'gemini') {
1503
- isActive = isGeminiSessionActive(sessionId);
1504
- } else {
1505
- // Use Claude Agents SDK
1506
- isActive = isClaudeSDKSessionActive(sessionId);
1507
- if (isActive) {
1508
- // Reconnect the session's writer to the new WebSocket so
1509
- // subsequent SDK output flows to the refreshed client.
1510
- reconnectSessionWriter(sessionId, ws);
1511
- }
1512
- }
1513
-
1514
- writer.send({
1515
- type: 'session-status',
1516
- sessionId,
1517
- provider,
1518
- isProcessing: isActive
1519
- });
1520
- } else if (data.type === 'get-pending-permissions') {
1521
- // Return pending permission requests for a session
1522
- const sessionId = data.sessionId;
1523
- if (sessionId && isClaudeSDKSessionActive(sessionId)) {
1524
- const pending = getPendingApprovalsForSession(sessionId);
1525
- writer.send({
1526
- type: 'pending-permissions-response',
1527
- sessionId,
1528
- data: pending
1529
- });
1530
- }
1531
- } else if (data.type === 'get-active-sessions') {
1532
- // Get all currently active sessions
1533
- const activeSessions = {
1534
- claude: getActiveClaudeSDKSessions(),
1535
- cursor: getActiveCursorSessions(),
1536
- codex: getActiveCodexSessions(),
1537
- gemini: getActiveGeminiSessions()
1538
- };
1539
- writer.send({
1540
- type: 'active-sessions',
1541
- sessions: activeSessions
1542
- });
1543
- }
1544
- } catch (error) {
1545
- console.error('[ERROR] Chat WebSocket error:', error.message);
1546
- writer.send({
1547
- type: 'error',
1548
- error: error.message
1549
- });
1550
- }
1551
- });
1552
-
1553
- ws.on('close', () => {
1554
- console.log('🔌 Chat client disconnected');
1555
- // Remove from connected clients
1556
- connectedClients.delete(ws);
1557
- });
1558
- }
1559
-
1560
- // Handle shell WebSocket connections
1561
- function handleShellConnection(ws) {
1562
- console.log('🐚 Shell client connected');
1563
- let shellProcess = null;
1564
- let ptySessionKey = null;
1565
- let urlDetectionBuffer = '';
1566
- const announcedAuthUrls = new Set();
1567
-
1568
- ws.on('message', async (message) => {
1569
- try {
1570
- const data = JSON.parse(message);
1571
- console.log('📨 Shell message received:', data.type);
1572
-
1573
- if (data.type === 'init') {
1574
- const projectPath = data.projectPath || process.cwd();
1575
- const sessionId = data.sessionId;
1576
- const hasSession = data.hasSession;
1577
- const provider = data.provider || 'claude';
1578
- const initialCommand = data.initialCommand;
1579
- const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
1580
- urlDetectionBuffer = '';
1581
- announcedAuthUrls.clear();
1582
-
1583
- // Login commands (Claude/Cursor auth) should never reuse cached sessions
1584
- const isLoginCommand = initialCommand && (
1585
- initialCommand.includes('setup-token') ||
1586
- initialCommand.includes('cursor-agent login') ||
1587
- initialCommand.includes('auth login')
1588
- );
1589
-
1590
- // Include command hash in session key so different commands get separate sessions
1591
- const commandSuffix = isPlainShell && initialCommand
1592
- ? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
1593
- : '';
1594
- ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
1595
-
1596
- // Kill any existing login session before starting fresh
1597
- if (isLoginCommand) {
1598
- const oldSession = ptySessionsMap.get(ptySessionKey);
1599
- if (oldSession) {
1600
- console.log('🧹 Cleaning up existing login session:', ptySessionKey);
1601
- if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
1602
- if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
1603
- ptySessionsMap.delete(ptySessionKey);
1604
- }
1605
- }
1606
-
1607
- const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
1608
- if (existingSession) {
1609
- console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
1610
- shellProcess = existingSession.pty;
1611
-
1612
- clearTimeout(existingSession.timeoutId);
1613
-
1614
- ws.send(JSON.stringify({
1615
- type: 'output',
1616
- data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
1617
- }));
1618
-
1619
- if (existingSession.buffer && existingSession.buffer.length > 0) {
1620
- console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
1621
- existingSession.buffer.forEach(bufferedData => {
1622
- ws.send(JSON.stringify({
1623
- type: 'output',
1624
- data: bufferedData
1625
- }));
1626
- });
1627
- }
1628
-
1629
- existingSession.ws = ws;
1630
-
1631
- return;
1632
- }
1633
-
1634
- console.log('[INFO] Starting shell in:', projectPath);
1635
- console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
1636
- console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
1637
- if (initialCommand) {
1638
- console.log('⚡ Initial command:', initialCommand);
1639
- }
1640
-
1641
- // First send a welcome message
1642
- let welcomeMsg;
1643
- if (isPlainShell) {
1644
- welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
1645
- } else {
1646
- const providerName = provider === 'cursor' ? 'Cursor' : (provider === 'codex' ? 'Codex' : (provider === 'gemini' ? 'Gemini' : 'Claude'));
1647
- welcomeMsg = hasSession ?
1648
- `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
1649
- `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
1650
- }
1651
-
1652
- ws.send(JSON.stringify({
1653
- type: 'output',
1654
- data: welcomeMsg
1655
- }));
1656
-
1657
- try {
1658
- // Validate projectPath — resolve to absolute and verify it exists
1659
- const resolvedProjectPath = path.resolve(projectPath);
1660
- try {
1661
- const stats = fs.statSync(resolvedProjectPath);
1662
- if (!stats.isDirectory()) {
1663
- throw new Error('Not a directory');
1664
- }
1665
- } catch (pathErr) {
1666
- ws.send(JSON.stringify({ type: 'error', message: 'Invalid project path' }));
1667
- return;
1668
- }
1669
-
1670
- // Validate sessionId — only allow safe characters
1671
- const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
1672
- if (sessionId && !safeSessionIdPattern.test(sessionId)) {
1673
- ws.send(JSON.stringify({ type: 'error', message: 'Invalid session ID' }));
1674
- return;
1675
- }
1676
-
1677
- // Build shell command — use cwd for project path (never interpolate into shell string)
1678
- let shellCommand;
1679
- if (isPlainShell) {
1680
- // Plain shell mode - run the initial command in the project directory
1681
- shellCommand = initialCommand;
1682
- } else if (provider === 'cursor') {
1683
- if (hasSession && sessionId) {
1684
- shellCommand = `cursor-agent --resume="${sessionId}"`;
1685
- } else {
1686
- shellCommand = 'cursor-agent';
1687
- }
1688
- } else if (provider === 'codex') {
1689
- // Use codex command; attempt to resume and fall back to a new session when the resume fails.
1690
- if (hasSession && sessionId) {
1691
- if (os.platform() === 'win32') {
1692
- // PowerShell syntax for fallback
1693
- shellCommand = `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
1694
- } else {
1695
- shellCommand = `codex resume "${sessionId}" || codex`;
1696
- }
1697
- } else {
1698
- shellCommand = 'codex';
1699
- }
1700
- } else if (provider === 'gemini') {
1701
- const command = initialCommand || 'gemini';
1702
- let resumeId = sessionId;
1703
- if (hasSession && sessionId) {
1704
- try {
1705
- // Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names.
1706
- // The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
1707
- // We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
1708
- const sess = sessionManager.getSession(sessionId);
1709
- if (sess && sess.cliSessionId) {
1710
- resumeId = sess.cliSessionId;
1711
- // Validate the looked-up CLI session ID too
1712
- if (!safeSessionIdPattern.test(resumeId)) {
1713
- resumeId = null;
1714
- }
1715
- }
1716
- } catch (err) {
1717
- console.error('Failed to get Gemini CLI session ID:', err);
1718
- }
1719
- }
1720
-
1721
- if (hasSession && resumeId) {
1722
- shellCommand = `${command} --resume "${resumeId}"`;
1723
- } else {
1724
- shellCommand = command;
1725
- }
1726
- } else {
1727
- // Claude (default provider)
1728
- const command = initialCommand || 'claude';
1729
- if (hasSession && sessionId) {
1730
- if (os.platform() === 'win32') {
1731
- shellCommand = `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
1732
- } else {
1733
- shellCommand = `claude --resume "${sessionId}" || claude`;
1734
- }
1735
- } else {
1736
- shellCommand = command;
1737
- }
1738
- }
1739
-
1740
- console.log('🔧 Executing shell command:', shellCommand);
1741
-
1742
- // Use appropriate shell based on platform
1743
- const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
1744
- const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
1745
-
1746
- // Use terminal dimensions from client if provided, otherwise use defaults
1747
- const termCols = data.cols || 80;
1748
- const termRows = data.rows || 24;
1749
- console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
1750
-
1751
- shellProcess = pty.spawn(shell, shellArgs, {
1752
- name: 'xterm-256color',
1753
- cols: termCols,
1754
- rows: termRows,
1755
- cwd: resolvedProjectPath,
1756
- env: {
1757
- ...process.env,
1758
- TERM: 'xterm-256color',
1759
- COLORTERM: 'truecolor',
1760
- FORCE_COLOR: '3'
1761
- }
1762
- });
1763
-
1764
- console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
1765
-
1766
- ptySessionsMap.set(ptySessionKey, {
1767
- pty: shellProcess,
1768
- ws: ws,
1769
- buffer: [],
1770
- timeoutId: null,
1771
- projectPath,
1772
- sessionId
1773
- });
1774
-
1775
- // Handle data output
1776
- shellProcess.onData((data) => {
1777
- const session = ptySessionsMap.get(ptySessionKey);
1778
- if (!session) return;
1779
-
1780
- if (session.buffer.length < 5000) {
1781
- session.buffer.push(data);
1782
- } else {
1783
- session.buffer.shift();
1784
- session.buffer.push(data);
1785
- }
1786
-
1787
- if (session.ws && session.ws.readyState === WebSocket.OPEN) {
1788
- let outputData = data;
1789
-
1790
- const cleanChunk = stripAnsiSequences(data);
1791
- urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
1792
-
1793
- outputData = outputData.replace(
1794
- /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
1795
- '[INFO] Opening in browser: $1'
1796
- );
1797
-
1798
- const emitAuthUrl = (detectedUrl, autoOpen = false) => {
1799
- const normalizedUrl = normalizeDetectedUrl(detectedUrl);
1800
- if (!normalizedUrl) return;
1801
-
1802
- const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
1803
- if (isNewUrl) {
1804
- announcedAuthUrls.add(normalizedUrl);
1805
- session.ws.send(JSON.stringify({
1806
- type: 'auth_url',
1807
- url: normalizedUrl,
1808
- autoOpen
1809
- }));
1810
- }
1811
-
1812
- };
1813
-
1814
- const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
1815
- .map((url) => normalizeDetectedUrl(url))
1816
- .filter(Boolean);
1817
-
1818
- // Prefer the most complete URL if shorter prefix variants are also present.
1819
- const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) =>
1820
- !urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
1821
- );
1822
-
1823
- dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
1824
-
1825
- if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
1826
- const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
1827
- current.length > longest.length ? current : longest
1828
- );
1829
- emitAuthUrl(bestUrl, true);
1830
- }
1831
-
1832
- // Send regular output
1833
- session.ws.send(JSON.stringify({
1834
- type: 'output',
1835
- data: outputData
1836
- }));
1837
- }
1838
- });
1839
-
1840
- // Handle process exit
1841
- shellProcess.onExit((exitCode) => {
1842
- console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
1843
- const session = ptySessionsMap.get(ptySessionKey);
1844
- if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
1845
- session.ws.send(JSON.stringify({
1846
- type: 'output',
1847
- data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
1848
- }));
1849
- }
1850
- if (session && session.timeoutId) {
1851
- clearTimeout(session.timeoutId);
1852
- }
1853
- ptySessionsMap.delete(ptySessionKey);
1854
- shellProcess = null;
1855
- });
1856
-
1857
- } catch (spawnError) {
1858
- console.error('[ERROR] Error spawning process:', spawnError);
1859
- ws.send(JSON.stringify({
1860
- type: 'output',
1861
- data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
1862
- }));
1863
- }
1864
-
1865
- } else if (data.type === 'input') {
1866
- // Send input to shell process
1867
- if (shellProcess && shellProcess.write) {
1868
- try {
1869
- shellProcess.write(data.data);
1870
- } catch (error) {
1871
- console.error('Error writing to shell:', error);
1872
- }
1873
- } else {
1874
- console.warn('No active shell process to send input to');
1875
- }
1876
- } else if (data.type === 'resize') {
1877
- // Handle terminal resize
1878
- if (shellProcess && shellProcess.resize) {
1879
- console.log('Terminal resize requested:', data.cols, 'x', data.rows);
1880
- shellProcess.resize(data.cols, data.rows);
1881
- }
1882
- }
1883
- } catch (error) {
1884
- console.error('[ERROR] Shell WebSocket error:', error.message);
1885
- if (ws.readyState === WebSocket.OPEN) {
1886
- ws.send(JSON.stringify({
1887
- type: 'output',
1888
- data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
1889
- }));
1890
- }
1891
- }
1892
- });
1893
-
1894
- ws.on('close', () => {
1895
- console.log('🔌 Shell client disconnected');
1896
-
1897
- if (ptySessionKey) {
1898
- const session = ptySessionsMap.get(ptySessionKey);
1899
- if (session) {
1900
- console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
1901
- session.ws = null;
1902
-
1903
- session.timeoutId = setTimeout(() => {
1904
- console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
1905
- if (session.pty && session.pty.kill) {
1906
- session.pty.kill();
1907
- }
1908
- ptySessionsMap.delete(ptySessionKey);
1909
- }, PTY_SESSION_TIMEOUT);
1910
- }
1911
- }
1912
- });
1913
-
1914
- ws.on('error', (error) => {
1915
- console.error('[ERROR] Shell WebSocket error:', error);
1916
- });
1917
- }
1918
- // Image upload endpoint
1919
- app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
1028
+ // Image upload endpoint. Accepts the DB-assigned `projectId` (not a folder name)
1029
+ // but the current implementation doesn't need to touch the project directory,
1030
+ // so we just leave the param rename for consistency with the rest of the API.
1031
+ app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req, res) => {
1920
1032
  try {
1921
1033
  const multer = (await import('multer')).default;
1922
1034
  const path = (await import('path')).default;
@@ -2000,10 +1112,11 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
2000
1112
  }
2001
1113
  });
2002
1114
 
2003
- // Get token usage for a specific session
2004
- app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1115
+ // Get token usage for a specific session. `projectId` is the DB primary key;
1116
+ // the Claude branch below resolves it to an absolute path via the DB.
1117
+ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
2005
1118
  try {
2006
- const { projectName, sessionId } = req.params;
1119
+ const { projectId, sessionId } = req.params;
2007
1120
  const { provider = 'claude' } = req.query;
2008
1121
  const homeDir = os.homedir();
2009
1122
 
@@ -2107,13 +1220,13 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
2107
1220
  }
2108
1221
 
2109
1222
  // Handle Claude sessions (default)
2110
- // Extract actual project path
2111
- let projectPath;
2112
- try {
2113
- projectPath = await extractProjectDirectory(projectName);
2114
- } catch (error) {
2115
- console.error('Error extracting project directory:', error);
2116
- return res.status(500).json({ error: 'Failed to determine project path' });
1223
+ // Resolve the project path through the DB using the caller-supplied
1224
+ // `projectId`. Legacy code here called extractProjectDirectory with a
1225
+ // folder-encoded project name; the migration centralizes that lookup
1226
+ // in the projects table.
1227
+ const projectPath = await projectsDb.getProjectPathById(projectId);
1228
+ if (!projectPath) {
1229
+ return res.status(404).json({ error: 'Project not found' });
2117
1230
  }
2118
1231
 
2119
1232
  // Construct the JSONL file path
@@ -2213,6 +1326,30 @@ app.get('*', (req, res) => {
2213
1326
  }
2214
1327
  });
2215
1328
 
1329
+ // global error middleware must be last
1330
+ app.use((err, req, res, next) => {
1331
+ if (err instanceof AppError) {
1332
+ return res.status(err.statusCode).json({
1333
+ success: false,
1334
+ error: {
1335
+ code: err.code,
1336
+ message: err.message,
1337
+ details: err.details,
1338
+ },
1339
+ });
1340
+ }
1341
+
1342
+ console.error(err);
1343
+
1344
+ return res.status(500).json({
1345
+ success: false,
1346
+ error: {
1347
+ code: 'INTERNAL_ERROR',
1348
+ message: 'Internal server error',
1349
+ },
1350
+ });
1351
+ });
1352
+
2216
1353
  // Helper function to convert permissions to rwx format
2217
1354
  function permToRwx(perm) {
2218
1355
  const r = perm & 4 ? 'r' : '-';
@@ -2339,7 +1476,7 @@ async function startServer() {
2339
1476
  console.log('');
2340
1477
 
2341
1478
  // Start watching the projects folder for changes
2342
- await setupProjectsWatcher();
1479
+ await initializeSessionsWatcher();
2343
1480
 
2344
1481
  // Start server-side plugin processes for enabled plugins
2345
1482
  startEnabledPluginServers().catch(err => {
@@ -2347,6 +1484,7 @@ async function startServer() {
2347
1484
  });
2348
1485
  });
2349
1486
 
1487
+ await closeSessionsWatcher();
2350
1488
  // Clean up plugin processes on shutdown
2351
1489
  const shutdownPlugins = async () => {
2352
1490
  await stopAllPlugins();