/** * Sessions Routes (Plural) * * Endpoints for listing and creating Claude Code sessions. * This complements session-routes.js which handles individual session operations. * * Mounted at /claude/api, routes are accessed as: * - GET /claude/api/claude/sessions (list) * - POST /claude/api/claude/sessions (create) * - GET /claude/api/claude/sessions/:sessionId (get) * - POST /claude/api/claude/sessions/:sessionId/prompt (send) */ const express = require('express'); const router = express.Router(); // Create sub-router for /claude prefix const claudeRouter = express.Router(); /** * List all sessions * GET /claude/sessions * * Returns both active and historical sessions. * * Response: * { * "active": [...], * "historical": [...], * "archived": [...] (if ?archived=true) * } */ claudeRouter.get('/sessions', (req, res) => { const claudeService = req.app.locals.claudeService; const { archived } = req.query; if (!claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } try { // Get active sessions const activeSessions = claudeService.listSessions(); // Get historical sessions let historicalSessions = claudeService.loadHistoricalSessions(); // Filter out archived sessions from normal list historicalSessions = historicalSessions.filter(s => !s.metadata?.archived); // If requesting archived sessions, return only archived if (archived === 'true') { const allHistorical = claudeService.loadHistoricalSessions(); const archivedSessions = allHistorical.filter(s => s.metadata?.archived); return res.json({ archived: archivedSessions }); } res.json({ active: activeSessions, historical: historicalSessions }); console.log(`[SessionsRoutes] Listed ${activeSessions.length} active, ${historicalSessions.length} historical sessions`); } catch (error) { console.error('[SessionsRoutes] Error listing sessions:', error); res.status(500).json({ error: 'Failed to list sessions', message: error.message }); } }); /** * Create a new session * POST /claude/sessions * * Request: * { * "workingDir": "/path/to/directory", * "mode": "code" | "full", * "metadata": { ... } * } * * Response: * { * "id": "session-123", * "status": "running", * "createdAt": "...", * "workingDir": "..." * } */ claudeRouter.post('/sessions', (req, res) => { const claudeService = req.app.locals.claudeService; if (!claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } const { workingDir, mode, metadata } = req.body; try { const session = claudeService.createSession({ workingDir: workingDir || req.app.locals.VAULT_PATH || '/home/uroma/obsidian-vault', mode, metadata }); // Emit session-created event const eventBus = require('../services/event-bus'); eventBus.emit('session-created', { sessionId: session.id, mode: mode || 'code', timestamp: Date.now() }); res.status(201).json({ id: session.id, status: session.status, createdAt: session.createdAt, workingDir: session.workingDir, metadata: session.metadata }); console.log(`[SessionsRoutes] Created session ${session.id}`); } catch (error) { console.error('[SessionsRoutes] Error creating session:', error); res.status(500).json({ error: 'Failed to create session', message: error.message }); } }); /** * Get session details * GET /claude/sessions/:sessionId * * Returns detailed information about a specific session. */ claudeRouter.get('/sessions/:sessionId', (req, res) => { const claudeService = req.app.locals.claudeService; if (!claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } const { sessionId } = req.params; try { const session = claudeService.getSession(sessionId); if (!session) { return res.status(404).json({ error: 'Session not found', sessionId }); } res.json(session); console.log(`[SessionsRoutes] Retrieved session ${sessionId}`); } catch (error) { console.error(`[SessionsRoutes] Error getting session ${sessionId}:`, error); // Check if it's a "not found" error if (error.message.includes('not found')) { return res.status(404).json({ error: 'Session not found', message: error.message, sessionId }); } res.status(500).json({ error: 'Failed to get session', message: error.message, sessionId }); } }); /** * Send prompt to session * POST /claude/sessions/:sessionId/prompt * * This forwards to the session-routes handler for consistency. * AUTO-RECREATES historical sessions as active sessions. */ claudeRouter.post('/sessions/:sessionId/prompt', (req, res) => { const { sessionId } = req.params; const { command, context } = req.body; const eventBus = require('../services/event-bus'); // Validate session ID if (!sessionId || sessionId.length < 10) { return res.status(400).json({ error: 'Invalid session ID', sessionId }); } // Validate command if (!command || typeof command !== 'string' || command.trim().length === 0) { return res.status(400).json({ error: 'Command is required', sessionId }); } const claudeService = req.app.locals.claudeService; console.log(`[SessionsRoutes] Sending command to session ${sessionId}: ${command.substring(0, 100)}...`); try { // Check if session is active const activeSession = claudeService.sessions.get(sessionId); if (!activeSession) { // Session not in active sessions - check if it's historical try { const historicalSession = claudeService.getSession(sessionId); if (historicalSession && historicalSession.metadata?.historical) { // ============================================================ // AUTO-RECREATE: Historical session detected, create new active one // ============================================================ console.log(`[SessionsRoutes] 🔄 Auto-recreating historical session ${sessionId}`); const newSession = claudeService.createSession({ workingDir: historicalSession.workingDir || '/home/uroma', metadata: { ...historicalSession.metadata, recreatedFrom: sessionId, originalSessionId: sessionId } }); console.log(`[SessionsRoutes] ✅ Created new active session ${newSession.id} replacing historical ${sessionId}`); // Send command to new session claudeService.sendCommand(newSession.id, command); eventBus.emit('command-sent', { sessionId: newSession.id, command: command.substring(0, 100) + (command.length > 100 ? '...' : ''), timestamp: Date.now() }); // Return with new session ID return res.json({ success: true, sessionId: newSession.id, message: 'Session was historical - created new active session', newSession: true, timestamp: Date.now() }); } } catch (historicalError) { // Not a historical session, continue to normal error } throw new Error(`Session ${sessionId} not found or not active`); } // Send command to Claude service (synchronous) 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(`[SessionsRoutes] Command sent successfully to session ${sessionId}`); } catch (error) { console.error(`[SessionsRoutes] 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 }); } }); /** * Delete session * DELETE /claude/sessions/:sessionId */ claudeRouter.delete('/sessions/:sessionId', (req, res) => { const claudeService = req.app.locals.claudeService; const { sessionId } = req.params; if (!claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } try { const session = claudeService.sessions.get(sessionId); if (!session) { return res.status(404).json({ error: 'Session not found', sessionId }); } // Terminate if running if (session.process && !session.process.killed) { session.process.kill(); } claudeService.sessions.delete(sessionId); const eventBus = require('../services/event-bus'); eventBus.emit('session-deleted', { sessionId, timestamp: Date.now() }); res.json({ success: true, message: 'Session deleted', sessionId }); console.log(`[SessionsRoutes] Session ${sessionId} deleted successfully`); } catch (error) { console.error(`[SessionsRoutes] Error deleting session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to delete session', message: error.message, sessionId }); } }); /** * Archive session (soft delete) * PATCH /claude/sessions/:sessionId/archive */ claudeRouter.patch('/sessions/:sessionId/archive', async (req, res) => { const claudeService = req.app.locals.claudeService; const { sessionId } = req.params; if (!claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } try { // First check active sessions let session = claudeService.sessions.get(sessionId); // If not found in active, check historical sessions if (!session) { try { session = claudeService.getSession(sessionId); } catch (e) { // Session not found anywhere return res.status(404).json({ error: 'Session not found', sessionId }); } } if (!session) { return res.status(404).json({ error: 'Session not found', sessionId }); } // Add archivedAt timestamp to metadata if (!session.metadata) { session.metadata = {}; } session.metadata.archivedAt = new Date().toISOString(); session.metadata.archived = true; // Save to historical sessions const fs = require('fs').promises; const path = require('path'); const sessionsDir = path.join(req.app.locals.VAULT_PATH, '.claude', 'sessions'); // Ensure directory exists await fs.mkdir(sessionsDir, { recursive: true }); // Save session as archived const sessionFile = path.join(sessionsDir, `${sessionId}.json`); await fs.writeFile(sessionFile, JSON.stringify({ id: session.id, status: 'archived', createdAt: session.createdAt, archivedAt: session.metadata.archivedAt, workingDir: session.workingDir, metadata: session.metadata, outputBuffer: session.outputBuffer || [] }, null, 2)); // Remove from active sessions (if it was active) if (claudeService.sessions.has(sessionId)) { // Terminate if running if (session.process && !session.process.killed) { session.process.kill(); } claudeService.sessions.delete(sessionId); } const eventBus = require('../services/event-bus'); eventBus.emit('session-archived', { sessionId, timestamp: Date.now() }); res.json({ success: true, message: 'Session archived', sessionId }); console.log(`[SessionsRoutes] Session ${sessionId} archived successfully`); } catch (error) { console.error(`[SessionsRoutes] Error archiving session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to archive session', message: error.message, sessionId }); } }); /** * Unarchive session * PATCH /claude/sessions/:sessionId/unarchive */ claudeRouter.patch('/sessions/:sessionId/unarchive', async (req, res) => { const claudeService = req.app.locals.claudeService; const { sessionId } = req.params; if (!claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } try { // Load the archived session file const fs = require('fs').promises; const path = require('path'); const sessionsDir = path.join(req.app.locals.VAULT_PATH, '.claude', 'sessions'); const sessionFile = path.join(sessionsDir, `${sessionId}.json`); const sessionData = JSON.parse(await fs.readFile(sessionFile, 'utf8')); // Remove archived flags delete sessionData.metadata.archivedAt; delete sessionData.metadata.archived; // Update status sessionData.status = 'historical'; // Save back as historical (not archived) await fs.writeFile(sessionFile, JSON.stringify(sessionData, null, 2)); const eventBus = require('../services/event-bus'); eventBus.emit('session-unarchived', { sessionId, timestamp: Date.now() }); res.json({ success: true, message: 'Session unarchived', sessionId }); console.log(`[SessionsRoutes] Session ${sessionId} unarchived successfully`); } catch (error) { console.error(`[SessionsRoutes] Error unarchiving session ${sessionId}:`, error); res.status(500).json({ error: 'Failed to unarchive session', message: error.message, sessionId }); } }); /** * Merge multiple sessions * POST /claude/sessions/merge */ claudeRouter.post('/sessions/merge', async (req, res) => { const claudeService = req.app.locals.claudeService; const { sessionIds } = req.body; if (!claudeService) { return res.status(500).json({ error: 'Service not available', message: 'claudeService not initialized' }); } if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length < 2) { return res.status(400).json({ error: 'At least 2 session IDs required', sessionIds }); } try { const fs = require('fs').promises; const path = require('path'); const sessionsDir = path.join(req.app.locals.VAULT_PATH, '.claude', 'sessions'); // Load all sessions const sessions = []; const metadata = { mergedFrom: sessionIds, mergedAt: new Date().toISOString() }; for (const sessionId of sessionIds) { const sessionFile = path.join(sessionsDir, `${sessionId}.json`); try { const sessionData = JSON.parse(await fs.readFile(sessionFile, 'utf8')); sessions.push(sessionData); // Collect all unique metadata if (sessionData.metadata) { Object.assign(metadata, sessionData.metadata); } } catch (error) { console.warn(`[SessionsRoutes] Could not load session ${sessionId}:`, error.message); } } if (sessions.length === 0) { return res.status(404).json({ error: 'No valid sessions found', sessionIds }); } // Combine output buffers from all sessions const combinedOutput = []; for (const session of sessions) { if (session.outputBuffer && Array.isArray(session.outputBuffer)) { combinedOutput.push(...session.outputBuffer); } } // Get working directory from first session const workingDir = sessions[0].workingDir || req.app.locals.VAULT_PATH || '/home/uroma'; // Create new merged session const newSession = claudeService.createSession({ workingDir, metadata: { ...metadata, project: `Merged (${sessions.length} sessions)` } }); // Add combined output buffer to the new session if (combinedOutput.length > 0) { newSession.outputBuffer = combinedOutput; } const eventBus = require('../services/event-bus'); eventBus.emit('sessions-merged', { sessionIds, newSessionId: newSession.id, timestamp: Date.now() }); res.json({ success: true, session: { id: newSession.id, status: newSession.status, createdAt: newSession.createdAt, workingDir: newSession.workingDir, metadata: newSession.metadata }, message: `Merged ${sessions.length} sessions into ${newSession.id}` }); console.log(`[SessionsRoutes] Merged ${sessions.length} sessions into ${newSession.id}`); } catch (error) { console.error('[SessionsRoutes] Error merging sessions:', error); res.status(500).json({ error: 'Failed to merge sessions', message: error.message }); } }); // Mount the /claude sub-router router.use('/claude', claudeRouter); module.exports = router;