@siteboon/claude-code-ui 1.8.2

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 (106) hide show
  1. package/.env.example +12 -0
  2. package/.nvmrc +1 -0
  3. package/LICENSE +675 -0
  4. package/README.md +275 -0
  5. package/index.html +48 -0
  6. package/package.json +84 -0
  7. package/postcss.config.js +6 -0
  8. package/public/convert-icons.md +53 -0
  9. package/public/favicon.png +0 -0
  10. package/public/favicon.svg +9 -0
  11. package/public/generate-icons.js +49 -0
  12. package/public/icons/claude-ai-icon.svg +1 -0
  13. package/public/icons/cursor.svg +1 -0
  14. package/public/icons/generate-icons.md +19 -0
  15. package/public/icons/icon-128x128.png +0 -0
  16. package/public/icons/icon-128x128.svg +12 -0
  17. package/public/icons/icon-144x144.png +0 -0
  18. package/public/icons/icon-144x144.svg +12 -0
  19. package/public/icons/icon-152x152.png +0 -0
  20. package/public/icons/icon-152x152.svg +12 -0
  21. package/public/icons/icon-192x192.png +0 -0
  22. package/public/icons/icon-192x192.svg +12 -0
  23. package/public/icons/icon-384x384.png +0 -0
  24. package/public/icons/icon-384x384.svg +12 -0
  25. package/public/icons/icon-512x512.png +0 -0
  26. package/public/icons/icon-512x512.svg +12 -0
  27. package/public/icons/icon-72x72.png +0 -0
  28. package/public/icons/icon-72x72.svg +12 -0
  29. package/public/icons/icon-96x96.png +0 -0
  30. package/public/icons/icon-96x96.svg +12 -0
  31. package/public/icons/icon-template.svg +12 -0
  32. package/public/logo.svg +9 -0
  33. package/public/manifest.json +61 -0
  34. package/public/screenshots/cli-selection.png +0 -0
  35. package/public/screenshots/desktop-main.png +0 -0
  36. package/public/screenshots/mobile-chat.png +0 -0
  37. package/public/screenshots/tools-modal.png +0 -0
  38. package/public/sw.js +49 -0
  39. package/server/claude-cli.js +391 -0
  40. package/server/cursor-cli.js +250 -0
  41. package/server/database/db.js +86 -0
  42. package/server/database/init.sql +16 -0
  43. package/server/index.js +1167 -0
  44. package/server/middleware/auth.js +80 -0
  45. package/server/projects.js +1063 -0
  46. package/server/routes/auth.js +135 -0
  47. package/server/routes/cursor.js +794 -0
  48. package/server/routes/git.js +823 -0
  49. package/server/routes/mcp-utils.js +48 -0
  50. package/server/routes/mcp.js +552 -0
  51. package/server/routes/taskmaster.js +1971 -0
  52. package/server/utils/mcp-detector.js +198 -0
  53. package/server/utils/taskmaster-websocket.js +129 -0
  54. package/src/App.jsx +751 -0
  55. package/src/components/ChatInterface.jsx +3485 -0
  56. package/src/components/ClaudeLogo.jsx +11 -0
  57. package/src/components/ClaudeStatus.jsx +107 -0
  58. package/src/components/CodeEditor.jsx +422 -0
  59. package/src/components/CreateTaskModal.jsx +88 -0
  60. package/src/components/CursorLogo.jsx +9 -0
  61. package/src/components/DarkModeToggle.jsx +35 -0
  62. package/src/components/DiffViewer.jsx +41 -0
  63. package/src/components/ErrorBoundary.jsx +73 -0
  64. package/src/components/FileTree.jsx +480 -0
  65. package/src/components/GitPanel.jsx +1283 -0
  66. package/src/components/ImageViewer.jsx +54 -0
  67. package/src/components/LoginForm.jsx +110 -0
  68. package/src/components/MainContent.jsx +577 -0
  69. package/src/components/MicButton.jsx +272 -0
  70. package/src/components/MobileNav.jsx +88 -0
  71. package/src/components/NextTaskBanner.jsx +695 -0
  72. package/src/components/PRDEditor.jsx +871 -0
  73. package/src/components/ProtectedRoute.jsx +44 -0
  74. package/src/components/QuickSettingsPanel.jsx +262 -0
  75. package/src/components/Settings.jsx +2023 -0
  76. package/src/components/SetupForm.jsx +135 -0
  77. package/src/components/Shell.jsx +663 -0
  78. package/src/components/Sidebar.jsx +1665 -0
  79. package/src/components/StandaloneShell.jsx +106 -0
  80. package/src/components/TaskCard.jsx +210 -0
  81. package/src/components/TaskDetail.jsx +406 -0
  82. package/src/components/TaskIndicator.jsx +108 -0
  83. package/src/components/TaskList.jsx +1054 -0
  84. package/src/components/TaskMasterSetupWizard.jsx +603 -0
  85. package/src/components/TaskMasterStatus.jsx +86 -0
  86. package/src/components/TodoList.jsx +91 -0
  87. package/src/components/Tooltip.jsx +91 -0
  88. package/src/components/ui/badge.jsx +31 -0
  89. package/src/components/ui/button.jsx +46 -0
  90. package/src/components/ui/input.jsx +19 -0
  91. package/src/components/ui/scroll-area.jsx +23 -0
  92. package/src/contexts/AuthContext.jsx +158 -0
  93. package/src/contexts/TaskMasterContext.jsx +324 -0
  94. package/src/contexts/TasksSettingsContext.jsx +95 -0
  95. package/src/contexts/ThemeContext.jsx +94 -0
  96. package/src/contexts/WebSocketContext.jsx +29 -0
  97. package/src/hooks/useAudioRecorder.js +109 -0
  98. package/src/hooks/useVersionCheck.js +39 -0
  99. package/src/index.css +822 -0
  100. package/src/lib/utils.js +6 -0
  101. package/src/main.jsx +10 -0
  102. package/src/utils/api.js +141 -0
  103. package/src/utils/websocket.js +109 -0
  104. package/src/utils/whisper.js +37 -0
  105. package/tailwind.config.js +63 -0
  106. package/vite.config.js +29 -0
@@ -0,0 +1,1167 @@
1
+ // Load environment variables from .env file
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ try {
11
+ const envPath = path.join(__dirname, '../.env');
12
+ const envFile = fs.readFileSync(envPath, 'utf8');
13
+ envFile.split('\n').forEach(line => {
14
+ const trimmedLine = line.trim();
15
+ if (trimmedLine && !trimmedLine.startsWith('#')) {
16
+ const [key, ...valueParts] = trimmedLine.split('=');
17
+ if (key && valueParts.length > 0 && !process.env[key]) {
18
+ process.env[key] = valueParts.join('=').trim();
19
+ }
20
+ }
21
+ });
22
+ } catch (e) {
23
+ console.log('No .env file found or error reading it:', e.message);
24
+ }
25
+
26
+ console.log('PORT from env:', process.env.PORT);
27
+
28
+ import express from 'express';
29
+ import { WebSocketServer } from 'ws';
30
+ import http from 'http';
31
+ import cors from 'cors';
32
+ import { promises as fsPromises } from 'fs';
33
+ import { spawn } from 'child_process';
34
+ import os from 'os';
35
+ import pty from 'node-pty';
36
+ import fetch from 'node-fetch';
37
+ import mime from 'mime-types';
38
+
39
+ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
40
+ import { spawnClaude, abortClaudeSession } from './claude-cli.js';
41
+ import { spawnCursor, abortCursorSession } from './cursor-cli.js';
42
+ import gitRoutes from './routes/git.js';
43
+ import authRoutes from './routes/auth.js';
44
+ import mcpRoutes from './routes/mcp.js';
45
+ import cursorRoutes from './routes/cursor.js';
46
+ import taskmasterRoutes from './routes/taskmaster.js';
47
+ import mcpUtilsRoutes from './routes/mcp-utils.js';
48
+ import { initializeDatabase } from './database/db.js';
49
+ import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
50
+
51
+ // File system watcher for projects folder
52
+ let projectsWatcher = null;
53
+ const connectedClients = new Set();
54
+
55
+ // Setup file system watcher for Claude projects folder using chokidar
56
+ async function setupProjectsWatcher() {
57
+ const chokidar = (await import('chokidar')).default;
58
+ const claudeProjectsPath = path.join(process.env.HOME, '.claude', 'projects');
59
+
60
+ if (projectsWatcher) {
61
+ projectsWatcher.close();
62
+ }
63
+
64
+ try {
65
+ // Initialize chokidar watcher with optimized settings
66
+ projectsWatcher = chokidar.watch(claudeProjectsPath, {
67
+ ignored: [
68
+ '**/node_modules/**',
69
+ '**/.git/**',
70
+ '**/dist/**',
71
+ '**/build/**',
72
+ '**/*.tmp',
73
+ '**/*.swp',
74
+ '**/.DS_Store'
75
+ ],
76
+ persistent: true,
77
+ ignoreInitial: true, // Don't fire events for existing files on startup
78
+ followSymlinks: false,
79
+ depth: 10, // Reasonable depth limit
80
+ awaitWriteFinish: {
81
+ stabilityThreshold: 100, // Wait 100ms for file to stabilize
82
+ pollInterval: 50
83
+ }
84
+ });
85
+
86
+ // Debounce function to prevent excessive notifications
87
+ let debounceTimer;
88
+ const debouncedUpdate = async (eventType, filePath) => {
89
+ clearTimeout(debounceTimer);
90
+ debounceTimer = setTimeout(async () => {
91
+ try {
92
+
93
+ // Clear project directory cache when files change
94
+ clearProjectDirectoryCache();
95
+
96
+ // Get updated projects list
97
+ const updatedProjects = await getProjects();
98
+
99
+ // Notify all connected clients about the project changes
100
+ const updateMessage = JSON.stringify({
101
+ type: 'projects_updated',
102
+ projects: updatedProjects,
103
+ timestamp: new Date().toISOString(),
104
+ changeType: eventType,
105
+ changedFile: path.relative(claudeProjectsPath, filePath)
106
+ });
107
+
108
+ connectedClients.forEach(client => {
109
+ if (client.readyState === client.OPEN) {
110
+ client.send(updateMessage);
111
+ }
112
+ });
113
+
114
+ } catch (error) {
115
+ console.error('❌ Error handling project changes:', error);
116
+ }
117
+ }, 300); // 300ms debounce (slightly faster than before)
118
+ };
119
+
120
+ // Set up event listeners
121
+ projectsWatcher
122
+ .on('add', (filePath) => debouncedUpdate('add', filePath))
123
+ .on('change', (filePath) => debouncedUpdate('change', filePath))
124
+ .on('unlink', (filePath) => debouncedUpdate('unlink', filePath))
125
+ .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath))
126
+ .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath))
127
+ .on('error', (error) => {
128
+ console.error('❌ Chokidar watcher error:', error);
129
+ })
130
+ .on('ready', () => {
131
+ });
132
+
133
+ } catch (error) {
134
+ console.error('❌ Failed to setup projects watcher:', error);
135
+ }
136
+ }
137
+
138
+
139
+ const app = express();
140
+ const server = http.createServer(app);
141
+
142
+ // Single WebSocket server that handles both paths
143
+ const wss = new WebSocketServer({
144
+ server,
145
+ verifyClient: (info) => {
146
+ console.log('WebSocket connection attempt to:', info.req.url);
147
+
148
+ // Extract token from query parameters or headers
149
+ const url = new URL(info.req.url, 'http://localhost');
150
+ const token = url.searchParams.get('token') ||
151
+ info.req.headers.authorization?.split(' ')[1];
152
+
153
+ // Verify token
154
+ const user = authenticateWebSocket(token);
155
+ if (!user) {
156
+ console.log('❌ WebSocket authentication failed');
157
+ return false;
158
+ }
159
+
160
+ // Store user info in the request for later use
161
+ info.req.user = user;
162
+ console.log('✅ WebSocket authenticated for user:', user.username);
163
+ return true;
164
+ }
165
+ });
166
+
167
+ // Make WebSocket server available to routes
168
+ app.locals.wss = wss;
169
+
170
+ app.use(cors());
171
+ app.use(express.json());
172
+
173
+ // Optional API key validation (if configured)
174
+ app.use('/api', validateApiKey);
175
+
176
+ // Authentication routes (public)
177
+ app.use('/api/auth', authRoutes);
178
+
179
+ // Git API Routes (protected)
180
+ app.use('/api/git', authenticateToken, gitRoutes);
181
+
182
+ // MCP API Routes (protected)
183
+ app.use('/api/mcp', authenticateToken, mcpRoutes);
184
+
185
+ // Cursor API Routes (protected)
186
+ app.use('/api/cursor', authenticateToken, cursorRoutes);
187
+
188
+ // TaskMaster API Routes (protected)
189
+ app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
190
+
191
+ // MCP utilities
192
+ app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
193
+
194
+ // Static files served after API routes
195
+ app.use(express.static(path.join(__dirname, '../dist')));
196
+
197
+ // API Routes (protected)
198
+ app.get('/api/config', authenticateToken, (req, res) => {
199
+ const host = req.headers.host || `${req.hostname}:${PORT}`;
200
+ const protocol = req.protocol === 'https' || req.get('x-forwarded-proto') === 'https' ? 'wss' : 'ws';
201
+
202
+ console.log('Config API called - Returning host:', host, 'Protocol:', protocol);
203
+
204
+ res.json({
205
+ serverPort: PORT,
206
+ wsUrl: `${protocol}://${host}`
207
+ });
208
+ });
209
+
210
+ app.get('/api/projects', authenticateToken, async (req, res) => {
211
+ try {
212
+ const projects = await getProjects();
213
+ res.json(projects);
214
+ } catch (error) {
215
+ res.status(500).json({ error: error.message });
216
+ }
217
+ });
218
+
219
+ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
220
+ try {
221
+ const { limit = 5, offset = 0 } = req.query;
222
+ const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
223
+ res.json(result);
224
+ } catch (error) {
225
+ res.status(500).json({ error: error.message });
226
+ }
227
+ });
228
+
229
+ // Get messages for a specific session
230
+ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
231
+ try {
232
+ const { projectName, sessionId } = req.params;
233
+ const { limit, offset } = req.query;
234
+
235
+ // Parse limit and offset if provided
236
+ const parsedLimit = limit ? parseInt(limit, 10) : null;
237
+ const parsedOffset = offset ? parseInt(offset, 10) : 0;
238
+
239
+ const result = await getSessionMessages(projectName, sessionId, parsedLimit, parsedOffset);
240
+
241
+ // Handle both old and new response formats
242
+ if (Array.isArray(result)) {
243
+ // Backward compatibility: no pagination parameters were provided
244
+ res.json({ messages: result });
245
+ } else {
246
+ // New format with pagination info
247
+ res.json(result);
248
+ }
249
+ } catch (error) {
250
+ res.status(500).json({ error: error.message });
251
+ }
252
+ });
253
+
254
+ // Rename project endpoint
255
+ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
256
+ try {
257
+ const { displayName } = req.body;
258
+ await renameProject(req.params.projectName, displayName);
259
+ res.json({ success: true });
260
+ } catch (error) {
261
+ res.status(500).json({ error: error.message });
262
+ }
263
+ });
264
+
265
+ // Delete session endpoint
266
+ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
267
+ try {
268
+ const { projectName, sessionId } = req.params;
269
+ await deleteSession(projectName, sessionId);
270
+ res.json({ success: true });
271
+ } catch (error) {
272
+ res.status(500).json({ error: error.message });
273
+ }
274
+ });
275
+
276
+ // Delete project endpoint (only if empty)
277
+ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
278
+ try {
279
+ const { projectName } = req.params;
280
+ await deleteProject(projectName);
281
+ res.json({ success: true });
282
+ } catch (error) {
283
+ res.status(500).json({ error: error.message });
284
+ }
285
+ });
286
+
287
+ // Create project endpoint
288
+ app.post('/api/projects/create', authenticateToken, async (req, res) => {
289
+ try {
290
+ const { path: projectPath } = req.body;
291
+
292
+ if (!projectPath || !projectPath.trim()) {
293
+ return res.status(400).json({ error: 'Project path is required' });
294
+ }
295
+
296
+ const project = await addProjectManually(projectPath.trim());
297
+ res.json({ success: true, project });
298
+ } catch (error) {
299
+ console.error('Error creating project:', error);
300
+ res.status(500).json({ error: error.message });
301
+ }
302
+ });
303
+
304
+ // Browse filesystem endpoint for project suggestions - uses existing getFileTree
305
+ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
306
+ try {
307
+ const { path: dirPath } = req.query;
308
+
309
+ // Default to home directory if no path provided
310
+ const homeDir = os.homedir();
311
+ let targetPath = dirPath ? dirPath.replace('~', homeDir) : homeDir;
312
+
313
+ // Resolve and normalize the path
314
+ targetPath = path.resolve(targetPath);
315
+
316
+ // Security check - ensure path is accessible
317
+ try {
318
+ await fs.promises.access(targetPath);
319
+ const stats = await fs.promises.stat(targetPath);
320
+
321
+ if (!stats.isDirectory()) {
322
+ return res.status(400).json({ error: 'Path is not a directory' });
323
+ }
324
+ } catch (err) {
325
+ return res.status(404).json({ error: 'Directory not accessible' });
326
+ }
327
+
328
+ // Use existing getFileTree function with shallow depth (only direct children)
329
+ const fileTree = await getFileTree(targetPath, 1, 0, false); // maxDepth=1, showHidden=false
330
+
331
+ // Filter only directories and format for suggestions
332
+ const directories = fileTree
333
+ .filter(item => item.type === 'directory')
334
+ .map(item => ({
335
+ path: item.path,
336
+ name: item.name,
337
+ type: 'directory'
338
+ }))
339
+ .slice(0, 20); // Limit results
340
+
341
+ // Add common directories if browsing home directory
342
+ const suggestions = [];
343
+ if (targetPath === homeDir) {
344
+ const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace'];
345
+ const existingCommon = directories.filter(dir => commonDirs.includes(dir.name));
346
+ const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name));
347
+
348
+ suggestions.push(...existingCommon, ...otherDirs);
349
+ } else {
350
+ suggestions.push(...directories);
351
+ }
352
+
353
+ res.json({
354
+ path: targetPath,
355
+ suggestions: suggestions
356
+ });
357
+
358
+ } catch (error) {
359
+ console.error('Error browsing filesystem:', error);
360
+ res.status(500).json({ error: 'Failed to browse filesystem' });
361
+ }
362
+ });
363
+
364
+ // Read file content endpoint
365
+ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
366
+ try {
367
+ const { projectName } = req.params;
368
+ const { filePath } = req.query;
369
+
370
+ console.log('📄 File read request:', projectName, filePath);
371
+
372
+ // Using fsPromises from import
373
+
374
+ // Security check - ensure the path is safe and absolute
375
+ if (!filePath || !path.isAbsolute(filePath)) {
376
+ return res.status(400).json({ error: 'Invalid file path' });
377
+ }
378
+
379
+ const content = await fsPromises.readFile(filePath, 'utf8');
380
+ res.json({ content, path: filePath });
381
+ } catch (error) {
382
+ console.error('Error reading file:', error);
383
+ if (error.code === 'ENOENT') {
384
+ res.status(404).json({ error: 'File not found' });
385
+ } else if (error.code === 'EACCES') {
386
+ res.status(403).json({ error: 'Permission denied' });
387
+ } else {
388
+ res.status(500).json({ error: error.message });
389
+ }
390
+ }
391
+ });
392
+
393
+ // Serve binary file content endpoint (for images, etc.)
394
+ app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
395
+ try {
396
+ const { projectName } = req.params;
397
+ const { path: filePath } = req.query;
398
+
399
+ console.log('🖼️ Binary file serve request:', projectName, filePath);
400
+
401
+ // Using fs from import
402
+ // Using mime from import
403
+
404
+ // Security check - ensure the path is safe and absolute
405
+ if (!filePath || !path.isAbsolute(filePath)) {
406
+ return res.status(400).json({ error: 'Invalid file path' });
407
+ }
408
+
409
+ // Check if file exists
410
+ try {
411
+ await fsPromises.access(filePath);
412
+ } catch (error) {
413
+ return res.status(404).json({ error: 'File not found' });
414
+ }
415
+
416
+ // Get file extension and set appropriate content type
417
+ const mimeType = mime.lookup(filePath) || 'application/octet-stream';
418
+ res.setHeader('Content-Type', mimeType);
419
+
420
+ // Stream the file
421
+ const fileStream = fs.createReadStream(filePath);
422
+ fileStream.pipe(res);
423
+
424
+ fileStream.on('error', (error) => {
425
+ console.error('Error streaming file:', error);
426
+ if (!res.headersSent) {
427
+ res.status(500).json({ error: 'Error reading file' });
428
+ }
429
+ });
430
+
431
+ } catch (error) {
432
+ console.error('Error serving binary file:', error);
433
+ if (!res.headersSent) {
434
+ res.status(500).json({ error: error.message });
435
+ }
436
+ }
437
+ });
438
+
439
+ // Save file content endpoint
440
+ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
441
+ try {
442
+ const { projectName } = req.params;
443
+ const { filePath, content } = req.body;
444
+
445
+ console.log('💾 File save request:', projectName, filePath);
446
+
447
+ // Using fsPromises from import
448
+
449
+ // Security check - ensure the path is safe and absolute
450
+ if (!filePath || !path.isAbsolute(filePath)) {
451
+ return res.status(400).json({ error: 'Invalid file path' });
452
+ }
453
+
454
+ if (content === undefined) {
455
+ return res.status(400).json({ error: 'Content is required' });
456
+ }
457
+
458
+ // Create backup of original file
459
+ try {
460
+ const backupPath = filePath + '.backup.' + Date.now();
461
+ await fsPromises.copyFile(filePath, backupPath);
462
+ console.log('📋 Created backup:', backupPath);
463
+ } catch (backupError) {
464
+ console.warn('Could not create backup:', backupError.message);
465
+ }
466
+
467
+ // Write the new content
468
+ await fsPromises.writeFile(filePath, content, 'utf8');
469
+
470
+ res.json({
471
+ success: true,
472
+ path: filePath,
473
+ message: 'File saved successfully'
474
+ });
475
+ } catch (error) {
476
+ console.error('Error saving file:', error);
477
+ if (error.code === 'ENOENT') {
478
+ res.status(404).json({ error: 'File or directory not found' });
479
+ } else if (error.code === 'EACCES') {
480
+ res.status(403).json({ error: 'Permission denied' });
481
+ } else {
482
+ res.status(500).json({ error: error.message });
483
+ }
484
+ }
485
+ });
486
+
487
+ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
488
+ try {
489
+
490
+ // Using fsPromises from import
491
+
492
+ // Use extractProjectDirectory to get the actual project path
493
+ let actualPath;
494
+ try {
495
+ actualPath = await extractProjectDirectory(req.params.projectName);
496
+ } catch (error) {
497
+ console.error('Error extracting project directory:', error);
498
+ // Fallback to simple dash replacement
499
+ actualPath = req.params.projectName.replace(/-/g, '/');
500
+ }
501
+
502
+ // Check if path exists
503
+ try {
504
+ await fsPromises.access(actualPath);
505
+ } catch (e) {
506
+ return res.status(404).json({ error: `Project path not found: ${actualPath}` });
507
+ }
508
+
509
+ const files = await getFileTree(actualPath, 10, 0, true);
510
+ const hiddenFiles = files.filter(f => f.name.startsWith('.'));
511
+ res.json(files);
512
+ } catch (error) {
513
+ console.error('❌ File tree error:', error.message);
514
+ res.status(500).json({ error: error.message });
515
+ }
516
+ });
517
+
518
+ // WebSocket connection handler that routes based on URL path
519
+ wss.on('connection', (ws, request) => {
520
+ const url = request.url;
521
+ console.log('🔗 Client connected to:', url);
522
+
523
+ // Parse URL to get pathname without query parameters
524
+ const urlObj = new URL(url, 'http://localhost');
525
+ const pathname = urlObj.pathname;
526
+
527
+ if (pathname === '/shell') {
528
+ handleShellConnection(ws);
529
+ } else if (pathname === '/ws') {
530
+ handleChatConnection(ws);
531
+ } else {
532
+ console.log('❌ Unknown WebSocket path:', pathname);
533
+ ws.close();
534
+ }
535
+ });
536
+
537
+ // Handle chat WebSocket connections
538
+ function handleChatConnection(ws) {
539
+ console.log('💬 Chat WebSocket connected');
540
+
541
+ // Add to connected clients for project updates
542
+ connectedClients.add(ws);
543
+
544
+ ws.on('message', async (message) => {
545
+ try {
546
+ const data = JSON.parse(message);
547
+
548
+ if (data.type === 'claude-command') {
549
+ console.log('💬 User message:', data.command || '[Continue/Resume]');
550
+ console.log('📁 Project:', data.options?.projectPath || 'Unknown');
551
+ console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
552
+ await spawnClaude(data.command, data.options, ws);
553
+ } else if (data.type === 'cursor-command') {
554
+ console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]');
555
+ console.log('📁 Project:', data.options?.cwd || 'Unknown');
556
+ console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
557
+ console.log('🤖 Model:', data.options?.model || 'default');
558
+ await spawnCursor(data.command, data.options, ws);
559
+ } else if (data.type === 'cursor-resume') {
560
+ // Backward compatibility: treat as cursor-command with resume and no prompt
561
+ console.log('🖱️ Cursor resume session (compat):', data.sessionId);
562
+ await spawnCursor('', {
563
+ sessionId: data.sessionId,
564
+ resume: true,
565
+ cwd: data.options?.cwd
566
+ }, ws);
567
+ } else if (data.type === 'abort-session') {
568
+ console.log('🛑 Abort session request:', data.sessionId);
569
+ const provider = data.provider || 'claude';
570
+ const success = provider === 'cursor'
571
+ ? abortCursorSession(data.sessionId)
572
+ : abortClaudeSession(data.sessionId);
573
+ ws.send(JSON.stringify({
574
+ type: 'session-aborted',
575
+ sessionId: data.sessionId,
576
+ provider,
577
+ success
578
+ }));
579
+ } else if (data.type === 'cursor-abort') {
580
+ console.log('🛑 Abort Cursor session:', data.sessionId);
581
+ const success = abortCursorSession(data.sessionId);
582
+ ws.send(JSON.stringify({
583
+ type: 'session-aborted',
584
+ sessionId: data.sessionId,
585
+ provider: 'cursor',
586
+ success
587
+ }));
588
+ }
589
+ } catch (error) {
590
+ console.error('❌ Chat WebSocket error:', error.message);
591
+ ws.send(JSON.stringify({
592
+ type: 'error',
593
+ error: error.message
594
+ }));
595
+ }
596
+ });
597
+
598
+ ws.on('close', () => {
599
+ console.log('🔌 Chat client disconnected');
600
+ // Remove from connected clients
601
+ connectedClients.delete(ws);
602
+ });
603
+ }
604
+
605
+ // Handle shell WebSocket connections
606
+ function handleShellConnection(ws) {
607
+ console.log('🐚 Shell client connected');
608
+ let shellProcess = null;
609
+
610
+ ws.on('message', async (message) => {
611
+ try {
612
+ const data = JSON.parse(message);
613
+ console.log('📨 Shell message received:', data.type);
614
+
615
+ if (data.type === 'init') {
616
+ // Initialize shell with project path and session info
617
+ const projectPath = data.projectPath || process.cwd();
618
+ const sessionId = data.sessionId;
619
+ const hasSession = data.hasSession;
620
+ const provider = data.provider || 'claude';
621
+ const initialCommand = data.initialCommand;
622
+ const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
623
+
624
+ console.log('🚀 Starting shell in:', projectPath);
625
+ console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
626
+ console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
627
+ if (initialCommand) {
628
+ console.log('⚡ Initial command:', initialCommand);
629
+ }
630
+
631
+ // First send a welcome message
632
+ let welcomeMsg;
633
+ if (isPlainShell) {
634
+ welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
635
+ } else {
636
+ const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
637
+ welcomeMsg = hasSession ?
638
+ `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
639
+ `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
640
+ }
641
+
642
+ ws.send(JSON.stringify({
643
+ type: 'output',
644
+ data: welcomeMsg
645
+ }));
646
+
647
+ try {
648
+ // Prepare the shell command adapted to the platform and provider
649
+ let shellCommand;
650
+ if (isPlainShell) {
651
+ // Plain shell mode - just run the initial command in the project directory
652
+ if (os.platform() === 'win32') {
653
+ shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
654
+ } else {
655
+ shellCommand = `cd "${projectPath}" && ${initialCommand}`;
656
+ }
657
+ } else if (provider === 'cursor') {
658
+ // Use cursor-agent command
659
+ if (os.platform() === 'win32') {
660
+ if (hasSession && sessionId) {
661
+ shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent --resume="${sessionId}"`;
662
+ } else {
663
+ shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent`;
664
+ }
665
+ } else {
666
+ if (hasSession && sessionId) {
667
+ shellCommand = `cd "${projectPath}" && cursor-agent --resume="${sessionId}"`;
668
+ } else {
669
+ shellCommand = `cd "${projectPath}" && cursor-agent`;
670
+ }
671
+ }
672
+ } else {
673
+ // Use claude command (default) or initialCommand if provided
674
+ const command = initialCommand || 'claude';
675
+ if (os.platform() === 'win32') {
676
+ if (hasSession && sessionId) {
677
+ // Try to resume session, but with fallback to new session if it fails
678
+ shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`;
679
+ } else {
680
+ shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
681
+ }
682
+ } else {
683
+ if (hasSession && sessionId) {
684
+ shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`;
685
+ } else {
686
+ shellCommand = `cd "${projectPath}" && ${command}`;
687
+ }
688
+ }
689
+ }
690
+
691
+ console.log('🔧 Executing shell command:', shellCommand);
692
+
693
+ // Use appropriate shell based on platform
694
+ const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
695
+ const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
696
+
697
+ shellProcess = pty.spawn(shell, shellArgs, {
698
+ name: 'xterm-256color',
699
+ cols: 80,
700
+ rows: 24,
701
+ cwd: process.env.HOME || (os.platform() === 'win32' ? process.env.USERPROFILE : '/'),
702
+ env: {
703
+ ...process.env,
704
+ TERM: 'xterm-256color',
705
+ COLORTERM: 'truecolor',
706
+ FORCE_COLOR: '3',
707
+ // Override browser opening commands to echo URL for detection
708
+ BROWSER: os.platform() === 'win32' ? 'echo "OPEN_URL:"' : 'echo "OPEN_URL:"'
709
+ }
710
+ });
711
+
712
+ console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
713
+
714
+ // Handle data output
715
+ shellProcess.onData((data) => {
716
+ if (ws.readyState === ws.OPEN) {
717
+ let outputData = data;
718
+
719
+ // Check for various URL opening patterns
720
+ const patterns = [
721
+ // Direct browser opening commands
722
+ /(?:xdg-open|open|start)\s+(https?:\/\/[^\s\x1b\x07]+)/g,
723
+ // BROWSER environment variable override
724
+ /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
725
+ // Git and other tools opening URLs
726
+ /Opening\s+(https?:\/\/[^\s\x1b\x07]+)/gi,
727
+ // General URL patterns that might be opened
728
+ /Visit:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
729
+ /View at:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
730
+ /Browse to:\s*(https?:\/\/[^\s\x1b\x07]+)/gi
731
+ ];
732
+
733
+ patterns.forEach(pattern => {
734
+ let match;
735
+ while ((match = pattern.exec(data)) !== null) {
736
+ const url = match[1];
737
+ console.log('🔗 Detected URL for opening:', url);
738
+
739
+ // Send URL opening message to client
740
+ ws.send(JSON.stringify({
741
+ type: 'url_open',
742
+ url: url
743
+ }));
744
+
745
+ // Replace the OPEN_URL pattern with a user-friendly message
746
+ if (pattern.source.includes('OPEN_URL')) {
747
+ outputData = outputData.replace(match[0], `🌐 Opening in browser: ${url}`);
748
+ }
749
+ }
750
+ });
751
+
752
+ // Send regular output
753
+ ws.send(JSON.stringify({
754
+ type: 'output',
755
+ data: outputData
756
+ }));
757
+ }
758
+ });
759
+
760
+ // Handle process exit
761
+ shellProcess.onExit((exitCode) => {
762
+ console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
763
+ if (ws.readyState === ws.OPEN) {
764
+ ws.send(JSON.stringify({
765
+ type: 'output',
766
+ data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
767
+ }));
768
+ }
769
+ shellProcess = null;
770
+ });
771
+
772
+ } catch (spawnError) {
773
+ console.error('❌ Error spawning process:', spawnError);
774
+ ws.send(JSON.stringify({
775
+ type: 'output',
776
+ data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
777
+ }));
778
+ }
779
+
780
+ } else if (data.type === 'input') {
781
+ // Send input to shell process
782
+ if (shellProcess && shellProcess.write) {
783
+ try {
784
+ shellProcess.write(data.data);
785
+ } catch (error) {
786
+ console.error('Error writing to shell:', error);
787
+ }
788
+ } else {
789
+ console.warn('No active shell process to send input to');
790
+ }
791
+ } else if (data.type === 'resize') {
792
+ // Handle terminal resize
793
+ if (shellProcess && shellProcess.resize) {
794
+ console.log('Terminal resize requested:', data.cols, 'x', data.rows);
795
+ shellProcess.resize(data.cols, data.rows);
796
+ }
797
+ }
798
+ } catch (error) {
799
+ console.error('❌ Shell WebSocket error:', error.message);
800
+ if (ws.readyState === ws.OPEN) {
801
+ ws.send(JSON.stringify({
802
+ type: 'output',
803
+ data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
804
+ }));
805
+ }
806
+ }
807
+ });
808
+
809
+ ws.on('close', () => {
810
+ console.log('🔌 Shell client disconnected');
811
+ if (shellProcess && shellProcess.kill) {
812
+ console.log('🔴 Killing shell process:', shellProcess.pid);
813
+ shellProcess.kill();
814
+ }
815
+ });
816
+
817
+ ws.on('error', (error) => {
818
+ console.error('❌ Shell WebSocket error:', error);
819
+ });
820
+ }
821
+ // Audio transcription endpoint
822
+ app.post('/api/transcribe', authenticateToken, async (req, res) => {
823
+ try {
824
+ const multer = (await import('multer')).default;
825
+ const upload = multer({ storage: multer.memoryStorage() });
826
+
827
+ // Handle multipart form data
828
+ upload.single('audio')(req, res, async (err) => {
829
+ if (err) {
830
+ return res.status(400).json({ error: 'Failed to process audio file' });
831
+ }
832
+
833
+ if (!req.file) {
834
+ return res.status(400).json({ error: 'No audio file provided' });
835
+ }
836
+
837
+ const apiKey = process.env.OPENAI_API_KEY;
838
+ if (!apiKey) {
839
+ return res.status(500).json({ error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in server environment.' });
840
+ }
841
+
842
+ try {
843
+ // Create form data for OpenAI
844
+ const FormData = (await import('form-data')).default;
845
+ const formData = new FormData();
846
+ formData.append('file', req.file.buffer, {
847
+ filename: req.file.originalname,
848
+ contentType: req.file.mimetype
849
+ });
850
+ formData.append('model', 'whisper-1');
851
+ formData.append('response_format', 'json');
852
+ formData.append('language', 'en');
853
+
854
+ // Make request to OpenAI
855
+ const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
856
+ method: 'POST',
857
+ headers: {
858
+ 'Authorization': `Bearer ${apiKey}`,
859
+ ...formData.getHeaders()
860
+ },
861
+ body: formData
862
+ });
863
+
864
+ if (!response.ok) {
865
+ const errorData = await response.json().catch(() => ({}));
866
+ throw new Error(errorData.error?.message || `Whisper API error: ${response.status}`);
867
+ }
868
+
869
+ const data = await response.json();
870
+ let transcribedText = data.text || '';
871
+
872
+ // Check if enhancement mode is enabled
873
+ const mode = req.body.mode || 'default';
874
+
875
+ // If no transcribed text, return empty
876
+ if (!transcribedText) {
877
+ return res.json({ text: '' });
878
+ }
879
+
880
+ // If default mode, return transcribed text without enhancement
881
+ if (mode === 'default') {
882
+ return res.json({ text: transcribedText });
883
+ }
884
+
885
+ // Handle different enhancement modes
886
+ try {
887
+ const OpenAI = (await import('openai')).default;
888
+ const openai = new OpenAI({ apiKey });
889
+
890
+ let prompt, systemMessage, temperature = 0.7, maxTokens = 800;
891
+
892
+ switch (mode) {
893
+ case 'prompt':
894
+ systemMessage = 'You are an expert prompt engineer who creates clear, detailed, and effective prompts.';
895
+ prompt = `You are an expert prompt engineer. Transform the following rough instruction into a clear, detailed, and context-aware AI prompt.
896
+
897
+ Your enhanced prompt should:
898
+ 1. Be specific and unambiguous
899
+ 2. Include relevant context and constraints
900
+ 3. Specify the desired output format
901
+ 4. Use clear, actionable language
902
+ 5. Include examples where helpful
903
+ 6. Consider edge cases and potential ambiguities
904
+
905
+ Transform this rough instruction into a well-crafted prompt:
906
+ "${transcribedText}"
907
+
908
+ Enhanced prompt:`;
909
+ break;
910
+
911
+ case 'vibe':
912
+ case 'instructions':
913
+ case 'architect':
914
+ systemMessage = 'You are a helpful assistant that formats ideas into clear, actionable instructions for AI agents.';
915
+ temperature = 0.5; // Lower temperature for more controlled output
916
+ prompt = `Transform the following idea into clear, well-structured instructions that an AI agent can easily understand and execute.
917
+
918
+ IMPORTANT RULES:
919
+ - Format as clear, step-by-step instructions
920
+ - Add reasonable implementation details based on common patterns
921
+ - Only include details directly related to what was asked
922
+ - Do NOT add features or functionality not mentioned
923
+ - Keep the original intent and scope intact
924
+ - Use clear, actionable language an agent can follow
925
+
926
+ Transform this idea into agent-friendly instructions:
927
+ "${transcribedText}"
928
+
929
+ Agent instructions:`;
930
+ break;
931
+
932
+ default:
933
+ // No enhancement needed
934
+ break;
935
+ }
936
+
937
+ // Only make GPT call if we have a prompt
938
+ if (prompt) {
939
+ const completion = await openai.chat.completions.create({
940
+ model: 'gpt-4o-mini',
941
+ messages: [
942
+ { role: 'system', content: systemMessage },
943
+ { role: 'user', content: prompt }
944
+ ],
945
+ temperature: temperature,
946
+ max_tokens: maxTokens
947
+ });
948
+
949
+ transcribedText = completion.choices[0].message.content || transcribedText;
950
+ }
951
+
952
+ } catch (gptError) {
953
+ console.error('GPT processing error:', gptError);
954
+ // Fall back to original transcription if GPT fails
955
+ }
956
+
957
+ res.json({ text: transcribedText });
958
+
959
+ } catch (error) {
960
+ console.error('Transcription error:', error);
961
+ res.status(500).json({ error: error.message });
962
+ }
963
+ });
964
+ } catch (error) {
965
+ console.error('Endpoint error:', error);
966
+ res.status(500).json({ error: 'Internal server error' });
967
+ }
968
+ });
969
+
970
+ // Image upload endpoint
971
+ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
972
+ try {
973
+ const multer = (await import('multer')).default;
974
+ const path = (await import('path')).default;
975
+ const fs = (await import('fs')).promises;
976
+ const os = (await import('os')).default;
977
+
978
+ // Configure multer for image uploads
979
+ const storage = multer.diskStorage({
980
+ destination: async (req, file, cb) => {
981
+ const uploadDir = path.join(os.tmpdir(), 'claude-ui-uploads', String(req.user.id));
982
+ await fs.mkdir(uploadDir, { recursive: true });
983
+ cb(null, uploadDir);
984
+ },
985
+ filename: (req, file, cb) => {
986
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
987
+ const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
988
+ cb(null, uniqueSuffix + '-' + sanitizedName);
989
+ }
990
+ });
991
+
992
+ const fileFilter = (req, file, cb) => {
993
+ const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
994
+ if (allowedMimes.includes(file.mimetype)) {
995
+ cb(null, true);
996
+ } else {
997
+ cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
998
+ }
999
+ };
1000
+
1001
+ const upload = multer({
1002
+ storage,
1003
+ fileFilter,
1004
+ limits: {
1005
+ fileSize: 5 * 1024 * 1024, // 5MB
1006
+ files: 5
1007
+ }
1008
+ });
1009
+
1010
+ // Handle multipart form data
1011
+ upload.array('images', 5)(req, res, async (err) => {
1012
+ if (err) {
1013
+ return res.status(400).json({ error: err.message });
1014
+ }
1015
+
1016
+ if (!req.files || req.files.length === 0) {
1017
+ return res.status(400).json({ error: 'No image files provided' });
1018
+ }
1019
+
1020
+ try {
1021
+ // Process uploaded images
1022
+ const processedImages = await Promise.all(
1023
+ req.files.map(async (file) => {
1024
+ // Read file and convert to base64
1025
+ const buffer = await fs.readFile(file.path);
1026
+ const base64 = buffer.toString('base64');
1027
+ const mimeType = file.mimetype;
1028
+
1029
+ // Clean up temp file immediately
1030
+ await fs.unlink(file.path);
1031
+
1032
+ return {
1033
+ name: file.originalname,
1034
+ data: `data:${mimeType};base64,${base64}`,
1035
+ size: file.size,
1036
+ mimeType: mimeType
1037
+ };
1038
+ })
1039
+ );
1040
+
1041
+ res.json({ images: processedImages });
1042
+ } catch (error) {
1043
+ console.error('Error processing images:', error);
1044
+ // Clean up any remaining files
1045
+ await Promise.all(req.files.map(f => fs.unlink(f.path).catch(() => { })));
1046
+ res.status(500).json({ error: 'Failed to process images' });
1047
+ }
1048
+ });
1049
+ } catch (error) {
1050
+ console.error('Error in image upload endpoint:', error);
1051
+ res.status(500).json({ error: 'Internal server error' });
1052
+ }
1053
+ });
1054
+
1055
+ // Serve React app for all other routes
1056
+ app.get('*', (req, res) => {
1057
+ if (process.env.NODE_ENV === 'production') {
1058
+ res.sendFile(path.join(__dirname, '../dist/index.html'));
1059
+ } else {
1060
+ // In development, redirect to Vite dev server
1061
+ res.redirect(`http://localhost:${process.env.VITE_PORT || 3001}`);
1062
+ }
1063
+ });
1064
+
1065
+ // Helper function to convert permissions to rwx format
1066
+ function permToRwx(perm) {
1067
+ const r = perm & 4 ? 'r' : '-';
1068
+ const w = perm & 2 ? 'w' : '-';
1069
+ const x = perm & 1 ? 'x' : '-';
1070
+ return r + w + x;
1071
+ }
1072
+
1073
+ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
1074
+ // Using fsPromises from import
1075
+ const items = [];
1076
+
1077
+ try {
1078
+ const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
1079
+
1080
+ for (const entry of entries) {
1081
+ // Debug: log all entries including hidden files
1082
+
1083
+
1084
+ // Skip only heavy build directories
1085
+ if (entry.name === 'node_modules' ||
1086
+ entry.name === 'dist' ||
1087
+ entry.name === 'build') continue;
1088
+
1089
+ const itemPath = path.join(dirPath, entry.name);
1090
+ const item = {
1091
+ name: entry.name,
1092
+ path: itemPath,
1093
+ type: entry.isDirectory() ? 'directory' : 'file'
1094
+ };
1095
+
1096
+ // Get file stats for additional metadata
1097
+ try {
1098
+ const stats = await fsPromises.stat(itemPath);
1099
+ item.size = stats.size;
1100
+ item.modified = stats.mtime.toISOString();
1101
+
1102
+ // Convert permissions to rwx format
1103
+ const mode = stats.mode;
1104
+ const ownerPerm = (mode >> 6) & 7;
1105
+ const groupPerm = (mode >> 3) & 7;
1106
+ const otherPerm = mode & 7;
1107
+ item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
1108
+ item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
1109
+ } catch (statError) {
1110
+ // If stat fails, provide default values
1111
+ item.size = 0;
1112
+ item.modified = null;
1113
+ item.permissions = '000';
1114
+ item.permissionsRwx = '---------';
1115
+ }
1116
+
1117
+ if (entry.isDirectory() && currentDepth < maxDepth) {
1118
+ // Recursively get subdirectories but limit depth
1119
+ try {
1120
+ // Check if we can access the directory before trying to read it
1121
+ await fsPromises.access(item.path, fs.constants.R_OK);
1122
+ item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
1123
+ } catch (e) {
1124
+ // Silently skip directories we can't access (permission denied, etc.)
1125
+ item.children = [];
1126
+ }
1127
+ }
1128
+
1129
+ items.push(item);
1130
+ }
1131
+ } catch (error) {
1132
+ // Only log non-permission errors to avoid spam
1133
+ if (error.code !== 'EACCES' && error.code !== 'EPERM') {
1134
+ console.error('Error reading directory:', error);
1135
+ }
1136
+ }
1137
+
1138
+ return items.sort((a, b) => {
1139
+ if (a.type !== b.type) {
1140
+ return a.type === 'directory' ? -1 : 1;
1141
+ }
1142
+ return a.name.localeCompare(b.name);
1143
+ });
1144
+ }
1145
+
1146
+ const PORT = process.env.PORT || 3001;
1147
+
1148
+ // Initialize database and start server
1149
+ async function startServer() {
1150
+ try {
1151
+ // Initialize authentication database
1152
+ await initializeDatabase();
1153
+ console.log('✅ Database initialization skipped (testing)');
1154
+
1155
+ server.listen(PORT, '0.0.0.0', async () => {
1156
+ console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
1157
+
1158
+ // Start watching the projects folder for changes
1159
+ await setupProjectsWatcher();
1160
+ });
1161
+ } catch (error) {
1162
+ console.error('❌ Failed to start server:', error);
1163
+ process.exit(1);
1164
+ }
1165
+ }
1166
+
1167
+ startServer();