Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/agentclientprotocol/typescript-sdk/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Terminal capabilities allow agents to execute shell commands in the client’s environment. Terminals are created, monitored, and eventually released through a series of client methods.

Advertising Terminal Support

Declare terminal support during initialization:
await connection.initialize({
  protocolVersion: acp.PROTOCOL_VERSION,
  clientCapabilities: {
    terminal: true,  // Enable terminal operations
  },
});
Terminal execution poses significant security risks. Implement careful validation and sandboxing.

Terminal Lifecycle

  1. Create: Agent calls createTerminal to start a command
  2. Monitor: Agent calls terminalOutput to get current output
  3. Wait: Agent calls waitForTerminalExit to wait for completion
  4. Kill (optional): Agent calls killTerminal to terminate the command
  5. Release: Agent calls releaseTerminal to free resources
Client                                Agent
  |                                     |
  |<-- createTerminal request ----------|
  |-- terminalId response ------------->|
  |                                     |
  |<-- terminalOutput request ----------|
  |-- output response ----------------->|
  |                                     |
  |<-- waitForTerminalExit request -----|
  |-- exitStatus response ------------->|
  |                                     |
  |<-- releaseTerminal request ---------|
  |-- (empty) response ---------------->|

createTerminal()

Creates a new terminal to execute a command.

Method Signature

async createTerminal(
  params: CreateTerminalRequest
): Promise<CreateTerminalResponse>
params.sessionId
string
The session creating the terminal.
params.command
string
The command to execute (e.g., "npm", "git").
params.args
string[]
Command arguments (e.g., ["install", "lodash"]).
params.cwd
string
Working directory for the command (absolute path).
params.env
Record<string, string>
Additional environment variables.
terminalId
string
required
Unique identifier for this terminal.

Basic Implementation

import { spawn, ChildProcess } from "node:child_process";
import * as acp from "@agentclientprotocol/acp";

class MyClient implements acp.Client {
  private terminals = new Map<string, {
    process: ChildProcess;
    output: string;
    exitStatus?: ExitStatus;
  }>();
  
  async createTerminal(
    params: acp.CreateTerminalRequest
  ): Promise<acp.CreateTerminalResponse> {
    const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    
    // Spawn process
    const proc = spawn(params.command, params.args, {
      cwd: params.cwd || process.cwd(),
      env: { ...process.env, ...params.env },
      shell: false,
    });
    
    // Capture output
    let output = "";
    
    proc.stdout?.on("data", (data) => {
      output += data.toString();
    });
    
    proc.stderr?.on("data", (data) => {
      output += data.toString();
    });
    
    // Store terminal state
    const terminal = { process: proc, output: "" };
    this.terminals.set(terminalId, terminal);
    
    // Update output reference
    const updateOutput = () => {
      terminal.output = output;
    };
    proc.stdout?.on("data", updateOutput);
    proc.stderr?.on("data", updateOutput);
    
    // Handle exit
    proc.on("exit", (code, signal) => {
      updateOutput();
      if (signal) {
        terminal.exitStatus = { type: "signal", signal };
      } else {
        terminal.exitStatus = { type: "exit_code", code: code ?? -1 };
      }
    });
    
    return { terminalId };
  }
}

terminalOutput()

Gets the current output and exit status of a terminal.

Method Signature

async terminalOutput(
  params: TerminalOutputRequest
): Promise<TerminalOutputResponse>
params.sessionId
string
The session ID.
params.terminalId
string
The terminal ID.
output
string
required
The terminal output (combined stdout/stderr).
exitStatus
ExitStatus
Exit status if the command has completed:
  • { type: "exit_code", code: number }
  • { type: "signal", signal: string }

Implementation

class MyClient implements acp.Client {
  async terminalOutput(
    params: acp.TerminalOutputRequest
  ): Promise<acp.TerminalOutputResponse> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    return {
      output: terminal.output,
      exitStatus: terminal.exitStatus,
    };
  }
}

waitForTerminalExit()

Waits for a terminal command to exit and returns its exit status.

Method Signature

async waitForTerminalExit(
  params: WaitForTerminalExitRequest
): Promise<WaitForTerminalExitResponse>
params.sessionId
string
The session ID.
params.terminalId
string
The terminal ID.
exitStatus
ExitStatus
required
The exit status:
  • { type: "exit_code", code: number }
  • { type: "signal", signal: string }

Implementation

class MyClient implements acp.Client {
  async waitForTerminalExit(
    params: acp.WaitForTerminalExitRequest
  ): Promise<acp.WaitForTerminalExitResponse> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    // If already exited, return immediately
    if (terminal.exitStatus) {
      return { exitStatus: terminal.exitStatus };
    }
    
    // Wait for exit
    return new Promise((resolve) => {
      terminal.process.on("exit", (code, signal) => {
        const exitStatus = signal
          ? { type: "signal" as const, signal }
          : { type: "exit_code" as const, code: code ?? -1 };
        
        terminal.exitStatus = exitStatus;
        resolve({ exitStatus });
      });
    });
  }
}

killTerminal()

Kills a terminal command without releasing the terminal.

Method Signature

async killTerminal(
  params: KillTerminalRequest
): Promise<KillTerminalResponse | void>
params.sessionId
string
The session ID.
params.terminalId
string
The terminal ID.
(empty)
object
Returns an empty object {} or void on success.

Implementation

class MyClient implements acp.Client {
  async killTerminal(
    params: acp.KillTerminalRequest
  ): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    // Kill the process
    if (!terminal.exitStatus) {
      terminal.process.kill();
    }
  }
}
The terminal remains valid after killing, allowing you to get final output with terminalOutput(). Call releaseTerminal() when done.

releaseTerminal()

Releases a terminal and frees all associated resources.

Method Signature

async releaseTerminal(
  params: ReleaseTerminalRequest
): Promise<ReleaseTerminalResponse | void>
params.sessionId
string
The session ID.
params.terminalId
string
The terminal ID to release.
(empty)
object
Returns an empty object {} or void on success.

Implementation

class MyClient implements acp.Client {
  async releaseTerminal(
    params: acp.ReleaseTerminalRequest
  ): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    
    if (!terminal) {
      // Already released, silently succeed
      return;
    }
    
    // Kill if still running
    if (!terminal.exitStatus) {
      terminal.process.kill();
    }
    
    // Remove from map
    this.terminals.delete(params.terminalId);
  }
}
Always call releaseTerminal() when done to prevent resource leaks.

Complete Implementation

Here’s a complete, production-ready terminal implementation:
import { spawn, ChildProcess } from "node:child_process";
import { resolve } from "node:path";
import * as acp from "@agentclientprotocol/acp";

interface Terminal {
  process: ChildProcess;
  output: string;
  exitStatus?: acp.ExitStatus;
}

class ProductionClient implements acp.Client {
  private terminals = new Map<string, Terminal>();
  private allowedCwd: string;
  
  constructor(allowedCwd: string) {
    this.allowedCwd = resolve(allowedCwd);
  }
  
  async createTerminal(
    params: acp.CreateTerminalRequest
  ): Promise<acp.CreateTerminalResponse> {
    // Validate working directory
    const cwd = resolve(params.cwd || this.allowedCwd);
    if (!cwd.startsWith(this.allowedCwd)) {
      throw new Error("Access denied: working directory outside allowed path");
    }
    
    // Validate command
    this.validateCommand(params.command, params.args);
    
    // Generate terminal ID
    const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    
    // Spawn process
    const proc = spawn(params.command, params.args, {
      cwd,
      env: { ...process.env, ...params.env },
      shell: false,
    });
    
    // Setup output capture
    let output = "";
    const terminal: Terminal = { process: proc, output: "", exitStatus: undefined };
    
    const appendOutput = (data: Buffer) => {
      const text = data.toString();
      output += text;
      terminal.output = output;
    };
    
    proc.stdout?.on("data", appendOutput);
    proc.stderr?.on("data", appendOutput);
    
    // Handle exit
    proc.on("exit", (code, signal) => {
      if (signal) {
        terminal.exitStatus = { type: "signal", signal };
      } else {
        terminal.exitStatus = { type: "exit_code", code: code ?? -1 };
      }
    });
    
    // Handle errors
    proc.on("error", (error) => {
      output += `\nError: ${error.message}\n`;
      terminal.output = output;
    });
    
    // Store terminal
    this.terminals.set(terminalId, terminal);
    
    return { terminalId };
  }
  
  async terminalOutput(
    params: acp.TerminalOutputRequest
  ): Promise<acp.TerminalOutputResponse> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    return {
      output: terminal.output,
      exitStatus: terminal.exitStatus,
    };
  }
  
  async waitForTerminalExit(
    params: acp.WaitForTerminalExitRequest
  ): Promise<acp.WaitForTerminalExitResponse> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) {
      throw new Error(`Terminal not found: ${params.terminalId}`);
    }
    
    if (terminal.exitStatus) {
      return { exitStatus: terminal.exitStatus };
    }
    
    return new Promise((resolve) => {
      terminal.process.on("exit", (code, signal) => {
        const exitStatus = signal
          ? { type: "signal" as const, signal }
          : { type: "exit_code" as const, code: code ?? -1 };
        resolve({ exitStatus });
      });
    });
  }
  
  async killTerminal(params: acp.KillTerminalRequest): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) return;
    
    if (!terminal.exitStatus) {
      terminal.process.kill("SIGTERM");
      
      // Force kill after timeout
      setTimeout(() => {
        if (!terminal.exitStatus) {
          terminal.process.kill("SIGKILL");
        }
      }, 5000);
    }
  }
  
  async releaseTerminal(params: acp.ReleaseTerminalRequest): Promise<void> {
    const terminal = this.terminals.get(params.terminalId);
    if (!terminal) return;
    
    if (!terminal.exitStatus) {
      terminal.process.kill();
    }
    
    this.terminals.delete(params.terminalId);
  }
  
  private validateCommand(command: string, args: string[]) {
    const BLOCKED_COMMANDS = ["rm -rf /", "mkfs", "dd if="];
    const fullCommand = `${command} ${args.join(" ")}`;
    
    for (const blocked of BLOCKED_COMMANDS) {
      if (fullCommand.includes(blocked)) {
        throw new Error(`Blocked dangerous command: ${blocked}`);
      }
    }
  }
}

Security Considerations

Command Validation

const ALLOWED_COMMANDS = new Set([
  "npm", "npx", "node",
  "git",
  "tsc", "eslint", "prettier",
]);

function validateCommand(command: string) {
  if (!ALLOWED_COMMANDS.has(command)) {
    throw new Error(`Command not allowed: ${command}`);
  }
}

Argument Sanitization

function sanitizeArgs(args: string[]): string[] {
  return args.map(arg => {
    // Remove shell metacharacters
    if (/[;&|`$()]/.test(arg)) {
      throw new Error(`Unsafe argument: ${arg}`);
    }
    return arg;
  });
}

Resource Limits

import { spawn } from "node:child_process";

function createTerminalWithLimits(command: string, args: string[]) {
  const proc = spawn(command, args, {
    shell: false,
    timeout: 5 * 60 * 1000, // 5 minute timeout
  });
  
  // Limit output size
  let outputSize = 0;
  const MAX_OUTPUT = 10 * 1024 * 1024; // 10MB
  
  proc.stdout?.on("data", (data: Buffer) => {
    outputSize += data.length;
    if (outputSize > MAX_OUTPUT) {
      proc.kill();
      throw new Error("Output size limit exceeded");
    }
  });
  
  return proc;
}

See Also

Build docs developers (and LLMs) love