@siteboon/claude-code-ui 1.25.2 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +239 -0
- package/README.ja.md +115 -230
- package/README.ko.md +116 -231
- package/README.md +2 -1
- package/README.ru.md +75 -54
- package/README.zh-CN.md +121 -238
- package/dist/assets/index-C08k8QbP.css +32 -0
- package/dist/assets/{index-DF_FFT3b.js → index-DnXcHp5q.js} +249 -242
- package/dist/index.html +2 -2
- package/dist/sw.js +59 -3
- package/package.json +3 -2
- package/server/claude-sdk.js +106 -62
- package/server/cli.js +10 -7
- package/server/cursor-cli.js +59 -73
- package/server/database/db.js +142 -1
- package/server/database/init.sql +28 -1
- package/server/gemini-cli.js +46 -48
- package/server/gemini-response-handler.js +12 -73
- package/server/index.js +82 -55
- package/server/middleware/auth.js +2 -2
- package/server/openai-codex.js +43 -28
- package/server/projects.js +1 -1
- package/server/providers/claude/adapter.js +278 -0
- package/server/providers/codex/adapter.js +248 -0
- package/server/providers/cursor/adapter.js +353 -0
- package/server/providers/gemini/adapter.js +186 -0
- package/server/providers/registry.js +44 -0
- package/server/providers/types.js +119 -0
- package/server/providers/utils.js +29 -0
- package/server/routes/agent.js +7 -5
- package/server/routes/cli-auth.js +38 -0
- package/server/routes/codex.js +1 -19
- package/server/routes/gemini.js +0 -30
- package/server/routes/git.js +48 -20
- package/server/routes/messages.js +61 -0
- package/server/routes/plugins.js +5 -1
- package/server/routes/settings.js +99 -1
- package/server/routes/taskmaster.js +2 -2
- package/server/services/notification-orchestrator.js +227 -0
- package/server/services/vapid-keys.js +35 -0
- package/server/utils/plugin-loader.js +53 -4
- package/shared/networkHosts.js +22 -0
- package/dist/assets/index-WNTmA_ug.css +0 -32
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Gemini Response Handler - JSON Stream processing
|
|
2
|
+
import { geminiAdapter } from './providers/gemini/adapter.js';
|
|
3
|
+
|
|
2
4
|
class GeminiResponseHandler {
|
|
3
5
|
constructor(ws, options = {}) {
|
|
4
6
|
this.ws = ws;
|
|
@@ -27,13 +29,12 @@ class GeminiResponseHandler {
|
|
|
27
29
|
this.handleEvent(event);
|
|
28
30
|
} catch (err) {
|
|
29
31
|
// Not a JSON line, probably debug output or CLI warnings
|
|
30
|
-
// console.error('[Gemini Handler] Non-JSON line ignored:', line);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
handleEvent(event) {
|
|
36
|
-
const
|
|
37
|
+
const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
|
|
37
38
|
|
|
38
39
|
if (event.type === 'init') {
|
|
39
40
|
if (this.onInit) {
|
|
@@ -42,88 +43,26 @@ class GeminiResponseHandler {
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// Invoke per-type callbacks for session tracking
|
|
45
47
|
if (event.type === 'message' && event.role === 'assistant') {
|
|
46
48
|
const content = event.content || '';
|
|
47
|
-
|
|
48
|
-
// Notify the parent CLI handler of accumulated text
|
|
49
49
|
if (this.onContentFragment && content) {
|
|
50
50
|
this.onContentFragment(content);
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
type: 'message',
|
|
57
|
-
content: content,
|
|
58
|
-
isPartial: event.delta === true
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
if (socketSessionId) payload.sessionId = socketSessionId;
|
|
62
|
-
this.ws.send(payload);
|
|
63
|
-
}
|
|
64
|
-
else if (event.type === 'tool_use') {
|
|
65
|
-
if (this.onToolUse) {
|
|
66
|
-
this.onToolUse(event);
|
|
67
|
-
}
|
|
68
|
-
let payload = {
|
|
69
|
-
type: 'gemini-tool-use',
|
|
70
|
-
toolName: event.tool_name,
|
|
71
|
-
toolId: event.tool_id,
|
|
72
|
-
parameters: event.parameters || {}
|
|
73
|
-
};
|
|
74
|
-
if (socketSessionId) payload.sessionId = socketSessionId;
|
|
75
|
-
this.ws.send(payload);
|
|
52
|
+
} else if (event.type === 'tool_use' && this.onToolUse) {
|
|
53
|
+
this.onToolUse(event);
|
|
54
|
+
} else if (event.type === 'tool_result' && this.onToolResult) {
|
|
55
|
+
this.onToolResult(event);
|
|
76
56
|
}
|
|
77
|
-
else if (event.type === 'tool_result') {
|
|
78
|
-
if (this.onToolResult) {
|
|
79
|
-
this.onToolResult(event);
|
|
80
|
-
}
|
|
81
|
-
let payload = {
|
|
82
|
-
type: 'gemini-tool-result',
|
|
83
|
-
toolId: event.tool_id,
|
|
84
|
-
status: event.status,
|
|
85
|
-
output: event.output || ''
|
|
86
|
-
};
|
|
87
|
-
if (socketSessionId) payload.sessionId = socketSessionId;
|
|
88
|
-
this.ws.send(payload);
|
|
89
|
-
}
|
|
90
|
-
else if (event.type === 'result') {
|
|
91
|
-
// Send a finalize message string
|
|
92
|
-
let payload = {
|
|
93
|
-
type: 'gemini-response',
|
|
94
|
-
data: {
|
|
95
|
-
type: 'message',
|
|
96
|
-
content: '',
|
|
97
|
-
isPartial: false
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
if (socketSessionId) payload.sessionId = socketSessionId;
|
|
101
|
-
this.ws.send(payload);
|
|
102
57
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
status: 'Complete',
|
|
108
|
-
tokens: event.stats.total_tokens
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
if (socketSessionId) statsPayload.sessionId = socketSessionId;
|
|
112
|
-
this.ws.send(statsPayload);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
else if (event.type === 'error') {
|
|
116
|
-
let payload = {
|
|
117
|
-
type: 'gemini-error',
|
|
118
|
-
error: event.error || event.message || 'Unknown Gemini streaming error'
|
|
119
|
-
};
|
|
120
|
-
if (socketSessionId) payload.sessionId = socketSessionId;
|
|
121
|
-
this.ws.send(payload);
|
|
58
|
+
// Normalize via adapter and send all resulting messages
|
|
59
|
+
const normalized = geminiAdapter.normalizeMessage(event, sid);
|
|
60
|
+
for (const msg of normalized) {
|
|
61
|
+
this.ws.send(msg);
|
|
122
62
|
}
|
|
123
63
|
}
|
|
124
64
|
|
|
125
65
|
forceFlush() {
|
|
126
|
-
// If the buffer has content, try to parse it one last time
|
|
127
66
|
if (this.buffer.trim()) {
|
|
128
67
|
try {
|
|
129
68
|
const event = JSON.parse(this.buffer);
|
package/server/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const c = {
|
|
|
31
31
|
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
console.log('
|
|
34
|
+
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
|
35
35
|
|
|
36
36
|
import express from 'express';
|
|
37
37
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
@@ -44,7 +44,7 @@ import pty from 'node-pty';
|
|
|
44
44
|
import fetch from 'node-fetch';
|
|
45
45
|
import mime from 'mime-types';
|
|
46
46
|
|
|
47
|
-
import { getProjects, getSessions,
|
|
47
|
+
import { getProjects, getSessions, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js';
|
|
48
48
|
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
|
|
49
49
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
50
50
|
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
@@ -65,10 +65,14 @@ import userRoutes from './routes/user.js';
|
|
|
65
65
|
import codexRoutes from './routes/codex.js';
|
|
66
66
|
import geminiRoutes from './routes/gemini.js';
|
|
67
67
|
import pluginsRoutes from './routes/plugins.js';
|
|
68
|
-
import
|
|
68
|
+
import messagesRoutes from './routes/messages.js';
|
|
69
|
+
import { createNormalizedMessage } from './providers/types.js';
|
|
70
|
+
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
69
71
|
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
|
|
72
|
+
import { configureWebPush } from './services/vapid-keys.js';
|
|
70
73
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
71
74
|
import { IS_PLATFORM } from './constants/config.js';
|
|
75
|
+
import { getConnectableHost } from '../shared/networkHosts.js';
|
|
72
76
|
|
|
73
77
|
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
|
|
74
78
|
|
|
@@ -394,6 +398,9 @@ app.use('/api/gemini', authenticateToken, geminiRoutes);
|
|
|
394
398
|
// Plugins API Routes (protected)
|
|
395
399
|
app.use('/api/plugins', authenticateToken, pluginsRoutes);
|
|
396
400
|
|
|
401
|
+
// Unified session messages route (protected)
|
|
402
|
+
app.use('/api/sessions', authenticateToken, messagesRoutes);
|
|
403
|
+
|
|
397
404
|
// Agent API Routes (uses API key authentication)
|
|
398
405
|
app.use('/api/agent', agentRoutes);
|
|
399
406
|
|
|
@@ -507,31 +514,6 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
|
|
|
507
514
|
}
|
|
508
515
|
});
|
|
509
516
|
|
|
510
|
-
// Get messages for a specific session
|
|
511
|
-
app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
|
|
512
|
-
try {
|
|
513
|
-
const { projectName, sessionId } = req.params;
|
|
514
|
-
const { limit, offset } = req.query;
|
|
515
|
-
|
|
516
|
-
// Parse limit and offset if provided
|
|
517
|
-
const parsedLimit = limit ? parseInt(limit, 10) : null;
|
|
518
|
-
const parsedOffset = offset ? parseInt(offset, 10) : 0;
|
|
519
|
-
|
|
520
|
-
const result = await getSessionMessages(projectName, sessionId, parsedLimit, parsedOffset);
|
|
521
|
-
|
|
522
|
-
// Handle both old and new response formats
|
|
523
|
-
if (Array.isArray(result)) {
|
|
524
|
-
// Backward compatibility: no pagination parameters were provided
|
|
525
|
-
res.json({ messages: result });
|
|
526
|
-
} else {
|
|
527
|
-
// New format with pagination info
|
|
528
|
-
res.json(result);
|
|
529
|
-
}
|
|
530
|
-
} catch (error) {
|
|
531
|
-
res.status(500).json({ error: error.message });
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
|
|
535
517
|
// Rename project endpoint
|
|
536
518
|
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
|
|
537
519
|
try {
|
|
@@ -956,7 +938,6 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|
|
956
938
|
}
|
|
957
939
|
|
|
958
940
|
const files = await getFileTree(actualPath, 10, 0, true);
|
|
959
|
-
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
|
|
960
941
|
res.json(files);
|
|
961
942
|
} catch (error) {
|
|
962
943
|
console.error('[ERROR] File tree error:', error.message);
|
|
@@ -1394,6 +1375,50 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1394
1375
|
|
|
1395
1376
|
app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
|
|
1396
1377
|
|
|
1378
|
+
/**
|
|
1379
|
+
* Proxy an authenticated client WebSocket to a plugin's internal WS server.
|
|
1380
|
+
* Auth is enforced by verifyClient before this function is reached.
|
|
1381
|
+
*/
|
|
1382
|
+
function handlePluginWsProxy(clientWs, pathname) {
|
|
1383
|
+
const pluginName = pathname.replace('/plugin-ws/', '');
|
|
1384
|
+
if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) {
|
|
1385
|
+
clientWs.close(4400, 'Invalid plugin name');
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const port = getPluginPort(pluginName);
|
|
1390
|
+
if (!port) {
|
|
1391
|
+
clientWs.close(4404, 'Plugin not running');
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
1396
|
+
|
|
1397
|
+
upstream.on('open', () => {
|
|
1398
|
+
console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
// Relay messages bidirectionally
|
|
1402
|
+
upstream.on('message', (data) => {
|
|
1403
|
+
if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data);
|
|
1404
|
+
});
|
|
1405
|
+
clientWs.on('message', (data) => {
|
|
1406
|
+
if (upstream.readyState === WebSocket.OPEN) upstream.send(data);
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
// Propagate close in both directions
|
|
1410
|
+
upstream.on('close', () => { if (clientWs.readyState === WebSocket.OPEN) clientWs.close(); });
|
|
1411
|
+
clientWs.on('close', () => { if (upstream.readyState === WebSocket.OPEN) upstream.close(); });
|
|
1412
|
+
|
|
1413
|
+
upstream.on('error', (err) => {
|
|
1414
|
+
console.error(`[Plugins] WS proxy error for "${pluginName}":`, err.message);
|
|
1415
|
+
if (clientWs.readyState === WebSocket.OPEN) clientWs.close(4502, 'Upstream error');
|
|
1416
|
+
});
|
|
1417
|
+
clientWs.on('error', () => {
|
|
1418
|
+
if (upstream.readyState === WebSocket.OPEN) upstream.close();
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1397
1422
|
// WebSocket connection handler that routes based on URL path
|
|
1398
1423
|
wss.on('connection', (ws, request) => {
|
|
1399
1424
|
const url = request.url;
|
|
@@ -1406,7 +1431,9 @@ wss.on('connection', (ws, request) => {
|
|
|
1406
1431
|
if (pathname === '/shell') {
|
|
1407
1432
|
handleShellConnection(ws);
|
|
1408
1433
|
} else if (pathname === '/ws') {
|
|
1409
|
-
handleChatConnection(ws);
|
|
1434
|
+
handleChatConnection(ws, request);
|
|
1435
|
+
} else if (pathname.startsWith('/plugin-ws/')) {
|
|
1436
|
+
handlePluginWsProxy(ws, pathname);
|
|
1410
1437
|
} else {
|
|
1411
1438
|
console.log('[WARN] Unknown WebSocket path:', pathname);
|
|
1412
1439
|
ws.close();
|
|
@@ -1415,17 +1442,21 @@ wss.on('connection', (ws, request) => {
|
|
|
1415
1442
|
|
|
1416
1443
|
/**
|
|
1417
1444
|
* WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
|
|
1445
|
+
*
|
|
1446
|
+
* Provider files use `createNormalizedMessage()` from `providers/types.js` and
|
|
1447
|
+
* adapter `normalizeMessage()` to produce unified NormalizedMessage events.
|
|
1448
|
+
* The writer simply serialises and sends.
|
|
1418
1449
|
*/
|
|
1419
1450
|
class WebSocketWriter {
|
|
1420
|
-
constructor(ws) {
|
|
1451
|
+
constructor(ws, userId = null) {
|
|
1421
1452
|
this.ws = ws;
|
|
1422
1453
|
this.sessionId = null;
|
|
1454
|
+
this.userId = userId;
|
|
1423
1455
|
this.isWebSocketWriter = true; // Marker for transport detection
|
|
1424
1456
|
}
|
|
1425
1457
|
|
|
1426
1458
|
send(data) {
|
|
1427
1459
|
if (this.ws.readyState === 1) { // WebSocket.OPEN
|
|
1428
|
-
// Providers send raw objects, we stringify for WebSocket
|
|
1429
1460
|
this.ws.send(JSON.stringify(data));
|
|
1430
1461
|
}
|
|
1431
1462
|
}
|
|
@@ -1444,14 +1475,14 @@ class WebSocketWriter {
|
|
|
1444
1475
|
}
|
|
1445
1476
|
|
|
1446
1477
|
// Handle chat WebSocket connections
|
|
1447
|
-
function handleChatConnection(ws) {
|
|
1478
|
+
function handleChatConnection(ws, request) {
|
|
1448
1479
|
console.log('[INFO] Chat WebSocket connected');
|
|
1449
1480
|
|
|
1450
1481
|
// Add to connected clients for project updates
|
|
1451
1482
|
connectedClients.add(ws);
|
|
1452
1483
|
|
|
1453
1484
|
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
|
|
1454
|
-
const writer = new WebSocketWriter(ws);
|
|
1485
|
+
const writer = new WebSocketWriter(ws, request?.user?.id ?? request?.user?.userId ?? null);
|
|
1455
1486
|
|
|
1456
1487
|
ws.on('message', async (message) => {
|
|
1457
1488
|
try {
|
|
@@ -1506,12 +1537,7 @@ function handleChatConnection(ws) {
|
|
|
1506
1537
|
success = await abortClaudeSDKSession(data.sessionId);
|
|
1507
1538
|
}
|
|
1508
1539
|
|
|
1509
|
-
writer.send({
|
|
1510
|
-
type: 'session-aborted',
|
|
1511
|
-
sessionId: data.sessionId,
|
|
1512
|
-
provider,
|
|
1513
|
-
success
|
|
1514
|
-
});
|
|
1540
|
+
writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider }));
|
|
1515
1541
|
} else if (data.type === 'claude-permission-response') {
|
|
1516
1542
|
// Relay UI approval decisions back into the SDK control flow.
|
|
1517
1543
|
// This does not persist permissions; it only resolves the in-flight request,
|
|
@@ -1527,12 +1553,7 @@ function handleChatConnection(ws) {
|
|
|
1527
1553
|
} else if (data.type === 'cursor-abort') {
|
|
1528
1554
|
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
|
|
1529
1555
|
const success = abortCursorSession(data.sessionId);
|
|
1530
|
-
writer.send({
|
|
1531
|
-
type: 'session-aborted',
|
|
1532
|
-
sessionId: data.sessionId,
|
|
1533
|
-
provider: 'cursor',
|
|
1534
|
-
success
|
|
1535
|
-
});
|
|
1556
|
+
writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider: 'cursor' }));
|
|
1536
1557
|
} else if (data.type === 'check-session-status') {
|
|
1537
1558
|
// Check if a specific session is currently processing
|
|
1538
1559
|
const provider = data.provider || 'claude';
|
|
@@ -2401,7 +2422,8 @@ app.get('*', (req, res) => {
|
|
|
2401
2422
|
res.sendFile(indexPath);
|
|
2402
2423
|
} else {
|
|
2403
2424
|
// In development, redirect to Vite dev server only if dist doesn't exist
|
|
2404
|
-
|
|
2425
|
+
const redirectHost = getConnectableHost(req.hostname);
|
|
2426
|
+
res.redirect(`${req.protocol}://${redirectHost}:${VITE_PORT}`);
|
|
2405
2427
|
}
|
|
2406
2428
|
});
|
|
2407
2429
|
|
|
@@ -2489,10 +2511,10 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
|
|
|
2489
2511
|
});
|
|
2490
2512
|
}
|
|
2491
2513
|
|
|
2492
|
-
const
|
|
2514
|
+
const SERVER_PORT = process.env.SERVER_PORT || 3001;
|
|
2493
2515
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
2494
|
-
|
|
2495
|
-
const
|
|
2516
|
+
const DISPLAY_HOST = getConnectableHost(HOST);
|
|
2517
|
+
const VITE_PORT = process.env.VITE_PORT || 5173;
|
|
2496
2518
|
|
|
2497
2519
|
// Initialize database and start server
|
|
2498
2520
|
async function startServer() {
|
|
@@ -2500,19 +2522,24 @@ async function startServer() {
|
|
|
2500
2522
|
// Initialize authentication database
|
|
2501
2523
|
await initializeDatabase();
|
|
2502
2524
|
|
|
2525
|
+
// Configure Web Push (VAPID keys)
|
|
2526
|
+
configureWebPush();
|
|
2527
|
+
|
|
2503
2528
|
// Check if running in production mode (dist folder exists)
|
|
2504
2529
|
const distIndexPath = path.join(__dirname, '../dist/index.html');
|
|
2505
2530
|
const isProduction = fs.existsSync(distIndexPath);
|
|
2506
2531
|
|
|
2507
2532
|
// Log Claude implementation mode
|
|
2508
2533
|
console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
|
|
2509
|
-
console.log(
|
|
2534
|
+
console.log('');
|
|
2510
2535
|
|
|
2511
|
-
if (
|
|
2512
|
-
console.log(`${c.
|
|
2536
|
+
if (isProduction) {
|
|
2537
|
+
console.log(`${c.info('[INFO]')} To run in production mode, go to http://${DISPLAY_HOST}:${SERVER_PORT}`);
|
|
2513
2538
|
}
|
|
2514
2539
|
|
|
2515
|
-
|
|
2540
|
+
console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`);
|
|
2541
|
+
|
|
2542
|
+
server.listen(SERVER_PORT, HOST, async () => {
|
|
2516
2543
|
const appInstallPath = path.join(__dirname, '..');
|
|
2517
2544
|
|
|
2518
2545
|
console.log('');
|
|
@@ -2520,7 +2547,7 @@ async function startServer() {
|
|
|
2520
2547
|
console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
|
|
2521
2548
|
console.log(c.dim('═'.repeat(63)));
|
|
2522
2549
|
console.log('');
|
|
2523
|
-
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' +
|
|
2550
|
+
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`);
|
|
2524
2551
|
console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
|
|
2525
2552
|
console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
|
|
2526
2553
|
console.log('');
|
|
@@ -95,7 +95,7 @@ const authenticateWebSocket = (token) => {
|
|
|
95
95
|
try {
|
|
96
96
|
const user = userDb.getFirstUser();
|
|
97
97
|
if (user) {
|
|
98
|
-
return { userId: user.id, username: user.username };
|
|
98
|
+
return { id: user.id, userId: user.id, username: user.username };
|
|
99
99
|
}
|
|
100
100
|
return null;
|
|
101
101
|
} catch (error) {
|
|
@@ -129,4 +129,4 @@ export {
|
|
|
129
129
|
generateToken,
|
|
130
130
|
authenticateWebSocket,
|
|
131
131
|
JWT_SECRET
|
|
132
|
-
};
|
|
132
|
+
};
|
package/server/openai-codex.js
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { Codex } from '@openai/codex-sdk';
|
|
17
|
+
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
18
|
+
import { codexAdapter } from './providers/codex/adapter.js';
|
|
19
|
+
import { createNormalizedMessage } from './providers/types.js';
|
|
17
20
|
|
|
18
21
|
// Track active sessions
|
|
19
22
|
const activeCodexSessions = new Map();
|
|
@@ -191,6 +194,7 @@ function mapPermissionModeToCodexOptions(permissionMode) {
|
|
|
191
194
|
export async function queryCodex(command, options = {}, ws) {
|
|
192
195
|
const {
|
|
193
196
|
sessionId,
|
|
197
|
+
sessionSummary,
|
|
194
198
|
cwd,
|
|
195
199
|
projectPath,
|
|
196
200
|
model,
|
|
@@ -203,6 +207,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
203
207
|
let codex;
|
|
204
208
|
let thread;
|
|
205
209
|
let currentSessionId = sessionId;
|
|
210
|
+
let terminalFailure = null;
|
|
206
211
|
const abortController = new AbortController();
|
|
207
212
|
|
|
208
213
|
try {
|
|
@@ -238,11 +243,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
238
243
|
});
|
|
239
244
|
|
|
240
245
|
// Send session created event
|
|
241
|
-
sendMessage(ws, {
|
|
242
|
-
type: 'session-created',
|
|
243
|
-
sessionId: currentSessionId,
|
|
244
|
-
provider: 'codex'
|
|
245
|
-
});
|
|
246
|
+
sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
|
|
246
247
|
|
|
247
248
|
// Execute with streaming
|
|
248
249
|
const streamedTurn = await thread.runStreamed(command, {
|
|
@@ -262,32 +263,41 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
262
263
|
|
|
263
264
|
const transformed = transformCodexEvent(event);
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
266
|
+
// Normalize the transformed event into NormalizedMessage(s) via adapter
|
|
267
|
+
const normalizedMsgs = codexAdapter.normalizeMessage(transformed, currentSessionId);
|
|
268
|
+
for (const msg of normalizedMsgs) {
|
|
269
|
+
sendMessage(ws, msg);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (event.type === 'turn.failed' && !terminalFailure) {
|
|
273
|
+
terminalFailure = event.error || new Error('Turn failed');
|
|
274
|
+
notifyRunFailed({
|
|
275
|
+
userId: ws?.userId || null,
|
|
276
|
+
provider: 'codex',
|
|
277
|
+
sessionId: currentSessionId,
|
|
278
|
+
sessionName: sessionSummary,
|
|
279
|
+
error: terminalFailure
|
|
280
|
+
});
|
|
281
|
+
}
|
|
270
282
|
|
|
271
283
|
// Extract and send token usage if available (normalized to match Claude format)
|
|
272
284
|
if (event.type === 'turn.completed' && event.usage) {
|
|
273
285
|
const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
|
|
274
|
-
sendMessage(ws, {
|
|
275
|
-
type: 'token-budget',
|
|
276
|
-
data: {
|
|
277
|
-
used: totalTokens,
|
|
278
|
-
total: 200000 // Default context window for Codex models
|
|
279
|
-
},
|
|
280
|
-
sessionId: currentSessionId
|
|
281
|
-
});
|
|
286
|
+
sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
|
|
282
287
|
}
|
|
283
288
|
}
|
|
284
289
|
|
|
285
290
|
// Send completion event
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
+
if (!terminalFailure) {
|
|
292
|
+
sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
|
|
293
|
+
notifyRunStopped({
|
|
294
|
+
userId: ws?.userId || null,
|
|
295
|
+
provider: 'codex',
|
|
296
|
+
sessionId: currentSessionId,
|
|
297
|
+
sessionName: sessionSummary,
|
|
298
|
+
stopReason: 'completed'
|
|
299
|
+
});
|
|
300
|
+
}
|
|
291
301
|
|
|
292
302
|
} catch (error) {
|
|
293
303
|
const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
|
|
@@ -298,11 +308,16 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
298
308
|
|
|
299
309
|
if (!wasAborted) {
|
|
300
310
|
console.error('[Codex] Error:', error);
|
|
301
|
-
sendMessage(ws, {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
sendMessage(ws, createNormalizedMessage({ kind: 'error', content: error.message, sessionId: currentSessionId, provider: 'codex' }));
|
|
312
|
+
if (!terminalFailure) {
|
|
313
|
+
notifyRunFailed({
|
|
314
|
+
userId: ws?.userId || null,
|
|
315
|
+
provider: 'codex',
|
|
316
|
+
sessionId: currentSessionId,
|
|
317
|
+
sessionName: sessionSummary,
|
|
318
|
+
error
|
|
319
|
+
});
|
|
320
|
+
}
|
|
306
321
|
}
|
|
307
322
|
|
|
308
323
|
} finally {
|
package/server/projects.js
CHANGED
|
@@ -1014,7 +1014,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
|
|
|
1014
1014
|
messages.push(entry);
|
|
1015
1015
|
}
|
|
1016
1016
|
} catch (parseError) {
|
|
1017
|
-
|
|
1017
|
+
// Silently skip malformed JSONL lines (common with concurrent writes)
|
|
1018
1018
|
}
|
|
1019
1019
|
}
|
|
1020
1020
|
}
|