claude-wec 1.0.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 (137) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +371 -0
  3. package/dist/api-docs.html +879 -0
  4. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  6. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  56. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  63. package/dist/assets/index-cIxJ4RXb.js +1226 -0
  64. package/dist/assets/index-oyEz69sP.css +32 -0
  65. package/dist/assets/vendor-codemirror-CJLzwpLB.js +39 -0
  66. package/dist/assets/vendor-react-DcyRfQm3.js +59 -0
  67. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  68. package/dist/clear-cache.html +85 -0
  69. package/dist/convert-icons.md +53 -0
  70. package/dist/favicon.png +0 -0
  71. package/dist/favicon.svg +9 -0
  72. package/dist/generate-icons.js +49 -0
  73. package/dist/icons/claude-ai-icon.svg +1 -0
  74. package/dist/icons/codex-white.svg +3 -0
  75. package/dist/icons/codex.svg +3 -0
  76. package/dist/icons/cursor-white.svg +12 -0
  77. package/dist/icons/cursor.svg +1 -0
  78. package/dist/icons/generate-icons.md +19 -0
  79. package/dist/icons/icon-128x128.png +0 -0
  80. package/dist/icons/icon-128x128.svg +12 -0
  81. package/dist/icons/icon-144x144.png +0 -0
  82. package/dist/icons/icon-144x144.svg +12 -0
  83. package/dist/icons/icon-152x152.png +0 -0
  84. package/dist/icons/icon-152x152.svg +12 -0
  85. package/dist/icons/icon-192x192.png +0 -0
  86. package/dist/icons/icon-192x192.svg +12 -0
  87. package/dist/icons/icon-384x384.png +0 -0
  88. package/dist/icons/icon-384x384.svg +12 -0
  89. package/dist/icons/icon-512x512.png +0 -0
  90. package/dist/icons/icon-512x512.svg +12 -0
  91. package/dist/icons/icon-72x72.png +0 -0
  92. package/dist/icons/icon-72x72.svg +12 -0
  93. package/dist/icons/icon-96x96.png +0 -0
  94. package/dist/icons/icon-96x96.svg +12 -0
  95. package/dist/icons/icon-template.svg +12 -0
  96. package/dist/index.html +52 -0
  97. package/dist/logo-128.png +0 -0
  98. package/dist/logo-256.png +0 -0
  99. package/dist/logo-32.png +0 -0
  100. package/dist/logo-512.png +0 -0
  101. package/dist/logo-64.png +0 -0
  102. package/dist/logo.svg +17 -0
  103. package/dist/manifest.json +61 -0
  104. package/dist/screenshots/cli-selection.png +0 -0
  105. package/dist/screenshots/desktop-main.png +0 -0
  106. package/dist/screenshots/mobile-chat.png +0 -0
  107. package/dist/screenshots/tools-modal.png +0 -0
  108. package/dist/sw.js +49 -0
  109. package/package.json +109 -0
  110. package/server/claude-sdk.js +721 -0
  111. package/server/cli.js +327 -0
  112. package/server/cursor-cli.js +267 -0
  113. package/server/database/auth.db +0 -0
  114. package/server/database/db.js +361 -0
  115. package/server/database/init.sql +52 -0
  116. package/server/index.js +1747 -0
  117. package/server/middleware/auth.js +111 -0
  118. package/server/openai-codex.js +389 -0
  119. package/server/projects.js +1604 -0
  120. package/server/routes/agent.js +1230 -0
  121. package/server/routes/auth.js +135 -0
  122. package/server/routes/cli-auth.js +263 -0
  123. package/server/routes/codex.js +345 -0
  124. package/server/routes/commands.js +521 -0
  125. package/server/routes/cursor.js +795 -0
  126. package/server/routes/git.js +1128 -0
  127. package/server/routes/mcp-utils.js +48 -0
  128. package/server/routes/mcp.js +552 -0
  129. package/server/routes/projects.js +378 -0
  130. package/server/routes/settings.js +178 -0
  131. package/server/routes/taskmaster.js +1963 -0
  132. package/server/routes/user.js +106 -0
  133. package/server/utils/commandParser.js +303 -0
  134. package/server/utils/gitConfig.js +24 -0
  135. package/server/utils/mcp-detector.js +198 -0
  136. package/server/utils/taskmaster-websocket.js +129 -0
  137. package/shared/modelConstants.js +65 -0
@@ -0,0 +1,111 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { userDb } from '../database/db.js';
3
+
4
+ // Get JWT secret from environment or use default (for development)
5
+ const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
6
+
7
+ // Optional API key middleware
8
+ const validateApiKey = (req, res, next) => {
9
+ // Skip API key validation if not configured
10
+ if (!process.env.API_KEY) {
11
+ return next();
12
+ }
13
+
14
+ const apiKey = req.headers['x-api-key'];
15
+ if (apiKey !== process.env.API_KEY) {
16
+ return res.status(401).json({ error: 'Invalid API key' });
17
+ }
18
+ next();
19
+ };
20
+
21
+ // JWT authentication middleware
22
+ const authenticateToken = async (req, res, next) => {
23
+ // Platform mode: use single database user
24
+ if (process.env.VITE_IS_PLATFORM === 'true') {
25
+ try {
26
+ const user = userDb.getFirstUser();
27
+ if (!user) {
28
+ return res.status(500).json({ error: 'Platform mode: No user found in database' });
29
+ }
30
+ req.user = user;
31
+ return next();
32
+ } catch (error) {
33
+ console.error('Platform mode error:', error);
34
+ return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
35
+ }
36
+ }
37
+
38
+ // Normal OSS JWT validation
39
+ const authHeader = req.headers['authorization'];
40
+ const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
41
+
42
+ if (!token) {
43
+ return res.status(401).json({ error: 'Access denied. No token provided.' });
44
+ }
45
+
46
+ try {
47
+ const decoded = jwt.verify(token, JWT_SECRET);
48
+
49
+ // Verify user still exists and is active
50
+ const user = userDb.getUserById(decoded.userId);
51
+ if (!user) {
52
+ return res.status(401).json({ error: 'Invalid token. User not found.' });
53
+ }
54
+
55
+ req.user = user;
56
+ next();
57
+ } catch (error) {
58
+ console.error('Token verification error:', error);
59
+ return res.status(403).json({ error: 'Invalid token' });
60
+ }
61
+ };
62
+
63
+ // Generate JWT token (never expires)
64
+ const generateToken = (user) => {
65
+ return jwt.sign(
66
+ {
67
+ userId: user.id,
68
+ username: user.username
69
+ },
70
+ JWT_SECRET
71
+ // No expiration - token lasts forever
72
+ );
73
+ };
74
+
75
+ // WebSocket authentication function
76
+ const authenticateWebSocket = (token) => {
77
+ // Platform mode: bypass token validation, return first user
78
+ if (process.env.VITE_IS_PLATFORM === 'true') {
79
+ try {
80
+ const user = userDb.getFirstUser();
81
+ if (user) {
82
+ return { userId: user.id, username: user.username };
83
+ }
84
+ return null;
85
+ } catch (error) {
86
+ console.error('Platform mode WebSocket error:', error);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ // Normal OSS JWT validation
92
+ if (!token) {
93
+ return null;
94
+ }
95
+
96
+ try {
97
+ const decoded = jwt.verify(token, JWT_SECRET);
98
+ return decoded;
99
+ } catch (error) {
100
+ console.error('WebSocket token verification error:', error);
101
+ return null;
102
+ }
103
+ };
104
+
105
+ export {
106
+ validateApiKey,
107
+ authenticateToken,
108
+ generateToken,
109
+ authenticateWebSocket,
110
+ JWT_SECRET
111
+ };
@@ -0,0 +1,389 @@
1
+ /**
2
+ * OpenAI Codex SDK Integration
3
+ * =============================
4
+ *
5
+ * This module provides integration with the OpenAI Codex SDK for non-interactive
6
+ * chat sessions. It mirrors the pattern used in claude-sdk.js for consistency.
7
+ *
8
+ * ## Usage
9
+ *
10
+ * - queryCodex(command, options, ws) - Execute a prompt with streaming via WebSocket
11
+ * - abortCodexSession(sessionId) - Cancel an active session
12
+ * - isCodexSessionActive(sessionId) - Check if a session is running
13
+ * - getActiveCodexSessions() - List all active sessions
14
+ */
15
+
16
+ import { Codex } from '@openai/codex-sdk';
17
+
18
+ // Track active sessions
19
+ const activeCodexSessions = new Map();
20
+
21
+ /**
22
+ * Transform Codex SDK event to WebSocket message format
23
+ * @param {object} event - SDK event
24
+ * @returns {object} - Transformed event for WebSocket
25
+ */
26
+ function transformCodexEvent(event) {
27
+ // Map SDK event types to a consistent format
28
+ switch (event.type) {
29
+ case 'item.started':
30
+ case 'item.updated':
31
+ case 'item.completed':
32
+ const item = event.item;
33
+ if (!item) {
34
+ return { type: event.type, item: null };
35
+ }
36
+
37
+ // Transform based on item type
38
+ switch (item.type) {
39
+ case 'agent_message':
40
+ return {
41
+ type: 'item',
42
+ itemType: 'agent_message',
43
+ message: {
44
+ role: 'assistant',
45
+ content: item.text
46
+ }
47
+ };
48
+
49
+ case 'reasoning':
50
+ return {
51
+ type: 'item',
52
+ itemType: 'reasoning',
53
+ message: {
54
+ role: 'assistant',
55
+ content: item.text,
56
+ isReasoning: true
57
+ }
58
+ };
59
+
60
+ case 'command_execution':
61
+ return {
62
+ type: 'item',
63
+ itemType: 'command_execution',
64
+ command: item.command,
65
+ output: item.aggregated_output,
66
+ exitCode: item.exit_code,
67
+ status: item.status
68
+ };
69
+
70
+ case 'file_change':
71
+ return {
72
+ type: 'item',
73
+ itemType: 'file_change',
74
+ changes: item.changes,
75
+ status: item.status
76
+ };
77
+
78
+ case 'mcp_tool_call':
79
+ return {
80
+ type: 'item',
81
+ itemType: 'mcp_tool_call',
82
+ server: item.server,
83
+ tool: item.tool,
84
+ arguments: item.arguments,
85
+ result: item.result,
86
+ error: item.error,
87
+ status: item.status
88
+ };
89
+
90
+ case 'web_search':
91
+ return {
92
+ type: 'item',
93
+ itemType: 'web_search',
94
+ query: item.query
95
+ };
96
+
97
+ case 'todo_list':
98
+ return {
99
+ type: 'item',
100
+ itemType: 'todo_list',
101
+ items: item.items
102
+ };
103
+
104
+ case 'error':
105
+ return {
106
+ type: 'item',
107
+ itemType: 'error',
108
+ message: {
109
+ role: 'error',
110
+ content: item.message
111
+ }
112
+ };
113
+
114
+ default:
115
+ return {
116
+ type: 'item',
117
+ itemType: item.type,
118
+ item: item
119
+ };
120
+ }
121
+
122
+ case 'turn.started':
123
+ return {
124
+ type: 'turn_started'
125
+ };
126
+
127
+ case 'turn.completed':
128
+ return {
129
+ type: 'turn_complete',
130
+ usage: event.usage
131
+ };
132
+
133
+ case 'turn.failed':
134
+ return {
135
+ type: 'turn_failed',
136
+ error: event.error
137
+ };
138
+
139
+ case 'thread.started':
140
+ return {
141
+ type: 'thread_started',
142
+ threadId: event.id
143
+ };
144
+
145
+ case 'error':
146
+ return {
147
+ type: 'error',
148
+ message: event.message
149
+ };
150
+
151
+ default:
152
+ return {
153
+ type: event.type,
154
+ data: event
155
+ };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Map permission mode to Codex SDK options
161
+ * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
162
+ * @returns {object} - { sandboxMode, approvalPolicy }
163
+ */
164
+ function mapPermissionModeToCodexOptions(permissionMode) {
165
+ switch (permissionMode) {
166
+ case 'acceptEdits':
167
+ return {
168
+ sandboxMode: 'workspace-write',
169
+ approvalPolicy: 'never'
170
+ };
171
+ case 'bypassPermissions':
172
+ return {
173
+ sandboxMode: 'danger-full-access',
174
+ approvalPolicy: 'never'
175
+ };
176
+ case 'default':
177
+ default:
178
+ return {
179
+ sandboxMode: 'workspace-write',
180
+ approvalPolicy: 'untrusted'
181
+ };
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Execute a Codex query with streaming
187
+ * @param {string} command - The prompt to send
188
+ * @param {object} options - Options including cwd, sessionId, model, permissionMode
189
+ * @param {WebSocket|object} ws - WebSocket connection or response writer
190
+ */
191
+ export async function queryCodex(command, options = {}, ws) {
192
+ const {
193
+ sessionId,
194
+ cwd,
195
+ projectPath,
196
+ model,
197
+ permissionMode = 'default'
198
+ } = options;
199
+
200
+ const workingDirectory = cwd || projectPath || process.cwd();
201
+ const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
202
+
203
+ let codex;
204
+ let thread;
205
+ let currentSessionId = sessionId;
206
+
207
+ try {
208
+ // Initialize Codex SDK
209
+ codex = new Codex();
210
+
211
+ // Thread options with sandbox and approval settings
212
+ const threadOptions = {
213
+ workingDirectory,
214
+ skipGitRepoCheck: true,
215
+ sandboxMode,
216
+ approvalPolicy,
217
+ model
218
+ };
219
+
220
+ // Start or resume thread
221
+ if (sessionId) {
222
+ thread = codex.resumeThread(sessionId, threadOptions);
223
+ } else {
224
+ thread = codex.startThread(threadOptions);
225
+ }
226
+
227
+ // Get the thread ID
228
+ currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
229
+
230
+ // Track the session
231
+ activeCodexSessions.set(currentSessionId, {
232
+ thread,
233
+ codex,
234
+ status: 'running',
235
+ startedAt: new Date().toISOString()
236
+ });
237
+
238
+ // Send session created event
239
+ sendMessage(ws, {
240
+ type: 'session-created',
241
+ sessionId: currentSessionId,
242
+ provider: 'codex'
243
+ });
244
+
245
+ // Execute with streaming
246
+ const streamedTurn = await thread.runStreamed(command);
247
+
248
+ for await (const event of streamedTurn.events) {
249
+ // Check if session was aborted
250
+ const session = activeCodexSessions.get(currentSessionId);
251
+ if (!session || session.status === 'aborted') {
252
+ break;
253
+ }
254
+
255
+ if (event.type === 'item.started' || event.type === 'item.updated') {
256
+ continue;
257
+ }
258
+
259
+ const transformed = transformCodexEvent(event);
260
+
261
+ sendMessage(ws, {
262
+ type: 'codex-response',
263
+ data: transformed,
264
+ sessionId: currentSessionId
265
+ });
266
+
267
+ // Extract and send token usage if available (normalized to match Claude format)
268
+ if (event.type === 'turn.completed' && event.usage) {
269
+ const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
270
+ sendMessage(ws, {
271
+ type: 'token-budget',
272
+ data: {
273
+ used: totalTokens,
274
+ total: 200000 // Default context window for Codex models
275
+ }
276
+ });
277
+ }
278
+ }
279
+
280
+ // Send completion event
281
+ sendMessage(ws, {
282
+ type: 'codex-complete',
283
+ sessionId: currentSessionId,
284
+ actualSessionId: thread.id
285
+ });
286
+
287
+ } catch (error) {
288
+ console.error('[Codex] Error:', error);
289
+
290
+ sendMessage(ws, {
291
+ type: 'codex-error',
292
+ error: error.message,
293
+ sessionId: currentSessionId
294
+ });
295
+
296
+ } finally {
297
+ // Update session status
298
+ if (currentSessionId) {
299
+ const session = activeCodexSessions.get(currentSessionId);
300
+ if (session) {
301
+ session.status = 'completed';
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Abort an active Codex session
309
+ * @param {string} sessionId - Session ID to abort
310
+ * @returns {boolean} - Whether abort was successful
311
+ */
312
+ export function abortCodexSession(sessionId) {
313
+ const session = activeCodexSessions.get(sessionId);
314
+
315
+ if (!session) {
316
+ return false;
317
+ }
318
+
319
+ session.status = 'aborted';
320
+
321
+ // The SDK doesn't have a direct abort method, but marking status
322
+ // will cause the streaming loop to exit
323
+
324
+ return true;
325
+ }
326
+
327
+ /**
328
+ * Check if a session is active
329
+ * @param {string} sessionId - Session ID to check
330
+ * @returns {boolean} - Whether session is active
331
+ */
332
+ export function isCodexSessionActive(sessionId) {
333
+ const session = activeCodexSessions.get(sessionId);
334
+ return session?.status === 'running';
335
+ }
336
+
337
+ /**
338
+ * Get all active sessions
339
+ * @returns {Array} - Array of active session info
340
+ */
341
+ export function getActiveCodexSessions() {
342
+ const sessions = [];
343
+
344
+ for (const [id, session] of activeCodexSessions.entries()) {
345
+ if (session.status === 'running') {
346
+ sessions.push({
347
+ id,
348
+ status: session.status,
349
+ startedAt: session.startedAt
350
+ });
351
+ }
352
+ }
353
+
354
+ return sessions;
355
+ }
356
+
357
+ /**
358
+ * Helper to send message via WebSocket or writer
359
+ * @param {WebSocket|object} ws - WebSocket or response writer
360
+ * @param {object} data - Data to send
361
+ */
362
+ function sendMessage(ws, data) {
363
+ try {
364
+ if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
365
+ // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
366
+ ws.send(data);
367
+ } else if (typeof ws.send === 'function') {
368
+ // Raw WebSocket - stringify here
369
+ ws.send(JSON.stringify(data));
370
+ }
371
+ } catch (error) {
372
+ console.error('[Codex] Error sending message:', error);
373
+ }
374
+ }
375
+
376
+ // Clean up old completed sessions periodically
377
+ setInterval(() => {
378
+ const now = Date.now();
379
+ const maxAge = 30 * 60 * 1000; // 30 minutes
380
+
381
+ for (const [id, session] of activeCodexSessions.entries()) {
382
+ if (session.status !== 'running') {
383
+ const startedAt = new Date(session.startedAt).getTime();
384
+ if (now - startedAt > maxAge) {
385
+ activeCodexSessions.delete(id);
386
+ }
387
+ }
388
+ }
389
+ }, 5 * 60 * 1000); // Every 5 minutes