UNPKG

claude-code-web

Version:

Web-based interface for Claude Code CLI accessible via browser

217 lines (187 loc) 5.95 kB
const { spawn } = require('node-pty'); const path = require('path'); const fs = require('fs'); class CodexBridge { constructor() { this.sessions = new Map(); this.codexCommand = this.findCodexCommand(); } findCodexCommand() { const possibleCommands = [ path.join(process.env.HOME || '/', '.codex', 'local', 'codex'), 'codex', 'codex-code', path.join(process.env.HOME || '/', '.local', 'bin', 'codex'), '/usr/local/bin/codex', '/usr/bin/codex' ]; for (const cmd of possibleCommands) { try { if (fs.existsSync(cmd) || this.commandExists(cmd)) { console.log(`Found Codex command at: ${cmd}`); return cmd; } } catch (error) { continue; } } console.error('Codex command not found, using default "codex"'); return 'codex'; } commandExists(command) { try { require('child_process').execFileSync('which', [command], { stdio: 'ignore' }); return true; } catch (error) { return false; } } async startSession(sessionId, options = {}) { if (this.sessions.has(sessionId)) { throw new Error(`Session ${sessionId} already exists`); } const { workingDir = process.cwd(), dangerouslySkipPermissions = false, onOutput = () => {}, onExit = () => {}, onError = () => {}, cols = 80, rows = 24 } = options; try { console.log(`Starting Codex session ${sessionId}`); console.log(`Command: ${this.codexCommand}`); console.log(`Working directory: ${workingDir}`); console.log(`Terminal size: ${cols}x${rows}`); if (dangerouslySkipPermissions) { console.log(`⚠️ WARNING: Bypassing approvals and sandbox with --dangerously-bypass-approvals-and-sandbox flag`); } const args = dangerouslySkipPermissions ? ['--dangerously-bypass-approvals-and-sandbox'] : []; const codexProcess = spawn(this.codexCommand, args, { cwd: workingDir, env: { ...process.env, TERM: 'xterm-256color', FORCE_COLOR: '1', COLORTERM: 'truecolor' }, cols, rows, name: 'xterm-color' }); const session = { process: codexProcess, workingDir, created: new Date(), active: true, killTimeout: null }; this.sessions.set(sessionId, session); let dataBuffer = ''; codexProcess.onData((data) => { if (process.env.DEBUG) { console.log(`Codex session ${sessionId} output:`, data); } // Keep a small rolling buffer to detect prompts if needed later dataBuffer += data; if (dataBuffer.length > 10000) { dataBuffer = dataBuffer.slice(-5000); } onOutput(data); }); codexProcess.onExit((exitCode, signal) => { console.log(`Codex session ${sessionId} exited with code ${exitCode}, signal ${signal}`); // Clear kill timeout if process exited naturally if (session.killTimeout) { clearTimeout(session.killTimeout); session.killTimeout = null; } session.active = false; this.sessions.delete(sessionId); onExit(exitCode, signal); }); codexProcess.on('error', (error) => { console.error(`Codex session ${sessionId} error:`, error); // Clear kill timeout if process errored if (session.killTimeout) { clearTimeout(session.killTimeout); session.killTimeout = null; } session.active = false; this.sessions.delete(sessionId); onError(error); }); console.log(`Codex session ${sessionId} started successfully`); return session; } catch (error) { console.error(`Failed to start Codex session ${sessionId}:`, error); throw new Error(`Failed to start Codex Code: ${error.message}`); } } async sendInput(sessionId, data) { const session = this.sessions.get(sessionId); if (!session || !session.active) { throw new Error(`Session ${sessionId} not found or not active`); } try { session.process.write(data); } catch (error) { throw new Error(`Failed to send input to session ${sessionId}: ${error.message}`); } } async resize(sessionId, cols, rows) { const session = this.sessions.get(sessionId); if (!session || !session.active) { throw new Error(`Session ${sessionId} not found or not active`); } try { session.process.resize(cols, rows); } catch (error) { console.warn(`Failed to resize session ${sessionId}:`, error.message); } } async stopSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return; } try { // Clear any existing kill timeout if (session.killTimeout) { clearTimeout(session.killTimeout); session.killTimeout = null; } if (session.active && session.process) { session.process.kill('SIGTERM'); session.killTimeout = setTimeout(() => { if (session.active && session.process) { session.process.kill('SIGKILL'); } }, 5000); } } catch (error) { console.warn(`Error stopping codex session ${sessionId}:`, error.message); } session.active = false; this.sessions.delete(sessionId); } getSession(sessionId) { return this.sessions.get(sessionId); } getAllSessions() { return Array.from(this.sessions.entries()).map(([id, session]) => ({ id, workingDir: session.workingDir, created: session.created, active: session.active })); } async cleanup() { const sessionIds = Array.from(this.sessions.keys()); for (const sessionId of sessionIds) { await this.stopSession(sessionId); } } } module.exports = CodexBridge;