Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ To test local development changes with Claude Code, add the local build as an MC
# From the shellwright repo root - build first!
npm run build
claude mcp add shellwright-dev --scope project -- node "${PWD}/dist/index.js"
# remove with:
claude mcp remove shellwright-dev --scope project
```

This registers your local build so you can test changes before publishing.
Expand Down
104 changes: 104 additions & 0 deletions docs/shell-attach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# shell_attach

The `shell_attach` tool allows you to connect Shellwright to an existing terminal session. This makes it easier to create recordings from your own interactive sessions (rather than instructing an AI agent to do it).

Being able to attach to a shell allows you to write commands and scripts that start recording your screen - which can be trigged by things like [Claude Code slash commands](https://code.claude.com/docs/en/slash-commands).

Shell sessions must be running in [`tmux`](https://github.com/tmux/tmux/wiki) for this to work. When tmux wraps a shell it allows content to be captures - which is how this shell attach command works. Support for `screen` and vanilla shells is tracked in [#51](https://github.com/dwmkerr/shellwright/issues/51).

Technically, the process is:

```bash
shell_attach # walk up process tree to find PID with tty
ps -p $PID -o tty= # get tty device for that process
tmux list-panes -a | grep $TTY # find tmux pane for the tty
tmux capture-pane -t $PANE -p -e # capture pane content with ANSI codes
```

For a screenshot or recording:

```bash
shell_attach {tty} # starts capture loop (tmux → xterm buffer, polls every 100ms)
shell_screenshot # reads xterm buffer once
shell_record_start # starts recording loop (xterm → PNG frames at configured FPS)
shell_record_stop # stops recording loop, renders GIF
shell_detach # stops capture loop, cleans up
```

## Usage

Start a tmux session, then run something like Claude code:

```bash
# Start tmux, then run Claude Code inside it
tmux new -s main
claude
```

Then attach to the terminal:

```bash
# Initialize MCP session (see scripts/mcp-init.sh)
SESSION=$(./scripts/mcp-init.sh)

# Attach to current terminal - pass your shell's TTY
./scripts/mcp-tool-call.sh $SESSION shell_attach "{\"tty\":\"$(tty)\"}"
```

Example response:

```json
{ "session_id": "shell-session-abc123" }
```

Note: `shell_attach` will only work when the Shellwright MCP server is running on the same host as the target terminal. Using the `stdio` transport should always work, and the `http` transport will work if running on the same host. The `http` transport on a remote host will not be able to access the local shell session.

The `session_id` can be used with `shell_screenshot`, `shell_record_start`, `shell_record_stop`, and `shell_read`.

Take a screenshot:

```bash
./scripts/mcp-tool-call.sh $SESSION shell_screenshot '{"session_id": "shell-session-abc123"}'
```

Example response:

```json
{ "filename": "screenshot.png", "download_url": "http://localhost:7498/files/..." }
```

Start a recording:

```bash
./scripts/mcp-tool-call.sh $SESSION shell_record_start '{"session_id": "shell-session-abc123"}'
```

Example response:

```json
{ "recording": true, "fps": 10 }
```

To stop recording:

```bash
./scripts/mcp-tool-call.sh $SESSION shell_record_stop '{"session_id": "shell-session-abc123"}'
```

Example response:

```json
{ "filename": "recording_1234567890.gif", "download_url": "http://localhost:7498/files/..." }
```

Detach the session with:

```bash
./scripts/mcp-tool-call.sh $SESSION shell_detach '{"session_id": "shell-session-abc123"}'
```

Example response:

```json
{ "success": true }
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:k9s": "tsx tests/04-k9s/run.ts",
"lint": "eslint src/**/*.ts",
"eval": "cd evaluations && npm start",
"eval:compare": "cd evaluations && npm run compare"
"eval:compare": "cd evaluations && npm run compare",
"inspect": "npx @modelcontextprotocol/inspector"
},
"repository": {
"type": "git",
Expand Down
19 changes: 19 additions & 0 deletions scripts/mcp-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
# Initialize MCP session and output session ID
# Usage: ./scripts/mcp-init.sh [port]

PORT="${1:-7498}"
URL="http://localhost:${PORT}/mcp"

SESSION_ID=$(curl -s -D - -X POST "$URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"mcp-cli","version":"1.0"}}}' \
2>/dev/null | grep -i "mcp-session-id:" | cut -d' ' -f2 | tr -d '\r')

if [ -z "$SESSION_ID" ]; then
echo "[mcp-init] error: failed to get session ID" >&2
exit 1
fi

echo "$SESSION_ID"
28 changes: 28 additions & 0 deletions scripts/mcp-tool-call.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# Call an MCP tool
# Usage: ./scripts/mcp-tool-call.sh <session_id> <tool_name> [args_json]
# Example: ./scripts/mcp-tool-call.sh $SESSION shell_start '{"command":"bash"}'

SESSION_ID="$1"
TOOL_NAME="$2"
ARGS_JSON="${3:-{}}"
PORT="${4:-7498}"

if [ -z "$SESSION_ID" ] || [ -z "$TOOL_NAME" ]; then
echo "Usage: $0 <session_id> <tool_name> [args_json] [port]" >&2
exit 1
fi

URL="http://localhost:${PORT}/mcp"

REQUEST=$(cat <<EOF
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"$TOOL_NAME","arguments":$ARGS_JSON}}
EOF
)

curl -s -X POST "$URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $SESSION_ID" \
-d "$REQUEST" \
2>/dev/null | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text // .error.message // .'
Loading