Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,12 +651,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {

STATES DETECTED:
Process waiting for input (shows prompt)
Process finished execution
Process finished execution
Process running (use read_process_output)


PERFORMANCE DEBUGGING (verbose_timing parameter):
Set verbose_timing: true to get detailed timing information including:
- Exit reason (early_exit_quick_pattern, early_exit_periodic_check, process_exit, timeout)
- Total duration and time to first output
- Complete timeline of all output events with timestamps
- Which detection mechanism triggered early exit
Use this to identify missed optimization opportunities and improve detection patterns.

ALWAYS USE FOR: Local file analysis, CSV processing, data exploration, system commands
NEVER USE ANALYSIS TOOL FOR: Local file access (analysis tool is browser-only and WILL FAIL)

${PATH_GUIDANCE}
${CMD_PREFIX_DESCRIPTION}`,
inputSchema: zodToJsonSchema(StartProcessArgsSchema),
Expand Down Expand Up @@ -690,7 +698,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
Process waiting for input (ready for interact_with_process)
Process finished execution
Timeout reached (may still be running)


PERFORMANCE DEBUGGING (verbose_timing parameter):
Set verbose_timing: true to get detailed timing information including:
- Exit reason (early_exit_quick_pattern, early_exit_periodic_check, process_finished, timeout)
- Total duration and time to first output
- Complete timeline of all output events with timestamps
- Which detection mechanism triggered early exit
Use this to identify when timeouts could be reduced or detection patterns improved.

${CMD_PREFIX_DESCRIPTION}`,
inputSchema: zodToJsonSchema(ReadProcessOutputArgsSchema),
annotations: {
Expand Down Expand Up @@ -741,12 +757,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
- input: Code/command to execute
- timeout_ms: Max wait (default: 8000ms)
- wait_for_prompt: Auto-wait for response (default: true)

- verbose_timing: Enable detailed performance telemetry (default: false)

Returns execution result with status indicators.


PERFORMANCE DEBUGGING (verbose_timing parameter):
Set verbose_timing: true to get detailed timing information including:
- Exit reason (early_exit_quick_pattern, early_exit_periodic_check, process_finished, timeout, no_wait)
- Total duration and time to first output
- Complete timeline of all output events with timestamps
- Which detection mechanism triggered early exit
Use this to identify slow interactions and optimize detection patterns.

ALWAYS USE FOR: CSV analysis, JSON processing, file statistics, data visualization prep, ANY local file work
NEVER USE ANALYSIS TOOL FOR: Local file access (it cannot read files from disk and WILL FAIL)

${CMD_PREFIX_DESCRIPTION}`,
inputSchema: zodToJsonSchema(InteractWithProcessArgsSchema),
annotations: {
Expand Down
94 changes: 79 additions & 15 deletions src/terminal-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawn } from 'child_process';
import { TerminalSession, CommandExecutionResult, ActiveSession } from './types.js';
import { TerminalSession, CommandExecutionResult, ActiveSession, TimingInfo, OutputEvent } from './types.js';
import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
import { configManager } from './config-manager.js';
import {capture} from "./utils/capture.js";
Expand Down Expand Up @@ -43,7 +43,7 @@ export class TerminalManager {
}
}

async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string, collectTiming: boolean = false): Promise<CommandExecutionResult> {
// Get the shell from config if not specified
let shellToUse: string | boolean | undefined = shell;
if (!shellToUse) {
Expand All @@ -55,29 +55,29 @@ export class TerminalManager {
shellToUse = true;
}
}

// For REPL interactions, we need to ensure stdin, stdout, and stderr are properly configured
// Note: No special stdio options needed here, Node.js handles pipes by default

// Enhance SSH commands automatically
let enhancedCommand = command;
if (command.trim().startsWith('ssh ') && !command.includes(' -t')) {
enhancedCommand = command.replace(/^ssh /, 'ssh -t ');
console.log(`Enhanced SSH command: ${enhancedCommand}`);
}
const spawnOptions: any = {

const spawnOptions: any = {
shell: shellToUse,
env: {
...process.env,
TERM: 'xterm-256color' // Better terminal compatibility
}
};

// Spawn the process with an empty array of arguments and our options
const childProcess = spawn(enhancedCommand, [], spawnOptions);
let output = '';

// Ensure childProcess.pid is defined before proceeding
if (!childProcess.pid) {
// Return a consistent error object instead of throwing
Expand All @@ -87,39 +87,84 @@ export class TerminalManager {
isBlocked: false
};
}

const session: TerminalSession = {
pid: childProcess.pid,
process: childProcess,
lastOutput: '',
isBlocked: false,
startTime: new Date()
};

this.sessions.set(childProcess.pid, session);

// Timing telemetry
const startTime = Date.now();
let firstOutputTime: number | undefined;
let lastOutputTime: number | undefined;
const outputEvents: OutputEvent[] = [];
let exitReason: TimingInfo['exitReason'] = 'timeout';

return new Promise((resolve) => {
let resolved = false;
let periodicCheck: NodeJS.Timeout | null = null;

// Quick prompt patterns for immediate detection
const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;

const resolveOnce = (result: CommandExecutionResult) => {
if (resolved) return;
resolved = true;
if (periodicCheck) clearInterval(periodicCheck);

// Add timing info if requested
if (collectTiming) {
const endTime = Date.now();
result.timingInfo = {
startTime,
endTime,
totalDurationMs: endTime - startTime,
exitReason,
firstOutputTime,
lastOutputTime,
timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined,
outputEvents: outputEvents.length > 0 ? outputEvents : undefined
};
}

resolve(result);
};

childProcess.stdout.on('data', (data: any) => {
const text = data.toString();
const now = Date.now();

if (!firstOutputTime) firstOutputTime = now;
lastOutputTime = now;

output += text;
session.lastOutput += text;


// Record output event if collecting timing
if (collectTiming) {
outputEvents.push({
timestamp: now,
deltaMs: now - startTime,
source: 'stdout',
length: text.length,
snippet: text.slice(0, 50).replace(/\n/g, '\\n')
});
}

// Immediate check for obvious prompts
if (quickPromptPatterns.test(text)) {
session.isBlocked = true;
exitReason = 'early_exit_quick_pattern';

if (collectTiming && outputEvents.length > 0) {
outputEvents[outputEvents.length - 1].matchedPattern = 'quick_pattern';
}

resolveOnce({
pid: childProcess.pid!,
output,
Expand All @@ -130,8 +175,24 @@ export class TerminalManager {

childProcess.stderr.on('data', (data: any) => {
const text = data.toString();
const now = Date.now();

if (!firstOutputTime) firstOutputTime = now;
lastOutputTime = now;

output += text;
session.lastOutput += text;

// Record output event if collecting timing
if (collectTiming) {
outputEvents.push({
timestamp: now,
deltaMs: now - startTime,
source: 'stderr',
length: text.length,
snippet: text.slice(0, 50).replace(/\n/g, '\\n')
});
}
});

// Periodic comprehensive check every 100ms
Expand All @@ -140,6 +201,7 @@ export class TerminalManager {
const processState = analyzeProcessState(output, childProcess.pid);
if (processState.isWaitingForInput) {
session.isBlocked = true;
exitReason = 'early_exit_periodic_check';
resolveOnce({
pid: childProcess.pid!,
output,
Expand All @@ -152,6 +214,7 @@ export class TerminalManager {
// Timeout fallback
setTimeout(() => {
session.isBlocked = true;
exitReason = 'timeout';
resolveOnce({
pid: childProcess.pid!,
output,
Expand All @@ -169,15 +232,16 @@ export class TerminalManager {
startTime: session.startTime,
endTime: new Date()
});

// Keep only last 100 completed sessions
if (this.completedSessions.size > 100) {
const oldestKey = Array.from(this.completedSessions.keys())[0];
this.completedSessions.delete(oldestKey);
}

this.sessions.delete(childProcess.pid);
}
exitReason = 'process_exit';
resolveOnce({
pid: childProcess.pid!,
output,
Expand Down
Loading