Skip to main content
Glama
ooples

MCP Console Automation Server

PromptDetector.ts20.5 kB
import { Logger } from '../utils/logger.js'; import stripAnsi from 'strip-ansi'; /** * Shell prompt patterns for different environments */ export interface PromptPattern { name: string; pattern: RegExp; description: string; shellType?: string; priority: number; contextual?: boolean; multiline?: boolean; } /** * Configuration for prompt detection per session */ export interface PromptDetectorConfig { sessionId: string; shellType?: | 'bash' | 'zsh' | 'fish' | 'tcsh' | 'csh' | 'dash' | 'sh' | 'powershell' | 'cmd' | 'auto'; hostname?: string; username?: string; customPrompts?: PromptPattern[]; enableAnsiStripping?: boolean; enableMultilineDetection?: boolean; timeout?: number; maxOutputBuffer?: number; adaptiveLearning?: boolean; } /** * Result of prompt detection attempt */ export interface PromptDetectionResult { detected: boolean; pattern?: PromptPattern; matchedText: string; confidence: number; position: number; context: { beforePrompt: string; promptLine: string; afterPrompt: string; }; timeTaken: number; } /** * Advanced prompt detector for SSH and local shells * Handles ANSI escape sequences, multiple shell types, and adaptive learning */ export class PromptDetector { private logger: Logger; private sessionConfigs: Map<string, PromptDetectorConfig> = new Map(); private learnedPrompts: Map<string, PromptPattern[]> = new Map(); private outputBuffers: Map<string, string> = new Map(); // Pre-defined shell prompt patterns with high accuracy private readonly DEFAULT_PATTERNS: PromptPattern[] = [ // Bash prompts { name: 'bash_standard', pattern: /(?:^|\n)([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:[~\/][^$#\n]*\$)\s*$/m, description: 'Standard bash prompt: user@host:path$', shellType: 'bash', priority: 10, contextual: true, }, { name: 'bash_root', pattern: /(?:^|\n)([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:[~\/][^$#\n]*#)\s*$/m, description: 'Root bash prompt: user@host:path#', shellType: 'bash', priority: 10, contextual: true, }, { name: 'bash_simple', pattern: /(?:^|\n)(\$)\s*$/m, description: 'Simple bash prompt: $', shellType: 'bash', priority: 5, }, { name: 'bash_root_simple', pattern: /(?:^|\n)(#)\s*$/m, description: 'Simple root prompt: #', shellType: 'bash', priority: 5, }, // Zsh prompts { name: 'zsh_standard', pattern: /(?:^|\n)([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\s+[~\/][^%#\n]*\s*[%#])\s*$/m, description: 'Standard zsh prompt: user@host path %', shellType: 'zsh', priority: 10, contextual: true, }, { name: 'zsh_simple', pattern: /(?:^|\n)([%])\s*$/m, description: 'Simple zsh prompt: %', shellType: 'zsh', priority: 5, }, // Fish shell prompts { name: 'fish_standard', pattern: /(?:^|\n)([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\s+[~\/][^>\n]*>)\s*$/m, description: 'Fish shell prompt: user@host path>', shellType: 'fish', priority: 10, contextual: true, }, // Ubuntu-specific patterns { name: 'ubuntu_standard', pattern: /(?:^|\n)(ubuntu@[a-zA-Z0-9._-]+:[~\/][^$\n]*\$)\s*$/m, description: 'Ubuntu default prompt: ubuntu@hostname:path$', shellType: 'bash', priority: 15, contextual: true, }, // Generic Unix prompts { name: 'unix_user_colon', pattern: /(?:^|\n)([a-zA-Z0-9._-]+:[~\/][^$#\n]*\$)\s*$/m, description: 'Unix-style user prompt: user:path$', priority: 7, contextual: true, }, { name: 'unix_root_colon', pattern: /(?:^|\n)([a-zA-Z0-9._-]+:[~\/][^$#\n]*#)\s*$/m, description: 'Unix-style root prompt: user:path#', priority: 7, contextual: true, }, // Alpine Linux prompts { name: 'alpine_standard', pattern: /(?:^|\n)([a-zA-Z0-9._-]+:[~\/][^#\n]*#)\s*$/m, description: 'Alpine Linux prompt: hostname:path#', priority: 8, contextual: true, }, // Docker container prompts { name: 'docker_root', pattern: /(?:^|\n)(root@[a-zA-Z0-9]+:[~\/][^#\n]*#)\s*$/m, description: 'Docker root prompt: root@containerid:path#', priority: 12, contextual: true, }, // Windows prompts (if running WSL or Windows SSH) { name: 'windows_cmd', pattern: /(?:^|\n)([A-Z]:[\\\/][^>]*>)\s*$/m, description: 'Windows CMD prompt: C:\\path>', shellType: 'cmd', priority: 8, contextual: true, }, { name: 'powershell_standard', pattern: /(?:^|\n)(PS\s+[A-Z]:[\\\/][^>]*>)\s*$/m, description: 'PowerShell prompt: PS C:\\path>', shellType: 'powershell', priority: 8, contextual: true, }, // Minimal prompts (low priority, used as fallbacks) { name: 'minimal_dollar', pattern: /(?:^|\n)(\$)\s*$/m, description: 'Minimal dollar prompt: $', priority: 2, }, { name: 'minimal_hash', pattern: /(?:^|\n)(#)\s*$/m, description: 'Minimal hash prompt: #', priority: 2, }, { name: 'minimal_percent', pattern: /(?:^|\n)(%)\s*$/m, description: 'Minimal percent prompt: %', priority: 2, }, { name: 'minimal_gt', pattern: /(?:^|\n)(>)\s*$/m, description: 'Minimal greater-than prompt: >', priority: 1, }, // Multi-line prompts { name: 'multiline_arrow', pattern: /(?:^|\n)[^\n]*\n(\s*[-=]>\s*)$/m, description: 'Multi-line arrow prompt', priority: 6, multiline: true, }, // Colored prompts (after ANSI stripping, will look for common patterns) { name: 'colored_user_host', pattern: /(?:^|\n)([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+.*[:\s][~\/][^$#%>\n]*[$#%>])\s*$/m, description: 'Colored user@host prompt (ANSI stripped)', priority: 9, contextual: true, }, ]; constructor() { this.logger = new Logger('PromptDetector'); } /** * Configure prompt detection for a session */ configureSession(config: PromptDetectorConfig): void { this.sessionConfigs.set(config.sessionId, { enableAnsiStripping: true, enableMultilineDetection: true, timeout: 5000, maxOutputBuffer: 10000, adaptiveLearning: true, ...config, }); this.outputBuffers.set(config.sessionId, ''); this.learnedPrompts.set(config.sessionId, []); this.logger.info( `Configured prompt detection for session ${config.sessionId}`, { shellType: config.shellType, customPrompts: config.customPrompts?.length || 0, adaptiveLearning: config.adaptiveLearning, } ); } /** * Remove configuration for a session */ removeSession(sessionId: string): void { this.sessionConfigs.delete(sessionId); this.outputBuffers.delete(sessionId); this.learnedPrompts.delete(sessionId); this.logger.debug( `Removed prompt detection config for session ${sessionId}` ); } /** * Add new output data and detect prompts */ addOutput(sessionId: string, data: string): PromptDetectionResult | null { const config = this.sessionConfigs.get(sessionId); if (!config) { this.logger.warn(`No configuration found for session ${sessionId}`); return null; } // Append to buffer let currentBuffer = this.outputBuffers.get(sessionId) || ''; currentBuffer += data; // Limit buffer size if (currentBuffer.length > config.maxOutputBuffer!) { currentBuffer = currentBuffer.slice(-config.maxOutputBuffer!); } this.outputBuffers.set(sessionId, currentBuffer); // Attempt prompt detection return this.detectPrompt(sessionId, currentBuffer); } /** * Detect prompt in the given output */ detectPrompt( sessionId: string, output: string ): PromptDetectionResult | null { const startTime = Date.now(); const config = this.sessionConfigs.get(sessionId); if (!config) { return null; } // Strip ANSI escape sequences if enabled let cleanOutput = output; if (config.enableAnsiStripping) { cleanOutput = this.stripAnsiSequences(output); } // Get all available patterns const patterns = this.getAvailablePatterns(sessionId); let bestMatch: PromptDetectionResult | null = null; let highestConfidence = 0; // Try each pattern for (const pattern of patterns) { const result = this.tryPattern(pattern, cleanOutput, output); if (result.detected && result.confidence > highestConfidence) { highestConfidence = result.confidence; bestMatch = result; } } if (bestMatch) { bestMatch.timeTaken = Date.now() - startTime; // Learn from successful detection if adaptive learning is enabled if (config.adaptiveLearning && bestMatch.confidence > 0.8) { this.learnFromDetection(sessionId, bestMatch); } this.logger.debug(`Prompt detected for session ${sessionId}`, { pattern: bestMatch.pattern?.name, confidence: bestMatch.confidence, matchedText: bestMatch.matchedText.substring(0, 50), timeTaken: bestMatch.timeTaken, }); } return bestMatch; } /** * Wait for a prompt to appear in the output */ async waitForPrompt( sessionId: string, timeout?: number ): Promise<PromptDetectionResult> { const config = this.sessionConfigs.get(sessionId); const actualTimeout = timeout || config?.timeout || 5000; const startTime = Date.now(); return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { const currentOutput = this.outputBuffers.get(sessionId) || ''; const result = this.detectPrompt(sessionId, currentOutput); if (result && result.detected) { clearInterval(checkInterval); resolve(result); return; } if (Date.now() - startTime > actualTimeout) { clearInterval(checkInterval); reject( new Error( `Timeout waiting for prompt in session ${sessionId} after ${actualTimeout}ms` ) ); return; } }, 100); }); } /** * Enhanced waitForOutput method with prompt-aware matching */ async waitForOutput( sessionId: string, pattern: string | RegExp, options: { timeout?: number; requirePrompt?: boolean; stripAnsi?: boolean; multiline?: boolean; } = {} ): Promise<{ output: string; promptDetected?: PromptDetectionResult }> { const config = this.sessionConfigs.get(sessionId); const timeout = options.timeout || config?.timeout || 5000; const startTime = Date.now(); const searchRegex = typeof pattern === 'string' ? new RegExp(pattern) : pattern; return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { let currentOutput = this.outputBuffers.get(sessionId) || ''; // Strip ANSI if requested if (options.stripAnsi !== false) { currentOutput = this.stripAnsiSequences(currentOutput); } // Check if pattern matches const patternMatch = searchRegex.test(currentOutput); // Check for prompt if required let promptResult: PromptDetectionResult | null = null; if (options.requirePrompt) { promptResult = this.detectPrompt(sessionId, currentOutput); } // Resolve if pattern matches and prompt is found (if required) if ( patternMatch && (!options.requirePrompt || (promptResult && promptResult.detected)) ) { clearInterval(checkInterval); resolve({ output: currentOutput, promptDetected: promptResult || undefined, }); return; } if (Date.now() - startTime > timeout) { clearInterval(checkInterval); // Provide debug information on timeout const debugInfo = { sessionId, pattern: pattern.toString(), outputLength: currentOutput.length, lastOutput: currentOutput.slice(-200), promptResult: promptResult ? { detected: promptResult.detected, confidence: promptResult.confidence, pattern: promptResult.pattern?.name, } : null, }; reject( new Error( `Timeout waiting for pattern: ${pattern}. Debug: ${JSON.stringify(debugInfo)}` ) ); return; } }, 100); }); } /** * Clear output buffer for session */ clearBuffer(sessionId: string): void { this.outputBuffers.set(sessionId, ''); this.logger.debug(`Cleared output buffer for session ${sessionId}`); } /** * Get current output buffer content */ getBuffer(sessionId: string): string { return this.outputBuffers.get(sessionId) || ''; } /** * Get learned patterns for a session */ getLearnedPatterns(sessionId: string): PromptPattern[] { return this.learnedPrompts.get(sessionId) || []; } /** * Manually add a learned pattern */ addLearnedPattern(sessionId: string, pattern: PromptPattern): void { const learned = this.learnedPrompts.get(sessionId) || []; learned.push(pattern); this.learnedPrompts.set(sessionId, learned); this.logger.info(`Added learned pattern for session ${sessionId}`, { patternName: pattern.name, description: pattern.description, }); } /** * Get statistics about prompt detection */ getDetectionStats(sessionId: string): { totalPatterns: number; learnedPatterns: number; bufferSize: number; shellType?: string; } { const config = this.sessionConfigs.get(sessionId); const learned = this.learnedPrompts.get(sessionId) || []; const buffer = this.outputBuffers.get(sessionId) || ''; return { totalPatterns: this.getAvailablePatterns(sessionId).length, learnedPatterns: learned.length, bufferSize: buffer.length, shellType: config?.shellType, }; } // Private helper methods private stripAnsiSequences(text: string): string { // Use strip-ansi for basic ANSI removal, then additional cleanup let cleaned = stripAnsi(text); // Additional ANSI sequence patterns that might be missed const additionalAnsiPatterns = [ /\x1B\[[\d;]*[mK]/g, // CSI sequences /\x1B\][\d;]*;[^\x07]*\x07/g, // OSC sequences /\x1B\[\?[\d;]*[hlH]/g, // Private mode sequences /\x1B\[[\d;]*[ABCDEFGJKST]/g, // Cursor control /\x1B[=>]/g, // Application keypad /\r/g, // Carriage returns that might interfere ]; additionalAnsiPatterns.forEach((pattern) => { cleaned = cleaned.replace(pattern, ''); }); // Clean up excessive whitespace but preserve structure cleaned = cleaned.replace(/\r\n/g, '\n'); // Normalize line endings cleaned = cleaned.replace(/\n\n+/g, '\n\n'); // Limit consecutive newlines return cleaned; } private getAvailablePatterns(sessionId: string): PromptPattern[] { const config = this.sessionConfigs.get(sessionId); const learned = this.learnedPrompts.get(sessionId) || []; let patterns: PromptPattern[] = [...this.DEFAULT_PATTERNS]; // Add custom patterns from config if (config?.customPrompts) { patterns = [...patterns, ...config.customPrompts]; } // Add learned patterns (with higher priority) const boostedLearned = learned.map((p) => ({ ...p, priority: p.priority + 5, })); patterns = [...patterns, ...boostedLearned]; // Filter by shell type if specified if (config?.shellType && config.shellType !== 'auto') { patterns = patterns.filter( (p) => !p.shellType || p.shellType === config.shellType ); } // Sort by priority (highest first) return patterns.sort((a, b) => b.priority - a.priority); } private tryPattern( pattern: PromptPattern, cleanOutput: string, originalOutput: string ): PromptDetectionResult { const match = pattern.pattern.exec(cleanOutput); if (!match) { return { detected: false, matchedText: '', confidence: 0, position: -1, context: { beforePrompt: '', promptLine: '', afterPrompt: '', }, timeTaken: 0, }; } const matchedText = match[1] || match[0]; const position = match.index || 0; // Calculate confidence based on various factors let confidence = this.calculateConfidence( pattern, matchedText, cleanOutput, position ); // Extract context const beforePrompt = cleanOutput.substring(0, position); const promptLine = matchedText; const afterPrompt = cleanOutput.substring(position + matchedText.length); return { detected: true, pattern, matchedText, confidence, position, context: { beforePrompt: beforePrompt.split('\n').slice(-2).join('\n'), promptLine, afterPrompt: afterPrompt.split('\n').slice(0, 2).join('\n'), }, timeTaken: 0, // Will be set by caller }; } private calculateConfidence( pattern: PromptPattern, matchedText: string, output: string, position: number ): number { let confidence = 0.5; // Base confidence // Higher confidence for contextual patterns if (pattern.contextual && matchedText.includes('@')) { confidence += 0.2; } // Higher confidence if match is at end of output const isAtEnd = position + matchedText.length >= output.trim().length; if (isAtEnd) { confidence += 0.2; } // Higher confidence for longer, more specific matches if (matchedText.length > 10) { confidence += 0.1; } // Higher confidence if preceded by command output const beforeMatch = output.substring(0, position); if (beforeMatch.includes('\n') && beforeMatch.trim().length > 0) { confidence += 0.1; } // Adjust based on pattern priority confidence += pattern.priority / 20; // Ensure confidence is between 0 and 1 return Math.min(1, Math.max(0, confidence)); } private learnFromDetection( sessionId: string, result: PromptDetectionResult ): void { if (!result.pattern || !result.detected) return; const learned = this.learnedPrompts.get(sessionId) || []; // Check if we already learned a similar pattern const similarExists = learned.some( (p) => p.pattern.toString() === result.pattern!.pattern.toString() || p.name === result.pattern!.name ); if (!similarExists && learned.length < 10) { // Limit learned patterns const learnedPattern: PromptPattern = { name: `learned_${Date.now()}`, pattern: result.pattern.pattern, description: `Learned pattern from ${result.pattern.description}`, shellType: result.pattern.shellType, priority: Math.min(15, result.pattern.priority + 3), // Boost priority but cap it contextual: result.pattern.contextual, }; learned.push(learnedPattern); this.learnedPrompts.set(sessionId, learned); this.logger.debug(`Learned new prompt pattern for session ${sessionId}`, { pattern: learnedPattern.name, confidence: result.confidence, }); } } /** * Get debug information about current detection state */ getDebugInfo(sessionId: string): any { const config = this.sessionConfigs.get(sessionId); const buffer = this.outputBuffers.get(sessionId) || ''; const learned = this.learnedPrompts.get(sessionId) || []; const patterns = this.getAvailablePatterns(sessionId); return { sessionId, config, bufferLength: buffer.length, bufferPreview: buffer.slice(-200), learnedPatternCount: learned.length, totalPatternCount: patterns.length, availablePatterns: patterns.map((p) => ({ name: p.name, priority: p.priority, shellType: p.shellType, })), }; } }

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