@counterposition/skill-pi 0.84.2 → 0.99.1

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.
@@ -81,28 +81,38 @@ Useful session events:
81
81
  - `resources_discover`
82
82
  - `project_trust` — user/global and CLI extensions only, fired before project resources load; return `{ trusted: "yes" | "no" | "undecided" }` (optionally `remember: true` to persist). The first yes/no decision wins and suppresses the built-in trust prompt.
83
83
 
84
- `session_before_compact` and `session_compact` carry `reason` and `willRetry` (Pi 0.79.10) so handlers can distinguish manual `/compact`, threshold auto-compaction, and overflow-retry compaction.
84
+ `session_before_compact` and `session_compact` carry `reason` and `willRetry` (Pi 0.79.10) so handlers can distinguish manual `/compact`, threshold auto-compaction, and overflow-retry compaction. `session_compact_failed` (Pi 0.84.3) reports failed or aborted compactions with `reason`, `aborted`, `willRetry`, `fromExtension`, and `errorMessage`.
85
85
 
86
86
  `session_start` carries `event.reason` — `"startup" | "reload" | "new" | "resume" | "fork"` — and `event.previousSessionFile` for `"new"`/`"resume"`/`"fork"`. The old post-transition `session_switch` / `session_fork` events were removed; route all of those flows through `session_start`.
87
87
 
88
88
  Useful agent events:
89
89
 
90
90
  - `input` — carries `event.streamingBehavior` (`"steer" | "followUp" | undefined`) so handlers can distinguish idle prompts, mid-stream steers, and queued follow-ups; return `{ action: "transform" | "handled" | "continue" }`
91
- - `before_agent_start` — receives `event.systemPromptOptions` (a `BuildSystemPromptOptions`) so handlers can inspect the structured inputs feeding the system prompt
91
+ - `before_agent_start` — `event.systemPrompt` is read-only; mutate `event.systemPromptOptions` (sections, selected tools, guidelines) so Pi appends a transcript delta that survives resume/branching and keeps the cached prefix (Pi 0.86.0). Returning `{ systemPrompt }` replaces the whole prompt for that run and is sent as the provider's leading system prompt
92
92
  - `agent_start`
93
93
  - `agent_end` — one low-level run; Pi may still auto-retry, auto-compact and retry, or continue with queued follow-ups
94
- - `agent_settled` (Pi 0.80.4) — fired when no retry/compaction/follow-up remains; use for status integrations that need to know Pi will not continue automatically (`ctx.isIdle()` is true here unless another extension started a new run)
94
+ - `agent_before_settle` (Pi 0.87.0) — the final actionable boundary before settlement (see below)
95
+ - `agent_settled` (Pi 0.80.4) — notification-only; fired when no retry/compaction/follow-up remains, for status integrations that need to know Pi will not continue automatically. Since Pi 0.87.0, runs requested from `agent_settled` handlers are deferred until every settled handler finishes (`ctx.isIdle()` stays true, no reentrant `agent_start`)
95
96
  - `turn_start`
96
- - `turn_end`
97
+ - `turn_end` — actionable since Pi 0.87.0; carries `turnIndex`, `message`, `toolResults`, and their session entry IDs
97
98
  - `message_start`
98
99
  - `message_update`
99
100
  - `message_end` — return a replacement message to override usage/cost or rewrite the finalized assistant message
100
- - `context`
101
+ - `context` — transforms conversation messages; since Pi 0.87.0 handlers no longer see prompt/tool system messages, and Pi restores that state afterward (filtering or slicing messages is safe)
102
+ - `context_with_system` (Pi 0.87.0) — runs after `context` on the full transcript including system messages and is sent verbatim; use only when a request-local transform must own the whole transcript, and keep a system message at index 0
101
103
  - `before_provider_headers` (Pi 0.80.4) — mutate `event.headers` in place after outgoing HTTP headers are assembled: set a key to a string to add/override, `null` to delete. Runs once per provider request; retries reuse the same headers
102
104
  - `before_provider_request`
103
105
  - `after_provider_response` — inspect the provider HTTP status and headers before stream consumption
106
+ - `provider_stream_event` (Pi 0.99.0) — notification-only view of each parsed provider stream event before Pi normalizes it (`provider`, `api`, `model`, `data`). Treat `data` as read-only; handlers are awaited in stream order, so keep them fast. Not persisted
104
107
  - `model_select`
105
108
  - `thinking_level_select` — observe interactive thinking-level changes
109
+ - `ui_prompt_start` / `ui_prompt_end` (Pi 0.84.4) — distinguish active agent work from time spent waiting on blocking `ctx.ui` prompts (`kind`: `select`/`confirm`/`input`/`editor`/`custom`)
110
+ - `cache_warming_decision` (Pi 0.86.0) — override an idle prompt-cache refresh with `{ action: "warm" }` or `{ action: "stop" }`; the last handler returning an action wins
111
+ - `user_bash` — user `!`/`!!` commands. Fails closed since Pi 0.86.0: return `undefined` to pass to the next handler (then local execution), `{ operations }` or `{ result }` to handle it; a throwing handler or invalid result aborts the command
112
+
113
+ `turn_end` and `agent_before_settle` are actionable boundaries (Pi 0.87.0). Handlers receive the proposed `event.entries`, `event.continue`, a `context` preview, and the run `outcome`, and may return `{ entries: [...event.entries, draft], continue: true }` to persist `custom`, `custom_message`, `context_edit`, or `compaction` entries in order and ensure exactly one next provider request without disturbing steering/follow-up queues. Guard the condition — an unconditional `continue: true` loops.
114
+
115
+ `pi.on()` returns an unsubscribe function (Pi 0.86.0); handlers added or removed during a dispatch apply from the next dispatch.
106
116
 
107
117
  Useful tool events:
108
118
 
@@ -112,6 +122,10 @@ Useful tool events:
112
122
  - `tool_result`
113
123
  - `tool_execution_end`
114
124
 
125
+ Calls another tool makes through `ctx.executeTool()` (codemode scripts, orchestrating tools) fire the same tool events, so permission gates see them; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions).
126
+
127
+ Since Pi 0.86.0, tool-call `arguments` and result `details` must be JSON-compatible values (no `Date`, `Map`, class instances, `undefined` array slots); use `details: undefined` when there is nothing structured to keep. Tools registered without a parameter schema are rejected at registration.
128
+
115
129
  Tool results may include `terminate: true` to end the current tool batch without an automatic follow-up LLM turn — useful for tools that produce a structured final answer (see `examples/extensions/structured-output.ts` in the Pi repo). Since Pi 0.84.1, blocked `tool_call` results may also return `terminate: true`; the agent stops early only when every finalized result in the batch is terminating. Tools that make nested LLM calls can return their combined `Usage` as `usage` on the tool result — Pi persists it and includes it in footer, `/session`, and RPC session totals (Pi 0.81.0), and `tool_result` handlers may inspect or replace that value.
116
130
 
117
131
  `session_shutdown` events carry `event.reason` (`"quit" | "reload" | "new" | "resume" | "fork"`) and, where applicable, `event.targetSessionFile` so cleanup logic can distinguish teardown paths.
@@ -125,6 +139,8 @@ Tool results may include `terminate: true` to end the current tool batch without
125
139
  - `ctx.cwd`
126
140
  - `ctx.sessionManager` (read-only; `buildContextEntries()` returns active-branch entries with compaction applied, Pi 0.80.4)
127
141
  - `ctx.modelRegistry` / `ctx.model` / `ctx.thinkingLevel` / `ctx.scopedModels` (Pi 0.83.0) — `ctx.modelRegistry` is the synchronous extension-facing facade; its `refresh()` became `Promise<void>` in Pi 0.80.8 (await it before synchronous registry reads). Since Pi 0.84.x it also exposes `getProvider(id)` (effective pi-ai provider) and `getProviderAuth(id)` (API key, headers, base URL, provider-scoped env — no loaded model required). `ctx.scopedModels` is the read-only session model scope resolved from `--models`/`enabledModels` (`{ model, thinkingLevel? }[]`, empty when unscoped); prefer it over enumerating `getAvailable()` for model pickers
142
+ - `ctx.modelRegistry.stream()` / `streamSimple()` / `complete()` (Pi 0.86.0) — nested model calls through configured providers with request-time auth; prefer `streamSimple()` for provider-neutral calls, pass `ctx.signal`, and report the combined `usage` on the tool result
143
+ - `ctx.modelRegistry.findOfType(type, provider, id)`, `getModelsOfType()`, `getAvailableOfType()`, and `classify(model, { state, questions }, { signal })` (Pi 0.99.0) — classifier and image models live beside chat models; `classify()` never rejects, so check `result.stopReason` (see `references/providers.md`)
128
144
  - `ctx.hasUI` — `true` in TUI and RPC modes
129
145
  - `ctx.signal` — the active agent abort signal (or `undefined` when idle); pass it to `fetch`/model calls for abort-aware nested work
130
146
  - `ctx.isIdle()` / `ctx.hasPendingMessages()`
@@ -134,6 +150,8 @@ Tool results may include `terminate: true` to end the current tool batch without
134
150
  - `ctx.getSystemPrompt()`
135
151
  - `ctx.isProjectTrusted()` — the effective project trust decision, including temporary `--approve`/`--no-approve` decisions
136
152
 
153
+ Tool `execute()` receives `ExtensionToolContext`, which adds `ctx.tools` (the tools this call can reach) and `ctx.executeTool(name, args, { signal?, onUpdate? })` (Pi 0.99.0). `executeTool()` runs validation and the `tool_call`/`tool_result` hooks like a model call and never rejects: unknown tools, validation errors, blocks, and thrown errors come back as `{ isError: true }`, including after an abort. The signal defaults to the calling tool's. Nested usage is added to the calling tool's result, so report only the tool's own `usage`.
154
+
137
155
  Use `CONFIG_DIR_NAME` (exported from `@earendil-works/pi-coding-agent`) instead of hardcoding `.pi` when building project config paths, e.g. `join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json")`.
138
156
 
139
157
  Command handlers receive `ExtensionCommandContext`, which extends the above with session-control methods that would deadlock from event handlers: `ctx.getSystemPromptOptions()` (inspect the base system-prompt inputs), `ctx.waitForIdle()`, `ctx.newSession()`, `ctx.fork()`, `ctx.switchSession()`, `ctx.navigateTree()`, and `ctx.reload()`.
@@ -169,20 +187,35 @@ Important rules:
169
187
 
170
188
  1. Truncate large output.
171
189
  2. Respect `signal.aborted`.
172
- 3. Throw on failure instead of returning fake success.
173
- 4. Use the file-mutation queue for write/edit style tools.
190
+ 3. Throw on failure instead of returning fake success, or return `isError: true` when the failure still carries data. A returned object without `isError` counts as success.
191
+ 4. Use the file-mutation queue (`withFileMutationQueue()`) for write/edit style tools.
174
192
  5. Put reconstructable state in `details`; it persists in session history.
175
193
 
194
+ ## Tool Exposure & Structured Results
195
+
196
+ Pi 0.99.0 added fields for tools that other tools, codemode scripts, and permission extensions consume ([docs](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/extensions.md#tool-exposure), [types](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/src/core/extensions/types.ts)):
197
+
198
+ - `outputSchema` (TypeBox) plus `structuredContent` on every result: the model still reads `content`; codemode scripts receive `structuredContent` instead of text. Without `outputSchema`, scripts get the text. `structuredContent` must be JSON-compatible. A `tool_result` handler that replaces `content` must also return `structuredContent`, or it is dropped.
199
+ - `isError: true` on a returned result: the model sees an error, while `details` and `structuredContent` are kept for the UI and scripts.
200
+ - `exposure`: `direct` (default), `model-only`, `codemode`, `deferred`, or `hidden`. `direct` and `model-only` tools activate on registration; the others do not (`defaultActive: false` also keeps a `direct` tool inactive until `--tools`/`defaultTools`/`setActiveTools()` names it). Tools cannot be unregistered: re-register with `exposure: "hidden"` to withdraw one. The table of what each value declares and allows is in `references/codemode.md`.
201
+ - `namespace: { name, description? }` groups related tools under one heading in the `codemode` description, as MCP servers do.
202
+ - `annotations`: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, with MCP meanings. Missing hints mean "not read-only, may be destructive, open world". They are unverified; mark writes neither read-only nor casually idempotent.
203
+ - `prepareLoadout(loadout)`: for tools that orchestrate others. Runs whenever the active tools change with `declared`, `callable`, and `registered` tools, and returns replacement `descriptions` and `hiddenDeclarations` (active tools left out of requests but still callable). The built-in `codemode` and `tool_search` use only this, `exposure`, and `ctx.executeTool()`.
204
+
205
+ `pi.getAllTools()` reports each tool's `exposure`, `namespace`, and `annotations`. `pi.setActiveTools()` ignores unknown and `hidden` names. Compilable examples: `examples/structured-tool.ts` and `examples/nested-tools.ts`.
206
+
176
207
  ## Dynamic Tool Loading
177
208
 
209
+ For tools the model should find on demand, prefer `exposure: "deferred"` with the built-in `tool_search` (Pi 0.99.0), or `exposure: "codemode"` for tools scripts call. `tool_search` is inactive by default: enable it with `"defaultTools": ["+tool_search"]` or `--tools`, or let a connected MCP server with `deferred` exposure turn it on. Without it, the model can reach `deferred` tools only through codemode. The manual loader pattern below still works.
210
+
178
211
  Pi 0.80.7 lets an extension register many tools while keeping only a small initial set active, then add more during execution — cache-friendly on models with native deferred loading. Lifecycle:
179
212
 
180
213
  1. Register every tool with `pi.registerTool()` (all appear in `pi.getAllTools()`).
181
214
  2. Keep a loader tool (e.g. `search_tools`) active; leave searchable tools inactive — e.g. on `session_start`, `pi.setActiveTools()` to the filtered set.
182
215
  3. During loader execution, call `pi.setActiveTools([...pi.getActiveTools(), ...matches])`. The change must be purely additive; unknown names are ignored.
183
- 4. Pi records the added tools on that tool result and exposes their definitions before the next model response.
216
+ 4. Pi records the added tools on that tool result and exposes their definitions before the next model response. Since Pi 0.86.0 prompt and tool changes are transcript-backed system messages, so they survive resume and branch navigation.
184
217
 
185
- Native deferred loading preserves the cached prompt prefix on Anthropic Sonnet/Opus/Fable ≥ 4.5 (not Haiku) and OpenAI `gpt-5.4`+; Kimi K3 works via `compat.deferredToolsMode: "kimi"` (Pi 0.80.9). For verified custom models/proxies, enable `compat.supportsToolReferences: true` (`anthropic-messages`) or `compat.supportsToolSearch: true` (`openai-responses`/`openai-codex-responses`). All other models fall back to sending the full active tool list on the next request — activation still works, but may invalidate the provider's cached prefix. Non-additive changes (removals/replacements) always use the fallback.
218
+ Native deferred loading preserves the cached prompt prefix on Anthropic Sonnet/Opus/Fable ≥ 4.5 (not Haiku), OpenAI `gpt-5.4`+, and Fireworks Messages models (Pi 0.86.0; name the loader `ToolSearch` or `tool_search`); Kimi K3 works via `compat.deferredToolsMode: "kimi"` (Pi 0.80.9). For verified custom models/proxies, enable `compat.supportsToolReferences: true` (`anthropic-messages`) or `compat.supportsToolSearch: true` (`openai-responses`/`openai-codex-responses`). All other models fall back to sending the full active tool list on the next request — activation still works, but may invalidate the provider's cached prefix. Non-additive changes (removals/replacements) always use the fallback.
186
219
 
187
220
  Cache tips: keep the loader active for the whole session; add rather than replace. Activating a tool that has `promptSnippet`/`promptGuidelines` rebuilds the system prompt and can invalidate the prefix even with native support — lazily loaded tools should rely on their `description` alone.
188
221
 
@@ -194,7 +227,7 @@ Cache tips: keep the loader active for the whole session; add rather than replac
194
227
  - `{ type: "grammar", variants: { openai_lark?: string, openai_regex?: string } }` — OpenAI Lark/regex grammar encodings
195
228
  - Set `false` to explicitly disable for that tool
196
229
 
197
- Model capability metadata gates what is actually sent: `compat.supportsStrictTools` / Anthropic built-ins enable strict JSON-schema tools, and `compat.supportsOpenAIGrammarTools` marks endpoints that accept Lark/regex grammar tools (enabled in generated metadata for GPT-5+ across OpenAI, Codex, Azure, GitHub Copilot, opencode, Cloudflare AI Gateway). Unsupported combinations fall back to normal function tools rather than failing. Independently, Pi 0.84.2 ships experimental strict JSON-schema sampling for its default `read`/`bash`/`edit`/`write` tools under `PI_EXPERIMENTAL=1`.
230
+ Model capability metadata gates what is actually sent: `compat.supportsStrictTools` / Anthropic built-ins enable strict JSON-schema tools, and `compat.supportsOpenAIGrammarTools` marks endpoints that accept Lark/regex grammar tools (enabled in generated metadata for GPT-5+ across OpenAI, Codex, Azure, GitHub Copilot, opencode, Cloudflare AI Gateway). Unsupported combinations fall back to normal function tools rather than failing. Since Pi 0.86.0, strict-prefer JSON-schema sampling is on by default for the built-in `read`, `bash`, `powershell`, `edit`, and `write` tools (no `PI_EXPERIMENTAL` needed); re-register a definition with `constrainedSampling: false` to opt out.
198
231
 
199
232
  ## Display Transformers
200
233
 
@@ -299,6 +332,14 @@ pi.on("session_start", async (_event, ctx) => {
299
332
  });
300
333
  ```
301
334
 
335
+ ## MCP Servers
336
+
337
+ `pi.registerMcpServer(name, config)` adds a server for the current session with the `mcp.json` entry shape; `pi.unregisterMcpServer(name)` closes it. Registrations are not saved, and an `mcp.json` server with the same name wins. Extensions that implement their own MCP client read `pi.getMcpServers()` on `session_start` and handle `mcp_servers_change`. See `references/mcp.md`.
338
+
339
+ ## Virtual Models
340
+
341
+ `pi.registerVirtualModel({ provider, id, name, thinkingLevels?, route(request, ctx) })` (experimental, Pi 0.99.0) adds a selectable model that picks a physical model and thinking level for every request. See `references/providers.md#virtual-models`.
342
+
302
343
  ## Provider Integration
303
344
 
304
345
  Extensions can register or override providers:
@@ -329,6 +370,8 @@ pi.unregisterProvider("my-provider");
329
370
 
330
371
  Dynamic providers can implement `refreshModels(context)` (Pi 0.80.8) for model discovery: Pi calls it during catalog refresh (`/model`, `pi update --models`) and publishes the returned list; its models replace extension-provided `models`. Since Pi 0.84.0 the context is read-snapshot + generation-checked publication: read `context.stored` (the persisted provider snapshot) instead of `context.store`, and persist through `context.publish({ update?, persist? })` — `persist` omitted leaves storage unchanged, a `ModelsStoreEntry` writes it, `persist: null` deletes it. Config-form callbacks that only return models remain unchanged.
331
372
 
373
+ A config-form `models` list replaces the provider's models across chat, image, and classifier operations (Pi 0.99.0). Omitted `type` means `"chat"`; image and classifier entries need an explicit `type` plus implementations keyed by `api` through `images`/`classifiers` (see `references/providers.md`).
374
+
332
375
  Since Pi 0.81.0, extensions can also register a complete pi-ai `Provider` via `pi.registerProvider(createProvider({...}))` — native auth (`login`/`resolve`), `getModels`, `refreshModels`, `filterModels`, and custom streaming. The registered provider becomes the composition base; `models.json` overrides still apply above it. Prefer this when you need real authentication or stream behavior rather than just a model list. The legacy config form (`name`, `baseUrl`, `apiKey`, `models`, `oauth`, `streamSimple`) remains supported. OAuth `refreshToken(credentials, signal)` callbacks must accept and honor the abort signal since Pi 0.84.0.
333
376
 
334
377
  If you need auth for a specific model request, use:
@@ -339,12 +382,15 @@ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
339
382
 
340
383
  (Still supported for extensions in Pi 0.80.8+; SDK code should use `ModelRuntime.getAuth()` — see `references/sdk.md`. `ProviderHeaders` values are `string | null` since Pi 0.84.0, where `null` is a delete marker: handle `null` when inspecting, pass through unchanged when forwarding to pi-ai streams.)
341
384
 
342
- Pass `shouldStopAfterTurn` via the SDK to exit the agent loop gracefully after a completed turn. See `references/sdk.md` and `references/providers.md` for the full provider/model schema.
385
+ Custom `streamSimple` implementations receive a normalized `TranscriptContext` since Pi 0.86.0 — see `references/providers.md`. See `references/sdk.md` and `references/providers.md` for the full provider/model schema.
343
386
 
344
387
  ## Handy Patterns
345
388
 
346
- - Permission gates via `tool_call`
347
- - Prompt injection via `before_agent_start`
389
+ - Permission gates via `tool_call` (they also see nested calls from codemode; see `references/codemode.md`)
390
+ - Data tools for codemode via `outputSchema` + `structuredContent`
391
+ - Orchestrating tools via `ctx.executeTool()` with `exposure: "model-only"`
392
+ - Prompt injection via `before_agent_start` (`systemPromptOptions.sections`)
393
+ - Model-context pruning via `context`, or durable omission via `context_edit` entries from `turn_end`/`agent_before_settle`
348
394
  - Branch or compaction customization via `session_before_tree` / `session_before_compact`
349
395
  - Provider payload inspection via `before_provider_request`
350
396
  - Stateful tools via `details` plus `session_start` reconstruction
@@ -0,0 +1,122 @@
1
+ # MCP Servers
2
+
3
+ Pi 0.99 connects to MCP servers through a built-in extension. There is no package to install. Checked against Pi v0.99.1: [mcp.md](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/mcp.md), [cli.md#mcp-commands](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/cli.md#mcp-commands), [extensions.md#mcp-servers](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/extensions.md#mcp-servers).
4
+
5
+ ## Configure
6
+
7
+ | File | Scope |
8
+ |------|-------|
9
+ | `~/.pi/agent/mcp.json` | Global. Put personal servers and servers with credentials here |
10
+ | `.pi/mcp.json` | Project. Read only after the project is trusted, because stdio servers run commands |
11
+
12
+ A project entry replaces a global entry with the same name. The format is the usual `mcpServers` object, so Claude Desktop, Claude Code, and Cursor entries copy over unchanged:
13
+
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "filesystem": {
18
+ "command": "npx",
19
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
20
+ },
21
+ "docs": {
22
+ "url": "https://example.com/mcp",
23
+ "headers": { "Authorization": "Bearer ${DOCS_TOKEN}" },
24
+ "exposure": "direct"
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ - stdio: `command` (one executable, not a shell string), `args`, `env`, `cwd`. Relative `cwd` resolves against the session directory; a leading `~/` works in `command`, args, and `cwd`.
31
+ - HTTP: `url`, `headers`, `oauth`. Only streamable HTTP; `"type": "sse"` is rejected (try the server's `/mcp` endpoint instead of `/sse`).
32
+ - `type` is optional (`stdio`, `http`, or `streamable-http`); `command` implies stdio and `url` implies HTTP.
33
+ - `env` and `headers` values accept `${NAME}` and `!command`. A command must produce the whole value: `"Authorization": "!echo Bearer $(gh auth token)"`.
34
+ - `timeout` is seconds per request (default 60); progress notifications reset it. `enabled: false` keeps the entry without connecting.
35
+ - Server names use letters, digits, `_`, `-`. Tools are named `mcp__<server>__<tool>`.
36
+ - Invalid entries are skipped and reported; other servers still connect.
37
+
38
+ Converting other clients: VS Code uses `servers` + `inputs` (move under `mcpServers`, replace `${input:...}` with `${NAME}`); Codex uses TOML `[mcp_servers.<name>]` (same fields as JSON); opencode uses `"type": "local"` with an array `command` (split into `command`/`args`), `environment` for `env`, and `{env:NAME}` for `${NAME}`.
39
+
40
+ ## Set Up From a Shell
41
+
42
+ These commands work outside a session, so an agent can run them through `bash`:
43
+
44
+ ```bash
45
+ pi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .
46
+ pi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN --exposure direct
47
+ pi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp # -l writes .pi/mcp.json
48
+ pi mcp list # connects to every enabled server; exits 1 while anything is wrong
49
+ pi mcp login sentry # OAuth: opens the browser and waits for the user to approve
50
+ pi mcp remove docs # keeps stored OAuth credentials
51
+ pi mcp logout sentry
52
+ ```
53
+
54
+ `pi mcp add` does not connect; verify with `pi mcp list`. A running session picks up added or changed servers after `/reload` or a new session.
55
+
56
+ In a session, `/mcp` opens the server manager (state, tools, errors, sign in/out, reconnect, exposure, enable/disable). Changes are written back to the `mcp.json` that defines the server. Outside the TUI, `/mcp` prints status and `/mcp login|logout|reconnect <server>` run directly.
57
+
58
+ Pi connects on session start; the first prompt waits up to 10 seconds for startup connections. OAuth tokens live in `~/.pi/agent/mcp-auth.json`; server log notifications go to `~/.pi/agent/mcp.log`. OAuth is used only for HTTP servers whose config has no `Authorization` header. The optional `oauth: { clientId?, clientSecret?, callbackPort?, callbackUrl?, scope? }` sets a pre-registered client for servers without dynamic client registration, the loopback redirect (`callbackPort` alone gives `http://127.0.0.1:<port>/callback`; a `callbackUrl` without a port gets `callbackPort` or a free one), and `scope`, space-separated scopes to request when the server does not advertise the ones it needs.
59
+
60
+ ## Exposure
61
+
62
+ `exposure` decides how the model reaches a server's tools:
63
+
64
+ | Value | Declared to the model | Callable from codemode | Notes |
65
+ |-------|----------------------|------------------------|-------|
66
+ | `codemode` (default) | No | Yes, listed in the `codemode` description | Pi activates `codemode` when such a server connects |
67
+ | `codemode-deferred` | No | Yes, found with `searchTools()` / `ALL_TOOLS` | Description names only the server and tool count; for large, rarely used servers |
68
+ | `deferred` | After `tool_search` loads them | Yes | Pi activates `tool_search`; for large servers without codemode |
69
+ | `direct` | Yes, like built-in tools | Yes | |
70
+ | `hidden` | No | No | Registered but unreachable |
71
+
72
+ `toolExposure` overrides single tools by name or `*` pattern (exact name wins, then first matching pattern):
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "github": {
78
+ "url": "https://api.githubcopilot.com/mcp/",
79
+ "exposure": "deferred",
80
+ "toolExposure": { "search_code": "direct", "get_*": "codemode", "delete_*": "hidden" }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ Undeclared tools (`codemode`, `codemode-deferred`, `deferred`) are reachable through either tool: codemode scripts can call a `deferred` server's tools, and `tool_search` can load a `codemode` server's tools. Set top-level `"autoEnableCodemode": false` in `mcp.json` to stop Pi from activating `codemode`. Pi warns once when neither `codemode` nor `tool_search` is active, because the tools are then unreachable.
87
+
88
+ ## Results
89
+
90
+ - Text results over 20KB reach the model with the middle cut out; the full text goes to a temp file named in the result.
91
+ - Codemode scripts receive the whole `CallToolResult` (`content`, `structuredContent`, `isError`). A result with `isError` resolves in scripts and is reported to the model as an error for direct calls. `image(result.content[0])` forwards an image block.
92
+ - Servers with resources add `list_mcp_resources`, `list_mcp_resource_templates`, and `read_mcp_resource` (marked read-only).
93
+ - Tool calls are never retried (the server may have run them); resource reads retry once on 408/429/5xx.
94
+
95
+ ## Permissions
96
+
97
+ Every MCP call goes through Pi's tool pipeline, so `tool_call` and `tool_result` handlers (permission gates included) see MCP tools. Calls from codemode scripts are nested calls; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions). `pi.getAllTools()` reports the server's `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` annotations. They are unverified hints; see [Tool Exposure](extensions.md#tool-exposure--structured-results).
98
+
99
+ ## Servers From Extensions
100
+
101
+ ```typescript
102
+ pi.registerMcpServer("jira", { url: "https://mcp.example.com/jira", exposure: "codemode" });
103
+ pi.unregisterMcpServer("jira");
104
+ ```
105
+
106
+ - Same config shape as an `mcpServers` entry. Registrations are not saved: register on every load.
107
+ - Registered during load: connects on `session_start` with the `mcp.json` servers. Registered later: connects right away.
108
+ - An `mcp.json` server with the same name wins. Another extension's name, an invalid name, or an invalid config throws.
109
+ - `pi mcp` shell commands do not load extensions and only see `mcp.json` servers.
110
+ - Alternative MCP extensions read registrations with `pi.getMcpServers()` on `session_start` and handle `mcp_servers_change`.
111
+
112
+ ## Turning It Off or Replacing It
113
+
114
+ - `"extensions": ["-builtin:mcp"]` in settings, or `pi config` → Built-in, disables the built-in support. `pi mcp` shell commands still work.
115
+ - `--no-extensions` disables built-ins too; `pi -ne -e builtin:mcp` keeps only MCP.
116
+ - An extension that registers the `/mcp` command (for example `pi-mcp-adapter`) replaces the built-in support: `mcp.json` is then not read in sessions.
117
+
118
+ ## SDK
119
+
120
+ SDK sessions do not load built-in extensions. Add `createMcpExtension()`, plus `createCodemodeExtension()` for `codemode`/`codemode-deferred` servers and `createToolSearchExtension()` for `deferred` servers, then call `session.bindExtensions()` so `session_start` connects the servers. See `references/sdk.md`.
121
+
122
+ The standalone client is published as `@earendil-works/pi-mcp` (stdio and streamable HTTP transports, OAuth, an in-memory testing transport). It has no Pi session dependency.
@@ -94,16 +94,21 @@ If there is no `pi` manifest, Pi auto-discovers:
94
94
 
95
95
  ## Dependencies
96
96
 
97
+ Checked against Pi v0.99.1 [packages.md](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/packages.md#declare-dependencies).
98
+
97
99
  - Normal runtime dependencies belong in `dependencies`
98
- - Pi core libraries should be peer deps with `"*"` ranges:
100
+ - Pi supplies these to extensions; declare them as peer deps with `"*"` ranges and never bundle them:
99
101
  - `@earendil-works/pi-ai`
100
102
  - `@earendil-works/pi-agent-core`
101
103
  - `@earendil-works/pi-coding-agent`
102
104
  - `@earendil-works/pi-tui`
103
105
  - `typebox`
104
- - Pin dev dependencies to a current Pi line (e.g. `^0.84.2`) so typecheck/tests validate against what users run; keep peers `"*"` so installs stay compatible across Pi versions
105
- - Other Pi packages must be bundled explicitly
106
- - Git package installs run `npm install --omit=dev` — runtime needs go in `dependencies`, not `devDependencies`
106
+ - Do not list those host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping and create duplicate classes and registries; since Pi 0.99.0 Pi reports an extension warning for this manifest.
107
+ - Pi suppresses automatic peer installation for managed npm packages and for git packages installed with npm (`--omit=dev --legacy-peer-deps`), pnpm (`--prod`, no auto-installed peers), or Bun (`--omit=dev --omit=peer`). Local path packages are not installed or modified; their dependency tree is the author's responsibility.
108
+ - Git package installs omit dev dependencies — runtime needs go in `dependencies`, not `devDependencies`
109
+ - Installed packages load with separate module roots. Do not rely on two packages sharing one dependency instance, or on one package resolving another package's undeclared dependency.
110
+ - Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths.
111
+ - Pin dev dependencies to the Pi line you test against (e.g. `^0.99.1`) so typecheck/tests validate against what users run; keep peers `"*"` so installs stay compatible across Pi versions. `"*"` does not prove compatibility: document the minimum supported Pi version and feature-check newer APIs (for example `ctx.executeTool`, `pi.registerMcpServer`) if you support older hosts.
107
112
 
108
113
  ## Package Filtering
109
114
 
@@ -134,4 +139,5 @@ Rules:
134
139
 
135
140
  - Project settings override global settings for the same package identity — unless the project entry has `autoload: false`, in which case it is applied as a delta over the global entry (Pi 0.80.4)
136
141
  - Identity is package name for npm, repo URL for git, and resolved absolute path for local sources
137
- - `pi config` can enable or disable package resources after installation; since Pi 0.80.4 it manages global vs project-local scopes (Tab to switch, `pi config -l` to start in project mode with inherited global resources dimmed)
142
+ - `pi config` can enable or disable package resources after installation; since Pi 0.80.4 it manages global vs project-local scopes (Tab to switch, `pi config -l` to start in project mode with inherited global resources dimmed). Since Pi 0.99.0 its Built-in section also disables the built-in `mcp`, `llama.cpp`, `codemode`, and `tool-search` extensions, stored as `-builtin:<name>` in `extensions`
143
+ - The `codemode`, `tool-search`, and `mcp` built-ins are replaceable: an extension that registers one of their tools, commands, or flags (for example `/mcp`, `codemode`, or `tool_search`) replaces that built-in, and Pi warns
@@ -19,6 +19,11 @@ Practical recipes for extending Pi. Each pattern is a self-contained example.
19
19
  13. [File Watcher / Trigger](#file-watcher--trigger)
20
20
  14. [Input Transform](#input-transform)
21
21
  15. [Tool Override](#tool-override)
22
+ 16. [Structured Tool](#structured-tool)
23
+ 17. [Nested Tool Calls](#nested-tool-calls)
24
+ 18. [Classifier Gate](#classifier-gate)
25
+
26
+ Patterns 16–18 are full files under `examples/` that type-check against Pi 0.99.1 in this skill's package; read them rather than retyping.
22
27
 
23
28
  ---
24
29
 
@@ -43,6 +48,8 @@ export default function (pi: ExtensionAPI) {
43
48
  }
44
49
  ```
45
50
 
51
+ The handler also runs for `bash` calls made by codemode scripts; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions).
52
+
46
53
  ## Protected Paths
47
54
 
48
55
  Block writes to sensitive files:
@@ -213,7 +220,7 @@ export default function (pi: ExtensionAPI) {
213
220
 
214
221
  ## Claude Rules Integration
215
222
 
216
- Scan `.claude/rules/` and inject into system prompt:
223
+ Scan `.claude/rules/` and add a system-prompt section. `event.systemPrompt` is read-only; mutating `event.systemPromptOptions` lets Pi append a transcript delta that keeps the cached prefix (returning `{ systemPrompt }` instead replaces the whole prompt for that run):
217
224
 
218
225
  ```typescript
219
226
  export default function (pi: ExtensionAPI) {
@@ -224,7 +231,8 @@ export default function (pi: ExtensionAPI) {
224
231
  .filter(f => f.toString().endsWith(".md"));
225
232
  if (files.length === 0) return;
226
233
  const listing = files.map(f => `- .claude/rules/${f}`).join("\n");
227
- event.systemPrompt += `\n\nProject rules (${files.length} files):\n${listing}\nUse the read tool to load relevant rules.`;
234
+ event.systemPromptOptions.sections.project_rules =
235
+ `Project rules (${files.length} files):\n${listing}\nUse the read tool to load relevant rules.`;
228
236
  });
229
237
  }
230
238
  ```
@@ -312,3 +320,30 @@ pi.on("tool_call", async (event, ctx) => {
312
320
  }
313
321
  });
314
322
  ```
323
+
324
+ ## Structured Tool
325
+
326
+ `examples/structured-tool.ts` registers a read-only data tool:
327
+
328
+ - `outputSchema` plus `structuredContent` on every result, so codemode scripts get `{ files, missing }` as an object while the model reads a text summary.
329
+ - `annotations: { readOnlyHint: true, openWorldHint: false }` for permission extensions, and a `namespace` for grouping in the `codemode` description.
330
+ - `isError: true` when no file was found: the model sees an error, scripts still get the data.
331
+ - Only `ENOENT` is treated as "missing"; other errors are rethrown.
332
+
333
+ ## Nested Tool Calls
334
+
335
+ `examples/nested-tools.ts` registers `read_many`, which reads several files in parallel through `ctx.executeTool("read", ...)`:
336
+
337
+ - `exposure: "model-only"`: the model can call it, other tools and codemode cannot, so it cannot recurse.
338
+ - Checks `ctx.tools` first: an inactive `direct` tool is not callable.
339
+ - Each nested call runs every `tool_call` gate (see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions)). `executeTool()` never rejects, and aborted calls come back as `isError`, so the tool calls `signal?.throwIfAborted()` after the batch to report the abort as an abort rather than as failed reads.
340
+ - Streams progress through `onUpdate`.
341
+
342
+ ## Classifier Gate
343
+
344
+ `examples/classifier-gate.ts` asks a classifier model whether a `bash` command is destructive before it runs:
345
+
346
+ - Finds Jev with `ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest")` and calls `ctx.modelRegistry.classify(..., { signal: ctx.signal })`.
347
+ - `classify()` never rejects: it checks `stopReason` (`"aborted"` blocks, `"error"` falls through to confirmation) and validates the `bool` answer's probability.
348
+ - Fails closed: a missing model, an error, an out-of-range answer, or a probability above the strict `MAX_ALLOWED_PROBABILITY` (0.2; tune it) asks the user, and blocks when `ctx.hasUI` is false.
349
+ - Uses `satisfies ClassifierContext["questions"]` so the question literal keeps its narrow types.