@siteboon/claude-code-ui 1.20.1 → 1.22.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/server/index.js CHANGED
@@ -48,6 +48,8 @@ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSess
48
48
  import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } 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';
51
+ import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
52
+ import sessionManager from './sessionManager.js';
51
53
  import gitRoutes from './routes/git.js';
52
54
  import authRoutes from './routes/auth.js';
53
55
  import mcpRoutes from './routes/mcp.js';
@@ -61,6 +63,7 @@ import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes
61
63
  import cliAuthRoutes from './routes/cli-auth.js';
62
64
  import userRoutes from './routes/user.js';
63
65
  import codexRoutes from './routes/codex.js';
66
+ import geminiRoutes from './routes/gemini.js';
64
67
  import { initializeDatabase } from './database/db.js';
65
68
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
66
69
  import { IS_PLATFORM } from './constants/config.js';
@@ -69,7 +72,9 @@ import { IS_PLATFORM } from './constants/config.js';
69
72
  const PROVIDER_WATCH_PATHS = [
70
73
  { provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
71
74
  { provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
72
- { provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') }
75
+ { provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') },
76
+ { provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
77
+ { provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
73
78
  ];
74
79
  const WATCHER_IGNORED_PATTERNS = [
75
80
  '**/node_modules/**',
@@ -319,25 +324,25 @@ app.locals.wss = wss;
319
324
 
320
325
  app.use(cors());
321
326
  app.use(express.json({
322
- limit: '50mb',
323
- type: (req) => {
324
- // Skip multipart/form-data requests (for file uploads like images)
325
- const contentType = req.headers['content-type'] || '';
326
- if (contentType.includes('multipart/form-data')) {
327
- return false;
327
+ limit: '50mb',
328
+ type: (req) => {
329
+ // Skip multipart/form-data requests (for file uploads like images)
330
+ const contentType = req.headers['content-type'] || '';
331
+ if (contentType.includes('multipart/form-data')) {
332
+ return false;
333
+ }
334
+ return contentType.includes('json');
328
335
  }
329
- return contentType.includes('json');
330
- }
331
336
  }));
332
337
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
333
338
 
334
339
  // Public health check endpoint (no authentication required)
335
340
  app.get('/health', (req, res) => {
336
- res.json({
337
- status: 'ok',
338
- timestamp: new Date().toISOString(),
339
- installMode
340
- });
341
+ res.json({
342
+ status: 'ok',
343
+ timestamp: new Date().toISOString(),
344
+ installMode
345
+ });
341
346
  });
342
347
 
343
348
  // Optional API key validation (if configured)
@@ -379,6 +384,9 @@ app.use('/api/user', authenticateToken, userRoutes);
379
384
  // Codex API Routes (protected)
380
385
  app.use('/api/codex', authenticateToken, codexRoutes);
381
386
 
387
+ // Gemini API Routes (protected)
388
+ app.use('/api/gemini', authenticateToken, geminiRoutes);
389
+
382
390
  // Agent API Routes (uses API key authentication)
383
391
  app.use('/api/agent', agentRoutes);
384
392
 
@@ -388,17 +396,17 @@ app.use(express.static(path.join(__dirname, '../public')));
388
396
  // Static files served after API routes
389
397
  // Add cache control: HTML files should not be cached, but assets can be cached
390
398
  app.use(express.static(path.join(__dirname, '../dist'), {
391
- setHeaders: (res, filePath) => {
392
- if (filePath.endsWith('.html')) {
393
- // Prevent HTML caching to avoid service worker issues after builds
394
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
395
- res.setHeader('Pragma', 'no-cache');
396
- res.setHeader('Expires', '0');
397
- } else if (filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)) {
398
- // Cache static assets for 1 year (they have hashed names)
399
- res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
399
+ setHeaders: (res, filePath) => {
400
+ if (filePath.endsWith('.html')) {
401
+ // Prevent HTML caching to avoid service worker issues after builds
402
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
403
+ res.setHeader('Pragma', 'no-cache');
404
+ res.setHeader('Expires', '0');
405
+ } else if (filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)) {
406
+ // Cache static assets for 1 year (they have hashed names)
407
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
408
+ }
400
409
  }
401
- }
402
410
  }));
403
411
 
404
412
  // API Routes (protected)
@@ -496,13 +504,13 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateT
496
504
  try {
497
505
  const { projectName, sessionId } = req.params;
498
506
  const { limit, offset } = req.query;
499
-
507
+
500
508
  // Parse limit and offset if provided
501
509
  const parsedLimit = limit ? parseInt(limit, 10) : null;
502
510
  const parsedOffset = offset ? parseInt(offset, 10) : 0;
503
-
511
+
504
512
  const result = await getSessionMessages(projectName, sessionId, parsedLimit, parsedOffset);
505
-
513
+
506
514
  // Handle both old and new response formats
507
515
  if (Array.isArray(result)) {
508
516
  // Backward compatibility: no pagination parameters were provided
@@ -585,13 +593,13 @@ const expandWorkspacePath = (inputPath) => {
585
593
  app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
586
594
  try {
587
595
  const { path: dirPath } = req.query;
588
-
596
+
589
597
  console.log('[API] Browse filesystem request for path:', dirPath);
590
598
  console.log('[API] WORKSPACES_ROOT is:', WORKSPACES_ROOT);
591
599
  // Default to home directory if no path provided
592
600
  const defaultRoot = WORKSPACES_ROOT;
593
601
  let targetPath = dirPath ? expandWorkspacePath(dirPath) : defaultRoot;
594
-
602
+
595
603
  // Resolve and normalize the path
596
604
  targetPath = path.resolve(targetPath);
597
605
 
@@ -601,22 +609,22 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
601
609
  return res.status(403).json({ error: validation.error });
602
610
  }
603
611
  const resolvedPath = validation.resolvedPath || targetPath;
604
-
612
+
605
613
  // Security check - ensure path is accessible
606
614
  try {
607
615
  await fs.promises.access(resolvedPath);
608
616
  const stats = await fs.promises.stat(resolvedPath);
609
-
617
+
610
618
  if (!stats.isDirectory()) {
611
619
  return res.status(400).json({ error: 'Path is not a directory' });
612
620
  }
613
621
  } catch (err) {
614
622
  return res.status(404).json({ error: 'Directory not accessible' });
615
623
  }
616
-
624
+
617
625
  // Use existing getFileTree function with shallow depth (only direct children)
618
626
  const fileTree = await getFileTree(resolvedPath, 1, 0, false); // maxDepth=1, showHidden=false
619
-
627
+
620
628
  // Filter only directories and format for suggestions
621
629
  const directories = fileTree
622
630
  .filter(item => item.type === 'directory')
@@ -632,7 +640,7 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
632
640
  if (!aHidden && bHidden) return -1;
633
641
  return a.name.localeCompare(b.name);
634
642
  });
635
-
643
+
636
644
  // Add common directories if browsing home directory
637
645
  const suggestions = [];
638
646
  let resolvedWorkspaceRoot = defaultRoot;
@@ -645,17 +653,17 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
645
653
  const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace'];
646
654
  const existingCommon = directories.filter(dir => commonDirs.includes(dir.name));
647
655
  const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name));
648
-
656
+
649
657
  suggestions.push(...existingCommon, ...otherDirs);
650
658
  } else {
651
659
  suggestions.push(...directories);
652
660
  }
653
-
661
+
654
662
  res.json({
655
663
  path: resolvedPath,
656
664
  suggestions: suggestions
657
665
  });
658
-
666
+
659
667
  } catch (error) {
660
668
  console.error('Error browsing filesystem:', error);
661
669
  res.status(500).json({ error: 'Failed to browse filesystem' });
@@ -876,6 +884,436 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
876
884
  }
877
885
  });
878
886
 
887
+ // ============================================================================
888
+ // FILE OPERATIONS API ENDPOINTS
889
+ // ============================================================================
890
+
891
+ /**
892
+ * Validate that a path is within the project root
893
+ * @param {string} projectRoot - The project root path
894
+ * @param {string} targetPath - The path to validate
895
+ * @returns {{ valid: boolean, resolved?: string, error?: string }}
896
+ */
897
+ function validatePathInProject(projectRoot, targetPath) {
898
+ const resolved = path.isAbsolute(targetPath)
899
+ ? path.resolve(targetPath)
900
+ : path.resolve(projectRoot, targetPath);
901
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
902
+ if (!resolved.startsWith(normalizedRoot)) {
903
+ return { valid: false, error: 'Path must be under project root' };
904
+ }
905
+ return { valid: true, resolved };
906
+ }
907
+
908
+ /**
909
+ * Validate filename - check for invalid characters
910
+ * @param {string} name - The filename to validate
911
+ * @returns {{ valid: boolean, error?: string }}
912
+ */
913
+ function validateFilename(name) {
914
+ if (!name || !name.trim()) {
915
+ return { valid: false, error: 'Filename cannot be empty' };
916
+ }
917
+ // Check for invalid characters (Windows + Unix)
918
+ const invalidChars = /[<>:"/\\|?*\x00-\x1f]/;
919
+ if (invalidChars.test(name)) {
920
+ return { valid: false, error: 'Filename contains invalid characters' };
921
+ }
922
+ // Check for reserved names (Windows)
923
+ const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
924
+ if (reserved.test(name)) {
925
+ return { valid: false, error: 'Filename is a reserved name' };
926
+ }
927
+ // Check for dots only
928
+ if (/^\.+$/.test(name)) {
929
+ return { valid: false, error: 'Filename cannot be only dots' };
930
+ }
931
+ return { valid: true };
932
+ }
933
+
934
+ // POST /api/projects/:projectName/files/create - Create new file or directory
935
+ app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => {
936
+ try {
937
+ const { projectName } = req.params;
938
+ const { path: parentPath, type, name } = req.body;
939
+
940
+ // Validate input
941
+ if (!name || !type) {
942
+ return res.status(400).json({ error: 'Name and type are required' });
943
+ }
944
+
945
+ if (!['file', 'directory'].includes(type)) {
946
+ return res.status(400).json({ error: 'Type must be "file" or "directory"' });
947
+ }
948
+
949
+ const nameValidation = validateFilename(name);
950
+ if (!nameValidation.valid) {
951
+ return res.status(400).json({ error: nameValidation.error });
952
+ }
953
+
954
+ // Get project root
955
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
956
+ if (!projectRoot) {
957
+ return res.status(404).json({ error: 'Project not found' });
958
+ }
959
+
960
+ // Build and validate target path
961
+ const targetDir = parentPath || '';
962
+ const targetPath = targetDir ? path.join(targetDir, name) : name;
963
+ const validation = validatePathInProject(projectRoot, targetPath);
964
+ if (!validation.valid) {
965
+ return res.status(403).json({ error: validation.error });
966
+ }
967
+
968
+ const resolvedPath = validation.resolved;
969
+
970
+ // Check if already exists
971
+ try {
972
+ await fsPromises.access(resolvedPath);
973
+ return res.status(409).json({ error: `${type === 'file' ? 'File' : 'Directory'} already exists` });
974
+ } catch {
975
+ // Doesn't exist, which is what we want
976
+ }
977
+
978
+ // Create file or directory
979
+ if (type === 'directory') {
980
+ await fsPromises.mkdir(resolvedPath, { recursive: false });
981
+ } else {
982
+ // Ensure parent directory exists
983
+ const parentDir = path.dirname(resolvedPath);
984
+ try {
985
+ await fsPromises.access(parentDir);
986
+ } catch {
987
+ await fsPromises.mkdir(parentDir, { recursive: true });
988
+ }
989
+ await fsPromises.writeFile(resolvedPath, '', 'utf8');
990
+ }
991
+
992
+ res.json({
993
+ success: true,
994
+ path: resolvedPath,
995
+ name,
996
+ type,
997
+ message: `${type === 'file' ? 'File' : 'Directory'} created successfully`
998
+ });
999
+ } catch (error) {
1000
+ console.error('Error creating file/directory:', error);
1001
+ if (error.code === 'EACCES') {
1002
+ res.status(403).json({ error: 'Permission denied' });
1003
+ } else if (error.code === 'ENOENT') {
1004
+ res.status(404).json({ error: 'Parent directory not found' });
1005
+ } else {
1006
+ res.status(500).json({ error: error.message });
1007
+ }
1008
+ }
1009
+ });
1010
+
1011
+ // PUT /api/projects/:projectName/files/rename - Rename file or directory
1012
+ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => {
1013
+ try {
1014
+ const { projectName } = req.params;
1015
+ const { oldPath, newName } = req.body;
1016
+
1017
+ // Validate input
1018
+ if (!oldPath || !newName) {
1019
+ return res.status(400).json({ error: 'oldPath and newName are required' });
1020
+ }
1021
+
1022
+ const nameValidation = validateFilename(newName);
1023
+ if (!nameValidation.valid) {
1024
+ return res.status(400).json({ error: nameValidation.error });
1025
+ }
1026
+
1027
+ // Get project root
1028
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
1029
+ if (!projectRoot) {
1030
+ return res.status(404).json({ error: 'Project not found' });
1031
+ }
1032
+
1033
+ // Validate old path
1034
+ const oldValidation = validatePathInProject(projectRoot, oldPath);
1035
+ if (!oldValidation.valid) {
1036
+ return res.status(403).json({ error: oldValidation.error });
1037
+ }
1038
+
1039
+ const resolvedOldPath = oldValidation.resolved;
1040
+
1041
+ // Check if old path exists
1042
+ try {
1043
+ await fsPromises.access(resolvedOldPath);
1044
+ } catch {
1045
+ return res.status(404).json({ error: 'File or directory not found' });
1046
+ }
1047
+
1048
+ // Build and validate new path
1049
+ const parentDir = path.dirname(resolvedOldPath);
1050
+ const resolvedNewPath = path.join(parentDir, newName);
1051
+ const newValidation = validatePathInProject(projectRoot, resolvedNewPath);
1052
+ if (!newValidation.valid) {
1053
+ return res.status(403).json({ error: newValidation.error });
1054
+ }
1055
+
1056
+ // Check if new path already exists
1057
+ try {
1058
+ await fsPromises.access(resolvedNewPath);
1059
+ return res.status(409).json({ error: 'A file or directory with this name already exists' });
1060
+ } catch {
1061
+ // Doesn't exist, which is what we want
1062
+ }
1063
+
1064
+ // Rename
1065
+ await fsPromises.rename(resolvedOldPath, resolvedNewPath);
1066
+
1067
+ res.json({
1068
+ success: true,
1069
+ oldPath: resolvedOldPath,
1070
+ newPath: resolvedNewPath,
1071
+ newName,
1072
+ message: 'Renamed successfully'
1073
+ });
1074
+ } catch (error) {
1075
+ console.error('Error renaming file/directory:', error);
1076
+ if (error.code === 'EACCES') {
1077
+ res.status(403).json({ error: 'Permission denied' });
1078
+ } else if (error.code === 'ENOENT') {
1079
+ res.status(404).json({ error: 'File or directory not found' });
1080
+ } else if (error.code === 'EXDEV') {
1081
+ res.status(400).json({ error: 'Cannot move across different filesystems' });
1082
+ } else {
1083
+ res.status(500).json({ error: error.message });
1084
+ }
1085
+ }
1086
+ });
1087
+
1088
+ // DELETE /api/projects/:projectName/files - Delete file or directory
1089
+ app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
1090
+ try {
1091
+ const { projectName } = req.params;
1092
+ const { path: targetPath, type } = req.body;
1093
+
1094
+ // Validate input
1095
+ if (!targetPath) {
1096
+ return res.status(400).json({ error: 'Path is required' });
1097
+ }
1098
+
1099
+ // Get project root
1100
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
1101
+ if (!projectRoot) {
1102
+ return res.status(404).json({ error: 'Project not found' });
1103
+ }
1104
+
1105
+ // Validate path
1106
+ const validation = validatePathInProject(projectRoot, targetPath);
1107
+ if (!validation.valid) {
1108
+ return res.status(403).json({ error: validation.error });
1109
+ }
1110
+
1111
+ const resolvedPath = validation.resolved;
1112
+
1113
+ // Check if path exists and get stats
1114
+ let stats;
1115
+ try {
1116
+ stats = await fsPromises.stat(resolvedPath);
1117
+ } catch {
1118
+ return res.status(404).json({ error: 'File or directory not found' });
1119
+ }
1120
+
1121
+ // Prevent deleting the project root itself
1122
+ if (resolvedPath === path.resolve(projectRoot)) {
1123
+ return res.status(403).json({ error: 'Cannot delete project root directory' });
1124
+ }
1125
+
1126
+ // Delete based on type
1127
+ if (stats.isDirectory()) {
1128
+ await fsPromises.rm(resolvedPath, { recursive: true, force: true });
1129
+ } else {
1130
+ await fsPromises.unlink(resolvedPath);
1131
+ }
1132
+
1133
+ res.json({
1134
+ success: true,
1135
+ path: resolvedPath,
1136
+ type: stats.isDirectory() ? 'directory' : 'file',
1137
+ message: 'Deleted successfully'
1138
+ });
1139
+ } catch (error) {
1140
+ console.error('Error deleting file/directory:', error);
1141
+ if (error.code === 'EACCES') {
1142
+ res.status(403).json({ error: 'Permission denied' });
1143
+ } else if (error.code === 'ENOENT') {
1144
+ res.status(404).json({ error: 'File or directory not found' });
1145
+ } else if (error.code === 'ENOTEMPTY') {
1146
+ res.status(400).json({ error: 'Directory is not empty' });
1147
+ } else {
1148
+ res.status(500).json({ error: error.message });
1149
+ }
1150
+ }
1151
+ });
1152
+
1153
+ // POST /api/projects/:projectName/files/upload - Upload files
1154
+ // Dynamic import of multer for file uploads
1155
+ const uploadFilesHandler = async (req, res) => {
1156
+ // Dynamic import of multer
1157
+ const multer = (await import('multer')).default;
1158
+
1159
+ const uploadMiddleware = multer({
1160
+ storage: multer.diskStorage({
1161
+ destination: (req, file, cb) => {
1162
+ cb(null, os.tmpdir());
1163
+ },
1164
+ filename: (req, file, cb) => {
1165
+ // Use a unique temp name, but preserve original name in file.originalname
1166
+ // Note: file.originalname may contain path separators for folder uploads
1167
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
1168
+ // For temp file, just use a safe unique name without the path
1169
+ cb(null, `upload-${uniqueSuffix}`);
1170
+ }
1171
+ }),
1172
+ limits: {
1173
+ fileSize: 50 * 1024 * 1024, // 50MB limit
1174
+ files: 20 // Max 20 files at once
1175
+ }
1176
+ });
1177
+
1178
+ // Use multer middleware
1179
+ uploadMiddleware.array('files', 20)(req, res, async (err) => {
1180
+ if (err) {
1181
+ console.error('Multer error:', err);
1182
+ if (err.code === 'LIMIT_FILE_SIZE') {
1183
+ return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' });
1184
+ }
1185
+ if (err.code === 'LIMIT_FILE_COUNT') {
1186
+ return res.status(400).json({ error: 'Too many files. Maximum is 20 files.' });
1187
+ }
1188
+ return res.status(500).json({ error: err.message });
1189
+ }
1190
+
1191
+ try {
1192
+ const { projectName } = req.params;
1193
+ const { targetPath, relativePaths } = req.body;
1194
+
1195
+ // Parse relative paths if provided (for folder uploads)
1196
+ let filePaths = [];
1197
+ if (relativePaths) {
1198
+ try {
1199
+ filePaths = JSON.parse(relativePaths);
1200
+ } catch (e) {
1201
+ console.log('[DEBUG] Failed to parse relativePaths:', relativePaths);
1202
+ }
1203
+ }
1204
+
1205
+ console.log('[DEBUG] File upload request:', {
1206
+ projectName,
1207
+ targetPath: JSON.stringify(targetPath),
1208
+ targetPathType: typeof targetPath,
1209
+ filesCount: req.files?.length,
1210
+ relativePaths: filePaths
1211
+ });
1212
+
1213
+ if (!req.files || req.files.length === 0) {
1214
+ return res.status(400).json({ error: 'No files provided' });
1215
+ }
1216
+
1217
+ // Get project root
1218
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
1219
+ if (!projectRoot) {
1220
+ return res.status(404).json({ error: 'Project not found' });
1221
+ }
1222
+
1223
+ console.log('[DEBUG] Project root:', projectRoot);
1224
+
1225
+ // Validate and resolve target path
1226
+ // If targetPath is empty or '.', use project root directly
1227
+ const targetDir = targetPath || '';
1228
+ let resolvedTargetDir;
1229
+
1230
+ console.log('[DEBUG] Target dir:', JSON.stringify(targetDir));
1231
+
1232
+ if (!targetDir || targetDir === '.' || targetDir === './') {
1233
+ // Empty path means upload to project root
1234
+ resolvedTargetDir = path.resolve(projectRoot);
1235
+ console.log('[DEBUG] Using project root as target:', resolvedTargetDir);
1236
+ } else {
1237
+ const validation = validatePathInProject(projectRoot, targetDir);
1238
+ if (!validation.valid) {
1239
+ console.log('[DEBUG] Path validation failed:', validation.error);
1240
+ return res.status(403).json({ error: validation.error });
1241
+ }
1242
+ resolvedTargetDir = validation.resolved;
1243
+ console.log('[DEBUG] Resolved target dir:', resolvedTargetDir);
1244
+ }
1245
+
1246
+ // Ensure target directory exists
1247
+ try {
1248
+ await fsPromises.access(resolvedTargetDir);
1249
+ } catch {
1250
+ await fsPromises.mkdir(resolvedTargetDir, { recursive: true });
1251
+ }
1252
+
1253
+ // Move uploaded files from temp to target directory
1254
+ const uploadedFiles = [];
1255
+ console.log('[DEBUG] Processing files:', req.files.map(f => ({ originalname: f.originalname, path: f.path })));
1256
+ for (let i = 0; i < req.files.length; i++) {
1257
+ const file = req.files[i];
1258
+ // Use relative path if provided (for folder uploads), otherwise use originalname
1259
+ const fileName = (filePaths && filePaths[i]) ? filePaths[i] : file.originalname;
1260
+ console.log('[DEBUG] Processing file:', fileName, '(originalname:', file.originalname + ')');
1261
+ const destPath = path.join(resolvedTargetDir, fileName);
1262
+
1263
+ // Validate destination path
1264
+ const destValidation = validatePathInProject(projectRoot, destPath);
1265
+ if (!destValidation.valid) {
1266
+ console.log('[DEBUG] Destination validation failed for:', destPath);
1267
+ // Clean up temp file
1268
+ await fsPromises.unlink(file.path).catch(() => {});
1269
+ continue;
1270
+ }
1271
+
1272
+ // Ensure parent directory exists (for nested files from folder upload)
1273
+ const parentDir = path.dirname(destPath);
1274
+ try {
1275
+ await fsPromises.access(parentDir);
1276
+ } catch {
1277
+ await fsPromises.mkdir(parentDir, { recursive: true });
1278
+ }
1279
+
1280
+ // Move file (copy + unlink to handle cross-device scenarios)
1281
+ await fsPromises.copyFile(file.path, destPath);
1282
+ await fsPromises.unlink(file.path);
1283
+
1284
+ uploadedFiles.push({
1285
+ name: fileName,
1286
+ path: destPath,
1287
+ size: file.size,
1288
+ mimeType: file.mimetype
1289
+ });
1290
+ }
1291
+
1292
+ res.json({
1293
+ success: true,
1294
+ files: uploadedFiles,
1295
+ targetPath: resolvedTargetDir,
1296
+ message: `Uploaded ${uploadedFiles.length} file(s) successfully`
1297
+ });
1298
+ } catch (error) {
1299
+ console.error('Error uploading files:', error);
1300
+ // Clean up any remaining temp files
1301
+ if (req.files) {
1302
+ for (const file of req.files) {
1303
+ await fsPromises.unlink(file.path).catch(() => {});
1304
+ }
1305
+ }
1306
+ if (error.code === 'EACCES') {
1307
+ res.status(403).json({ error: 'Permission denied' });
1308
+ } else {
1309
+ res.status(500).json({ error: error.message });
1310
+ }
1311
+ }
1312
+ });
1313
+ };
1314
+
1315
+ app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
1316
+
879
1317
  // WebSocket connection handler that routes based on URL path
880
1318
  wss.on('connection', (ws, request) => {
881
1319
  const url = request.url;
@@ -899,26 +1337,26 @@ wss.on('connection', (ws, request) => {
899
1337
  * WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
900
1338
  */
901
1339
  class WebSocketWriter {
902
- constructor(ws) {
903
- this.ws = ws;
904
- this.sessionId = null;
905
- this.isWebSocketWriter = true; // Marker for transport detection
906
- }
907
-
908
- send(data) {
909
- if (this.ws.readyState === 1) { // WebSocket.OPEN
910
- // Providers send raw objects, we stringify for WebSocket
911
- this.ws.send(JSON.stringify(data));
1340
+ constructor(ws) {
1341
+ this.ws = ws;
1342
+ this.sessionId = null;
1343
+ this.isWebSocketWriter = true; // Marker for transport detection
912
1344
  }
913
- }
914
1345
 
915
- setSessionId(sessionId) {
916
- this.sessionId = sessionId;
917
- }
1346
+ send(data) {
1347
+ if (this.ws.readyState === 1) { // WebSocket.OPEN
1348
+ // Providers send raw objects, we stringify for WebSocket
1349
+ this.ws.send(JSON.stringify(data));
1350
+ }
1351
+ }
918
1352
 
919
- getSessionId() {
920
- return this.sessionId;
921
- }
1353
+ setSessionId(sessionId) {
1354
+ this.sessionId = sessionId;
1355
+ }
1356
+
1357
+ getSessionId() {
1358
+ return this.sessionId;
1359
+ }
922
1360
  }
923
1361
 
924
1362
  // Handle chat WebSocket connections
@@ -954,6 +1392,12 @@ function handleChatConnection(ws) {
954
1392
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
955
1393
  console.log('🤖 Model:', data.options?.model || 'default');
956
1394
  await queryCodex(data.command, data.options, writer);
1395
+ } else if (data.type === 'gemini-command') {
1396
+ console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
1397
+ console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1398
+ console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1399
+ console.log('🤖 Model:', data.options?.model || 'default');
1400
+ await spawnGemini(data.command, data.options, writer);
957
1401
  } else if (data.type === 'cursor-resume') {
958
1402
  // Backward compatibility: treat as cursor-command with resume and no prompt
959
1403
  console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
@@ -971,6 +1415,8 @@ function handleChatConnection(ws) {
971
1415
  success = abortCursorSession(data.sessionId);
972
1416
  } else if (provider === 'codex') {
973
1417
  success = abortCodexSession(data.sessionId);
1418
+ } else if (provider === 'gemini') {
1419
+ success = abortGeminiSession(data.sessionId);
974
1420
  } else {
975
1421
  // Use Claude Agents SDK
976
1422
  success = await abortClaudeSDKSession(data.sessionId);
@@ -1013,6 +1459,8 @@ function handleChatConnection(ws) {
1013
1459
  isActive = isCursorSessionActive(sessionId);
1014
1460
  } else if (provider === 'codex') {
1015
1461
  isActive = isCodexSessionActive(sessionId);
1462
+ } else if (provider === 'gemini') {
1463
+ isActive = isGeminiSessionActive(sessionId);
1016
1464
  } else {
1017
1465
  // Use Claude Agents SDK
1018
1466
  isActive = isClaudeSDKSessionActive(sessionId);
@@ -1029,7 +1477,8 @@ function handleChatConnection(ws) {
1029
1477
  const activeSessions = {
1030
1478
  claude: getActiveClaudeSDKSessions(),
1031
1479
  cursor: getActiveCursorSessions(),
1032
- codex: getActiveCodexSessions()
1480
+ codex: getActiveCodexSessions(),
1481
+ gemini: getActiveGeminiSessions()
1033
1482
  };
1034
1483
  writer.send({
1035
1484
  type: 'active-sessions',
@@ -1138,7 +1587,7 @@ function handleShellConnection(ws) {
1138
1587
  if (isPlainShell) {
1139
1588
  welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
1140
1589
  } else {
1141
- const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
1590
+ const providerName = provider === 'cursor' ? 'Cursor' : (provider === 'codex' ? 'Codex' : (provider === 'gemini' ? 'Gemini' : 'Claude'));
1142
1591
  welcomeMsg = hasSession ?
1143
1592
  `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
1144
1593
  `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
@@ -1174,6 +1623,55 @@ function handleShellConnection(ws) {
1174
1623
  shellCommand = `cd "${projectPath}" && cursor-agent`;
1175
1624
  }
1176
1625
  }
1626
+
1627
+ } else if (provider === 'codex') {
1628
+ // Use codex command
1629
+ if (os.platform() === 'win32') {
1630
+ if (hasSession && sessionId) {
1631
+ // Try to resume session, but with fallback to a new session if it fails
1632
+ shellCommand = `Set-Location -Path "${projectPath}"; codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
1633
+ } else {
1634
+ shellCommand = `Set-Location -Path "${projectPath}"; codex`;
1635
+ }
1636
+ } else {
1637
+ if (hasSession && sessionId) {
1638
+ // Try to resume session, but with fallback to a new session if it fails
1639
+ shellCommand = `cd "${projectPath}" && codex resume "${sessionId}" || codex`;
1640
+ } else {
1641
+ shellCommand = `cd "${projectPath}" && codex`;
1642
+ }
1643
+ }
1644
+ } else if (provider === 'gemini') {
1645
+ // Use gemini command
1646
+ const command = initialCommand || 'gemini';
1647
+ let resumeId = sessionId;
1648
+ if (hasSession && sessionId) {
1649
+ try {
1650
+ // Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names.
1651
+ // The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
1652
+ // We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
1653
+ const sess = sessionManager.getSession(sessionId);
1654
+ if (sess && sess.cliSessionId) {
1655
+ resumeId = sess.cliSessionId;
1656
+ }
1657
+ } catch (err) {
1658
+ console.error('Failed to get Gemini CLI session ID:', err);
1659
+ }
1660
+ }
1661
+
1662
+ if (os.platform() === 'win32') {
1663
+ if (hasSession && resumeId) {
1664
+ shellCommand = `Set-Location -Path "${projectPath}"; ${command} --resume "${resumeId}"`;
1665
+ } else {
1666
+ shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
1667
+ }
1668
+ } else {
1669
+ if (hasSession && resumeId) {
1670
+ shellCommand = `cd "${projectPath}" && ${command} --resume "${resumeId}"`;
1671
+ } else {
1672
+ shellCommand = `cd "${projectPath}" && ${command}`;
1673
+ }
1674
+ }
1177
1675
  } else {
1178
1676
  // Use claude command (default) or initialCommand if provided
1179
1677
  const command = initialCommand || 'claude';
@@ -1607,203 +2105,214 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
1607
2105
 
1608
2106
  // Get token usage for a specific session
1609
2107
  app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1610
- try {
1611
- const { projectName, sessionId } = req.params;
1612
- const { provider = 'claude' } = req.query;
1613
- const homeDir = os.homedir();
1614
-
1615
- // Allow only safe characters in sessionId
1616
- const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
1617
- if (!safeSessionId) {
1618
- return res.status(400).json({ error: 'Invalid sessionId' });
1619
- }
2108
+ try {
2109
+ const { projectName, sessionId } = req.params;
2110
+ const { provider = 'claude' } = req.query;
2111
+ const homeDir = os.homedir();
1620
2112
 
1621
- // Handle Cursor sessions - they use SQLite and don't have token usage info
1622
- if (provider === 'cursor') {
1623
- return res.json({
1624
- used: 0,
1625
- total: 0,
1626
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1627
- unsupported: true,
1628
- message: 'Token usage tracking not available for Cursor sessions'
1629
- });
1630
- }
2113
+ // Allow only safe characters in sessionId
2114
+ const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
2115
+ if (!safeSessionId) {
2116
+ return res.status(400).json({ error: 'Invalid sessionId' });
2117
+ }
1631
2118
 
1632
- // Handle Codex sessions
1633
- if (provider === 'codex') {
1634
- const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
2119
+ // Handle Cursor sessions - they use SQLite and don't have token usage info
2120
+ if (provider === 'cursor') {
2121
+ return res.json({
2122
+ used: 0,
2123
+ total: 0,
2124
+ breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
2125
+ unsupported: true,
2126
+ message: 'Token usage tracking not available for Cursor sessions'
2127
+ });
2128
+ }
1635
2129
 
1636
- // Find the session file by searching for the session ID
1637
- const findSessionFile = async (dir) => {
1638
- try {
1639
- const entries = await fsPromises.readdir(dir, { withFileTypes: true });
1640
- for (const entry of entries) {
1641
- const fullPath = path.join(dir, entry.name);
1642
- if (entry.isDirectory()) {
1643
- const found = await findSessionFile(fullPath);
1644
- if (found) return found;
1645
- } else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
1646
- return fullPath;
2130
+ // Handle Gemini sessions - they are raw logs in our current setup
2131
+ if (provider === 'gemini') {
2132
+ return res.json({
2133
+ used: 0,
2134
+ total: 0,
2135
+ breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
2136
+ unsupported: true,
2137
+ message: 'Token usage tracking not available for Gemini sessions'
2138
+ });
2139
+ }
2140
+
2141
+ // Handle Codex sessions
2142
+ if (provider === 'codex') {
2143
+ const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
2144
+
2145
+ // Find the session file by searching for the session ID
2146
+ const findSessionFile = async (dir) => {
2147
+ try {
2148
+ const entries = await fsPromises.readdir(dir, { withFileTypes: true });
2149
+ for (const entry of entries) {
2150
+ const fullPath = path.join(dir, entry.name);
2151
+ if (entry.isDirectory()) {
2152
+ const found = await findSessionFile(fullPath);
2153
+ if (found) return found;
2154
+ } else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
2155
+ return fullPath;
2156
+ }
2157
+ }
2158
+ } catch (error) {
2159
+ // Skip directories we can't read
2160
+ }
2161
+ return null;
2162
+ };
2163
+
2164
+ const sessionFilePath = await findSessionFile(codexSessionsDir);
2165
+
2166
+ if (!sessionFilePath) {
2167
+ return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
1647
2168
  }
1648
- }
2169
+
2170
+ // Read and parse the Codex JSONL file
2171
+ let fileContent;
2172
+ try {
2173
+ fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
2174
+ } catch (error) {
2175
+ if (error.code === 'ENOENT') {
2176
+ return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
2177
+ }
2178
+ throw error;
2179
+ }
2180
+ const lines = fileContent.trim().split('\n');
2181
+ let totalTokens = 0;
2182
+ let contextWindow = 200000; // Default for Codex/OpenAI
2183
+
2184
+ // Find the latest token_count event with info (scan from end)
2185
+ for (let i = lines.length - 1; i >= 0; i--) {
2186
+ try {
2187
+ const entry = JSON.parse(lines[i]);
2188
+
2189
+ // Codex stores token info in event_msg with type: "token_count"
2190
+ if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
2191
+ const tokenInfo = entry.payload.info;
2192
+ if (tokenInfo.total_token_usage) {
2193
+ totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
2194
+ }
2195
+ if (tokenInfo.model_context_window) {
2196
+ contextWindow = tokenInfo.model_context_window;
2197
+ }
2198
+ break; // Stop after finding the latest token count
2199
+ }
2200
+ } catch (parseError) {
2201
+ // Skip lines that can't be parsed
2202
+ continue;
2203
+ }
2204
+ }
2205
+
2206
+ return res.json({
2207
+ used: totalTokens,
2208
+ total: contextWindow
2209
+ });
2210
+ }
2211
+
2212
+ // Handle Claude sessions (default)
2213
+ // Extract actual project path
2214
+ let projectPath;
2215
+ try {
2216
+ projectPath = await extractProjectDirectory(projectName);
1649
2217
  } catch (error) {
1650
- // Skip directories we can't read
2218
+ console.error('Error extracting project directory:', error);
2219
+ return res.status(500).json({ error: 'Failed to determine project path' });
1651
2220
  }
1652
- return null;
1653
- };
1654
2221
 
1655
- const sessionFilePath = await findSessionFile(codexSessionsDir);
2222
+ // Construct the JSONL file path
2223
+ // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
2224
+ // The encoding replaces any non-alphanumeric character (except -) with -
2225
+ const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
2226
+ const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
1656
2227
 
1657
- if (!sessionFilePath) {
1658
- return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
1659
- }
2228
+ const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
1660
2229
 
1661
- // Read and parse the Codex JSONL file
1662
- let fileContent;
1663
- try {
1664
- fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1665
- } catch (error) {
1666
- if (error.code === 'ENOENT') {
1667
- return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
2230
+ // Constrain to projectDir
2231
+ const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
2232
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
2233
+ return res.status(400).json({ error: 'Invalid path' });
1668
2234
  }
1669
- throw error;
1670
- }
1671
- const lines = fileContent.trim().split('\n');
1672
- let totalTokens = 0;
1673
- let contextWindow = 200000; // Default for Codex/OpenAI
1674
2235
 
1675
- // Find the latest token_count event with info (scan from end)
1676
- for (let i = lines.length - 1; i >= 0; i--) {
2236
+ // Read and parse the JSONL file
2237
+ let fileContent;
1677
2238
  try {
1678
- const entry = JSON.parse(lines[i]);
1679
-
1680
- // Codex stores token info in event_msg with type: "token_count"
1681
- if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1682
- const tokenInfo = entry.payload.info;
1683
- if (tokenInfo.total_token_usage) {
1684
- totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
1685
- }
1686
- if (tokenInfo.model_context_window) {
1687
- contextWindow = tokenInfo.model_context_window;
2239
+ fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
2240
+ } catch (error) {
2241
+ if (error.code === 'ENOENT') {
2242
+ return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
1688
2243
  }
1689
- break; // Stop after finding the latest token count
1690
- }
1691
- } catch (parseError) {
1692
- // Skip lines that can't be parsed
1693
- continue;
1694
- }
1695
- }
1696
-
1697
- return res.json({
1698
- used: totalTokens,
1699
- total: contextWindow
1700
- });
1701
- }
2244
+ throw error; // Re-throw other errors to be caught by outer try-catch
2245
+ }
2246
+ const lines = fileContent.trim().split('\n');
1702
2247
 
1703
- // Handle Claude sessions (default)
1704
- // Extract actual project path
1705
- let projectPath;
1706
- try {
1707
- projectPath = await extractProjectDirectory(projectName);
1708
- } catch (error) {
1709
- console.error('Error extracting project directory:', error);
1710
- return res.status(500).json({ error: 'Failed to determine project path' });
1711
- }
2248
+ const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
2249
+ const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
2250
+ let inputTokens = 0;
2251
+ let cacheCreationTokens = 0;
2252
+ let cacheReadTokens = 0;
1712
2253
 
1713
- // Construct the JSONL file path
1714
- // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
1715
- // The encoding replaces /, spaces, ~, and _ with -
1716
- const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
1717
- const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
2254
+ // Find the latest assistant message with usage data (scan from end)
2255
+ for (let i = lines.length - 1; i >= 0; i--) {
2256
+ try {
2257
+ const entry = JSON.parse(lines[i]);
1718
2258
 
1719
- const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
2259
+ // Only count assistant messages which have usage data
2260
+ if (entry.type === 'assistant' && entry.message?.usage) {
2261
+ const usage = entry.message.usage;
1720
2262
 
1721
- // Constrain to projectDir
1722
- const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
1723
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
1724
- return res.status(400).json({ error: 'Invalid path' });
1725
- }
2263
+ // Use token counts from latest assistant message only
2264
+ inputTokens = usage.input_tokens || 0;
2265
+ cacheCreationTokens = usage.cache_creation_input_tokens || 0;
2266
+ cacheReadTokens = usage.cache_read_input_tokens || 0;
1726
2267
 
1727
- // Read and parse the JSONL file
1728
- let fileContent;
1729
- try {
1730
- fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
1731
- } catch (error) {
1732
- if (error.code === 'ENOENT') {
1733
- return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
1734
- }
1735
- throw error; // Re-throw other errors to be caught by outer try-catch
1736
- }
1737
- const lines = fileContent.trim().split('\n');
1738
-
1739
- const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
1740
- const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
1741
- let inputTokens = 0;
1742
- let cacheCreationTokens = 0;
1743
- let cacheReadTokens = 0;
1744
-
1745
- // Find the latest assistant message with usage data (scan from end)
1746
- for (let i = lines.length - 1; i >= 0; i--) {
1747
- try {
1748
- const entry = JSON.parse(lines[i]);
1749
-
1750
- // Only count assistant messages which have usage data
1751
- if (entry.type === 'assistant' && entry.message?.usage) {
1752
- const usage = entry.message.usage;
1753
-
1754
- // Use token counts from latest assistant message only
1755
- inputTokens = usage.input_tokens || 0;
1756
- cacheCreationTokens = usage.cache_creation_input_tokens || 0;
1757
- cacheReadTokens = usage.cache_read_input_tokens || 0;
1758
-
1759
- break; // Stop after finding the latest assistant message
1760
- }
1761
- } catch (parseError) {
1762
- // Skip lines that can't be parsed
1763
- continue;
1764
- }
1765
- }
2268
+ break; // Stop after finding the latest assistant message
2269
+ }
2270
+ } catch (parseError) {
2271
+ // Skip lines that can't be parsed
2272
+ continue;
2273
+ }
2274
+ }
1766
2275
 
1767
- // Calculate total context usage (excluding output_tokens, as per ccusage)
1768
- const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
2276
+ // Calculate total context usage (excluding output_tokens, as per ccusage)
2277
+ const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
1769
2278
 
1770
- res.json({
1771
- used: totalUsed,
1772
- total: contextWindow,
1773
- breakdown: {
1774
- input: inputTokens,
1775
- cacheCreation: cacheCreationTokens,
1776
- cacheRead: cacheReadTokens
1777
- }
1778
- });
1779
- } catch (error) {
1780
- console.error('Error reading session token usage:', error);
1781
- res.status(500).json({ error: 'Failed to read session token usage' });
1782
- }
2279
+ res.json({
2280
+ used: totalUsed,
2281
+ total: contextWindow,
2282
+ breakdown: {
2283
+ input: inputTokens,
2284
+ cacheCreation: cacheCreationTokens,
2285
+ cacheRead: cacheReadTokens
2286
+ }
2287
+ });
2288
+ } catch (error) {
2289
+ console.error('Error reading session token usage:', error);
2290
+ res.status(500).json({ error: 'Failed to read session token usage' });
2291
+ }
1783
2292
  });
1784
2293
 
1785
2294
  // Serve React app for all other routes (excluding static files)
1786
2295
  app.get('*', (req, res) => {
1787
- // Skip requests for static assets (files with extensions)
1788
- if (path.extname(req.path)) {
1789
- return res.status(404).send('Not found');
1790
- }
1791
-
1792
- // Only serve index.html for HTML routes, not for static assets
1793
- // Static assets should already be handled by express.static middleware above
1794
- const indexPath = path.join(__dirname, '../dist/index.html');
1795
-
1796
- // Check if dist/index.html exists (production build available)
1797
- if (fs.existsSync(indexPath)) {
1798
- // Set no-cache headers for HTML to prevent service worker issues
1799
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1800
- res.setHeader('Pragma', 'no-cache');
1801
- res.setHeader('Expires', '0');
1802
- res.sendFile(indexPath);
1803
- } else {
1804
- // In development, redirect to Vite dev server only if dist doesn't exist
1805
- res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
1806
- }
2296
+ // Skip requests for static assets (files with extensions)
2297
+ if (path.extname(req.path)) {
2298
+ return res.status(404).send('Not found');
2299
+ }
2300
+
2301
+ // Only serve index.html for HTML routes, not for static assets
2302
+ // Static assets should already be handled by express.static middleware above
2303
+ const indexPath = path.join(__dirname, '../dist/index.html');
2304
+
2305
+ // Check if dist/index.html exists (production build available)
2306
+ if (fs.existsSync(indexPath)) {
2307
+ // Set no-cache headers for HTML to prevent service worker issues
2308
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
2309
+ res.setHeader('Pragma', 'no-cache');
2310
+ res.setHeader('Expires', '0');
2311
+ res.sendFile(indexPath);
2312
+ } else {
2313
+ // In development, redirect to Vite dev server only if dist doesn't exist
2314
+ res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
2315
+ }
1807
2316
  });
1808
2317
 
1809
2318
  // Helper function to convert permissions to rwx format