Files
pi-extensions/README.md
T
joakimp 141bf64d81 Add mcp-loader extension: generic MCP server registration via settings.json
Reads an `mcp` block from ~/.pi/agent/settings.json (same shape as
opencode and Claude Desktop) and connects to each declared MCP server,
exposing all of their tools to pi as native tools namespaced as
<server-name>_<tool-name>.

Why: pi has no built-in MCP loader. Adding each new MCP server as a
hand-rolled extension (the way mempalace.ts does it) doesn't scale.
This is the config-driven generalization — one extension, any number
of servers, no per-server boilerplate.

Settings.json schema matches opencode and Claude Desktop verbatim:

  {
    "mcp": {
      "searxng": {
        "type": "local",
        "command": ["uvx", "mcp-searxng"],
        "env": { "SEARXNG_URL": "https://searxng.your-host.lan" }
      },
      "context7": {
        "type": "remote",
        "url": "https://mcp.context7.com/mcp"
      }
    }
  }

Per-server keys: type (local/remote), command, url, enabled, env.

Implementation:
  • StdioMcpClient class spawns subprocess, performs MCP initialize
    handshake (protocol 2024-11-05), lists tools, exposes a callTool()
    method. Newline-delimited JSON-RPC over stdio.
  • Each MCP tool registered via pi.registerTool with the server-
    namespaced name, the upstream MCP inputSchema passed through
    via Type.Unsafe (TypeBox is JSON-Schema-compatible at runtime).
  • Per-server fail-soft: a server that won't start logs one stderr
    line and is skipped; others continue.
  • SIGTERM all subprocesses on session_shutdown so /reload doesn't
    leak processes.

Tool naming: prefix with <serverName>_ except when the upstream tool
name already starts with that prefix (mempalace's tools are already
mempalace_search, mempalace_kg_query, etc — avoids double-prefixing).

Coexists with mempalace.ts but does not replace it. The mempalace
bridge has bespoke agent-identity injection that's worth preserving.

v1 limitations:
  • Stdio transport only. Remote (streamable-HTTP) servers are
    detected and skipped with a warning. v2 will add streamable-HTTP.
  • No reconnect on subprocess death — same limitation as mempalace.ts.

Verification:
  • node --check syntax clean
  • Standalone smoke test against `uvx mcp-server-time`: handshake +
    tools/list (2 tools) + tools/call (get_current_time) all green
    on the same JSON-RPC code that lives inside the loader.

Debug: set PI_MCP_LOADER_DEBUG=1 to surface per-server stderr.
2026-05-08 20:02:21 +02:00

282 lines
12 KiB
Markdown

# pi-extensions
Custom and modified extensions for the [pi coding-agent](https://github.com/mariozechner/pi-coding-agent).
This repo is the single source of truth for extensions that aren't suitable for general publishing — personal workflow tweaks, modified versions of built-in examples, and extensions written for specific infrastructure. Symlinked into `~/.pi/agent/extensions/` so pi loads them automatically.
Part of the same family as [`pi-toolkit`](https://gitea.jordbo.se/joakimp/pi-toolkit) (bring-up) and [`skillset`](https://gitea.jordbo.se/joakimp/skillset) (agent skills).
---
## Install
```bash
git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-extensions.git ~/src/src_local/pi-extensions
cd ~/src/src_local/pi-extensions
chmod +x install.sh
./install.sh
```
Each `.ts` file in `extensions/` is symlinked into `~/.pi/agent/extensions/`. Existing real files are backed up with a timestamp. Re-runs are idempotent.
**Install a subset:**
```bash
./install.sh --only ssh-controlmaster # just this one
./install.sh --only "ssh-controlmaster,other" # explicit list
./install.sh --skip "git-checkpoint" # all except these
```
`--only` and `--skip` accept comma-separated names without the `.ts` suffix. `--only` takes precedence if both are given.
### Alternative: pi install (local path)
Because `package.json` declares a `pi` manifest, you can also register this repo as a pi package:
```bash
pi install ~/src/src_local/pi-extensions
```
This makes pi manage the extension loading directly. The `install.sh` approach (symlinks) and `pi install` are mutually exclusive for the same extension — pick one per machine.
### Uninstall
```bash
./install.sh --uninstall
```
Removes symlinks that point into this repo. Your own files in `~/.pi/agent/extensions/` are never touched.
---
## Extensions
### `ssh-controlmaster.ts`
Transparent SSH remote execution via a persistent ControlMaster socket.
When launched with `--ssh user@host`, all of pi's native file and shell tools
(`read`, `write`, `edit`, `bash`) are transparently redirected to execute on
the remote machine. One SSH connection is established at session start; all
subsequent tool calls multiplex over it via a Unix socket. Much faster than
the plain `ssh.ts` example which opens a new connection per tool call.
**Use cases:**
- Diagnose and fix issues on a remote server without installing pi there
- Work on Proxmox hosts, LXC containers, or ephemeral VMs
- Any machine you have SSH key access to but don't own
**Usage:**
```bash
# Key-based auth (normal)
pi --ssh user@192.168.1.10
# Explicit remote path (skips the initial pwd call)
pi --ssh root@proxmox-node:/etc/pve
# Password auth — prompts before connecting
pi --ssh user@host --ssh-ask-pass
# Try without modifying your global install
pi -e ~/src/src_local/pi-extensions/extensions/ssh-controlmaster.ts --ssh user@host
```
**Requirements:**
- SSH key-based auth (preferred), or password auth via `--ssh-ask-pass` (see below)
- `bash` available on the remote
> **Note on `--ssh-ask-pass`:** The password is prompted via pi's input dialog
> before the SSH connection is opened. Input is **not masked** — the password
> is visible while typing. It is passed to SSH via a temporary `SSH_ASKPASS`
> script (`/tmp/pi-askpass-<pid>.sh`, `chmod 700`) which is deleted
> immediately after the master is established.
**How it works:**
1. On `session_start`, runs `ssh -G <host>` to read the effective config for that host
2. If `~/.ssh/config` already configures `ControlMaster auto` or `yes` for the host, the existing system socket is reused — no second connection is opened and pi does **not** tear down the master on exit (it was the system's to manage)
3. Otherwise pi establishes its own master: `ssh -fN -o ControlMaster=yes -o ControlPersist=yes -o ControlPath=/tmp/pi-cm-<pid>.sock <remote>` and shuts it down cleanly on exit
4. All tool calls multiplex over the socket with `-o ControlMaster=no -o ControlPath=<socket>` — near-zero per-call overhead
5. The system prompt is patched to tell the LLM it's operating on `<remoteCwd> (via SSH ControlMaster: <remote>)`
6. User `!` shell commands are also routed over SSH
The status bar shows `⚡ own master` or `⚡ system master` so you can see which path was taken.
**Status bar:** Shows `SSH ⚡ user@host:/path` when the master is ready, `⟳ connecting…` during setup, and an error state if the master fails to start.
**Path mapping:** Paths are rewritten by replacing the local `cwd` with the remote `cwd`. This means pi should be started from a directory that maps cleanly to a path on the remote. Use the `user@host:/explicit/path` form when the remote path differs significantly from your local working directory.
---
### `confirm-destructive.ts`
Confirmation gates for dangerous bash commands and destructive session actions. Always-on — no flag needed.
**Bash commands intercepted:**
- Recursive removes (`rm -rf`, `rm -r`, etc.)
- Any `sudo` command
- `chmod`/`chown 777`
- `dd if=` (disk operations)
- `mkfs` (format filesystem)
- `git push --force` / `git push -f`
- Writes to `/dev/*`
- `truncate --size 0`
In non-interactive mode (e.g. `pi -p`) dangerous commands are blocked outright rather than prompted.
**Session actions gated:**
- `/new` — confirms before clearing the session
- `/resume` — confirms before switching away if the current session has messages
- `/fork` — always confirms
---
### `git-checkpoint.ts`
Creates a git stash checkpoint at the start of each turn, keyed to the session entry ID. If you `/fork` from a past entry, you're offered the option to restore the code to that point.
Silently skips when the working directory isn't inside a git repo, or when there are no changes to stash. Status bar shows `⎇ N checkpoints` during active sessions.
**Notes:**
- Uses `git stash create` — non-destructive, doesn't touch your working tree
- Stash objects persist in the git repo even after pi exits, so you can apply them manually with `git stash apply <ref>` if needed
- Checkpoints are in-memory per session — the entry→ref mapping is lost on restart, but the underlying stash objects remain
---
### `notify.ts`
Sends a native terminal notification when the agent finishes and is waiting for input. Only fires when the agent ran for longer than the threshold (default 8 seconds) — quick responses are silently skipped.
**Terminal support:**
- Kitty (`KITTY_WINDOW_ID`) → OSC 99
- Windows Terminal / WSL (`WT_SESSION`) → Windows toast
- Everything else (iTerm2, WezTerm, Ghostty) → OSC 777
**Flag:**
```bash
pi --notify-min-secs 15 # only notify for tasks over 15 seconds
pi --notify-min-secs 0 # notify on every agent completion
```
### `ext-toggle.ts`
Registers `/ext` — a slash command that lists extensions in `~/.pi/agent/extensions/` and toggles individual ones on/off without leaving the TUI.
**How it works:** pi auto-discovers `*.ts` only. Toggling renames a file (or symlink) between `name.ts` and `name.ts.off`, so a disabled extension is invisible to the loader. After a toggle, the extension calls `ctx.reload()` so the change takes effect immediately — no restart needed.
**Usage:**
```
/ext # opens the multi-toggle overlay
```
- `↑` / `↓` — navigate
- `space` — stage a toggle (visual `●` / `○` flip; not yet applied)
- `enter` — commit all staged changes and reload pi
- `esc` — cancel, no changes
A footer line shows pending changes (e.g. `pending: notify→off, foo→on`) so you can see exactly what `enter` will apply. Guard rejections appear there too (`⊘ ssh-controlmaster: …`).
**Notes:**
- Subdirectory-style extensions (`name/index.ts`) are listed read-only — v1 doesn't toggle them. Move the directory aside manually if needed.
- `install.sh --uninstall` cleans up both `.ts` and `.ts.off` symlinks pointing into this repo, so a disabled extension won't be left behind.
- Re-running `./install.sh` respects a prior `/ext` disable: if `<name>.ts.off` already exists, the installer leaves it alone instead of silently re-enabling.
- `ssh-controlmaster` cannot be disabled via `/ext` while pi was launched with `--ssh` — disabling mid-session would silently revert tool calls to the local filesystem. Exit pi and relaunch without `--ssh` instead.
## Adding a new extension
1. Drop a `.ts` file into `extensions/`
2. Re-run `./install.sh` — it picks up the new file and symlinks it
3. In a running pi session, `/reload` is enough; no restart needed
4. (or, with `ext-toggle` installed: `/ext` to disable noisy ones at runtime)
### `todo.ts`
Gives the agent a `todo` tool (actions: `list` / `add` / `toggle` / `clear`) so it can externalize a multi-step plan and tick items off as it works. Also registers `/todos` so you can inspect the current list at any time.
State lives in the session's tool result `details`, not an external file. So:
- `pi --continue` / `--resume` brings the todos back with the conversation.
- `/fork` forks the todo list along with the branch — each branch has its own state.
This is a verbatim copy of the upstream `examples/extensions/todo.ts` shipped with `pi-coding-agent`. Refresh from upstream when desired (see `AGENTS.md`).
### `mcp-loader.ts`
Generic MCP server loader. Reads an `mcp` block from `~/.pi/agent/settings.json` (same shape as opencode and Claude Desktop) and connects to each declared server, exposing all of their tools to pi as native tools — namespaced as `<server-name>_<tool-name>` to avoid collisions.
**Settings.json shape:**
```jsonc
{
// … existing pi settings …
"mcp": {
"searxng": {
"type": "local",
"command": ["uvx", "mcp-searxng"],
"env": { "SEARXNG_URL": "https://searxng.your-host.lan" }
},
"gitea": {
"type": "local",
"command": ["gitea-mcp", "-t", "stdio"],
"enabled": false,
"env": { "GITEA_ACCESS_TOKEN": "...", "GITEA_HOST": "https://gitea.example.com" }
},
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp"
}
}
}
```
**Per-server keys:**
| Key | Description |
|---|---|
| `type` | `"local"` (stdio subprocess) or `"remote"` (streamable-http). Default `"local"`. |
| `command` | Argv array. First element is the executable, rest are args. Local servers only. |
| `url` | Remote MCP endpoint URL. Remote servers only. |
| `enabled` | Default `true`. Set `false` to disable a server without removing the entry. |
| `env` | Optional object of env vars injected into the subprocess. Inherits parent env first, then overlays these keys. Local servers only. |
**Limitations (v1):**
- **Stdio only.** Remote/streamable-HTTP transport is detected and skipped with a warning. Server like `context7` configured as `"type": "remote"` will not load until v2.
- **No reconnect** if a subprocess dies mid-session — those tools become unavailable until `/reload` (same as `mempalace.ts`).
- **Coexists with `mempalace.ts`** but does not replace it. The mempalace bridge has bespoke handling (agent identity injection) that's worth keeping. Don't list `mempalace` in the `mcp` block too — you'd get duplicate tool registrations.
**Debug:** set `PI_MCP_LOADER_DEBUG=1` in the environment to surface per-server stderr and connection logs.
Each extension is a TypeScript module loaded by [jiti](https://github.com/unjs/jiti) — no compilation step. See the [pi extensions docs](https://github.com/mariozechner/pi-coding-agent/blob/main/docs/extensions.md) and the [built-in examples](https://github.com/mariozechner/pi-coding-agent/tree/main/examples/extensions) for the API surface.
---
## Deploying on a new machine
```bash
# 1. Prerequisites: pi installed, SSH key auth working
pi --help # creates ~/.pi/agent/ on first run
# 2. Clone and install
git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-extensions.git ~/src/src_local/pi-extensions
cd ~/src/src_local/pi-extensions && ./install.sh
# 3. Verify
ls -la ~/.pi/agent/extensions/ # should show symlinks into this repo
```
---
## Related repos
- [`pi-toolkit`](https://gitea.jordbo.se/joakimp/pi-toolkit) — pi bring-up: settings template, keybindings, shell env loader
- [`mempalace-toolkit`](https://gitea.jordbo.se/joakimp/mempalace-toolkit) — persistent memory layer for pi via MemPalace MCP
- [`skillset`](https://gitea.jordbo.se/joakimp/skillset) — agent skills for pi, opencode, and Claude
## License
MIT — see [`LICENSE`](LICENSE).