@cloudcli-ai/cloudcli 1.30.0 → 1.31.1

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 (238) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-B37ufcoi.js +1262 -0
  3. package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
  4. package/dist/index.html +2 -2
  5. package/dist-server/server/index.js +115 -955
  6. package/dist-server/server/index.js.map +1 -1
  7. package/dist-server/server/middleware/auth.js +1 -1
  8. package/dist-server/server/middleware/auth.js.map +1 -1
  9. package/dist-server/server/modules/database/connection.js +125 -0
  10. package/dist-server/server/modules/database/connection.js.map +1 -0
  11. package/dist-server/server/modules/database/index.js +13 -0
  12. package/dist-server/server/modules/database/index.js.map +1 -0
  13. package/dist-server/server/modules/database/init-db.js +18 -0
  14. package/dist-server/server/modules/database/init-db.js.map +1 -0
  15. package/dist-server/server/modules/database/migrations.js +370 -0
  16. package/dist-server/server/modules/database/migrations.js.map +1 -0
  17. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  18. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  19. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  20. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  21. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  22. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  23. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  24. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  25. package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
  26. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
  28. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
  29. package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
  30. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  31. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  32. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  33. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  34. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  35. package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
  36. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  37. package/dist-server/server/modules/database/repositories/users.js +88 -0
  38. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  39. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  40. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  41. package/dist-server/server/modules/database/schema.js +143 -0
  42. package/dist-server/server/modules/database/schema.js.map +1 -0
  43. package/dist-server/server/modules/projects/index.js +4 -0
  44. package/dist-server/server/modules/projects/index.js.map +1 -0
  45. package/dist-server/server/modules/projects/projects.routes.js +188 -0
  46. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  47. package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
  48. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  49. package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
  50. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  51. package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
  52. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  53. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  54. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  55. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
  56. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  57. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
  58. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  59. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
  60. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  61. package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
  62. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  63. package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
  64. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  65. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
  66. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  67. package/dist-server/server/modules/providers/index.js +4 -0
  68. package/dist-server/server/modules/providers/index.js.map +1 -0
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
  70. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
  72. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  73. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  74. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
  76. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
  78. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  79. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  80. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  81. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
  82. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  84. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  85. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
  88. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  89. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  90. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  91. package/dist-server/server/modules/providers/provider.routes.js +172 -0
  92. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  93. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
  94. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  95. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
  96. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  97. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
  98. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  99. package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
  100. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  101. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  102. package/dist-server/server/modules/websocket/index.js +3 -0
  103. package/dist-server/server/modules/websocket/index.js.map +1 -0
  104. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
  105. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  106. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  107. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  108. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
  109. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  110. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
  111. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  112. package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
  113. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  114. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  115. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  116. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  117. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  118. package/dist-server/server/routes/agent.js +10 -17
  119. package/dist-server/server/routes/agent.js.map +1 -1
  120. package/dist-server/server/routes/auth.js +3 -1
  121. package/dist-server/server/routes/auth.js.map +1 -1
  122. package/dist-server/server/routes/commands.js +1 -1
  123. package/dist-server/server/routes/commands.js.map +1 -1
  124. package/dist-server/server/routes/gemini.js +2 -2
  125. package/dist-server/server/routes/gemini.js.map +1 -1
  126. package/dist-server/server/routes/git.js +33 -29
  127. package/dist-server/server/routes/git.js.map +1 -1
  128. package/dist-server/server/routes/settings.js +1 -1
  129. package/dist-server/server/routes/settings.js.map +1 -1
  130. package/dist-server/server/routes/taskmaster.js +97 -110
  131. package/dist-server/server/routes/taskmaster.js.map +1 -1
  132. package/dist-server/server/routes/user.js +1 -1
  133. package/dist-server/server/routes/user.js.map +1 -1
  134. package/dist-server/server/services/notification-orchestrator.js +2 -2
  135. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  136. package/dist-server/server/services/vapid-keys.js +2 -1
  137. package/dist-server/server/services/vapid-keys.js.map +1 -1
  138. package/dist-server/server/shared/types.js +0 -1
  139. package/dist-server/server/shared/types.js.map +1 -1
  140. package/dist-server/server/shared/utils.js +408 -7
  141. package/dist-server/server/shared/utils.js.map +1 -1
  142. package/dist-server/server/utils/taskmaster-websocket.js +18 -12
  143. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
  144. package/dist-server/shared/modelConstants.js +2 -1
  145. package/dist-server/shared/modelConstants.js.map +1 -1
  146. package/package.json +3 -1
  147. package/server/index.js +146 -1012
  148. package/server/middleware/auth.js +1 -1
  149. package/server/modules/database/connection.ts +143 -0
  150. package/server/modules/database/index.ts +12 -0
  151. package/server/modules/database/init-db.ts +17 -0
  152. package/server/modules/database/migrations.ts +442 -0
  153. package/server/modules/database/repositories/api-keys.ts +119 -0
  154. package/server/modules/database/repositories/app-config.ts +53 -0
  155. package/server/modules/database/repositories/credentials.ts +106 -0
  156. package/server/modules/database/repositories/github-tokens.ts +100 -0
  157. package/server/modules/database/repositories/notification-preferences.ts +103 -0
  158. package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
  159. package/server/modules/database/repositories/projects.db.ts +183 -0
  160. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  161. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  162. package/server/modules/database/repositories/sessions.db.ts +174 -0
  163. package/server/modules/database/repositories/users.ts +140 -0
  164. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  165. package/server/modules/database/schema.ts +152 -0
  166. package/server/modules/projects/index.ts +6 -0
  167. package/server/modules/projects/projects.routes.ts +247 -0
  168. package/server/modules/projects/services/project-clone.service.ts +321 -0
  169. package/server/modules/projects/services/project-delete.service.ts +75 -0
  170. package/server/modules/projects/services/project-management.service.ts +150 -0
  171. package/server/modules/projects/services/project-star.service.ts +78 -0
  172. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
  173. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
  174. package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
  175. package/server/modules/projects/tests/project-management.service.test.ts +117 -0
  176. package/server/modules/projects/tests/project-star.service.test.ts +123 -0
  177. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
  178. package/server/modules/providers/index.ts +4 -0
  179. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
  180. package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
  181. package/server/modules/providers/list/claude/claude.provider.ts +3 -1
  182. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
  183. package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
  184. package/server/modules/providers/list/codex/codex.provider.ts +3 -1
  185. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
  186. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
  187. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
  188. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
  189. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
  190. package/server/modules/providers/provider.routes.ts +220 -12
  191. package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
  192. package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
  193. package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
  194. package/server/modules/providers/services/sessions.service.ts +89 -4
  195. package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
  196. package/server/modules/websocket/README.md +267 -0
  197. package/server/modules/websocket/index.ts +2 -0
  198. package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
  199. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  200. package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
  201. package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
  202. package/server/modules/websocket/services/websocket-server.service.ts +58 -0
  203. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  204. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  205. package/server/routes/agent.js +11 -16
  206. package/server/routes/auth.js +4 -2
  207. package/server/routes/commands.js +1 -1
  208. package/server/routes/gemini.js +3 -2
  209. package/server/routes/git.js +33 -28
  210. package/server/routes/settings.js +1 -1
  211. package/server/routes/taskmaster.js +108 -111
  212. package/server/routes/user.js +1 -1
  213. package/server/services/notification-orchestrator.js +3 -2
  214. package/server/services/vapid-keys.js +2 -1
  215. package/server/shared/interfaces.ts +39 -1
  216. package/server/shared/types.ts +185 -30
  217. package/server/shared/utils.ts +485 -9
  218. package/server/utils/taskmaster-websocket.js +19 -13
  219. package/shared/modelConstants.js +2 -1
  220. package/dist/assets/index-RX-W_g_Q.js +0 -1262
  221. package/dist-server/server/database/db.js +0 -535
  222. package/dist-server/server/database/db.js.map +0 -1
  223. package/dist-server/server/database/schema.js +0 -97
  224. package/dist-server/server/database/schema.js.map +0 -1
  225. package/dist-server/server/projects.js +0 -2325
  226. package/dist-server/server/projects.js.map +0 -1
  227. package/dist-server/server/routes/codex.js +0 -18
  228. package/dist-server/server/routes/codex.js.map +0 -1
  229. package/dist-server/server/routes/messages.js +0 -56
  230. package/dist-server/server/routes/messages.js.map +0 -1
  231. package/dist-server/server/routes/projects.js +0 -505
  232. package/dist-server/server/routes/projects.js.map +0 -1
  233. package/server/database/db.js +0 -593
  234. package/server/database/schema.js +0 -102
  235. package/server/projects.js +0 -2555
  236. package/server/routes/codex.js +0 -19
  237. package/server/routes/messages.js +0 -61
  238. package/server/routes/projects.js +0 -548
@@ -1,7 +1,19 @@
1
-
2
1
  import { randomUUID } from 'node:crypto';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import fs from 'node:fs';
3
+ import {
4
+ access,
5
+ lstat,
6
+ mkdir,
7
+ readFile,
8
+ readdir,
9
+ readlink,
10
+ realpath,
11
+ stat,
12
+ writeFile,
13
+ } from 'node:fs/promises';
14
+ import os from 'node:os';
4
15
  import path from 'node:path';
16
+ import readline from 'node:readline';
5
17
 
6
18
  import type { NextFunction, Request, RequestHandler, Response } from 'express';
7
19
 
@@ -10,8 +22,17 @@ import type {
10
22
  ApiSuccessShape,
11
23
  AppErrorOptions,
12
24
  NormalizedMessage,
25
+ WorkspacePathValidationResult,
13
26
  } from '@/shared/types.js';
14
27
 
28
+ //----------------- NORMALIZED MESSAGE HELPER INPUT TYPES ------------
29
+ /**
30
+ * Input payload accepted by `createNormalizedMessage`.
31
+ *
32
+ * Callers provide provider-specific fields plus the required `kind/provider`
33
+ * pair; this helper fills missing envelope fields (`id`, `sessionId`,
34
+ * `timestamp`) in a consistent way.
35
+ */
15
36
  type NormalizedMessageInput =
16
37
  {
17
38
  kind: NormalizedMessage['kind'];
@@ -21,6 +42,14 @@ type NormalizedMessageInput =
21
42
  timestamp?: string | null;
22
43
  } & Record<string, unknown>;
23
44
 
45
+ // ---------------------------
46
+ //----------------- HTTP HANDLER UTILITIES ------------
47
+ /**
48
+ * Wraps arbitrary data in the standard API success envelope.
49
+ *
50
+ * Use this helper in route handlers to keep successful JSON responses consistent
51
+ * across endpoints.
52
+ */
24
53
  export function createApiSuccessResponse<TData>(
25
54
  data: TData,
26
55
  ): ApiSuccessShape<TData> {
@@ -30,6 +59,12 @@ export function createApiSuccessResponse<TData>(
30
59
  };
31
60
  }
32
61
 
62
+ /**
63
+ * Converts an async Express handler into a standard `RequestHandler` and routes
64
+ * rejected promises to Express error middleware.
65
+ *
66
+ * Use this to avoid repeating `try/catch(next)` in every async route.
67
+ */
33
68
  export function asyncHandler(
34
69
  handler: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
35
70
  ): RequestHandler {
@@ -38,7 +73,14 @@ export function asyncHandler(
38
73
  };
39
74
  }
40
75
 
41
- // --------- Global app error class for consistent error handling across the server ---------
76
+ // ---------------------------
77
+ //----------------- SHARED ERROR UTILITIES ------------
78
+ /**
79
+ * Shared application error with HTTP status and machine-readable code metadata.
80
+ *
81
+ * Throw this from service/route layers when the caller should receive a
82
+ * controlled error response rather than a generic 500.
83
+ */
42
84
  export class AppError extends Error {
43
85
  readonly code: string;
44
86
  readonly statusCode: number;
@@ -53,9 +95,226 @@ export class AppError extends Error {
53
95
  }
54
96
  }
55
97
 
56
- // -------------------------------------------------------------------------------------------
98
+ // ---------------------------
99
+ //----------------- WORKSPACE PATH VALIDATION UTILITIES ------------
100
+ /**
101
+ * Root directory that all workspace/project paths must stay under.
102
+ *
103
+ * This is resolved from `WORKSPACES_ROOT` when configured; otherwise it falls
104
+ * back to the current user's home directory.
105
+ */
106
+ export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir();
57
107
 
58
- // ------------------------ Normalized provider message helpers ------------------------
108
+ /**
109
+ * System-critical paths that must never be used as workspace roots.
110
+ *
111
+ * The validation helper blocks these values directly and also blocks paths
112
+ * nested under them (with explicit allow-list exceptions where necessary).
113
+ */
114
+ export const FORBIDDEN_WORKSPACE_PATHS = [
115
+ // Unix
116
+ '/',
117
+ '/etc',
118
+ '/bin',
119
+ '/sbin',
120
+ '/usr',
121
+ '/dev',
122
+ '/proc',
123
+ '/sys',
124
+ '/var',
125
+ '/boot',
126
+ '/root',
127
+ '/lib',
128
+ '/lib64',
129
+ '/opt',
130
+ '/tmp',
131
+ '/run',
132
+ // Windows
133
+ 'C:\\Windows',
134
+ 'C:\\Program Files',
135
+ 'C:\\Program Files (x86)',
136
+ 'C:\\ProgramData',
137
+ 'C:\\System Volume Information',
138
+ 'C:\\$Recycle.Bin',
139
+ ];
140
+
141
+ function stripWindowsLongPathPrefix(inputPath: string): string {
142
+ if (inputPath.startsWith('\\\\?\\UNC\\')) {
143
+ return `\\\\${inputPath.slice('\\\\?\\UNC\\'.length)}`;
144
+ }
145
+
146
+ if (inputPath.startsWith('\\\\?\\')) {
147
+ return inputPath.slice('\\\\?\\'.length);
148
+ }
149
+
150
+ return inputPath;
151
+ }
152
+
153
+ function shouldUseWindowsPathNormalization(inputPath: string): boolean {
154
+ if (process.platform === 'win32') {
155
+ return true;
156
+ }
157
+
158
+ return inputPath.startsWith('\\\\') || /^[a-zA-Z]:([\\/]|$)/.test(inputPath);
159
+ }
160
+
161
+ /**
162
+ * Canonicalizes project/workspace paths for stable DB keys and comparisons.
163
+ *
164
+ * Normalization rules:
165
+ * - trim whitespace
166
+ * - strip Windows long-path prefixes (`\\?\` and `\\?\UNC\`)
167
+ * - normalize path separators and dot segments
168
+ * - trim trailing separators except for filesystem roots
169
+ */
170
+ export function normalizeProjectPath(inputPath: string): string {
171
+ if (typeof inputPath !== 'string') {
172
+ return '';
173
+ }
174
+
175
+ const trimmed = inputPath.trim();
176
+ if (!trimmed) {
177
+ return '';
178
+ }
179
+
180
+ const withoutLongPrefix = stripWindowsLongPathPrefix(trimmed);
181
+ const useWindowsPathRules = shouldUseWindowsPathNormalization(withoutLongPrefix);
182
+ const normalized = useWindowsPathRules
183
+ ? path.win32.normalize(withoutLongPrefix)
184
+ : path.posix.normalize(withoutLongPrefix);
185
+
186
+ if (!normalized) {
187
+ return '';
188
+ }
189
+
190
+ const parser = useWindowsPathRules ? path.win32 : path.posix;
191
+ const root = parser.parse(normalized).root;
192
+ if (normalized === root) {
193
+ return normalized;
194
+ }
195
+
196
+ return normalized.replace(/[\\/]+$/, '');
197
+ }
198
+
199
+ /**
200
+ * Validates that a user-supplied workspace path is safe to use.
201
+ *
202
+ * Call this before any filesystem mutation that creates or registers projects.
203
+ * The function resolves symlinks, enforces `WORKSPACES_ROOT` containment, and
204
+ * blocks known system directories.
205
+ */
206
+ export async function validateWorkspacePath(requestedPath: string): Promise<WorkspacePathValidationResult> {
207
+ try {
208
+ const normalizedRequestedPath = normalizeProjectPath(requestedPath);
209
+ if (!normalizedRequestedPath) {
210
+ return {
211
+ valid: false,
212
+ error: 'Workspace path is required',
213
+ };
214
+ }
215
+
216
+ const absolutePath = path.resolve(normalizedRequestedPath);
217
+ const normalizedPath = normalizeProjectPath(absolutePath);
218
+
219
+ if (FORBIDDEN_WORKSPACE_PATHS.includes(normalizedPath) || normalizedPath === '/') {
220
+ return {
221
+ valid: false,
222
+ error: 'Cannot use system-critical directories as workspace locations',
223
+ };
224
+ }
225
+
226
+ for (const forbiddenPath of FORBIDDEN_WORKSPACE_PATHS) {
227
+ const normalizedForbiddenPath = normalizeProjectPath(forbiddenPath);
228
+ if (
229
+ normalizedPath === normalizedForbiddenPath
230
+ || normalizedPath.startsWith(`${normalizedForbiddenPath}${path.sep}`)
231
+ ) {
232
+ // Allow specific user-writable folders under /var.
233
+ if (
234
+ normalizedForbiddenPath === '/var'
235
+ && (normalizedPath.startsWith('/var/tmp') || normalizedPath.startsWith('/var/folders'))
236
+ ) {
237
+ continue;
238
+ }
239
+
240
+ return {
241
+ valid: false,
242
+ error: `Cannot create workspace in system directory: ${forbiddenPath}`,
243
+ };
244
+ }
245
+ }
246
+
247
+ let resolvedPath = normalizeProjectPath(absolutePath);
248
+ try {
249
+ await access(absolutePath);
250
+ resolvedPath = normalizeProjectPath(await realpath(absolutePath));
251
+ } catch (error) {
252
+ const fileError = error as NodeJS.ErrnoException;
253
+ if (fileError.code !== 'ENOENT') {
254
+ throw fileError;
255
+ }
256
+
257
+ const parentPath = path.dirname(absolutePath);
258
+ try {
259
+ const parentRealPath = await realpath(parentPath);
260
+ resolvedPath = normalizeProjectPath(path.join(parentRealPath, path.basename(absolutePath)));
261
+ } catch (parentError) {
262
+ const parentFileError = parentError as NodeJS.ErrnoException;
263
+ if (parentFileError.code !== 'ENOENT') {
264
+ throw parentFileError;
265
+ }
266
+ }
267
+ }
268
+
269
+ const resolvedWorkspaceRoot = normalizeProjectPath(await realpath(WORKSPACES_ROOT));
270
+ if (
271
+ !resolvedPath.startsWith(`${resolvedWorkspaceRoot}${path.sep}`)
272
+ && resolvedPath !== resolvedWorkspaceRoot
273
+ ) {
274
+ return {
275
+ valid: false,
276
+ error: `Workspace path must be within the allowed workspace root: ${WORKSPACES_ROOT}`,
277
+ };
278
+ }
279
+
280
+ try {
281
+ await access(absolutePath);
282
+ const pathStats = await lstat(absolutePath);
283
+ if (pathStats.isSymbolicLink()) {
284
+ const symlinkTarget = await readlink(absolutePath);
285
+ const resolvedSymlinkPath = path.resolve(path.dirname(absolutePath), symlinkTarget);
286
+ const realSymlinkPath = await realpath(resolvedSymlinkPath);
287
+ if (
288
+ !realSymlinkPath.startsWith(`${resolvedWorkspaceRoot}${path.sep}`)
289
+ && realSymlinkPath !== resolvedWorkspaceRoot
290
+ ) {
291
+ return {
292
+ valid: false,
293
+ error: 'Symlink target is outside the allowed workspace root',
294
+ };
295
+ }
296
+ }
297
+ } catch (error) {
298
+ const fileError = error as NodeJS.ErrnoException;
299
+ if (fileError.code !== 'ENOENT') {
300
+ throw fileError;
301
+ }
302
+ }
303
+
304
+ return {
305
+ valid: true,
306
+ resolvedPath,
307
+ };
308
+ } catch (error) {
309
+ return {
310
+ valid: false,
311
+ error: `Path validation failed: ${(error as Error).message}`,
312
+ };
313
+ }
314
+ }
315
+
316
+ // ---------------------------
317
+ //----------------- NORMALIZED PROVIDER MESSAGE UTILITIES ------------
59
318
  /**
60
319
  * Generates a stable unique id for normalized provider messages.
61
320
  */
@@ -80,9 +339,8 @@ export function createNormalizedMessage(fields: NormalizedMessageInput): Normali
80
339
  };
81
340
  }
82
341
 
83
- // -------------------------------------------------------------------------------------------
84
-
85
- // ------------------------ The following are mainly for provider MCP runtimes ------------------------
342
+ // ---------------------------
343
+ //----------------- MCP CONFIG PARSING UTILITIES ------------
86
344
  /**
87
345
  * Safely narrows an unknown value to a plain object record.
88
346
  *
@@ -154,6 +412,62 @@ export const readStringRecord = (value: unknown): Record<string, string> | undef
154
412
  return Object.keys(normalized).length > 0 ? normalized : undefined;
155
413
  };
156
414
 
415
+ // ---------------------------
416
+ //----------------- WEBSOCKET PAYLOAD PARSING UTILITIES ------------
417
+ /**
418
+ * Parses one websocket message payload into a plain JSON object record.
419
+ *
420
+ * Use this in realtime handlers that receive raw websocket payloads as `string`,
421
+ * `Buffer`, `ArrayBuffer`, or chunk arrays. The helper converts supported
422
+ * payload formats to UTF-8 text, parses JSON, and returns only object payloads.
423
+ * Primitive/array/invalid payloads return `null` so callers can handle bad input
424
+ * without throwing from deeply nested message handlers.
425
+ */
426
+ export const parseIncomingJsonObject = (payload: unknown): AnyRecord | null => {
427
+ let text: string | null = null;
428
+
429
+ if (typeof payload === 'string') {
430
+ text = payload;
431
+ } else if (Buffer.isBuffer(payload)) {
432
+ text = payload.toString('utf8');
433
+ } else if (payload instanceof ArrayBuffer) {
434
+ text = Buffer.from(payload).toString('utf8');
435
+ } else if (Array.isArray(payload)) {
436
+ const buffers = payload
437
+ .map((entry) => {
438
+ if (Buffer.isBuffer(entry)) {
439
+ return entry;
440
+ }
441
+
442
+ if (entry instanceof ArrayBuffer) {
443
+ return Buffer.from(entry);
444
+ }
445
+
446
+ if (ArrayBuffer.isView(entry)) {
447
+ return Buffer.from(entry.buffer, entry.byteOffset, entry.byteLength);
448
+ }
449
+
450
+ return null;
451
+ })
452
+ .filter((entry): entry is Buffer => entry !== null);
453
+
454
+ if (buffers.length > 0) {
455
+ text = Buffer.concat(buffers).toString('utf8');
456
+ }
457
+ }
458
+
459
+ if (typeof text !== 'string' || text.trim().length === 0) {
460
+ return null;
461
+ }
462
+
463
+ try {
464
+ const parsed = JSON.parse(text) as unknown;
465
+ return readObjectRecord(parsed);
466
+ } catch {
467
+ return null;
468
+ }
469
+ };
470
+
157
471
  /**
158
472
  * Reads a JSON config file and guarantees a plain object result.
159
473
  *
@@ -189,5 +503,167 @@ export const writeJsonConfig = async (filePath: string, data: Record<string, unk
189
503
  await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
190
504
  };
191
505
 
192
- // -------------------------------------------------------------------------------------------
506
+ // ---------------------------
507
+ //----------------- SESSION SYNCHRONIZER TITLE HELPERS ------------
508
+ /**
509
+ * Produces a compact session title suitable for UI rendering and DB storage.
510
+ *
511
+ * Use this when converting provider-native names into a consistent title value.
512
+ * The helper collapses repeated whitespace, trims the result, and truncates it
513
+ * to 120 characters so every provider writes stable and bounded metadata.
514
+ * If the normalized input is empty, it returns the supplied fallback title.
515
+ */
516
+ export function normalizeSessionName(rawValue: string | undefined, fallback: string): string {
517
+ const normalized = (rawValue ?? '').replace(/\s+/g, ' ').trim();
518
+ if (!normalized) {
519
+ return fallback;
520
+ }
521
+
522
+ return normalized.slice(0, 120);
523
+ }
524
+
525
+ // ---------------------------
526
+ //----------------- SESSION SYNCHRONIZER FILESYSTEM HELPERS ------------
527
+ /**
528
+ * Recursively discovers files that match one extension, with optional incremental filtering.
529
+ *
530
+ * Provider synchronizers call this to find transcript artifacts under provider
531
+ * home directories. Pass `lastScanAt` to include only files created after the
532
+ * previous scan, or pass `null` to perform a full rescan. Missing directories
533
+ * are treated as empty because not every provider exists on every machine.
534
+ */
535
+ export async function findFilesRecursivelyCreatedAfter(
536
+ rootDir: string,
537
+ extension: string,
538
+ lastScanAt: Date | null,
539
+ fileList: string[] = []
540
+ ): Promise<string[]> {
541
+ try {
542
+ const entries = await readdir(rootDir, { withFileTypes: true });
543
+ for (const entry of entries) {
544
+ const fullPath = path.join(rootDir, entry.name);
545
+
546
+ if (entry.isDirectory()) {
547
+ await findFilesRecursivelyCreatedAfter(fullPath, extension, lastScanAt, fileList);
548
+ continue;
549
+ }
550
+
551
+ if (!entry.isFile() || !entry.name.endsWith(extension)) {
552
+ continue;
553
+ }
554
+
555
+ if (!lastScanAt) {
556
+ fileList.push(fullPath);
557
+ continue;
558
+ }
559
+
560
+ const fileStat = await stat(fullPath);
561
+ if (fileStat.birthtime > lastScanAt) {
562
+ fileList.push(fullPath);
563
+ }
564
+ }
565
+ } catch {
566
+ // Missing provider folders are expected in first-run or partial setups.
567
+ }
568
+
569
+ return fileList;
570
+ }
571
+
572
+ /**
573
+ * Reads file creation/update timestamps and maps them to DB-friendly ISO strings.
574
+ *
575
+ * Session indexers use this to persist `created_at` and `updated_at` metadata
576
+ * when upserting sessions. If the file cannot be read, an empty object is
577
+ * returned so indexing can continue for other files.
578
+ */
579
+ export async function readFileTimestamps(
580
+ filePath: string
581
+ ): Promise<{ createdAt?: string; updatedAt?: string }> {
582
+ try {
583
+ const fileStat = await stat(filePath);
584
+ return {
585
+ createdAt: fileStat.birthtime.toISOString(),
586
+ updatedAt: fileStat.mtime.toISOString(),
587
+ };
588
+ } catch {
589
+ return {};
590
+ }
591
+ }
592
+
593
+ // ---------------------------
594
+ //----------------- SESSION SYNCHRONIZER JSONL PARSING HELPERS ------------
595
+ /**
596
+ * Builds a first-seen key/value lookup map from a JSONL file.
597
+ *
598
+ * Use this for provider index files where session id -> display name metadata
599
+ * is stored line-by-line. The first value for each key wins, preserving the
600
+ * earliest known label while avoiding repeated map overwrites.
601
+ */
602
+ export async function buildLookupMap(
603
+ filePath: string,
604
+ keyField: string,
605
+ valueField: string
606
+ ): Promise<Map<string, string>> {
607
+ const lookup = new Map<string, string>();
608
+
609
+ try {
610
+ const fileStream = fs.createReadStream(filePath);
611
+ const lineReader = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
612
+
613
+ for await (const line of lineReader) {
614
+ const trimmed = line.trim();
615
+ if (!trimmed) {
616
+ continue;
617
+ }
618
+
619
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
620
+ const key = parsed[keyField];
621
+ const value = parsed[valueField];
622
+
623
+ if (typeof key === 'string' && typeof value === 'string' && !lookup.has(key)) {
624
+ lookup.set(key, value);
625
+ }
626
+ }
627
+ } catch {
628
+ // Missing or unreadable lookup files should not block session sync.
629
+ }
630
+
631
+ return lookup;
632
+ }
633
+
634
+ /**
635
+ * Reads a JSONL file and returns the first extracted payload that matches caller criteria.
636
+ *
637
+ * The caller supplies an `extractor` that validates provider-specific row
638
+ * shapes. This helper centralizes line-by-line parsing and lets indexers stop
639
+ * scanning as soon as one valid row is found.
640
+ */
641
+ export async function extractFirstValidJsonlData<T>(
642
+ filePath: string,
643
+ extractor: (parsedJson: unknown) => T | null | undefined
644
+ ): Promise<T | null> {
645
+ try {
646
+ const fileStream = fs.createReadStream(filePath);
647
+ const lineReader = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
648
+
649
+ for await (const line of lineReader) {
650
+ const trimmed = line.trim();
651
+ if (!trimmed) {
652
+ continue;
653
+ }
654
+
655
+ const parsed = JSON.parse(trimmed);
656
+ const extracted = extractor(parsed);
657
+ if (extracted) {
658
+ lineReader.close();
659
+ fileStream.close();
660
+ return extracted;
661
+ }
662
+ }
663
+ } catch {
664
+ // Ignore malformed or missing artifacts so full scans keep progressing.
665
+ }
666
+
667
+ return null;
668
+ }
193
669
 
@@ -7,20 +7,25 @@
7
7
  */
8
8
 
9
9
  /**
10
- * Broadcast TaskMaster project update to all connected clients
10
+ * Broadcast TaskMaster project update to all connected clients.
11
+ *
12
+ * The payload key is `projectId` post-migration so frontend listeners can
13
+ * match notifications against the DB-assigned project identifier they
14
+ * already use everywhere else.
15
+ *
11
16
  * @param {WebSocket.Server} wss - WebSocket server instance
12
- * @param {string} projectName - Name of the updated project
17
+ * @param {string} projectId - DB id of the updated project
13
18
  * @param {Object} taskMasterData - Updated TaskMaster data
14
19
  */
15
- export function broadcastTaskMasterProjectUpdate(wss, projectName, taskMasterData) {
16
- if (!wss || !projectName) {
17
- console.warn('TaskMaster WebSocket broadcast: Missing wss or projectName');
20
+ export function broadcastTaskMasterProjectUpdate(wss, projectId, taskMasterData) {
21
+ if (!wss || !projectId) {
22
+ console.warn('TaskMaster WebSocket broadcast: Missing wss or projectId');
18
23
  return;
19
24
  }
20
25
 
21
26
  const message = {
22
27
  type: 'taskmaster-project-updated',
23
- projectName,
28
+ projectId,
24
29
  taskMasterData,
25
30
  timestamp: new Date().toISOString()
26
31
  };
@@ -38,20 +43,21 @@ export function broadcastTaskMasterProjectUpdate(wss, projectName, taskMasterDat
38
43
  }
39
44
 
40
45
  /**
41
- * Broadcast TaskMaster tasks update for a specific project
42
- * @param {WebSocket.Server} wss - WebSocket server instance
43
- * @param {string} projectName - Name of the project with updated tasks
46
+ * Broadcast TaskMaster tasks update for a specific project.
47
+ *
48
+ * @param {WebSocket.Server} wss - WebSocket server instance
49
+ * @param {string} projectId - DB id of the project with updated tasks
44
50
  * @param {Object} tasksData - Updated tasks data
45
51
  */
46
- export function broadcastTaskMasterTasksUpdate(wss, projectName, tasksData) {
47
- if (!wss || !projectName) {
48
- console.warn('TaskMaster WebSocket broadcast: Missing wss or projectName');
52
+ export function broadcastTaskMasterTasksUpdate(wss, projectId, tasksData) {
53
+ if (!wss || !projectId) {
54
+ console.warn('TaskMaster WebSocket broadcast: Missing wss or projectId');
49
55
  return;
50
56
  }
51
57
 
52
58
  const message = {
53
59
  type: 'taskmaster-tasks-updated',
54
- projectName,
60
+ projectId,
55
61
  tasksData,
56
62
  timestamp: new Date().toISOString()
57
63
  };
@@ -13,8 +13,8 @@
13
13
  export const CLAUDE_MODELS = {
14
14
  // Models in SDK format (what the actual SDK accepts)
15
15
  OPTIONS: [
16
- { value: "sonnet", label: "Sonnet" },
17
16
  { value: "opus", label: "Opus" },
17
+ { value: "sonnet", label: "Sonnet" },
18
18
  { value: "haiku", label: "Haiku" },
19
19
  { value: "claude-opus-4-6", label: "Opus 4.6" },
20
20
  { value: "opusplan", label: "Opus Plan" },
@@ -59,6 +59,7 @@ export const CURSOR_MODELS = {
59
59
  */
60
60
  export const CODEX_MODELS = {
61
61
  OPTIONS: [
62
+ { value: "gpt-5.5", label: "GPT-5.5" },
62
63
  { value: "gpt-5.4", label: "GPT-5.4" },
63
64
  { value: "gpt-5.4-mini", label: "GPT-5.4 mini" },
64
65
  { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" },