interm-mcp
Version: 
MCP server for terminal applications and TUI automation with 127 tools
270 lines (269 loc) • 10.9 kB
JavaScript
import { TerminalManager } from './terminal-manager.js';
import { SessionManager } from './session-manager.js';
import { createTerminalError, handleError } from './utils/error-utils.js';
export class InteractionReplayManager {
    static instance;
    terminalManager;
    sessionManager;
    recordings = new Map();
    snapshots = new Map();
    diffs = new Map();
    activeRecordings = new Map();
    constructor() {
        this.terminalManager = TerminalManager.getInstance();
        this.sessionManager = SessionManager.getInstance();
    }
    static getInstance() {
        if (!InteractionReplayManager.instance) {
            InteractionReplayManager.instance = new InteractionReplayManager();
        }
        return InteractionReplayManager.instance;
    }
    async startRecording(sessionId, name, description) {
        try {
            const session = this.terminalManager.getSession(sessionId);
            if (!session) {
                throw createTerminalError('SESSION_NOT_FOUND', `Session ${sessionId} not found`);
            }
            const recordingId = `rec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
            this.activeRecordings.set(recordingId, {
                sessionId,
                name,
                startTime: new Date(),
                events: []
            });
            return recordingId;
        }
        catch (error) {
            throw handleError(error, `Failed to start recording for session ${sessionId}`);
        }
    }
    async stopRecording(recordingId) {
        try {
            const activeRecording = this.activeRecordings.get(recordingId);
            if (!activeRecording) {
                throw createTerminalError('RESOURCE_ERROR', `Recording ${recordingId} not found`);
            }
            const endTime = new Date();
            const totalDuration = endTime.getTime() - activeRecording.startTime.getTime();
            const recording = {
                id: recordingId,
                sessionId: activeRecording.sessionId,
                name: activeRecording.name,
                startTime: activeRecording.startTime,
                endTime,
                events: activeRecording.events,
                totalDuration,
                eventCount: activeRecording.events.length
            };
            this.recordings.set(recordingId, recording);
            this.activeRecordings.delete(recordingId);
            return recording;
        }
        catch (error) {
            throw handleError(error, `Failed to stop recording ${recordingId}`);
        }
    }
    async addEventToRecording(recordingId, event) {
        try {
            const activeRecording = this.activeRecordings.get(recordingId);
            if (!activeRecording) {
                return; // Recording might have been stopped
            }
            activeRecording.events.push(event);
        }
        catch (error) {
            console.error('Error adding event to recording:', error);
        }
    }
    async replayInteraction(recordingId, targetSessionId, speedMultiplier = 1.0) {
        try {
            const recording = this.recordings.get(recordingId);
            if (!recording) {
                throw createTerminalError('RESOURCE_ERROR', `Recording ${recordingId} not found`);
            }
            const session = this.terminalManager.getSession(targetSessionId);
            if (!session) {
                throw createTerminalError('SESSION_NOT_FOUND', `Session ${targetSessionId} not found`);
            }
            const adjustedSpeedMultiplier = Math.max(0.1, Math.min(10.0, speedMultiplier));
            for (let i = 0; i < recording.events.length; i++) {
                const event = recording.events[i];
                const nextEvent = recording.events[i + 1];
                // Execute the current event
                await this.executeEvent(targetSessionId, event);
                // Calculate delay until next event
                if (nextEvent) {
                    const originalDelay = nextEvent.timestamp.getTime() - event.timestamp.getTime();
                    const adjustedDelay = Math.max(10, originalDelay / adjustedSpeedMultiplier);
                    await new Promise(resolve => setTimeout(resolve, adjustedDelay));
                }
            }
        }
        catch (error) {
            throw handleError(error, `Failed to replay interaction ${recordingId}`);
        }
    }
    async executeEvent(sessionId, event) {
        try {
            if (event.type === 'keydown' || event.type === 'keypress') {
                const keyEvent = event;
                let input = keyEvent.key;
                // Handle special keys
                if (keyEvent.modifiers && keyEvent.modifiers.length > 0) {
                    if (keyEvent.modifiers.includes('ctrl') && keyEvent.key.length === 1) {
                        const charCode = keyEvent.key.toLowerCase().charCodeAt(0) - 96;
                        input = String.fromCharCode(charCode);
                    }
                }
                await this.terminalManager.sendInput(sessionId, input);
            }
            else if (event.type === 'click' || event.type === 'move') {
                const mouseEvent = event;
                // Mouse events would be handled by the mouse manager
                console.log(`Mouse event: ${mouseEvent.type} at (${mouseEvent.x}, ${mouseEvent.y})`);
            }
            else if (event.type === 'touch') {
                const touchEvent = event;
                // Touch events would be handled by the touch manager
                console.log(`Touch event: ${touchEvent.type} at (${touchEvent.x}, ${touchEvent.y})`);
            }
        }
        catch (error) {
            console.error('Error executing event:', error);
        }
    }
    async createStateSnapshot(sessionId, metadata) {
        try {
            const session = this.terminalManager.getSession(sessionId);
            if (!session) {
                throw createTerminalError('SESSION_NOT_FOUND', `Session ${sessionId} not found`);
            }
            const state = await this.terminalManager.getTerminalState(sessionId);
            const snapshotId = `snap_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
            const snapshot = {
                id: snapshotId,
                sessionId,
                timestamp: new Date(),
                state,
                metadata
            };
            this.snapshots.set(snapshotId, snapshot);
            return snapshotId;
        }
        catch (error) {
            throw handleError(error, `Failed to create state snapshot for session ${sessionId}`);
        }
    }
    async generateStateDiff(fromSnapshotId, toSnapshotId) {
        try {
            const fromSnapshot = this.snapshots.get(fromSnapshotId);
            const toSnapshot = this.snapshots.get(toSnapshotId);
            if (!fromSnapshot) {
                throw createTerminalError('RESOURCE_ERROR', `Snapshot ${fromSnapshotId} not found`);
            }
            if (!toSnapshot) {
                throw createTerminalError('RESOURCE_ERROR', `Snapshot ${toSnapshotId} not found`);
            }
            const diffId = `diff_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
            const changes = [];
            // Compare content
            if (fromSnapshot.state.content !== toSnapshot.state.content) {
                changes.push({
                    type: 'content',
                    oldValue: fromSnapshot.state.content,
                    newValue: toSnapshot.state.content,
                    path: 'state.content'
                });
            }
            // Compare cursor position
            const fromCursor = fromSnapshot.state.cursor;
            const toCursor = toSnapshot.state.cursor;
            if (fromCursor.x !== toCursor.x || fromCursor.y !== toCursor.y || fromCursor.visible !== toCursor.visible) {
                changes.push({
                    type: 'cursor',
                    oldValue: fromCursor,
                    newValue: toCursor,
                    path: 'state.cursor'
                });
            }
            // Compare dimensions
            const fromDim = fromSnapshot.state.dimensions;
            const toDim = toSnapshot.state.dimensions;
            if (fromDim.cols !== toDim.cols || fromDim.rows !== toDim.rows) {
                changes.push({
                    type: 'dimensions',
                    oldValue: fromDim,
                    newValue: toDim,
                    path: 'state.dimensions'
                });
            }
            // Generate summary
            let summary = '';
            if (changes.length === 0) {
                summary = 'No changes detected';
            }
            else {
                const changeTypes = changes.map(c => c.type);
                summary = `Changes detected in: ${changeTypes.join(', ')}`;
            }
            const diff = {
                id: diffId,
                fromSnapshotId,
                toSnapshotId,
                timestamp: new Date(),
                changes,
                summary
            };
            this.diffs.set(diffId, diff);
            return diffId;
        }
        catch (error) {
            throw handleError(error, `Failed to generate state diff between ${fromSnapshotId} and ${toSnapshotId}`);
        }
    }
    getRecording(recordingId) {
        return this.recordings.get(recordingId) || null;
    }
    getAllRecordings(sessionId) {
        const recordings = Array.from(this.recordings.values());
        return sessionId ? recordings.filter(r => r.sessionId === sessionId) : recordings;
    }
    getSnapshot(snapshotId) {
        return this.snapshots.get(snapshotId) || null;
    }
    getAllSnapshots(sessionId) {
        const snapshots = Array.from(this.snapshots.values());
        return sessionId ? snapshots.filter(s => s.sessionId === sessionId) : snapshots;
    }
    getDiff(diffId) {
        return this.diffs.get(diffId) || null;
    }
    getAllDiffs() {
        return Array.from(this.diffs.values());
    }
    getActiveRecordings() {
        return Array.from(this.activeRecordings.entries()).map(([id, recording]) => ({
            id,
            sessionId: recording.sessionId,
            name: recording.name,
            startTime: recording.startTime,
            eventCount: recording.events.length
        }));
    }
    async deleteRecording(recordingId) {
        return this.recordings.delete(recordingId);
    }
    async deleteSnapshot(snapshotId) {
        return this.snapshots.delete(snapshotId);
    }
    async deleteStateDiff(diffId) {
        return this.diffs.delete(diffId);
    }
    async cleanup() {
        this.recordings.clear();
        this.snapshots.clear();
        this.diffs.clear();
        this.activeRecordings.clear();
    }
}