@pixelbyte-software/pixcode 1.30.2 → 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 (202) hide show
  1. package/LICENSE +718 -718
  2. package/README.de.md +248 -248
  3. package/README.ja.md +240 -240
  4. package/README.ko.md +240 -240
  5. package/README.md +295 -285
  6. package/README.ru.md +248 -248
  7. package/README.tr.md +250 -250
  8. package/README.zh-CN.md +240 -240
  9. package/dist/api-docs.html +879 -879
  10. package/dist/assets/index-BRRJ47XQ.css +32 -0
  11. package/dist/assets/index-EQohwyiC.js +837 -0
  12. package/dist/clear-cache.html +85 -85
  13. package/dist/convert-icons.md +52 -52
  14. package/dist/favicon.png +0 -0
  15. package/dist/favicon.svg +7 -8
  16. package/dist/generate-icons.js +48 -48
  17. package/dist/icons/codex-white.svg +3 -3
  18. package/dist/icons/codex.svg +3 -3
  19. package/dist/icons/cursor-white.svg +11 -11
  20. package/dist/icons/icon-128x128.png +0 -0
  21. package/dist/icons/icon-128x128.svg +9 -12
  22. package/dist/icons/icon-144x144.png +0 -0
  23. package/dist/icons/icon-144x144.svg +9 -12
  24. package/dist/icons/icon-152x152.png +0 -0
  25. package/dist/icons/icon-152x152.svg +9 -12
  26. package/dist/icons/icon-192x192.png +0 -0
  27. package/dist/icons/icon-192x192.svg +9 -12
  28. package/dist/icons/icon-384x384.png +0 -0
  29. package/dist/icons/icon-384x384.svg +9 -12
  30. package/dist/icons/icon-512x512.png +0 -0
  31. package/dist/icons/icon-512x512.svg +9 -12
  32. package/dist/icons/icon-72x72.png +0 -0
  33. package/dist/icons/icon-72x72.svg +9 -12
  34. package/dist/icons/icon-96x96.png +0 -0
  35. package/dist/icons/icon-96x96.svg +9 -12
  36. package/dist/icons/icon-template.svg +9 -12
  37. package/dist/icons/qwen-ai-icon.png +0 -0
  38. package/dist/index.html +59 -49
  39. package/dist/logo.png +0 -0
  40. package/dist/logo.svg +11 -16
  41. package/dist/manifest.json +60 -60
  42. package/dist/sw.js +124 -124
  43. package/dist-server/server/cli.js +100 -97
  44. package/dist-server/server/cli.js.map +1 -1
  45. package/dist-server/server/daemon/manager.js +33 -33
  46. package/dist-server/server/daemon-manager.js +62 -62
  47. package/dist-server/server/database/db.js +114 -22
  48. package/dist-server/server/database/db.js.map +1 -1
  49. package/dist-server/server/database/schema.js +122 -89
  50. package/dist-server/server/database/schema.js.map +1 -1
  51. package/dist-server/server/gemini-cli.js +6 -1
  52. package/dist-server/server/gemini-cli.js.map +1 -1
  53. package/dist-server/server/index.js +234 -64
  54. package/dist-server/server/index.js.map +1 -1
  55. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +29 -2
  56. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  57. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +22 -2
  58. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -1
  59. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +2 -2
  60. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  61. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +14 -2
  62. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  63. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js +132 -0
  64. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -0
  65. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js +87 -0
  66. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js.map +1 -0
  67. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js +201 -0
  68. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js.map +1 -0
  69. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js +19 -0
  70. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js.map +1 -0
  71. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  72. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  73. package/dist-server/server/modules/providers/provider.routes.js +310 -1
  74. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  75. package/dist-server/server/projects.js +197 -6
  76. package/dist-server/server/projects.js.map +1 -1
  77. package/dist-server/server/qwen-code-cli.js +350 -0
  78. package/dist-server/server/qwen-code-cli.js.map +1 -0
  79. package/dist-server/server/qwen-response-handler.js +70 -0
  80. package/dist-server/server/qwen-response-handler.js.map +1 -0
  81. package/dist-server/server/routes/commands.js +25 -25
  82. package/dist-server/server/routes/git.js +17 -17
  83. package/dist-server/server/routes/network.js +116 -0
  84. package/dist-server/server/routes/network.js.map +1 -0
  85. package/dist-server/server/routes/projects.js +43 -0
  86. package/dist-server/server/routes/projects.js.map +1 -1
  87. package/dist-server/server/routes/qwen.js +23 -0
  88. package/dist-server/server/routes/qwen.js.map +1 -0
  89. package/dist-server/server/routes/taskmaster.js +419 -419
  90. package/dist-server/server/routes/telegram.js +119 -0
  91. package/dist-server/server/routes/telegram.js.map +1 -0
  92. package/dist-server/server/services/external-access.js +228 -0
  93. package/dist-server/server/services/external-access.js.map +1 -0
  94. package/dist-server/server/services/install-jobs.js +394 -0
  95. package/dist-server/server/services/install-jobs.js.map +1 -0
  96. package/dist-server/server/services/notification-orchestrator.js +19 -5
  97. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  98. package/dist-server/server/services/provider-credentials.js +154 -0
  99. package/dist-server/server/services/provider-credentials.js.map +1 -0
  100. package/dist-server/server/services/provider-models.js +218 -0
  101. package/dist-server/server/services/provider-models.js.map +1 -0
  102. package/dist-server/server/services/telegram/bot.js +259 -0
  103. package/dist-server/server/services/telegram/bot.js.map +1 -0
  104. package/dist-server/server/services/telegram/translations.js +160 -0
  105. package/dist-server/server/services/telegram/translations.js.map +1 -0
  106. package/dist-server/server/utils/port-access.js +196 -0
  107. package/dist-server/server/utils/port-access.js.map +1 -0
  108. package/dist-server/shared/modelConstants.js +18 -0
  109. package/dist-server/shared/modelConstants.js.map +1 -1
  110. package/package.json +177 -168
  111. package/scripts/fix-node-pty.js +67 -67
  112. package/server/claude-sdk.js +834 -834
  113. package/server/cli.js +940 -937
  114. package/server/constants/config.js +4 -4
  115. package/server/cursor-cli.js +342 -342
  116. package/server/daemon/manager.js +564 -564
  117. package/server/daemon-manager.js +920 -920
  118. package/server/database/db.js +696 -593
  119. package/server/database/schema.js +138 -102
  120. package/server/gemini-cli.js +475 -469
  121. package/server/gemini-response-handler.js +79 -79
  122. package/server/index.js +2730 -2556
  123. package/server/load-env.js +34 -34
  124. package/server/middleware/auth.js +132 -132
  125. package/server/modules/providers/list/claude/claude-auth.provider.ts +145 -123
  126. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  127. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  128. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  129. package/server/modules/providers/list/codex/codex-auth.provider.ts +115 -100
  130. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  131. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  132. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  133. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  134. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  135. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  136. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  137. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +163 -151
  138. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  139. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  140. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  141. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -0
  142. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -0
  143. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +218 -0
  144. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -0
  145. package/server/modules/providers/provider.registry.ts +38 -36
  146. package/server/modules/providers/provider.routes.ts +583 -217
  147. package/server/modules/providers/services/mcp.service.ts +94 -94
  148. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  149. package/server/modules/providers/services/sessions.service.ts +45 -45
  150. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  151. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  152. package/server/modules/providers/tests/mcp.test.ts +293 -293
  153. package/server/openai-codex.js +426 -426
  154. package/server/projects.js +2993 -2792
  155. package/server/qwen-code-cli.js +392 -0
  156. package/server/qwen-response-handler.js +73 -0
  157. package/server/routes/agent.js +1245 -1245
  158. package/server/routes/auth.js +134 -134
  159. package/server/routes/codex.js +19 -19
  160. package/server/routes/commands.js +554 -554
  161. package/server/routes/cursor.js +52 -52
  162. package/server/routes/gemini.js +24 -24
  163. package/server/routes/git.js +1488 -1488
  164. package/server/routes/mcp-utils.js +31 -31
  165. package/server/routes/messages.js +61 -61
  166. package/server/routes/network.js +128 -0
  167. package/server/routes/plugins.js +307 -307
  168. package/server/routes/projects.js +675 -627
  169. package/server/routes/qwen.js +27 -0
  170. package/server/routes/settings.js +286 -286
  171. package/server/routes/taskmaster.js +1471 -1471
  172. package/server/routes/telegram.js +125 -0
  173. package/server/routes/user.js +123 -123
  174. package/server/services/external-access.js +240 -0
  175. package/server/services/install-jobs.js +410 -0
  176. package/server/services/notification-orchestrator.js +242 -227
  177. package/server/services/provider-credentials.js +151 -0
  178. package/server/services/provider-models.js +225 -0
  179. package/server/services/telegram/bot.js +280 -0
  180. package/server/services/telegram/translations.js +170 -0
  181. package/server/services/vapid-keys.js +35 -35
  182. package/server/sessionManager.js +225 -225
  183. package/server/shared/interfaces.ts +54 -54
  184. package/server/shared/types.ts +172 -172
  185. package/server/shared/utils.ts +193 -193
  186. package/server/tsconfig.json +36 -36
  187. package/server/utils/colors.js +21 -21
  188. package/server/utils/commandParser.js +303 -303
  189. package/server/utils/frontmatter.js +18 -18
  190. package/server/utils/gitConfig.js +34 -34
  191. package/server/utils/mcp-detector.js +147 -147
  192. package/server/utils/plugin-loader.js +457 -457
  193. package/server/utils/plugin-process-manager.js +184 -184
  194. package/server/utils/port-access.js +209 -0
  195. package/server/utils/runtime-paths.js +37 -37
  196. package/server/utils/taskmaster-websocket.js +128 -128
  197. package/server/utils/url-detection.js +71 -71
  198. package/server/vite-daemon.js +78 -78
  199. package/shared/modelConstants.js +117 -97
  200. package/shared/networkHosts.js +22 -22
  201. package/dist/assets/index-C2c9QNwK.css +0 -32
  202. package/dist/assets/index-DyXDZED-.js +0 -1277
@@ -1,426 +1,426 @@
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 { sessionsService } from './modules/providers/services/sessions.service.js';
19
- import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
20
- import { createNormalizedMessage } from './shared/utils.js';
21
-
22
- // Track active sessions
23
- const activeCodexSessions = new Map();
24
-
25
- /**
26
- * Transform Codex SDK event to WebSocket message format
27
- * @param {object} event - SDK event
28
- * @returns {object} - Transformed event for WebSocket
29
- */
30
- function transformCodexEvent(event) {
31
- // Map SDK event types to a consistent format
32
- switch (event.type) {
33
- case 'item.started':
34
- case 'item.updated':
35
- case 'item.completed':
36
- const item = event.item;
37
- if (!item) {
38
- return { type: event.type, item: null };
39
- }
40
-
41
- // Transform based on item type
42
- switch (item.type) {
43
- case 'agent_message':
44
- return {
45
- type: 'item',
46
- itemType: 'agent_message',
47
- message: {
48
- role: 'assistant',
49
- content: item.text
50
- }
51
- };
52
-
53
- case 'reasoning':
54
- return {
55
- type: 'item',
56
- itemType: 'reasoning',
57
- message: {
58
- role: 'assistant',
59
- content: item.text,
60
- isReasoning: true
61
- }
62
- };
63
-
64
- case 'command_execution':
65
- return {
66
- type: 'item',
67
- itemType: 'command_execution',
68
- command: item.command,
69
- output: item.aggregated_output,
70
- exitCode: item.exit_code,
71
- status: item.status
72
- };
73
-
74
- case 'file_change':
75
- return {
76
- type: 'item',
77
- itemType: 'file_change',
78
- changes: item.changes,
79
- status: item.status
80
- };
81
-
82
- case 'mcp_tool_call':
83
- return {
84
- type: 'item',
85
- itemType: 'mcp_tool_call',
86
- server: item.server,
87
- tool: item.tool,
88
- arguments: item.arguments,
89
- result: item.result,
90
- error: item.error,
91
- status: item.status
92
- };
93
-
94
- case 'web_search':
95
- return {
96
- type: 'item',
97
- itemType: 'web_search',
98
- query: item.query
99
- };
100
-
101
- case 'todo_list':
102
- return {
103
- type: 'item',
104
- itemType: 'todo_list',
105
- items: item.items
106
- };
107
-
108
- case 'error':
109
- return {
110
- type: 'item',
111
- itemType: 'error',
112
- message: {
113
- role: 'error',
114
- content: item.message
115
- }
116
- };
117
-
118
- default:
119
- return {
120
- type: 'item',
121
- itemType: item.type,
122
- item: item
123
- };
124
- }
125
-
126
- case 'turn.started':
127
- return {
128
- type: 'turn_started'
129
- };
130
-
131
- case 'turn.completed':
132
- return {
133
- type: 'turn_complete',
134
- usage: event.usage
135
- };
136
-
137
- case 'turn.failed':
138
- return {
139
- type: 'turn_failed',
140
- error: event.error
141
- };
142
-
143
- case 'thread.started':
144
- return {
145
- type: 'thread_started',
146
- threadId: event.id
147
- };
148
-
149
- case 'error':
150
- return {
151
- type: 'error',
152
- message: event.message
153
- };
154
-
155
- default:
156
- return {
157
- type: event.type,
158
- data: event
159
- };
160
- }
161
- }
162
-
163
- /**
164
- * Map permission mode to Codex SDK options
165
- * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
166
- * @returns {object} - { sandboxMode, approvalPolicy }
167
- */
168
- function mapPermissionModeToCodexOptions(permissionMode) {
169
- switch (permissionMode) {
170
- case 'acceptEdits':
171
- return {
172
- sandboxMode: 'workspace-write',
173
- approvalPolicy: 'never'
174
- };
175
- case 'bypassPermissions':
176
- return {
177
- sandboxMode: 'danger-full-access',
178
- approvalPolicy: 'never'
179
- };
180
- case 'default':
181
- default:
182
- return {
183
- sandboxMode: 'workspace-write',
184
- approvalPolicy: 'untrusted'
185
- };
186
- }
187
- }
188
-
189
- /**
190
- * Execute a Codex query with streaming
191
- * @param {string} command - The prompt to send
192
- * @param {object} options - Options including cwd, sessionId, model, permissionMode
193
- * @param {WebSocket|object} ws - WebSocket connection or response writer
194
- */
195
- export async function queryCodex(command, options = {}, ws) {
196
- const {
197
- sessionId,
198
- sessionSummary,
199
- cwd,
200
- projectPath,
201
- model,
202
- permissionMode = 'default'
203
- } = options;
204
-
205
- const workingDirectory = cwd || projectPath || process.cwd();
206
- const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
207
-
208
- let codex;
209
- let thread;
210
- let currentSessionId = sessionId;
211
- let terminalFailure = null;
212
- const abortController = new AbortController();
213
-
214
- try {
215
- // Initialize Codex SDK
216
- codex = new Codex();
217
-
218
- // Thread options with sandbox and approval settings
219
- const threadOptions = {
220
- workingDirectory,
221
- skipGitRepoCheck: true,
222
- sandboxMode,
223
- approvalPolicy,
224
- model
225
- };
226
-
227
- // Start or resume thread
228
- if (sessionId) {
229
- thread = codex.resumeThread(sessionId, threadOptions);
230
- } else {
231
- thread = codex.startThread(threadOptions);
232
- }
233
-
234
- // Get the thread ID
235
- currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
236
-
237
- // Track the session
238
- activeCodexSessions.set(currentSessionId, {
239
- thread,
240
- codex,
241
- status: 'running',
242
- abortController,
243
- startedAt: new Date().toISOString()
244
- });
245
-
246
- // Send session created event
247
- sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
248
-
249
- // Execute with streaming
250
- const streamedTurn = await thread.runStreamed(command, {
251
- signal: abortController.signal
252
- });
253
-
254
- for await (const event of streamedTurn.events) {
255
- // Check if session was aborted
256
- const session = activeCodexSessions.get(currentSessionId);
257
- if (!session || session.status === 'aborted') {
258
- break;
259
- }
260
-
261
- if (event.type === 'item.started' || event.type === 'item.updated') {
262
- continue;
263
- }
264
-
265
- const transformed = transformCodexEvent(event);
266
-
267
- // Normalize the transformed event into NormalizedMessage(s) via adapter
268
- const normalizedMsgs = sessionsService.normalizeMessage('codex', transformed, currentSessionId);
269
- for (const msg of normalizedMsgs) {
270
- sendMessage(ws, msg);
271
- }
272
-
273
- if (event.type === 'turn.failed' && !terminalFailure) {
274
- terminalFailure = event.error || new Error('Turn failed');
275
- notifyRunFailed({
276
- userId: ws?.userId || null,
277
- provider: 'codex',
278
- sessionId: currentSessionId,
279
- sessionName: sessionSummary,
280
- error: terminalFailure
281
- });
282
- }
283
-
284
- // Extract and send token usage if available (normalized to match Claude format)
285
- if (event.type === 'turn.completed' && event.usage) {
286
- const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
287
- sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
288
- }
289
- }
290
-
291
- // Send completion event
292
- if (!terminalFailure) {
293
- sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
294
- notifyRunStopped({
295
- userId: ws?.userId || null,
296
- provider: 'codex',
297
- sessionId: currentSessionId,
298
- sessionName: sessionSummary,
299
- stopReason: 'completed'
300
- });
301
- }
302
-
303
- } catch (error) {
304
- const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
305
- const wasAborted =
306
- session?.status === 'aborted' ||
307
- error?.name === 'AbortError' ||
308
- String(error?.message || '').toLowerCase().includes('aborted');
309
-
310
- if (!wasAborted) {
311
- console.error('[Codex] Error:', error);
312
-
313
- // Check if Codex SDK is available for a clearer error message
314
- const installed = await providerAuthService.isProviderInstalled('codex');
315
- const errorContent = !installed
316
- ? 'Codex CLI is not configured. Please set up authentication first.'
317
- : error.message;
318
-
319
- sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: currentSessionId, provider: 'codex' }));
320
- if (!terminalFailure) {
321
- notifyRunFailed({
322
- userId: ws?.userId || null,
323
- provider: 'codex',
324
- sessionId: currentSessionId,
325
- sessionName: sessionSummary,
326
- error
327
- });
328
- }
329
- }
330
-
331
- } finally {
332
- // Update session status
333
- if (currentSessionId) {
334
- const session = activeCodexSessions.get(currentSessionId);
335
- if (session) {
336
- session.status = session.status === 'aborted' ? 'aborted' : 'completed';
337
- }
338
- }
339
- }
340
- }
341
-
342
- /**
343
- * Abort an active Codex session
344
- * @param {string} sessionId - Session ID to abort
345
- * @returns {boolean} - Whether abort was successful
346
- */
347
- export function abortCodexSession(sessionId) {
348
- const session = activeCodexSessions.get(sessionId);
349
-
350
- if (!session) {
351
- return false;
352
- }
353
-
354
- session.status = 'aborted';
355
- try {
356
- session.abortController?.abort();
357
- } catch (error) {
358
- console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
359
- }
360
-
361
- return true;
362
- }
363
-
364
- /**
365
- * Check if a session is active
366
- * @param {string} sessionId - Session ID to check
367
- * @returns {boolean} - Whether session is active
368
- */
369
- export function isCodexSessionActive(sessionId) {
370
- const session = activeCodexSessions.get(sessionId);
371
- return session?.status === 'running';
372
- }
373
-
374
- /**
375
- * Get all active sessions
376
- * @returns {Array} - Array of active session info
377
- */
378
- export function getActiveCodexSessions() {
379
- const sessions = [];
380
-
381
- for (const [id, session] of activeCodexSessions.entries()) {
382
- if (session.status === 'running') {
383
- sessions.push({
384
- id,
385
- status: session.status,
386
- startedAt: session.startedAt
387
- });
388
- }
389
- }
390
-
391
- return sessions;
392
- }
393
-
394
- /**
395
- * Helper to send message via WebSocket or writer
396
- * @param {WebSocket|object} ws - WebSocket or response writer
397
- * @param {object} data - Data to send
398
- */
399
- function sendMessage(ws, data) {
400
- try {
401
- if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
402
- // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
403
- ws.send(data);
404
- } else if (typeof ws.send === 'function') {
405
- // Raw WebSocket - stringify here
406
- ws.send(JSON.stringify(data));
407
- }
408
- } catch (error) {
409
- console.error('[Codex] Error sending message:', error);
410
- }
411
- }
412
-
413
- // Clean up old completed sessions periodically
414
- setInterval(() => {
415
- const now = Date.now();
416
- const maxAge = 30 * 60 * 1000; // 30 minutes
417
-
418
- for (const [id, session] of activeCodexSessions.entries()) {
419
- if (session.status !== 'running') {
420
- const startedAt = new Date(session.startedAt).getTime();
421
- if (now - startedAt > maxAge) {
422
- activeCodexSessions.delete(id);
423
- }
424
- }
425
- }
426
- }, 5 * 60 * 1000); // Every 5 minutes
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 { sessionsService } from './modules/providers/services/sessions.service.js';
19
+ import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
20
+ import { createNormalizedMessage } from './shared/utils.js';
21
+
22
+ // Track active sessions
23
+ const activeCodexSessions = new Map();
24
+
25
+ /**
26
+ * Transform Codex SDK event to WebSocket message format
27
+ * @param {object} event - SDK event
28
+ * @returns {object} - Transformed event for WebSocket
29
+ */
30
+ function transformCodexEvent(event) {
31
+ // Map SDK event types to a consistent format
32
+ switch (event.type) {
33
+ case 'item.started':
34
+ case 'item.updated':
35
+ case 'item.completed':
36
+ const item = event.item;
37
+ if (!item) {
38
+ return { type: event.type, item: null };
39
+ }
40
+
41
+ // Transform based on item type
42
+ switch (item.type) {
43
+ case 'agent_message':
44
+ return {
45
+ type: 'item',
46
+ itemType: 'agent_message',
47
+ message: {
48
+ role: 'assistant',
49
+ content: item.text
50
+ }
51
+ };
52
+
53
+ case 'reasoning':
54
+ return {
55
+ type: 'item',
56
+ itemType: 'reasoning',
57
+ message: {
58
+ role: 'assistant',
59
+ content: item.text,
60
+ isReasoning: true
61
+ }
62
+ };
63
+
64
+ case 'command_execution':
65
+ return {
66
+ type: 'item',
67
+ itemType: 'command_execution',
68
+ command: item.command,
69
+ output: item.aggregated_output,
70
+ exitCode: item.exit_code,
71
+ status: item.status
72
+ };
73
+
74
+ case 'file_change':
75
+ return {
76
+ type: 'item',
77
+ itemType: 'file_change',
78
+ changes: item.changes,
79
+ status: item.status
80
+ };
81
+
82
+ case 'mcp_tool_call':
83
+ return {
84
+ type: 'item',
85
+ itemType: 'mcp_tool_call',
86
+ server: item.server,
87
+ tool: item.tool,
88
+ arguments: item.arguments,
89
+ result: item.result,
90
+ error: item.error,
91
+ status: item.status
92
+ };
93
+
94
+ case 'web_search':
95
+ return {
96
+ type: 'item',
97
+ itemType: 'web_search',
98
+ query: item.query
99
+ };
100
+
101
+ case 'todo_list':
102
+ return {
103
+ type: 'item',
104
+ itemType: 'todo_list',
105
+ items: item.items
106
+ };
107
+
108
+ case 'error':
109
+ return {
110
+ type: 'item',
111
+ itemType: 'error',
112
+ message: {
113
+ role: 'error',
114
+ content: item.message
115
+ }
116
+ };
117
+
118
+ default:
119
+ return {
120
+ type: 'item',
121
+ itemType: item.type,
122
+ item: item
123
+ };
124
+ }
125
+
126
+ case 'turn.started':
127
+ return {
128
+ type: 'turn_started'
129
+ };
130
+
131
+ case 'turn.completed':
132
+ return {
133
+ type: 'turn_complete',
134
+ usage: event.usage
135
+ };
136
+
137
+ case 'turn.failed':
138
+ return {
139
+ type: 'turn_failed',
140
+ error: event.error
141
+ };
142
+
143
+ case 'thread.started':
144
+ return {
145
+ type: 'thread_started',
146
+ threadId: event.id
147
+ };
148
+
149
+ case 'error':
150
+ return {
151
+ type: 'error',
152
+ message: event.message
153
+ };
154
+
155
+ default:
156
+ return {
157
+ type: event.type,
158
+ data: event
159
+ };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Map permission mode to Codex SDK options
165
+ * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
166
+ * @returns {object} - { sandboxMode, approvalPolicy }
167
+ */
168
+ function mapPermissionModeToCodexOptions(permissionMode) {
169
+ switch (permissionMode) {
170
+ case 'acceptEdits':
171
+ return {
172
+ sandboxMode: 'workspace-write',
173
+ approvalPolicy: 'never'
174
+ };
175
+ case 'bypassPermissions':
176
+ return {
177
+ sandboxMode: 'danger-full-access',
178
+ approvalPolicy: 'never'
179
+ };
180
+ case 'default':
181
+ default:
182
+ return {
183
+ sandboxMode: 'workspace-write',
184
+ approvalPolicy: 'untrusted'
185
+ };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Execute a Codex query with streaming
191
+ * @param {string} command - The prompt to send
192
+ * @param {object} options - Options including cwd, sessionId, model, permissionMode
193
+ * @param {WebSocket|object} ws - WebSocket connection or response writer
194
+ */
195
+ export async function queryCodex(command, options = {}, ws) {
196
+ const {
197
+ sessionId,
198
+ sessionSummary,
199
+ cwd,
200
+ projectPath,
201
+ model,
202
+ permissionMode = 'default'
203
+ } = options;
204
+
205
+ const workingDirectory = cwd || projectPath || process.cwd();
206
+ const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
207
+
208
+ let codex;
209
+ let thread;
210
+ let currentSessionId = sessionId;
211
+ let terminalFailure = null;
212
+ const abortController = new AbortController();
213
+
214
+ try {
215
+ // Initialize Codex SDK
216
+ codex = new Codex();
217
+
218
+ // Thread options with sandbox and approval settings
219
+ const threadOptions = {
220
+ workingDirectory,
221
+ skipGitRepoCheck: true,
222
+ sandboxMode,
223
+ approvalPolicy,
224
+ model
225
+ };
226
+
227
+ // Start or resume thread
228
+ if (sessionId) {
229
+ thread = codex.resumeThread(sessionId, threadOptions);
230
+ } else {
231
+ thread = codex.startThread(threadOptions);
232
+ }
233
+
234
+ // Get the thread ID
235
+ currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
236
+
237
+ // Track the session
238
+ activeCodexSessions.set(currentSessionId, {
239
+ thread,
240
+ codex,
241
+ status: 'running',
242
+ abortController,
243
+ startedAt: new Date().toISOString()
244
+ });
245
+
246
+ // Send session created event
247
+ sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
248
+
249
+ // Execute with streaming
250
+ const streamedTurn = await thread.runStreamed(command, {
251
+ signal: abortController.signal
252
+ });
253
+
254
+ for await (const event of streamedTurn.events) {
255
+ // Check if session was aborted
256
+ const session = activeCodexSessions.get(currentSessionId);
257
+ if (!session || session.status === 'aborted') {
258
+ break;
259
+ }
260
+
261
+ if (event.type === 'item.started' || event.type === 'item.updated') {
262
+ continue;
263
+ }
264
+
265
+ const transformed = transformCodexEvent(event);
266
+
267
+ // Normalize the transformed event into NormalizedMessage(s) via adapter
268
+ const normalizedMsgs = sessionsService.normalizeMessage('codex', transformed, currentSessionId);
269
+ for (const msg of normalizedMsgs) {
270
+ sendMessage(ws, msg);
271
+ }
272
+
273
+ if (event.type === 'turn.failed' && !terminalFailure) {
274
+ terminalFailure = event.error || new Error('Turn failed');
275
+ notifyRunFailed({
276
+ userId: ws?.userId || null,
277
+ provider: 'codex',
278
+ sessionId: currentSessionId,
279
+ sessionName: sessionSummary,
280
+ error: terminalFailure
281
+ });
282
+ }
283
+
284
+ // Extract and send token usage if available (normalized to match Claude format)
285
+ if (event.type === 'turn.completed' && event.usage) {
286
+ const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
287
+ sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
288
+ }
289
+ }
290
+
291
+ // Send completion event
292
+ if (!terminalFailure) {
293
+ sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
294
+ notifyRunStopped({
295
+ userId: ws?.userId || null,
296
+ provider: 'codex',
297
+ sessionId: currentSessionId,
298
+ sessionName: sessionSummary,
299
+ stopReason: 'completed'
300
+ });
301
+ }
302
+
303
+ } catch (error) {
304
+ const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
305
+ const wasAborted =
306
+ session?.status === 'aborted' ||
307
+ error?.name === 'AbortError' ||
308
+ String(error?.message || '').toLowerCase().includes('aborted');
309
+
310
+ if (!wasAborted) {
311
+ console.error('[Codex] Error:', error);
312
+
313
+ // Check if Codex SDK is available for a clearer error message
314
+ const installed = await providerAuthService.isProviderInstalled('codex');
315
+ const errorContent = !installed
316
+ ? 'Codex CLI is not configured. Please set up authentication first.'
317
+ : error.message;
318
+
319
+ sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: currentSessionId, provider: 'codex' }));
320
+ if (!terminalFailure) {
321
+ notifyRunFailed({
322
+ userId: ws?.userId || null,
323
+ provider: 'codex',
324
+ sessionId: currentSessionId,
325
+ sessionName: sessionSummary,
326
+ error
327
+ });
328
+ }
329
+ }
330
+
331
+ } finally {
332
+ // Update session status
333
+ if (currentSessionId) {
334
+ const session = activeCodexSessions.get(currentSessionId);
335
+ if (session) {
336
+ session.status = session.status === 'aborted' ? 'aborted' : 'completed';
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Abort an active Codex session
344
+ * @param {string} sessionId - Session ID to abort
345
+ * @returns {boolean} - Whether abort was successful
346
+ */
347
+ export function abortCodexSession(sessionId) {
348
+ const session = activeCodexSessions.get(sessionId);
349
+
350
+ if (!session) {
351
+ return false;
352
+ }
353
+
354
+ session.status = 'aborted';
355
+ try {
356
+ session.abortController?.abort();
357
+ } catch (error) {
358
+ console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
359
+ }
360
+
361
+ return true;
362
+ }
363
+
364
+ /**
365
+ * Check if a session is active
366
+ * @param {string} sessionId - Session ID to check
367
+ * @returns {boolean} - Whether session is active
368
+ */
369
+ export function isCodexSessionActive(sessionId) {
370
+ const session = activeCodexSessions.get(sessionId);
371
+ return session?.status === 'running';
372
+ }
373
+
374
+ /**
375
+ * Get all active sessions
376
+ * @returns {Array} - Array of active session info
377
+ */
378
+ export function getActiveCodexSessions() {
379
+ const sessions = [];
380
+
381
+ for (const [id, session] of activeCodexSessions.entries()) {
382
+ if (session.status === 'running') {
383
+ sessions.push({
384
+ id,
385
+ status: session.status,
386
+ startedAt: session.startedAt
387
+ });
388
+ }
389
+ }
390
+
391
+ return sessions;
392
+ }
393
+
394
+ /**
395
+ * Helper to send message via WebSocket or writer
396
+ * @param {WebSocket|object} ws - WebSocket or response writer
397
+ * @param {object} data - Data to send
398
+ */
399
+ function sendMessage(ws, data) {
400
+ try {
401
+ if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
402
+ // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
403
+ ws.send(data);
404
+ } else if (typeof ws.send === 'function') {
405
+ // Raw WebSocket - stringify here
406
+ ws.send(JSON.stringify(data));
407
+ }
408
+ } catch (error) {
409
+ console.error('[Codex] Error sending message:', error);
410
+ }
411
+ }
412
+
413
+ // Clean up old completed sessions periodically
414
+ setInterval(() => {
415
+ const now = Date.now();
416
+ const maxAge = 30 * 60 * 1000; // 30 minutes
417
+
418
+ for (const [id, session] of activeCodexSessions.entries()) {
419
+ if (session.status !== 'running') {
420
+ const startedAt = new Date(session.startedAt).getTime();
421
+ if (now - startedAt > maxAge) {
422
+ activeCodexSessions.delete(id);
423
+ }
424
+ }
425
+ }
426
+ }, 5 * 60 * 1000); // Every 5 minutes