notioncode 0.1.1 → 0.1.3

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 (77) hide show
  1. package/README.md +10 -4
  2. package/agent-runtime-server/package-lock.json +4381 -0
  3. package/agent-runtime-server/package.json +36 -0
  4. package/agent-runtime-server/scripts/fix-node-pty.js +67 -0
  5. package/agent-runtime-server/server/agent-session-service.js +816 -0
  6. package/agent-runtime-server/server/claude-sdk.js +836 -0
  7. package/agent-runtime-server/server/cli.js +330 -0
  8. package/agent-runtime-server/server/constants/config.js +5 -0
  9. package/agent-runtime-server/server/cursor-cli.js +335 -0
  10. package/agent-runtime-server/server/database/db.js +653 -0
  11. package/agent-runtime-server/server/database/init.sql +99 -0
  12. package/agent-runtime-server/server/gemini-cli.js +460 -0
  13. package/agent-runtime-server/server/gemini-response-handler.js +79 -0
  14. package/agent-runtime-server/server/index.js +2569 -0
  15. package/agent-runtime-server/server/load-env.js +32 -0
  16. package/agent-runtime-server/server/middleware/auth.js +132 -0
  17. package/agent-runtime-server/server/openai-codex.js +512 -0
  18. package/agent-runtime-server/server/projects.js +2594 -0
  19. package/agent-runtime-server/server/providers/claude/adapter.js +278 -0
  20. package/agent-runtime-server/server/providers/codex/adapter.js +248 -0
  21. package/agent-runtime-server/server/providers/cursor/adapter.js +353 -0
  22. package/agent-runtime-server/server/providers/gemini/adapter.js +186 -0
  23. package/agent-runtime-server/server/providers/registry.js +44 -0
  24. package/agent-runtime-server/server/providers/types.js +119 -0
  25. package/agent-runtime-server/server/providers/utils.js +29 -0
  26. package/agent-runtime-server/server/routes/agent-sessions.js +238 -0
  27. package/agent-runtime-server/server/routes/agent.js +1244 -0
  28. package/agent-runtime-server/server/routes/auth.js +144 -0
  29. package/agent-runtime-server/server/routes/cli-auth.js +478 -0
  30. package/agent-runtime-server/server/routes/codex.js +329 -0
  31. package/agent-runtime-server/server/routes/commands.js +596 -0
  32. package/agent-runtime-server/server/routes/cursor.js +798 -0
  33. package/agent-runtime-server/server/routes/gemini.js +24 -0
  34. package/agent-runtime-server/server/routes/git.js +1508 -0
  35. package/agent-runtime-server/server/routes/mcp-utils.js +48 -0
  36. package/agent-runtime-server/server/routes/mcp.js +552 -0
  37. package/agent-runtime-server/server/routes/messages.js +61 -0
  38. package/agent-runtime-server/server/routes/plugins.js +307 -0
  39. package/agent-runtime-server/server/routes/projects.js +548 -0
  40. package/agent-runtime-server/server/routes/settings.js +276 -0
  41. package/agent-runtime-server/server/routes/taskmaster.js +1963 -0
  42. package/agent-runtime-server/server/routes/user.js +123 -0
  43. package/agent-runtime-server/server/services/notification-orchestrator.js +227 -0
  44. package/agent-runtime-server/server/services/vapid-keys.js +35 -0
  45. package/agent-runtime-server/server/sessionManager.js +226 -0
  46. package/agent-runtime-server/server/utils/commandParser.js +303 -0
  47. package/agent-runtime-server/server/utils/frontmatter.js +18 -0
  48. package/agent-runtime-server/server/utils/gitConfig.js +34 -0
  49. package/agent-runtime-server/server/utils/mcp-detector.js +198 -0
  50. package/agent-runtime-server/server/utils/plugin-loader.js +457 -0
  51. package/agent-runtime-server/server/utils/plugin-process-manager.js +184 -0
  52. package/agent-runtime-server/server/utils/taskmaster-websocket.js +129 -0
  53. package/agent-runtime-server/shared/modelConstants.js +12 -0
  54. package/agent-runtime-server/shared/modelConstants.test.js +34 -0
  55. package/agent-runtime-server/shared/networkHosts.js +22 -0
  56. package/agent-runtime-server/test_sdk.mjs +16 -0
  57. package/bin/bridges/darwin-x64/nocode-bridge +0 -0
  58. package/bin/{nocode-local.js → notioncode.js} +0 -0
  59. package/dist/assets/icon-CQtd7WEB.png +0 -0
  60. package/dist/assets/index-Ctr1ES45.js +1 -0
  61. package/dist/assets/index-DhCWie1Z.css +1 -0
  62. package/dist/assets/index-DzqxG7Z8.js +689 -0
  63. package/dist/index.html +46 -0
  64. package/dist/onboarding/step1_create.png +0 -0
  65. package/dist/onboarding/step2_capabilities.png +0 -0
  66. package/dist/onboarding/step2b_content_access.png +0 -0
  67. package/dist/onboarding/step2c_page_access.png +0 -0
  68. package/dist/onboarding/step3_token.png +0 -0
  69. package/dist/onboarding/step4_webhook.png +0 -0
  70. package/dist/onboarding/step6a_verify.png +0 -0
  71. package/dist/onboarding/step6b_copy_verify_token.png +0 -0
  72. package/dist/tinyfish-fish-only.png +0 -0
  73. package/lib/install.js +33 -2
  74. package/lib/start.js +157 -25
  75. package/package.json +7 -4
  76. package/src/shared/modelRegistry.d.ts +24 -0
  77. package/src/shared/modelRegistry.js +163 -0
@@ -0,0 +1,32 @@
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
+ let envPath = path.join(__dirname, '../../.env'); // Root workspace .env
13
+ if (!fs.existsSync(envPath)) {
14
+ envPath = path.join(__dirname, '../.env'); // Local module .env fallback
15
+ }
16
+ const envFile = fs.readFileSync(envPath, 'utf8');
17
+ envFile.split('\n').forEach(line => {
18
+ const trimmedLine = line.trim();
19
+ if (trimmedLine && !trimmedLine.startsWith('#')) {
20
+ const [key, ...valueParts] = trimmedLine.split('=');
21
+ if (key && valueParts.length > 0 && !process.env[key]) {
22
+ process.env[key] = valueParts.join('=').trim();
23
+ }
24
+ }
25
+ });
26
+ } catch (e) {
27
+ console.log('No .env file found or error reading it:', e.message);
28
+ }
29
+
30
+ if (!process.env.DATABASE_PATH) {
31
+ process.env.DATABASE_PATH = path.join(os.homedir(), '.notion-code', 'agent-runtime', 'auth.db');
32
+ }
@@ -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.ensurePlatformUser();
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.ensurePlatformUser();
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,512 @@
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 fs from 'fs';
17
+ import { Codex } from '@openai/codex-sdk';
18
+ import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
19
+ import { codexAdapter } from './providers/codex/adapter.js';
20
+ import { createNormalizedMessage } from './providers/types.js';
21
+
22
+ // Track active sessions
23
+ const activeCodexSessions = new Map();
24
+
25
+ const DEFAULT_CLI_PATH_DIRS = [
26
+ '/opt/homebrew/bin',
27
+ '/usr/local/bin',
28
+ '/usr/bin',
29
+ '/bin',
30
+ '/usr/sbin',
31
+ '/sbin'
32
+ ];
33
+
34
+ function withDefaultCliPath(env = process.env) {
35
+ const pathParts = [
36
+ ...(env.PATH || '').split(':'),
37
+ ...DEFAULT_CLI_PATH_DIRS
38
+ ].filter(Boolean);
39
+
40
+ return {
41
+ ...env,
42
+ PATH: [...new Set(pathParts)].join(':')
43
+ };
44
+ }
45
+
46
+ function isExecutable(filePath) {
47
+ try {
48
+ fs.accessSync(filePath, fs.constants.X_OK);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ function resolveCodexCliPath(env) {
56
+ const override = env.CODEX_BINARY || env.CODEX_CLI_PATH || env.CODEX_PATH;
57
+ if (override && isExecutable(override)) {
58
+ return override;
59
+ }
60
+
61
+ for (const dir of (env.PATH || '').split(':')) {
62
+ const candidate = `${dir.replace(/\/$/, '')}/codex`;
63
+ if (isExecutable(candidate)) {
64
+ return candidate;
65
+ }
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Transform Codex SDK event to WebSocket message format
73
+ * @param {object} event - SDK event
74
+ * @returns {object} - Transformed event for WebSocket
75
+ */
76
+ function transformCodexEvent(event) {
77
+ // Map SDK event types to a consistent format
78
+ switch (event.type) {
79
+ case 'item.started':
80
+ case 'item.updated':
81
+ case 'item.completed':
82
+ const item = event.item;
83
+ if (!item) {
84
+ return { type: event.type, item: null };
85
+ }
86
+
87
+ // Transform based on item type
88
+ switch (item.type) {
89
+ case 'agent_message':
90
+ return {
91
+ type: 'item',
92
+ itemType: 'agent_message',
93
+ message: {
94
+ role: 'assistant',
95
+ content: item.text
96
+ }
97
+ };
98
+
99
+ case 'reasoning':
100
+ return {
101
+ type: 'item',
102
+ itemType: 'reasoning',
103
+ message: {
104
+ role: 'assistant',
105
+ content: item.text,
106
+ isReasoning: true
107
+ }
108
+ };
109
+
110
+ case 'command_execution':
111
+ return {
112
+ type: 'item',
113
+ itemType: 'command_execution',
114
+ command: item.command,
115
+ output: item.aggregated_output,
116
+ exitCode: item.exit_code,
117
+ status: item.status
118
+ };
119
+
120
+ case 'file_change':
121
+ return {
122
+ type: 'item',
123
+ itemType: 'file_change',
124
+ changes: item.changes,
125
+ status: item.status
126
+ };
127
+
128
+ case 'mcp_tool_call':
129
+ return {
130
+ type: 'item',
131
+ itemType: 'mcp_tool_call',
132
+ server: item.server,
133
+ tool: item.tool,
134
+ arguments: item.arguments,
135
+ result: item.result,
136
+ error: item.error,
137
+ status: item.status
138
+ };
139
+
140
+ case 'web_search':
141
+ return {
142
+ type: 'item',
143
+ itemType: 'web_search',
144
+ query: item.query
145
+ };
146
+
147
+ case 'todo_list':
148
+ return {
149
+ type: 'item',
150
+ itemType: 'todo_list',
151
+ items: item.items
152
+ };
153
+
154
+ case 'error':
155
+ return {
156
+ type: 'item',
157
+ itemType: 'error',
158
+ message: {
159
+ role: 'error',
160
+ content: item.message
161
+ }
162
+ };
163
+
164
+ default:
165
+ return {
166
+ type: 'item',
167
+ itemType: item.type,
168
+ item: item
169
+ };
170
+ }
171
+
172
+ case 'turn.started':
173
+ return {
174
+ type: 'turn_started'
175
+ };
176
+
177
+ case 'turn.completed':
178
+ return {
179
+ type: 'turn_complete',
180
+ usage: event.usage
181
+ };
182
+
183
+ case 'turn.failed':
184
+ return {
185
+ type: 'turn_failed',
186
+ error: event.error
187
+ };
188
+
189
+ case 'thread.started':
190
+ return {
191
+ type: 'thread_started',
192
+ threadId: event.id
193
+ };
194
+
195
+ case 'error':
196
+ return {
197
+ type: 'error',
198
+ message: event.message
199
+ };
200
+
201
+ default:
202
+ return {
203
+ type: event.type,
204
+ data: event
205
+ };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Map permission mode to Codex SDK options
211
+ * @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
212
+ * @returns {object} - { sandboxMode, approvalPolicy }
213
+ */
214
+ function mapPermissionModeToCodexOptions(permissionMode) {
215
+ switch (permissionMode) {
216
+ case 'acceptEdits':
217
+ return {
218
+ sandboxMode: 'workspace-write',
219
+ approvalPolicy: 'never'
220
+ };
221
+ case 'bypassPermissions':
222
+ return {
223
+ sandboxMode: 'danger-full-access',
224
+ approvalPolicy: 'never'
225
+ };
226
+ case 'default':
227
+ default:
228
+ return {
229
+ sandboxMode: 'workspace-write',
230
+ approvalPolicy: 'untrusted'
231
+ };
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Execute a Codex query with streaming
237
+ * @param {string} command - The prompt to send
238
+ * @param {object} options - Options including cwd, sessionId, model, permissionMode
239
+ * @param {WebSocket|object} ws - WebSocket connection or response writer
240
+ */
241
+ export async function queryCodex(command, options = {}, ws) {
242
+ const {
243
+ sessionId,
244
+ sessionSummary,
245
+ cwd,
246
+ projectPath,
247
+ model,
248
+ modelReasoningEffort,
249
+ apiKey,
250
+ baseUrl,
251
+ permissionMode = 'default',
252
+ sandboxMode,
253
+ approvalPolicy,
254
+ webSearchMode,
255
+ webSearchEnabled,
256
+ networkAccessEnabled,
257
+ additionalDirectories,
258
+ outputSchema
259
+ } = options;
260
+
261
+ const workingDirectory = cwd || projectPath || process.cwd();
262
+ const mappedPermissions = mapPermissionModeToCodexOptions(permissionMode);
263
+ const effectiveSandboxMode = sandboxMode || mappedPermissions.sandboxMode;
264
+ const effectiveApprovalPolicy = approvalPolicy || mappedPermissions.approvalPolicy;
265
+
266
+ let codex;
267
+ let thread;
268
+ let currentSessionId = sessionId;
269
+ let terminalFailure = null;
270
+ const abortController = new AbortController();
271
+
272
+ try {
273
+ // Initialize Codex SDK
274
+ // When no apiKey/baseUrl is provided, the SDK spawns the codex CLI which
275
+ // uses its own auth (ChatGPT OAuth stored in ~/.codex/auth.json).
276
+ // Only override when explicitly configured (e.g. API key or custom proxy).
277
+ const codexOptions = {};
278
+ codexOptions.env = withDefaultCliPath(process.env);
279
+ const codexCliPath = resolveCodexCliPath(codexOptions.env);
280
+ if (codexCliPath) {
281
+ codexOptions.codexPathOverride = codexCliPath;
282
+ }
283
+ if (baseUrl || process.env.OPENAI_BASE_URL) {
284
+ codexOptions.baseUrl = baseUrl || process.env.OPENAI_BASE_URL;
285
+ }
286
+ if (apiKey) {
287
+ codexOptions.apiKey = apiKey;
288
+ }
289
+ codex = new Codex(codexOptions);
290
+
291
+ // Thread options with sandbox and approval settings
292
+ const threadOptions = {
293
+ workingDirectory,
294
+ skipGitRepoCheck: true,
295
+ sandboxMode: effectiveSandboxMode,
296
+ approvalPolicy: effectiveApprovalPolicy,
297
+ model,
298
+ modelReasoningEffort,
299
+ webSearchMode,
300
+ webSearchEnabled,
301
+ networkAccessEnabled,
302
+ additionalDirectories
303
+ };
304
+
305
+ // Start or resume thread
306
+ if (sessionId) {
307
+ thread = codex.resumeThread(sessionId, threadOptions);
308
+ } else {
309
+ thread = codex.startThread(threadOptions);
310
+ }
311
+
312
+ // Get the thread ID
313
+ currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
314
+
315
+ // Track the session
316
+ activeCodexSessions.set(currentSessionId, {
317
+ thread,
318
+ codex,
319
+ status: 'running',
320
+ abortController,
321
+ startedAt: new Date().toISOString()
322
+ });
323
+
324
+ // Send session created event
325
+ sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
326
+
327
+ // Execute with streaming
328
+ const streamedTurn = await thread.runStreamed(command, {
329
+ signal: abortController.signal,
330
+ outputSchema
331
+ });
332
+
333
+ for await (const event of streamedTurn.events) {
334
+ // Check if session was aborted
335
+ const session = activeCodexSessions.get(currentSessionId);
336
+ if (!session || session.status === 'aborted') {
337
+ break;
338
+ }
339
+
340
+ if (event.type === 'item.started' || event.type === 'item.updated') {
341
+ continue;
342
+ }
343
+
344
+ const transformed = transformCodexEvent(event);
345
+
346
+ // Normalize the transformed event into NormalizedMessage(s) via adapter
347
+ const normalizedMsgs = codexAdapter.normalizeMessage(transformed, currentSessionId);
348
+ for (const msg of normalizedMsgs) {
349
+ sendMessage(ws, msg);
350
+ }
351
+
352
+ if (event.type === 'turn.failed' && !terminalFailure) {
353
+ terminalFailure = event.error || new Error('Turn failed');
354
+ notifyRunFailed({
355
+ userId: ws?.userId || null,
356
+ provider: 'codex',
357
+ sessionId: currentSessionId,
358
+ sessionName: sessionSummary,
359
+ error: terminalFailure
360
+ });
361
+ break; // [FIX] Abort iterator to prevent Codex SDK from throwing process termination exceptions
362
+ }
363
+
364
+ // Extract and send token usage if available (normalized to match Claude format)
365
+ if (event.type === 'turn.completed' && event.usage) {
366
+ const inputTokens = event.usage.input_tokens || 0;
367
+ const outputTokens = event.usage.output_tokens || 0;
368
+ const totalTokens = inputTokens + outputTokens;
369
+ sendMessage(ws, createNormalizedMessage({
370
+ kind: 'status',
371
+ text: 'token_budget',
372
+ tokenBudget: {
373
+ used: totalTokens,
374
+ total: 200000,
375
+ inputTokens,
376
+ outputTokens,
377
+ },
378
+ sessionId: currentSessionId,
379
+ provider: 'codex'
380
+ }));
381
+ }
382
+ }
383
+
384
+ // Send completion event
385
+ if (!terminalFailure) {
386
+ sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
387
+ notifyRunStopped({
388
+ userId: ws?.userId || null,
389
+ provider: 'codex',
390
+ sessionId: currentSessionId,
391
+ sessionName: sessionSummary,
392
+ stopReason: 'completed'
393
+ });
394
+ }
395
+
396
+ } catch (error) {
397
+ const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
398
+ const wasAborted =
399
+ session?.status === 'aborted' ||
400
+ error?.name === 'AbortError' ||
401
+ String(error?.message || '').toLowerCase().includes('aborted');
402
+
403
+ if (!wasAborted) {
404
+ console.error('[Codex] Error:', error);
405
+ sendMessage(ws, createNormalizedMessage({ kind: 'error', content: error.message, sessionId: currentSessionId, provider: 'codex' }));
406
+ if (!terminalFailure) {
407
+ notifyRunFailed({
408
+ userId: ws?.userId || null,
409
+ provider: 'codex',
410
+ sessionId: currentSessionId,
411
+ sessionName: sessionSummary,
412
+ error
413
+ });
414
+ }
415
+ }
416
+
417
+ } finally {
418
+ // Update session status
419
+ if (currentSessionId) {
420
+ const session = activeCodexSessions.get(currentSessionId);
421
+ if (session) {
422
+ session.status = session.status === 'aborted' ? 'aborted' : 'completed';
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Abort an active Codex session
430
+ * @param {string} sessionId - Session ID to abort
431
+ * @returns {boolean} - Whether abort was successful
432
+ */
433
+ export function abortCodexSession(sessionId) {
434
+ const session = activeCodexSessions.get(sessionId);
435
+
436
+ if (!session) {
437
+ return false;
438
+ }
439
+
440
+ session.status = 'aborted';
441
+ try {
442
+ session.abortController?.abort();
443
+ } catch (error) {
444
+ console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
445
+ }
446
+
447
+ return true;
448
+ }
449
+
450
+ /**
451
+ * Check if a session is active
452
+ * @param {string} sessionId - Session ID to check
453
+ * @returns {boolean} - Whether session is active
454
+ */
455
+ export function isCodexSessionActive(sessionId) {
456
+ const session = activeCodexSessions.get(sessionId);
457
+ return session?.status === 'running';
458
+ }
459
+
460
+ /**
461
+ * Get all active sessions
462
+ * @returns {Array} - Array of active session info
463
+ */
464
+ export function getActiveCodexSessions() {
465
+ const sessions = [];
466
+
467
+ for (const [id, session] of activeCodexSessions.entries()) {
468
+ if (session.status === 'running') {
469
+ sessions.push({
470
+ id,
471
+ status: session.status,
472
+ startedAt: session.startedAt
473
+ });
474
+ }
475
+ }
476
+
477
+ return sessions;
478
+ }
479
+
480
+ /**
481
+ * Helper to send message via WebSocket or writer
482
+ * @param {WebSocket|object} ws - WebSocket or response writer
483
+ * @param {object} data - Data to send
484
+ */
485
+ function sendMessage(ws, data) {
486
+ try {
487
+ if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
488
+ // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
489
+ ws.send(data);
490
+ } else if (typeof ws.send === 'function') {
491
+ // Raw WebSocket - stringify here
492
+ ws.send(JSON.stringify(data));
493
+ }
494
+ } catch (error) {
495
+ console.error('[Codex] Error sending message:', error);
496
+ }
497
+ }
498
+
499
+ // Clean up old completed sessions periodically
500
+ setInterval(() => {
501
+ const now = Date.now();
502
+ const maxAge = 30 * 60 * 1000; // 30 minutes
503
+
504
+ for (const [id, session] of activeCodexSessions.entries()) {
505
+ if (session.status !== 'running') {
506
+ const startedAt = new Date(session.startedAt).getTime();
507
+ if (now - startedAt > maxAge) {
508
+ activeCodexSessions.delete(id);
509
+ }
510
+ }
511
+ }
512
+ }, 5 * 60 * 1000); // Every 5 minutes