@microwiseai/claude-web-server
Version:
Local server for Web Claude Code - Unofficial web interface for Claude Code
294 lines (293 loc) • 12.3 kB
JavaScript
;
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}`);
});