add AGENTS.md
This commit is contained in:
@@ -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 <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.
|
||||
|
||||
## 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-<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.
|
||||
Reference in New Issue
Block a user