alpha — kaval and kaval-tui are an early preview. Commands, flags, and output formats will change before they're finalized for production use; don't script against them yet.
the PTY daemon & its client · alpha
a watch over
your terminals.
kaval (Tamil kāval — watch,
guard; said KAH-val, the first a
long, as in father) is a small, standalone PTY daemon: it owns
your shells, mirrors
their screens, and serves them over a local unix socket — outliving the
clients that come and go. kaval-tui is its terminal client: list
your shells, create new ones, dump their
scrollback, or attach and type — locally, or on a remote machine over ssh
(--host). Run the pair on a box where
kolu has never been installed — a tmux/zellij-shaped duo, minus the
multiplexer's session model.
run the pair
- 1
Start the daemon. It claims its socket and stands watch — leave it running in a pane, a service, wherever.
nix run github:juspay/kolu#kaval - 2
Drive it from any other shell. kaval-tui finds the running daemon on its own — no socket path to remember.
nix run github:juspay/kolu#kaval-tui -- create
nix run github:juspay/kolu#kaval-tui -- list
nix run github:juspay/kolu#kaval-tui -- attach <id>
drive a running kolu
kolu doesn't run terminals in-process anymore — it spawns a kaval daemon
of its own and is just another client of it. So the same kaval-tui reaches the terminals you have
open in kolu, from the shell, with no flags:
kaval-tui list # the terminals open in your kolu
kaval-tui snapshot <id> | grep BUILD-
kaval-tui discovers the daemon by scanning the per-user runtime dir — both
a standalone kaval and every kolu (each
kolu-server namespaces its daemon by listen port). One running → it's
picked automatically. More than one → kaval-tui lists them and asks you to
choose with --socket <path>.
reach a remote kaval — over ssh
--host <ssh> drives a kaval on another machine, with nothing to install
there first. kaval-tui provisions the daemon with Nix (ships the
right-arch build over ssh, realises it), runs it, and dials it — every
subcommand works exactly as it does locally, just pointed at the remote.
kaval-tui create --host nix@prod
kaval-tui list --host nix@prod
kaval-tui attach --host nix@prod <id>
The remote daemon is durable: a terminal you
create outlives the ssh link, so create on prod and attach to it later —
even after you closed the laptop and changed networks. One shared daemon
per host. --host is mutually exclusive
with --socket; it needs passwordless ssh
and your user trusted by the remote's Nix daemon.
A remote terminal runs in the host's
environment, not yours: its $SHELL, $HOME, and $PATH come from the remote machine (so
its own commands resolve), and only your terminal's presentation
vars (TERM, COLORTERM, LANG/LC_*) are carried across. Your local environment — and any secrets in it — never crosses the wire.
what kaval owns
Under the daemon sits a single primitive — a multi-client PTY owner. One host owns any number of PTYs; each PTY is a real shell child paired with a headless screen mirror, fanned out to any number of consumers. It owns only the PTY: it knows nothing about git, pull requests, agent detection, or any wire protocol — those compose on top. The same primitive backs kolu's terminals; kaval just serves it over a socket.
race-free attach
A late-joining client gets a screen snapshot and then live deltas with no gap and no overlap — every byte lands in exactly one of the two, so the screen reconstructs perfectly and then streams.
drop-slow-subscriber
A wedged client that stops draining its output is dropped rather than pinning the daemon's memory without bound — kaval-tui then transparently re-subscribes and gets a fresh snapshot.
taps kaval surfaces per terminal
screen snapshot + live deltas
cwd from OSC 7 reports
title from OSC 0/2 changes
command from OSC 633 preexec
exit the child's exit code
commands · kaval-tui
kaval-tui list One row per live terminal — id · pid · idle · cmd · cwd. --json emits a top-level array for jq.
kaval-tui create Spawn a new terminal on the daemon and print its short id — a plain $SHELL, or a command you pass (create -- htop -d 5). A freshly-started daemon owns nothing, so create is what attach needs first; the daemon then holds the terminal until something kills it. --json emits { id, pid, cwd }.
kaval-tui snapshot <id> Print a terminal's current scrollback as plain text and exit — built for piping and grepping (a trailer line goes to stderr, so stdout stays clean).
kaval-tui attach <id> Take the terminal over, full screen. Raw passthrough — every keystroke and chord reaches the inner program; your window size follows along. Detach with the escape below.
detaching — the ssh model
While attached, nothing is intercepted except a ~ typed at the start of a line
(right after Enter — session start counts too). Mid-line tildes, every
Ctrl chord, and pasted text all pass straight through, so the program
inside never loses a key.
~. detach — kaval-tui exits, the daemon keeps the terminal; re-attach anytime
~~ send one literal ~ to the shell
~? show this escape help
// ~ clashes (nested ssh?) — rebind it: kaval-tui attach <id> --escape %
fine print
-
A standalone kaval's socket lives at
$XDG_RUNTIME_DIR/kaval/pty-host.sock(or/tmp/kaval-$UID/pty-host.sockwhen$XDG_RUNTIME_DIRis unset) and appears when the daemon boots.--socketgoes after the subcommand (kaval-tui list --socket …). -
kolu's daemon is namespaced by listen port —
$XDG_RUNTIME_DIR/kaval-<port>/pty-host.sock— so two kolus on one box never collide. kaval-tui's auto-discovery walks all of them; pass--socketonly to disambiguate when several daemons are up. - Detach is client-side: kaval-tui can come and go while the daemon holds the terminal. Restarting the daemon itself still ends its terminals — that persistence is later work.
- When the program inside exits, kaval-tui exits with the same code. An unreachable daemon is a one-line error, never a hang.
-
createand remote--hosthave shipped; next up iskill— ending a terminal from the shell. - The full design — architecture, phasing, and the decisions behind the escape model — lives in the kaval atlas note.