A WebSocket service that exposes gastown agents as a programmatic interface. Clients interact with agents — tmux is an internal implementation detail.
go build -o bin/tmux-adapter .
gt start
bin/tmux-adapter --gt-dir ~/gt --port 8080Connect with any WebSocket client:
websocat ws://localhost:8080/wsThe Gastown Dashboard lives in samples/adapter.html — a consumer of the WebSocket API, not part of the server. The adapter serves the <tmux-adapter-web> web component at /tmux-adapter-web/, so the sample (or any consumer) imports it directly from the adapter — no local file paths needed.
Quick (single port, development):
bin/tmux-adapter --gt-dir ~/gt --port 8080 --debug-serve-dir ./samples
open http://localhost:8080Separate servers (production-like):
# Terminal 1: start the adapter
bin/tmux-adapter --gt-dir ~/gt --port 8080
# Terminal 2: serve the sample
python3 -m http.server 8000 --directory samples
open http://localhost:8000When served separately, the sample connects to localhost:8080 by default. To point at a different adapter (e.g. via ngrok), pass ?adapter=:
http://localhost:8000/?adapter=abc123.ngrok-free.app
The ?adapter= parameter controls both the WebSocket connection and the component import origin. If the adapter is behind TLS, the sample auto-upgrades to wss:// and https://.
The simplest approach uses --debug-serve-dir so only one ngrok tunnel is needed:
# 1. Start the adapter serving the sample
bin/tmux-adapter --gt-dir ~/gt --port 8080 --debug-serve-dir ./samples
# 2. Expose via ngrok (single tunnel)
ngrok http 8080For a stable URL across restarts, claim a free static domain at https://dashboard.ngrok.com/domains, then:
ngrok http --url your-name.ngrok-free.app 8080To tear down:
pkill -f ngrokThe adapter uses a mixed JSON + binary protocol over one WebSocket connection at /ws:
- JSON text frames for control flow (
subscribe-*,list-agents,send-prompt) - Binary frames for terminal data (output, keyboard input, resize)
Requests include an id for correlation; responses echo it back.
Security notes:
- WebSocket upgrades are checked against
--allowed-origins(default:localhost:*). Cross-origin clients must be explicitly allowed. - Optional auth token can be required via
--auth-token; clients sendAuthorization: Bearer <token>or?token=<token>.
msgType(1 byte) + agentName(utf8) + 0x00 + payload(bytes)
| Type | Direction | Meaning |
|---|---|---|
0x01 |
server → client | terminal output bytes |
0x02 |
client → server | keyboard input bytes |
0x03 |
client → server | resize payload ("cols:rows") |
0x04 |
client → server | file upload payload (fileName + 0x00 + mimeType + 0x00 + fileBytes) |
→ {"id":"1", "type":"list-agents"}
← {"id":"1", "type":"list-agents", "agents":[
{"name":"hq-mayor", "role":"mayor", "runtime":"claude", "rig":null, "workDir":"/Users/me/gt", "attached":false},
{"name":"gt-myrig-crew-bob", "role":"crew", "runtime":"claude", "rig":"myrig", "workDir":"/Users/me/gt/myrig/crew/bob", "attached":false}
]}→ {"id":"2", "type":"send-prompt", "agent":"hq-mayor", "prompt":"please review the PR"}
← {"id":"2", "type":"send-prompt", "ok":true}The adapter handles the full NudgeSession delivery sequence internally (literal mode, 500ms debounce, Escape, Enter with retry, SIGWINCH wake for detached sessions).
Clients can drag/drop or paste files into an agent terminal by sending binary 0x04 frames.
Behavior:
- Max upload size is 8MB per file.
- File bytes are transferred to the server and saved under
<agent workDir>/.tmux-adapter/uploads(fallback:/tmp/tmux-adapter/uploads/...). - If the file is text-like and <= 256KB, the file contents are pasted into tmux.
- Images (
image/*) paste the absolute server-side path so that agents like Claude Code can read and render the image inline. - Other binary files paste a relative server-side path (relative to the agent workdir when possible, absolute fallback).
- The adapter also attempts to mirror the same pasted payload into the server's local clipboard (
pbcopy,wl-copy,xclip,xsel; best effort).
Start streaming output (default stream=true):
→ {"id":"3", "type":"subscribe-output", "agent":"hq-mayor"}
← {"id":"3", "type":"subscribe-output", "ok":true}After this JSON ack, the server sends:
- a binary
0x01snapshot frame with current pane content (so quiet/paused sessions are not blank) - then ongoing binary
0x01live stream frames frompipe-pane
History-only (no stream):
→ {"id":"4", "type":"subscribe-output", "agent":"hq-mayor", "stream":false}
← {"id":"4", "type":"subscribe-output", "ok":true, "history":"..."}Unsubscribe:
→ {"id":"5", "type":"unsubscribe-output", "agent":"hq-mayor"}
← {"id":"5", "type":"unsubscribe-output", "ok":true}→ {"id":"6", "type":"subscribe-agents"}
← {"id":"6", "type":"subscribe-agents", "ok":true, "agents":[...]}
← {"type":"agent-added", "agent":{...}}
← {"type":"agent-removed", "name":"gt-myrig-SomeTask"}
← {"type":"agent-updated", "agent":{...}}agent-updated fires when a human attaches to or detaches from a session. Hot-reloads (same session, process restarts) emit agent-removed then agent-added in quick succession.
Unsubscribe:
→ {"id":"7", "type":"unsubscribe-agents"}
← {"id":"7", "type":"unsubscribe-agents", "ok":true}{
"name": "hq-mayor",
"role": "mayor",
"runtime": "claude",
"rig": null,
"workDir": "/Users/me/gt",
"attached": false
}| Field | Type | Description |
|---|---|---|
name |
string | Session identifier (hq-mayor, gt-myrig-crew-bob) |
role |
string | mayor, deacon, overseer, witness, refinery, crew, polecat, boot |
runtime |
string | claude, gemini, codex, cursor, auggie, amp, opencode |
rig |
string? | Rig name for rig-level agents, null for town-level |
workDir |
string | Agent's working directory |
attached |
bool | Whether a human is viewing the session |
Only agents with a live process are exposed — zombie sessions are filtered out.
A companion service that streams structured conversation events from CLI AI agents over WebSocket. Instead of raw terminal bytes, it watches the conversation files agents write to disk (.jsonl for Claude Code) and streams normalized JSON events.
go build -o bin/tmux-converter ./cmd/tmux-converter/
gt start
bin/tmux-converter --gt-dir ~/gt --listen :8081bin/tmux-converter --gt-dir ~/gt --listen :8081 --debug-serve-dir ./samples
open http://localhost:8081/converter.htmlgo build -o bin/tmux-adapter . && go build -o bin/tmux-converter ./cmd/tmux-converter/
bin/tmux-adapter --gt-dir ~/gt --port 8080 --debug-serve-dir ./samples &
bin/tmux-converter --gt-dir ~/gt --listen :8081 --debug-serve-dir ./samples &
# Adapter dashboard: http://localhost:8080/adapter.html
# Converter dashboard: http://localhost:8081/converter.htmlJSON-only WebSocket protocol at /ws. Requires a protocol handshake as the first message:
→ {"id":"1", "type":"hello", "protocol":"tmux-converter.v1"}
← {"id":"1", "type":"hello", "ok":true, "protocol":"tmux-converter.v1"}Follow an agent (auto-subscribes to current conversation, auto-switches on rotation):
→ {"id":"2", "type":"follow-agent", "agent":"hq-mayor", "filter":{"excludeProgress":true}}
← {"id":"2", "type":"follow-agent", "ok":true, "conversationId":"claude:hq-mayor:abc123",
"events":[...], "totalEvents":835}List agents:
→ {"id":"3", "type":"list-agents"}
← {"id":"3", "type":"list-agents", "agents":[...]}Subscribe to agent lifecycle:
→ {"id":"4", "type":"subscribe-agents"}
← {"id":"4", "type":"subscribe-agents", "ok":true, "agents":[...]}
← {"type":"agent-added", "agent":{...}}
← {"type":"agent-removed", "name":"..."}Unsubscribe:
→ {"id":"5", "type":"unsubscribe-agent", "agent":"hq-mayor"}
← {"id":"5", "type":"unsubscribe-agent", "ok":true}GET /ws→ WebSocket endpointGET /healthz→ process liveness ({"ok":true})GET /readyz→ tmux + registry readinessGET /conversations→ list active conversations with metadata
| Flag | Default | Description |
|---|---|---|
--gt-dir |
~/gt |
Gastown town directory |
--listen |
:8081 |
HTTP/WebSocket listen address |
--debug-serve-dir |
`` | Serve static files at / (development only) |
- Connects to tmux via control mode (
converter-monitorsession) - Agent registry scans for gastown agents, emits lifecycle events
- For each agent with runtime
claude, discovers conversation files at~/.claude/projects/{encoded-workdir}/*.jsonl - Streams only the active conversation (most recent file) per agent — older files are inactive conversations from previous sessions
- Parses Claude Code JSONL into normalized
ConversationEventstructs - Buffers up to 100,000 events per conversation in a ring buffer
- WebSocket clients get a snapshot (capped at 20,000 events) plus live streaming
Active vs inactive conversations: Each agent may have many .jsonl files — one per CLI session. Only the most recent is the active conversation and is streamed live. Older files are inactive conversations with stable ConversationIDs (e.g., claude:agent-name:uuid). Future: inactive conversations can be loaded on demand as independent read-only threads.
Clients ◄──ws──► tmux-adapter ◄──control mode──► tmux server
│
├──pipe-pane (per agent)──► output files
│
└──/tmux-adapter-web/ ──► embedded web component (go:embed)
Clients ◄──ws──► tmux-converter ◄──control mode──► tmux server
│
└──file watching──► ~/.claude/projects/*/*.jsonl
- Component serving: the
<tmux-adapter-web>web component is embedded in the adapter binary viago:embedand served at/tmux-adapter-web/with CORS headers. Consumers import directly from the adapter — the server is its own CDN. - Control mode: each service maintains its own
tmux -Cconnection (adapter usesadapter-monitor, converter usesconverter-monitor) - Agent detection: reads
GT_ROLE/GT_RIGenv vars, checkspane_current_commandagainst known runtimes, walks process descendants for shell-wrapped agents, handles version-as-argv[0] (e.g., Claude showing2.1.38) - Output streaming (adapter):
pipe-pane -oactivated per-agent on first subscriber, deactivated on last unsubscribe; each subscribe also sends an immediatecapture-panesnapshot frame - Conversation streaming (converter): discovers
.jsonlfiles, tails only the active (most recent) file for live events, parses into structured events, buffers and broadcasts to subscribers. Older files are inactive conversations available for future on-demand loading. - Send prompt: full NudgeSession sequence with per-agent mutex to prevent interleaving
| Flag | Default | Description |
|---|---|---|
--gt-dir |
~/gt |
Gastown town directory |
--port |
8080 |
WebSocket server port |
--auth-token |
`` | Optional WebSocket auth token |
--allowed-origins |
localhost:* |
Comma-separated origin patterns for WebSocket CORS |
--debug-serve-dir |
`` | Serve static files from this directory at / (development only) |
GET /tmux-adapter-web/*→ embedded web component files (CORS-enabled)GET /healthz→ static process liveness ({"ok":true})GET /readyz→ tmux control mode readiness check (200on success,503with error on failure)
make checkArchitecture standards and constraints are documented in ARCHITECTURE.md.
