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.
- package/README.md +10 -4
- package/agent-runtime-server/package-lock.json +4381 -0
- package/agent-runtime-server/package.json +36 -0
- package/agent-runtime-server/scripts/fix-node-pty.js +67 -0
- package/agent-runtime-server/server/agent-session-service.js +816 -0
- package/agent-runtime-server/server/claude-sdk.js +836 -0
- package/agent-runtime-server/server/cli.js +330 -0
- package/agent-runtime-server/server/constants/config.js +5 -0
- package/agent-runtime-server/server/cursor-cli.js +335 -0
- package/agent-runtime-server/server/database/db.js +653 -0
- package/agent-runtime-server/server/database/init.sql +99 -0
- package/agent-runtime-server/server/gemini-cli.js +460 -0
- package/agent-runtime-server/server/gemini-response-handler.js +79 -0
- package/agent-runtime-server/server/index.js +2569 -0
- package/agent-runtime-server/server/load-env.js +32 -0
- package/agent-runtime-server/server/middleware/auth.js +132 -0
- package/agent-runtime-server/server/openai-codex.js +512 -0
- package/agent-runtime-server/server/projects.js +2594 -0
- package/agent-runtime-server/server/providers/claude/adapter.js +278 -0
- package/agent-runtime-server/server/providers/codex/adapter.js +248 -0
- package/agent-runtime-server/server/providers/cursor/adapter.js +353 -0
- package/agent-runtime-server/server/providers/gemini/adapter.js +186 -0
- package/agent-runtime-server/server/providers/registry.js +44 -0
- package/agent-runtime-server/server/providers/types.js +119 -0
- package/agent-runtime-server/server/providers/utils.js +29 -0
- package/agent-runtime-server/server/routes/agent-sessions.js +238 -0
- package/agent-runtime-server/server/routes/agent.js +1244 -0
- package/agent-runtime-server/server/routes/auth.js +144 -0
- package/agent-runtime-server/server/routes/cli-auth.js +478 -0
- package/agent-runtime-server/server/routes/codex.js +329 -0
- package/agent-runtime-server/server/routes/commands.js +596 -0
- package/agent-runtime-server/server/routes/cursor.js +798 -0
- package/agent-runtime-server/server/routes/gemini.js +24 -0
- package/agent-runtime-server/server/routes/git.js +1508 -0
- package/agent-runtime-server/server/routes/mcp-utils.js +48 -0
- package/agent-runtime-server/server/routes/mcp.js +552 -0
- package/agent-runtime-server/server/routes/messages.js +61 -0
- package/agent-runtime-server/server/routes/plugins.js +307 -0
- package/agent-runtime-server/server/routes/projects.js +548 -0
- package/agent-runtime-server/server/routes/settings.js +276 -0
- package/agent-runtime-server/server/routes/taskmaster.js +1963 -0
- package/agent-runtime-server/server/routes/user.js +123 -0
- package/agent-runtime-server/server/services/notification-orchestrator.js +227 -0
- package/agent-runtime-server/server/services/vapid-keys.js +35 -0
- package/agent-runtime-server/server/sessionManager.js +226 -0
- package/agent-runtime-server/server/utils/commandParser.js +303 -0
- package/agent-runtime-server/server/utils/frontmatter.js +18 -0
- package/agent-runtime-server/server/utils/gitConfig.js +34 -0
- package/agent-runtime-server/server/utils/mcp-detector.js +198 -0
- package/agent-runtime-server/server/utils/plugin-loader.js +457 -0
- package/agent-runtime-server/server/utils/plugin-process-manager.js +184 -0
- package/agent-runtime-server/server/utils/taskmaster-websocket.js +129 -0
- package/agent-runtime-server/shared/modelConstants.js +12 -0
- package/agent-runtime-server/shared/modelConstants.test.js +34 -0
- package/agent-runtime-server/shared/networkHosts.js +22 -0
- package/agent-runtime-server/test_sdk.mjs +16 -0
- package/bin/bridges/darwin-x64/nocode-bridge +0 -0
- package/bin/{nocode-local.js → notioncode.js} +0 -0
- package/dist/assets/icon-CQtd7WEB.png +0 -0
- package/dist/assets/index-Ctr1ES45.js +1 -0
- package/dist/assets/index-DhCWie1Z.css +1 -0
- package/dist/assets/index-DzqxG7Z8.js +689 -0
- package/dist/index.html +46 -0
- package/dist/onboarding/step1_create.png +0 -0
- package/dist/onboarding/step2_capabilities.png +0 -0
- package/dist/onboarding/step2b_content_access.png +0 -0
- package/dist/onboarding/step2c_page_access.png +0 -0
- package/dist/onboarding/step3_token.png +0 -0
- package/dist/onboarding/step4_webhook.png +0 -0
- package/dist/onboarding/step6a_verify.png +0 -0
- package/dist/onboarding/step6b_copy_verify_token.png +0 -0
- package/dist/tinyfish-fish-only.png +0 -0
- package/lib/install.js +33 -2
- package/lib/start.js +157 -25
- package/package.json +7 -4
- package/src/shared/modelRegistry.d.ts +24 -0
- 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
|