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
JavaScript
/**
* 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