kolu
Docs

drive kolu from your coding agent · alpha

MCP: drive kolu
from your agent.

kolu mcp connects your coding agent — Claude Code, Codex, or any MCP host — to your running kolu. Once it’s wired, the agent can do from code exactly what you do by hand on the canvas: open a terminal, type a task into it, wait for the reply, read the screen, and see which terminals need you. That’s what turns one agent into a driver of many — a coordinator spawning and steering a fleet of worker agents, each in its own terminal, while you sit one layer up.

It runs as a small kolu mcp process your agent starts over stdio. Nothing new to install if you already run kolu; no browser involved. If no padi is reachable when it starts — nothing listening on the resolved socket, or no daemon discoverable at all — kolu mcp exits non-zero before the MCP handshake, so a consumer can ask “is kolu usable on this host?” by spawning and checking the exit code rather than reimplementing socket discovery. The same tool table is also spelled as shell verbs — kolu surface projects it to argv for a script, exactly as kolu mcp serves it to an agent.

Connect it to your agent

  1. Make sure the kolu binary is on your PATH — if you installed kolu with the home-manager module, it already is (the module puts kolu on PATH by default), and inside a kolu terminal it is put there for you, on your own machine and on a remote host alike — and in bash and zsh it is re-applied after your dotfiles run, so a PATH= line can’t remove it (fish and other shells get it at spawn only; see Inside a kolu terminal). Then register the server with your agent:

    # Claude Code
    claude mcp add kolu -- kolu mcp

    No kolu install? Run it straight from the flake instead — same server, one command:

    claude mcp add kolu -- nix --refresh run github:juspay/kolu -- mcp
  2. Prefer a config file? Add an .mcp.json entry — any MCP host that reads that file will pick it up:

    {
      "mcpServers": {
        "kolu": { "command": "kolu", "args": ["mcp"] }
      }
    }
  3. Drive a kolu on another machine by adding --host — it reaches the remote over ssh, nothing to install there first:

    claude mcp add kolu-prod -- kolu mcp --host nix@prod

    kolu mcp takes the same endpoint flags every verb does, so --socket <path> and --state-root <dir> name a specific local daemon the same way — which is how you point an agent at a development kolu instead of the running one.

None of that needs a flag when you are simply running kolu: kolu mcp finds this machine’s kolu on its own. If you also have a development or test kolu up, it still picks your everyday one — the daemons differ by where they keep their state, and only one of them keeps it where a plain kolu looks. That matters most when the thing spawning kolu mcp is a background service rather than you at a shell: there is nobody there to point it at the right daemon, and it no longer needs one. If it still can’t tell — several kolus are running and none of them is the everyday one — it says so and names what it looked for, rather than connecting to whichever it found first.

Inside a kolu terminal

An agent running in a kolu terminal needs no step above, and this is true on a remote host exactly as it is on your own machine. The terminal is spawned with kolu’s own CLIs — kolu, kaval-tui, padi-tui — already on its PATH, and with $PADI_SOCKET naming the daemon that owns it. So a plain .mcp.json with "command": "kolu" resolves and connects, with no --host flag and nothing installed on that machine: an agent you started on a remote host can drive its sibling terminals the moment it boots.

The tools it gets are the ones from the same build as the daemon serving it — they arrive in the closure kolu copies to the host when it dials, so a tool and its daemon can never disagree about the wire between them. Your own PATH is untouched apart from these names taking precedence.

One bound worth stating plainly: in bash and zsh kolu re-applies its PATH entries after your dotfiles have run, so an absolute PATH=… line in ~/.bashrc or ~/.zshrc cannot quietly remove the tools. In fish and any other shell the tools are only put in the terminal’s spawn environment — that is best-effort, and an absolute set -x PATH … in config.fish will still drop them. Prepend rather than assign there, or add the paths back yourself.

Drive a terminal

Driving another agent is a short loop of discrete, observable moves — the same rhythm whether you do it by hand or your agent does it in code:

  1. Create a terminal for the work (optionally as a split of another, or in a fresh worktree) and keep the id it returns.
  2. Send the prompt text into it.
  3. Wait for the output to settle, then send Enter as its own step — text and the submit key are always two separate calls, so a same-breath Enter can’t race the paste.
  4. Wait for the agent’s turn to end, then read the screen to see what it said.
  5. Send the next prompt, and repeat — or kill the terminal when the work is done.

What your agent can do

The tools and live views below are what kolu mcp gives the agent, and the name in the first column is what your agent calls. Eight of them — screen_text and screen_image, lifecycle_create and lifecycle_sendInput, the two waits and the two watch tools — also carry a readable display name a host can show instead (“Send input to a terminal”).

Every successful answer comes back as structured data, so your agent acts on a field rather than re-reading prose — alongside text for every tool but screen_image, whose answer is the image itself (its structured arm carries the size and mime type, and the picture is not repeated as base64 prose). A refusal carries data when the tool has a machine-readable reason to give: lifecycle_sendInput labels its four refusals text-and-key, key-refused, text-refused or no-input, and lifecycle_create labels its directory refusals (cwd-and-worktree, worktree-needs-repo, repo-without-worktree, blank-field) and the partial-failure report stopped-partway, beside the sentence explaining each. Other failures arrive as their message alone — including a missing or malformed placement, which is refused by the tool’s own schema before anything runs, so the sentence names both spellings and no terminal was created to clean up.

Create and drive terminals

ToolWhat it does
lifecycle_createOpen a new terminal and return its id. placement is required{"kind":"toplevel"} for a tile of its own, or {"kind":"child-of","parentId":"…"} to open it as a split inside that terminal; there is no default, and a call that omits it is refused with both spellings. Optionally: in a working directory (cwd) or a fresh git worktree (repo + worktree, cut at <repo>/.worktrees/<name>), labelled on the canvas (intent), typing a first command at its shell prompt (run). One call does what kolu create --toplevel --repo … --worktree … -- <cmd> does. A create that fails after the worktree or terminal already landed names the survivors as data (stopped-partway), so nothing orphaned goes unnameable. It picks up your new-terminal theme setting like a terminal you open yourself.
lifecycle_sendInputType into a terminal: text, or one named key or chord (Enter, Escape, C-c, arrows…) — never both in one call.
lifecycle_killClose one terminal by id.

Wait for the right moment

ToolWhat it does
wait_outputSettledBlock until a terminal’s output goes quiet — the turn ended, or it’s waiting for input.
wait_agentStateBlock until the terminal’s agent reaches a state you name (working, awaiting, waiting) — a precise done-signal, not a guess from silence.

Both take two extras that turn a three-call routine into one call, and both exist because the gaps between those calls are where a driving agent gets misled:

  • settledMs — also require the output to have been quiet that long. It’s an and, not a second wait: bytes still arriving keep the wait open, and an agent that goes back to work re-enters it. This is what tells a genuinely finished agent from one whose main loop ended while a background helper is still running — the second reads as done within milliseconds otherwise.
  • screenTail: N — hand back the last N screen lines on the signal itself, read while the wait is still watching. A separate read afterwards can catch a screen that has already moved on.

wait_agentState with until: ["awaiting","waiting"], settledMs: 15000 and screenTail: 40 answers “is this worker’s turn really over, and what did it say?” in one go — the same thing kolu debrief does from a shell.

Supervise several terminals at once

The two waits above each watch one terminal, and only while the call is open — so an agent running several workers used to need a wait per worker, kept re-armed by hand, and anything that happened between two waits was invisible. These replace that with one subscription:

ToolWhat it does
watch_openStart watching — the whole fleet by default, or a named few. You choose the subscription’s name.
watch_nextBlock until any watched terminal needs you, then hand over everything that has happened since you last asked.
watch_closeStop watching and discard the queue.

Anything that happens while your agent is not asking is kept for it, so the time between two calls is not a blind spot — and the queue lives in kolu’s background service, so it survives your agent restarting. Reopen with the same name and you pick up where you left off. Each event says which terminal and why: it’s asking for input, it finished its turn, or it’s gone.

ids is optional, and leaving it off is the better default: a subscription scoped to the lanes you remembered goes blind to the one you didn’t.

Terminals that keep asking until you deal with them

A settle event fires once. An agent that finishes and then sits there for an hour is reported at the start of that hour and never again — so an orchestrator that was busy when the report landed loses the terminal entirely. Optional parameters on watch_open replace that with a level you cannot miss:

ParameterWhat it does
stateswhich agent states to report — any of working, awaiting, waiting. Defaults to awaiting + waiting, the two that need somebody
heldForMsreport a terminal only once it has held that state this long. An agent handed more work inside the window is never reported
nagMsre-report it every this many milliseconds for as long as it keeps holding
ignoreSelfmute the terminal this MCP server is running inside. Refused if the transport cannot identify the caller, and refused if the padi this server is connected to does not know that terminal (another machine’s fleet, or a daemon restart that re-keyed them) — pass ignoreIds rather than guessing
ignoreIdsmute these terminals. Fail-open: a stale id costs nothing, and every new terminal is still watched. Prefer this over ids. An ids list that is entirely muted is refused rather than hanging silent
{ "name": "campaign", "states": ["awaiting", "waiting"],
  "heldForMs": 60000, "nagMs": 300000, "ignoreSelf": true }

A subscription opened this way reports snapshot (already in that state when you opened — handed over before anything that changed since), transition (it entered the state and held it), and nag (still holding, one interval later). Reopening after a restart leads with a fresh snapshot, so an agent that comes back finds what is standing rather than only what happens next.

kolu asks each agent for its own state rather than watching its output: an idle agent that repaints its prompt every second is still idle, and heldForMs times the state, not the screen. The same knobs are kolu watch’s flags from a shell — one implementation, two faces.

Two things to know before adopting them. A subscription answers one of the two questions, never both: one opened with these parameters reports states, so it does not report gone when a terminal disappears. That is not an oversight — this feed is a level, so a terminal that goes away simply stops being reported, and nothing is left waiting on it. An agent that specifically needs to hear about a terminal dying keeps a second subscription without the parameters, or reads the terminals live view. And reopening a name with different parameters starts its queue over: the events it held are answers to the question you just stopped asking, and the fresh snapshot is what replaces them. Reopening with the same parameters — the ordinary restart — keeps the queue, as it always has.

A batch is handed over until your agent confirms it received it, so a reply lost to a timeout costs a repeat rather than a missed report. A queue holds the most recent 512 events; if your agent stays away long enough to overflow one, the next batch comes back with a count of what was dropped rather than quietly losing the tail. Restarting kolu’s background service itself — an upgrade does it — clears subscriptions; the next call says so plainly instead of reporting silence.

Read the screen and the code

ToolWhat it does
screen_textRead a terminal’s rendered screen as text; ask for just the last N lines.
screen_imageSee the same screen as a picture — a themed PNG with the colours, box drawing and highlights text throws away. Ask for the last N rows (up to 200), or omit that for the visible screen. Reach for it when the answer is visual — a TUI’s layout, a coloured diff, a pass/fail run; screen_text stays the cheaper everyday read, and your agent’s host has to be able to show images. CJK and emoji render as empty boxes.
screen_historyRead the older scrollback above the current screen.
git_getStatus · git_getDiffThe git status and diff for a terminal’s repo.
fs_listAll · fs_readFileList and read files in a terminal’s project.

Watch the whole workspace (live views)

Live viewWhat it shows
terminalsThe live roster of your terminals — what’s open, what each is running, and each agent’s state.
urgencyWhich terminals need attention now — the ones waiting on a human or an answer.
daemonStatus · status · identityWhether kolu’s background service is live, up to date, and hasn’t restarted underneath the agent.

These update live — an agent subscribed to urgency is notified the moment a worker terminal starts waiting on it.

It survives kolu restarting underneath it

kolu’s background service restarts routinely — an upgrade does it — and the restart deliberately keeps your terminals running, so nothing else notices. kolu mcp doesn’t need restarting alongside it, and your agent doesn’t need to reconnect: the moment the old connection drops, the next tool call opens a new one against the restarted service. Terminals your agent was driving are still there afterwards, and their ids still work.

That “the moment it drops” is specific to the default, local connection, which is told when the service goes away. Pointed at another machine with --host, kolu mcp does not yet carry the drop announcement, so it still finds out by making one call that fails — the retryable failure described below, just after an idle gap rather than only mid-call.

If a restart happens to land in the middle of a call, that one call fails and says so — that it is the connection that dropped, that kolu mcp is still running, and that retrying will re-open it. Nothing is queued and nothing is re-sent behind your back.

What an agent can’t touch

kolu mcp exposes a deliberate, reviewed allowlist. Reads and terminal control are in — creating, driving, killing, and observing terminals. Out, by design: administering the background service, changing session policy, or rearranging your canvas. Anything not on the list is simply unreachable, and widening it is a reviewed change, never a runtime toggle. So handing an agent your kolu lets it do the work in your terminals without handing it the keys to the whole workspace.

Your theming preferences sit on that line: an agent’s terminals obey the new-terminal theme policy, but the agent can’t read or change it — only your browser sets it.