Skip to content

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

FieldRequiredDescription
nameYesUnique extension name. Must match the directory name.
versionYesSemver version string.
descriptionYesOne-line description shown in pi-code list.
categoryNoOne of tool, ui, language-support, integration, utility.
tagsNoSearchable tags.
filesYesFiles to copy on install. Glob patterns supported. extension.json is always copied automatically.
relationshipsNoDependencies and integrations (see below).
npmDependenciesNonpm packages needed at runtime. Merged into a shared package.json on install.
configFilesNoConfig files to copy or merge into the install root.
postInstallNoScript to run after install (relative to source dir).
piVersionNoMinimum 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

extensions/my-extension/index.ts
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:

Terminal window
# Install it
bun run cli install my-extension
# Force reinstall after code changes
bun run cli install my-extension --force
# Test with pi
./pi-dev.sh

How 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

  1. Single responsibility — each extension should do one thing well
  2. Document your extension — include a README with usage examples
  3. Handle errors gracefully — provide meaningful error messages via ctx.ui.notify()
  4. Declare relationships — if your extension depends on or enhances another, say so in the manifest
  5. Use deliverAs for async messages — if calling pi.sendUserMessage() from an async context (subprocess callback, timer), pass { deliverAs: "followUp" } to avoid errors when the agent is mid-turn