Skip to main content
Glama
ooples

MCP Console Automation Server

SmartExecutor.ts9.66 kB
import { ConsoleManager } from './ConsoleManager.js'; import type { ConsoleOutput, BackgroundJob } from '../types/index.js'; interface CommandAnalysis { isLongRunner: boolean; isQuick: boolean; isStreaming: boolean; estimatedDuration?: number; } interface ExecuteOptions { sessionId: string; args?: string[]; timeout?: number; env?: Record<string, string>; } interface UnifiedResult { success: boolean; output: string; exitCode?: number; duration?: number; strategyUsed: 'fast' | 'streaming' | 'background'; switched: boolean; switchReason?: string; } class OutputMonitor { private _outputSize: number = 0; private _lastActivity: number = Date.now(); private _isStillRunning: boolean = true; get outputSize(): number { return this._outputSize; } get isStillRunning(): boolean { return this._isStillRunning; } recordOutput(size: number): void { this._outputSize += size; this._lastActivity = Date.now(); } markCompleted(): void { this._isStillRunning = false; } getIdleTime(): number { return Date.now() - this._lastActivity; } } class CommandAnalyzer { private commandHistory: Map< string, { duration: number; outputSize: number } > = new Map(); analyze(command: string): CommandAnalysis { const longRunners = [ /npm\s+(install|ci|update)/, /pip\s+install/, /cargo\s+build/, /mvn\s+clean\s+install/, /gradle\s+build/, /(train|build|compile|bundle|webpack)/i, ]; const quickCommands = [ /^(ls|pwd|echo|cat|head|tail|grep|find)/, /^git\s+(status|log|diff|branch)/, /^(cd|mkdir|rm|mv|cp|touch)\s+/, ]; const streamingCommands = [ /tail\s+-f/, /watch\s+/, /journalctl\s+-f/, /kubectl\s+(logs|get|describe).*(-f|--follow)/, ]; const isLongRunner = longRunners.some((r) => r.test(command)); const isQuick = quickCommands.some((r) => r.test(command)); const isStreaming = streamingCommands.some((r) => r.test(command)); const estimatedDuration = this.estimateFromHistory(command); return { isLongRunner, isQuick, isStreaming, estimatedDuration, }; } private estimateFromHistory(command: string): number | undefined { const normalizedCommand = this.normalizeCommand(command); const history = this.commandHistory.get(normalizedCommand); return history?.duration; } private normalizeCommand(command: string): string { return command.split(/\s+/)[0].toLowerCase(); } recordExecution(command: string, duration: number, outputSize: number): void { const normalizedCommand = this.normalizeCommand(command); this.commandHistory.set(normalizedCommand, { duration, outputSize }); } } class StrategySelector { select(analysis: CommandAnalysis): 'fast' | 'streaming' | 'background' { if (analysis.isStreaming) return 'streaming'; if (analysis.isLongRunner) return 'background'; if (analysis.isQuick) return 'fast'; if (analysis.estimatedDuration) { if (analysis.estimatedDuration > 60000) return 'background'; if (analysis.estimatedDuration > 10000) return 'streaming'; } return 'fast'; } } class ResultAggregator { aggregate( output: ConsoleOutput[] | string, exitCode: number | undefined, duration: number | undefined, strategy: 'fast' | 'streaming' | 'background', switched: boolean, switchReason?: string ): UnifiedResult { const outputText = Array.isArray(output) ? output.map((chunk) => chunk.data).join('') : output; return { success: exitCode === 0 || exitCode === undefined, output: outputText, exitCode, duration, strategyUsed: strategy, switched, switchReason, }; } } export class SmartExecutor { private consoleManager: ConsoleManager; private analyzer: CommandAnalyzer; private selector: StrategySelector; private aggregator: ResultAggregator; constructor(consoleManager: ConsoleManager) { this.consoleManager = consoleManager; this.analyzer = new CommandAnalyzer(); this.selector = new StrategySelector(); this.aggregator = new ResultAggregator(); } async execute( command: string, options: ExecuteOptions ): Promise<UnifiedResult> { const analysis = this.analyzer.analyze(command); const strategy = this.selector.select(analysis); const startTime = Date.now(); try { let result: UnifiedResult; if (strategy === 'fast') { result = await this.executeFastPath(command, options); } else if (strategy === 'streaming') { result = await this.executeStreaming(command, options); } else { result = await this.executeBackground(command, options); } const duration = Date.now() - startTime; const outputSize = result.output.length; this.analyzer.recordExecution(command, duration, outputSize); return result; } catch (error: any) { throw new Error(`Smart execution failed: ${error.message}`); } } private async executeFastPath( command: string, options: ExecuteOptions ): Promise<UnifiedResult> { const monitor = new OutputMonitor(); try { const result = await this.consoleManager.executeCommandInSession( options.sessionId, command, options.args || [], options.timeout || 30000 ); monitor.markCompleted(); return this.aggregator.aggregate( result.output, result.exitCode, result.duration, 'fast', false ); } catch (error: any) { if (error.message.includes('timeout')) { return await this.switchToBackground(command, options, monitor); } throw error; } } private async executeStreaming( command: string, options: ExecuteOptions ): Promise<UnifiedResult> { const sessionId = options.sessionId; const chunks: string[] = []; await this.consoleManager.sendInput(sessionId, command + '\n'); let sequenceId = 0; let attempts = 0; const maxAttempts = 100; while (attempts < maxAttempts) { const output = await this.consoleManager.getOutputImmediate( sessionId, 50 ); if (output.length > sequenceId) { const newChunks = output.slice(sequenceId); chunks.push(...newChunks.map((c) => c.data)); sequenceId = output.length; } const lastChunk = output[output.length - 1]; if (lastChunk && this.isPromptLine(lastChunk.data)) { break; } attempts++; await this.sleep(200); } return this.aggregator.aggregate( chunks.join(''), 0, undefined, 'streaming', false ); } private async executeBackground( command: string, options: ExecuteOptions ): Promise<UnifiedResult> { const jobId = await this.consoleManager.startBackgroundJob( command, options.args || [], { command, args: options.args || [], sessionId: options.sessionId, timeout: options.timeout || 600000, } ); let status: BackgroundJob | null; let pollAttempts = 0; const maxPollAttempts = 600; do { await this.sleep(1000); status = await this.consoleManager.getBackgroundJobStatus(jobId); pollAttempts++; if (pollAttempts >= maxPollAttempts) { throw new Error('Background job polling exceeded maximum attempts'); } } while (status && status.status === 'running'); const outputArray = await this.consoleManager.getBackgroundJobOutput(jobId); const duration = status?.startedAt && status?.finishedAt ? new Date(status.finishedAt).getTime() - new Date(status.startedAt).getTime() : undefined; return this.aggregator.aggregate( outputArray.map((o) => o.data).join(''), status?.exitCode, duration, 'background', false ); } private async switchToBackground( command: string, options: ExecuteOptions, _monitor: OutputMonitor ): Promise<UnifiedResult> { try { const jobId = await this.consoleManager.startBackgroundJob( command, options.args || [], { command, args: options.args || [], sessionId: options.sessionId, timeout: 600000, } ); let status: BackgroundJob | null; do { await this.sleep(1000); status = await this.consoleManager.getBackgroundJobStatus(jobId); } while (status && status.status === 'running'); const outputArray = await this.consoleManager.getBackgroundJobOutput(jobId); const duration = status?.startedAt && status?.finishedAt ? new Date(status.finishedAt).getTime() - new Date(status.startedAt).getTime() : undefined; return this.aggregator.aggregate( outputArray.map((o) => o.data).join(''), status?.exitCode, duration, 'background', true, 'Command exceeded timeout, switched to background execution' ); } catch (error: any) { throw new Error(`Strategy switch to background failed: ${error.message}`); } } private isPromptLine(line: string): boolean { const promptPatterns = [ /[$#>]\s*$/, /PS\s+[A-Z]:\\.*>\s*$/, /^\s*~\s*[$#>]\s*$/, ]; return promptPatterns.some((pattern) => pattern.test(line.trim())); } private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server