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

306 lines 13 kB
/** * Terminal MCP Tools for CLI * * Terminal session management with real command execution. */ import { getProjectCwd } from './types.js'; import { existsSync } from 'node:fs'; import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js'; import { validateEnv, validateIdentifier, validatePath, validateText } from './validate-input.js'; import { join } from 'node:path'; import { execSync } from 'node:child_process'; // Storage paths const STORAGE_DIR = '.claude-flow'; const TERMINAL_DIR = 'terminals'; const TERMINAL_FILE = 'store.json'; function getTerminalDir() { return join(getProjectCwd(), STORAGE_DIR, TERMINAL_DIR); } function getTerminalPath() { return join(getTerminalDir(), TERMINAL_FILE); } function ensureTerminalDir() { const dir = getTerminalDir(); if (!existsSync(dir)) { mkdirRestricted(dir); } } function loadTerminalStore() { try { const path = getTerminalPath(); if (existsSync(path)) { // ADR-096 Phase 3: readFileMaybeEncrypted handles both legacy // plaintext stores and post-migration encrypted ones via the RFE1 // magic-byte sniff. return JSON.parse(readFileMaybeEncrypted(path, 'utf-8')); } } catch { // Return empty store } return { sessions: {}, version: '3.0.0' }; } function saveTerminalStore(store) { ensureTerminalDir(); // audit_1776853149979: terminal command history can contain credentials // pasted into commands; restrict to owner read/write (mode 0600). // ADR-096 Phase 3: opt-in AES-256-GCM encrypt-at-rest. Honored only // when CLAUDE_FLOW_ENCRYPT_AT_REST is set; otherwise legacy plaintext // path runs unchanged. writeFileRestricted(getTerminalPath(), JSON.stringify(store, null, 2), { encrypt: true }); } export const terminalTools = [ { name: 'terminal_create', description: 'Create a new terminal session Use when native Bash is wrong because you need a persistent terminal session across turns/agents with output capture and replay. For one-shot shell commands, native Bash is fine.', category: 'terminal', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Session name' }, workingDir: { type: 'string', description: 'Working directory' }, env: { type: 'object', description: 'Environment variables' }, }, }, handler: async (input) => { // Validate user-provided input (#1425, audit_1776853149979) if (input.name) { const v = validateText(input.name, 'name', 256); if (!v.valid) return { success: false, error: v.error }; } if (input.workingDir) { const v = validatePath(input.workingDir, 'workingDir'); if (!v.valid) return { success: false, error: v.error }; } // env is merged into execSync's process env on every command; reject // loader/runtime hijack vars (LD_PRELOAD, NODE_OPTIONS, …) and enforce // POSIX-shaped names + null-byte-free values. const vEnv = validateEnv(input.env, 'env'); if (!vEnv.valid) return { success: false, error: vEnv.error }; const store = loadTerminalStore(); const id = `term-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const session = { id, name: input.name || `Terminal ${Object.keys(store.sessions).length + 1}`, status: 'active', createdAt: new Date().toISOString(), lastActivity: new Date().toISOString(), workingDir: input.workingDir || getProjectCwd(), history: [], env: vEnv.sanitized, }; store.sessions[id] = session; saveTerminalStore(store); return { success: true, sessionId: id, name: session.name, status: session.status, workingDir: session.workingDir, createdAt: session.createdAt, }; }, }, { name: 'terminal_execute', description: 'Execute a command in a terminal session Use when native Bash is wrong because you need a persistent terminal session across turns/agents with output capture and replay. For one-shot shell commands, native Bash is fine.', category: 'terminal', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Terminal session ID' }, command: { type: 'string', description: 'Command to execute' }, timeout: { type: 'number', description: 'Command timeout in ms' }, captureOutput: { type: 'boolean', description: 'Capture command output' }, }, required: ['command'], }, handler: async (input) => { // Validate user-provided input (#1425) const vCmd = validateText(input.command, 'command', 10_000); if (!vCmd.valid) return { success: false, error: vCmd.error }; if (input.sessionId) { const v = validateIdentifier(input.sessionId, 'sessionId'); if (!v.valid) return { success: false, error: v.error }; } const store = loadTerminalStore(); const sessionId = input.sessionId; const command = input.command; // Find or create default session let session = sessionId ? store.sessions[sessionId] : Object.values(store.sessions).find(s => s.status === 'active'); if (!session) { // Create default session const id = `term-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; session = { id, name: 'Default Terminal', status: 'active', createdAt: new Date().toISOString(), lastActivity: new Date().toISOString(), workingDir: getProjectCwd(), history: [], env: {}, }; store.sessions[id] = session; } const timeout = input.timeout || 30_000; const cwd = session.workingDir || getProjectCwd(); const startTime = Date.now(); let output; let exitCode; try { output = execSync(command, { cwd, encoding: 'utf-8', timeout, maxBuffer: 5 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ...session.env }, }); exitCode = 0; } catch (err) { output = (err.stdout || '') + (err.stderr ? `\n[stderr] ${err.stderr}` : ''); exitCode = err.status ?? 1; } const duration = Date.now() - startTime; const timestamp = new Date().toISOString(); // Record in history session.history.push({ command, output, timestamp, exitCode, }); session.lastActivity = timestamp; session.status = 'active'; saveTerminalStore(store); return { success: exitCode === 0, sessionId: session.id, command, output, exitCode, executedAt: timestamp, duration, }; }, }, { name: 'terminal_list', description: 'List all terminal sessions Use when native Bash is wrong because you need a persistent terminal session across turns/agents with output capture and replay. For one-shot shell commands, native Bash is fine.', category: 'terminal', inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['all', 'active', 'idle', 'closed'], description: 'Filter by status' }, includeHistory: { type: 'boolean', description: 'Include command history' }, }, }, handler: async (input) => { const store = loadTerminalStore(); let sessions = Object.values(store.sessions); if (input.status && input.status !== 'all') { sessions = sessions.filter(s => s.status === input.status); } return { sessions: sessions.map(s => ({ id: s.id, name: s.name, status: s.status, workingDir: s.workingDir, createdAt: s.createdAt, lastActivity: s.lastActivity, historyLength: s.history.length, ...(input.includeHistory ? { history: s.history.slice(-10) } : {}), })), total: sessions.length, active: sessions.filter(s => s.status === 'active').length, }; }, }, { name: 'terminal_close', description: 'Close a terminal session Use when native Bash is wrong because you need a persistent terminal session across turns/agents with output capture and replay. For one-shot shell commands, native Bash is fine.', category: 'terminal', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session ID to close' }, force: { type: 'boolean', description: 'Force close' }, }, required: ['sessionId'], }, handler: async (input) => { // Validate user-provided input (#1425) const vId = validateIdentifier(input.sessionId, 'sessionId'); if (!vId.valid) return { success: false, error: vId.error }; const store = loadTerminalStore(); const sessionId = input.sessionId; const session = store.sessions[sessionId]; if (!session) { return { success: false, error: 'Session not found' }; } session.status = 'closed'; saveTerminalStore(store); return { success: true, sessionId, closedAt: new Date().toISOString(), }; }, }, { name: 'terminal_history', description: 'Get command history for a terminal session Use when native Bash is wrong because you need a persistent terminal session across turns/agents with output capture and replay. For one-shot shell commands, native Bash is fine.', category: 'terminal', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session ID' }, limit: { type: 'number', description: 'Number of entries to return' }, offset: { type: 'number', description: 'Offset from latest' }, }, }, handler: async (input) => { // Validate user-provided input (#1425) if (input.sessionId) { const v = validateIdentifier(input.sessionId, 'sessionId'); if (!v.valid) return { success: false, error: v.error }; } const store = loadTerminalStore(); const sessionId = input.sessionId; const limit = input.limit || 50; const offset = input.offset || 0; if (sessionId) { const session = store.sessions[sessionId]; if (!session) { return { success: false, error: 'Session not found' }; } const history = session.history.slice(-(limit + offset), offset ? -offset : undefined); return { sessionId, history, total: session.history.length, }; } // Return combined history from all sessions const allHistory = Object.values(store.sessions) .flatMap(s => s.history.map(h => ({ ...h, sessionId: s.id }))) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .slice(offset, offset + limit); return { history: allHistory, total: allHistory.length, }; }, }, ]; //# sourceMappingURL=terminal-tools.js.map