Skip to content

Instantly share code, notes, and snippets.

@joshuatvernon
Created April 24, 2026 18:42
Show Gist options
  • Select an option

  • Save joshuatvernon/ceb6c1c2bd1700d6bab6af5e463c67e8 to your computer and use it in GitHub Desktop.

Select an option

Save joshuatvernon/ceb6c1c2bd1700d6bab6af5e463c67e8 to your computer and use it in GitHub Desktop.
Interactive CLI support for Cursor agents (terminalcp + DSR workaround)

Interactive CLI Support for Cursor Agents

Drive any interactive CLI tool (newt init, npm init, gt submit, ssh, docker login, etc.) from a Cursor agent using a PTY-based MCP server.

The Problem

Cursor agents can't handle interactive CLI prompts natively. When a tool like newt init asks "What type of component?", the agent has no way to respond. This setup solves that.

Setup (3 steps)

1. Install terminalcp

mkdir -p ~/.local/terminalcp && cd ~/.local/terminalcp
npm init -y
npm install @mariozechner/terminalcp

If you hit native module issues with node-pty on macOS:

cd ~/.local/terminalcp/node_modules/node-pty
npm run build

2. Add to Cursor MCP config

Add this to ~/.cursor/mcp.json (create the file if it doesn't exist):

{
  "mcpServers": {
    "terminal": {
      "command": "node",
      "args": ["<HOME>/.local/terminalcp/node_modules/@mariozechner/terminalcp/dist/index.js", "--mcp"],
      "env": {
        "PATH": "<your PATH including node, homebrew, nflx bins>"
      },
      "alwaysAllow": ["terminalcp"]
    }
  }
}

Replace <HOME> with your actual home directory path (e.g. /Users/yourname) and set PATH to include the directories your CLI tools live in.

Tip: Run echo $PATH in your terminal and use that value, or at minimum include:

/opt/homebrew/bin:/usr/local/bin:/opt/nflx/bin:~/.local/bin:/usr/bin:/bin

3. Add the Cursor rule

Save this to ~/.cursor/rules/interactive-cli.mdc:

---
description: Use the terminal MCP server (terminalcp) to drive interactive CLI tools that prompt for input (newt init, gt submit, npm init, ssh, docker, etc.)
globs:
alwaysApply: true
---

# Interactive CLI Usage

Use the `terminal` MCP server (terminalcp) when a command requires interaction (prompts, REPLs, TUIs, auth flows).

## When to use
- CLI prompts for input (y/n, passwords, selections)
- Long-running commands with staged output
- REPLs or shells (node, python, db clients)
- Tools like git, ssh, docker, kubectl that may prompt

## Core workflow (strict loop)

1. **Start**: `action: "start"`, `command: "<cli>"`, `cwd: "<dir>"` -- returns an `id`

2. **Wait for startup**: pause 3-5 seconds for the process to initialize

3. **Read output**: `action: "stdout"` to see what the CLI printed

4. **LOOP**:
   - `action: "stream"`, `since_last: true`, `strip_ansi: false` -- get raw output
   - **Check for DSR queries** (see below)
   - Analyze:
       - Is there a prompt waiting for input?
       - Is the command still running?
       - Did it complete or error?

   - If **DSR query detected** (`ESC[6n` in raw output):
       - Send response: `action: "stdin"`, `data: "\u001b[40;80R"`
       - Send one response per `[6n` found
       - Then re-read output before sending any answer

   - If **prompt detected**:
       - Decide response
       - If destructive or unclear: **ASK USER** before proceeding
       - `action: "stdin"`, `data: "<answer>\r"`

   - If **still running** without prompt:
       - Wait 2-3 seconds, then read again

   - If **complete** (process exited):
       - Break loop

5. **Stop**: `action: "stop"`, `id: "<id>"`

## DSR (Device Status Report) workaround -- CRITICAL

Many Node.js CLIs (Inquirer.js, prompts, ora) send `ESC[6n` to query terminal dimensions. PTY-based MCP servers do NOT respond to these automatically. If you don't handle them, the CLI hangs forever.

**How to detect**: Use `action: "stream"` with `strip_ansi: false` and look for `[6n` in the raw output.

**How to fix**: Send `\u001b[40;80R` (ESC [ rows ; cols R) back via stdin for each `[6n` found. This tells the CLI the terminal is 40 rows by 80 columns.

**Pattern**: After every `stdin` send to an Inquirer-based CLI:
1. Wait 1-2 seconds
2. Read raw stream (`strip_ansi: false`, `since_last: true`)
3. Count occurrences of `[6n`
4. Send that many `\u001b[40;80R` responses
5. Wait 1-2 seconds
6. Read `stdout` to see the rendered prompt

**Which CLIs need this**: newt init, npm init (newer versions), yeoman generators, create-react-app, and anything using @inquirer/prompts or the ora spinner library.

## API reference

| Action | Purpose | Key args |
|--------|---------|----------|
| `start` | Spawn process | `command`, `cwd`, `name` |
| `stdin` | Send keystrokes | `id`, `data` (use `\r` for Enter, `\u0003` for Ctrl+C) |
| `stdout` | Read rendered screen | `id`, `lines` (optional) |
| `stream` | Read raw output | `id`, `since_last`, `strip_ansi` |
| `stop` | Kill process | `id` (omit to stop all) |
| `list` | Show active sessions | none |

## Prompt detection
- Look for: `y/n`, `yes/no`, `(Y/n)`, `(y/N)`, `Enter value:`, `?`, `>`, `[default]`, menus, numbered selections
- Do NOT assume defaults unless safe and obvious

## Escape sequences for stdin
- Enter: `\r`
- Ctrl+C: `\u0003`
- Ctrl+D: `\u0004`
- Arrow keys: Up=`\u001b[A` Down=`\u001b[B` Right=`\u001b[C` Left=`\u001b[D`
- Tab: `\t`
- Escape: `\u001b`

## Safety rules
- ALWAYS confirm with user before: creating/modifying infrastructure, deleting data, running migrations, auth/credential entry, accepting SSH fingerprints

## Output handling
- Use `stream` with `since_last: true` and `strip_ansi: false` for prompt detection (see raw escape codes)
- Use `stdout` for clean rendered view to show the user
- Prefer frequent reads over one long blocking call
- Summarize progress between steps

## Failure handling
- If command errors: analyze, retry if safe, otherwise ask user
- If stuck: `stdin` with `\u0003` (Ctrl+C) to interrupt
- If session dies: `stop`, start a new one, retry
- If prompt loops (same question repeated): stop, report to user

## Cleanup
- ALWAYS `stop` when done, even on errors

How it works

The setup gives Cursor agents a PTY (pseudo-terminal) they can read from and write to, just like a human at a terminal. The rule teaches the agent a strict read-respond loop so it doesn't get confused by interactive prompts.

The DSR workaround is the key insight: many Node.js CLI tools (Inquirer.js, ora) query terminal dimensions via ESC[6n]. Without a response, they hang forever. The rule teaches the agent to detect these queries and respond with fake dimensions.

What works well

  • newt init / newt add (Jetpack, NodeQuark, etc.)
  • gt submit / gt create (Graphite stacked PRs)
  • npm init
  • ssh sessions
  • docker login
  • Any y/n or menu-based CLI

Known limitations

  • Full-screen TUIs (vim, htop, lazygit) are not reliably automatable
  • Some CLIs with aggressive screen redraws can confuse the agent
  • Password prompts with no echo require the agent to send input blind
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment