@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
@@ -1,221 +1,87 @@
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
- const __dirname = getModuleDir(import.meta.url);
8
- // The server source runs from /server, while the compiled output runs from /dist-server/server.
9
- // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
10
- const APP_ROOT = findAppRoot(__dirname);
11
- const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
12
- import { c } from './utils/colors.js';
13
- console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
14
- import express from 'express';
15
- import { WebSocketServer, WebSocket } from 'ws';
16
6
  import os from 'os';
17
7
  import http from 'http';
18
- import cors from 'cors';
19
- import { promises as fsPromises } from 'fs';
20
8
  import { spawn } from 'child_process';
21
- import pty from 'node-pty';
22
- import fetch from 'node-fetch';
9
+ import express from 'express';
10
+ import cors from 'cors';
23
11
  import mime from 'mime-types';
24
- import { getProjects, getSessions, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js';
25
- import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
26
- import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
27
- import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
28
- import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
12
+ import { AppError, WORKSPACES_ROOT, validateWorkspacePath } from './shared/utils.js';
13
+ import { closeSessionsWatcher, initializeSessionsWatcher } from './modules/providers/index.js';
14
+ import { createWebSocketServer } from './modules/websocket/index.js';
15
+ import { getConnectableHost } from '../shared/networkHosts.js';
16
+ import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
17
+ import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter, } from './claude-sdk.js';
18
+ import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions, } from './cursor-cli.js';
19
+ import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions, } from './openai-codex.js';
20
+ import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions, } from './gemini-cli.js';
29
21
  import sessionManager from './sessionManager.js';
22
+ import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput, } from './utils/url-detection.js';
30
23
  import gitRoutes from './routes/git.js';
31
24
  import authRoutes from './routes/auth.js';
32
- import mcpRoutes from './routes/mcp.js';
33
25
  import cursorRoutes from './routes/cursor.js';
34
26
  import taskmasterRoutes from './routes/taskmaster.js';
35
27
  import mcpUtilsRoutes from './routes/mcp-utils.js';
36
28
  import commandsRoutes from './routes/commands.js';
37
29
  import settingsRoutes from './routes/settings.js';
38
30
  import agentRoutes from './routes/agent.js';
39
- import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes/projects.js';
40
- import cliAuthRoutes from './routes/cli-auth.js';
31
+ import projectModuleRoutes from './modules/projects/projects.routes.js';
41
32
  import userRoutes from './routes/user.js';
42
- import codexRoutes from './routes/codex.js';
43
33
  import geminiRoutes from './routes/gemini.js';
44
34
  import pluginsRoutes from './routes/plugins.js';
45
- import messagesRoutes from './routes/messages.js';
46
- import { createNormalizedMessage } from './providers/types.js';
35
+ import providerRoutes from './modules/providers/provider.routes.js';
47
36
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
48
- import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
37
+ import { initializeDatabase, projectsDb } from './modules/database/index.js';
49
38
  import { configureWebPush } from './services/vapid-keys.js';
50
39
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
51
40
  import { IS_PLATFORM } from './constants/config.js';
52
- import { getConnectableHost } from '../shared/networkHosts.js';
53
- const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
54
- // File system watchers for provider project/session folders
55
- const PROVIDER_WATCH_PATHS = [
56
- { provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
57
- { provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
58
- { provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') },
59
- { provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
60
- { provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
61
- ];
62
- const WATCHER_IGNORED_PATTERNS = [
63
- '**/node_modules/**',
64
- '**/.git/**',
65
- '**/dist/**',
66
- '**/build/**',
67
- '**/*.tmp',
68
- '**/*.swp',
69
- '**/.DS_Store'
70
- ];
71
- const WATCHER_DEBOUNCE_MS = 300;
72
- let projectsWatchers = [];
73
- let projectsWatcherDebounceTimer = null;
74
- const connectedClients = new Set();
75
- let isGetProjectsRunning = false; // Flag to prevent reentrant calls
76
- // Broadcast progress to all connected WebSocket clients
77
- function broadcastProgress(progress) {
78
- const message = JSON.stringify({
79
- type: 'loading_progress',
80
- ...progress
81
- });
82
- connectedClients.forEach(client => {
83
- if (client.readyState === WebSocket.OPEN) {
84
- client.send(message);
85
- }
86
- });
87
- }
88
- // Setup file system watchers for Claude, Cursor, and Codex project/session folders
89
- async function setupProjectsWatcher() {
90
- const chokidar = (await import('chokidar')).default;
91
- if (projectsWatcherDebounceTimer) {
92
- clearTimeout(projectsWatcherDebounceTimer);
93
- projectsWatcherDebounceTimer = null;
94
- }
95
- await Promise.all(projectsWatchers.map(async (watcher) => {
96
- try {
97
- await watcher.close();
98
- }
99
- catch (error) {
100
- console.error('[WARN] Failed to close watcher:', error);
101
- }
102
- }));
103
- projectsWatchers = [];
104
- const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
105
- if (projectsWatcherDebounceTimer) {
106
- clearTimeout(projectsWatcherDebounceTimer);
107
- }
108
- projectsWatcherDebounceTimer = setTimeout(async () => {
109
- // Prevent reentrant calls
110
- if (isGetProjectsRunning) {
111
- return;
112
- }
113
- try {
114
- isGetProjectsRunning = true;
115
- // Clear project directory cache when files change
116
- clearProjectDirectoryCache();
117
- // Get updated projects list
118
- const updatedProjects = await getProjects(broadcastProgress);
119
- // Notify all connected clients about the project changes
120
- const updateMessage = JSON.stringify({
121
- type: 'projects_updated',
122
- projects: updatedProjects,
123
- timestamp: new Date().toISOString(),
124
- changeType: eventType,
125
- changedFile: path.relative(rootPath, filePath),
126
- watchProvider: provider
127
- });
128
- connectedClients.forEach(client => {
129
- if (client.readyState === WebSocket.OPEN) {
130
- client.send(updateMessage);
131
- }
132
- });
133
- }
134
- catch (error) {
135
- console.error('[ERROR] Error handling project changes:', error);
136
- }
137
- finally {
138
- isGetProjectsRunning = false;
139
- }
140
- }, WATCHER_DEBOUNCE_MS);
141
- };
142
- for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
143
- try {
144
- // chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
145
- // Ensure provider folders exist before creating the watcher so watching stays active.
146
- await fsPromises.mkdir(rootPath, { recursive: true });
147
- // Initialize chokidar watcher with optimized settings
148
- const watcher = chokidar.watch(rootPath, {
149
- ignored: WATCHER_IGNORED_PATTERNS,
150
- persistent: true,
151
- ignoreInitial: true, // Don't fire events for existing files on startup
152
- followSymlinks: false,
153
- depth: 10, // Reasonable depth limit
154
- awaitWriteFinish: {
155
- stabilityThreshold: 100, // Wait 100ms for file to stabilize
156
- pollInterval: 50
157
- }
158
- });
159
- // Set up event listeners
160
- watcher
161
- .on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
162
- .on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
163
- .on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
164
- .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
165
- .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
166
- .on('error', (error) => {
167
- console.error(`[ERROR] ${provider} watcher error:`, error);
168
- })
169
- .on('ready', () => {
170
- });
171
- projectsWatchers.push(watcher);
172
- }
173
- catch (error) {
174
- console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
175
- }
176
- }
177
- if (projectsWatchers.length === 0) {
178
- console.error('[ERROR] Failed to setup any provider watchers');
179
- }
180
- }
41
+ import { c } from './utils/colors.js';
42
+ const __dirname = getModuleDir(import.meta.url);
43
+ // The server source runs from /server, while the compiled output runs from /dist-server/server.
44
+ // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
45
+ const APP_ROOT = findAppRoot(__dirname);
46
+ const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
47
+ console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
181
48
  const app = express();
182
49
  const server = http.createServer(app);
183
- const ptySessionsMap = new Map();
184
- const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
185
- const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
186
- import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
187
- // Single WebSocket server that handles both paths
188
- const wss = new WebSocketServer({
189
- server,
190
- verifyClient: (info) => {
191
- console.log('WebSocket connection attempt to:', info.req.url);
192
- // Platform mode: always allow connection
193
- if (IS_PLATFORM) {
194
- const user = authenticateWebSocket(null); // Will return first user
195
- if (!user) {
196
- console.log('[WARN] Platform mode: No user found in database');
197
- return false;
198
- }
199
- info.req.user = user;
200
- console.log('[OK] Platform mode WebSocket authenticated for user:', user.username);
201
- return true;
202
- }
203
- // Normal mode: verify token
204
- // Extract token from query parameters or headers
205
- const url = new URL(info.req.url, 'http://localhost');
206
- const token = url.searchParams.get('token') ||
207
- info.req.headers.authorization?.split(' ')[1];
208
- // Verify token
209
- const user = authenticateWebSocket(token);
210
- if (!user) {
211
- console.log('[WARN] WebSocket authentication failed');
212
- return false;
213
- }
214
- // Store user info in the request for later use
215
- info.req.user = user;
216
- console.log('[OK] WebSocket authenticated for user:', user.username);
217
- return true;
218
- }
50
+ // Single WebSocket server that handles chat, shell, and plugin proxy paths.
51
+ const wss = createWebSocketServer(server, {
52
+ verifyClient: {
53
+ isPlatform: IS_PLATFORM,
54
+ authenticateWebSocket,
55
+ },
56
+ chat: {
57
+ queryClaudeSDK,
58
+ spawnCursor,
59
+ queryCodex,
60
+ spawnGemini,
61
+ abortClaudeSDKSession,
62
+ abortCursorSession,
63
+ abortCodexSession,
64
+ abortGeminiSession,
65
+ resolveToolApproval,
66
+ isClaudeSDKSessionActive,
67
+ isCursorSessionActive,
68
+ isCodexSessionActive,
69
+ isGeminiSessionActive,
70
+ reconnectSessionWriter,
71
+ getPendingApprovalsForSession,
72
+ getActiveClaudeSDKSessions,
73
+ getActiveCursorSessions,
74
+ getActiveCodexSessions,
75
+ getActiveGeminiSessions,
76
+ },
77
+ shell: {
78
+ getSessionById: (sessionId) => sessionManager.getSession(sessionId),
79
+ stripAnsiSequences,
80
+ normalizeDetectedUrl,
81
+ extractUrlsFromText,
82
+ shouldAutoOpenUrlFromOutput,
83
+ },
84
+ getPluginPort,
219
85
  });
220
86
  // Make WebSocket server available to routes
221
87
  app.locals.wss = wss;
@@ -245,11 +111,9 @@ app.use('/api', validateApiKey);
245
111
  // Authentication routes (public)
246
112
  app.use('/api/auth', authRoutes);
247
113
  // Projects API Routes (protected)
248
- app.use('/api/projects', authenticateToken, projectsRoutes);
114
+ app.use('/api/projects', authenticateToken, projectModuleRoutes);
249
115
  // Git API Routes (protected)
250
116
  app.use('/api/git', authenticateToken, gitRoutes);
251
- // MCP API Routes (protected)
252
- app.use('/api/mcp', authenticateToken, mcpRoutes);
253
117
  // Cursor API Routes (protected)
254
118
  app.use('/api/cursor', authenticateToken, cursorRoutes);
255
119
  // TaskMaster API Routes (protected)
@@ -260,18 +124,14 @@ app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
260
124
  app.use('/api/commands', authenticateToken, commandsRoutes);
261
125
  // Settings API Routes (protected)
262
126
  app.use('/api/settings', authenticateToken, settingsRoutes);
263
- // CLI Authentication API Routes (protected)
264
- app.use('/api/cli', authenticateToken, cliAuthRoutes);
265
127
  // User API Routes (protected)
266
128
  app.use('/api/user', authenticateToken, userRoutes);
267
- // Codex API Routes (protected)
268
- app.use('/api/codex', authenticateToken, codexRoutes);
269
129
  // Gemini API Routes (protected)
270
130
  app.use('/api/gemini', authenticateToken, geminiRoutes);
271
131
  // Plugins API Routes (protected)
272
132
  app.use('/api/plugins', authenticateToken, pluginsRoutes);
273
- // Unified session messages route (protected)
274
- app.use('/api/sessions', authenticateToken, messagesRoutes);
133
+ // Unified provider MCP routes (protected)
134
+ app.use('/api/providers', authenticateToken, providerRoutes);
275
135
  // Agent API Routes (uses API key authentication)
276
136
  app.use('/api/agent', agentRoutes);
277
137
  // Serve public files (like api-docs.html)
@@ -360,152 +220,6 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
360
220
  });
361
221
  }
362
222
  });
363
- app.get('/api/projects', authenticateToken, async (req, res) => {
364
- try {
365
- const projects = await getProjects(broadcastProgress);
366
- res.json(projects);
367
- }
368
- catch (error) {
369
- res.status(500).json({ error: error.message });
370
- }
371
- });
372
- app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
373
- try {
374
- const { limit = 5, offset = 0 } = req.query;
375
- const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
376
- applyCustomSessionNames(result.sessions, 'claude');
377
- res.json(result);
378
- }
379
- catch (error) {
380
- res.status(500).json({ error: error.message });
381
- }
382
- });
383
- // Rename project endpoint
384
- app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
385
- try {
386
- const { displayName } = req.body;
387
- await renameProject(req.params.projectName, displayName);
388
- res.json({ success: true });
389
- }
390
- catch (error) {
391
- res.status(500).json({ error: error.message });
392
- }
393
- });
394
- // Delete session endpoint
395
- app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
396
- try {
397
- const { projectName, sessionId } = req.params;
398
- console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
399
- await deleteSession(projectName, sessionId);
400
- sessionNamesDb.deleteName(sessionId, 'claude');
401
- console.log(`[API] Session ${sessionId} deleted successfully`);
402
- res.json({ success: true });
403
- }
404
- catch (error) {
405
- console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
406
- res.status(500).json({ error: error.message });
407
- }
408
- });
409
- // Rename session endpoint
410
- app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
411
- try {
412
- const { sessionId } = req.params;
413
- const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
414
- if (!safeSessionId || safeSessionId !== String(sessionId)) {
415
- return res.status(400).json({ error: 'Invalid sessionId' });
416
- }
417
- const { summary, provider } = req.body;
418
- if (!summary || typeof summary !== 'string' || summary.trim() === '') {
419
- return res.status(400).json({ error: 'Summary is required' });
420
- }
421
- if (summary.trim().length > 500) {
422
- return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
423
- }
424
- if (!provider || !VALID_PROVIDERS.includes(provider)) {
425
- return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
426
- }
427
- sessionNamesDb.setName(safeSessionId, provider, summary.trim());
428
- res.json({ success: true });
429
- }
430
- catch (error) {
431
- console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
432
- res.status(500).json({ error: error.message });
433
- }
434
- });
435
- // Delete project endpoint
436
- // force=true to allow removal even when sessions exist
437
- // deleteData=true to also delete session/memory files on disk (destructive)
438
- app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
439
- try {
440
- const { projectName } = req.params;
441
- const force = req.query.force === 'true';
442
- const deleteData = req.query.deleteData === 'true';
443
- await deleteProject(projectName, force, deleteData);
444
- res.json({ success: true });
445
- }
446
- catch (error) {
447
- res.status(500).json({ error: error.message });
448
- }
449
- });
450
- // Create project endpoint
451
- app.post('/api/projects/create', authenticateToken, async (req, res) => {
452
- try {
453
- const { path: projectPath } = req.body;
454
- if (!projectPath || !projectPath.trim()) {
455
- return res.status(400).json({ error: 'Project path is required' });
456
- }
457
- const project = await addProjectManually(projectPath.trim());
458
- res.json({ success: true, project });
459
- }
460
- catch (error) {
461
- console.error('Error creating project:', error);
462
- res.status(500).json({ error: error.message });
463
- }
464
- });
465
- // Search conversations content (SSE streaming)
466
- app.get('/api/search/conversations', authenticateToken, async (req, res) => {
467
- const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
468
- const parsedLimit = Number.parseInt(String(req.query.limit), 10);
469
- const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100));
470
- if (query.length < 2) {
471
- return res.status(400).json({ error: 'Query must be at least 2 characters' });
472
- }
473
- res.writeHead(200, {
474
- 'Content-Type': 'text/event-stream',
475
- 'Cache-Control': 'no-cache',
476
- 'Connection': 'keep-alive',
477
- 'X-Accel-Buffering': 'no',
478
- });
479
- let closed = false;
480
- const abortController = new AbortController();
481
- req.on('close', () => { closed = true; abortController.abort(); });
482
- try {
483
- await searchConversations(query, limit, ({ projectResult, totalMatches, scannedProjects, totalProjects }) => {
484
- if (closed)
485
- return;
486
- if (projectResult) {
487
- res.write(`event: result\ndata: ${JSON.stringify({ projectResult, totalMatches, scannedProjects, totalProjects })}\n\n`);
488
- }
489
- else {
490
- res.write(`event: progress\ndata: ${JSON.stringify({ totalMatches, scannedProjects, totalProjects })}\n\n`);
491
- }
492
- }, abortController.signal);
493
- if (!closed) {
494
- res.write(`event: done\ndata: {}\n\n`);
495
- }
496
- }
497
- catch (error) {
498
- console.error('Error searching conversations:', error);
499
- if (!closed) {
500
- res.write(`event: error\ndata: ${JSON.stringify({ error: 'Search failed' })}\n\n`);
501
- }
502
- }
503
- finally {
504
- if (!closed) {
505
- res.end();
506
- }
507
- }
508
- });
509
223
  const expandWorkspacePath = (inputPath) => {
510
224
  if (!inputPath)
511
225
  return inputPath;
@@ -636,15 +350,17 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
636
350
  }
637
351
  });
638
352
  // Read file content endpoint
639
- app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
353
+ app.get('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
640
354
  try {
641
- const { projectName } = req.params;
355
+ const { projectId } = req.params;
642
356
  const { filePath } = req.query;
643
357
  // Security: ensure the requested path is inside the project root
644
358
  if (!filePath) {
645
359
  return res.status(400).json({ error: 'Invalid file path' });
646
360
  }
647
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
361
+ // Resolve the absolute project root via the DB-backed helper; the
362
+ // caller passes the DB-assigned `projectId`, not a folder name.
363
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
648
364
  if (!projectRoot) {
649
365
  return res.status(404).json({ error: 'Project not found' });
650
366
  }
@@ -673,15 +389,16 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
673
389
  }
674
390
  });
675
391
  // Serve raw file bytes for previews and downloads.
676
- app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
392
+ app.get('/api/projects/:projectId/files/content', authenticateToken, async (req, res) => {
677
393
  try {
678
- const { projectName } = req.params;
394
+ const { projectId } = req.params;
679
395
  const { path: filePath } = req.query;
680
396
  // Security: ensure the requested path is inside the project root
681
397
  if (!filePath) {
682
398
  return res.status(400).json({ error: 'Invalid file path' });
683
399
  }
684
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
400
+ // Projects are now addressed by DB `projectId`, resolved to their path here.
401
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
685
402
  if (!projectRoot) {
686
403
  return res.status(404).json({ error: 'Project not found' });
687
404
  }
@@ -722,9 +439,9 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
722
439
  }
723
440
  });
724
441
  // Save file content endpoint
725
- app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
442
+ app.put('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
726
443
  try {
727
- const { projectName } = req.params;
444
+ const { projectId } = req.params;
728
445
  const { filePath, content } = req.body;
729
446
  // Security: ensure the requested path is inside the project root
730
447
  if (!filePath) {
@@ -733,7 +450,8 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
733
450
  if (content === undefined) {
734
451
  return res.status(400).json({ error: 'Content is required' });
735
452
  }
736
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
453
+ // Projects are now addressed by DB `projectId`, resolved to their path here.
454
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
737
455
  if (!projectRoot) {
738
456
  return res.status(404).json({ error: 'Project not found' });
739
457
  }
@@ -766,18 +484,14 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
766
484
  }
767
485
  }
768
486
  });
769
- app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
487
+ app.get('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
770
488
  try {
771
489
  // Using fsPromises from import
772
- // Use extractProjectDirectory to get the actual project path
773
- let actualPath;
774
- try {
775
- actualPath = await extractProjectDirectory(req.params.projectName);
776
- }
777
- catch (error) {
778
- console.error('Error extracting project directory:', error);
779
- // Fallback to simple dash replacement
780
- actualPath = req.params.projectName.replace(/-/g, '/');
490
+ // Resolve the project's absolute path through the DB (projectId is the
491
+ // primary key of the `projects` table after the identifier migration).
492
+ const actualPath = await projectsDb.getProjectPathById(req.params.projectId);
493
+ if (!actualPath) {
494
+ return res.status(404).json({ error: 'Project not found' });
781
495
  }
782
496
  // Check if path exists
783
497
  try {
@@ -838,10 +552,10 @@ function validateFilename(name) {
838
552
  }
839
553
  return { valid: true };
840
554
  }
841
- // POST /api/projects/:projectName/files/create - Create new file or directory
842
- app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => {
555
+ // POST /api/projects/:projectId/files/create - Create new file or directory
556
+ app.post('/api/projects/:projectId/files/create', authenticateToken, async (req, res) => {
843
557
  try {
844
- const { projectName } = req.params;
558
+ const { projectId } = req.params;
845
559
  const { path: parentPath, type, name } = req.body;
846
560
  // Validate input
847
561
  if (!name || !type) {
@@ -854,8 +568,8 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
854
568
  if (!nameValidation.valid) {
855
569
  return res.status(400).json({ error: nameValidation.error });
856
570
  }
857
- // Get project root
858
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
571
+ // Resolve the project directory through the DB using the new projectId.
572
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
859
573
  if (!projectRoot) {
860
574
  return res.status(404).json({ error: 'Project not found' });
861
575
  }
@@ -911,10 +625,10 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
911
625
  }
912
626
  }
913
627
  });
914
- // PUT /api/projects/:projectName/files/rename - Rename file or directory
915
- app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => {
628
+ // PUT /api/projects/:projectId/files/rename - Rename file or directory
629
+ app.put('/api/projects/:projectId/files/rename', authenticateToken, async (req, res) => {
916
630
  try {
917
- const { projectName } = req.params;
631
+ const { projectId } = req.params;
918
632
  const { oldPath, newName } = req.body;
919
633
  // Validate input
920
634
  if (!oldPath || !newName) {
@@ -924,8 +638,8 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
924
638
  if (!nameValidation.valid) {
925
639
  return res.status(400).json({ error: nameValidation.error });
926
640
  }
927
- // Get project root
928
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
641
+ // Resolve the project directory through the DB using the new projectId.
642
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
929
643
  if (!projectRoot) {
930
644
  return res.status(404).json({ error: 'Project not found' });
931
645
  }
@@ -983,17 +697,17 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
983
697
  }
984
698
  }
985
699
  });
986
- // DELETE /api/projects/:projectName/files - Delete file or directory
987
- app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
700
+ // DELETE /api/projects/:projectId/files - Delete file or directory
701
+ app.delete('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
988
702
  try {
989
- const { projectName } = req.params;
703
+ const { projectId } = req.params;
990
704
  const { path: targetPath, type } = req.body;
991
705
  // Validate input
992
706
  if (!targetPath) {
993
707
  return res.status(400).json({ error: 'Path is required' });
994
708
  }
995
- // Get project root
996
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
709
+ // Resolve the project directory through the DB using the new projectId.
710
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
997
711
  if (!projectRoot) {
998
712
  return res.status(404).json({ error: 'Project not found' });
999
713
  }
@@ -1045,7 +759,7 @@ app.delete('/api/projects/:projectName/files', authenticateToken, async (req, re
1045
759
  }
1046
760
  }
1047
761
  });
1048
- // POST /api/projects/:projectName/files/upload - Upload files
762
+ // POST /api/projects/:projectId/files/upload - Upload files
1049
763
  // Dynamic import of multer for file uploads
1050
764
  const uploadFilesHandler = async (req, res) => {
1051
765
  // Dynamic import of multer
@@ -1081,7 +795,7 @@ const uploadFilesHandler = async (req, res) => {
1081
795
  return res.status(500).json({ error: err.message });
1082
796
  }
1083
797
  try {
1084
- const { projectName } = req.params;
798
+ const { projectId } = req.params;
1085
799
  const { targetPath, relativePaths } = req.body;
1086
800
  // Parse relative paths if provided (for folder uploads)
1087
801
  let filePaths = [];
@@ -1094,7 +808,7 @@ const uploadFilesHandler = async (req, res) => {
1094
808
  }
1095
809
  }
1096
810
  console.log('[DEBUG] File upload request:', {
1097
- projectName,
811
+ projectId,
1098
812
  targetPath: JSON.stringify(targetPath),
1099
813
  targetPathType: typeof targetPath,
1100
814
  filesCount: req.files?.length,
@@ -1103,8 +817,8 @@ const uploadFilesHandler = async (req, res) => {
1103
817
  if (!req.files || req.files.length === 0) {
1104
818
  return res.status(400).json({ error: 'No files provided' });
1105
819
  }
1106
- // Get project root
1107
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
820
+ // Resolve the project directory through the DB using the new projectId.
821
+ const projectRoot = await projectsDb.getProjectPathById(projectId);
1108
822
  if (!projectRoot) {
1109
823
  return res.status(404).json({ error: 'Project not found' });
1110
824
  }
@@ -1194,585 +908,11 @@ const uploadFilesHandler = async (req, res) => {
1194
908
  }
1195
909
  });
1196
910
  };
1197
- app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
1198
- /**
1199
- * Proxy an authenticated client WebSocket to a plugin's internal WS server.
1200
- * Auth is enforced by verifyClient before this function is reached.
1201
- */
1202
- function handlePluginWsProxy(clientWs, pathname) {
1203
- const pluginName = pathname.replace('/plugin-ws/', '');
1204
- if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) {
1205
- clientWs.close(4400, 'Invalid plugin name');
1206
- return;
1207
- }
1208
- const port = getPluginPort(pluginName);
1209
- if (!port) {
1210
- clientWs.close(4404, 'Plugin not running');
1211
- return;
1212
- }
1213
- const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1214
- upstream.on('open', () => {
1215
- console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
1216
- });
1217
- // Relay messages bidirectionally
1218
- upstream.on('message', (data) => {
1219
- if (clientWs.readyState === WebSocket.OPEN)
1220
- clientWs.send(data);
1221
- });
1222
- clientWs.on('message', (data) => {
1223
- if (upstream.readyState === WebSocket.OPEN)
1224
- upstream.send(data);
1225
- });
1226
- // Propagate close in both directions
1227
- upstream.on('close', () => { if (clientWs.readyState === WebSocket.OPEN)
1228
- clientWs.close(); });
1229
- clientWs.on('close', () => { if (upstream.readyState === WebSocket.OPEN)
1230
- upstream.close(); });
1231
- upstream.on('error', (err) => {
1232
- console.error(`[Plugins] WS proxy error for "${pluginName}":`, err.message);
1233
- if (clientWs.readyState === WebSocket.OPEN)
1234
- clientWs.close(4502, 'Upstream error');
1235
- });
1236
- clientWs.on('error', () => {
1237
- if (upstream.readyState === WebSocket.OPEN)
1238
- upstream.close();
1239
- });
1240
- }
1241
- // WebSocket connection handler that routes based on URL path
1242
- wss.on('connection', (ws, request) => {
1243
- const url = request.url;
1244
- console.log('[INFO] Client connected to:', url);
1245
- // Parse URL to get pathname without query parameters
1246
- const urlObj = new URL(url, 'http://localhost');
1247
- const pathname = urlObj.pathname;
1248
- if (pathname === '/shell') {
1249
- handleShellConnection(ws);
1250
- }
1251
- else if (pathname === '/ws') {
1252
- handleChatConnection(ws, request);
1253
- }
1254
- else if (pathname.startsWith('/plugin-ws/')) {
1255
- handlePluginWsProxy(ws, pathname);
1256
- }
1257
- else {
1258
- console.log('[WARN] Unknown WebSocket path:', pathname);
1259
- ws.close();
1260
- }
1261
- });
1262
- /**
1263
- * WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
1264
- *
1265
- * Provider files use `createNormalizedMessage()` from `providers/types.js` and
1266
- * adapter `normalizeMessage()` to produce unified NormalizedMessage events.
1267
- * The writer simply serialises and sends.
1268
- */
1269
- class WebSocketWriter {
1270
- constructor(ws, userId = null) {
1271
- this.ws = ws;
1272
- this.sessionId = null;
1273
- this.userId = userId;
1274
- this.isWebSocketWriter = true; // Marker for transport detection
1275
- }
1276
- send(data) {
1277
- if (this.ws.readyState === 1) { // WebSocket.OPEN
1278
- this.ws.send(JSON.stringify(data));
1279
- }
1280
- }
1281
- updateWebSocket(newRawWs) {
1282
- this.ws = newRawWs;
1283
- }
1284
- setSessionId(sessionId) {
1285
- this.sessionId = sessionId;
1286
- }
1287
- getSessionId() {
1288
- return this.sessionId;
1289
- }
1290
- }
1291
- // Handle chat WebSocket connections
1292
- function handleChatConnection(ws, request) {
1293
- console.log('[INFO] Chat WebSocket connected');
1294
- // Add to connected clients for project updates
1295
- connectedClients.add(ws);
1296
- // Wrap WebSocket with writer for consistent interface with SSEStreamWriter
1297
- const writer = new WebSocketWriter(ws, request?.user?.id ?? request?.user?.userId ?? null);
1298
- ws.on('message', async (message) => {
1299
- try {
1300
- const data = JSON.parse(message);
1301
- if (data.type === 'claude-command') {
1302
- console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
1303
- console.log('📁 Project:', data.options?.projectPath || 'Unknown');
1304
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1305
- // Use Claude Agents SDK
1306
- await queryClaudeSDK(data.command, data.options, writer);
1307
- }
1308
- else if (data.type === 'cursor-command') {
1309
- console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
1310
- console.log('📁 Project:', data.options?.cwd || 'Unknown');
1311
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1312
- console.log('🤖 Model:', data.options?.model || 'default');
1313
- await spawnCursor(data.command, data.options, writer);
1314
- }
1315
- else if (data.type === 'codex-command') {
1316
- console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
1317
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1318
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1319
- console.log('🤖 Model:', data.options?.model || 'default');
1320
- await queryCodex(data.command, data.options, writer);
1321
- }
1322
- else if (data.type === 'gemini-command') {
1323
- console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
1324
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1325
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1326
- console.log('🤖 Model:', data.options?.model || 'default');
1327
- await spawnGemini(data.command, data.options, writer);
1328
- }
1329
- else if (data.type === 'cursor-resume') {
1330
- // Backward compatibility: treat as cursor-command with resume and no prompt
1331
- console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
1332
- await spawnCursor('', {
1333
- sessionId: data.sessionId,
1334
- resume: true,
1335
- cwd: data.options?.cwd
1336
- }, writer);
1337
- }
1338
- else if (data.type === 'abort-session') {
1339
- console.log('[DEBUG] Abort session request:', data.sessionId);
1340
- const provider = data.provider || 'claude';
1341
- let success;
1342
- if (provider === 'cursor') {
1343
- success = abortCursorSession(data.sessionId);
1344
- }
1345
- else if (provider === 'codex') {
1346
- success = abortCodexSession(data.sessionId);
1347
- }
1348
- else if (provider === 'gemini') {
1349
- success = abortGeminiSession(data.sessionId);
1350
- }
1351
- else {
1352
- // Use Claude Agents SDK
1353
- success = await abortClaudeSDKSession(data.sessionId);
1354
- }
1355
- writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider }));
1356
- }
1357
- else if (data.type === 'claude-permission-response') {
1358
- // Relay UI approval decisions back into the SDK control flow.
1359
- // This does not persist permissions; it only resolves the in-flight request,
1360
- // introduced so the SDK can resume once the user clicks Allow/Deny.
1361
- if (data.requestId) {
1362
- resolveToolApproval(data.requestId, {
1363
- allow: Boolean(data.allow),
1364
- updatedInput: data.updatedInput,
1365
- message: data.message,
1366
- rememberEntry: data.rememberEntry
1367
- });
1368
- }
1369
- }
1370
- else if (data.type === 'cursor-abort') {
1371
- console.log('[DEBUG] Abort Cursor session:', data.sessionId);
1372
- const success = abortCursorSession(data.sessionId);
1373
- writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider: 'cursor' }));
1374
- }
1375
- else if (data.type === 'check-session-status') {
1376
- // Check if a specific session is currently processing
1377
- const provider = data.provider || 'claude';
1378
- const sessionId = data.sessionId;
1379
- let isActive;
1380
- if (provider === 'cursor') {
1381
- isActive = isCursorSessionActive(sessionId);
1382
- }
1383
- else if (provider === 'codex') {
1384
- isActive = isCodexSessionActive(sessionId);
1385
- }
1386
- else if (provider === 'gemini') {
1387
- isActive = isGeminiSessionActive(sessionId);
1388
- }
1389
- else {
1390
- // Use Claude Agents SDK
1391
- isActive = isClaudeSDKSessionActive(sessionId);
1392
- if (isActive) {
1393
- // Reconnect the session's writer to the new WebSocket so
1394
- // subsequent SDK output flows to the refreshed client.
1395
- reconnectSessionWriter(sessionId, ws);
1396
- }
1397
- }
1398
- writer.send({
1399
- type: 'session-status',
1400
- sessionId,
1401
- provider,
1402
- isProcessing: isActive
1403
- });
1404
- }
1405
- else if (data.type === 'get-pending-permissions') {
1406
- // Return pending permission requests for a session
1407
- const sessionId = data.sessionId;
1408
- if (sessionId && isClaudeSDKSessionActive(sessionId)) {
1409
- const pending = getPendingApprovalsForSession(sessionId);
1410
- writer.send({
1411
- type: 'pending-permissions-response',
1412
- sessionId,
1413
- data: pending
1414
- });
1415
- }
1416
- }
1417
- else if (data.type === 'get-active-sessions') {
1418
- // Get all currently active sessions
1419
- const activeSessions = {
1420
- claude: getActiveClaudeSDKSessions(),
1421
- cursor: getActiveCursorSessions(),
1422
- codex: getActiveCodexSessions(),
1423
- gemini: getActiveGeminiSessions()
1424
- };
1425
- writer.send({
1426
- type: 'active-sessions',
1427
- sessions: activeSessions
1428
- });
1429
- }
1430
- }
1431
- catch (error) {
1432
- console.error('[ERROR] Chat WebSocket error:', error.message);
1433
- writer.send({
1434
- type: 'error',
1435
- error: error.message
1436
- });
1437
- }
1438
- });
1439
- ws.on('close', () => {
1440
- console.log('🔌 Chat client disconnected');
1441
- // Remove from connected clients
1442
- connectedClients.delete(ws);
1443
- });
1444
- }
1445
- // Handle shell WebSocket connections
1446
- function handleShellConnection(ws) {
1447
- console.log('🐚 Shell client connected');
1448
- let shellProcess = null;
1449
- let ptySessionKey = null;
1450
- let urlDetectionBuffer = '';
1451
- const announcedAuthUrls = new Set();
1452
- ws.on('message', async (message) => {
1453
- try {
1454
- const data = JSON.parse(message);
1455
- console.log('📨 Shell message received:', data.type);
1456
- if (data.type === 'init') {
1457
- const projectPath = data.projectPath || process.cwd();
1458
- const sessionId = data.sessionId;
1459
- const hasSession = data.hasSession;
1460
- const provider = data.provider || 'claude';
1461
- const initialCommand = data.initialCommand;
1462
- const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
1463
- urlDetectionBuffer = '';
1464
- announcedAuthUrls.clear();
1465
- // Login commands (Claude/Cursor auth) should never reuse cached sessions
1466
- const isLoginCommand = initialCommand && (initialCommand.includes('setup-token') ||
1467
- initialCommand.includes('cursor-agent login') ||
1468
- initialCommand.includes('auth login'));
1469
- // Include command hash in session key so different commands get separate sessions
1470
- const commandSuffix = isPlainShell && initialCommand
1471
- ? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
1472
- : '';
1473
- ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
1474
- // Kill any existing login session before starting fresh
1475
- if (isLoginCommand) {
1476
- const oldSession = ptySessionsMap.get(ptySessionKey);
1477
- if (oldSession) {
1478
- console.log('🧹 Cleaning up existing login session:', ptySessionKey);
1479
- if (oldSession.timeoutId)
1480
- clearTimeout(oldSession.timeoutId);
1481
- if (oldSession.pty && oldSession.pty.kill)
1482
- oldSession.pty.kill();
1483
- ptySessionsMap.delete(ptySessionKey);
1484
- }
1485
- }
1486
- const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
1487
- if (existingSession) {
1488
- console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
1489
- shellProcess = existingSession.pty;
1490
- clearTimeout(existingSession.timeoutId);
1491
- ws.send(JSON.stringify({
1492
- type: 'output',
1493
- data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
1494
- }));
1495
- if (existingSession.buffer && existingSession.buffer.length > 0) {
1496
- console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
1497
- existingSession.buffer.forEach(bufferedData => {
1498
- ws.send(JSON.stringify({
1499
- type: 'output',
1500
- data: bufferedData
1501
- }));
1502
- });
1503
- }
1504
- existingSession.ws = ws;
1505
- return;
1506
- }
1507
- console.log('[INFO] Starting shell in:', projectPath);
1508
- console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
1509
- console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
1510
- if (initialCommand) {
1511
- console.log('⚡ Initial command:', initialCommand);
1512
- }
1513
- // First send a welcome message
1514
- let welcomeMsg;
1515
- if (isPlainShell) {
1516
- welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
1517
- }
1518
- else {
1519
- const providerName = provider === 'cursor' ? 'Cursor' : (provider === 'codex' ? 'Codex' : (provider === 'gemini' ? 'Gemini' : 'Claude'));
1520
- welcomeMsg = hasSession ?
1521
- `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
1522
- `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
1523
- }
1524
- ws.send(JSON.stringify({
1525
- type: 'output',
1526
- data: welcomeMsg
1527
- }));
1528
- try {
1529
- // Validate projectPath — resolve to absolute and verify it exists
1530
- const resolvedProjectPath = path.resolve(projectPath);
1531
- try {
1532
- const stats = fs.statSync(resolvedProjectPath);
1533
- if (!stats.isDirectory()) {
1534
- throw new Error('Not a directory');
1535
- }
1536
- }
1537
- catch (pathErr) {
1538
- ws.send(JSON.stringify({ type: 'error', message: 'Invalid project path' }));
1539
- return;
1540
- }
1541
- // Validate sessionId — only allow safe characters
1542
- const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
1543
- if (sessionId && !safeSessionIdPattern.test(sessionId)) {
1544
- ws.send(JSON.stringify({ type: 'error', message: 'Invalid session ID' }));
1545
- return;
1546
- }
1547
- // Build shell command — use cwd for project path (never interpolate into shell string)
1548
- let shellCommand;
1549
- if (isPlainShell) {
1550
- // Plain shell mode - run the initial command in the project directory
1551
- shellCommand = initialCommand;
1552
- }
1553
- else if (provider === 'cursor') {
1554
- if (hasSession && sessionId) {
1555
- shellCommand = `cursor-agent --resume="${sessionId}"`;
1556
- }
1557
- else {
1558
- shellCommand = 'cursor-agent';
1559
- }
1560
- }
1561
- else if (provider === 'codex') {
1562
- // Use codex command; attempt to resume and fall back to a new session when the resume fails.
1563
- if (hasSession && sessionId) {
1564
- if (os.platform() === 'win32') {
1565
- // PowerShell syntax for fallback
1566
- shellCommand = `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
1567
- }
1568
- else {
1569
- shellCommand = `codex resume "${sessionId}" || codex`;
1570
- }
1571
- }
1572
- else {
1573
- shellCommand = 'codex';
1574
- }
1575
- }
1576
- else if (provider === 'gemini') {
1577
- const command = initialCommand || 'gemini';
1578
- let resumeId = sessionId;
1579
- if (hasSession && sessionId) {
1580
- try {
1581
- // Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names.
1582
- // The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
1583
- // We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
1584
- const sess = sessionManager.getSession(sessionId);
1585
- if (sess && sess.cliSessionId) {
1586
- resumeId = sess.cliSessionId;
1587
- // Validate the looked-up CLI session ID too
1588
- if (!safeSessionIdPattern.test(resumeId)) {
1589
- resumeId = null;
1590
- }
1591
- }
1592
- }
1593
- catch (err) {
1594
- console.error('Failed to get Gemini CLI session ID:', err);
1595
- }
1596
- }
1597
- if (hasSession && resumeId) {
1598
- shellCommand = `${command} --resume "${resumeId}"`;
1599
- }
1600
- else {
1601
- shellCommand = command;
1602
- }
1603
- }
1604
- else {
1605
- // Claude (default provider)
1606
- const command = initialCommand || 'claude';
1607
- if (hasSession && sessionId) {
1608
- if (os.platform() === 'win32') {
1609
- shellCommand = `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
1610
- }
1611
- else {
1612
- shellCommand = `claude --resume "${sessionId}" || claude`;
1613
- }
1614
- }
1615
- else {
1616
- shellCommand = command;
1617
- }
1618
- }
1619
- console.log('🔧 Executing shell command:', shellCommand);
1620
- // Use appropriate shell based on platform
1621
- const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
1622
- const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
1623
- // Use terminal dimensions from client if provided, otherwise use defaults
1624
- const termCols = data.cols || 80;
1625
- const termRows = data.rows || 24;
1626
- console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
1627
- shellProcess = pty.spawn(shell, shellArgs, {
1628
- name: 'xterm-256color',
1629
- cols: termCols,
1630
- rows: termRows,
1631
- cwd: resolvedProjectPath,
1632
- env: {
1633
- ...process.env,
1634
- TERM: 'xterm-256color',
1635
- COLORTERM: 'truecolor',
1636
- FORCE_COLOR: '3'
1637
- }
1638
- });
1639
- console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
1640
- ptySessionsMap.set(ptySessionKey, {
1641
- pty: shellProcess,
1642
- ws: ws,
1643
- buffer: [],
1644
- timeoutId: null,
1645
- projectPath,
1646
- sessionId
1647
- });
1648
- // Handle data output
1649
- shellProcess.onData((data) => {
1650
- const session = ptySessionsMap.get(ptySessionKey);
1651
- if (!session)
1652
- return;
1653
- if (session.buffer.length < 5000) {
1654
- session.buffer.push(data);
1655
- }
1656
- else {
1657
- session.buffer.shift();
1658
- session.buffer.push(data);
1659
- }
1660
- if (session.ws && session.ws.readyState === WebSocket.OPEN) {
1661
- let outputData = data;
1662
- const cleanChunk = stripAnsiSequences(data);
1663
- urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
1664
- outputData = outputData.replace(/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g, '[INFO] Opening in browser: $1');
1665
- const emitAuthUrl = (detectedUrl, autoOpen = false) => {
1666
- const normalizedUrl = normalizeDetectedUrl(detectedUrl);
1667
- if (!normalizedUrl)
1668
- return;
1669
- const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
1670
- if (isNewUrl) {
1671
- announcedAuthUrls.add(normalizedUrl);
1672
- session.ws.send(JSON.stringify({
1673
- type: 'auth_url',
1674
- url: normalizedUrl,
1675
- autoOpen
1676
- }));
1677
- }
1678
- };
1679
- const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
1680
- .map((url) => normalizeDetectedUrl(url))
1681
- .filter(Boolean);
1682
- // Prefer the most complete URL if shorter prefix variants are also present.
1683
- const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) => !urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url)));
1684
- dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
1685
- if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
1686
- const bestUrl = dedupedDetectedUrls.reduce((longest, current) => current.length > longest.length ? current : longest);
1687
- emitAuthUrl(bestUrl, true);
1688
- }
1689
- // Send regular output
1690
- session.ws.send(JSON.stringify({
1691
- type: 'output',
1692
- data: outputData
1693
- }));
1694
- }
1695
- });
1696
- // Handle process exit
1697
- shellProcess.onExit((exitCode) => {
1698
- console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
1699
- const session = ptySessionsMap.get(ptySessionKey);
1700
- if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
1701
- session.ws.send(JSON.stringify({
1702
- type: 'output',
1703
- data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
1704
- }));
1705
- }
1706
- if (session && session.timeoutId) {
1707
- clearTimeout(session.timeoutId);
1708
- }
1709
- ptySessionsMap.delete(ptySessionKey);
1710
- shellProcess = null;
1711
- });
1712
- }
1713
- catch (spawnError) {
1714
- console.error('[ERROR] Error spawning process:', spawnError);
1715
- ws.send(JSON.stringify({
1716
- type: 'output',
1717
- data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
1718
- }));
1719
- }
1720
- }
1721
- else if (data.type === 'input') {
1722
- // Send input to shell process
1723
- if (shellProcess && shellProcess.write) {
1724
- try {
1725
- shellProcess.write(data.data);
1726
- }
1727
- catch (error) {
1728
- console.error('Error writing to shell:', error);
1729
- }
1730
- }
1731
- else {
1732
- console.warn('No active shell process to send input to');
1733
- }
1734
- }
1735
- else if (data.type === 'resize') {
1736
- // Handle terminal resize
1737
- if (shellProcess && shellProcess.resize) {
1738
- console.log('Terminal resize requested:', data.cols, 'x', data.rows);
1739
- shellProcess.resize(data.cols, data.rows);
1740
- }
1741
- }
1742
- }
1743
- catch (error) {
1744
- console.error('[ERROR] Shell WebSocket error:', error.message);
1745
- if (ws.readyState === WebSocket.OPEN) {
1746
- ws.send(JSON.stringify({
1747
- type: 'output',
1748
- data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
1749
- }));
1750
- }
1751
- }
1752
- });
1753
- ws.on('close', () => {
1754
- console.log('🔌 Shell client disconnected');
1755
- if (ptySessionKey) {
1756
- const session = ptySessionsMap.get(ptySessionKey);
1757
- if (session) {
1758
- console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
1759
- session.ws = null;
1760
- session.timeoutId = setTimeout(() => {
1761
- console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
1762
- if (session.pty && session.pty.kill) {
1763
- session.pty.kill();
1764
- }
1765
- ptySessionsMap.delete(ptySessionKey);
1766
- }, PTY_SESSION_TIMEOUT);
1767
- }
1768
- }
1769
- });
1770
- ws.on('error', (error) => {
1771
- console.error('[ERROR] Shell WebSocket error:', error);
1772
- });
1773
- }
1774
- // Image upload endpoint
1775
- app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
911
+ app.post('/api/projects/:projectId/files/upload', authenticateToken, uploadFilesHandler);
912
+ // Image upload endpoint. Accepts the DB-assigned `projectId` (not a folder name)
913
+ // but the current implementation doesn't need to touch the project directory,
914
+ // so we just leave the param rename for consistency with the rest of the API.
915
+ app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req, res) => {
1776
916
  try {
1777
917
  const multer = (await import('multer')).default;
1778
918
  const path = (await import('path')).default;
@@ -1847,10 +987,11 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
1847
987
  res.status(500).json({ error: 'Internal server error' });
1848
988
  }
1849
989
  });
1850
- // Get token usage for a specific session
1851
- app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
990
+ // Get token usage for a specific session. `projectId` is the DB primary key;
991
+ // the Claude branch below resolves it to an absolute path via the DB.
992
+ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1852
993
  try {
1853
- const { projectName, sessionId } = req.params;
994
+ const { projectId, sessionId } = req.params;
1854
995
  const { provider = 'claude' } = req.query;
1855
996
  const homeDir = os.homedir();
1856
997
  // Allow only safe characters in sessionId
@@ -1947,14 +1088,13 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
1947
1088
  });
1948
1089
  }
1949
1090
  // Handle Claude sessions (default)
1950
- // Extract actual project path
1951
- let projectPath;
1952
- try {
1953
- projectPath = await extractProjectDirectory(projectName);
1954
- }
1955
- catch (error) {
1956
- console.error('Error extracting project directory:', error);
1957
- return res.status(500).json({ error: 'Failed to determine project path' });
1091
+ // Resolve the project path through the DB using the caller-supplied
1092
+ // `projectId`. Legacy code here called extractProjectDirectory with a
1093
+ // folder-encoded project name; the migration centralizes that lookup
1094
+ // in the projects table.
1095
+ const projectPath = await projectsDb.getProjectPathById(projectId);
1096
+ if (!projectPath) {
1097
+ return res.status(404).json({ error: 'Project not found' });
1958
1098
  }
1959
1099
  // Construct the JSONL file path
1960
1100
  // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
@@ -2043,6 +1183,27 @@ app.get('*', (req, res) => {
2043
1183
  res.redirect(`${req.protocol}://${redirectHost}:${VITE_PORT}`);
2044
1184
  }
2045
1185
  });
1186
+ // global error middleware must be last
1187
+ app.use((err, req, res, next) => {
1188
+ if (err instanceof AppError) {
1189
+ return res.status(err.statusCode).json({
1190
+ success: false,
1191
+ error: {
1192
+ code: err.code,
1193
+ message: err.message,
1194
+ details: err.details,
1195
+ },
1196
+ });
1197
+ }
1198
+ console.error(err);
1199
+ return res.status(500).json({
1200
+ success: false,
1201
+ error: {
1202
+ code: 'INTERNAL_ERROR',
1203
+ message: 'Internal server error',
1204
+ },
1205
+ });
1206
+ });
2046
1207
  // Helper function to convert permissions to rwx format
2047
1208
  function permToRwx(perm) {
2048
1209
  const r = perm & 4 ? 'r' : '-';
@@ -2152,12 +1313,13 @@ async function startServer() {
2152
1313
  console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
2153
1314
  console.log('');
2154
1315
  // Start watching the projects folder for changes
2155
- await setupProjectsWatcher();
1316
+ await initializeSessionsWatcher();
2156
1317
  // Start server-side plugin processes for enabled plugins
2157
1318
  startEnabledPluginServers().catch(err => {
2158
1319
  console.error('[Plugins] Error during startup:', err.message);
2159
1320
  });
2160
1321
  });
1322
+ await closeSessionsWatcher();
2161
1323
  // Clean up plugin processes on shutdown
2162
1324
  const shutdownPlugins = async () => {
2163
1325
  await stopAllPlugins();