@geraschenko/pictl
v0.1.4
Published
Spawn, observe, control, and attach to a fleet of pi coding-agent instances
Readme
pictl: a pi agent orchestration CLI
pictl aims to let humans, agents, scripts, and code interact with live pi instances simultaneously, each on their own terms. Humans can attach with the stock pi TUI, and agents/scripts get ergonomic (but unfettered) access to the RPC interface. You don't have to give up your /tree, and they aren't forced to tmux capture-pane.
Like pi, pictl is meant to be minimal and composable. Its main components and associated subcommands are:
- Lifecycle management (
spawn/archive/list/status/…). Each agent runs as a background process, automatically resumed when needed. There is no central orchestrator; each agent's daemon manages its own entry in an on-disk registry. - CLI wrappers for all RPC commands (
prompt/set-model/abort/compact/…).promptstreams the response; everything else returns the RPC response as JSON. - Monitoring with
tailandwait.promptandtailcan emit formatted messages (default), session entries, or the RPC event stream — human-readable by default,--jsonfor machines. See Getting fancy. - TUI access with
attach. You get pi's stock interface, including any custom UI extensions.
Installation
npm install -g @geraschenko/pictl
pictl --versionpictl bundles its own lightly modified pi, so you don't need pi installed. See docs/pi-modifications.md for what's changed.
[!NOTE]
pictlruns on Linux and macOS only; it uses Unix domain sockets and has no native Windows support. Node 22.18 or newer is required.On Linux,
pictl'snode-ptydependency has no prebuilt binary and compiles a native addon during install, so you need a C/C++ toolchain and Python:build-essentialandpython3on Debian/Ubuntu, or the equivalent.
Quickstart
Start an agent and make it the default target for subsequent commands:
export PICTL_TARGET="$(pictl spawn)"
echo "$PICTL_TARGET"- If you don't set
$PICTL_TARGET, commands that require a target need--target PREFIXor-t PREFIX. Any unique prefix of the agent id is accepted. - If you want to pass extra args to
pi, put them after--when you spawn the agent, like this:pictl spawn -- -e my_extension.ts. To "wrap" an existing pi session, exit the session and runpictl spawn -- --resume <session-id>(NOTE: the agent id is different from the session id).
Attach to the TUI in another terminal if you want to follow along in "regular pi view" (recommended).
pictl attach --target <PREFIX_OF_PICTL_TARGET>- You can detach with
ctrl+]. Detaching does not stop the agent. - You can also exit with
ctrl+das usual, which does stop the agent since it forwards the exit command to the pi process. But it'll be automatically revived when any commands are sent to it. When it's revived, the same cwd and arguments to pi are used that were used when it was first created.
Send commands to the agent with prompt and pi's other RPC commands. Do this from the first terminal (or wherever you've set PICTL_TARGET):
pictl prompt "Say hello. Keep it short"
pictl set-model openai-codex gpt-5.5
# Note: compaction will fail if the session is already tiny.
pictl compact --custom-instructions "Next we're going to say goodbye. Only keep context relevant to that task."
# RPC pass-through commands (except for `prompt`) return json. `pictl format`
# can prettify the responses from `get-messages`, `get-entries`, and `get-tree`.
pictl get-entries | pictl format entries
# Note: you have to use a real entry id from your actual session here;
# see the output from `get-entries` above.
pictl navigate-tree 8c5cb595[!NOTE]
- For all available RPC commands, see pi's
rpc-types.ts, or runpictl -H. The tweaked version of pi used bypictlalso includes the commandnavigate-tree, which stock pi does not.- The help also includes details about subcommand arguments, e.g.
pictl set-model -Hto learn about arguments ofset-model.
Manage your agents
pictl list [--all] [--cwd PATH]
# Non-destructive. Shuts down processes and removes from default `pictl list`.
pictl archive -t <PREFIX_OF_PICTL_TARGET>
# Destructive. Deletes file from registry.
pictl purge -t <PREFIX_OF_PICTL_TARGET>[!NOTE]
- The actual session messages live in your
~/.pias usual.pictl's agent registry lives in your per-OS user data dir (~/.local/share/pictlon Linux), or wherever$PICTL_DIRpoints if you set it.
Setup tab completion with pictl completion install (bash only).
Getting fancy with pictl [prompt|tail|format]
pictl prompt and pictl tail both show a live agent's activity. prompt sends a message and streams until the end of the assistant turn. tail doesn't send anything; it prints the agent's current context and then keeps streaming new activity indefinitely. Use tail --until turn-end to return once the current turn (if any) finishes, or tail --timeout 0 to print the current context and return immediately.
Machine-readable output
Both prompt and tail print human-readable, formatted output by default, but add --json to get machine-readable output. If you want finer control over the formatting use --json and pipe to pictl format.
Async prompting
Send a prompt without waiting for the reply with pictl prompt --detach. Then check back with tail --until turn-end, which returns once the current turn (if any) finishes. The output of prompt/tail --until ends with a "cursor" that identifies the entry id of the last message returned, and pictl tail --since <entry-id> will return all the messages after that. For example:
$ pictl tail -t c8b --until turn-end
...
== assistant ==
You are so smart.
[cursor: 6be9380a]Then you can send an async message with
$ pictl prompt -t c8b -d "Stop being such a boot-licker and write a compiler. No mistakes."The -d causes the prompt command to return immediately with no output, but then when you're ready to check back in on this agent, you can see what's happened since last you checked with
$ pictl tail -t c8b --since 6be9380a --until turn-end
== user ==
Stop being such a boot-licker and write a compiler. No mistakes.
== assistant ==
[thinking]
Yes sir. I'll get right on it.
[tool: bash ...]
...[!NOTE]
- If no
--sinceis provided,tailwill give you all the messages in the agent's current context.- If
--sinceis provided, you get all messages from all activity since that entry, which can cross compaction boundaries and tree navitation. If you used/treeto navigate back and forth between branches of the conversation, sending messages here and there, what you'll get is the messages in the order they were inserted into the session file (i.e. chronological order).- If the underlying session file has changed (e.g. you typed
/newor/forkinto the interactive TUI, or used theswitch-sessioncommand), then you'll only get the new activity on the current session file, and you'll get an error if the current file doesn't contain the given entry id. You can usepictl statusto see what session files have been associated with a given agent.
Messages vs Entries (vs RPC events)
pi distinguishes between messages (the agent/LLM-facing conversation units) and entries (durable, branchable session history). Messages are derived from entries. If you need the greater fidelity of entries, you can get it. You can even stream the RPC socket events if you really need to, but watch out because that includes a bunch of stuff that doesn't get written to the session file, like incremental message updates.
You can control what you get from prompt/tail with --type:
--type messages(default): one block per message (and per control event like compaction), the same rendering aspictl format messages.--jsonfor no formatting.--type entries: one line per session entry (<id> <role> <summary>), the same rendering aspictl format entries.--jsonfor no formatting.--type events: pi socket events, one line per event, the same rendering aspictl format events.--jsonfor no formatting. Since events aren't persisted, you can only get events that appear after you run the command; there is no historical output, so-nand--sincedon't apply.
If the reason you're after entries is to recover the tree structure from the parentId field, you may want to get the tree structure directly with get-tree. The output of get-tree is json (like pretty much all subcommands other than prompt and tail), but you can pretty print it like this:
pictl get-tree -t c8b | pictl format treeFurther reading
How does pictl work? See
docs/architecture.mdfor details about the agent registry, command and tty sockets, daemon processes, and how I expect pictl to interact with other languages.Tweaks to pi. See
docs/pi-modifications.mdfor details about the tweaked version of pipictldepends on, especially--rpc-socketmode.For all available subcommands, run
pictl --help-all. Subcommands have their own help info; e.g.pictl format entries --help.
