UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

750 lines 28 kB
/** * V3 CLI Session Command * Session management for Claude Flow */ import { output } from '../output.js'; import { confirm, input, select } from '../prompt.js'; import { callMCPTool, MCPClientError } from '../mcp-client.js'; import * as fs from 'fs'; import * as path from 'path'; // Format date for display function formatDate(dateStr) { const date = new Date(dateStr); const now = new Date(); const diff = now.getTime() - date.getTime(); // Less than 24 hours - show relative time if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); const minutes = Math.floor((diff % (60 * 60 * 1000)) / (60 * 1000)); if (hours > 0) { return `${hours}h ${minutes}m ago`; } return `${minutes}m ago`; } // Otherwise show date return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); } // Format session status function formatStatus(status) { switch (status) { case 'active': return output.success(status); case 'saved': return output.info(status); case 'archived': return output.dim(status); default: return status; } } // List subcommand const listCommand = { name: 'list', aliases: ['ls'], description: 'List all sessions', options: [ { name: 'active', short: 'a', description: 'Show only active sessions', type: 'boolean', default: false }, { name: 'all', description: 'Include archived sessions', type: 'boolean', default: false }, { name: 'limit', short: 'l', description: 'Maximum sessions to show', type: 'number', default: 20 } ], action: async (ctx) => { const activeOnly = ctx.flags.active; const includeArchived = ctx.flags.all; const limit = ctx.flags.limit; try { const result = await callMCPTool('session_list', { status: activeOnly ? 'active' : includeArchived ? 'all' : 'active,saved', limit }); if (ctx.flags.format === 'json') { output.printJson(result); return { success: true, data: result }; } output.writeln(); output.writeln(output.bold('Sessions')); output.writeln(); if (result.sessions.length === 0) { output.printInfo('No sessions found'); output.printInfo('Run "claude-flow session save" to create a session'); return { success: true, data: result }; } output.printTable({ columns: [ { key: 'id', header: 'ID', width: 20 }, { key: 'name', header: 'Name', width: 20 }, { key: 'status', header: 'Status', width: 10 }, { key: 'agents', header: 'Agents', width: 8, align: 'right' }, { key: 'tasks', header: 'Tasks', width: 8, align: 'right' }, { key: 'updated', header: 'Last Updated', width: 18 } ], data: result.sessions.map(s => ({ id: s.id, name: s.name || '-', status: formatStatus(s.status), agents: s.agentCount, tasks: s.taskCount, updated: formatDate(s.updatedAt) })) }); output.writeln(); output.printInfo(`Showing ${result.sessions.length} of ${result.total} sessions`); return { success: true, data: result }; } catch (error) { if (error instanceof MCPClientError) { output.printError(`Failed to list sessions: ${error.message}`); } else { output.printError(`Unexpected error: ${String(error)}`); } return { success: false, exitCode: 1 }; } } }; // Save subcommand const saveCommand = { name: 'save', aliases: ['create', 'checkpoint'], description: 'Save current session state', options: [ { name: 'name', short: 'n', description: 'Session name', type: 'string' }, { name: 'description', short: 'd', description: 'Session description', type: 'string' }, { name: 'include-memory', description: 'Include memory state in session', type: 'boolean', default: true }, { name: 'include-agents', description: 'Include agent state in session', type: 'boolean', default: true }, { name: 'include-tasks', description: 'Include task state in session', type: 'boolean', default: true } ], action: async (ctx) => { let sessionName = ctx.flags.name; let description = ctx.flags.description; // Interactive mode if (!sessionName && ctx.interactive) { sessionName = await input({ message: 'Session name:', default: `session-${Date.now().toString(36)}`, validate: (v) => v.length > 0 || 'Name is required' }); } if (!description && ctx.interactive) { description = await input({ message: 'Session description (optional):', default: '' }); } const spinner = output.createSpinner({ text: 'Saving session...' }); spinner.start(); try { const result = await callMCPTool('session_save', { name: sessionName, description, includeMemory: ctx.flags['include-memory'] !== false, includeAgents: ctx.flags['include-agents'] !== false, includeTasks: ctx.flags['include-tasks'] !== false }); spinner.succeed('Session saved'); output.writeln(); output.printTable({ columns: [ { key: 'property', header: 'Property', width: 18 }, { key: 'value', header: 'Value', width: 35 } ], data: [ { property: 'Session ID', value: result.sessionId }, { property: 'Name', value: result.name }, { property: 'Description', value: result.description || '-' }, { property: 'Saved At', value: new Date(result.savedAt).toLocaleString() }, { property: 'Agents', value: result.stats.agentCount }, { property: 'Tasks', value: result.stats.taskCount }, { property: 'Memory Entries', value: result.stats.memoryEntries }, { property: 'Total Size', value: formatSize(result.stats.totalSize) } ] }); output.writeln(); output.printSuccess(`Session saved: ${result.sessionId}`); output.printInfo(`Restore with: claude-flow session restore ${result.sessionId}`); if (ctx.flags.format === 'json') { output.printJson(result); } return { success: true, data: result }; } catch (error) { spinner.fail('Failed to save session'); if (error instanceof MCPClientError) { output.printError(`Error: ${error.message}`); } else { output.printError(`Unexpected error: ${String(error)}`); } return { success: false, exitCode: 1 }; } } }; // Restore subcommand const restoreCommand = { name: 'restore', aliases: ['load'], description: 'Restore a saved session', options: [ { name: 'force', short: 'f', description: 'Overwrite current state without confirmation', type: 'boolean', default: false }, { name: 'memory-only', description: 'Only restore memory state', type: 'boolean', default: false }, { name: 'agents-only', description: 'Only restore agent state', type: 'boolean', default: false }, { name: 'tasks-only', description: 'Only restore task state', type: 'boolean', default: false } ], action: async (ctx) => { let sessionId = ctx.args[0]; const force = ctx.flags.force; if (!sessionId && ctx.interactive) { // Show list to select from try { const sessions = await callMCPTool('session_list', { status: 'saved', limit: 20 }); if (sessions.sessions.length === 0) { output.printWarning('No saved sessions found'); return { success: false, exitCode: 1 }; } sessionId = await select({ message: 'Select session to restore:', options: sessions.sessions.map(s => ({ value: s.id, label: s.name || s.id, hint: formatDate(s.updatedAt) })) }); } catch (error) { if (error instanceof Error && error.message === 'User cancelled') { output.printInfo('Operation cancelled'); return { success: true }; } throw error; } } if (!sessionId) { output.printError('Session ID is required'); return { success: false, exitCode: 1 }; } // Confirm unless forced if (!force && ctx.interactive) { const confirmed = await confirm({ message: 'This will overwrite current state. Continue?', default: false }); if (!confirmed) { output.printInfo('Operation cancelled'); return { success: true }; } } const spinner = output.createSpinner({ text: 'Restoring session...' }); spinner.start(); try { // Determine what to restore const restoreMemory = !ctx.flags['agents-only'] && !ctx.flags['tasks-only']; const restoreAgents = !ctx.flags['memory-only'] && !ctx.flags['tasks-only']; const restoreTasks = !ctx.flags['memory-only'] && !ctx.flags['agents-only']; const result = await callMCPTool('session_restore', { sessionId, restoreMemory, restoreAgents, restoreTasks }); spinner.succeed('Session restored'); output.writeln(); output.printTable({ columns: [ { key: 'component', header: 'Component', width: 20 }, { key: 'status', header: 'Status', width: 15 }, { key: 'count', header: 'Items', width: 10, align: 'right' } ], data: [ { component: 'Memory', status: result.restored.memory ? output.success('Restored') : output.dim('Skipped'), count: result.stats.memoryEntriesRestored }, { component: 'Agents', status: result.restored.agents ? output.success('Restored') : output.dim('Skipped'), count: result.stats.agentsRestored }, { component: 'Tasks', status: result.restored.tasks ? output.success('Restored') : output.dim('Skipped'), count: result.stats.tasksRestored } ] }); output.writeln(); output.printSuccess(`Session ${sessionId} restored successfully`); if (ctx.flags.format === 'json') { output.printJson(result); } return { success: true, data: result }; } catch (error) { spinner.fail('Failed to restore session'); if (error instanceof MCPClientError) { output.printError(`Error: ${error.message}`); } else { output.printError(`Unexpected error: ${String(error)}`); } return { success: false, exitCode: 1 }; } } }; // Delete subcommand const deleteCommand = { name: 'delete', aliases: ['rm', 'remove'], description: 'Delete a saved session', options: [ { name: 'force', short: 'f', description: 'Delete without confirmation', type: 'boolean', default: false } ], action: async (ctx) => { const sessionId = ctx.args[0]; const force = ctx.flags.force; if (!sessionId) { output.printError('Session ID is required'); return { success: false, exitCode: 1 }; } if (!force && ctx.interactive) { const confirmed = await confirm({ message: `Delete session ${sessionId}? This cannot be undone.`, default: false }); if (!confirmed) { output.printInfo('Operation cancelled'); return { success: true }; } } try { const result = await callMCPTool('session_delete', { sessionId }); output.writeln(); output.printSuccess(`Session ${sessionId} deleted`); if (ctx.flags.format === 'json') { output.printJson(result); } return { success: true, data: result }; } catch (error) { if (error instanceof MCPClientError) { output.printError(`Failed to delete session: ${error.message}`); } else { output.printError(`Unexpected error: ${String(error)}`); } return { success: false, exitCode: 1 }; } } }; // Export subcommand const exportCommand = { name: 'export', description: 'Export session to file', options: [ { name: 'output', short: 'o', description: 'Output file path', type: 'string' }, { name: 'format', short: 'f', description: 'Export format (json, yaml)', type: 'string', choices: ['json', 'yaml'], default: 'json' }, { name: 'include-memory', description: 'Include memory data', type: 'boolean', default: true }, { name: 'compress', description: 'Compress output', type: 'boolean', default: false } ], action: async (ctx) => { let sessionId = ctx.args[0]; let outputPath = ctx.flags.output; const exportFormat = ctx.flags.format; const compress = ctx.flags.compress; // Get current session if no ID provided if (!sessionId) { try { const current = await callMCPTool('session_current', {}); sessionId = current.sessionId; } catch { output.printError('No active session. Provide a session ID to export.'); return { success: false, exitCode: 1 }; } } // Generate output path if not provided if (!outputPath) { const ext = compress ? '.gz' : ''; outputPath = `session-${sessionId}.${exportFormat}${ext}`; } const spinner = output.createSpinner({ text: 'Exporting session...' }); spinner.start(); try { const result = await callMCPTool('session_export', { sessionId, includeMemory: ctx.flags['include-memory'] !== false }); // Format output let content; if (exportFormat === 'yaml') { content = toSimpleYaml(result.data); } else { content = JSON.stringify(result.data, null, 2); } // Write to file const absolutePath = path.isAbsolute(outputPath) ? outputPath : path.join(ctx.cwd, outputPath); fs.writeFileSync(absolutePath, content, 'utf-8'); spinner.succeed('Session exported'); output.writeln(); output.printTable({ columns: [ { key: 'property', header: 'Property', width: 18 }, { key: 'value', header: 'Value', width: 40 } ], data: [ { property: 'Session ID', value: sessionId }, { property: 'Output File', value: absolutePath }, { property: 'Format', value: exportFormat.toUpperCase() }, { property: 'Agents', value: result.stats.agentCount }, { property: 'Tasks', value: result.stats.taskCount }, { property: 'Memory Entries', value: result.stats.memoryEntries }, { property: 'File Size', value: formatSize(content.length) } ] }); output.writeln(); output.printSuccess(`Session exported to ${outputPath}`); return { success: true, data: { sessionId, outputPath, format: exportFormat, size: content.length } }; } catch (error) { spinner.fail('Failed to export session'); if (error instanceof MCPClientError) { output.printError(`Error: ${error.message}`); } else { output.printError(`Unexpected error: ${String(error)}`); } return { success: false, exitCode: 1 }; } } }; // Import subcommand const importCommand = { name: 'import', description: 'Import session from file', options: [ { name: 'name', short: 'n', description: 'Session name for imported session', type: 'string' }, { name: 'activate', description: 'Activate session after import', type: 'boolean', default: false } ], action: async (ctx) => { const filePath = ctx.args[0]; const sessionName = ctx.flags.name; const activate = ctx.flags.activate; if (!filePath) { output.printError('File path is required'); return { success: false, exitCode: 1 }; } const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(ctx.cwd, filePath); if (!fs.existsSync(absolutePath)) { output.printError(`File not found: ${absolutePath}`); return { success: false, exitCode: 1 }; } const spinner = output.createSpinner({ text: 'Importing session...' }); spinner.start(); try { const content = fs.readFileSync(absolutePath, 'utf-8'); let data; // Parse based on extension if (absolutePath.endsWith('.yaml') || absolutePath.endsWith('.yml')) { // Simple YAML parsing (basic implementation) data = JSON.parse(content); // Would need proper YAML parser } else { data = JSON.parse(content); } const result = await callMCPTool('session_import', { data, name: sessionName, activate }); spinner.succeed('Session imported'); output.writeln(); output.printTable({ columns: [ { key: 'property', header: 'Property', width: 20 }, { key: 'value', header: 'Value', width: 35 } ], data: [ { property: 'Session ID', value: result.sessionId }, { property: 'Name', value: result.name }, { property: 'Source File', value: path.basename(absolutePath) }, { property: 'Agents Imported', value: result.stats.agentsImported }, { property: 'Tasks Imported', value: result.stats.tasksImported }, { property: 'Memory Entries', value: result.stats.memoryEntriesImported }, { property: 'Activated', value: result.activated ? 'Yes' : 'No' } ] }); output.writeln(); output.printSuccess(`Session imported: ${result.sessionId}`); if (!result.activated) { output.printInfo(`Restore with: claude-flow session restore ${result.sessionId}`); } if (ctx.flags.format === 'json') { output.printJson(result); } return { success: true, data: result }; } catch (error) { spinner.fail('Failed to import session'); if (error instanceof MCPClientError) { output.printError(`Error: ${error.message}`); } else if (error instanceof SyntaxError) { output.printError('Invalid file format. Expected JSON or YAML.'); } else { output.printError(`Unexpected error: ${String(error)}`); } return { success: false, exitCode: 1 }; } } }; // Current subcommand const currentCommand = { name: 'current', description: 'Show current active session', action: async (ctx) => { try { const result = await callMCPTool('session_current', { includeStats: true }); if (ctx.flags.format === 'json') { output.printJson(result); return { success: true, data: result }; } output.writeln(); output.writeln(output.bold('Current Session')); output.writeln(); output.printTable({ columns: [ { key: 'property', header: 'Property', width: 18 }, { key: 'value', header: 'Value', width: 35 } ], data: [ { property: 'Session ID', value: result.sessionId }, { property: 'Name', value: result.name || '-' }, { property: 'Status', value: formatStatus(result.status) }, { property: 'Started', value: new Date(result.startedAt).toLocaleString() }, { property: 'Duration', value: formatDuration(result.stats.duration) }, { property: 'Agents', value: result.stats.agentCount }, { property: 'Tasks', value: result.stats.taskCount }, { property: 'Memory Entries', value: result.stats.memoryEntries } ] }); return { success: true, data: result }; } catch (error) { if (error instanceof MCPClientError) { output.printWarning('No active session'); output.printInfo('Start a session with "claude-flow start"'); return { success: true, data: { active: false } }; } output.printError(`Unexpected error: ${String(error)}`); return { success: false, exitCode: 1 }; } } }; // Helper functions function formatSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } return `${seconds}s`; } function toSimpleYaml(obj, indent = 0) { // Simple YAML serializer (for basic types) if (obj === null) return 'null'; if (typeof obj === 'boolean') return String(obj); if (typeof obj === 'number') return String(obj); if (typeof obj === 'string') return obj.includes(':') ? `"${obj}"` : obj; const spaces = ' '.repeat(indent); let result = ''; if (Array.isArray(obj)) { for (const item of obj) { result += `${spaces}- ${toSimpleYaml(item, indent + 1).trim()}\n`; } return result; } if (typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { if (typeof value === 'object' && value !== null) { result += `${spaces}${key}:\n${toSimpleYaml(value, indent + 1)}`; } else { result += `${spaces}${key}: ${toSimpleYaml(value, indent)}\n`; } } return result; } return String(obj); } // Main session command export const sessionCommand = { name: 'session', description: 'Session management commands', subcommands: [ listCommand, saveCommand, restoreCommand, deleteCommand, exportCommand, importCommand, currentCommand ], options: [], examples: [ { command: 'claude-flow session list', description: 'List all sessions' }, { command: 'claude-flow session save -n "checkpoint-1"', description: 'Save current session' }, { command: 'claude-flow session restore session-123', description: 'Restore a session' }, { command: 'claude-flow session delete session-123', description: 'Delete a session' }, { command: 'claude-flow session export -o backup.json', description: 'Export session to file' }, { command: 'claude-flow session import backup.json', description: 'Import session from file' }, { command: 'claude-flow session current', description: 'Show current session' } ], action: async (ctx) => { // Show help if no subcommand output.writeln(); output.writeln(output.bold('Session Management Commands')); output.writeln(); output.writeln('Usage: claude-flow session <subcommand> [options]'); output.writeln(); output.writeln('Subcommands:'); output.printList([ `${output.highlight('list')} - List all sessions`, `${output.highlight('save')} - Save current session state`, `${output.highlight('restore')} - Restore a saved session`, `${output.highlight('delete')} - Delete a saved session`, `${output.highlight('export')} - Export session to file`, `${output.highlight('import')} - Import session from file`, `${output.highlight('current')} - Show current active session` ]); output.writeln(); output.writeln('Run "claude-flow session <subcommand> --help" for subcommand help'); return { success: true }; } }; export default sessionCommand; //# sourceMappingURL=session.js.map