UNPKG

@microwiseai/claude-web-server

Version:

Local server for Web Claude Code - Unofficial web interface for Claude Code

294 lines (293 loc) 12.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const http_1 = require("http"); const socket_io_1 = require("socket.io"); const cors_1 = __importDefault(require("cors")); const pty = __importStar(require("node-pty")); const child_process_1 = require("child_process"); const path = __importStar(require("path")); const fs = __importStar(require("fs")); const app = (0, express_1.default)(); const httpServer = (0, http_1.createServer)(app); const io = new socket_io_1.Server(httpServer, { cors: { origin: [ "http://localhost:5173", // Development "http://localhost:3001", // Local production "https://claude.microwiseai.com", // Production domain "https://*.microwiseai.com", // All subdomains // Temporary for development/testing "http://localhost:3000", "http://localhost:4173" ], methods: ["GET", "POST"], credentials: true } }); app.use((0, cors_1.default)()); app.use(express_1.default.json()); // Serve static files in production if (process.env.NODE_ENV === 'production' || process.argv.includes('--daemon')) { const staticPath = path.join(__dirname, '../../dist'); app.use(express_1.default.static(staticPath)); app.get('/', (req, res) => { res.sendFile(path.join(staticPath, 'index.html')); }); } // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }); const sessions = new Map(); // Function to cleanup tmux settings from VS Code settings.json function cleanupTmuxSettings(settingsPath) { try { if (!fs.existsSync(settingsPath)) return; const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); const metadata = settings['__tmux_session_temp__']; if (!metadata) return; // Restore original settings const { originalProfiles } = metadata; // Remove tmux-attach profile if (settings['terminal.integrated.profiles.linux']) { delete settings['terminal.integrated.profiles.linux']['tmux-attach']; if (Object.keys(settings['terminal.integrated.profiles.linux']).length === 0) { delete settings['terminal.integrated.profiles.linux']; } } if (settings['terminal.integrated.profiles.osx']) { delete settings['terminal.integrated.profiles.osx']['tmux-attach']; if (Object.keys(settings['terminal.integrated.profiles.osx']).length === 0) { delete settings['terminal.integrated.profiles.osx']; } } // Restore original default profiles if (originalProfiles.defaultLinux !== undefined) { settings['terminal.integrated.defaultProfile.linux'] = originalProfiles.defaultLinux; } else { delete settings['terminal.integrated.defaultProfile.linux']; } if (originalProfiles.defaultOsx !== undefined) { settings['terminal.integrated.defaultProfile.osx'] = originalProfiles.defaultOsx; } else { delete settings['terminal.integrated.defaultProfile.osx']; } // Remove metadata delete settings['__tmux_session_temp__']; // Write back cleaned settings fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); console.log('Cleaned up tmux settings from:', settingsPath); } catch (e) { console.error('Error cleaning up tmux settings:', e); } } io.on('connection', (socket) => { console.log('Client connected:', socket.id); socket.on('create-terminal', (data) => { const { workspacePath } = data; if (!fs.existsSync(workspacePath)) { socket.emit('error', { message: 'Workspace path does not exist' }); return; } // Create unique tmux session name const tmuxSessionName = `claude-web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Create tmux session with specific working directory const ptyProcess = pty.spawn('tmux', [ 'new-session', '-d', '-s', tmuxSessionName, '-c', workspacePath ], { name: 'xterm-color', cols: 80, rows: 30, cwd: workspacePath, env: process.env }); // Wait a bit for tmux to create the session, then attach to it setTimeout(() => { const attachProcess = pty.spawn('tmux', ['attach-session', '-t', tmuxSessionName], { name: 'xterm-color', cols: 80, rows: 30, env: process.env }); sessions.set(socket.id, { ptyProcess: attachProcess, workspacePath, tmuxSessionName }); attachProcess.onData((data) => { socket.emit('terminal-data', data); }); socket.emit('terminal-created', { sessionId: socket.id, tmuxSessionName }); }, 500); }); socket.on('terminal-input', (data) => { const session = sessions.get(socket.id); if (session) { session.ptyProcess.write(data); } }); socket.on('resize', (data) => { const session = sessions.get(socket.id); if (session) { session.ptyProcess.resize(data.cols, data.rows); } }); socket.on('start-claude-code', (data) => { const { workspacePath } = data; if (!fs.existsSync(workspacePath)) { socket.emit('error', { message: 'Workspace path does not exist' }); return; } // Start VS Code in the background const vscodeProcess = (0, child_process_1.spawn)('code', [workspacePath], { detached: true, stdio: 'ignore' }); vscodeProcess.unref(); // Wait a bit for VS Code to start, then start Claude Code setTimeout(() => { const session = sessions.get(socket.id); if (session) { session.ptyProcess.write('claude\r'); } }, 2000); socket.emit('vscode-started', { workspacePath }); }); socket.on('open-vscode', () => { const session = sessions.get(socket.id); if (session) { // Create a VS Code workspace settings file with terminal configuration const vscodeDir = path.join(session.workspacePath, '.vscode'); if (!fs.existsSync(vscodeDir)) { fs.mkdirSync(vscodeDir, { recursive: true }); } // Create settings.json with terminal auto-run command const settingsPath = path.join(vscodeDir, 'settings.json'); let settings = {}; let originalSettings = null; // Read existing settings if they exist if (fs.existsSync(settingsPath)) { try { const content = fs.readFileSync(settingsPath, 'utf8'); settings = JSON.parse(content); // Keep a copy of original settings for restoration originalSettings = JSON.parse(content); } catch (e) { console.error('Error reading settings.json:', e); } } // Store original terminal profile settings if they exist const originalProfiles = { linux: settings['terminal.integrated.profiles.linux'], osx: settings['terminal.integrated.profiles.osx'], windows: settings['terminal.integrated.profiles.windows'], defaultLinux: settings['terminal.integrated.defaultProfile.linux'], defaultOsx: settings['terminal.integrated.defaultProfile.osx'], defaultWindows: settings['terminal.integrated.defaultProfile.windows'] }; // Add tmux terminal profile if (!settings['terminal.integrated.profiles.linux']) { settings['terminal.integrated.profiles.linux'] = {}; } if (!settings['terminal.integrated.profiles.osx']) { settings['terminal.integrated.profiles.osx'] = {}; } // Add tmux attach profile settings['terminal.integrated.profiles.linux']['tmux-attach'] = { path: 'tmux', args: ['attach-session', '-t', session.tmuxSessionName] }; settings['terminal.integrated.profiles.osx']['tmux-attach'] = { path: 'tmux', args: ['attach-session', '-t', session.tmuxSessionName] }; // Set as default profile temporarily settings['terminal.integrated.defaultProfile.linux'] = 'tmux-attach'; settings['terminal.integrated.defaultProfile.osx'] = 'tmux-attach'; // Add metadata for cleanup settings['__tmux_session_temp__'] = { sessionName: session.tmuxSessionName, originalProfiles: originalProfiles, timestamp: new Date().toISOString() }; // Write settings fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); // Open VS Code with workspace const vscodeProcess = (0, child_process_1.spawn)('code', [session.workspacePath], { detached: true, stdio: 'ignore' }); vscodeProcess.unref(); // Schedule cleanup after VS Code has likely opened setTimeout(() => { cleanupTmuxSettings(settingsPath); }, 10000); // 10 seconds later socket.emit('vscode-opened', { workspacePath: session.workspacePath, tmuxSessionName: session.tmuxSessionName, attachCommand: `tmux attach-session -t ${session.tmuxSessionName}`, note: 'Terminal will automatically attach to tmux session' }); } }); socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); const session = sessions.get(socket.id); if (session) { // Kill the pty process session.ptyProcess.kill(); // Kill the tmux session (0, child_process_1.spawn)('tmux', ['kill-session', '-t', session.tmuxSessionName], { detached: true, stdio: 'ignore' }); sessions.delete(socket.id); } }); }); const PORT = process.env.PORT || 3001; httpServer.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });