- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
639 lines
18 KiB
JavaScript
639 lines
18 KiB
JavaScript
/**
|
|
* 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;
|