@geraschenko/pictl
v0.1.2
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, extensible, and composable.
Installation
npm install -g @geraschenko/pictl
pictl --version[!NOTE]
pictldepends onnode-pty, a native addon compiled during install, so you need a C/C++ toolchain and Python on your machine:build-essentialandpython3on Debian/Ubuntu, the Xcode Command Line Tools on macOS, or the equivalent for your platform. Node 20 or newer is required.
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
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 commandsget-entries,get-tree, andnavigate-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 and prints the agent's current context, returning immediately even if the agent is still streaming. Use tail --follow to keep streaming indefinitely, or tail --until turn-end to wait for the current turn (if any) to finish before returning.
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. The output of prompt/tail end with a "cursor" that identifies the entry id of last message returned, and pictl tail --since <entry-id> will return all the messages after that. For example:
$ pictl tail -t c8b
...
== 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
== 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 raw RPC messages)
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 raw RPC messages 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 raw: raw pi socket events, one JSON object per line.rawis inherently JSON, so--jsonis a no-op for it. Since raw RPC messages aren't persisted, you can only get raw messages that appear after you run the command. Fortail,--type rawimplies--follow.
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.
