@bachi/pi-coder 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to this package. The extensions themselves are snapshot copies from the author's pi environment; their individual histories live in that repository.
4
4
 
5
+ ## 1.1.0 — 2026-09-18
6
+
7
+ Snapshot sync: the environment gained an MCP client and a startup fix for pi's built-in footer, and the `ayu` theme was resynced.
8
+
9
+ ### Added
10
+
11
+ - **`mcp/`** — MCP servers registered directly as pi tools (`mcp__<server>__<tool>`, Claude Code's naming). Config follows Claude Code's `.mcp.json` shape: global `~/.pi/agent/mcp.json` plus the nearest project `.mcp.json`. Three transports, implemented without `@modelcontextprotocol/sdk`: stdio, streamable HTTP and legacy HTTP+SSE. `${VAR}` / `${VAR:-default}` expansion, and `headersCommand` (aliases `headersHelper` / `http_headers_helper`) for dynamic auth headers. Commands: `/mcp`, `/mcp reload`, `/mcp <server>`. Diagnostics stay in an in-memory ring buffer rather than on stderr.
12
+ - **`statusline/footer-suppress.ts`** — pi's built-in footer is patched to render zero lines during the boot window, so it no longer paints its default state line before this statusline is installed. `PI_STATUSLINE_BOOT_SUPPRESS=off` disables it.
13
+ - Two `ayu` captures in the README, and a `pi.image` gallery preview in `package.json`.
14
+
15
+ ### Changed
16
+
17
+ - `themes/ayu.json` resynced: `userMessageText` now points at a new `textColor` var (`#dbdbdd`), and `toolPendingBg` now matches `userMessageBg` (`#1b1c1d`).
18
+ - The suite grows from **454 to 596 tests**.
19
+
20
+ ### Not included
21
+
22
+ - `config/mcp.json` — the snapshot's entries hold absolute paths of local MCP server executables, the same class of machine-specific value as `models.json`'s gateway registrations. `config/models.json` and the three model-selection keys in `config/settings.json` remain out as well.
23
+
5
24
  ## 1.0.0 — 2026-09-18
6
25
 
7
26
  First release. A complete pi coding-agent environment packaged for npm.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bachi/pi-coder
2
2
 
3
- A complete [Pi](https://pi.dev) coding-agent environment packaged for npm: **22 extensions**, **3 themes**, and the global config files that make them work together.
3
+ A complete [Pi](https://pi.dev) coding-agent environment packaged for npm: **23 extensions**, **3 themes**, and the global config files that make them work together.
4
4
 
5
5
  This is a working setup, not a collection of demos. Every extension is used daily, and each one documents the pi internals it depends on in its own file header — including the failure that motivated it and the things that look like they could be simplified but cannot be.
6
6
 
@@ -22,7 +22,7 @@ escape interrupt · ctrl+c/ctrl+d clear/exit · / commands · ! bash
22
22
  [Extensions]
23
23
  ask-user-question, auto-default-model, bash-command-collapse.ts, below-editor-after-statusline.ts,
24
24
  clear-command.ts, cwd-statusline.ts, exit-command.ts, fenceless-code-block, folder-history.ts,
25
- init-command.ts, prompt-editor.ts, read-path-collapse.ts, recap, rewind, simple-task, startup-logo,
25
+ init-command.ts, mcp, prompt-editor.ts, read-path-collapse.ts, recap, rewind, simple-task, startup-logo,
26
26
  statusline, subagent-log-guard, theme-command.ts, thinking-collapse.ts, tool-diff.ts, working-indicator
27
27
  ```
28
28
 
@@ -38,6 +38,14 @@ The startup list also loses its `[Context]`, `[Prompts]` and `[Themes]` sections
38
38
 
39
39
  Colors come from the active theme rather than from hardcoded values, so `/theme` repaints everything on the next frame.
40
40
 
41
+ ### The `ayu` theme
42
+
43
+ Two captures in `ayu`:
44
+
45
+ ![ayu theme, first capture](https://cdn.jsdelivr.net/gh/jayli/pi-coder@main/assets/ayu1.png)
46
+
47
+ ![ayu theme, second capture](https://cdn.jsdelivr.net/gh/jayli/pi-coder@main/assets/ayu2.png)
48
+
41
49
  ## Install
42
50
 
43
51
  ```bash
@@ -76,6 +84,7 @@ Without them two extensions degrade instead of failing: `recap` cannot tell whet
76
84
  | [`recap/`](extensions/recap/) | `/recap`, plus an automatic summary above the editor after 30s of idling. |
77
85
  | [`rewind/`](extensions/rewind/) | Shadow-git checkpoints and `/rewind` (or Esc Esc) to restore code and/or conversation. |
78
86
  | [`ask-user-question/`](extensions/ask-user-question/) | An `ask_user_question` tool: up to 4 questions with 2–4 described options plus a free-text row, answered in the terminal. |
87
+ | [`mcp/`](extensions/mcp/) | MCP servers become pi tools (`mcp__<server>__<tool>`) over stdio, streamable HTTP or legacy SSE, with `/mcp` status commands. |
79
88
  | [`auto-default-model/`](extensions/auto-default-model/) | Writes every model switch to `settings.json` — the Ctrl+S step, automated. |
80
89
  | [`subagent-log-guard/`](extensions/subagent-log-guard/) | Stops `[pi-subagents]` stderr diagnostics from corrupting the TUI. |
81
90
  | [`cwd-statusline.ts`](extensions/cwd-statusline.ts) | Prints the full working directory as a second statusline line. |
@@ -92,7 +101,7 @@ Without them two extensions degrade instead of failing: `recap` cannot tell whet
92
101
 
93
102
  ### Commands
94
103
 
95
- `/ask` `/bash-collapse` `/bash-preview` `/bash-stream` `/bash-timeout` `/bash-tree` `/clear` `/exit` `/init` `/read-collapse` `/recap` `/rewind` `/tasks` `/theme`
104
+ `/ask` `/bash-collapse` `/bash-preview` `/bash-stream` `/bash-timeout` `/bash-tree` `/clear` `/exit` `/init` `/mcp` `/read-collapse` `/recap` `/rewind` `/tasks` `/theme`
96
105
 
97
106
  Esc Esc opens `/rewind` (requires `doubleEscapeAction: "none"`, which the shipped config sets).
98
107
 
@@ -130,7 +139,7 @@ cp "$PKG/themes/"*.json ~/.pi/agent/themes/ # optional: a
130
139
  - `npmCommand` pins `pnpm --config.node-linker=hoisted`. Remove it if you do not have pnpm, or `pi install` will fail.
131
140
  - `doubleEscapeAction: "none"` hands Esc-Esc to the `rewind` extension instead of pi's built-in tree navigator.
132
141
 
133
- `config/models.json` is **not** shipped: provider and model registrations point at a local gateway and belong to the machine that runs it. See [docs/configuration.md](docs/configuration.md).
142
+ `config/models.json` and `config/mcp.json` are **not** shipped: provider registrations point at a local gateway and the MCP file holds absolute paths of local server executables, so both belong to the machine that runs them. MCP servers are configured in `~/.pi/agent/mcp.json` or a project `.mcp.json` — the `mcp/` extension reads both. See [docs/configuration.md](docs/configuration.md).
134
143
 
135
144
  ## Requirements
136
145
 
@@ -144,15 +153,15 @@ cp "$PKG/themes/"*.json ~/.pi/agent/themes/ # optional: a
144
153
  | --- | --- |
145
154
  | [docs/installation.md](docs/installation.md) | Install, verify, upgrade, uninstall, and the local-checkout workflow. |
146
155
  | [docs/configuration.md](docs/configuration.md) | Every shipped config file, what was removed from the snapshot, and why. |
147
- | [docs/extensions.md](docs/extensions.md) | Reference for all 22 extensions: commands, switches, caveats, storage. |
156
+ | [docs/extensions.md](docs/extensions.md) | Reference for all 23 extensions: commands, switches, caveats, storage. |
148
157
  | [docs/themes.md](docs/themes.md) | Theme files, the custom tokens, and the rules that make them load. |
149
- | [docs/development.md](docs/development.md) | Running the 454 unit tests, verifying against a real pi, publishing. |
158
+ | [docs/development.md](docs/development.md) | Running the 596 unit tests, verifying against a real pi, publishing. |
150
159
  | [docs/handbook.zh.md](docs/handbook.zh.md) | **Chinese.** The original handbook this package was extracted from: the author's machine, gateway setup, and the full rationale behind every design decision. |
151
160
 
152
161
  ## Development
153
162
 
154
163
  ```bash
155
- npm test # node --test, 454 tests
164
+ npm test # node --test, 596 tests
156
165
  ```
157
166
 
158
167
  The pure-logic modules are deliberately free of `@earendil-works/pi-*` imports so they run under plain `node --test`; see [docs/development.md](docs/development.md) for the layout rules, the tmux verification procedure and the traps this codebase documents.
@@ -71,7 +71,7 @@ Interaction tools need no exclusion: `ask_user_question` checks `ctx.hasUI` and
71
71
 
72
72
  Provider and model registrations are machine-specific: this setup's `litellm-any` provider points at a LiteLLM gateway on `127.0.0.1:996` (LAN address on other machines), carries a compat configuration, and registers six model ids that must match the gateway's routes exactly. Shipping it would be wrong on every other machine, so it is excluded.
73
73
 
74
- `models.json` is the only file excluded, and the three settings keys that select a model were removed along with it:
74
+ The three settings keys that select a model were removed along with it:
75
75
 
76
76
  | Removed key | Why |
77
77
  | --- | --- |
@@ -89,6 +89,14 @@ Everything else in `settings.json` is byte-for-byte the author's file. If you ru
89
89
 
90
90
  For how providers and thinking levels work, see pi's own `docs/models.md` and `docs/custom-provider.md`.
91
91
 
92
+ ### `mcp.json`
93
+
94
+ The MCP server list is machine-specific in the same way: the snapshot's only entry points at the absolute path of a local server executable, which exists on one machine only.
95
+
96
+ MCP servers are configured in `~/.pi/agent/mcp.json` and/or the nearest project `.mcp.json`, in Claude Code's shape. Neither file is shipped. With no config at all the `mcp/` extension loads, registers no tools and says so in `/mcp`. The format — including `headersCommand` for dynamic auth headers — is documented in [extensions.md](extensions.md) and, in more detail, in the [Chinese handbook](handbook.zh.md).
97
+
98
+ So two snapshot config files are deliberately left out of this package: `models.json` (gateway registrations) and `mcp.json` (paths of local MCP server executables). `AGENTS.md` and `settings.json` are shipped, and `settings.json` is the only shipped config file that differs from the snapshot — the three removed model keys.
99
+
92
100
  ### `pi-statusline.json` is legacy
93
101
 
94
102
  This file configures `npm:@narumitw/pi-statusline`, a package this environment no longer uses — `extensions/statusline/` replaced it. The local statusline reads **no config file at all**: colors come from `theme.fg(...)`, so it follows whatever theme is active, and the second line comes from other extensions calling `ctx.ui.setStatus()`.
@@ -9,6 +9,7 @@ These come from pi's extension discovery and they decide where a file may live:
9
9
  | `extensions/*.ts`, `extensions/*.js` | **Yes** — top-level files only. |
10
10
  | `extensions/<dir>/index.ts` or `index.js` | **Yes**. |
11
11
  | `extensions/<dir>/*.ts` without an `index` | No. Helper modules, imported by other extensions. |
12
+ | `extensions/<dir>/<subdir>/*` | No — nested directories are never scanned, which is how `mcp/fixtures/` ships test servers. |
12
13
  | `extensions/<dir>/*.test.ts` | No — only the directory's `index.ts` is loaded. |
13
14
  | `extensions/*.test.ts` (top level) | **Yes** — pi would try to load it. Never put tests at the top level. |
14
15
 
@@ -22,10 +23,18 @@ Two consequences worth remembering:
22
23
  ## Tests
23
24
 
24
25
  ```bash
25
- npm test # node --test — 454 tests, ~72 s
26
+ npm test # node --test — 596 tests, ~73 s
26
27
  ```
27
28
 
28
- The pure-logic modules are written so this works: they do not import `@earendil-works/pi-*` at all, take injected dependencies instead (a `widthOf` function, an `exec` function, a minimal theme interface), and are duck-typed against structural interfaces. That is why `thinking-collapse/window.ts`, `statusline/line.ts`, `tool-diff/title-row.ts`, `rewind/checkpoints.ts`, `prompt-editor/bash-prompt.ts` and the rest can run under plain `node --test`.
29
+ Test files run in parallel (`os.availableParallelism()` — 15 on the machine this was written on). Under that load one case is unreliable: the real spawned MCP handshake in `mcp/client.test.ts` intermittently hits its own 5 s handshake budget (seen twice in four full runs here, and never in isolation). The whole suite passes reliably with reduced parallelism at the same wall time:
30
+
31
+ ```bash
32
+ node --test --test-concurrency=4 # 596 tests, ~74 s
33
+ ```
34
+
35
+ The 5 s budget is inside the snapshot's `client.test.ts`, which this package keeps byte-identical — it belongs upstream in `clients/pi/`, not here.
36
+
37
+ The pure-logic modules are written so this works: they do not import `@earendil-works/pi-*` at all, take injected dependencies instead (a `widthOf` function, an `exec` function, a minimal theme interface), and are duck-typed against structural interfaces. That is why `thinking-collapse/window.ts`, `statusline/line.ts`, `tool-diff/title-row.ts`, `rewind/checkpoints.ts`, `prompt-editor/bash-prompt.ts` and the rest can run under plain `node --test`. `mcp/` goes further in the same direction: `protocol.ts`, `config.ts`, `client.ts`, `tools.ts` and `headers-command.ts` are pi-free too, so the whole chain — including a **real** spawned stdio server (`fixtures/fake-mcp-server.mjs`) and real `node:http` servers for the HTTP and SSE transports — is covered with no transport mocking.
29
38
 
30
39
  One test file goes the other way: [`prompt-editor/render.test.ts`](../extensions/prompt-editor/render.test.ts) loads the **real** extension through pi's own loader and asserts the `!` bash-mode render contract line by line and column by column, with only the surroundings faked (a `tui` that has just `terminal.rows` and `requestRender()`, an identity `borderColor`, keybindings that never match). It locates pi's library entry by reading the `# cmd-shim-target=` line out of the `pi` shim, and it **skips** — rather than failing or faking a pass — when pi cannot be resolved, because the copy under `~/.pi/agent/npm` is often an empty shell after `pi update --extensions`. Point it at a real entry with `PI_TEST_PI_ENTRY=/path/to/index.js`.
31
40
 
@@ -51,7 +60,7 @@ Isolate the run instead — a scratch agent directory has no global extensions,
51
60
  PI_CODING_AGENT_DIR=$(mktemp -d) pi -e /absolute/path/to/pi-coder
52
61
  ```
53
62
 
54
- Then check that all 22 loaded by reading the startup list:
63
+ Then check that all 23 loaded by reading the startup list:
55
64
 
56
65
  ```
57
66
  [Extensions]
@@ -78,7 +87,8 @@ Everything below is documented because it cost real debugging time. The full rea
78
87
  - **A `ctx` captured before a session replacement goes stale**, and reading `ctx.ui` throws `This extension ctx is stale after session replacement or reload`. The throw happens when you read the property, before any widget `render()` runs, so a `try/catch` inside `render()` cannot catch it. A timer that outlives the session takes the host process down with it (`exit=1`). `simple-task/` and `working-indicator/` therefore all three: catch inside the callback and stop the timer, wrap every `ctx.ui` access, and stop timers in `session_shutdown`.
79
88
  - **A throwing `renderCall` is silently swallowed** and replaced by `createCallFallback()`: something disappears from the UI and nothing is logged.
80
89
  - **Tool registration is first-registration-wins per name.** A second extension registering `bash` is ignored without a warning — which is why everything that shapes `bash` rendering lives in one file.
81
- - **`keyHint` and `keyText` must not be imported from the package root.** In the bundled CLI, `@earendil-works/pi-coding-agent` is aliased to a different module instance, so the extension gets another copy of the stateful APIs (`Theme not initialized`, or an empty string). Read key names from `~/.pi/agent/keybindings.json` instead. `startup-logo` is the one file that imports from the package root, wrapped in a `try/catch`.
90
+ - **Reading pi state at module top level breaks; patching a class prototype does not.** In the bundled CLI, `@earendil-works/pi-coding-agent` resolves through the loader's `virtualModules` to the same chunk `interactive-mode.js` uses but importing `keyHint`/`keyText` yields another module instance's state (`Theme not initialized`, or an empty string), so key names are read from `~/.pi/agent/keybindings.json` instead. The rule is about *state*, not classes: `statusline/footer-suppress.ts` imports `FooterComponent` from the package root and patches `prototype.render`, and an A/B capture shows the patch landing on the instance pi itself constructs. `startup-logo` still wraps its package-root import in a `try/catch`.
91
+ - **Extensions are loaded before pi constructs the TUI.** That ordering is what makes the footer patch above possible at factory time, and it sets the price: anything installed that early must be reversible. `/reload` re-evaluates the module (the patch key is a `Symbol.for` in the global registry so a new instance releases the old one), and the 30 s cap covers the case where the handoff never happens.
82
92
  - **Patching a pi-tui prototype works; patching the copy in `node_modules` does nothing** — silently. pi's bundled loader points extensions at its own inlined namespace, which is why `fenceless-code-block/` can patch `Markdown.prototype` and `index.test.ts` can assert it with pi's own renderer.
83
93
  - **`usage.output` is always `0` while streaming**, so token counts must be estimated from streamed characters.
84
94
  - **`renderResult` receives no `isError`**; read it from `context`. Reading `result.isError` silently paints failures as successes.
@@ -96,24 +106,29 @@ A new tool name and a new command name must not collide with any other extension
96
106
 
97
107
  ## Keeping this package in sync
98
108
 
99
- This package is a distribution copy, not the master copy. The author's live environment is `~/.pi/agent/`, snapshotted into a separate repository under `clients/pi/`; this package was produced by copying that snapshot verbatim (extensions, themes, and the config files) with two deliberate deltas:
109
+ This package is a distribution copy, not the master copy. The author's live environment is `~/.pi/agent/`, snapshotted into a separate repository under `clients/pi/`; this package was produced by copying that snapshot verbatim (extensions, themes, and the config files) with three deliberate deltas:
100
110
 
101
- 1. `config/models.json` is not shipped, and the three model-selection keys were removed from `config/settings.json` (`defaultProvider`, `defaultModel`, `modelThinkingLevels`). See [configuration.md](configuration.md#what-is-not-shipped).
111
+ 1. `config/models.json` and `config/mcp.json` are not shipped, and the three model-selection keys were removed from `config/settings.json` (`defaultProvider`, `defaultModel`, `modelThinkingLevels`). Both excluded files hold machine-local values — gateway registrations and absolute paths of local MCP server executables. See [configuration.md](configuration.md#what-is-not-shipped).
102
112
  2. `docs/handbook.zh.md` is the snapshot's README, kept verbatim as the Chinese handbook.
113
+ 3. Everything else under `docs/`, plus `README.md` and `CHANGELOG.md`, is written for this package: extension count, test count and the switch tables have to be updated by hand.
103
114
 
104
- So when the extensions change upstream:
115
+ So when the snapshot changes upstream:
105
116
 
106
117
  ```bash
107
118
  SRC=/Users/bachi/jaylli/litellm-any/clients/pi # the snapshot the extension lives in
108
119
  DST=/Users/bachi/jaylli/pi-coder # this package
109
120
  cp -R "$SRC/extensions/." "$DST/extensions/"
110
121
  cp "$SRC/themes/"*.json "$DST/themes/"
111
- diff -r "$SRC/extensions" "$DST/extensions" # expect: no output
122
+ cp "$SRC/AGENTS.md" "$DST/config/AGENTS.md"
123
+ cp "$SRC/README.md" "$DST/docs/handbook.zh.md" # the handbook is the snapshot README, verbatim
124
+ diff -r "$SRC/extensions" "$DST/extensions" # expect: no output
125
+ diff -r "$SRC/themes" "$DST/themes" # expect: no output
126
+ diff "$SRC/AGENTS.md" "$DST/config/AGENTS.md" # expect: no output
112
127
  npm test
113
- # bump "version" in package.json, add a CHANGELOG entry
128
+ # bump "version" in package.json, add a CHANGELOG entry, update the counts in README.md and docs/
114
129
  ```
115
130
 
116
- Keep the copies byte-identical. The only files that should ever differ from the snapshot are `config/settings.json` (the removed model keys) and anything under `docs/`.
131
+ Nothing else is copied. `config/settings.json` is the only file in the package that may differ from the snapshot, and `diff` on it is expected to show exactly the three removed model keys.
117
132
 
118
133
  ## Publishing
119
134
 
@@ -164,7 +179,7 @@ Once the package exists on npm, two optional additions become safe (they render
164
179
  ![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux-blue?style=for-the-badge)
165
180
  ```
166
181
 
167
- 2. **A preview asset**, which is what makes a TUI package legible in the gallery. Upload a screenshot (PNG/JPEG/GIF/WebP) or a screencast (MP4 only) a `github.com/user-attachments/...` URL from a README upload works then declare it:
182
+ 2. **A preview asset** — done: `package.json` declares `pi.image` pointing at `assets/ayu1.png`, and the README shows both `ayu` captures. The PNGs live in `assets/`, which is not in the npm `files` list, so they stay out of the tarball. They are served through jsDelivr (`https://cdn.jsdelivr.net/gh/jayli/pi-coder@main/assets/...`) rather than `raw.githubusercontent.com`, which timed out intermittently from this machine; either form needs `assets/` to be on `main`. To use a video instead, upload an MP4 (a `github.com/user-attachments/...` URL works) and declare `pi.video`:
168
183
 
169
184
  ```json
170
185
  "pi": {
@@ -1,6 +1,6 @@
1
1
  # Extensions reference
2
2
 
3
- 22 extensions load from this package. Twelve are single files in `extensions/`, ten are directories whose entry point is `index.ts`. Three more directories (`thinking-collapse/`, `tool-diff/`, `prompt-editor/`) contain pure-logic modules only — they have no `index.ts`, so pi never loads them as extensions, but the top-level files import them.
3
+ 23 extensions load from this package. Twelve are single files in `extensions/`, eleven are directories whose entry point is `index.ts`. Three more directories (`thinking-collapse/`, `tool-diff/`, `prompt-editor/`) contain pure-logic modules only — they have no `index.ts`, so pi never loads them as extensions, but the top-level files import them.
4
4
 
5
5
  Every extension is also documented in its own header comment (Chinese, except `rewind/`): the pi internals it relies on, the failure that motivated it and the trade-offs that are not visible in the code. This page is the map.
6
6
 
@@ -17,6 +17,9 @@ Every extension is also documented in its own header comment (Chinese, except `r
17
17
  | `/clear` | `clear-command` | — Alias of `/new`. |
18
18
  | `/exit` | `exit-command` | — Alias of `/quit` (the argument-free form of the quit words). |
19
19
  | `/init` | `init-command` | `[file.md] [extra instructions]` |
20
+ | `/mcp` | `mcp` | — Status of every configured server: transport, tool count, protocol version, config source. |
21
+ | `/mcp reload` | `mcp` | — Re-read the config files, reconnect and re-register tools. |
22
+ | `/mcp <server>` | `mcp` | — One server's details and its recent diagnostics. |
20
23
  | `/read-collapse` | `read-path-collapse` | `off` \| `on` |
21
24
  | `/recap` | `recap` | — Summarizes the conversation now. |
22
25
  | `/rewind` | `rewind` | — Checkpoint menu; also Esc Esc at an empty prompt. |
@@ -83,6 +86,7 @@ Replaces pi's footer with one status line and one status row:
83
86
  The main row shows model/thinking level, context usage, git branch and diff stat; when the working directory is not a git repository it says `no git`. The second row renders whatever other extensions pass to `ctx.ui.setStatus()` (this is where `cwd-statusline`, `simple-task` and `rewind` write). Lines are truncated, never wrapped. Git reads happen on a debounced background path (400 ms after `turn_end`/`agent_end`/`tool_execution_end`, immediately on branch change, with a 30 s fallback poll) so the render path is a map lookup.
84
87
 
85
88
  - `PI_STATUSLINE_FREEZE=off` — disable the footer freeze. On every session switch pi unconditionally restores its builtin footer and clears all `setStatus` values, and no extension hook runs before that frame. The guard replays the previous frame's lines instead, which removes a visible flash. Turning it off restores the flash.
89
+ - `PI_STATUSLINE_BOOT_SUPPRESS=off` — disable boot-window suppression. pi's built-in footer exists before the first extension runs (measured on this setup: its first frame lands at ~480 ms, this statusline at ~1.2 s), so without it you see the default state line and then watch the statusline replace it. [`statusline/footer-suppress.ts`](../extensions/statusline/footer-suppress.ts) patches `FooterComponent.prototype.render` at **extension-factory time** — before pi's TUI is constructed — to return zero lines, and releases it the moment our footer is installed. A 30 s cap releases it anyway when the handoff never happens (an extension error, or a non-TUI mode), so the bottom is never left permanently empty. The two windows have independent switches because they need different remedies: this one has no previous frame to replay, the freeze above has one.
86
90
  - No config file. Colors come from `theme.fg(...)`, so `/theme` repaints on the next frame.
87
91
 
88
92
  ### `cwd-statusline.ts` — full working directory
@@ -222,6 +226,34 @@ The labels `Other` and `Type something.` are reserved — validation rejects the
222
226
  - `PI_ASK_USER_QUESTION=off` — do not register the tool.
223
227
  - `/ask` previews the dialog with a demo questionnaire.
224
228
 
229
+ ### `mcp/` — MCP servers as tools
230
+
231
+ Every MCP tool is registered as a pi tool directly, named `mcp__<server>__<tool>` — Claude Code's convention, so prompts, skills and permission rules written for it keep working. There is deliberately no single "mcp" proxy tool: direct tools are friendlier to the model, and the only cost is a longer system prompt.
232
+
233
+ Configuration follows Claude Code's `.mcp.json` shape, read from two places: the global `~/.pi/agent/mcp.json`, plus the **first** `.mcp.json` found walking up from the working directory (at most 32 levels). Project entries override global ones by name, so an existing repo-local `.mcp.json` works as it is.
234
+
235
+ | Field | Transport | Notes |
236
+ | --- | --- | --- |
237
+ | `command` / `args` / `env` / `cwd` / `timeout` | stdio | `timeout` is the per-call budget in milliseconds (default `120000`). |
238
+ | `url` / `headers` | HTTP | Streamable HTTP, or legacy HTTP+SSE when `type: "sse"`. |
239
+ | `headersCommand` | HTTP | Dynamic auth headers: the command's output becomes headers. |
240
+ | `enabled: false`, `disabled: true` | either | Keep the entry for `/mcp`, do not connect. |
241
+
242
+ String fields expand `${VAR}` and `${VAR:-default}`. Sessions connect every enabled server in parallel at `session_start` and close them at `session_shutdown`; handshakes have their own 20 s cap and a server that fails costs one warning, not the session.
243
+
244
+ **`headersCommand`** is the cheap half of OAuth: most SaaS MCP servers also accept a static token (a GitHub PAT, `CONTEXT7_API_KEY`, a Sentry or Figma token), so fetching one with a command avoids implementing OAuth 2.1. Three output shapes are accepted — a flat JSON object, a `{"headers": {...}}` wrapper, or `Name: Value` lines — and `headersHelper` (Claude Code) and `http_headers_helper` (Codex) are aliases, so a copied config needs no field edits. `headersCommandTimeout` defaults to 10 s.
245
+
246
+ Four semantics worth knowing:
247
+
248
+ - It runs **once per connection**, merged over the static `headers` — the dynamic value is the fresher credential and wins. HTTP protocol headers (`content-type`, `accept`, `mcp-protocol-version`, `mcp-session-id`) cannot be set from config.
249
+ - A **401/403 re-runs the command once, but the request is retried only if the headers actually changed**, so a command that returns the same token does not pay for a second round trip. On the legacy SSE transport only the POST is rebuilt, not the GET stream.
250
+ - **Failure is not fatal.** A timeout, a non-zero exit or unparseable output falls back to the static headers and is recorded in the diagnostics; a later genuine rejection carries that reason in its error message, so a dead command is not mistaken for an expired token.
251
+ - **Header values are never logged or displayed.** Diagnostics name headers only, and a parse failure does not echo the command output, which may be a secret in full. `/mcp <server>` shows the command from your config, not what it returned.
252
+
253
+ The wire layer is implemented here (`protocol.ts`, `client.ts`) and does not use `@modelcontextprotocol/sdk` — the extension directory has no `node_modules`. Only `initialize`, `notifications/initialized`, `tools/list` and `tools/call` are implemented; OAuth, sampling, elicitation, progress and `tools/list_changed` are deliberately absent, and server-to-client requests are answered `-32601` instead of being left to hang. Tool output is truncated at pi's built-in 50 KB / 2000-line limit, and MCP `resource`, `resource_link` and `audio` blocks degrade to a text note, because pi's tool content accepts only text and images.
254
+
255
+ Diagnostics go to a per-server in-memory ring buffer (20 lines kept, the most recent 8 printed by `/mcp <server>`) and never to stdout or stderr, which in an interactive session would land on top of the editor — the reason `subagent-log-guard/` exists. There is no environment switch: with no config file the extension loads, registers nothing and says so in `/mcp`.
256
+
225
257
  ### `subagent-log-guard/` — stderr guard
226
258
 
227
259
  `pi-subagents` prints launch diagnostics such as `[pi-subagents] Agent 'researcher': host runtime tool availability omitted [...]` with `console.warn`. In interactive mode pi does not take over stdout/stderr, so that text is written straight into the alternate screen at the hardware cursor — right on top of the editor row — and the differential renderer will not repaint it. The result is permanent garbage across the input box.
@@ -258,6 +290,7 @@ Every switch is an environment variable read at use time, not cached at load, so
258
290
  | `PI_READ_COLLAPSE=off` | on | `read-path-collapse` | Keep pi's built-in `read` title row. |
259
291
  | `PI_SPINNER_COLOR_HOLD` | `19` | `working-indicator` | Frames per color in the spinner cycle. |
260
292
  | `PI_SPINNER_RAINBOW=off` | on | `working-indicator` | Disable the rainbow spinner. |
293
+ | `PI_STATUSLINE_BOOT_SUPPRESS=off` | on | `statusline` | Do not silence pi's built-in footer during the boot window, before this statusline is installed. |
261
294
  | `PI_STATUSLINE_FREEZE=off` | on | `statusline` | Disable the footer freeze that hides the one-frame flash on session switch. |
262
295
  | `PI_SUBAGENT_LOG_GUARD` | `drop` | `subagent-log-guard` | `notify` shows the diagnostics through `ctx.ui.notify`; `off` disables the guard. |
263
296
  | `PI_WORKING_SUMMARY=off` | on | `working-indicator` | Disable the prompt summary line. |
@@ -272,6 +305,7 @@ Every switch is an environment variable read at use time, not cached at load, so
272
305
  - **The `bash` tool can only be registered once.** Everything that shapes its rendering lives in `bash-command-collapse.ts` for that reason — a second file registering `bash` would be ignored silently.
273
306
  - **`recap` imports `simple-task/gap.ts`.** The neighbour-gap heuristic is shared rather than duplicated, so `recap` and `simple-task` must be installed together. In this package they always are; if you copy extensions individually, copy both.
274
307
  - **The theme preview and the theme files are coupled.** `/theme` persists the name it previewed, and the name must match the `theme` field's expectations in [themes.md](themes.md).
308
+ - **MCP tool names are namespaced.** `mcp__<server>__<tool>` collides with neither the builtins nor the extensions' own tools; names past 64 characters are truncated with a hash suffix, which stays inside the tool-name limit the model APIs enforce while keeping truncated names distinguishable.
275
309
  - **Three extensions read theme tokens that pi's schema does not define** (`toolDiffAddedBg`, `toolDiffRemovedBg`, `bashOutput`) and degrade quietly when a theme omits them.
276
310
 
277
311
  ## State on disk
@@ -283,6 +317,7 @@ Every switch is an environment variable read at use time, not cached at load, so
283
317
  | `~/.pi/folder-history/<path-with-dashes>.jsonl` | `folder-history` | Command history per working directory. |
284
318
  | Session log (via `appendEntry`) | `simple-task` | Task list state; discarded with the session, never written to the repo. |
285
319
  | In memory only | `recap` | The current summary; lost on `/new` or `/resume` by design. |
320
+ | In memory only | `mcp` | Per-server status, the registered tool table and a 20-line diagnostic ring buffer per server. Config files are read, never written. |
286
321
  | Nothing | everything else | The remaining extensions are pure display or event wiring. |
287
322
 
288
323
  ## Adding, disabling and removing extensions
@@ -17,6 +17,7 @@ adapter(局域网别的机器用则换成网关主机 LAN IP)。
17
17
  | `AGENTS.md` | `~/.pi/agent/AGENTS.md`(机器全局行为规则) |
18
18
  | `config/settings.json` | `~/.pi/agent/settings.json` |
19
19
  | `config/models.json` | `~/.pi/agent/models.json` |
20
+ | `config/mcp.json` | `~/.pi/agent/mcp.json`(MCP 服务器;不装就没有 MCP 工具,`/mcp` 会给出提示) |
20
21
  | `config/web-search.json` | `~/.pi/agent/web-search.json`(`pi-web-access` 自己的配置) |
21
22
  | `config/pi-statusline.json` | `~/.pi/agent/pi-statusline.json`(**已失效的遗留配置**:旧 npm statusline 包专用,留着只为随时换回那个包) |
22
23
  | `extensions/*.ts` | `~/.pi/agent/extensions/` |
@@ -30,6 +31,7 @@ cp clients/pi/AGENTS.md ~/.pi/agent/AGENTS.md
30
31
  cp clients/pi/config/settings.json ~/.pi/agent/settings.json
31
32
  cp clients/pi/config/models.json ~/.pi/agent/models.json
32
33
  cp clients/pi/config/pi-statusline.json ~/.pi/agent/pi-statusline.json # 可选:只有要回退到 npm statusline 包时才需要
34
+ cp clients/pi/config/mcp.json ~/.pi/agent/mcp.json # 可选:不装就没有 MCP 工具(见下文)
33
35
  cp clients/pi/config/web-search.json ~/.pi/agent/web-search.json
34
36
  cp clients/pi/extensions/*.ts ~/.pi/agent/extensions/
35
37
  cp -R clients/pi/extensions/tool-diff ~/.pi/agent/extensions/ # tool-diff.ts 的纯排版模块(无 index.ts,不会被当成扩展)
@@ -44,6 +46,7 @@ cp -R clients/pi/extensions/ask-user-question ~/.pi/agent/extensions/
44
46
  cp -R clients/pi/extensions/subagent-log-guard ~/.pi/agent/extensions/
45
47
  cp -R clients/pi/extensions/fenceless-code-block ~/.pi/agent/extensions/ # 子目录形式:纯逻辑在 render.ts(不 import pi,可单测)
46
48
  cp -R clients/pi/extensions/working-indicator ~/.pi/agent/extensions/
49
+ cp -R clients/pi/extensions/mcp ~/.pi/agent/extensions/ # MCP(纯逻辑模块 + fixtures 一起拷)
47
50
  mkdir -p ~/.pi/agent/themes && cp clients/pi/themes/*.json ~/.pi/agent/themes/
48
51
 
49
52
  pi install npm:pi-web-access # 外部包;装完必须配 web-search.json(见下文)
@@ -266,6 +269,68 @@ agent,加 `workflowScript` 脚本化编排。工具名(`subagent` / `subagen
266
269
  一并给出去,破坏只读契约;`evidence-auditor` 的白名单是同一个坏形状,但给它 `inherit` 等于白送写权限,
267
270
  所以留原样。交互类工具不用手动排除:`ask_user_question` 自己按 `ctx.hasUI` 判断,子会话里自动摘掉。
268
271
 
272
+ ## MCP 服务器(`mcp/`)
273
+
274
+ 让 pi 用上 Claude Code 那套 MCP 服务器:**每个 MCP 工具直接注册成一个 pi 工具**,名字
275
+ `mcp__<server>__<tool>`(Claude Code 同款,skill 与权限规则里的写法可以直接搬过来)。
276
+
277
+ 配置按 Claude Code 的 `.mcp.json` 形状:全局 `~/.pi/agent/mcp.json`,再加上从 cwd 往上找到的
278
+ **第一个**项目根 `.mcp.json`(同名 server 项目覆盖全局)。所以 `~/jayli/homework/.mcp.json` 里已有的
279
+ `wechat-local` 在 pi 里开箱即用,全局那份则是「在哪个目录都能用」。字段:`command` / `args` / `env` /
280
+ `cwd` / `timeout`(毫秒,工具调用用;握手另有 20s 上限)走 stdio;`url` / `headers`(`type: "sse"` 走旧版
281
+ HTTP+SSE,否则 streamable HTTP)走远程;字符串值支持 `${VAR}` 与 `${VAR:-默认值}`;`enabled: false` 保留条目但不连。
282
+
283
+ **动态请求头(`headersCommand`)**是 OAuth 的“便宜档”:很多 SaaS MCP 既支持 OAuth、也支持静态 token(GitHub PAT、
284
+ `CONTEXT7_API_KEY`、Sentry/Figma 的 token),所以与其为一个 header 实现整套 OAuth 2.1,不如让命令自己去取:
285
+ ```json
286
+ { "mcpServers": { "remote": {
287
+ "url": "https://mcp.example.com/mcp",
288
+ "headersCommand": "security find-generic-password -s example-mcp -w"
289
+ } } }
290
+ ```
291
+ 命令输出三种形状都认:扁 JSON 对象、`{"headers":{...}}` 包装、或 `Name: Value` 行(值里的冒号不会被切开)。
292
+ 别名 `headersHelper`(Claude Code)与 `http_headers_helper`(Codex)同样可用,从那边拷配置不用改字段名;
293
+ `headersCommandTimeout` 默认 10s。语义上有四条要记住:
294
+
295
+ - **每次连接只跑一次**,结果与静态 `headers` 合并(**动态的赢**,它是更新鲜的凭据);HTTP 协议头(`content-type`/
296
+ `accept`/`mcp-protocol-version`/`mcp-session-id`)优先级最高,配置改不动它们。
297
+ - **401/403 会重跑一次,但只有头真的变了才重试请求**(命令每次都返回同一个 token 就不会白重试一遍)。旧版 SSE
298
+ 只重建 POST,不重建 GET 长连接。
299
+ - **失败不致命**:命令超时/非零退出/输出不可解析时退回静态 headers 继续连,原因记进诊断;真被拒时错误信息里
300
+ 会带上这条原因(否则你只看到 401,以为是 token 过期)。
301
+ - **绝不记录头的值**:诊断只输出头的**名字**(`头命令取到 1 个头(Authorization)`),解析失败也不回显命令输出
302
+ —— 输出可能整段都是密钥。`/mcp <server>` 里显示的是命令本身(你自己的配置),不是取回来的值。
303
+
304
+ 没有浏览器弹窗、不写任何凭据存储:token 的生命周期完全归那条命令管(钥匙串、vault、`opencode auth` 都行)。
305
+ 只支持 OAuth(不接受静态 token)的 server 目前用不了,要支持得上第二档(OAuth 2.1 + PRM + DCR + 回调服务器),
306
+ 那基本就是 pi-mcp-adapter 的领域。
307
+
308
+ 三个命令入口:`/mcp` 看状态(server / 工具数 / 版本 / 配置来源),`/mcp reload` 改完配置不用重启 pi,
309
+ `/mcp <server>` 看单个 server 的详情与最近诊断。不开 pi 想验证配置就 `npm run mcp:probe -- <server> [tool]`
310
+ (用的是扩展里同一份客户端,通了 pi 里就通;**本机 Node 22 专用** —— 它直接 import `.ts`,靠 Node 的类型擦除,
311
+ 与 `npm run usage` 一样不在 Node 20 的路由器上跑)。
312
+
313
+ 改之前的约束:
314
+
315
+ - **传输是自己实现的**(`protocol.ts` + `client.ts`),**不依赖 `@modelcontextprotocol/sdk`** —— 扩展目录
316
+ 里没有 node_modules,引 SDK 就得给 `~/.pi/agent/extensions/mcp/` 铺依赖。协议面只做
317
+ initialize / notifications/initialized / tools/list / tools/call,其余(OAuth、sampling、elicitation、
318
+ progress、`tools/list_changed` 热更新)**刻意不做**;服务端反向请求一律回 `-32601`,不留傻等的对端。
319
+ - **诊断输出只进内存环形缓冲**(每个 server 20 行,`/mcp <server>` 看),**不写 stdout/stderr** ——
320
+ interactive pi 里往 stderr 写会直接糊在输入框上(`subagent-log-guard/` 就是为这个存在的)。
321
+ - **会话开始时连接、结束时断开**。工具表必须先 `tools/list` 才能注册,所以不能等首次调用才连;
322
+ 多个 server 并行握手,单个失败只影响它自己(启动时给一条 warning,不阻塞会话)。
323
+ - **工具输出必须截断**:沿用 pi 内建工具的 50KB / 2000 行上限(`tools.ts` 的头截断),图片块不计入、
324
+ 也不被截掉。MCP 的 `resource` / `resource_link` / `audio` 会降级成一行文本说明 —— pi 的 tool content
325
+ 只认 `text` 与 `image`,原样塞进去会被静默丢掉。
326
+ - **工具名有 64 字符硬上限**(Anthropic / OpenAI 的 tool 名限制):超长时截断工具名并接 FNV-1a 哈希后缀,
327
+ 保证截断后仍可区分。改命名规则时 `tools.test.ts` 的哈希稳定性用例会拦住手滑。
328
+ - **头命令的设计约束(`headers-command.ts`)**:① 子进程**刻意不 unref** —— 它是我们正在等的结果,unref
329
+ 会让 `pi -p` / probe 这类短命进程先退出、promise 永远不 resolve(单测当场拦到过);② 诊断只能用
330
+ `describeHeaderNames` 输出**头名**,头的值与解析失败的原文一律不打印(命令输出可能整段是 token);
331
+ ③ 命令失败不当致命错误,退回静态 headers 并把原因带进最终错误信息,否则用户只看到 401 而不知道是命令挂了;
332
+ ④ 401/403 重跑命令后**只在头真的变化时**重试(headless 与交互两种模式行为要一致)。
333
+
269
334
  ## 自写扩展:改之前要知道的
270
335
 
271
336
  每个扩展的完整理由都写在**它自己的文件头注释**里,这里只列「不在文件里、但改错了会静默坏掉」的约束。
@@ -282,6 +347,7 @@ agent,加 `workflowScript` 脚本化编排。工具名(`subagent` / `subagen
282
347
  | `exit-command.ts` | 整行 `exit` / `quit` 优雅退出(只在 TUI 模式;`--print` 里仍是普通 prompt) |
283
348
  | `init-command.ts` | Claude Code 式 `/init`:`CLAUDE.md` → 否则 `AGENTS.md` → 否则新建 `AGENTS.md` |
284
349
  | `ask-user-question/` | Claude Code `AskUserQuestion` 式的结构化提问工具(子会话里按 `ctx.hasUI` 自动摘掉) |
350
+ | `mcp/` | MCP 服务器 → pi 工具(`mcp__<server>__<tool>`);自带 stdio / streamable HTTP / 旧版 SSE 三种传输与 `/mcp` 命令。配置、约束与验证方式见上一节 |
285
351
 
286
352
  ### 跨扩展 / 跨文件
287
353
 
@@ -294,10 +360,22 @@ agent,加 `workflowScript` 脚本化编排。工具名(`subagent` / `subagen
294
360
  探针 widget,遍历 `tui.children` 找到「子树里装着这个探针」的顶层 child,再把它移到末尾。
295
361
  探针本身必须**显式传 `placement: "belowEditor"`**:漏写(默认落到 `aboveEditor`)会把「上方」那个容器
296
362
  整块搬走,而且没有任何运行时报错(实测踩到过,代码里只有一行注释提醒)。找不到容器就什么都不做。
363
+ - **「内置 statusline 露出来」有两个窗口,用了两套办法**:① **启动窗口**(进程刚起来 → `session_start` 轮到我们。
364
+ 实测:内置 footer 在 ~470ms 出首帧,我们的 statusline 到 ~1.2s 才装上)没有上一帧可重放,由
365
+ `statusline/footer-suppress.ts` 在**扩展工厂**里(此时 TUI 还没 new 出来)接管 `FooterComponent.prototype.render`,
366
+ 窗口内渲染 0 行 —— 底部留白,而不是先画一个马上要变的默认状态行;我们的 footer 挂上的一刻交还,
367
+ 30s 兜底(`mcp` 握手 20s 上限也在这个窗口里,因为 `Runner.emit()` 串行 await,字母序在前的 `mcp` 先跑)。
368
+ ② **换会话窗口**由 `footer-guard.ts` 重放上一帧压住(有旧状态可留,比留白更好)。两个开关独立:
369
+ `PI_STATUSLINE_BOOT_SUPPRESS=off` / `PI_STATUSLINE_FREEZE=off`。
370
+ 补丁打的是包根导出的 `FooterComponent` —— 实测(0.85.1 bundle 形态,A/B pty 捕获,两次只差这一处)
371
+ 它**就是** pi 自己 `new` 出来那个类:临时改成返回 `["PROBE-FOOTER-MARKER"]` 时屏幕上真的出现这一行,
372
+ 关掉开关后内置 footer 照旧。
297
373
  - **`statusline/footer-guard.ts` 与 `startup-logo/header-guard.ts` 是同一套机制的两份**(接管容器的
298
374
  `render`、重放上一帧的行),互不依赖、符号键不同。原因是 pi 换会话时 `resetExtensionUI()` 会
299
375
  **无条件**把内置 footer / header 装回去并清空所有 `setStatus`,而扩展侧没有比 `session_start`
300
376
  更早的钩子 —— 所以保证只能挪到「出帧那一刻」。`PI_STATUSLINE_FREEZE=off` 关掉冻结。
377
+ (启动窗口那个留白补丁**也可以**这样扩到 header 上,`startup-logo/` 目前没做:启动那一段顶部
378
+ 仍会先闪一下内置 header 再换成 logo。)
301
379
 
302
380
  ### pi 平台的坑
303
381
 
@@ -323,7 +401,10 @@ agent,加 `workflowScript` 脚本化编排。工具名(`subagent` / `subagen
323
401
  一条 assistant 消息来断言)。反过来,patch `node_modules` 里那份**毫无效果且没有任何报错**(实测)。
324
402
  同一个原因的另一面:`@earendil-works/pi-coding-agent` 在 bundle 形态下被 alias 到 `dist/index.js`
325
403
  (未打包的那套模块图),所以包根的状态型 API(`keyHint` / `keyText`)拿到的是另一个副本 ——
326
- 即上面那条“绝不能 import”的由来。
404
+ 即上面那条“绝不能 import”的由来。**这一条对「类」不成立(实测)**:`statusline/footer-suppress.ts`
405
+ 从包根 import `FooterComponent` 打原型,A/B 捕获证明补丁落在 pi 自己 new 的那个实例上(见「跨扩展」一节),
406
+ 而 `dist/bundle/index.js` 确实是从 `./chunks/chunk-*.js` re-export 的。`keyHint` / `keyText` 的现象仍是事实,
407
+ 但按「别在模块顶层读 pi 的状态」理解即可:打类方法没事,**读状态**别放在模块顶层。
327
408
  - **`keyHint` / `keyText` 绝不能 import**(`bash-command-collapse.ts` 与 `read-path-collapse.ts`
328
409
  都踩过:扩展拿到的是 npm/dist 副本,前者抛 `Theme not initialized`、后者返回空串)。要从
329
410
  `~/.pi/agent/keybindings.json` 读键名。`startup-logo` 的提示行是唯一从包根 import 的,它整行包了 try/catch。
@@ -411,8 +492,10 @@ agent,加 `workflowScript` 脚本化编排。工具名(`subagent` / `subagen
411
492
  没有安装脚本,所以同步是**双向手动**的:
412
493
 
413
494
  - **改了本机全局配置 / 扩展** → 手动把 `~/.pi/agent/` 下的 `AGENTS.md` / `settings.json` / `models.json` /
414
- `pi-statusline.json` / `web-search.json` / `extensions/*` / `themes/*.json` 拷回本目录,
495
+ `mcp.json` / `pi-statusline.json` / `web-search.json` / `extensions/*` / `themes/*.json` 拷回本目录,
415
496
  保持模板与实际环境一致 —— 只有上文列的那三处是刻意差异,其余应当逐字节相同。
497
+ (`mcp.json` 里是**本机 MCP 可执行文件的绝对路径**,与 `models.json` 的 `baseUrl` 同类:入库作模板,
498
+ 换机器照着改 `command`。)
416
499
  - **换机器 / 重装** → 按前面的 `cp` 装回去,再 `pi install npm:pi-web-access` 与 `pi install npm:pi-subagents`。
417
500
  - **改完扩展的最低验证**是真起一次 pi(见上文「pi 平台的坑」——`node --test` 不校验语法)。
418
501
  - **面向本机 pi 的写法约定**:纯逻辑模块刻意**不 import pi / pi-tui**(鸭子类型 + 结构化最小接口),
@@ -420,6 +503,8 @@ agent,加 `workflowScript` 脚本化编排。工具名(`subagent` / `subagen
420
503
  `working-indicator/`、`startup-logo/`、`thinking-collapse/`、`fenceless-code-block/`、`prompt-editor/`
421
504
  都按这个约定拆出了可单测的伴生模块
422
505
  (`thinking-collapse/window.ts` 只注入一个 `widthOf`,`node --test clients/pi/extensions/thinking-collapse/window.test.ts`)。
506
+ `mcp/` 更进一步:`protocol.ts` / `config.ts` / `client.ts` / `tools.ts` / `headers-command.ts` **全部不 import pi**,
507
+ 只有 `index.ts` 接线 —— 所以整条 MCP 链路(含真实 spawn 子进程)都能 `node --test` 覆盖。
423
508
  - **`AGENTS.md` 自设 8000 字符预算**(当前 **7996 字符** ≈ 1999 tokens,落在盘上是 8028 字节,余量仅 4 字符):
424
509
  pi 本身没有上限 —— 0.85.1 的 `system-prompt.js` 是原样拼接 context files、无截断,实测把标记放在
425
510
  9500 字符处仍被模型逐字读回;7400 那条是自设的每请求固定开销预算,已为 skill 优先级与 shell 卫生
@@ -51,7 +51,7 @@ Expected visible results of a successful load:
51
51
  - The header is replaced by a logo with the version and the shortened working directory (`startup-logo`).
52
52
  - The footer is a single statusline line (`statusline`), plus a second line with the working directory (`cwd-statusline`).
53
53
  - The editor shows a `❯ ` prompt (`prompt-editor`).
54
- - `/theme`, `/tasks`, `/recap`, `/rewind`, `/init`, `/clear`, `/exit`, `/ask`, `/read-collapse` and the `/bash-*` family all exist. Type `/` and scroll the command list.
54
+ - `/theme`, `/tasks`, `/recap`, `/rewind`, `/init`, `/clear`, `/exit`, `/ask`, `/read-collapse`, `/mcp` and the `/bash-*` family all exist. Type `/` and scroll the command list.
55
55
 
56
56
  If something is missing, start pi and search the screen for `Failed to load extension` — a parse error in one file does not stop the others.
57
57
 
@@ -80,6 +80,8 @@ mkdir -p ~/.pi/agent/themes
80
80
  cp "$PKG/themes/"*.json ~/.pi/agent/themes/ # optional: themes are already loaded from the package
81
81
  ```
82
82
 
83
+ `config/mcp.json` is **not** shipped, for the same reason as `config/models.json`: its entries are absolute paths of local MCP server executables. To use MCP servers, create `~/.pi/agent/mcp.json` (global) or a project `.mcp.json` yourself — the `mcp/` extension reads both, and registers no tools until one exists. See [configuration.md](configuration.md#mcpjson).
84
+
83
85
  `config/settings.json` also overwrites your settings wholesale — read [configuration.md](configuration.md) first, because it pins `pnpm` in `npmCommand` and disables pi's built-in double-Escape action.
84
86
 
85
87
  Then restart pi. Extensions are hot-reloadable in their auto-discovery directories (`/reload`), but `settings.json`, `AGENTS.md` and themes are read once at startup.
package/docs/themes.md CHANGED
@@ -35,11 +35,12 @@ A port of Catppuccin Mocha from [bacnh85/pi-extensions](https://github.com/bacnh
35
35
 
36
36
  A port of the official `ayu-dark` palette from [iodic/pi-ayu-themes](https://github.com/iodic/pi-ayu-themes), reformatted to the same four-section shape as `catppuccin.json` and with all 55 colors going through `vars`. It is the only theme here that defines `bashOutput`.
37
37
 
38
- Three deliberate deviations from upstream:
38
+ Four deliberate deviations from upstream:
39
39
 
40
40
  1. **Strings vs. added diff lines.** Upstream points `syntaxString` (strings in code) and `toolDiffAdded` (the foreground of added diff lines, including line numbers and `+`) at the same green `#AAD94C`. Here `syntaxString` points at a darker `stringGreen` (`#67a567`) so code strings and diff additions do not glow identically; diff additions keep the upstream green. Side effect: the new green is close in luminance to `muted`, so strings and comments are told apart mostly by hue.
41
41
  2. **Thinking borders.** Upstream paints `thinkingXhigh` red (`#D95757`). Here `thinkingXhigh` and `thinkingMax` are both a neutral grey (`#626262`), because the editor border takes the color of the current level and this setup defaults to `xhigh` — a red border reads as an error. The top two levels are now distinguished from each other only by luminance.
42
- 3. **Pending and finished tool cards are inverted.** Upstream has `toolPendingBg: #10151F` (darker) and `toolSuccessBg: #171F24` (lighter); this file swaps them, so a tool call that is still running gets the lighter background and a finished one the darker. Nothing in the file explains the choice — it is a value inversion, not a renamed variable.
42
+ 3. **Pending and finished tool cards are exchanged, and the pending value was re-tuned.** Upstream has `toolPendingBg: #10151F` (darker) and `toolSuccessBg: #171F24` (lighter); this file uses `#1b1c1d` for pending and `#10151F` for success, so a tool call that is still running gets the lighter background and a finished one the darker. After the exchange the pending value moved three more times (`#171F24` → `#191919` → `#1d1c1d` → `#1b1c1d`) and now equals `userMessageBg`, so a running tool card shares the background of a user message. Nothing in the file explains the choice — it is a value inversion, not a renamed variable.
43
+ 4. **User message text has a fixed color.** `userMessageText` points at `textColor` (`#dbdbdd`, a var added for it) rather than `fg`, the terminal's default foreground, so user messages read the same on any terminal.
43
44
 
44
45
  ## Anatomy of a theme file
45
46