@siteboon/claude-code-ui 1.16.3 → 1.17.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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/assets/index-1FHLTpt_.js +1239 -0
- package/dist/assets/index-BZ3x0u4p.css +32 -0
- package/dist/assets/{vendor-codemirror-CJLzwpLB.js → vendor-codemirror-BXil-2fV.js} +1 -1
- package/dist/assets/{vendor-react-DcyRfQm3.js → vendor-react-DIN4KjD2.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +3 -3
- package/server/constants/config.js +5 -0
- package/server/index.js +217 -126
- package/server/load-env.js +24 -0
- package/server/middleware/auth.js +3 -2
- package/server/openai-codex.js +25 -12
- package/server/projects.js +117 -63
- package/server/routes/agent.js +3 -2
- package/server/routes/commands.js +80 -0
- package/server/routes/git.js +57 -17
- package/shared/modelConstants.js +1 -1
- package/dist/assets/index-BQGOOBNa.css +0 -32
- package/dist/assets/index-D9u4X-u6.js +0 -1239
package/server/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Load environment variables
|
|
2
|
+
// Load environment variables before other imports execute
|
|
3
|
+
import './load-env.js';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
@@ -28,22 +29,6 @@ const c = {
|
|
|
28
29
|
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
try {
|
|
32
|
-
const envPath = path.join(__dirname, '../.env');
|
|
33
|
-
const envFile = fs.readFileSync(envPath, 'utf8');
|
|
34
|
-
envFile.split('\n').forEach(line => {
|
|
35
|
-
const trimmedLine = line.trim();
|
|
36
|
-
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
37
|
-
const [key, ...valueParts] = trimmedLine.split('=');
|
|
38
|
-
if (key && valueParts.length > 0 && !process.env[key]) {
|
|
39
|
-
process.env[key] = valueParts.join('=').trim();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
} catch (e) {
|
|
44
|
-
console.log('No .env file found or error reading it:', e.message);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
32
|
console.log('PORT from env:', process.env.PORT);
|
|
48
33
|
|
|
49
34
|
import express from 'express';
|
|
@@ -76,9 +61,26 @@ import userRoutes from './routes/user.js';
|
|
|
76
61
|
import codexRoutes from './routes/codex.js';
|
|
77
62
|
import { initializeDatabase } from './database/db.js';
|
|
78
63
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
import { IS_PLATFORM } from './constants/config.js';
|
|
65
|
+
|
|
66
|
+
// File system watchers for provider project/session folders
|
|
67
|
+
const PROVIDER_WATCH_PATHS = [
|
|
68
|
+
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
69
|
+
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
|
|
70
|
+
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') }
|
|
71
|
+
];
|
|
72
|
+
const WATCHER_IGNORED_PATTERNS = [
|
|
73
|
+
'**/node_modules/**',
|
|
74
|
+
'**/.git/**',
|
|
75
|
+
'**/dist/**',
|
|
76
|
+
'**/build/**',
|
|
77
|
+
'**/*.tmp',
|
|
78
|
+
'**/*.swp',
|
|
79
|
+
'**/.DS_Store'
|
|
80
|
+
];
|
|
81
|
+
const WATCHER_DEBOUNCE_MS = 300;
|
|
82
|
+
let projectsWatchers = [];
|
|
83
|
+
let projectsWatcherDebounceTimer = null;
|
|
82
84
|
const connectedClients = new Set();
|
|
83
85
|
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
84
86
|
|
|
@@ -95,94 +97,110 @@ function broadcastProgress(progress) {
|
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
// Setup file system
|
|
100
|
+
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
|
|
99
101
|
async function setupProjectsWatcher() {
|
|
100
102
|
const chokidar = (await import('chokidar')).default;
|
|
101
|
-
const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
|
|
102
103
|
|
|
103
|
-
if (
|
|
104
|
-
|
|
104
|
+
if (projectsWatcherDebounceTimer) {
|
|
105
|
+
clearTimeout(projectsWatcherDebounceTimer);
|
|
106
|
+
projectsWatcherDebounceTimer = null;
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
'**/dist/**',
|
|
114
|
-
'**/build/**',
|
|
115
|
-
'**/*.tmp',
|
|
116
|
-
'**/*.swp',
|
|
117
|
-
'**/.DS_Store'
|
|
118
|
-
],
|
|
119
|
-
persistent: true,
|
|
120
|
-
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
121
|
-
followSymlinks: false,
|
|
122
|
-
depth: 10, // Reasonable depth limit
|
|
123
|
-
awaitWriteFinish: {
|
|
124
|
-
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
125
|
-
pollInterval: 50
|
|
109
|
+
await Promise.all(
|
|
110
|
+
projectsWatchers.map(async (watcher) => {
|
|
111
|
+
try {
|
|
112
|
+
await watcher.close();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[WARN] Failed to close watcher:', error);
|
|
126
115
|
}
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let debounceTimer;
|
|
131
|
-
const debouncedUpdate = async (eventType, filePath) => {
|
|
132
|
-
clearTimeout(debounceTimer);
|
|
133
|
-
debounceTimer = setTimeout(async () => {
|
|
134
|
-
// Prevent reentrant calls
|
|
135
|
-
if (isGetProjectsRunning) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
projectsWatchers = [];
|
|
138
119
|
|
|
139
|
-
|
|
140
|
-
|
|
120
|
+
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
|
121
|
+
if (projectsWatcherDebounceTimer) {
|
|
122
|
+
clearTimeout(projectsWatcherDebounceTimer);
|
|
123
|
+
}
|
|
141
124
|
|
|
142
|
-
|
|
143
|
-
|
|
125
|
+
projectsWatcherDebounceTimer = setTimeout(async () => {
|
|
126
|
+
// Prevent reentrant calls
|
|
127
|
+
if (isGetProjectsRunning) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
144
130
|
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
try {
|
|
132
|
+
isGetProjectsRunning = true;
|
|
133
|
+
|
|
134
|
+
// Clear project directory cache when files change
|
|
135
|
+
clearProjectDirectoryCache();
|
|
136
|
+
|
|
137
|
+
// Get updated projects list
|
|
138
|
+
const updatedProjects = await getProjects(broadcastProgress);
|
|
139
|
+
|
|
140
|
+
// Notify all connected clients about the project changes
|
|
141
|
+
const updateMessage = JSON.stringify({
|
|
142
|
+
type: 'projects_updated',
|
|
143
|
+
projects: updatedProjects,
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
changeType: eventType,
|
|
146
|
+
changedFile: path.relative(rootPath, filePath),
|
|
147
|
+
watchProvider: provider
|
|
148
|
+
});
|
|
147
149
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
changeType: eventType,
|
|
154
|
-
changedFile: path.relative(claudeProjectsPath, filePath)
|
|
155
|
-
});
|
|
150
|
+
connectedClients.forEach(client => {
|
|
151
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
152
|
+
client.send(updateMessage);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('[ERROR] Error handling project changes:', error);
|
|
158
|
+
} finally {
|
|
159
|
+
isGetProjectsRunning = false;
|
|
160
|
+
}
|
|
161
|
+
}, WATCHER_DEBOUNCE_MS);
|
|
162
|
+
};
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
|
|
165
|
+
try {
|
|
166
|
+
// chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
|
|
167
|
+
// Ensure provider folders exist before creating the watcher so watching stays active.
|
|
168
|
+
await fsPromises.mkdir(rootPath, { recursive: true });
|
|
169
|
+
|
|
170
|
+
// Initialize chokidar watcher with optimized settings
|
|
171
|
+
const watcher = chokidar.watch(rootPath, {
|
|
172
|
+
ignored: WATCHER_IGNORED_PATTERNS,
|
|
173
|
+
persistent: true,
|
|
174
|
+
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
175
|
+
followSymlinks: false,
|
|
176
|
+
depth: 10, // Reasonable depth limit
|
|
177
|
+
awaitWriteFinish: {
|
|
178
|
+
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
179
|
+
pollInterval: 50
|
|
167
180
|
}
|
|
168
|
-
}, 300); // 300ms debounce (slightly faster than before)
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
// Set up event listeners
|
|
172
|
-
projectsWatcher
|
|
173
|
-
.on('add', (filePath) => debouncedUpdate('add', filePath))
|
|
174
|
-
.on('change', (filePath) => debouncedUpdate('change', filePath))
|
|
175
|
-
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath))
|
|
176
|
-
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath))
|
|
177
|
-
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath))
|
|
178
|
-
.on('error', (error) => {
|
|
179
|
-
console.error('[ERROR] Chokidar watcher error:', error);
|
|
180
|
-
})
|
|
181
|
-
.on('ready', () => {
|
|
182
181
|
});
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// Set up event listeners
|
|
184
|
+
watcher
|
|
185
|
+
.on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
|
|
186
|
+
.on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
|
|
187
|
+
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
|
|
188
|
+
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
|
|
189
|
+
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
|
|
190
|
+
.on('error', (error) => {
|
|
191
|
+
console.error(`[ERROR] ${provider} watcher error:`, error);
|
|
192
|
+
})
|
|
193
|
+
.on('ready', () => {
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
projectsWatchers.push(watcher);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (projectsWatchers.length === 0) {
|
|
203
|
+
console.error('[ERROR] Failed to setup any provider watchers');
|
|
186
204
|
}
|
|
187
205
|
}
|
|
188
206
|
|
|
@@ -192,6 +210,69 @@ const server = http.createServer(app);
|
|
|
192
210
|
|
|
193
211
|
const ptySessionsMap = new Map();
|
|
194
212
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
213
|
+
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
|
214
|
+
const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
|
|
215
|
+
const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
|
|
216
|
+
|
|
217
|
+
function stripAnsiSequences(value = '') {
|
|
218
|
+
return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeDetectedUrl(url) {
|
|
222
|
+
if (!url || typeof url !== 'string') return null;
|
|
223
|
+
|
|
224
|
+
const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
|
|
225
|
+
if (!cleaned) return null;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const parsed = new URL(cleaned);
|
|
229
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return parsed.toString();
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractUrlsFromText(value = '') {
|
|
239
|
+
const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
|
|
240
|
+
|
|
241
|
+
// Handle wrapped terminal URLs split across lines by terminal width.
|
|
242
|
+
const wrappedMatches = [];
|
|
243
|
+
const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
|
|
244
|
+
const lines = value.split(/\r?\n/);
|
|
245
|
+
for (let i = 0; i < lines.length; i++) {
|
|
246
|
+
const line = lines[i].trim();
|
|
247
|
+
const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
|
|
248
|
+
if (!startMatch) continue;
|
|
249
|
+
|
|
250
|
+
let combined = startMatch[0];
|
|
251
|
+
let j = i + 1;
|
|
252
|
+
while (j < lines.length) {
|
|
253
|
+
const continuation = lines[j].trim();
|
|
254
|
+
if (!continuation) break;
|
|
255
|
+
if (!continuationRegex.test(continuation)) break;
|
|
256
|
+
combined += continuation;
|
|
257
|
+
j++;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return Array.from(new Set([...directMatches, ...wrappedMatches]));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function shouldAutoOpenUrlFromOutput(value = '') {
|
|
267
|
+
const normalized = value.toLowerCase();
|
|
268
|
+
return (
|
|
269
|
+
normalized.includes('browser didn\'t open') ||
|
|
270
|
+
normalized.includes('open this url') ||
|
|
271
|
+
normalized.includes('continue in your browser') ||
|
|
272
|
+
normalized.includes('press enter to open') ||
|
|
273
|
+
normalized.includes('open_url:')
|
|
274
|
+
);
|
|
275
|
+
}
|
|
195
276
|
|
|
196
277
|
// Single WebSocket server that handles both paths
|
|
197
278
|
const wss = new WebSocketServer({
|
|
@@ -200,7 +281,7 @@ const wss = new WebSocketServer({
|
|
|
200
281
|
console.log('WebSocket connection attempt to:', info.req.url);
|
|
201
282
|
|
|
202
283
|
// Platform mode: always allow connection
|
|
203
|
-
if (
|
|
284
|
+
if (IS_PLATFORM) {
|
|
204
285
|
const user = authenticateWebSocket(null); // Will return first user
|
|
205
286
|
if (!user) {
|
|
206
287
|
console.log('[WARN] Platform mode: No user found in database');
|
|
@@ -974,7 +1055,8 @@ function handleShellConnection(ws) {
|
|
|
974
1055
|
console.log('🐚 Shell client connected');
|
|
975
1056
|
let shellProcess = null;
|
|
976
1057
|
let ptySessionKey = null;
|
|
977
|
-
let
|
|
1058
|
+
let urlDetectionBuffer = '';
|
|
1059
|
+
const announcedAuthUrls = new Set();
|
|
978
1060
|
|
|
979
1061
|
ws.on('message', async (message) => {
|
|
980
1062
|
try {
|
|
@@ -988,6 +1070,8 @@ function handleShellConnection(ws) {
|
|
|
988
1070
|
const provider = data.provider || 'claude';
|
|
989
1071
|
const initialCommand = data.initialCommand;
|
|
990
1072
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
1073
|
+
urlDetectionBuffer = '';
|
|
1074
|
+
announcedAuthUrls.clear();
|
|
991
1075
|
|
|
992
1076
|
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
|
993
1077
|
const isLoginCommand = initialCommand && (
|
|
@@ -1127,9 +1211,7 @@ function handleShellConnection(ws) {
|
|
|
1127
1211
|
...process.env,
|
|
1128
1212
|
TERM: 'xterm-256color',
|
|
1129
1213
|
COLORTERM: 'truecolor',
|
|
1130
|
-
FORCE_COLOR: '3'
|
|
1131
|
-
// Override browser opening commands to echo URL for detection
|
|
1132
|
-
BROWSER: os.platform() === 'win32' ? 'echo "OPEN_URL:"' : 'echo "OPEN_URL:"'
|
|
1214
|
+
FORCE_COLOR: '3'
|
|
1133
1215
|
}
|
|
1134
1216
|
});
|
|
1135
1217
|
|
|
@@ -1159,38 +1241,47 @@ function handleShellConnection(ws) {
|
|
|
1159
1241
|
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1160
1242
|
let outputData = data;
|
|
1161
1243
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
// BROWSER environment variable override
|
|
1244
|
+
const cleanChunk = stripAnsiSequences(data);
|
|
1245
|
+
urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
|
|
1246
|
+
|
|
1247
|
+
outputData = outputData.replace(
|
|
1167
1248
|
/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
while ((match = pattern.exec(data)) !== null) {
|
|
1179
|
-
const url = match[1];
|
|
1180
|
-
console.log('[DEBUG] Detected URL for opening:', url);
|
|
1181
|
-
|
|
1182
|
-
// Send URL opening message to client
|
|
1249
|
+
'[INFO] Opening in browser: $1'
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
const emitAuthUrl = (detectedUrl, autoOpen = false) => {
|
|
1253
|
+
const normalizedUrl = normalizeDetectedUrl(detectedUrl);
|
|
1254
|
+
if (!normalizedUrl) return;
|
|
1255
|
+
|
|
1256
|
+
const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
|
|
1257
|
+
if (isNewUrl) {
|
|
1258
|
+
announcedAuthUrls.add(normalizedUrl);
|
|
1183
1259
|
session.ws.send(JSON.stringify({
|
|
1184
|
-
type: '
|
|
1185
|
-
url:
|
|
1260
|
+
type: 'auth_url',
|
|
1261
|
+
url: normalizedUrl,
|
|
1262
|
+
autoOpen
|
|
1186
1263
|
}));
|
|
1187
|
-
|
|
1188
|
-
// Replace the OPEN_URL pattern with a user-friendly message
|
|
1189
|
-
if (pattern.source.includes('OPEN_URL')) {
|
|
1190
|
-
outputData = outputData.replace(match[0], `[INFO] Opening in browser: ${url}`);
|
|
1191
|
-
}
|
|
1192
1264
|
}
|
|
1193
|
-
|
|
1265
|
+
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
|
|
1269
|
+
.map((url) => normalizeDetectedUrl(url))
|
|
1270
|
+
.filter(Boolean);
|
|
1271
|
+
|
|
1272
|
+
// Prefer the most complete URL if shorter prefix variants are also present.
|
|
1273
|
+
const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) =>
|
|
1274
|
+
!urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
|
|
1278
|
+
|
|
1279
|
+
if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
|
|
1280
|
+
const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
|
|
1281
|
+
current.length > longest.length ? current : longest
|
|
1282
|
+
);
|
|
1283
|
+
emitAuthUrl(bestUrl, true);
|
|
1284
|
+
}
|
|
1194
1285
|
|
|
1195
1286
|
// Send regular output
|
|
1196
1287
|
session.ws.send(JSON.stringify({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Load environment variables from .env before other imports execute.
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const envPath = path.join(__dirname, '../.env');
|
|
12
|
+
const envFile = fs.readFileSync(envPath, 'utf8');
|
|
13
|
+
envFile.split('\n').forEach(line => {
|
|
14
|
+
const trimmedLine = line.trim();
|
|
15
|
+
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
16
|
+
const [key, ...valueParts] = trimmedLine.split('=');
|
|
17
|
+
if (key && valueParts.length > 0 && !process.env[key]) {
|
|
18
|
+
process.env[key] = valueParts.join('=').trim();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.log('No .env file found or error reading it:', e.message);
|
|
24
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
2
|
import { userDb } from '../database/db.js';
|
|
3
|
+
import { IS_PLATFORM } from '../constants/config.js';
|
|
3
4
|
|
|
4
5
|
// Get JWT secret from environment or use default (for development)
|
|
5
6
|
const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
|
|
@@ -21,7 +22,7 @@ const validateApiKey = (req, res, next) => {
|
|
|
21
22
|
// JWT authentication middleware
|
|
22
23
|
const authenticateToken = async (req, res, next) => {
|
|
23
24
|
// Platform mode: use single database user
|
|
24
|
-
if (
|
|
25
|
+
if (IS_PLATFORM) {
|
|
25
26
|
try {
|
|
26
27
|
const user = userDb.getFirstUser();
|
|
27
28
|
if (!user) {
|
|
@@ -80,7 +81,7 @@ const generateToken = (user) => {
|
|
|
80
81
|
// WebSocket authentication function
|
|
81
82
|
const authenticateWebSocket = (token) => {
|
|
82
83
|
// Platform mode: bypass token validation, return first user
|
|
83
|
-
if (
|
|
84
|
+
if (IS_PLATFORM) {
|
|
84
85
|
try {
|
|
85
86
|
const user = userDb.getFirstUser();
|
|
86
87
|
if (user) {
|
package/server/openai-codex.js
CHANGED
|
@@ -203,6 +203,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
203
203
|
let codex;
|
|
204
204
|
let thread;
|
|
205
205
|
let currentSessionId = sessionId;
|
|
206
|
+
const abortController = new AbortController();
|
|
206
207
|
|
|
207
208
|
try {
|
|
208
209
|
// Initialize Codex SDK
|
|
@@ -232,6 +233,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
232
233
|
thread,
|
|
233
234
|
codex,
|
|
234
235
|
status: 'running',
|
|
236
|
+
abortController,
|
|
235
237
|
startedAt: new Date().toISOString()
|
|
236
238
|
});
|
|
237
239
|
|
|
@@ -243,7 +245,9 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
243
245
|
});
|
|
244
246
|
|
|
245
247
|
// Execute with streaming
|
|
246
|
-
const streamedTurn = await thread.runStreamed(command
|
|
248
|
+
const streamedTurn = await thread.runStreamed(command, {
|
|
249
|
+
signal: abortController.signal
|
|
250
|
+
});
|
|
247
251
|
|
|
248
252
|
for await (const event of streamedTurn.events) {
|
|
249
253
|
// Check if session was aborted
|
|
@@ -286,20 +290,27 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
286
290
|
});
|
|
287
291
|
|
|
288
292
|
} catch (error) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
error
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
|
|
294
|
+
const wasAborted =
|
|
295
|
+
session?.status === 'aborted' ||
|
|
296
|
+
error?.name === 'AbortError' ||
|
|
297
|
+
String(error?.message || '').toLowerCase().includes('aborted');
|
|
298
|
+
|
|
299
|
+
if (!wasAborted) {
|
|
300
|
+
console.error('[Codex] Error:', error);
|
|
301
|
+
sendMessage(ws, {
|
|
302
|
+
type: 'codex-error',
|
|
303
|
+
error: error.message,
|
|
304
|
+
sessionId: currentSessionId
|
|
305
|
+
});
|
|
306
|
+
}
|
|
296
307
|
|
|
297
308
|
} finally {
|
|
298
309
|
// Update session status
|
|
299
310
|
if (currentSessionId) {
|
|
300
311
|
const session = activeCodexSessions.get(currentSessionId);
|
|
301
312
|
if (session) {
|
|
302
|
-
session.status = 'completed';
|
|
313
|
+
session.status = session.status === 'aborted' ? 'aborted' : 'completed';
|
|
303
314
|
}
|
|
304
315
|
}
|
|
305
316
|
}
|
|
@@ -318,9 +329,11 @@ export function abortCodexSession(sessionId) {
|
|
|
318
329
|
}
|
|
319
330
|
|
|
320
331
|
session.status = 'aborted';
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
try {
|
|
333
|
+
session.abortController?.abort();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
|
|
336
|
+
}
|
|
324
337
|
|
|
325
338
|
return true;
|
|
326
339
|
}
|