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