13  Extensions

13.1 Quick Start

Extensions are TypeScript modules that extend Pi’s behavior. They can subscribe to lifecycle events, register custom tools callable by the LLM, add commands, and more.

Create ~/.pi/agent/extensions/my-extension.ts:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  // React to events
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("Extension loaded!", "info");
  });

  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
      const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
      if (!ok) return { block: true, reason: "Blocked by user" };
    }
  });

  // Register a custom tool
  pi.registerTool({
    name: "greet",
    label: "Greet",
    description: "Greet someone by name",
    parameters: Type.Object({
      name: Type.String({ description: "Name to greet" }),
    }),
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      return {
        content: [{ type: "text", text: `Hello, ${params.name}!` }],
        details: {},
      };
    },
  });

  // Register a command
  pi.registerCommand("hello", {
    description: "Say hello",
    handler: async (args, ctx) => {
      ctx.ui.notify(`Hello ${args || "world"}!`, "info");
    },
  });
}

Test with --extension (or -e) flag:

pi -e ./my-extension.ts
Warning

Extensions run with your full system permissions and can execute arbitrary code. Only install from sources you trust.

13.2 Extension Locations

Extensions are auto-discovered from trusted locations. Project-local .pi/extensions entries load only after the project is trusted.

Location Scope
~/.pi/agent/extensions/*.ts Global (all projects)
~/.pi/agent/extensions/*/index.ts Global (subdirectory)
.pi/extensions/*.ts Project-local
.pi/extensions/*/index.ts Project-local (subdirectory)

Additional paths via settings.json:

{
  "extensions": [
    "/path/to/local/extension.ts",
    "/path/to/local/extension/dir"
  ]
}

Extensions in auto-discovered locations can be hot-reloaded with /reload.

13.3 Available Imports

Package Purpose
@earendil-works/pi-coding-agent Extension types (ExtensionAPI, ExtensionContext, events)
typebox Schema definitions for tool parameters
@earendil-works/pi-ai AI utilities (StringEnum for Google-compatible enums)
@earendil-works/pi-tui TUI components for custom rendering

npm dependencies work too. Add a package.json next to your extension, run npm install, and imports from node_modules/ are resolved automatically. Node.js built-ins (node:fs, node:path, etc.) are also available.

13.4 Writing an Extension

An extension exports a default factory function that receives ExtensionAPI. The factory can be synchronous or asynchronous:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("event_name", async (event, ctx) => {
    const ok = await ctx.ui.confirm("Title", "Are you sure?");
    ctx.ui.notify("Done!", "info");
    ctx.ui.setStatus("my-ext", "Processing...");
    ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]);
  });

  pi.registerTool({ /* ... */ });
  pi.registerCommand("name", { /* ... */ });
  pi.registerShortcut("ctrl+x", { /* ... */ });
  pi.registerFlag("my-flag", { /* ... */ });
}

Extensions are loaded via jiti, so TypeScript works without compilation. If the factory returns a Promise, Pi awaits it before continuing startup.

13.4.1 Async Factory Functions

Use an async factory for one-time startup work such as fetching remote configuration:

export default async function (pi: ExtensionAPI) {
  const response = await fetch("http://localhost:1234/v1/models");
  const payload = await response.json();
  
  pi.registerProvider("local-openai", {
    baseUrl: "http://localhost:1234/v1",
    apiKey: "$LOCAL_OPENAI_API_KEY",
    api: "openai-completions",
    models: payload.data.map((model) => ({
      id: model.id,
      name: model.name ?? model.id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: model.context_window ?? 128000,
      maxTokens: model.max_tokens ?? 4096,
    })),
  });
}

13.4.2 Long-Lived Resources and Shutdown

Extension factories may run in invocations that never start a session. Do not start background resources from the factory. Defer resource startup until session_start or the command/tool/event that needs it. Register an idempotent session_shutdown handler to close session-scoped resources.

13.5 Extension Styles

13.5.1 Single File

~/.pi/agent/extensions/
└── my-extension.ts

13.5.2 Directory with index.ts

~/.pi/agent/extensions/
└── my-extension/
    ├── index.ts
    ├── tools.ts
    └── utils.ts

13.5.3 Package with Dependencies

~/.pi/agent/extensions/
└── my-extension/
    ├── package.json
    ├── package-lock.json
    ├── index.ts
    └── node_modules/

13.6 Events

13.6.1 Lifecycle Overview

graph TD
    A[Startup] --> B[resources_discover]
    B --> C[project_trust]
    C --> D[session_start]
    D --> E[User message]
    E --> F[Assistant turn]
    F --> G[tool_call]
    G --> H[tool_result]
    H --> F
    F --> I[Turn end]
    I --> E
    E --> J[session_shutdown]

13.6.2 Startup Events

Event Description
resources_discover After settings and resources are loaded; before trust check
project_trust When a project trust decision is needed; first extension returning a decision wins
session_start After the session is loaded or created and context is built
session_shutdown Before the session exits; cleanup resources here

13.6.3 Resource Events

Event Description
resources_discover Discover extensions, skills, prompts, themes

13.6.4 Session Events

Event Description
session_start Session loaded or created
session_shutdown Session about to exit
session_saved Session file written to disk

13.6.5 Agent Events

Event Description
message_queued A message was queued while agent is busy
message_delivered A queued message was delivered
before_user_message Before sending user message to model
after_user_message After user message is sent
assistant_response Assistant produced output
turn_start Agent turn started
turn_end Agent turn ended

13.6.6 Model Events

Event Description
model_start Model API call started
model_end Model API call completed
model_error Model API call failed
compaction:before Before context compaction
compaction:after After context compaction

13.6.7 Tool Events

Event Description
tool_call Before a tool is called; can block or modify
tool_result After a tool returns; can modify result

Tool call handlers can return { block: true, reason: "..." } to prevent execution, or { input: {...} } to modify parameters.

13.6.8 User Bash Events

Event Description
user_bash User ran !command in the editor

13.6.9 Input Events

Event Description
input_submit User submitted input in the editor

13.7 ExtensionContext

The ExtensionContext (ctx) provides access to Pi’s internal state and UI:

13.7.1 ctx.ui

User interaction methods: notify(), confirm(), select(), input(), setStatus(), setWidget(), custom().

13.7.2 ctx.mode

Current run mode: "tui", "json", "rpc", or "print".

13.7.3 ctx.hasUI

Whether a TUI is available (only true in "tui" mode).

13.7.4 ctx.cwd

Current working directory.

13.7.5 ctx.isProjectTrusted()

Whether the project has been trusted.

13.7.6 ctx.sessionManager

Access to the session manager for reading and manipulating sessions.

13.7.7 ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels

Access to the model registry, current model, thinking level, and scoped models.

13.7.8 ctx.signal

AbortSignal for cooperative cancellation.

13.7.9 ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()

Check if agent is idle, abort current work, or check for queued messages.

13.7.10 ctx.shutdown()

Trigger session shutdown.

13.7.11 ctx.getContextUsage()

Current context window usage (tokens used, percentage).

13.7.12 ctx.compact(options?)

Trigger manual compaction.

13.7.13 ctx.getSystemPrompt()

Get the current system prompt.

13.8 ExtensionCommandContext

When inside a command handler, the context includes additional methods:

13.8.1 ctx.getSystemPromptOptions()

Get current system prompt options.

13.8.2 ctx.waitForIdle()

Wait for the agent to become idle.

13.8.3 ctx.newSession(options?)

Create a new session.

13.8.4 ctx.fork(entryId, options?)

Fork the session from a specific entry.

13.8.5 ctx.navigateTree(targetId, options?)

Navigate to a specific entry in the session tree.

13.8.6 ctx.switchSession(sessionPath, options?)

Switch to a different session file.

13.8.7 ctx.reload()

Reload extensions, skills, prompts, themes, and context files.

13.9 ExtensionAPI Methods

13.9.1 pi.on(event, handler)

Register an event handler.

13.9.2 pi.registerTool(definition)

Register a custom tool callable by the LLM.

13.9.3 pi.sendMessage(message, options?)

Send a message to the model programmatically.

13.9.4 pi.sendUserMessage(content, options?)

Send a user message programmatically.

13.9.5 pi.appendEntry(customType, data?)

Append a custom entry to the session file for persistence.

13.9.6 pi.setSessionName(name) / pi.getSessionName()

Set or get the session display name.

13.9.7 pi.setLabel(entryId, label)

Set a custom label on a session entry.

13.9.8 pi.registerCommand(name, options)

Register a slash command.

13.9.9 pi.getCommands()

Get all registered commands.

13.9.10 pi.registerMessageRenderer(customType, renderer)

Custom rendering for messages of a custom type.

13.9.11 pi.registerMarkdownTransformer(transformer)

Transform markdown before rendering.

13.9.12 pi.registerEntryRenderer(customType, renderer)

Custom rendering for session entries.

13.9.13 pi.registerShortcut(shortcut, options)

Register a keyboard shortcut.

13.9.14 pi.registerFlag(name, options)

Register a CLI flag.

13.9.15 pi.exec(command, args, options?)

Execute a shell command.

13.9.16 pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)

Manage the active tool set.

13.9.17 pi.setModel(model) / pi.getThinkingLevel() / pi.setThinkingLevel(level)

Manage the current model and thinking level.

13.9.18 pi.events

Direct access to the event emitter.

13.9.19 pi.registerProvider(name, config) / pi.unregisterProvider(name)

Register or unregister a custom provider.

13.10 State Management

Extensions can persist state across restarts by appending custom entries to the session file:

pi.appendEntry("my-ext:state", {
  counter: 42,
  lastAction: "greet",
  items: ["apple", "banana"],
});

On session_start, read custom entries from the session tree to restore state.

13.11 Custom Tools

13.11.1 Tool Definition

pi.registerTool({
  name: "search_files",
  label: "Search",
  description: "Search file contents for a pattern",
  parameters: Type.Object({
    pattern: Type.String({ description: "Regex pattern to search for" }),
    path: Type.Optional(Type.String({ description: "Directory to search in" })),
  }),
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // Implementation
    return {
      content: [{ type: "text", text: "Found 3 matches..." }],
      details: { matchCount: 3 },
    };
  },
});

13.11.2 Overriding Built-in Tools

Register a tool with the same name as a built-in to override it. The tool name must match exactly.

13.11.3 Remote Execution

Tools can execute code on remote systems:

pi.registerTool({
  name: "deploy",
  description: "Deploy to staging",
  parameters: Type.Object({}),
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    const result = await fetch("https://ci.example.com/deploy", { /* ... */ });
    return { content: [{ type: "text", text: "Deployed!" }], details: {} };
  },
});

13.11.4 Dynamic Tool Loading

Tools can be registered and unregistered at runtime:

pi.on("session_start", async (_event, ctx) => {
  if (await shouldEnableFeature()) {
    pi.registerTool({ /* ... */ });
  }
});

13.12 Custom UI

13.12.1 Dialogs

const choice = await ctx.ui.select("Choose option", [
  "Option A",
  "Option B",
  "Option C",
]);

const confirmed = await ctx.ui.confirm("Delete files?", "This cannot be undone.");

const name = await ctx.ui.input("Enter name:", { placeholder: "John Doe" });

13.12.3 Autocomplete Providers

Register custom autocomplete providers for the @ file reference menu or Tab completion.

13.12.4 Custom Components

For complex interactions, use ctx.ui.custom() to render a full custom TUI component with keyboard input handling.

13.13 Error Handling

Extension errors are caught by Pi. Tool errors are returned to the model as error results. Event handler errors are logged and displayed as notifications. Extensions should handle expected errors gracefully:

async execute(toolCallId, params, signal, onUpdate, ctx) {
  try {
    const result = await riskyOperation();
    return { content: [{ type: "text", text: result }], details: {} };
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${error.message}` }],
      details: { error: true },
    };
  }
}

13.14 Mode Behavior

Extensions behave differently depending on the run mode:

Mode UI Available Events Tools
tui Full UI All events Full interaction
print No UI Most events No interactive UI
json No UI Most events No interactive UI
rpc No UI Most events No interactive UI

Always check ctx.hasUI before calling UI methods. In non-interactive modes, ctx.ui methods are no-ops or return defaults.

Tip

See examples/extensions/ in the Pi repository for working implementations including permission gates, git checkpointing, path protection, conversation summaries, interactive tools, and even games.