@cloudcli-ai/cloudcli 1.28.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 (163) hide show
  1. package/LICENSE +718 -0
  2. package/README.de.md +239 -0
  3. package/README.ja.md +231 -0
  4. package/README.ko.md +230 -0
  5. package/README.md +242 -0
  6. package/README.ru.md +239 -0
  7. package/README.zh-CN.md +230 -0
  8. package/dist/api-docs.html +879 -0
  9. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  10. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  27. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  33. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  39. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  51. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  54. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  57. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  60. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  62. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  65. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  68. package/dist/assets/index-B1XAhG_4.css +32 -0
  69. package/dist/assets/index-BWQPmiP5.js +1275 -0
  70. package/dist/assets/vendor-codemirror-C8f1vU1x.js +41 -0
  71. package/dist/assets/vendor-react-CdSTmIF1.js +59 -0
  72. package/dist/assets/vendor-xterm-CJZjLICi.js +66 -0
  73. package/dist/clear-cache.html +85 -0
  74. package/dist/convert-icons.md +53 -0
  75. package/dist/favicon.png +0 -0
  76. package/dist/favicon.svg +9 -0
  77. package/dist/generate-icons.js +49 -0
  78. package/dist/icons/claude-ai-icon.svg +1 -0
  79. package/dist/icons/codex-white.svg +3 -0
  80. package/dist/icons/codex.svg +3 -0
  81. package/dist/icons/cursor-white.svg +12 -0
  82. package/dist/icons/cursor.svg +1 -0
  83. package/dist/icons/gemini-ai-icon.svg +1 -0
  84. package/dist/icons/icon-128x128.png +0 -0
  85. package/dist/icons/icon-128x128.svg +12 -0
  86. package/dist/icons/icon-144x144.png +0 -0
  87. package/dist/icons/icon-144x144.svg +12 -0
  88. package/dist/icons/icon-152x152.png +0 -0
  89. package/dist/icons/icon-152x152.svg +12 -0
  90. package/dist/icons/icon-192x192.png +0 -0
  91. package/dist/icons/icon-192x192.svg +12 -0
  92. package/dist/icons/icon-384x384.png +0 -0
  93. package/dist/icons/icon-384x384.svg +12 -0
  94. package/dist/icons/icon-512x512.png +0 -0
  95. package/dist/icons/icon-512x512.svg +12 -0
  96. package/dist/icons/icon-72x72.png +0 -0
  97. package/dist/icons/icon-72x72.svg +12 -0
  98. package/dist/icons/icon-96x96.png +0 -0
  99. package/dist/icons/icon-96x96.svg +12 -0
  100. package/dist/icons/icon-template.svg +12 -0
  101. package/dist/index.html +52 -0
  102. package/dist/logo-128.png +0 -0
  103. package/dist/logo-256.png +0 -0
  104. package/dist/logo-32.png +0 -0
  105. package/dist/logo-512.png +0 -0
  106. package/dist/logo-64.png +0 -0
  107. package/dist/logo.svg +17 -0
  108. package/dist/manifest.json +61 -0
  109. package/dist/screenshots/cli-selection.png +0 -0
  110. package/dist/screenshots/desktop-main.png +0 -0
  111. package/dist/screenshots/mobile-chat.png +0 -0
  112. package/dist/screenshots/tools-modal.png +0 -0
  113. package/dist/sw.js +124 -0
  114. package/package.json +153 -0
  115. package/scripts/fix-node-pty.js +67 -0
  116. package/server/claude-sdk.js +817 -0
  117. package/server/cli.js +330 -0
  118. package/server/constants/config.js +5 -0
  119. package/server/cursor-cli.js +335 -0
  120. package/server/database/db.js +630 -0
  121. package/server/database/init.sql +99 -0
  122. package/server/gemini-cli.js +453 -0
  123. package/server/gemini-response-handler.js +79 -0
  124. package/server/index.js +2577 -0
  125. package/server/load-env.js +29 -0
  126. package/server/middleware/auth.js +132 -0
  127. package/server/openai-codex.js +418 -0
  128. package/server/projects.js +2561 -0
  129. package/server/providers/claude/adapter.js +278 -0
  130. package/server/providers/codex/adapter.js +248 -0
  131. package/server/providers/cursor/adapter.js +353 -0
  132. package/server/providers/gemini/adapter.js +186 -0
  133. package/server/providers/registry.js +44 -0
  134. package/server/providers/types.js +119 -0
  135. package/server/providers/utils.js +29 -0
  136. package/server/routes/agent.js +1245 -0
  137. package/server/routes/auth.js +135 -0
  138. package/server/routes/cli-auth.js +434 -0
  139. package/server/routes/codex.js +329 -0
  140. package/server/routes/commands.js +601 -0
  141. package/server/routes/cursor.js +798 -0
  142. package/server/routes/gemini.js +24 -0
  143. package/server/routes/git.js +1488 -0
  144. package/server/routes/mcp-utils.js +48 -0
  145. package/server/routes/mcp.js +552 -0
  146. package/server/routes/messages.js +61 -0
  147. package/server/routes/plugins.js +307 -0
  148. package/server/routes/projects.js +548 -0
  149. package/server/routes/settings.js +276 -0
  150. package/server/routes/taskmaster.js +1963 -0
  151. package/server/routes/user.js +123 -0
  152. package/server/services/notification-orchestrator.js +227 -0
  153. package/server/services/vapid-keys.js +35 -0
  154. package/server/sessionManager.js +226 -0
  155. package/server/utils/commandParser.js +303 -0
  156. package/server/utils/frontmatter.js +18 -0
  157. package/server/utils/gitConfig.js +34 -0
  158. package/server/utils/mcp-detector.js +198 -0
  159. package/server/utils/plugin-loader.js +457 -0
  160. package/server/utils/plugin-process-manager.js +184 -0
  161. package/server/utils/taskmaster-websocket.js +129 -0
  162. package/shared/modelConstants.js +92 -0
  163. package/shared/networkHosts.js +22 -0
@@ -0,0 +1,29 @@
1
+ // Load environment variables from .env before other imports execute.
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ try {
12
+ const envPath = path.join(__dirname, '../.env');
13
+ const envFile = fs.readFileSync(envPath, 'utf8');
14
+ envFile.split('\n').forEach(line => {
15
+ const trimmedLine = line.trim();
16
+ if (trimmedLine && !trimmedLine.startsWith('#')) {
17
+ const [key, ...valueParts] = trimmedLine.split('=');
18
+ if (key && valueParts.length > 0 && !process.env[key]) {
19
+ process.env[key] = valueParts.join('=').trim();
20
+ }
21
+ }
22
+ });
23
+ } catch (e) {
24
+ console.log('No .env file found or error reading it:', e.message);
25
+ }
26
+
27
+ if (!process.env.DATABASE_PATH) {
28
+ process.env.DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db');
29
+ }
@@ -0,0 +1,132 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { userDb, appConfigDb } from '../database/db.js';
3
+ import { IS_PLATFORM } from '../constants/config.js';
4
+
5
+ // Use env var if set, otherwise auto-generate a unique secret per installation
6
+ const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
7
+
8
+ // Optional API key middleware
9
+ const validateApiKey = (req, res, next) => {
10
+ // Skip API key validation if not configured
11
+ if (!process.env.API_KEY) {
12
+ return next();
13
+ }
14
+
15
+ const apiKey = req.headers['x-api-key'];
16
+ if (apiKey !== process.env.API_KEY) {
17
+ return res.status(401).json({ error: 'Invalid API key' });
18
+ }
19
+ next();
20
+ };
21
+
22
+ // JWT authentication middleware
23
+ const authenticateToken = async (req, res, next) => {
24
+ // Platform mode: use single database user
25
+ if (IS_PLATFORM) {
26
+ try {
27
+ const user = userDb.getFirstUser();
28
+ if (!user) {
29
+ return res.status(500).json({ error: 'Platform mode: No user found in database' });
30
+ }
31
+ req.user = user;
32
+ return next();
33
+ } catch (error) {
34
+ console.error('Platform mode error:', error);
35
+ return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
36
+ }
37
+ }
38
+
39
+ // Normal OSS JWT validation
40
+ const authHeader = req.headers['authorization'];
41
+ let token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
42
+
43
+ // Also check query param for SSE endpoints (EventSource can't set headers)
44
+ if (!token && req.query.token) {
45
+ token = req.query.token;
46
+ }
47
+
48
+ if (!token) {
49
+ return res.status(401).json({ error: 'Access denied. No token provided.' });
50
+ }
51
+
52
+ try {
53
+ const decoded = jwt.verify(token, JWT_SECRET);
54
+
55
+ // Verify user still exists and is active
56
+ const user = userDb.getUserById(decoded.userId);
57
+ if (!user) {
58
+ return res.status(401).json({ error: 'Invalid token. User not found.' });
59
+ }
60
+
61
+ // Auto-refresh: if token is past halfway through its lifetime, issue a new one
62
+ if (decoded.exp && decoded.iat) {
63
+ const now = Math.floor(Date.now() / 1000);
64
+ const halfLife = (decoded.exp - decoded.iat) / 2;
65
+ if (now > decoded.iat + halfLife) {
66
+ const newToken = generateToken(user);
67
+ res.setHeader('X-Refreshed-Token', newToken);
68
+ }
69
+ }
70
+
71
+ req.user = user;
72
+ next();
73
+ } catch (error) {
74
+ console.error('Token verification error:', error);
75
+ return res.status(403).json({ error: 'Invalid token' });
76
+ }
77
+ };
78
+
79
+ // Generate JWT token
80
+ const generateToken = (user) => {
81
+ return jwt.sign(
82
+ {
83
+ userId: user.id,
84
+ username: user.username
85
+ },
86
+ JWT_SECRET,
87
+ { expiresIn: '7d' }
88
+ );
89
+ };
90
+
91
+ // WebSocket authentication function
92
+ const authenticateWebSocket = (token) => {
93
+ // Platform mode: bypass token validation, return first user
94
+ if (IS_PLATFORM) {
95
+ try {
96
+ const user = userDb.getFirstUser();
97
+ if (user) {
98
+ return { id: user.id, userId: user.id, username: user.username };
99
+ }
100
+ return null;
101
+ } catch (error) {
102
+ console.error('Platform mode WebSocket error:', error);
103
+ return null;
104
+ }
105
+ }
106
+
107
+ // Normal OSS JWT validation
108
+ if (!token) {
109
+ return null;
110
+ }
111
+
112
+ try {
113
+ const decoded = jwt.verify(token, JWT_SECRET);
114
+ // Verify user actually exists in database (matches REST authenticateToken behavior)
115
+ const user = userDb.getUserById(decoded.userId);
116
+ if (!user) {
117
+ return null;
118
+ }
119
+ return { userId: user.id, username: user.username };
120
+ } catch (error) {
121
+ console.error('WebSocket token verification error:', error);
122
+ return null;
123
+ }
124
+ };
125
+
126
+ export {
127
+ validateApiKey,
128
+ authenticateToken,
129
+ generateToken,
130
+ authenticateWebSocket,
131
+ JWT_SECRET
132
+ };
@@ -0,0 +1,418 @@
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
+ import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
18
+ import { codexAdapter } from './providers/codex/adapter.js';
19
+ import { createNormalizedMessage } from './providers/types.js';
20
+
21
+ // Track active sessions
22
+ const activeCodexSessions = new Map();
23
+
24
+ /**
25
+ * Transform Codex SDK event to WebSocket message format
26
+ * @param {object} event - SDK event
27
+ * @returns {object} - Transformed event for WebSocket
28
+ */
29
+ function transformCodexEvent(event) {
30
+ // Map SDK event types to a consistent format
31
+ switch (event.type) {
32
+ case 'item.started':
33
+ case 'item.updated':
34
+ case 'item.completed':
35
+ const item = event.item;
36
+ if (!item) {
37
+ return { type: event.type, item: null };
38
+ }
39
+
40
+ // Transform based on item type
41
+ switch (item.type) {
42
+ case 'agent_message':
43
+ return {
44
+ type: 'item',
45
+ itemType: 'agent_message',
46
+ message: {
47
+ role: 'assistant',
48
+ content: item.text
49
+ }
50
+ };
51
+
52
+ case 'reasoning':
53
+ return {
54
+ type: 'item',
55
+ itemType: 'reasoning',
56
+ message: {
57
+ role: 'assistant',
58
+ content: item.text,
59
+ isReasoning: true
60
+ }
61
+ };
62
+
63
+ case 'command_execution':
64
+ return {
65
+ type: 'item',
66
+ itemType: 'command_execution',
67
+ command: item.command,
68
+ output: item.aggregated_output,
69
+ exitCode: item.exit_code,
70
+ status: item.status
71
+ };
72
+
73
+ case 'file_change':
74
+ return {
75
+ type: 'item',
76
+ itemType: 'file_change',
77
+ changes: item.changes,
78
+ status: item.status
79
+ };
80
+
81
+ case 'mcp_tool_call':
82
+ return {
83
+ type: 'item',
84
+ itemType: 'mcp_tool_call',
85
+ server: item.server,
86
+ tool: item.tool,
87
+ arguments: item.arguments,
88
+ result: item.result,
89
+ error: item.error,
90
+ status: item.status
91
+ };
92
+
93
+ case 'web_search':
94
+ return {
95
+ type: 'item',
96
+ itemType: 'web_search',
97
+ query: item.query
98
+ };
99
+
100
+ case 'todo_list':
101
+ return {
102
+ type: 'item',
103
+ itemType: 'todo_list',
104
+ items: item.items
105
+ };
106
+
107
+ case 'error':
108
+ return {
109
+ type: 'item',
110
+ itemType: 'error',
111
+ message: {
112
+ role: 'error',
113
+ content: item.message
114
+ }
115
+ };
116
+
117
+ default:
118
+ return {
119
+ type: 'item',
120
+ itemType: item.type,
121
+ item: item
122
+ };
123
+ }
124
+
125
+ case 'turn.started':
126
+ return {
127
+ type: 'turn_started'
128
+ };
129
+
130
+ case 'turn.completed':
131
+ return {
132
+ type: 'turn_complete',
133
+ usage: event.usage
134
+ };
135
+
136
+ case 'turn.failed':
137
+ return {
138
+ type: 'turn_failed',
139
+ error: event.error
140
+ };
141
+
142
+ case 'thread.started':
143
+ return {
144
+ type: 'thread_started',
145
+ threadId: event.id
146
+ };
147
+
148
+ case 'error':
149
+ return {
150
+ type: 'error',
151
+ message: event.message
152
+ };
153
+
154
+ default:
155
+ return {
156
+ type: event.type,
157
+ data: event
158
+ };
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Map permission mode to Codex SDK options
164
+ * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
165
+ * @returns {object} - { sandboxMode, approvalPolicy }
166
+ */
167
+ function mapPermissionModeToCodexOptions(permissionMode) {
168
+ switch (permissionMode) {
169
+ case 'acceptEdits':
170
+ return {
171
+ sandboxMode: 'workspace-write',
172
+ approvalPolicy: 'never'
173
+ };
174
+ case 'bypassPermissions':
175
+ return {
176
+ sandboxMode: 'danger-full-access',
177
+ approvalPolicy: 'never'
178
+ };
179
+ case 'default':
180
+ default:
181
+ return {
182
+ sandboxMode: 'workspace-write',
183
+ approvalPolicy: 'untrusted'
184
+ };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Execute a Codex query with streaming
190
+ * @param {string} command - The prompt to send
191
+ * @param {object} options - Options including cwd, sessionId, model, permissionMode
192
+ * @param {WebSocket|object} ws - WebSocket connection or response writer
193
+ */
194
+ export async function queryCodex(command, options = {}, ws) {
195
+ const {
196
+ sessionId,
197
+ sessionSummary,
198
+ cwd,
199
+ projectPath,
200
+ model,
201
+ permissionMode = 'default'
202
+ } = options;
203
+
204
+ const workingDirectory = cwd || projectPath || process.cwd();
205
+ const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
206
+
207
+ let codex;
208
+ let thread;
209
+ let currentSessionId = sessionId;
210
+ let terminalFailure = null;
211
+ const abortController = new AbortController();
212
+
213
+ try {
214
+ // Initialize Codex SDK
215
+ codex = new Codex();
216
+
217
+ // Thread options with sandbox and approval settings
218
+ const threadOptions = {
219
+ workingDirectory,
220
+ skipGitRepoCheck: true,
221
+ sandboxMode,
222
+ approvalPolicy,
223
+ model
224
+ };
225
+
226
+ // Start or resume thread
227
+ if (sessionId) {
228
+ thread = codex.resumeThread(sessionId, threadOptions);
229
+ } else {
230
+ thread = codex.startThread(threadOptions);
231
+ }
232
+
233
+ // Get the thread ID
234
+ currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
235
+
236
+ // Track the session
237
+ activeCodexSessions.set(currentSessionId, {
238
+ thread,
239
+ codex,
240
+ status: 'running',
241
+ abortController,
242
+ startedAt: new Date().toISOString()
243
+ });
244
+
245
+ // Send session created event
246
+ sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
247
+
248
+ // Execute with streaming
249
+ const streamedTurn = await thread.runStreamed(command, {
250
+ signal: abortController.signal
251
+ });
252
+
253
+ for await (const event of streamedTurn.events) {
254
+ // Check if session was aborted
255
+ const session = activeCodexSessions.get(currentSessionId);
256
+ if (!session || session.status === 'aborted') {
257
+ break;
258
+ }
259
+
260
+ if (event.type === 'item.started' || event.type === 'item.updated') {
261
+ continue;
262
+ }
263
+
264
+ const transformed = transformCodexEvent(event);
265
+
266
+ // Normalize the transformed event into NormalizedMessage(s) via adapter
267
+ const normalizedMsgs = codexAdapter.normalizeMessage(transformed, currentSessionId);
268
+ for (const msg of normalizedMsgs) {
269
+ sendMessage(ws, msg);
270
+ }
271
+
272
+ if (event.type === 'turn.failed' && !terminalFailure) {
273
+ terminalFailure = event.error || new Error('Turn failed');
274
+ notifyRunFailed({
275
+ userId: ws?.userId || null,
276
+ provider: 'codex',
277
+ sessionId: currentSessionId,
278
+ sessionName: sessionSummary,
279
+ error: terminalFailure
280
+ });
281
+ }
282
+
283
+ // Extract and send token usage if available (normalized to match Claude format)
284
+ if (event.type === 'turn.completed' && event.usage) {
285
+ const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
286
+ sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
287
+ }
288
+ }
289
+
290
+ // Send completion event
291
+ if (!terminalFailure) {
292
+ sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
293
+ notifyRunStopped({
294
+ userId: ws?.userId || null,
295
+ provider: 'codex',
296
+ sessionId: currentSessionId,
297
+ sessionName: sessionSummary,
298
+ stopReason: 'completed'
299
+ });
300
+ }
301
+
302
+ } catch (error) {
303
+ const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
304
+ const wasAborted =
305
+ session?.status === 'aborted' ||
306
+ error?.name === 'AbortError' ||
307
+ String(error?.message || '').toLowerCase().includes('aborted');
308
+
309
+ if (!wasAborted) {
310
+ console.error('[Codex] Error:', error);
311
+ sendMessage(ws, createNormalizedMessage({ kind: 'error', content: error.message, sessionId: currentSessionId, provider: 'codex' }));
312
+ if (!terminalFailure) {
313
+ notifyRunFailed({
314
+ userId: ws?.userId || null,
315
+ provider: 'codex',
316
+ sessionId: currentSessionId,
317
+ sessionName: sessionSummary,
318
+ error
319
+ });
320
+ }
321
+ }
322
+
323
+ } finally {
324
+ // Update session status
325
+ if (currentSessionId) {
326
+ const session = activeCodexSessions.get(currentSessionId);
327
+ if (session) {
328
+ session.status = session.status === 'aborted' ? 'aborted' : 'completed';
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Abort an active Codex session
336
+ * @param {string} sessionId - Session ID to abort
337
+ * @returns {boolean} - Whether abort was successful
338
+ */
339
+ export function abortCodexSession(sessionId) {
340
+ const session = activeCodexSessions.get(sessionId);
341
+
342
+ if (!session) {
343
+ return false;
344
+ }
345
+
346
+ session.status = 'aborted';
347
+ try {
348
+ session.abortController?.abort();
349
+ } catch (error) {
350
+ console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
351
+ }
352
+
353
+ return true;
354
+ }
355
+
356
+ /**
357
+ * Check if a session is active
358
+ * @param {string} sessionId - Session ID to check
359
+ * @returns {boolean} - Whether session is active
360
+ */
361
+ export function isCodexSessionActive(sessionId) {
362
+ const session = activeCodexSessions.get(sessionId);
363
+ return session?.status === 'running';
364
+ }
365
+
366
+ /**
367
+ * Get all active sessions
368
+ * @returns {Array} - Array of active session info
369
+ */
370
+ export function getActiveCodexSessions() {
371
+ const sessions = [];
372
+
373
+ for (const [id, session] of activeCodexSessions.entries()) {
374
+ if (session.status === 'running') {
375
+ sessions.push({
376
+ id,
377
+ status: session.status,
378
+ startedAt: session.startedAt
379
+ });
380
+ }
381
+ }
382
+
383
+ return sessions;
384
+ }
385
+
386
+ /**
387
+ * Helper to send message via WebSocket or writer
388
+ * @param {WebSocket|object} ws - WebSocket or response writer
389
+ * @param {object} data - Data to send
390
+ */
391
+ function sendMessage(ws, data) {
392
+ try {
393
+ if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
394
+ // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
395
+ ws.send(data);
396
+ } else if (typeof ws.send === 'function') {
397
+ // Raw WebSocket - stringify here
398
+ ws.send(JSON.stringify(data));
399
+ }
400
+ } catch (error) {
401
+ console.error('[Codex] Error sending message:', error);
402
+ }
403
+ }
404
+
405
+ // Clean up old completed sessions periodically
406
+ setInterval(() => {
407
+ const now = Date.now();
408
+ const maxAge = 30 * 60 * 1000; // 30 minutes
409
+
410
+ for (const [id, session] of activeCodexSessions.entries()) {
411
+ if (session.status !== 'running') {
412
+ const startedAt = new Date(session.startedAt).getTime();
413
+ if (now - startedAt > maxAge) {
414
+ activeCodexSessions.delete(id);
415
+ }
416
+ }
417
+ }
418
+ }, 5 * 60 * 1000); // Every 5 minutes