Creating Extensions
Extensions are the building blocks of pi-code. Each extension provides specific functionality that can be installed and managed via the CLI.
Extension Structure
Each extension lives in the extensions/ directory:
extensions/my-extension/├── extension.json # Manifest (required)├── index.ts # Entry point└── README.md # Documentation (optional)The Manifest (extension.json)
Every extension needs an extension.json manifest. This file is the source of truth — the CLI discovers installed extensions by scanning for extension.json on disk. No central registry, no database.
{ "name": "my-extension", "version": "0.1.0", "description": "What the extension does in one line", "category": "tool", "tags": ["utility"], "files": ["index.ts"], "relationships": [], "npmDependencies": {}}Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Unique extension name. Must match the directory name. |
version | Yes | Semver version string. |
description | Yes | One-line description shown in pi-code list. |
category | No | One of tool, ui, language-support, integration, utility. |
tags | No | Searchable tags. |
files | Yes | Files to copy on install. Glob patterns supported. extension.json is always copied automatically. |
relationships | No | Dependencies and integrations (see below). |
npmDependencies | No | npm packages needed at runtime. Merged into a shared package.json on install. |
configFiles | No | Config files to copy or merge into the install root. |
postInstall | No | Script to run after install (relative to source dir). |
piVersion | No | Minimum pi version required. |
Relationships
Extensions can declare relationships with each other:
{ "relationships": [ { "name": "hooks", "kind": "requires", "reason": "Needs hook system for event handling" }, { "name": "tasks", "kind": "enhances", "reason": "Adds progress tracking to task widget" }, { "name": "old-extension", "kind": "conflicts", "reason": "Replaces old-extension entirely" } ]}requires— Hard dependency. Automatically installed together.enhances— Soft link. If both are installed, they integrate. Installing one triggers a re-copy of the other.conflicts— Cannot coexist. Blocks installation if the other is present.
Basic Extension
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) { // Register a slash command pi.registerCommand("greet", { description: "Say hello", handler: async (args, ctx) => { ctx.ui.notify(`Hello, ${args || "world"}!`, "info"); }, });
// Hook into lifecycle events pi.on("session_start", async (_event, ctx) => { // Runs when a session starts });}Install & Test
After creating your extension:
# Install itbun run cli install my-extension
# Force reinstall after code changesbun run cli install my-extension --force
# Test with pi./pi-dev.shHow Install Works
The CLI copies your extension’s files to ~/.pi/agent/extensions/my-extension/ — pi’s native agent directory. The extension.json manifest is always included — it’s how the CLI knows what’s installed. There’s no separate registry file; the filesystem is the source of truth.
- Install = copy files to the extensions directory
- Uninstall = delete the extension’s directory
- List = scan
extensions/*/extension.json
Best Practices
- Single responsibility — each extension should do one thing well
- Document your extension — include a README with usage examples
- Handle errors gracefully — provide meaningful error messages via
ctx.ui.notify() - Declare relationships — if your extension depends on or enhances another, say so in the manifest
- Use
deliverAsfor async messages — if callingpi.sendUserMessage()from an async context (subprocess callback, timer), pass{ deliverAs: "followUp" }to avoid errors when the agent is mid-turn