/** * Session Routes * * RESTful API endpoints for managing Claude Code sessions. * All routes are scoped to a specific session via :sessionId parameter. */ const express = require('express'); const eventBus = require('../services/event-bus'); const { validateSessionId, validateCommand, validateResponse, validateOperations } = require('../middleware/validation'); const router = express.Router(); // Middleware to ensure services are available router.use((req, res, next) => { if (!req.app.locals.claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } next(); }); /** * Send command/prompt to session * POST /api/session/:sessionId/prompt * * Sends a command or prompt to the Claude Code session. * The response will be streamed via SSE to connected clients. * * Request: * { * "command": "ls -la", * "context": { "optional": "context data" } * } * * Response: * { * "success": true, * "sessionId": "session-123", * "message": "Command sent", * "timestamp": 1234567890 * } */ router.post('/session/:sessionId/prompt', validateSessionId, validateCommand, async (req, res) => { const { sessionId } = req.params; const { command, context } = req.body; console.log(`[SessionRoutes] Sending command to session ${sessionId}: ${command.substring(0, 100)}...`); try { // Send command to Claude service await req.app.locals.claudeService.sendCommand(sessionId, command); // Emit command-sent event eventBus.emit('command-sent', { sessionId, command: command.substring(0, 100) + (command.length > 100 ? '...' : ''), timestamp: Date.now() }); // Return immediately (actual response comes via SSE) res.json({ success: true, sessionId, message: 'Command sent', timestamp: Date.now() }); console.log(`[SessionRoutes] Command sent successfully to session ${sessionId}`); } catch (error) { console.error(`[SessionRoutes] Error sending command to session ${sessionId}:`, error); // Emit error event eventBus.emit('session-error', { sessionId, error: error.message, code: 'COMMAND_SEND_ERROR', recoverable: false }); res.status(500).json({ error: 'Failed to send command', message: error.message, sessionId }); } }); /** * Get session status * GET /api/session/:sessionId/status * * Returns current status and information about the session. * * Response: * { * "sessionId": "session-123", * "status": "running", * "mode": "full", * "createdAt": "2025-01-21T10:00:00.000Z", * "lastActivity": "2025-01-21T10:05:00.000Z", * "pid": 12345, * "uptime": 300000 * } */ router.get('/session/:sessionId/status', validateSessionId, (req, res) => { const { sessionId } = req.params; try { const session = req.app.locals.claudeService.getSession(sessionId); if (!session) { return res.status(404).json({ error: 'Session not found', sessionId }); } const uptime = Date.now() - new Date(session.createdAt).getTime(); res.json({ sessionId: session.id, status: session.status || 'unknown', mode: session.mode || 'unknown', createdAt: session.createdAt, lastActivity: session.lastActivity || session.createdAt, pid: process.pid, uptime }); console.log(`[SessionRoutes] Status retrieved for session ${sessionId}: ${session.status}`); } catch (error) { console.error(`[SessionRoutes] Error getting status for session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to get session status', message: error.message, sessionId }); } }); /** * Get session context files * GET /api/session/:sessionId/context * * Returns the files currently in the session's context. * * Response: * { * "sessionId": "session-123", * "context": [ * { "path": "/path/to/file.js", "content": "..." } * ], * "timestamp": 1234567890 * } */ router.get('/session/:sessionId/context', validateSessionId, async (req, res) => { const { sessionId } = req.params; try { // Assuming claudeService has a getContext method // If not, this will need to be implemented const context = await req.app.locals.claudeService.getSessionContext?.(sessionId) || { files: [] }; res.json({ sessionId, context: context.files, timestamp: Date.now() }); console.log(`[SessionRoutes] Context retrieved for session ${sessionId}: ${context.files.length} files`); } catch (error) { console.error(`[SessionRoutes] Error getting context for session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to get session context', message: error.message, sessionId }); } }); /** * Preview operations for session * POST /api/session/:sessionId/operations/preview * * Parses a Claude response to detect file operations. * * Request: * { * "response": "I'll create a new file..." * } * * Response: * { * "success": true, * "operations": [ * { "type": "write", "path": "/path/to/file.js", "content": "..." } * ], * "count": 1 * } */ router.post('/session/:sessionId/operations/preview', validateSessionId, validateResponse, async (req, res) => { const { sessionId } = req.params; const { response } = req.body; console.log(`[SessionRoutes] Previewing operations for session ${sessionId}`); try { const operations = await req.app.locals.claudeService.previewOperations(sessionId, response); // Emit operations-detected event eventBus.emit('operations-detected', { sessionId, operations, response: response.substring(0, 200) + '...' }); res.json({ success: true, operations, count: operations.length }); console.log(`[SessionRoutes] Previewed ${operations.length} operations for session ${sessionId}`); } catch (error) { console.error(`[SessionRoutes] Error previewing operations for session ${sessionId}:`, error); // Emit error event eventBus.emit('session-error', { sessionId, error: error.message, code: 'PREVIEW_ERROR', recoverable: true }); res.status(500).json({ error: 'Failed to preview operations', message: error.message, sessionId }); } }); /** * Execute operations for session * POST /api/session/:sessionId/operations/execute * * Executes confirmed file operations. * * Request: * { * "operations": [ * { "type": "write", "path": "/path/to/file.js", "content": "..." } * ] * } * * Response: * { * "success": true, * "results": [ * { "success": true, "path": "/path/to/file.js", "operation": "write" } * ], * "executed": 1 * } */ router.post('/session/:sessionId/operations/execute', validateSessionId, validateOperations, async (req, res) => { const { sessionId } = req.params; const { operations } = req.body; console.log(`[SessionRoutes] Executing ${operations.length} operations for session ${sessionId}`); try { const results = await req.app.locals.claudeService.executeOperations(sessionId, operations); // Emit operations-executed event eventBus.emit('operations-executed', { sessionId, results, count: results.length }); res.json({ success: true, results, executed: results.length }); console.log(`[SessionRoutes] Executed ${results.length} operations for session ${sessionId}`); } catch (error) { console.error(`[SessionRoutes] Error executing operations for session ${sessionId}:`, error); // Emit operations-error event eventBus.emit('operations-error', { sessionId, error: error.message, operations }); res.status(500).json({ error: 'Failed to execute operations', message: error.message, sessionId }); } }); /** * Delete/terminate session * DELETE /api/session/:sessionId * * Terminates the Claude Code session and cleans up resources. * * Response: * { * "success": true, * "message": "Session deleted", * "sessionId": "session-123" * } */ router.delete('/session/:sessionId', validateSessionId, async (req, res) => { const { sessionId } = req.params; console.log(`[SessionRoutes] Deleting session ${sessionId}`); try { await req.app.locals.claudeService.deleteSession(sessionId); // Emit session-deleted event eventBus.emit('session-deleted', { sessionId, timestamp: Date.now() }); res.json({ success: true, message: 'Session deleted', sessionId }); console.log(`[SessionRoutes] Session ${sessionId} deleted successfully`); } catch (error) { console.error(`[SessionRoutes] Error deleting session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to delete session', message: error.message, sessionId }); } }); /** * Duplicate session * POST /api/session/:sessionId/duplicate * * Creates a copy of the session with a new ID. * * Request: * { * "name": "Copy of session-123" // optional * } * * Response: * { * "success": true, * "newSessionId": "session-456", * "originalSessionId": "session-123" * } */ router.post('/session/:sessionId/duplicate', validateSessionId, async (req, res) => { const { sessionId } = req.params; const { name } = req.body; console.log(`[SessionRoutes] Duplicating session ${sessionId}`); try { const result = await req.app.locals.claudeService.duplicateSession(sessionId, name); // Emit session-created event eventBus.emit('session-created', { sessionId: result.newSessionId, sourceSessionId: sessionId, mode: 'duplicate' }); res.json({ success: true, newSessionId: result.newSessionId, originalSessionId: sessionId, name: result.name }); console.log(`[SessionRoutes] Session ${sessionId} duplicated as ${result.newSessionId}`); } catch (error) { console.error(`[SessionRoutes] Error duplicating session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to duplicate session', message: error.message, sessionId }); } }); /** * Fork session * POST /api/session/:sessionId/fork * * Creates a new session with the same context but starts fresh. * * Response: * { * "success": true, * "newSessionId": "session-789", * "forkedFrom": "session-123" * } */ router.post('/session/:sessionId/fork', validateSessionId, async (req, res) => { const { sessionId } = req.params; console.log(`[SessionRoutes] Forking session ${sessionId}`); try { const result = await req.app.locals.claudeService.forkSession(sessionId); // Emit session-created event eventBus.emit('session-created', { sessionId: result.newSessionId, sourceSessionId: sessionId, mode: 'fork' }); res.json({ success: true, newSessionId: result.newSessionId, forkedFrom: sessionId }); console.log(`[SessionRoutes] Session ${sessionId} forked as ${result.newSessionId}`); } catch (error) { console.error(`[SessionRoutes] Error forking session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to fork session', message: error.message, sessionId }); } }); /** * Move session to project * POST /api/session/:sessionId/move * * Moves a session to a different project. * * Request: * { * "projectId": "project-456" * } * * Response: * { * "success": true, * "sessionId": "session-123", * "projectId": "project-456" * } */ router.post('/session/:sessionId/move', validateSessionId, async (req, res) => { const { sessionId } = req.params; const { projectId } = req.body; if (!projectId) { return res.status(400).json({ error: 'Project ID is required' }); } console.log(`[SessionRoutes] Moving session ${sessionId} to project ${projectId}`); try { await req.app.locals.claudeService.moveSession(sessionId, projectId); // Emit session-moved event eventBus.emit('session-moved', { sessionId, projectId, timestamp: Date.now() }); res.json({ success: true, sessionId, projectId }); console.log(`[SessionRoutes] Session ${sessionId} moved to project ${projectId}`); } catch (error) { console.error(`[SessionRoutes] Error moving session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to move session', message: error.message, sessionId }); } }); module.exports = router;