mcpretentious
Version:
MCPretentious - Universal Terminal MCP. High-performance terminal automation for iTerm2 (WebSocket) and tmux (control mode). Cross-platform support with cursor position, colors, and layered screenshots.
215 lines (167 loc) • 6.77 kB
JavaScript
/**
* iTerm2 backend implementation for MCPretentious
* Uses WebSocket API with Protocol Buffers for high-performance terminal control
*/
import { TerminalBackend } from './terminal-backend.js';
import { getClient } from './iterm2-client.js';
import { getCurrentFocus, restoreFocus } from './focus-manager.js';
import { ITERM_DEFAULTS } from './constants.js';
import { execSync } from 'child_process';
import { platform } from 'os';
import { generateMouseEvent } from './mouse-sgr-protocol.js';
export class ITerm2Backend extends TerminalBackend {
constructor() {
super();
this.client = null;
}
async init() {
if (!this.client) {
this.client = await getClient();
}
return this.client;
}
async isAvailable() {
// iTerm2 only available on macOS
if (platform() !== 'darwin') {
return false;
}
try {
// Check if iTerm2 is installed
execSync('osascript -e \'tell application "System Events" to name of application processes\' | grep -q iTerm', {
stdio: 'ignore'
});
// Try to initialize client to verify API is enabled
await this.init();
return true;
} catch (error) {
return false;
}
}
getName() {
return 'iTerm2';
}
getType() {
return 'iterm';
}
async createSession(options = {}) {
await this.init();
const columns = options.columns || ITERM_DEFAULTS.COLUMNS;
const rows = options.rows || ITERM_DEFAULTS.ROWS;
// Store current focus to restore later
const previousFocus = await getCurrentFocus();
// Create new tab (passing null for windowId creates a new window)
// TODO: In the future, we should properly handle window creation with dimensions
const sessionId = await this.client.createTab(null, null);
if (!sessionId) {
throw new Error('Failed to create iTerm2 session');
}
// Note: columns and rows parameters are currently ignored for iTerm2
// iTerm2's createTab doesn't support setting dimensions directly
// Restore focus to previous application
if (previousFocus) {
await restoreFocus(previousFocus);
}
// Return terminal ID in new format
return TerminalBackend.generateTerminalId('iterm', sessionId);
}
async closeSession(sessionId) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
return await this.client.closeSession(actualSessionId, true);
}
async listSessions() {
await this.init();
const sessions = await this.client.listSessions();
// Convert to terminal IDs
return sessions.map(session => ({
terminalId: TerminalBackend.generateTerminalId('iterm', session.uniqueIdentifier),
sessionId: session.uniqueIdentifier,
backend: this.getName()
}));
}
async sendText(sessionId, text) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
return await this.client.sendText(actualSessionId, text);
}
async getScreenContents(sessionId, includeStyles = false) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
return await this.client.getScreenContents(actualSessionId, includeStyles);
}
async getSessionInfo(sessionId) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
return await this.client.getSessionInfo(actualSessionId);
}
async setSessionSize(sessionId, columns, rows) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
return await this.client.setSessionSize(actualSessionId, columns, rows);
}
async getProperty(sessionId, property) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
return await this.client.getProperty(actualSessionId, property);
}
/**
* Send mouse event using SGR protocol
* @param {string} sessionId - Target session
* @param {string} event - Event type ('press', 'release', 'drag')
* @param {number} x - X coordinate (0-based)
* @param {number} y - Y coordinate (0-based)
* @param {string|number} button - Button name or code
* @param {Object} modifiers - Modifier keys ({shift: bool, alt: bool, ctrl: bool})
* @returns {Promise<boolean>} Success status
*/
async sendMouseEvent(sessionId, event, x, y, button, modifiers = {}) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
// Generate SGR escape sequence using the shared protocol function
const sequence = generateMouseEvent(event, x, y, button, modifiers);
// Send the sequence to the terminal
await this.client.sendText(actualSessionId, sequence);
return true;
}
async close() {
if (this.client) {
this.client.close();
this.client = null;
}
}
isValidSessionId(sessionId) {
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
// iTerm2 uses UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(actualSessionId);
}
/**
* Check if a session exists
* @param {string} sessionId - Session to check
* @returns {Promise<boolean>}
*/
async sessionExists(sessionId) {
await this.init();
// Extract actual session ID if full terminal ID was passed
const parsed = TerminalBackend.parseTerminalId(sessionId);
const actualSessionId = parsed ? parsed.sessionId : sessionId;
const sessions = await this.client.listSessions();
return sessions.some(s => s.uniqueIdentifier === actualSessionId);
}
}