Files
pi-extensions/AGENTS.md
T
joakimp d2b2b3fb43 Add ext-toggle extension and /ext slash command
extensions/ext-toggle.ts:
  /ext lists ~/.pi/agent/extensions/ with active/disabled markers
  and toggles individual extensions by renaming between name.ts and
  name.ts.off (pi only auto-discovers *.ts). Calls ctx.reload() so the
  change takes effect without restarting pi.

  Subdirectory-style extensions (name/index.ts) are listed read-only
  in v1 — toggling a directory cleanly is more work than the rename
  trick is worth.

install.sh:
  --uninstall now matches both *.ts and *.ts.off symlinks pointing
  into this repo, so a disabled extension is still cleaned up.

README.md / AGENTS.md:
  Document ext-toggle alongside the others; AGENTS notes the API
  surface used (registerCommand, ui.select/confirm/notify, reload)
  and the rename-not-delete design decision.
2026-05-07 20:26:41 +02:00

259 lines
12 KiB
Markdown

# AGENTS.md
## What this is
A version-controlled collection of custom and modified pi coding-agent extensions,
symlinked into `~/.pi/agent/extensions/` by `install.sh`.
Companion to [`pi-toolkit`](https://gitea.jordbo.se/joakimp/pi-toolkit) (bring-up,
keybindings, env loader) — that repo gets pi running; this repo adds behaviour on
top of it. Also related to [`skillset`](https://gitea.jordbo.se/joakimp/skillset)
which does the same job for agent skills.
Read [`README.md`](README.md) first for the user-facing walk-through.
This file is for agents modifying the repo.
## Structure
```
extensions/
ssh-controlmaster.ts # ControlMaster SSH remote execution (see below)
confirm-destructive.ts # Confirm before dangerous bash commands and session actions
git-checkpoint.ts # Git stash checkpoint per turn, restorable on /fork
notify.ts # Native terminal notification when agent finishes
ext-toggle.ts # /ext slash command — list & toggle extensions at runtime
install.sh # Idempotent installer — symlinks extensions/ into ~/.pi/agent/extensions/
package.json # pi package manifest — enables `pi install /path` as an alternative
README.md # User-facing docs.
AGENTS.md # This file.
LICENSE # MIT.
```
## Conventions
- **One `.ts` file per extension**, flat under `extensions/`. If an extension
grows multiple files, use a subdirectory with an `index.ts` entry point —
pi's auto-discovery handles both shapes.
- **Extensions are symlinked, not copied.** `ln -s <repo>/extensions/foo.ts
~/.pi/agent/extensions/foo.ts`. Edits flow through git without a re-install
step. The symlink is what pi loads at startup.
- **Existing files at link destinations are backed up** with a timestamp
(`.bak.YYYYMMDD-HHMMSS`) before a new symlink is created. Never silently
overwrite.
- **`install.sh` is idempotent.** Re-running it is always safe. Links already
pointing into this repo are left alone (`link_into_repo` guard).
- **`--only` / `--skip` flags** for subset installs. `--only` is an explicit
allowlist; `--skip` starts with everything and removes named entries; `--only`
wins if both are given. Names are bare (no `.ts`).
- **No always-on side effects at module load time.** Extensions should be inert
unless the user passes a flag or the relevant event fires. The `--ssh` flag in
`ssh-controlmaster.ts` is the model: the module loads on every pi session but
does nothing unless `--ssh` is present.
- **Extension flag names must be globally unique** across all installed extensions.
Prefix with a short namespace if there's any risk of collision
(e.g. `ssh-` for ssh-related flags).
## What `install.sh` does
1. `require_pi_installed` — aborts with exit 4 if `~/.pi/agent/` is missing.
Creates `~/.pi/agent/extensions/` proactively.
2. Calls `build_install_set` to resolve which extensions to install based on
`--only` / `--skip` flags (default: all `.ts` files in `extensions/`).
3. For each selected extension: symlinks `extensions/<name>.ts` →
`~/.pi/agent/extensions/<name>.ts`. Backs up any pre-existing real file or
foreign symlink.
Uninstall: removes symlinks that point into this repo, leaves everything else
untouched.
## Adding a new extension
1. **Drop a `.ts` file into `extensions/`.** No other config needed —
`install.sh` discovers all `.ts` files automatically.
2. **Export a default factory function** `(pi: ExtensionAPI) => void`.
See the [pi extensions docs](https://github.com/mariozechner/pi-coding-agent/blob/main/docs/extensions.md)
and [built-in examples](https://github.com/mariozechner/pi-coding-agent/tree/main/examples/extensions).
3. **Register any CLI flags** via `pi.registerFlag()` so they appear in
`pi --help`.
4. **Keep the extension inert without its flag** (or equivalent trigger). Don't
run side effects unconditionally at load time.
5. **Update `README.md`** — add an entry under the Extensions section
documenting flags, use cases, requirements, and how it works.
6. **Re-run `./install.sh`** — it picks up the new file and symlinks it.
In a running pi session, `/reload` is enough; no restart needed.
## Extension-specific notes
### `ssh-controlmaster.ts`
Overrides the four native pi tools (`read`, `write`, `edit`, `bash`) to execute
on a remote machine via SSH when `--ssh user@host` is passed.
**Key design decisions:**
- **ControlMaster negotiation via `ssh -G`.**
Before starting any connection, `readSshConfig(remote)` runs `ssh -G <host>`
and inspects the `controlmaster` and `controlpath` fields. If the effective
config already has `ControlMaster auto` or `yes`, the system socket is reused
(`ownsmaster: false`). Otherwise, the extension starts its own master at
`/tmp/pi-cm-<pid>.sock` (`ownsmaster: true`). The `session_shutdown` handler
only calls `ssh -O exit` when `ownsmaster` is true — it never tears down a
connection it didn't create.
- **Password auth via `--ssh-ask-pass`.**
When the flag is set, `ctx.ui.input()` prompts for a password before
connecting. The password is passed to SSH via `SSH_ASKPASS`: a temp script at
`/tmp/pi-askpass-<pid>.sh` (`chmod 700`) is written, SSH is spawned with
`SSH_ASKPASS` + `SSH_ASKPASS_REQUIRE=force` (plus `DISPLAY=dummy` for older
OpenSSH), and the script is deleted in a `finally` block. Input is **not**
masked — visible while typing.
- **Path mapping.**
Remote paths are derived by replacing the local `cwd` with `remoteCwd` in
every path argument. This works cleanly when pi is started from a directory
that has a matching counterpart on the remote. Use the `user@host:/path` form
when the paths diverge.
- **`ownsmaster` vs system master and `--ssh-ask-pass`.**
If the system already has a ControlMaster configured for the target host,
`--ssh-ask-pass` is silently ignored — the system master handles auth
independently and the socket is just reused.
### `confirm-destructive.ts`
Always-on (no flag). Two layers:
1. **Bash gate** — intercepts `tool_call` for `bash` and checks the command
against a pattern list. On match, shows a `select` dialog. Blocks in
non-interactive mode by default.
Patterns: recursive `rm`, `sudo`, `chmod/chown 777`, `dd if=`, `mkfs`,
`git push --force`, writes to `/dev/*`, `truncate --size 0`.
2. **Session gate** — hooks `session_before_switch` and `session_before_fork`.
Confirms before `/new` (clear) and `/resume` (switch, only if there are
unsaved messages). Always confirms before `/fork`.
When adding new patterns, add to the `DANGEROUS` array at the top of the file.
Each entry has a `pattern` (RegExp) and a `label` shown in the dialog.
### `git-checkpoint.ts`
Always-on. Silently skips when the `cwd` is not inside a git repo.
- At `turn_start`: runs `git stash create` (non-destructive — creates a stash
object without touching the working tree). Stores the stash ref keyed to
the current session entry ID. Empty output (nothing to stash) is silently
ignored.
- At `session_before_fork`: if a checkpoint exists for the selected entry,
offers to `git stash apply` it.
- Status bar shows `⎇ N checkpoints` when checkpoints are present.
- Checkpoints are in-memory only — they do not survive a pi restart, but stash
objects remain in the git repo and can be applied manually.
### `notify.ts`
Always-on. Records `agent_start` timestamp; on `agent_end` checks if elapsed
time exceeds `--notify-min-secs` (default 8). Silently skips short responses.
Terminal detection order: `KITTY_WINDOW_ID` → OSC 99 (Kitty) →
`WT_SESSION` → Windows toast → OSC 777 (iTerm2, WezTerm, Ghostty).
Notification text: `Pi — Done (Ns)` where N is the rounded elapsed seconds.
### `ext-toggle.ts`
Registers `/ext` slash command. Lists files in `~/.pi/agent/extensions/`,
shows `` (active) / `` (disabled) plus dir/symlink hints, and lets the
user toggle individual extensions by renaming them between `name.ts` and
`name.ts.off`. Calls `ctx.reload()` after a toggle so the change takes
effect without restarting pi.
**Key design decisions:**
- **Rename, not delete.** Disabling a built-in produces a `name.ts.off`
symlink/file that's invisible to pi's `*.ts` discovery glob but trivially
reversible. No state stored elsewhere.
- **Symlink-friendly.** `fs.renameSync` renames the symlink itself; the repo
target is untouched. Toggling an extension installed by this repo is
reversible without re-running `install.sh`.
- **Subdir extensions are read-only in v1.** `name/index.ts` shapes show up in
the listing with a `[dir]` tag but cannot be toggled — the cleanest disable
for a directory would need a hidden-prefix or move-aside dance that adds
more failure modes than it's worth for now.
- **`install.sh --uninstall` matches both `*.ts` and `*.ts.off`.** Means a
disabled extension is still cleaned up on uninstall, regardless of which
state it was left in.
- **Listing is recomputed on every `/ext` invocation.** No cache, no event
subscription — cheap enough for the tens of files this directory will ever
contain.
**API used:**
- `pi.registerCommand(name, { description, handler })` — registers `/ext`.
- `ctx.ui.select(title, items)` — picker; returns selected string or `undefined`.
- `ctx.ui.confirm(title, message)` — yes/no dialog returning `boolean`.
- `ctx.ui.notify(message, level)` — transient toast.
- `ctx.reload()` — reloads extensions/skills/prompts/themes; same as `/reload`.
No flags, no `agent_*` event handlers — fully passive until `/ext` is invoked.
No framework. Manual:
```bash
./install.sh --help
./install.sh --yes # fresh install
./install.sh --yes # re-run (idempotent — all "already linked")
./install.sh --only ssh-controlmaster --yes # subset
./install.sh --skip ssh-controlmaster --yes # inverse subset
./install.sh --uninstall --yes # remove
./install.sh --yes # reinstall
```
Manual extension tests (requires a reachable SSH host):
```bash
# Key-based auth
pi --ssh user@host
# Explicit remote path
pi --ssh user@host:/etc
# Password auth
pi --ssh user@host --ssh-ask-pass
# Verify status bar shows ⚡ own master or ⚡ system master
# Verify /reload works without restarting pi
```
## Gotchas
- **`ctx.ui.input()` does not mask input.** Passwords typed via `--ssh-ask-pass`
are visible in the terminal. A masked alternative would require a custom
`ctx.ui.custom()` component.
- **Socket path length limit on macOS.** Unix socket paths are capped at ~104
characters. `/tmp/pi-cm-<pid>.sock` is safe. Don't use paths under `$HOME`
which can be long.
- **`ssh -G` strips the `user@` prefix before querying config.** The `readSshConfig`
helper does this automatically. Don't pass `user@host` directly to `ssh -G`.
- **`SSH_ASKPASS_REQUIRE=force` requires OpenSSH 8.4+.** `DISPLAY=dummy` is set
as a fallback for older versions. Both are needed for full compatibility.
- **Extensions loaded globally affect every pi session.** Extensions without
an activating flag (e.g. a `session_start` hook that always fires) will
run on every invocation. Keep that surface small.
- **`pi install /path` and `install.sh` symlinks are mutually exclusive** for
the same extension. `pi install` manages its own copy; `install.sh` manages
a symlink. Pick one per machine.
## Related repos
- [`pi-toolkit`](https://gitea.jordbo.se/joakimp/pi-toolkit)
— pi bring-up: settings template, keybindings, shell env loader. Install
this first.
- [`mempalace-toolkit`](https://gitea.jordbo.se/joakimp/mempalace-toolkit)
— persistent memory layer. Installs `mempalace.ts` into
`~/.pi/agent/extensions/` separately from this repo.
- [`skillset`](https://gitea.jordbo.se/joakimp/skillset)
— same pattern for agent skills rather than extensions.
- [`opencode-devbox`](https://gitea.jordbo.se/joakimp/opencode-devbox)
— Docker containers; composes toolkits via independent install.sh calls.