@pixelbyte-software/pixcode 1.30.2 → 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 (209) 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-BtOeB3cE.js +837 -0
  11. package/dist/assets/index-CDpePeIN.css +32 -0
  12. package/dist/assets/vendor-codemirror-CzYAOTxS.js +41 -0
  13. package/dist/clear-cache.html +85 -85
  14. package/dist/convert-icons.md +52 -52
  15. package/dist/favicon.png +0 -0
  16. package/dist/favicon.svg +7 -8
  17. package/dist/generate-icons.js +48 -48
  18. package/dist/icons/codex-white.svg +3 -3
  19. package/dist/icons/codex.svg +3 -3
  20. package/dist/icons/cursor-white.svg +11 -11
  21. package/dist/icons/icon-128x128.png +0 -0
  22. package/dist/icons/icon-128x128.svg +9 -12
  23. package/dist/icons/icon-144x144.png +0 -0
  24. package/dist/icons/icon-144x144.svg +9 -12
  25. package/dist/icons/icon-152x152.png +0 -0
  26. package/dist/icons/icon-152x152.svg +9 -12
  27. package/dist/icons/icon-192x192.png +0 -0
  28. package/dist/icons/icon-192x192.svg +9 -12
  29. package/dist/icons/icon-384x384.png +0 -0
  30. package/dist/icons/icon-384x384.svg +9 -12
  31. package/dist/icons/icon-512x512.png +0 -0
  32. package/dist/icons/icon-512x512.svg +9 -12
  33. package/dist/icons/icon-72x72.png +0 -0
  34. package/dist/icons/icon-72x72.svg +9 -12
  35. package/dist/icons/icon-96x96.png +0 -0
  36. package/dist/icons/icon-96x96.svg +9 -12
  37. package/dist/icons/icon-template.svg +9 -12
  38. package/dist/icons/qwen-ai-icon.png +0 -0
  39. package/dist/index.html +60 -50
  40. package/dist/logo.png +0 -0
  41. package/dist/logo.svg +11 -16
  42. package/dist/manifest.json +60 -60
  43. package/dist/sw.js +124 -124
  44. package/dist-server/server/claude-sdk.js +28 -5
  45. package/dist-server/server/claude-sdk.js.map +1 -1
  46. package/dist-server/server/cli.js +100 -97
  47. package/dist-server/server/cli.js.map +1 -1
  48. package/dist-server/server/daemon/manager.js +33 -33
  49. package/dist-server/server/daemon-manager.js +62 -62
  50. package/dist-server/server/database/db.js +114 -22
  51. package/dist-server/server/database/db.js.map +1 -1
  52. package/dist-server/server/database/schema.js +122 -89
  53. package/dist-server/server/database/schema.js.map +1 -1
  54. package/dist-server/server/gemini-cli.js +6 -1
  55. package/dist-server/server/gemini-cli.js.map +1 -1
  56. package/dist-server/server/index.js +346 -61
  57. package/dist-server/server/index.js.map +1 -1
  58. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +29 -2
  59. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +22 -2
  61. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -1
  62. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +2 -2
  63. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  64. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +14 -2
  65. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  66. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js +132 -0
  67. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -0
  68. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js +87 -0
  69. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js.map +1 -0
  70. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js +201 -0
  71. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js.map +1 -0
  72. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js +19 -0
  73. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js.map +1 -0
  74. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  75. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  76. package/dist-server/server/modules/providers/provider.routes.js +478 -1
  77. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  78. package/dist-server/server/modules/providers/shared/provider-configs.js +105 -0
  79. package/dist-server/server/modules/providers/shared/provider-configs.js.map +1 -0
  80. package/dist-server/server/projects.js +197 -6
  81. package/dist-server/server/projects.js.map +1 -1
  82. package/dist-server/server/qwen-code-cli.js +350 -0
  83. package/dist-server/server/qwen-code-cli.js.map +1 -0
  84. package/dist-server/server/qwen-response-handler.js +70 -0
  85. package/dist-server/server/qwen-response-handler.js.map +1 -0
  86. package/dist-server/server/routes/commands.js +25 -25
  87. package/dist-server/server/routes/git.js +17 -17
  88. package/dist-server/server/routes/network.js +116 -0
  89. package/dist-server/server/routes/network.js.map +1 -0
  90. package/dist-server/server/routes/projects.js +166 -1
  91. package/dist-server/server/routes/projects.js.map +1 -1
  92. package/dist-server/server/routes/qwen.js +23 -0
  93. package/dist-server/server/routes/qwen.js.map +1 -0
  94. package/dist-server/server/routes/taskmaster.js +419 -419
  95. package/dist-server/server/routes/telegram.js +119 -0
  96. package/dist-server/server/routes/telegram.js.map +1 -0
  97. package/dist-server/server/services/external-access.js +228 -0
  98. package/dist-server/server/services/external-access.js.map +1 -0
  99. package/dist-server/server/services/install-jobs.js +552 -0
  100. package/dist-server/server/services/install-jobs.js.map +1 -0
  101. package/dist-server/server/services/notification-orchestrator.js +19 -5
  102. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  103. package/dist-server/server/services/provider-credentials.js +154 -0
  104. package/dist-server/server/services/provider-credentials.js.map +1 -0
  105. package/dist-server/server/services/provider-models.js +218 -0
  106. package/dist-server/server/services/provider-models.js.map +1 -0
  107. package/dist-server/server/services/telegram/bot.js +259 -0
  108. package/dist-server/server/services/telegram/bot.js.map +1 -0
  109. package/dist-server/server/services/telegram/translations.js +160 -0
  110. package/dist-server/server/services/telegram/translations.js.map +1 -0
  111. package/dist-server/server/utils/port-access.js +196 -0
  112. package/dist-server/server/utils/port-access.js.map +1 -0
  113. package/dist-server/shared/modelConstants.js +18 -0
  114. package/dist-server/shared/modelConstants.js.map +1 -1
  115. package/package.json +177 -168
  116. package/scripts/fix-node-pty.js +67 -67
  117. package/server/claude-sdk.js +857 -834
  118. package/server/cli.js +940 -937
  119. package/server/constants/config.js +4 -4
  120. package/server/cursor-cli.js +342 -342
  121. package/server/daemon/manager.js +564 -564
  122. package/server/daemon-manager.js +920 -920
  123. package/server/database/db.js +696 -593
  124. package/server/database/schema.js +138 -102
  125. package/server/gemini-cli.js +475 -469
  126. package/server/gemini-response-handler.js +79 -79
  127. package/server/index.js +2854 -2556
  128. package/server/load-env.js +34 -34
  129. package/server/middleware/auth.js +132 -132
  130. package/server/modules/providers/list/claude/claude-auth.provider.ts +145 -123
  131. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  132. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  133. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  134. package/server/modules/providers/list/codex/codex-auth.provider.ts +115 -100
  135. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  136. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  137. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  138. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  139. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  140. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  141. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  142. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +163 -151
  143. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  144. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  145. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  146. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -0
  147. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -0
  148. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +218 -0
  149. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -0
  150. package/server/modules/providers/provider.registry.ts +38 -36
  151. package/server/modules/providers/provider.routes.ts +781 -217
  152. package/server/modules/providers/services/mcp.service.ts +94 -94
  153. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  154. package/server/modules/providers/services/sessions.service.ts +45 -45
  155. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  156. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  157. package/server/modules/providers/shared/provider-configs.ts +118 -0
  158. package/server/modules/providers/tests/mcp.test.ts +293 -293
  159. package/server/openai-codex.js +426 -426
  160. package/server/projects.js +2993 -2792
  161. package/server/qwen-code-cli.js +392 -0
  162. package/server/qwen-response-handler.js +73 -0
  163. package/server/routes/agent.js +1245 -1245
  164. package/server/routes/auth.js +134 -134
  165. package/server/routes/codex.js +19 -19
  166. package/server/routes/commands.js +554 -554
  167. package/server/routes/cursor.js +52 -52
  168. package/server/routes/gemini.js +24 -24
  169. package/server/routes/git.js +1488 -1488
  170. package/server/routes/mcp-utils.js +31 -31
  171. package/server/routes/messages.js +61 -61
  172. package/server/routes/network.js +128 -0
  173. package/server/routes/plugins.js +307 -307
  174. package/server/routes/projects.js +795 -627
  175. package/server/routes/qwen.js +27 -0
  176. package/server/routes/settings.js +286 -286
  177. package/server/routes/taskmaster.js +1471 -1471
  178. package/server/routes/telegram.js +125 -0
  179. package/server/routes/user.js +123 -123
  180. package/server/services/external-access.js +240 -0
  181. package/server/services/install-jobs.js +569 -0
  182. package/server/services/notification-orchestrator.js +242 -227
  183. package/server/services/provider-credentials.js +151 -0
  184. package/server/services/provider-models.js +225 -0
  185. package/server/services/telegram/bot.js +280 -0
  186. package/server/services/telegram/translations.js +170 -0
  187. package/server/services/vapid-keys.js +35 -35
  188. package/server/sessionManager.js +225 -225
  189. package/server/shared/interfaces.ts +54 -54
  190. package/server/shared/types.ts +172 -172
  191. package/server/shared/utils.ts +193 -193
  192. package/server/tsconfig.json +36 -36
  193. package/server/utils/colors.js +21 -21
  194. package/server/utils/commandParser.js +303 -303
  195. package/server/utils/frontmatter.js +18 -18
  196. package/server/utils/gitConfig.js +34 -34
  197. package/server/utils/mcp-detector.js +147 -147
  198. package/server/utils/plugin-loader.js +457 -457
  199. package/server/utils/plugin-process-manager.js +184 -184
  200. package/server/utils/port-access.js +209 -0
  201. package/server/utils/runtime-paths.js +37 -37
  202. package/server/utils/taskmaster-websocket.js +128 -128
  203. package/server/utils/url-detection.js +71 -71
  204. package/server/vite-daemon.js +78 -78
  205. package/shared/modelConstants.js +117 -97
  206. package/shared/networkHosts.js +22 -22
  207. package/dist/assets/index-C2c9QNwK.css +0 -32
  208. package/dist/assets/index-DyXDZED-.js +0 -1277
  209. package/dist/assets/vendor-codemirror-NA4v81it.js +0 -41
@@ -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