UNPKG

context-forge

Version:

AI orchestration platform with autonomous teams, enhancement planning, migration tools, 25+ slash commands, checkpoints & hooks. Multi-IDE: Claude, Cursor, Windsurf, Cline, Copilot

308 lines 10.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TmuxManager = void 0; const child_process_1 = require("child_process"); const util_1 = require("util"); const execAsync = (0, util_1.promisify)(child_process_1.exec); class TmuxManager { constructor() { this.maxLinesCapture = 1000; this.safetyMode = true; } /** * Check if tmux is installed and available */ async checkTmuxAvailable() { try { await execAsync('which tmux'); return true; } catch { return false; } } /** * Get all tmux sessions */ async getSessions() { try { const { stdout } = await execAsync('tmux list-sessions -F "#{session_name}:#{session_attached}"'); if (!stdout.trim()) { return []; } const sessions = []; const lines = stdout.trim().split('\n'); for (const line of lines) { const [sessionName, attached] = line.split(':'); const windows = await this.getSessionWindows(sessionName); sessions.push({ name: sessionName, windows, attached: attached === '1', }); } return sessions; } catch (error) { // No sessions exist if (error instanceof Error && 'code' in error && error.code === 1 && error.message.includes('no server running')) { return []; } throw error; } } /** * Get windows for a specific session */ async getSessionWindows(sessionName) { try { const { stdout } = await execAsync(`tmux list-windows -t ${sessionName} -F "#{window_index}:#{window_name}:#{window_active}"`); if (!stdout.trim()) { return []; } return stdout .trim() .split('\n') .map((line) => { const [windowIndex, windowName, windowActive] = line.split(':'); return { sessionName, windowIndex: parseInt(windowIndex), windowName, active: windowActive === '1', }; }); } catch { return []; } } /** * Create a new tmux session */ async createSession(sessionName, workingDirectory) { const args = ['new-session', '-d', '-s', sessionName]; if (workingDirectory) { args.push('-c', workingDirectory); } await execAsync(`tmux ${args.join(' ')}`); } /** * Create a new window in a session */ async createWindow(config) { const args = [ 'new-window', '-t', `${config.sessionName}`, '-n', config.windowName, '-c', config.workingDirectory, ]; await execAsync(`tmux ${args.join(' ')}`); // Set window index if needed if (config.windowIndex !== undefined) { await this.moveWindow(config.sessionName, await this.getLastWindowIndex(config.sessionName), config.windowIndex); } // Run initial command if provided if (config.command) { await this.sendCommand(config.sessionName, config.windowIndex, config.command); } } /** * Rename a window */ async renameWindow(sessionName, windowIndex, newName) { await execAsync(`tmux rename-window -t ${sessionName}:${windowIndex} "${newName}"`); } /** * Send keys to a window */ async sendKeys(sessionName, windowIndex, keys) { await execAsync(`tmux send-keys -t ${sessionName}:${windowIndex} "${keys}"`); } /** * Send a command to a window (adds Enter automatically) */ async sendCommand(sessionName, windowIndex, command) { await this.sendKeys(sessionName, windowIndex, command); await execAsync(`tmux send-keys -t ${sessionName}:${windowIndex} Enter`); } /** * Send a message to a Claude agent window * Uses proper timing to ensure Claude receives the message */ async sendClaudeMessage(sessionName, windowIndex, message) { // Send the message await this.sendKeys(sessionName, windowIndex, message); // Wait for UI to register (critical for Claude) await new Promise((resolve) => setTimeout(resolve, 500)); // Send Enter await execAsync(`tmux send-keys -t ${sessionName}:${windowIndex} Enter`); } /** * Capture window content */ async captureWindowContent(sessionName, windowIndex, lines = 50) { const actualLines = Math.min(lines, this.maxLinesCapture); try { const { stdout } = await execAsync(`tmux capture-pane -t ${sessionName}:${windowIndex} -p -S -${actualLines}`); return stdout; } catch (error) { throw new Error(`Failed to capture window content: ${error}`); } } /** * Get window information */ async getWindowInfo(sessionName, windowIndex) { try { const { stdout } = await execAsync(`tmux display-message -t ${sessionName}:${windowIndex} -p "#{window_name}:#{window_active}:#{window_panes}"`); const [name, active, panes] = stdout.trim().split(':'); return { name, active: active === '1', panes: parseInt(panes), content: await this.captureWindowContent(sessionName, windowIndex), }; } catch (error) { throw new Error(`Failed to get window info: ${error}`); } } /** * Check if a session exists */ async sessionExists(sessionName) { try { await execAsync(`tmux has-session -t ${sessionName}`); return true; } catch { return false; } } /** * Kill a window */ async killWindow(sessionName, windowIndex) { await execAsync(`tmux kill-window -t ${sessionName}:${windowIndex}`); } /** * Kill a session */ async killSession(sessionName) { await execAsync(`tmux kill-session -t ${sessionName}`); } /** * Find windows by name pattern */ async findWindowsByName(pattern) { const sessions = await this.getSessions(); const matches = []; for (const session of sessions) { for (const window of session.windows) { if (window.windowName.toLowerCase().includes(pattern.toLowerCase())) { matches.push({ session: session.name, window: window.windowIndex, name: window.windowName, }); } } } return matches; } /** * Create a monitoring snapshot for all sessions */ async createMonitoringSnapshot() { const sessions = await this.getSessions(); const timestamp = new Date().toISOString(); let snapshot = `Tmux Monitoring Snapshot - ${timestamp}\n`; snapshot += '='.repeat(50) + '\n\n'; for (const session of sessions) { snapshot += `Session: ${session.name} (${session.attached ? 'ATTACHED' : 'DETACHED'})\n`; snapshot += '-'.repeat(30) + '\n'; for (const window of session.windows) { snapshot += ` Window ${window.windowIndex}: ${window.windowName}`; if (window.active) { snapshot += ' (ACTIVE)'; } snapshot += '\n'; try { const content = await this.captureWindowContent(session.name, window.windowIndex, 10); const lines = content.split('\n').filter((line) => line.trim()); if (lines.length > 0) { snapshot += ' Recent output:\n'; lines.forEach((line) => { snapshot += ` | ${line}\n`; }); } } catch { snapshot += ' | [Unable to capture content]\n'; } snapshot += '\n'; } } return snapshot; } /** * Wait for a window to contain specific text */ async waitForText(sessionName, windowIndex, text, timeout = 30000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { const content = await this.captureWindowContent(sessionName, windowIndex, 50); if (content.includes(text)) { return true; } } catch { // Window might not exist yet } await new Promise((resolve) => setTimeout(resolve, 1000)); } return false; } /** * Get the last window index for a session */ async getLastWindowIndex(sessionName) { const windows = await this.getSessionWindows(sessionName); return Math.max(...windows.map((w) => w.windowIndex), -1); } /** * Move a window to a new index */ async moveWindow(sessionName, from, to) { await execAsync(`tmux move-window -s ${sessionName}:${from} -t ${sessionName}:${to}`); } /** * Execute a shell command and return the result */ async executeShellCommand(command, cwd) { return execAsync(command, { cwd }); } /** * Convert AgentSession to TmuxWindowConfig */ agentSessionToWindowConfig(session, workingDirectory) { return { sessionName: session.sessionName, windowIndex: session.windowIndex, windowName: session.windowName, workingDirectory, command: 'claude', // Start Claude in the window }; } } exports.TmuxManager = TmuxManager; //# sourceMappingURL=tmuxManager.js.map