@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
@@ -0,0 +1,453 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import pty, { type IPty } from 'node-pty';
6
+ import { WebSocket, type RawData } from 'ws';
7
+
8
+ import { parseIncomingJsonObject } from '@/shared/utils.js';
9
+
10
+ type ShellIncomingMessage = {
11
+ type?: string;
12
+ data?: string;
13
+ cols?: number;
14
+ rows?: number;
15
+ projectPath?: string;
16
+ sessionId?: string;
17
+ hasSession?: boolean;
18
+ provider?: string;
19
+ initialCommand?: string;
20
+ isPlainShell?: boolean;
21
+ };
22
+
23
+ type PtySessionEntry = {
24
+ pty: IPty;
25
+ ws: WebSocket | null;
26
+ buffer: string[];
27
+ timeoutId: NodeJS.Timeout | null;
28
+ projectPath: string;
29
+ sessionId: string | null;
30
+ };
31
+
32
+ const ptySessionsMap = new Map<string, PtySessionEntry>();
33
+ const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
34
+ const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
35
+
36
+ type ShellWebSocketDependencies = {
37
+ getSessionById: (sessionId: string) => { cliSessionId?: string } | null | undefined;
38
+ stripAnsiSequences: (content: string) => string;
39
+ normalizeDetectedUrl: (url: string) => string | null;
40
+ extractUrlsFromText: (content: string) => string[];
41
+ shouldAutoOpenUrlFromOutput: (content: string) => boolean;
42
+ };
43
+
44
+ /**
45
+ * Reads a string field from untyped payloads and falls back when absent.
46
+ */
47
+ function readString(value: unknown, fallback = ''): string {
48
+ return typeof value === 'string' ? value : fallback;
49
+ }
50
+
51
+ /**
52
+ * Reads a boolean field from untyped payloads and falls back when absent.
53
+ */
54
+ function readBoolean(value: unknown, fallback = false): boolean {
55
+ return typeof value === 'boolean' ? value : fallback;
56
+ }
57
+
58
+ /**
59
+ * Reads a finite number field from untyped payloads and falls back when absent.
60
+ */
61
+ function readNumber(value: unknown, fallback: number): number {
62
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
63
+ }
64
+
65
+ /**
66
+ * Parses incoming websocket shell messages and keeps processing safe when
67
+ * malformed payloads are received.
68
+ */
69
+ function parseShellMessage(rawMessage: RawData): ShellIncomingMessage | null {
70
+ const payload = parseIncomingJsonObject(rawMessage);
71
+ if (!payload) {
72
+ return null;
73
+ }
74
+
75
+ return payload as ShellIncomingMessage;
76
+ }
77
+
78
+ /**
79
+ * Resolves provider command line for plain shell and agent-backed shell modes.
80
+ */
81
+ function buildShellCommand(
82
+ message: ShellIncomingMessage,
83
+ dependencies: ShellWebSocketDependencies
84
+ ): string {
85
+ const hasSession = readBoolean(message.hasSession);
86
+ const sessionId = readString(message.sessionId);
87
+ const initialCommand = readString(message.initialCommand);
88
+ const provider = readString(message.provider, 'claude');
89
+ const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
90
+ const isPlainShell =
91
+ readBoolean(message.isPlainShell) ||
92
+ (!!initialCommand && !hasSession) ||
93
+ provider === 'plain-shell';
94
+
95
+ if (isPlainShell) {
96
+ return initialCommand;
97
+ }
98
+
99
+ if (provider === 'cursor') {
100
+ if (hasSession && sessionId) {
101
+ return `cursor-agent --resume="${sessionId}"`;
102
+ }
103
+ return 'cursor-agent';
104
+ }
105
+
106
+ if (provider === 'codex') {
107
+ if (hasSession && sessionId) {
108
+ if (os.platform() === 'win32') {
109
+ return `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
110
+ }
111
+ return `codex resume "${sessionId}" || codex`;
112
+ }
113
+ return 'codex';
114
+ }
115
+
116
+ if (provider === 'gemini') {
117
+ const command = initialCommand || 'gemini';
118
+ let resumeId = sessionId;
119
+ if (hasSession && sessionId) {
120
+ try {
121
+ const existingSession = dependencies.getSessionById(sessionId);
122
+ if (existingSession && existingSession.cliSessionId) {
123
+ resumeId = existingSession.cliSessionId;
124
+ if (!safeSessionIdPattern.test(resumeId)) {
125
+ resumeId = '';
126
+ }
127
+ }
128
+ } catch (error) {
129
+ console.error('Failed to get Gemini CLI session ID:', error);
130
+ }
131
+ }
132
+
133
+ if (hasSession && resumeId) {
134
+ return `${command} --resume "${resumeId}"`;
135
+ }
136
+ return command;
137
+ }
138
+
139
+ const command = initialCommand || 'claude';
140
+ if (hasSession && sessionId) {
141
+ if (os.platform() === 'win32') {
142
+ return `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
143
+ }
144
+ return `claude --resume "${sessionId}" || claude`;
145
+ }
146
+ return command;
147
+ }
148
+
149
+ /**
150
+ * Handles websocket connections used by the standalone shell terminal UI.
151
+ */
152
+ export function handleShellConnection(
153
+ ws: WebSocket,
154
+ dependencies: ShellWebSocketDependencies
155
+ ): void {
156
+ console.log('[INFO] Shell websocket connected');
157
+
158
+ let shellProcess: IPty | null = null;
159
+ let ptySessionKey: string | null = null;
160
+ let urlDetectionBuffer = '';
161
+ const announcedAuthUrls = new Set<string>();
162
+
163
+ ws.on('message', async (rawMessage) => {
164
+ try {
165
+ const data = parseShellMessage(rawMessage);
166
+ if (!data?.type) {
167
+ throw new Error('Invalid websocket payload');
168
+ }
169
+
170
+ if (data.type === 'init') {
171
+ const projectPath = readString(data.projectPath, process.cwd());
172
+ const sessionId = readString(data.sessionId) || null;
173
+ const hasSession = readBoolean(data.hasSession);
174
+ const provider = readString(data.provider, 'claude');
175
+ const initialCommand = readString(data.initialCommand);
176
+ const isPlainShell =
177
+ readBoolean(data.isPlainShell) ||
178
+ (!!initialCommand && !hasSession) ||
179
+ provider === 'plain-shell';
180
+
181
+ urlDetectionBuffer = '';
182
+ announcedAuthUrls.clear();
183
+
184
+ const isLoginCommand =
185
+ !!initialCommand &&
186
+ (initialCommand.includes('setup-token') ||
187
+ initialCommand.includes('cursor-agent login') ||
188
+ initialCommand.includes('auth login'));
189
+
190
+ const commandSuffix =
191
+ isPlainShell && initialCommand
192
+ ? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
193
+ : '';
194
+ ptySessionKey = `${projectPath}_${sessionId ?? 'default'}${commandSuffix}`;
195
+
196
+ if (isLoginCommand) {
197
+ const oldSession = ptySessionsMap.get(ptySessionKey);
198
+ if (oldSession) {
199
+ if (oldSession.timeoutId) {
200
+ clearTimeout(oldSession.timeoutId);
201
+ }
202
+ oldSession.pty.kill();
203
+ ptySessionsMap.delete(ptySessionKey);
204
+ }
205
+ }
206
+
207
+ const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
208
+ if (existingSession) {
209
+ shellProcess = existingSession.pty;
210
+ if (existingSession.timeoutId) {
211
+ clearTimeout(existingSession.timeoutId);
212
+ }
213
+
214
+ ws.send(
215
+ JSON.stringify({
216
+ type: 'output',
217
+ data: '\x1b[36m[Reconnected to existing session]\x1b[0m\r\n',
218
+ })
219
+ );
220
+
221
+ if (existingSession.buffer.length > 0) {
222
+ existingSession.buffer.forEach((bufferedData) => {
223
+ ws.send(
224
+ JSON.stringify({
225
+ type: 'output',
226
+ data: bufferedData,
227
+ })
228
+ );
229
+ });
230
+ }
231
+
232
+ existingSession.ws = ws;
233
+ return;
234
+ }
235
+
236
+ const resolvedProjectPath = path.resolve(projectPath);
237
+ try {
238
+ const stats = fs.statSync(resolvedProjectPath);
239
+ if (!stats.isDirectory()) {
240
+ throw new Error('Not a directory');
241
+ }
242
+ } catch {
243
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid project path' }));
244
+ return;
245
+ }
246
+
247
+ const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
248
+ if (sessionId && !safeSessionIdPattern.test(sessionId)) {
249
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid session ID' }));
250
+ return;
251
+ }
252
+
253
+ const shellCommand = buildShellCommand(data, dependencies);
254
+ const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
255
+ const shellArgs =
256
+ os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
257
+ const termCols = readNumber(data.cols, 80);
258
+ const termRows = readNumber(data.rows, 24);
259
+
260
+ shellProcess = pty.spawn(shell, shellArgs, {
261
+ name: 'xterm-256color',
262
+ cols: termCols,
263
+ rows: termRows,
264
+ cwd: resolvedProjectPath,
265
+ env: {
266
+ ...process.env,
267
+ TERM: 'xterm-256color',
268
+ COLORTERM: 'truecolor',
269
+ FORCE_COLOR: '3',
270
+ },
271
+ });
272
+
273
+ ptySessionsMap.set(ptySessionKey, {
274
+ pty: shellProcess,
275
+ ws,
276
+ buffer: [],
277
+ timeoutId: null,
278
+ projectPath,
279
+ sessionId,
280
+ });
281
+
282
+ shellProcess.onData((chunk) => {
283
+ if (!ptySessionKey) {
284
+ return;
285
+ }
286
+
287
+ const session = ptySessionsMap.get(ptySessionKey);
288
+ if (!session) {
289
+ return;
290
+ }
291
+
292
+ if (session.buffer.length < 5000) {
293
+ session.buffer.push(chunk);
294
+ } else {
295
+ session.buffer.shift();
296
+ session.buffer.push(chunk);
297
+ }
298
+
299
+ if (session.ws && session.ws.readyState === WebSocket.OPEN) {
300
+ let outputData = chunk;
301
+ const cleanChunk = dependencies.stripAnsiSequences(chunk);
302
+ urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
303
+
304
+ outputData = outputData.replace(
305
+ /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
306
+ '[INFO] Opening in browser: $1'
307
+ );
308
+
309
+ const emitAuthUrl = (detectedUrl: string, autoOpen = false) => {
310
+ const normalizedUrl = dependencies.normalizeDetectedUrl(detectedUrl);
311
+ if (!normalizedUrl) {
312
+ return;
313
+ }
314
+
315
+ const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
316
+ if (isNewUrl) {
317
+ announcedAuthUrls.add(normalizedUrl);
318
+ session.ws?.send(
319
+ JSON.stringify({
320
+ type: 'auth_url',
321
+ url: normalizedUrl,
322
+ autoOpen,
323
+ })
324
+ );
325
+ }
326
+ };
327
+
328
+ const normalizedDetectedUrls = dependencies.extractUrlsFromText(urlDetectionBuffer)
329
+ .map((url) => dependencies.normalizeDetectedUrl(url))
330
+ .filter((url): url is string => Boolean(url));
331
+
332
+ const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter(
333
+ (url, _, urls) =>
334
+ !urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
335
+ );
336
+
337
+ dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
338
+
339
+ if (
340
+ dependencies.shouldAutoOpenUrlFromOutput(cleanChunk) &&
341
+ dedupedDetectedUrls.length > 0
342
+ ) {
343
+ const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
344
+ current.length > longest.length ? current : longest
345
+ );
346
+ emitAuthUrl(bestUrl, true);
347
+ }
348
+
349
+ session.ws.send(
350
+ JSON.stringify({
351
+ type: 'output',
352
+ data: outputData,
353
+ })
354
+ );
355
+ }
356
+ });
357
+
358
+ shellProcess.onExit((exitCode) => {
359
+ if (!ptySessionKey) {
360
+ return;
361
+ }
362
+
363
+ const session = ptySessionsMap.get(ptySessionKey);
364
+ if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
365
+ session.ws.send(
366
+ JSON.stringify({
367
+ type: 'output',
368
+ data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${
369
+ exitCode.signal != null ? ` (${exitCode.signal})` : ''
370
+ }\x1b[0m\r\n`,
371
+ })
372
+ );
373
+ }
374
+
375
+ if (session?.timeoutId) {
376
+ clearTimeout(session.timeoutId);
377
+ }
378
+
379
+ ptySessionsMap.delete(ptySessionKey);
380
+ shellProcess = null;
381
+ });
382
+
383
+ let welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
384
+ if (!isPlainShell) {
385
+ const providerName =
386
+ provider === 'cursor'
387
+ ? 'Cursor'
388
+ : provider === 'codex'
389
+ ? 'Codex'
390
+ : provider === 'gemini'
391
+ ? 'Gemini'
392
+ : 'Claude';
393
+ welcomeMsg = hasSession
394
+ ? `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n`
395
+ : `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
396
+ }
397
+
398
+ ws.send(
399
+ JSON.stringify({
400
+ type: 'output',
401
+ data: welcomeMsg,
402
+ })
403
+ );
404
+ return;
405
+ }
406
+
407
+ if (data.type === 'input') {
408
+ if (shellProcess) {
409
+ shellProcess.write(readString(data.data));
410
+ }
411
+ return;
412
+ }
413
+
414
+ if (data.type === 'resize') {
415
+ if (shellProcess) {
416
+ shellProcess.resize(readNumber(data.cols, 80), readNumber(data.rows, 24));
417
+ }
418
+ }
419
+ } catch (error) {
420
+ const message = error instanceof Error ? error.message : String(error);
421
+ console.error('[ERROR] Shell WebSocket error:', message);
422
+ if (ws.readyState === WebSocket.OPEN) {
423
+ ws.send(
424
+ JSON.stringify({
425
+ type: 'output',
426
+ data: `\r\n\x1b[31mError: ${message}\x1b[0m\r\n`,
427
+ })
428
+ );
429
+ }
430
+ }
431
+ });
432
+
433
+ ws.on('close', () => {
434
+ if (!ptySessionKey) {
435
+ return;
436
+ }
437
+
438
+ const session = ptySessionsMap.get(ptySessionKey);
439
+ if (!session) {
440
+ return;
441
+ }
442
+
443
+ session.ws = null;
444
+ session.timeoutId = setTimeout(() => {
445
+ session.pty.kill();
446
+ ptySessionsMap.delete(ptySessionKey as string);
447
+ }, PTY_SESSION_TIMEOUT);
448
+ });
449
+
450
+ ws.on('error', (error) => {
451
+ console.error('[ERROR] Shell WebSocket error:', error);
452
+ });
453
+ }
@@ -0,0 +1,54 @@
1
+ import type { VerifyClientCallbackSync } from 'ws';
2
+
3
+ import type { AuthenticatedWebSocketRequest } from '@/shared/types.js';
4
+
5
+ type WebSocketAuthDependencies = {
6
+ isPlatform: boolean;
7
+ authenticateWebSocket: (token: string | null) => {
8
+ id?: string | number;
9
+ userId?: string | number;
10
+ username?: string;
11
+ [key: string]: unknown;
12
+ } | null;
13
+ };
14
+
15
+ /**
16
+ * Authenticates websocket upgrade requests before the `connection` handler runs.
17
+ */
18
+ export function verifyWebSocketClient(
19
+ info: Parameters<VerifyClientCallbackSync<AuthenticatedWebSocketRequest>>[0],
20
+ dependencies: WebSocketAuthDependencies
21
+ ): boolean {
22
+ const request = info.req as AuthenticatedWebSocketRequest;
23
+ console.log('WebSocket connection attempt to:', request.url);
24
+
25
+ // Platform mode: use the first DB user and skip token checks.
26
+ if (dependencies.isPlatform) {
27
+ const user = dependencies.authenticateWebSocket(null);
28
+ if (!user) {
29
+ console.log('[WARN] Platform mode: No user found in database');
30
+ return false;
31
+ }
32
+
33
+ request.user = user;
34
+ console.log('[OK] Platform mode WebSocket authenticated for user:', user.username);
35
+ return true;
36
+ }
37
+
38
+ // OSS mode: read JWT from query string first, then Authorization header.
39
+ const upgradeUrl = new URL(request.url ?? '/', 'http://localhost');
40
+ const token =
41
+ upgradeUrl.searchParams.get('token') ??
42
+ request.headers.authorization?.split(' ')[1] ??
43
+ null;
44
+
45
+ const user = dependencies.authenticateWebSocket(token);
46
+ if (!user) {
47
+ console.log('[WARN] WebSocket authentication failed');
48
+ return false;
49
+ }
50
+
51
+ request.user = user;
52
+ console.log('[OK] WebSocket authenticated for user:', user.username);
53
+ return true;
54
+ }
@@ -0,0 +1,58 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+
3
+ import { WebSocketServer, type VerifyClientCallbackSync } from 'ws';
4
+
5
+ import { handleChatConnection } from '@/modules/websocket/services/chat-websocket.service.js';
6
+ import { verifyWebSocketClient } from '@/modules/websocket/services/websocket-auth.service.js';
7
+ import { handlePluginWsProxy } from '@/modules/websocket/services/plugin-websocket-proxy.service.js';
8
+ import { handleShellConnection } from '@/modules/websocket/services/shell-websocket.service.js';
9
+ import type { AuthenticatedWebSocketRequest } from '@/shared/types.js';
10
+
11
+ type WebSocketServerDependencies = {
12
+ verifyClient: Parameters<typeof verifyWebSocketClient>[1];
13
+ chat: Parameters<typeof handleChatConnection>[2];
14
+ shell: Parameters<typeof handleShellConnection>[1];
15
+ getPluginPort: Parameters<typeof handlePluginWsProxy>[2];
16
+ };
17
+
18
+ /**
19
+ * Creates and wires the server-wide websocket gateway used for chat, shell, and
20
+ * plugin proxy routes.
21
+ */
22
+ export function createWebSocketServer(
23
+ server: HttpServer,
24
+ dependencies: WebSocketServerDependencies
25
+ ): WebSocketServer {
26
+ const wss = new WebSocketServer({
27
+ server,
28
+ verifyClient: ((
29
+ info: Parameters<VerifyClientCallbackSync<AuthenticatedWebSocketRequest>>[0]
30
+ ) => verifyWebSocketClient(info, dependencies.verifyClient)),
31
+ });
32
+
33
+ wss.on('connection', (ws, request) => {
34
+ const incomingRequest = request as AuthenticatedWebSocketRequest;
35
+ const url = incomingRequest.url ?? '/';
36
+ const pathname = new URL(url, 'http://localhost').pathname;
37
+
38
+ if (pathname === '/shell') {
39
+ handleShellConnection(ws, dependencies.shell);
40
+ return;
41
+ }
42
+
43
+ if (pathname === '/ws') {
44
+ handleChatConnection(ws, incomingRequest, dependencies.chat);
45
+ return;
46
+ }
47
+
48
+ if (pathname.startsWith('/plugin-ws/')) {
49
+ handlePluginWsProxy(ws, pathname, dependencies.getPluginPort);
50
+ return;
51
+ }
52
+
53
+ console.log('[WARN] Unknown WebSocket path:', pathname);
54
+ ws.close();
55
+ });
56
+
57
+ return wss;
58
+ }
@@ -0,0 +1,16 @@
1
+ import type { RealtimeClientConnection } from '@/shared/types.js';
2
+
3
+ /**
4
+ * Numeric readyState for an open WebSocket connection.
5
+ *
6
+ * We keep this in module state so services that broadcast updates do not need
7
+ * to import `ws` directly just to compare open/closed state.
8
+ */
9
+ export const WS_OPEN_STATE = 1;
10
+
11
+ /**
12
+ * Shared registry of active chat WebSocket connections.
13
+ *
14
+ * Project/session services publish realtime updates by iterating this set.
15
+ */
16
+ export const connectedClients = new Set<RealtimeClientConnection>();
@@ -0,0 +1,38 @@
1
+ import { WS_OPEN_STATE } from '@/modules/websocket/services/websocket-state.service.js';
2
+ import type { RealtimeClientConnection } from '@/shared/types.js';
3
+
4
+ /**
5
+ * Thin transport adapter that gives WebSocket connections the same interface as
6
+ * SSE writers used by API routes (`send`, `setSessionId`, `getSessionId`).
7
+ */
8
+ export class WebSocketWriter {
9
+ ws: RealtimeClientConnection;
10
+ sessionId: string | null;
11
+ userId: string | number | null;
12
+ isWebSocketWriter: boolean;
13
+
14
+ constructor(ws: RealtimeClientConnection, userId: string | number | null = null) {
15
+ this.ws = ws;
16
+ this.sessionId = null;
17
+ this.userId = userId;
18
+ this.isWebSocketWriter = true;
19
+ }
20
+
21
+ send(data: unknown): void {
22
+ if (this.ws.readyState === WS_OPEN_STATE) {
23
+ this.ws.send(JSON.stringify(data));
24
+ }
25
+ }
26
+
27
+ updateWebSocket(newRawWs: RealtimeClientConnection): void {
28
+ this.ws = newRawWs;
29
+ }
30
+
31
+ setSessionId(sessionId: string): void {
32
+ this.sessionId = sessionId;
33
+ }
34
+
35
+ getSessionId(): string | null {
36
+ return this.sessionId;
37
+ }
38
+ }
@@ -4,8 +4,7 @@ import path from 'path';
4
4
  import os from 'os';
5
5
  import { promises as fs } from 'fs';
6
6
  import crypto from 'crypto';
7
- import { userDb, apiKeysDb, githubTokensDb } from '../database/db.js';
8
- import { addProjectManually } from '../projects.js';
7
+ import { userDb, apiKeysDb, githubTokensDb, projectsDb } from '../modules/database/index.js';
9
8
  import { queryClaudeSDK } from '../claude-sdk.js';
10
9
  import { spawnCursor } from '../cursor-cli.js';
11
10
  import { queryCodex } from '../openai-codex.js';
@@ -13,6 +12,7 @@ import { spawnGemini } from '../gemini-cli.js';
13
12
  import { Octokit } from '@octokit/rest';
14
13
  import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
15
14
  import { IS_PLATFORM } from '../constants/config.js';
15
+ import { normalizeProjectPath } from '../shared/utils.js';
16
16
 
17
17
  const router = express.Router();
18
18
 
@@ -890,7 +890,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
890
890
  finalProjectPath = await cloneGitHubRepo(githubUrl.trim(), tokenToUse, targetPath);
891
891
  } else {
892
892
  // Use existing project path
893
- finalProjectPath = path.resolve(projectPath);
893
+ finalProjectPath = normalizeProjectPath(path.resolve(projectPath));
894
894
 
895
895
  // Verify the path exists
896
896
  try {
@@ -900,19 +900,14 @@ router.post('/', validateExternalApiKey, async (req, res) => {
900
900
  }
901
901
  }
902
902
 
903
- // Register the project (or use existing registration)
904
- let project;
905
- try {
906
- project = await addProjectManually(finalProjectPath);
907
- console.log('📦 Project registered:', project);
908
- } catch (error) {
909
- // If project already exists, that's fine - continue with the existing registration
910
- if (error.message && error.message.includes('Project already configured')) {
911
- console.log('📦 Using existing project registration for:', finalProjectPath);
912
- project = { path: finalProjectPath };
913
- } else {
914
- throw error;
915
- }
903
+ finalProjectPath = normalizeProjectPath(finalProjectPath);
904
+
905
+ // Register project path in DB (or reuse existing active registration)
906
+ const registrationResult = projectsDb.createProjectPath(finalProjectPath, null);
907
+ if (registrationResult.outcome === 'active_conflict') {
908
+ console.log('Project registration already exists for:', finalProjectPath);
909
+ } else {
910
+ console.log('Project registered:', registrationResult.project);
916
911
  }
917
912
 
918
913
  // Set up writer based on streaming mode
@@ -1,9 +1,11 @@
1
1
  import express from 'express';
2
2
  import bcrypt from 'bcrypt';
3
- import { userDb, db } from '../database/db.js';
3
+ import { userDb } from '../modules/database/index.js';
4
+ import { getConnection } from '../modules/database/connection.js';
4
5
  import { generateToken, authenticateToken } from '../middleware/auth.js';
5
6
 
6
7
  const router = express.Router();
8
+ const db = getConnection();
7
9
 
8
10
  // Check auth status and setup requirements
9
11
  router.get('/status', async (req, res) => {
@@ -132,4 +134,4 @@ router.post('/logout', authenticateToken, (req, res) => {
132
134
  res.json({ success: true, message: 'Logged out successfully' });
133
135
  });
134
136
 
135
- export default router;
137
+ export default router;