---
name: pi-extension
description: Create, update, and publish Pi extensions. Use when working on extensions in this repository.
---

# Pi Extension Development

Guide for creating and maintaining Pi extensions. Read the relevant reference files before implementing.

## Key Imports

```typescript
// Core types
import type { ExtensionAPI, ExtensionContext, ToolDefinition, ProviderDefinition } from "@mariozechner/pi-coding-agent";

// Schema (TypeBox)
import { Type } from "@mariozechner/pi-coding-agent";

// TUI components
import type { Component, Theme } from "@mariozechner/pi-tui";
import { Text, Box, Container, Markdown, SelectList } from "@mariozechner/pi-tui";

// Tool UI components (from @aliou/pi-utils-ui)
import { ToolCallHeader, ToolBody, ToolFooter } from "@aliou/pi-utils-ui";

// Rendering utilities
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";

// General utilities
import { truncateHead, highlightCode, getLanguageFromPath, DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
```

## Workflow

### Creating a New Extension

1. Read `references/structure.md` for the project layout and package.json template.
2. Create the entry point (`src/index.ts`) with a default export function.
3. Decide what the extension provides:
   - **Tools** (LLM-callable): Read `references/tools.md`.
   - **Commands** (user-invoked): Read `references/commands.md`.
   - **Providers** (LLM backends): Read `references/providers.md`.
   - **Hooks** (event handlers): Read `references/hooks.md`. Includes both `tool_call` blocking hooks and spawn hooks for transparent command rewriting via `createBashTool`.
4. Read `references/modes.md` for mode-awareness guidelines. Every extension must handle Interactive, RPC, and Print modes.
5. If the extension displays rich UI: Read `references/components.md` for TUI components and `references/messages.md` for message display patterns.
6. If the extension tracks state: Read `references/state.md`.
7. For less common APIs: Read `references/additional-apis.md`.
8. If the extension adds a tool that competes with a natural bash fallback (process managers, CI watchers, search tools): inject system prompt guidance. Read the **Guidance Injection Pattern** section in `references/additional-apis.md`.
9. Before publishing: Read `references/publish.md` and `references/documentation.md`.

### Modifying an Existing Extension

1. Read the extension's `index.ts` to understand its structure.
2. Read the relevant reference file for the area you are modifying.
3. Check `references/modes.md` if adding any UI interaction.
4. Run type checking after changes.

## Reference Files

| File | Content |
|---|---|
| `references/structure.md` | Project layout, package.json, tsconfig, biome.json, config.ts, entry point patterns (including acceptable exceptions), API key pattern, imports |
| `references/tools.md` | Tool registration, execute signature, parameters, streaming, rendering, naming, renderCall/renderResult UI guidelines |
| `references/hooks.md` | Events, blocking/cancelling, input transformation, system prompt modification, bash spawn hooks (command rewriting) |
| `references/commands.md` | Command registration, three-tier pattern, component extraction |
| `references/components.md` | TUI components (pi-tui + pi-coding-agent), custom(), theme styling, keyboard handling |
| `references/providers.md` | Provider registration, model definition, compat field, API key gating |
| `references/modes.md` | Mode behavior matrix, ctx.hasUI, dialog vs fire-and-forget, three-tier pattern |
| `references/messages.md` | sendMessage, registerMessageRenderer, notify, when to use each |
| `references/state.md` | appendEntry, state reconstruction, appendEntry vs sendMessage |
| `references/additional-apis.md` | Shortcuts, flags, exec, sendUserMessage, session name, labels, model control, EventBus, theme, UI customization, system prompt guidance injection |
| `references/publish.md` | npm publishing, changesets (manual file format + CI automation), GitHub Actions publish workflow, first-time setup, NPM_TOKEN, pre-publish checklist |
| `references/testing.md` | Local development, type checking, manual testing, debugging |
| `references/documentation.md` | README template, what to document, changelog |

## Reference Extensions

When implementing, look at these existing extensions for patterns:

**Standalone repos (recommended structure):**
- `pi-linkup` (`/Users/alioudiallo/code/src/github.com/aliou/pi-linkup/`): Tools wrapping a third-party API. Has tools, a command, custom message rendering, API key gating, system prompt guidance injection (`src/guidance.ts` + `src/hooks/system-prompt.ts`).
- `pi-synthetic` (`/Users/alioudiallo/code/src/github.com/aliou/pi-synthetic/`): Provider + tools. Has a provider with models, a command with `custom()` component, API key gating, async entry point.
- `pi-processes` (`/Users/alioudiallo/code/src/github.com/aliou/pi-processes/`): Multi-action tool with system prompt guidance injection (`src/guidance.ts` + `src/hooks/system-prompt.ts`), config toggle, background blocker hook.

**Monorepo extensions (simpler structure):**
- `extensions/defaults/` in this repo: Simple tool registration (get_current_time).
- `extensions/guardrails/` in this repo: Event hooks (tool_call blocking). Has `hooks/`, `commands/`, `components/`, `utils/` directories with config types in `config.ts`.
- `extensions/toolchain/` in this repo: Bash spawn hooks (command rewriting via `createBashTool`) combined with tool_call blockers. Has `blockers/`, `rewriters/`, `commands/`, `utils/` directories.
- `extensions/processes/` in this repo: Multi-action tool with StringEnum parameters.

## Critical Rules

1. **Execute parameter order**: `(toolCallId, params, signal, onUpdate, ctx)`. Signal before onUpdate.
2. **Always use `onUpdate?.()`**: Optional chaining. The parameter can be `undefined`.
3. **No `.js` in imports**: Use bare module paths (`./tools/my-tool`, not `./tools/my-tool.js`).
4. **Mode awareness**: Every `ctx.ui.custom()` call needs an RPC fallback (use `select`/`confirm`/`notify` -- they work in RPC). Do not use `done(undefined)` for normal interactive close paths when you detect fallback with `result === undefined`; use explicit sentinels (`null`, `"closed"`, boolean). Every `tool_call` hook with dialogs needs a `ctx.hasUI` check.
5. **API key gating**: Check before registering tools that require the key. Providers handle missing keys internally via their `models()` function.
6. **Tool naming**: Prefix with API name for third-party integrations (`linkup_web_search`). No prefix for internal tools (`get_current_time`).
7. **Tool call header pattern**: Keep `renderCall` consistent: first line `[Tool Name]: [Action] [Main arg] [Option args]`, extra lines for long args. Use display names, not raw tool IDs.
8. **Deterministic call rendering**: Build `renderCall` with a stable extraction order (action → main arg → option args → long args), process-style. Same input should produce same header layout.
9. **Long args placement**: Put long prompt/task/question/context strings on following lines. Keep first line scannable.
10. **Result layout consistency**: In `renderResult`, handle `isPartial` first, start final output with a clear state summary, use `expanded` for compact-vs-full detail, and keep one blank line before any footer.
11. **peerDependencies**: Any package Pi already ships (`@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`) must be listed in `peerDependencies` with `optional: true` in `peerDependenciesMeta` if imported at runtime. Without `optional: true`, npm 7+ auto-installs peers, adding hundreds of packages on every install even though Pi already provides them. Keep them in `devDependencies` too for local type checking — `pnpm install` installs peers, so development is unaffected. Use `>=CURRENT_VERSION` range, not `*`.
12. **Check existing components**: Before creating a new TUI component, check if `pi-tui` or `pi-coding-agent` already exports one that fits.
13. **Forward abort signals**: Always pass `signal` through to `fetch()`, `pi.exec()`, and API client methods. A tool that ignores its signal prevents cancellation from reaching the underlying operation. Never prefix with `_signal` unless the tool truly has no async work to cancel.
14. **Never use Node child_process APIs**: Do not use `child_process.exec`, `execSync`, `spawn`, `spawnSync`, `execFile`, or `execFileSync` to run binaries or shell scripts. Always use `pi.exec()`. `pi.exec` handles CWD, signal propagation, and output capture consistently. The only exception is if you need a long-lived streaming process with stdin/stdout piping that `pi.exec` cannot support — document the reason in code comments.
15. **Never use `homedir()` for pi paths**: Use the SDK helpers from `@mariozechner/pi-coding-agent` instead. They respect the `PI_CODING_AGENT_DIR` env var which is used for testing and custom setups. Key functions: `getAgentDir()`, `getSettingsPath()`, `getSessionsDir()`, `getPromptsDir()`, `getToolsDir()`, `getCustomThemesDir()`, `getModelsPath()`, `getAuthPath()`, `getBinDir()`, `getDebugLogPath()`. All exported from the main package entry point.
16. **Config uses the interface pattern**: `config.ts` defines two TypeScript interfaces (`RawConfig` with all fields optional, `ResolvedConfig` with all fields required) and a `ConfigLoader<Raw, Resolved>` instance. Do not use TypeBox schemas for config types.
17. **Entry point deviations must be documented**: The standard entry point pattern is load config → check `enabled` → register. Deviations (no config, API-key-first ordering, no `enabled` toggle) are acceptable when justified, but must be noted in `AGENTS.md`.

## Checklist

Before considering an extension complete:

- [ ] Entry point has correct default export signature.
- [ ] All tools have correct execute parameter order.
- [ ] All `onUpdate` calls use optional chaining.
- [ ] No `.js` file extensions in imports.
- [ ] `renderCall` uses a consistent first-line pattern (tool, action if any, main arg, options).
- [ ] `renderCall` arg extraction is deterministic (action → main arg → option args → long args).
- [ ] Long call arguments are moved to follow-up lines, not crammed into first line.
- [ ] `renderResult` handles `isPartial`, starts with a clear state summary, and uses `expanded` for compact-vs-full detail.
- [ ] If result includes a footer, there is a blank line above it.
- [ ] `renderResult` handles errors from thrown exceptions: checks for missing expected fields in `details` (framework passes `{}`, not `undefined`), extracts error message from `content`.
- [ ] `ctx.ui.custom()` calls have RPC fallback, and interactive close/cancel paths do not rely on `done(undefined)` when fallback detection uses `result === undefined`.
- [ ] `tool_call` hooks check `ctx.hasUI` before dialog methods.
- [ ] Fire-and-forget methods (notify, setStatus, etc.) are used without hasUI guards.
- [ ] If using custom message renderers: collapsed view is scannable, expanded view adds depth, and renderer has plain-text fallback when `details` is missing.
- [ ] `signal` is forwarded to all async operations (fetch, `pi.exec`, API clients). No unused `_signal`.
- [ ] Missing API keys produce a notification, not a crash.
- [ ] If in a monorepo: package doesn't depend on private workspace packages (run `pnpm run check:public-deps` if available).
- [ ] `pnpm typecheck` passes.
- [ ] No `child_process` imports -- uses `pi.exec()` for shell commands.
- [ ] No `homedir()` calls for pi paths -- uses SDK helpers (`getAgentDir()`, etc.).
- [ ] README documents tools, commands, env vars.
- [ ] `@mariozechner/pi-tui` (and any other Pi-provided package) is in `peerDependencies` with `optional: true` if imported at runtime, not just `devDependencies`.
- [ ] `prepare` script is `"[ -d .git ] && husky || true"`, not bare `"husky"`.
- [ ] `config.ts` uses `ConfigLoader<Raw, Resolved>` with TypeScript interfaces, not TypeBox schemas.
- [ ] If deviating from the standard entry point pattern (load-config → check-enabled → register), the reason is documented in `AGENTS.md`.
- [ ] If the extension adds a tool that competes with a bash fallback: guidance is injected via `src/guidance.ts` + `src/hooks/system-prompt.ts`, with a `systemPromptGuidance` config toggle defaulting to `true`.
