#!/usr/bin/env node const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); class SessionManager { constructor(messageQueue, elicitationBroker, options = {}) { this.messageQueue = messageQueue; this.elicitationBroker = elicitationBroker; this.basePath = options.basePath || path.join(process.env.HOME, '.bmad'); this.sessionsPath = path.join(this.basePath, 'sessions'); this.activeSessions = new Map(); this.sessionOrder = []; // Track order of sessions for switching } async initialize() { await fs.mkdir(this.sessionsPath, { recursive: true }); await this.loadActiveSessions(); } generateSessionId() { return `session-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`; } async createAgentSession(agentName, context = {}) { const sessionId = this.generateSessionId(); const session = { id: sessionId, agent: agentName, status: 'active', created: new Date().toISOString(), lastActivity: Date.now(), context: { ...context, conversationHistory: [], elicitationSessions: [] }, ui: { color: this.getAgentColor(agentName), icon: this.getAgentIcon(agentName), displayName: this.getAgentDisplayName(agentName) } }; this.activeSessions.set(sessionId, session); this.sessionOrder.push(sessionId); await this.saveSession(session); return session; } async switchSession(sessionId) { if (!this.activeSessions.has(sessionId)) { throw new Error(`Session ${sessionId} not found`); } // Move session to front of order this.sessionOrder = this.sessionOrder.filter(id => id !== sessionId); this.sessionOrder.unshift(sessionId); const session = this.activeSessions.get(sessionId); session.lastActivity = Date.now(); await this.saveSession(session); return session; } async suspendSession(sessionId, reason = 'user_switch') { const session = this.activeSessions.get(sessionId); if (!session) return; session.status = 'suspended'; session.suspendedAt = Date.now(); session.suspendReason = reason; // If in elicitation, save state if (session.currentElicitation) { session.context.suspendedElicitation = { sessionId: session.currentElicitation, state: await this.elicitationBroker.loadSession(session.currentElicitation) }; } await this.saveSession(session); } async resumeSession(sessionId) { const session = this.activeSessions.get(sessionId); if (!session) { // Try to load from disk const loadedSession = await this.loadSession(sessionId); if (loadedSession) { this.activeSessions.set(sessionId, loadedSession); return loadedSession; } throw new Error(`Session ${sessionId} not found`); } session.status = 'active'; session.resumedAt = Date.now(); delete session.suspendedAt; delete session.suspendReason; // Resume elicitation if needed if (session.context.suspendedElicitation) { session.currentElicitation = session.context.suspendedElicitation.sessionId; delete session.context.suspendedElicitation; } await this.saveSession(session); return session; } async addToConversation(sessionId, entry) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } const conversationEntry = { ...entry, timestamp: new Date().toISOString(), id: `conv-${Date.now()}-${crypto.randomBytes(4).toString('hex')}` }; session.context.conversationHistory.push(conversationEntry); session.lastActivity = Date.now(); await this.saveSession(session); return conversationEntry; } async startElicitation(sessionId, elicitationSessionId) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } session.currentElicitation = elicitationSessionId; session.context.elicitationSessions.push({ id: elicitationSessionId, startedAt: Date.now() }); await this.saveSession(session); } async completeElicitation(sessionId, elicitationSessionId, result) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } if (session.currentElicitation === elicitationSessionId) { delete session.currentElicitation; } const elicitationRecord = session.context.elicitationSessions.find( e => e.id === elicitationSessionId ); if (elicitationRecord) { elicitationRecord.completedAt = Date.now(); elicitationRecord.result = result; } await this.saveSession(session); } formatSessionPrompt(session) { const header = `\n${'='.repeat(60)}\n` + `${session.ui.icon} **${session.ui.displayName}** Session\n` + `Session ID: ${session.id}\n` + `${'='.repeat(60)}\n`; let prompt = header; if (session.status === 'suspended') { prompt += `\nāš ļø This session is currently suspended. Would you like to resume?\n`; } if (session.currentElicitation) { prompt += `\nšŸ“ Currently in elicitation phase...\n`; } return prompt; } formatSessionList() { const sessions = Array.from(this.activeSessions.values()); if (sessions.length === 0) { return 'No active sessions.'; } let output = '**Active BMAD Sessions:**\n\n'; sessions.forEach((session, index) => { const isActive = this.sessionOrder[0] === session.id; const status = isActive ? '🟢' : (session.status === 'suspended' ? '🟔' : '⚪'); const lastActive = new Date(session.lastActivity).toLocaleTimeString(); output += `${status} **${index + 1}. ${session.ui.icon} ${session.ui.displayName}**\n`; output += ` Session: ${session.id}\n`; output += ` Status: ${session.status} | Last active: ${lastActive}\n`; if (session.currentElicitation) { output += ` šŸ“ In elicitation phase\n`; } output += '\n'; }); output += '\nšŸ’” Use `/switch ` to switch between sessions\n'; output += 'šŸ’” Use `/suspend` to pause current session\n'; output += 'šŸ’” Use `/sessions` to see this list again\n'; return output; } getAgentColor(agentName) { const colors = { 'bmad-master': 'purple', 'bmad-orchestrator': 'blue', 'pm': 'green', 'architect': 'orange', 'dev': 'cyan', 'qa': 'red', 'ux-expert': 'pink', 'sm': 'yellow' }; return colors[agentName] || 'gray'; } getAgentIcon(agentName) { const icons = { 'bmad-master': 'šŸ§™', 'bmad-orchestrator': 'šŸŽ­', 'pm': 'šŸ“‹', 'architect': 'šŸ—ļø', 'dev': 'šŸ’»', 'qa': 'šŸ›', 'ux-expert': 'šŸŽØ', 'sm': 'šŸƒ' }; return icons[agentName] || 'šŸ¤–'; } getAgentDisplayName(agentName) { const names = { 'bmad-master': 'BMAD Master', 'bmad-orchestrator': 'BMAD Orchestrator', 'pm': 'Project Manager', 'architect': 'Architect', 'dev': 'Developer', 'qa': 'QA Engineer', 'ux-expert': 'UX Expert', 'sm': 'Scrum Master' }; return names[agentName] || agentName; } async saveSession(session) { const sessionPath = path.join(this.sessionsPath, `${session.id}.json`); await fs.writeFile(sessionPath, JSON.stringify(session, null, 2)); } async loadSession(sessionId) { const sessionPath = path.join(this.sessionsPath, `${sessionId}.json`); try { const content = await fs.readFile(sessionPath, 'utf8'); return JSON.parse(content); } catch (error) { return null; } } async loadActiveSessions() { try { const files = await fs.readdir(this.sessionsPath); for (const file of files) { if (file.endsWith('.json')) { const session = await this.loadSession(file.replace('.json', '')); if (session && (session.status === 'active' || session.status === 'suspended')) { this.activeSessions.set(session.id, session); this.sessionOrder.push(session.id); } } } // Sort by last activity this.sessionOrder.sort((a, b) => { const sessionA = this.activeSessions.get(a); const sessionB = this.activeSessions.get(b); return (sessionB?.lastActivity || 0) - (sessionA?.lastActivity || 0); }); } catch (error) { // Directory doesn't exist yet } } async cleanup() { const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours for (const [sessionId, session] of this.activeSessions.entries()) { if (now - session.lastActivity > maxAge && session.status !== 'active') { this.activeSessions.delete(sessionId); this.sessionOrder = this.sessionOrder.filter(id => id !== sessionId); const sessionPath = path.join(this.sessionsPath, `${sessionId}.json`); await fs.unlink(sessionPath).catch(() => {}); } } } } // CLI interface for testing if (require.main === module) { const BMADMessageQueue = require('./message-queue'); const ElicitationBroker = require('./elicitation-broker'); const queue = new BMADMessageQueue(); const broker = new ElicitationBroker(queue); const manager = new SessionManager(queue, broker); const commands = { async init() { await manager.initialize(); console.log('Session manager initialized'); }, async create(agent) { const session = await manager.createAgentSession(agent); console.log(`Session created: ${session.id}`); console.log(manager.formatSessionPrompt(session)); }, async list() { await manager.initialize(); console.log(manager.formatSessionList()); }, async switch(sessionId) { const session = await manager.switchSession(sessionId); console.log(manager.formatSessionPrompt(session)); }, async suspend(sessionId) { await manager.suspendSession(sessionId); console.log(`Session ${sessionId} suspended`); } }; const [,, command, ...args] = process.argv; if (commands[command]) { commands[command](...args).catch(console.error); } else { console.log('Usage: session-manager.js [args]'); console.log('Commands:', Object.keys(commands).join(', ')); } } module.exports = SessionManager;