diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5d5155b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,178 @@ +# 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) +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 /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/.ts` → + `~/.pi/agent/extensions/.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 ` + 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-.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-.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. + +## Testing + +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-.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.