@siteboon/claude-code-ui 1.11.0 → 1.13.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.
Files changed (94) hide show
  1. package/README.md +19 -16
  2. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/assets/index-Cc6pl7ji.css +32 -0
  62. package/dist/assets/index-Zq2roSUR.js +1206 -0
  63. package/dist/assets/{vendor-codemirror-B7BYDWj-.js → vendor-codemirror-CnTQH7Pk.js} +1 -1
  64. package/dist/assets/{vendor-react-7V_UDHjJ.js → vendor-react-DVSKlM5e.js} +9 -9
  65. package/dist/assets/{vendor-xterm-jI4BCHEb.js → vendor-xterm-DfaPXD3y.js} +12 -12
  66. package/dist/icons/codex-white.svg +3 -0
  67. package/dist/icons/codex.svg +3 -0
  68. package/dist/icons/cursor-white.svg +12 -0
  69. package/dist/index.html +6 -6
  70. package/dist/logo-128.png +0 -0
  71. package/dist/logo-256.png +0 -0
  72. package/dist/logo-32.png +0 -0
  73. package/dist/logo-512.png +0 -0
  74. package/dist/logo-64.png +0 -0
  75. package/dist/logo.svg +17 -9
  76. package/package.json +7 -1
  77. package/server/claude-sdk.js +20 -19
  78. package/server/database/auth.db +0 -0
  79. package/server/database/db.js +73 -0
  80. package/server/database/init.sql +4 -1
  81. package/server/index.js +263 -29
  82. package/server/middleware/auth.js +34 -3
  83. package/server/openai-codex.js +387 -0
  84. package/server/projects.js +448 -7
  85. package/server/routes/agent.js +42 -4
  86. package/server/routes/cli-auth.js +263 -0
  87. package/server/routes/codex.js +310 -0
  88. package/server/routes/git.js +123 -28
  89. package/server/routes/projects.js +378 -0
  90. package/server/routes/taskmaster.js +2 -10
  91. package/server/routes/user.js +106 -0
  92. package/server/utils/gitConfig.js +24 -0
  93. package/dist/assets/index-B4_v-YUz.css +0 -32
  94. package/dist/assets/index-BZX1vtg9.js +0 -932
package/server/index.js CHANGED
@@ -60,6 +60,7 @@ import mime from 'mime-types';
60
60
  import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
61
61
  import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js';
62
62
  import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
63
+ import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
63
64
  import gitRoutes from './routes/git.js';
64
65
  import authRoutes from './routes/auth.js';
65
66
  import mcpRoutes from './routes/mcp.js';
@@ -69,6 +70,10 @@ import mcpUtilsRoutes from './routes/mcp-utils.js';
69
70
  import commandsRoutes from './routes/commands.js';
70
71
  import settingsRoutes from './routes/settings.js';
71
72
  import agentRoutes from './routes/agent.js';
73
+ import projectsRoutes from './routes/projects.js';
74
+ import cliAuthRoutes from './routes/cli-auth.js';
75
+ import userRoutes from './routes/user.js';
76
+ import codexRoutes from './routes/codex.js';
72
77
  import { initializeDatabase } from './database/db.js';
73
78
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
74
79
 
@@ -163,12 +168,28 @@ async function setupProjectsWatcher() {
163
168
  const app = express();
164
169
  const server = http.createServer(app);
165
170
 
171
+ const ptySessionsMap = new Map();
172
+ const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
173
+
166
174
  // Single WebSocket server that handles both paths
167
175
  const wss = new WebSocketServer({
168
176
  server,
169
177
  verifyClient: (info) => {
170
178
  console.log('WebSocket connection attempt to:', info.req.url);
171
179
 
180
+ // Platform mode: always allow connection
181
+ if (process.env.VITE_IS_PLATFORM === 'true') {
182
+ const user = authenticateWebSocket(null); // Will return first user
183
+ if (!user) {
184
+ console.log('[WARN] Platform mode: No user found in database');
185
+ return false;
186
+ }
187
+ info.req.user = user;
188
+ console.log('[OK] Platform mode WebSocket authenticated for user:', user.username);
189
+ return true;
190
+ }
191
+
192
+ // Normal mode: verify token
172
193
  // Extract token from query parameters or headers
173
194
  const url = new URL(info.req.url, 'http://localhost');
174
195
  const token = url.searchParams.get('token') ||
@@ -192,15 +213,36 @@ const wss = new WebSocketServer({
192
213
  app.locals.wss = wss;
193
214
 
194
215
  app.use(cors());
195
- app.use(express.json({ limit: '50mb' }));
216
+ app.use(express.json({
217
+ limit: '50mb',
218
+ type: (req) => {
219
+ // Skip multipart/form-data requests (for file uploads like images)
220
+ const contentType = req.headers['content-type'] || '';
221
+ if (contentType.includes('multipart/form-data')) {
222
+ return false;
223
+ }
224
+ return contentType.includes('json');
225
+ }
226
+ }));
196
227
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
197
228
 
229
+ // Public health check endpoint (no authentication required)
230
+ app.get('/health', (req, res) => {
231
+ res.json({
232
+ status: 'ok',
233
+ timestamp: new Date().toISOString()
234
+ });
235
+ });
236
+
198
237
  // Optional API key validation (if configured)
199
238
  app.use('/api', validateApiKey);
200
239
 
201
240
  // Authentication routes (public)
202
241
  app.use('/api/auth', authRoutes);
203
242
 
243
+ // Projects API Routes (protected)
244
+ app.use('/api/projects', authenticateToken, projectsRoutes);
245
+
204
246
  // Git API Routes (protected)
205
247
  app.use('/api/git', authenticateToken, gitRoutes);
206
248
 
@@ -222,6 +264,15 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
222
264
  // Settings API Routes (protected)
223
265
  app.use('/api/settings', authenticateToken, settingsRoutes);
224
266
 
267
+ // CLI Authentication API Routes (protected)
268
+ app.use('/api/cli', authenticateToken, cliAuthRoutes);
269
+
270
+ // User API Routes (protected)
271
+ app.use('/api/user', authenticateToken, userRoutes);
272
+
273
+ // Codex API Routes (protected)
274
+ app.use('/api/codex', authenticateToken, codexRoutes);
275
+
225
276
  // Agent API Routes (uses API key authentication)
226
277
  app.use('/api/agent', agentRoutes);
227
278
 
@@ -245,17 +296,8 @@ app.use(express.static(path.join(__dirname, '../dist'), {
245
296
  }));
246
297
 
247
298
  // API Routes (protected)
248
- app.get('/api/config', authenticateToken, (req, res) => {
249
- const host = req.headers.host || `${req.hostname}:${PORT}`;
250
- const protocol = req.protocol === 'https' || req.get('x-forwarded-proto') === 'https' ? 'wss' : 'ws';
251
-
252
- console.log('Config API called - Returning host:', host, 'Protocol:', protocol);
253
-
254
- res.json({
255
- serverPort: PORT,
256
- wsUrl: `${protocol}://${host}`
257
- });
258
- });
299
+ // /api/config endpoint removed - no longer needed
300
+ // Frontend now uses window.location for WebSocket URLs
259
301
 
260
302
  // System update endpoint
261
303
  app.post('/api/system/update', authenticateToken, async (req, res) => {
@@ -381,9 +423,12 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
381
423
  app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
382
424
  try {
383
425
  const { projectName, sessionId } = req.params;
426
+ console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
384
427
  await deleteSession(projectName, sessionId);
428
+ console.log(`[API] Session ${sessionId} deleted successfully`);
385
429
  res.json({ success: true });
386
430
  } catch (error) {
431
+ console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
387
432
  res.status(500).json({ error: error.message });
388
433
  }
389
434
  });
@@ -696,6 +741,12 @@ function handleChatConnection(ws) {
696
741
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
697
742
  console.log('🤖 Model:', data.options?.model || 'default');
698
743
  await spawnCursor(data.command, data.options, ws);
744
+ } else if (data.type === 'codex-command') {
745
+ console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
746
+ console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
747
+ console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
748
+ console.log('🤖 Model:', data.options?.model || 'default');
749
+ await queryCodex(data.command, data.options, ws);
699
750
  } else if (data.type === 'cursor-resume') {
700
751
  // Backward compatibility: treat as cursor-command with resume and no prompt
701
752
  console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
@@ -711,6 +762,8 @@ function handleChatConnection(ws) {
711
762
 
712
763
  if (provider === 'cursor') {
713
764
  success = abortCursorSession(data.sessionId);
765
+ } else if (provider === 'codex') {
766
+ success = abortCodexSession(data.sessionId);
714
767
  } else {
715
768
  // Use Claude Agents SDK
716
769
  success = await abortClaudeSDKSession(data.sessionId);
@@ -739,6 +792,8 @@ function handleChatConnection(ws) {
739
792
 
740
793
  if (provider === 'cursor') {
741
794
  isActive = isCursorSessionActive(sessionId);
795
+ } else if (provider === 'codex') {
796
+ isActive = isCodexSessionActive(sessionId);
742
797
  } else {
743
798
  // Use Claude Agents SDK
744
799
  isActive = isClaudeSDKSessionActive(sessionId);
@@ -754,7 +809,8 @@ function handleChatConnection(ws) {
754
809
  // Get all currently active sessions
755
810
  const activeSessions = {
756
811
  claude: getActiveClaudeSDKSessions(),
757
- cursor: getActiveCursorSessions()
812
+ cursor: getActiveCursorSessions(),
813
+ codex: getActiveCodexSessions()
758
814
  };
759
815
  ws.send(JSON.stringify({
760
816
  type: 'active-sessions',
@@ -781,6 +837,8 @@ function handleChatConnection(ws) {
781
837
  function handleShellConnection(ws) {
782
838
  console.log('🐚 Shell client connected');
783
839
  let shellProcess = null;
840
+ let ptySessionKey = null;
841
+ let outputBuffer = [];
784
842
 
785
843
  ws.on('message', async (message) => {
786
844
  try {
@@ -788,7 +846,6 @@ function handleShellConnection(ws) {
788
846
  console.log('📨 Shell message received:', data.type);
789
847
 
790
848
  if (data.type === 'init') {
791
- // Initialize shell with project path and session info
792
849
  const projectPath = data.projectPath || process.cwd();
793
850
  const sessionId = data.sessionId;
794
851
  const hasSession = data.hasSession;
@@ -796,6 +853,57 @@ function handleShellConnection(ws) {
796
853
  const initialCommand = data.initialCommand;
797
854
  const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
798
855
 
856
+ // Login commands (Claude/Cursor auth) should never reuse cached sessions
857
+ const isLoginCommand = initialCommand && (
858
+ initialCommand.includes('setup-token') ||
859
+ initialCommand.includes('cursor-agent login') ||
860
+ initialCommand.includes('auth login')
861
+ );
862
+
863
+ // Include command hash in session key so different commands get separate sessions
864
+ const commandSuffix = isPlainShell && initialCommand
865
+ ? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
866
+ : '';
867
+ ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
868
+
869
+ // Kill any existing login session before starting fresh
870
+ if (isLoginCommand) {
871
+ const oldSession = ptySessionsMap.get(ptySessionKey);
872
+ if (oldSession) {
873
+ console.log('🧹 Cleaning up existing login session:', ptySessionKey);
874
+ if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
875
+ if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
876
+ ptySessionsMap.delete(ptySessionKey);
877
+ }
878
+ }
879
+
880
+ const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
881
+ if (existingSession) {
882
+ console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
883
+ shellProcess = existingSession.pty;
884
+
885
+ clearTimeout(existingSession.timeoutId);
886
+
887
+ ws.send(JSON.stringify({
888
+ type: 'output',
889
+ data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
890
+ }));
891
+
892
+ if (existingSession.buffer && existingSession.buffer.length > 0) {
893
+ console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
894
+ existingSession.buffer.forEach(bufferedData => {
895
+ ws.send(JSON.stringify({
896
+ type: 'output',
897
+ data: bufferedData
898
+ }));
899
+ });
900
+ }
901
+
902
+ existingSession.ws = ws;
903
+
904
+ return;
905
+ }
906
+
799
907
  console.log('[INFO] Starting shell in:', projectPath);
800
908
  console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
801
909
  console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
@@ -869,10 +977,15 @@ function handleShellConnection(ws) {
869
977
  const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
870
978
  const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
871
979
 
980
+ // Use terminal dimensions from client if provided, otherwise use defaults
981
+ const termCols = data.cols || 80;
982
+ const termRows = data.rows || 24;
983
+ console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
984
+
872
985
  shellProcess = pty.spawn(shell, shellArgs, {
873
986
  name: 'xterm-256color',
874
- cols: 80,
875
- rows: 24,
987
+ cols: termCols,
988
+ rows: termRows,
876
989
  cwd: process.env.HOME || (os.platform() === 'win32' ? process.env.USERPROFILE : '/'),
877
990
  env: {
878
991
  ...process.env,
@@ -886,9 +999,28 @@ function handleShellConnection(ws) {
886
999
 
887
1000
  console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
888
1001
 
1002
+ ptySessionsMap.set(ptySessionKey, {
1003
+ pty: shellProcess,
1004
+ ws: ws,
1005
+ buffer: [],
1006
+ timeoutId: null,
1007
+ projectPath,
1008
+ sessionId
1009
+ });
1010
+
889
1011
  // Handle data output
890
1012
  shellProcess.onData((data) => {
891
- if (ws.readyState === WebSocket.OPEN) {
1013
+ const session = ptySessionsMap.get(ptySessionKey);
1014
+ if (!session) return;
1015
+
1016
+ if (session.buffer.length < 5000) {
1017
+ session.buffer.push(data);
1018
+ } else {
1019
+ session.buffer.shift();
1020
+ session.buffer.push(data);
1021
+ }
1022
+
1023
+ if (session.ws && session.ws.readyState === WebSocket.OPEN) {
892
1024
  let outputData = data;
893
1025
 
894
1026
  // Check for various URL opening patterns
@@ -912,7 +1044,7 @@ function handleShellConnection(ws) {
912
1044
  console.log('[DEBUG] Detected URL for opening:', url);
913
1045
 
914
1046
  // Send URL opening message to client
915
- ws.send(JSON.stringify({
1047
+ session.ws.send(JSON.stringify({
916
1048
  type: 'url_open',
917
1049
  url: url
918
1050
  }));
@@ -925,7 +1057,7 @@ function handleShellConnection(ws) {
925
1057
  });
926
1058
 
927
1059
  // Send regular output
928
- ws.send(JSON.stringify({
1060
+ session.ws.send(JSON.stringify({
929
1061
  type: 'output',
930
1062
  data: outputData
931
1063
  }));
@@ -935,12 +1067,17 @@ function handleShellConnection(ws) {
935
1067
  // Handle process exit
936
1068
  shellProcess.onExit((exitCode) => {
937
1069
  console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
938
- if (ws.readyState === WebSocket.OPEN) {
939
- ws.send(JSON.stringify({
1070
+ const session = ptySessionsMap.get(ptySessionKey);
1071
+ if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
1072
+ session.ws.send(JSON.stringify({
940
1073
  type: 'output',
941
1074
  data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
942
1075
  }));
943
1076
  }
1077
+ if (session && session.timeoutId) {
1078
+ clearTimeout(session.timeoutId);
1079
+ }
1080
+ ptySessionsMap.delete(ptySessionKey);
944
1081
  shellProcess = null;
945
1082
  });
946
1083
 
@@ -983,9 +1120,21 @@ function handleShellConnection(ws) {
983
1120
 
984
1121
  ws.on('close', () => {
985
1122
  console.log('🔌 Shell client disconnected');
986
- if (shellProcess && shellProcess.kill) {
987
- console.log('🔴 Killing shell process:', shellProcess.pid);
988
- shellProcess.kill();
1123
+
1124
+ if (ptySessionKey) {
1125
+ const session = ptySessionsMap.get(ptySessionKey);
1126
+ if (session) {
1127
+ console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
1128
+ session.ws = null;
1129
+
1130
+ session.timeoutId = setTimeout(() => {
1131
+ console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
1132
+ if (session.pty && session.pty.kill) {
1133
+ session.pty.kill();
1134
+ }
1135
+ ptySessionsMap.delete(ptySessionKey);
1136
+ }, PTY_SESSION_TIMEOUT);
1137
+ }
989
1138
  }
990
1139
  });
991
1140
 
@@ -1231,8 +1380,98 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
1231
1380
  app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1232
1381
  try {
1233
1382
  const { projectName, sessionId } = req.params;
1383
+ const { provider = 'claude' } = req.query;
1234
1384
  const homeDir = os.homedir();
1235
1385
 
1386
+ // Allow only safe characters in sessionId
1387
+ const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
1388
+ if (!safeSessionId) {
1389
+ return res.status(400).json({ error: 'Invalid sessionId' });
1390
+ }
1391
+
1392
+ // Handle Cursor sessions - they use SQLite and don't have token usage info
1393
+ if (provider === 'cursor') {
1394
+ return res.json({
1395
+ used: 0,
1396
+ total: 0,
1397
+ breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1398
+ unsupported: true,
1399
+ message: 'Token usage tracking not available for Cursor sessions'
1400
+ });
1401
+ }
1402
+
1403
+ // Handle Codex sessions
1404
+ if (provider === 'codex') {
1405
+ const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
1406
+
1407
+ // Find the session file by searching for the session ID
1408
+ const findSessionFile = async (dir) => {
1409
+ try {
1410
+ const entries = await fsPromises.readdir(dir, { withFileTypes: true });
1411
+ for (const entry of entries) {
1412
+ const fullPath = path.join(dir, entry.name);
1413
+ if (entry.isDirectory()) {
1414
+ const found = await findSessionFile(fullPath);
1415
+ if (found) return found;
1416
+ } else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
1417
+ return fullPath;
1418
+ }
1419
+ }
1420
+ } catch (error) {
1421
+ // Skip directories we can't read
1422
+ }
1423
+ return null;
1424
+ };
1425
+
1426
+ const sessionFilePath = await findSessionFile(codexSessionsDir);
1427
+
1428
+ if (!sessionFilePath) {
1429
+ return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
1430
+ }
1431
+
1432
+ // Read and parse the Codex JSONL file
1433
+ let fileContent;
1434
+ try {
1435
+ fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1436
+ } catch (error) {
1437
+ if (error.code === 'ENOENT') {
1438
+ return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
1439
+ }
1440
+ throw error;
1441
+ }
1442
+ const lines = fileContent.trim().split('\n');
1443
+ let totalTokens = 0;
1444
+ let contextWindow = 200000; // Default for Codex/OpenAI
1445
+
1446
+ // Find the latest token_count event with info (scan from end)
1447
+ for (let i = lines.length - 1; i >= 0; i--) {
1448
+ try {
1449
+ const entry = JSON.parse(lines[i]);
1450
+
1451
+ // Codex stores token info in event_msg with type: "token_count"
1452
+ if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1453
+ const tokenInfo = entry.payload.info;
1454
+ if (tokenInfo.total_token_usage) {
1455
+ totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
1456
+ }
1457
+ if (tokenInfo.model_context_window) {
1458
+ contextWindow = tokenInfo.model_context_window;
1459
+ }
1460
+ break; // Stop after finding the latest token count
1461
+ }
1462
+ } catch (parseError) {
1463
+ // Skip lines that can't be parsed
1464
+ continue;
1465
+ }
1466
+ }
1467
+
1468
+ return res.json({
1469
+ used: totalTokens,
1470
+ total: contextWindow
1471
+ });
1472
+ }
1473
+
1474
+ // Handle Claude sessions (default)
1236
1475
  // Extract actual project path
1237
1476
  let projectPath;
1238
1477
  try {
@@ -1248,11 +1487,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
1248
1487
  const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
1249
1488
  const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
1250
1489
 
1251
- // Allow only safe characters in sessionId
1252
- const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
1253
- if (!safeSessionId) {
1254
- return res.status(400).json({ error: 'Invalid sessionId' });
1255
- }
1256
1490
  const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
1257
1491
 
1258
1492
  // Constrain to projectDir
@@ -20,6 +20,22 @@ const validateApiKey = (req, res, next) => {
20
20
 
21
21
  // JWT authentication middleware
22
22
  const authenticateToken = async (req, res, next) => {
23
+ // Platform mode: use single database user
24
+ if (process.env.VITE_IS_PLATFORM === 'true') {
25
+ try {
26
+ const user = userDb.getFirstUser();
27
+ if (!user) {
28
+ return res.status(500).json({ error: 'Platform mode: No user found in database' });
29
+ }
30
+ req.user = user;
31
+ return next();
32
+ } catch (error) {
33
+ console.error('Platform mode error:', error);
34
+ return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
35
+ }
36
+ }
37
+
38
+ // Normal OSS JWT validation
23
39
  const authHeader = req.headers['authorization'];
24
40
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
25
41
 
@@ -29,13 +45,13 @@ const authenticateToken = async (req, res, next) => {
29
45
 
30
46
  try {
31
47
  const decoded = jwt.verify(token, JWT_SECRET);
32
-
48
+
33
49
  // Verify user still exists and is active
34
50
  const user = userDb.getUserById(decoded.userId);
35
51
  if (!user) {
36
52
  return res.status(401).json({ error: 'Invalid token. User not found.' });
37
53
  }
38
-
54
+
39
55
  req.user = user;
40
56
  next();
41
57
  } catch (error) {
@@ -58,10 +74,25 @@ const generateToken = (user) => {
58
74
 
59
75
  // WebSocket authentication function
60
76
  const authenticateWebSocket = (token) => {
77
+ // Platform mode: bypass token validation, return first user
78
+ if (process.env.VITE_IS_PLATFORM === 'true') {
79
+ try {
80
+ const user = userDb.getFirstUser();
81
+ if (user) {
82
+ return { userId: user.id, username: user.username };
83
+ }
84
+ return null;
85
+ } catch (error) {
86
+ console.error('Platform mode WebSocket error:', error);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ // Normal OSS JWT validation
61
92
  if (!token) {
62
93
  return null;
63
94
  }
64
-
95
+
65
96
  try {
66
97
  const decoded = jwt.verify(token, JWT_SECRET);
67
98
  return decoded;