UNPKG

claude-flow-tbowman01

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

548 lines 22.1 kB
/** * Session management commands for Claude-Flow */ import { Command } from 'commander'; import { promises as fs } from 'node:fs'; import Table from 'cli-table3'; import inquirer from 'inquirer'; import { formatStatusIndicator } from '../formatter.js'; import { generateId } from '../../utils/helpers.js'; import chalk from 'chalk'; export const sessionCommand = new Command() .name('session') .description('Manage Claude-Flow sessions') .action(() => { sessionCommand.help(); }); // List command sessionCommand .command('list') .description('List all saved sessions') .option('-a, --active', 'Show only active sessions') .option('--format <format>', 'Output format (table, json)', 'table') .action(async (options) => { await listSessions(options); }); // Save command sessionCommand .command('save') .description('Save current session state') .arguments('[name]') .option('-d, --description <desc>', 'Session description') .option('-t, --tags <tags>', 'Comma-separated tags') .option('--auto', 'Auto-generate session name') .action(async (name, options) => { await saveSession(name, options); }); // Restore command sessionCommand .command('restore') .description('Restore a saved session') .arguments('<session-id>') .option('-f, --force', 'Force restore without confirmation') .option('--merge', 'Merge with current session instead of replacing') .action(async (sessionId, options) => { await restoreSession(sessionId, options); }); // Delete command sessionCommand .command('delete') .description('Delete a saved session') .arguments('<session-id>') .option('-f, --force', 'Skip confirmation prompt') .action(async (sessionId, options) => { await deleteSession(sessionId, options); }); // Export command sessionCommand .command('export') .description('Export session to file') .arguments('<session-id> <output-file>') .option('--format <format>', 'Export format (json, yaml)', 'json') .option('--include-memory', 'Include agent memory in export') .action(async (sessionId, outputFile, options) => { await exportSession(sessionId, outputFile, options); }); // Import command sessionCommand .command('import') .description('Import session from file') .arguments('<input-file>') .option('-n, --name <name>', 'Custom session name') .option('--overwrite', 'Overwrite existing session with same ID') .action(async (inputFile, options) => { await importSession(inputFile, options); }); // Info command sessionCommand .command('info') .description('Show detailed session information') .arguments('<session-id>') .action(async (sessionId, options) => { await showSessionInfo(sessionId); }); // Clean command sessionCommand .command('clean') .description('Clean up old or orphaned sessions') .option('--older-than <days>', 'Delete sessions older than N days', '30') .option('--dry-run', 'Show what would be deleted without deleting') .option('--orphaned', 'Only clean orphaned sessions') .action(async (options) => { await cleanSessions(options); }); const SESSION_DIR = '.claude-flow/sessions'; async function ensureSessionDir() { try { await fs.mkdir(SESSION_DIR, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { throw error; } } } async function listSessions(options) { try { await ensureSessionDir(); const sessions = await loadAllSessions(); let filteredSessions = sessions; if (options.active) { // In production, this would check if the session is currently active filteredSessions = sessions.filter((s) => s.metadata.active); } if (options.format === 'json') { console.log(JSON.stringify(filteredSessions, null, 2)); return; } if (filteredSessions.length === 0) { console.log(chalk.gray('No sessions found')); return; } console.log(chalk.cyan.bold(`Sessions (${filteredSessions.length})`)); console.log('─'.repeat(60)); const rows = []; for (const session of filteredSessions) { rows.push([ chalk.gray(session.id.substring(0, 8) + '...'), chalk.white(session.name), session.description ? session.description.substring(0, 30) + (session.description.length > 30 ? '...' : '') : '-', session.state.agents.length.toString(), session.state.tasks.length.toString(), session.createdAt.toLocaleDateString(), ]); } const table = new Table({ head: ['ID', 'Name', 'Description', 'Agents', 'Tasks', 'Created'], }); for (const row of rows) { table.push(row); } console.log(table.toString()); } catch (error) { console.error(chalk.red('Failed to list sessions:'), error.message); } } async function saveSession(name, options) { try { // Get current session state (mock for now) const currentState = await getCurrentSessionState(); if (!name) { if (options.auto) { name = `session-${new Date().toISOString().split('T')[0]}-${Date.now().toString().slice(-4)}`; } else { const response = await inquirer.prompt({ type: 'input', name: 'sessionName', message: 'Enter session name:', default: `session-${new Date().toISOString().split('T')[0]}`, }); name = response.sessionName; } } const session = { id: generateId('session'), name: name, description: options.description, tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : [], createdAt: new Date(), updatedAt: new Date(), state: currentState, metadata: { version: '1.0.0', platform: process.platform, checksum: await calculateChecksum(currentState), }, }; await ensureSessionDir(); const filePath = `${SESSION_DIR}/${session.id}.json`; await fs.writeFile(filePath, JSON.stringify(session, null, 2)); console.log(chalk.green('✓ Session saved successfully')); console.log(`${chalk.white('ID:')} ${session.id}`); console.log(`${chalk.white('Name:')} ${session.name}`); console.log(`${chalk.white('File:')} ${filePath}`); if (session.description) { console.log(`${chalk.white('Description:')} ${session.description}`); } console.log(`${chalk.white('Agents:')} ${session.state.agents.length}`); console.log(`${chalk.white('Tasks:')} ${session.state.tasks.length}`); } catch (error) { console.error(chalk.red('Failed to save session:'), error.message); } } async function restoreSession(sessionId, options) { try { const session = await loadSession(sessionId); if (!session) { console.error(chalk.red(`Session '${sessionId}' not found`)); return; } // Show session info console.log(chalk.cyan.bold('Session to restore:')); console.log(`${chalk.white('Name:')} ${session.name}`); console.log(`${chalk.white('Description:')} ${session.description || 'None'}`); console.log(`${chalk.white('Agents:')} ${session.state.agents.length}`); console.log(`${chalk.white('Tasks:')} ${session.state.tasks.length}`); console.log(`${chalk.white('Created:')} ${session.createdAt.toLocaleString()}`); // Confirmation if (!options.force) { const action = options.merge ? 'merge with current session' : 'replace current session'; const response = await inquirer.prompt({ type: 'confirm', name: 'confirmed', message: `Are you sure you want to ${action}?`, default: false, }); const confirmed = response.confirmed; if (!confirmed) { console.log(chalk.gray('Restore cancelled')); return; } } // Validate session integrity const expectedChecksum = await calculateChecksum(session.state); if (session.metadata.checksum !== expectedChecksum) { console.log(chalk.yellow('⚠ Warning: Session checksum mismatch. Data may be corrupted.')); if (!options.force) { const response = await inquirer.prompt({ type: 'confirm', name: 'proceed', message: 'Continue anyway?', default: false, }); const proceed = response.proceed; if (!proceed) { console.log(chalk.gray('Restore cancelled')); return; } } } // Restore session (mock for now) console.log(chalk.yellow('Restoring session...')); if (options.merge) { console.log(chalk.blue('• Merging agents...')); console.log(chalk.blue('• Merging tasks...')); console.log(chalk.blue('• Merging memory...')); } else { console.log(chalk.blue('• Stopping current agents...')); console.log(chalk.blue('• Clearing current tasks...')); console.log(chalk.blue('• Restoring agents...')); console.log(chalk.blue('• Restoring tasks...')); console.log(chalk.blue('• Restoring memory...')); } // Update session metadata session.updatedAt = new Date(); const filePath = `${SESSION_DIR}/${session.id}.json`; await fs.writeFile(filePath, JSON.stringify(session, null, 2)); console.log(chalk.green('✓ Session restored successfully')); console.log(chalk.yellow('Note: This is a mock implementation. In production, this would connect to the orchestrator.')); } catch (error) { console.error(chalk.red('Failed to restore session:'), error.message); } } async function deleteSession(sessionId, options) { try { const session = await loadSession(sessionId); if (!session) { console.error(chalk.red(`Session '${sessionId}' not found`)); return; } // Confirmation if (!options.force) { console.log(`${chalk.white('Session:')} ${session.name}`); console.log(`${chalk.white('Created:')} ${session.createdAt.toLocaleString()}`); const response = await inquirer.prompt({ type: 'confirm', name: 'confirmed', message: 'Are you sure you want to delete this session?', default: false, }); const confirmed = response.confirmed; if (!confirmed) { console.log(chalk.gray('Delete cancelled')); return; } } const filePath = `${SESSION_DIR}/${session.id}.json`; await fs.unlink(filePath); console.log(chalk.green('✓ Session deleted successfully')); } catch (error) { console.error(chalk.red('Failed to delete session:'), error.message); } } async function exportSession(sessionId, outputFile, options) { try { const session = await loadSession(sessionId); if (!session) { console.error(chalk.red(`Session '${sessionId}' not found`)); return; } let exportData = session; if (!options.includeMemory) { exportData = { ...session, state: { ...session.state, memory: [], // Exclude memory data }, }; } let content; if (options.format === 'yaml') { // In production, you'd use a YAML library console.log(chalk.yellow('YAML export not implemented yet, using JSON')); content = JSON.stringify(exportData, null, 2); } else { content = JSON.stringify(exportData, null, 2); } await fs.writeFile(outputFile, content); console.log(chalk.green('✓ Session exported successfully')); console.log(`${chalk.white('File:')} ${outputFile}`); console.log(`${chalk.white('Format:')} ${options.format}`); console.log(`${chalk.white('Size:')} ${Buffer.from(content).length} bytes`); } catch (error) { console.error(chalk.red('Failed to export session:'), error.message); } } async function importSession(inputFile, options) { try { const content = await fs.readFile(inputFile, 'utf-8'); const sessionData = JSON.parse(content); // Validate session data structure if (!sessionData.id || !sessionData.name || !sessionData.state) { throw new Error('Invalid session file format'); } // Generate new ID if not overwriting if (!options.overwrite) { sessionData.id = generateId('session'); } // Update name if specified if (options.name) { sessionData.name = options.name; } // Check if session already exists const existingSession = await loadSession(sessionData.id); if (existingSession && !options.overwrite) { console.error(chalk.red('Session with this ID already exists')); console.log(chalk.gray('Use --overwrite to replace it')); return; } // Update timestamps if (options.overwrite && existingSession) { sessionData.updatedAt = new Date(); } else { sessionData.createdAt = new Date(); sessionData.updatedAt = new Date(); } await ensureSessionDir(); const filePath = `${SESSION_DIR}/${sessionData.id}.json`; await fs.writeFile(filePath, JSON.stringify(sessionData, null, 2)); console.log(chalk.green('✓ Session imported successfully')); console.log(`${chalk.white('ID:')} ${sessionData.id}`); console.log(`${chalk.white('Name:')} ${sessionData.name}`); console.log(`${chalk.white('Action:')} ${options.overwrite ? 'Overwritten' : 'Created'}`); } catch (error) { console.error(chalk.red('Failed to import session:'), error.message); } } async function showSessionInfo(sessionId) { try { const session = await loadSession(sessionId); if (!session) { console.error(chalk.red(`Session '${sessionId}' not found`)); return; } console.log(chalk.cyan.bold('Session Information')); console.log('─'.repeat(40)); console.log(`${chalk.white('ID:')} ${session.id}`); console.log(`${chalk.white('Name:')} ${session.name}`); console.log(`${chalk.white('Description:')} ${session.description || 'None'}`); console.log(`${chalk.white('Tags:')} ${session.tags.join(', ') || 'None'}`); console.log(`${chalk.white('Created:')} ${session.createdAt.toLocaleString()}`); console.log(`${chalk.white('Updated:')} ${session.updatedAt.toLocaleString()}`); console.log(); console.log(chalk.cyan.bold('State Summary')); console.log('─'.repeat(40)); console.log(`${chalk.white('Agents:')} ${session.state.agents.length}`); console.log(`${chalk.white('Tasks:')} ${session.state.tasks.length}`); console.log(`${chalk.white('Memory Entries:')} ${session.state.memory.length}`); console.log(); console.log(chalk.cyan.bold('Metadata')); console.log('─'.repeat(40)); console.log(`${chalk.white('Version:')} ${session.metadata.version}`); console.log(`${chalk.white('Platform:')} ${session.metadata.platform}`); console.log(`${chalk.white('Checksum:')} ${session.metadata.checksum}`); // Verify integrity const currentChecksum = await calculateChecksum(session.state); const integrity = currentChecksum === session.metadata.checksum; const integrityIcon = formatStatusIndicator(integrity ? 'success' : 'error'); console.log(`${chalk.white('Integrity:')} ${integrityIcon} ${integrity ? 'Valid' : 'Corrupted'}`); // File info const filePath = `${SESSION_DIR}/${session.id}.json`; try { const fileInfo = await fs.stat(filePath); console.log(); console.log(chalk.cyan.bold('File Information')); console.log('─'.repeat(40)); console.log(`${chalk.white('Path:')} ${filePath}`); console.log(`${chalk.white('Size:')} ${fileInfo.size} bytes`); console.log(`${chalk.white('Modified:')} ${fileInfo.mtime?.toLocaleString() || 'Unknown'}`); } catch { console.log(chalk.red('Warning: Session file not found')); } } catch (error) { console.error(chalk.red('Failed to show session info:'), error.message); } } async function cleanSessions(options) { try { await ensureSessionDir(); const sessions = await loadAllSessions(); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - parseInt(options.olderThan)); let toDelete = sessions.filter((session) => session.createdAt < cutoffDate); if (options.orphaned) { // In production, check if sessions have valid references toDelete = toDelete.filter((session) => session.metadata.orphaned); } if (toDelete.length === 0) { console.log(chalk.gray('No sessions to clean')); return; } console.log(chalk.cyan.bold(`Sessions to clean (${toDelete.length})`)); console.log('─'.repeat(50)); for (const session of toDelete) { const age = Math.floor((Date.now() - session.createdAt.getTime()) / (1000 * 60 * 60 * 24)); console.log(`• ${session.name} (${chalk.gray(session.id.substring(0, 8) + '...')}) - ${age} days old`); } if (options.dryRun) { console.log('\n' + chalk.yellow('Dry run mode - no files were deleted')); return; } console.log(); const response = await inquirer.prompt({ type: 'confirm', name: 'confirmed', message: `Delete ${toDelete.length} sessions?`, default: false, }); const confirmed = response.confirmed; if (!confirmed) { console.log(chalk.gray('Clean cancelled')); return; } let deleted = 0; for (const session of toDelete) { try { const filePath = `${SESSION_DIR}/${session.id}.json`; await fs.unlink(filePath); deleted++; } catch (error) { console.error(chalk.red(`Failed to delete ${session.name}:`), error.message); } } console.log(chalk.green(`✓ Cleaned ${deleted} sessions`)); } catch (error) { console.error(chalk.red('Failed to clean sessions:'), error.message); } } async function loadAllSessions() { const sessions = []; try { const entries = await fs.readdir(SESSION_DIR, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.json')) { try { const content = await fs.readFile(`${SESSION_DIR}/${entry.name}`, 'utf-8'); const session = JSON.parse(content); // Convert date strings back to Date objects session.createdAt = new Date(session.createdAt); session.updatedAt = new Date(session.updatedAt); sessions.push(session); } catch (error) { console.warn(chalk.yellow(`Warning: Failed to load session file ${entry.name}:`), error.message); } } } } catch (error) { if (error.code !== 'ENOENT') { throw error; } } return sessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); } async function loadSession(sessionId) { const sessions = await loadAllSessions(); return sessions.find((s) => s.id === sessionId || s.id.startsWith(sessionId)) || null; } async function getCurrentSessionState() { // Mock current session state - in production, this would connect to the orchestrator return { agents: [ { id: 'agent-001', type: 'coordinator', status: 'active' }, { id: 'agent-002', type: 'researcher', status: 'active' }, ], tasks: [ { id: 'task-001', type: 'research', status: 'running' }, { id: 'task-002', type: 'analysis', status: 'pending' }, ], memory: [ { id: 'memory-001', type: 'conversation', agentId: 'agent-001' }, { id: 'memory-002', type: 'result', agentId: 'agent-002' }, ], configuration: { orchestrator: { maxAgents: 10 }, memory: { backend: 'hybrid' }, }, }; } async function calculateChecksum(data) { const content = JSON.stringify(data, null, 0); const encoder = new TextEncoder(); const dataBuffer = encoder.encode(content); const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray .map((b) => b.toString(16).padStart(2, '0')) .join('') .substring(0, 16); } //# sourceMappingURL=session.js.map