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
- Create: Agent calls
createTerminal to start a command
- Monitor: Agent calls
terminalOutput to get current output
- Wait: Agent calls
waitForTerminalExit to wait for completion
- Kill (optional): Agent calls
killTerminal to terminate the command
- 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>
The session creating the terminal.
The command to execute (e.g., "npm", "git").
Command arguments (e.g., ["install", "lodash"]).
Working directory for the command (absolute path).
Additional environment variables.
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>
The terminal output (combined stdout/stderr).
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>
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>
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>
The terminal ID to release.
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