Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/badlogic/pi-mono/llms.txt

Use this file to discover all available pages before exploring further.

Extensions are TypeScript modules that extend Pi’s behavior. They can register custom tools, subscribe to lifecycle events, add commands, and create custom UI components.

What You Can Build

Extensions enable:
  • Custom tools - Register tools the LLM can call via pi.registerTool()
  • Event interception - Block or modify tool calls, inject context, customize compaction
  • User interaction - Prompt users via ctx.ui (select, confirm, input, notify)
  • Custom UI components - Full TUI components with keyboard input
  • Custom commands - Register commands like /mycommand
  • Session persistence - Store state that survives restarts
  • Custom rendering - Control how tool calls/results and messages appear

Quick Start

Create ~/.pi/agent/extensions/my-extension.ts:
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/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:
pi -e ./my-extension.ts

Extension Structure

1
Create Extension File
2
Extensions export a default function that receives ExtensionAPI:
3
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  // Extension code
}
4
Choose Location
5
Extensions are auto-discovered from:
6
LocationScope~/.pi/agent/extensions/*.tsGlobal (all projects)~/.pi/agent/extensions/*/index.tsGlobal (subdirectory).pi/extensions/*.tsProject-local.pi/extensions/*/index.tsProject-local (subdirectory)
7
Additional paths via settings.json:
8
{
  "extensions": [
    "/path/to/local/extension.ts",
    "/path/to/local/extension/dir"
  ]
}
9
Add Dependencies (Optional)
10
For extensions that need npm packages, add a package.json:
11
~/.pi/agent/extensions/
└── my-extension/
    ├── package.json
    ├── package-lock.json
    ├── node_modules/
    └── index.ts
12
{
  "name": "my-extension",
  "dependencies": {
    "axios": "^1.0.0"
  }
}
13
Run npm install in the extension directory.

Building a Custom Tool

1
Define Tool Schema
2
Use TypeBox to define parameters:
3
import { Type } from "@sinclair/typebox";
import { StringEnum } from "@mariozechner/pi-ai";

pi.registerTool({
  name: "search",
  label: "Search",
  description: "Search for content",
  parameters: Type.Object({
    query: Type.String({ description: "Search query" }),
    type: StringEnum(["web", "code"] as const),
    limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
  }),
  // ...
});
4
Use StringEnum from @mariozechner/pi-ai for string enums. Type.Union/Type.Literal doesn’t work with Google’s API.
5
Implement Execution
6
The execute function runs when the LLM calls the tool:
7
async execute(toolCallId, params, signal, onUpdate, ctx) {
  // Check for cancellation
  if (signal?.aborted) {
    return { content: [{ type: "text", text: "Cancelled" }] };
  }

  // Stream progress updates
  onUpdate?.({
    content: [{ type: "text", text: "Searching..." }],
    details: { progress: 50 },
  });

  // Perform the work
  const results = await performSearch(params.query);

  // Return result
  return {
    content: [{ type: "text", text: JSON.stringify(results) }],
    details: { count: results.length },
  };
}
8
Add Custom Rendering (Optional)
9
Customize how the tool appears in the TUI:
10
import { Text } from "@mariozechner/pi-tui";

renderCall(args, theme) {
  let text = theme.fg("toolTitle", theme.bold("search "));
  text += theme.fg("muted", args.query);
  return new Text(text, 0, 0);
}

renderResult(result, { expanded }, theme) {
  if (result.details?.error) {
    return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
  }

  let text = theme.fg("success", `✓ Found ${result.details?.count} results`);
  if (expanded) {
    // Show full results when expanded (Ctrl+O)
    text += "\n" + result.content[0].text;
  }
  return new Text(text, 0, 0);
}

Complete Tool Example

Here’s a complete working example:
import { Type } from "@sinclair/typebox";
import { Text } from "@mariozechner/pi-tui";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "hello",
    label: "Hello",
    description: "A simple greeting tool",
    parameters: Type.Object({
      name: Type.String({ description: "Name to greet" }),
    }),

    async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
      const { name } = params as { name: string };
      return {
        content: [{ type: "text", text: `Hello, ${name}!` }],
        details: { greeted: name },
      };
    },

    renderCall(args, theme) {
      return new Text(
        theme.fg("toolTitle", theme.bold("hello ")) +
        theme.fg("muted", args.name),
        0, 0
      );
    },

    renderResult(result, options, theme) {
      return new Text(
        theme.fg("success", result.content[0].text),
        0, 0
      );
    },
  });
}

Event Handling

Subscribe to lifecycle events:
pi.on("session_start", async (_event, ctx) => {
  // Session started
});

pi.on("session_shutdown", async (_event, ctx) => {
  // Clean up resources
});

State Management

Store state in tool result details for proper branching support:
export default function (pi: ExtensionAPI) {
  let items: string[] = [];

  // Reconstruct state from session
  pi.on("session_start", async (_event, ctx) => {
    items = [];
    for (const entry of ctx.sessionManager.getBranch()) {
      if (entry.type === "message" && entry.message.role === "toolResult") {
        if (entry.message.toolName === "my_tool") {
          items = entry.message.details?.items ?? [];
        }
      }
    }
  });

  pi.registerTool({
    name: "my_tool",
    // ...
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      items.push("new item");
      return {
        content: [{ type: "text", text: "Added" }],
        details: { items: [...items] },  // Store for reconstruction
      };
    },
  });
}

User Interaction

Prompt users with dialogs:
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
if (!ok) return;

Available Imports

import type {
  ExtensionAPI,
  ExtensionContext,
  // Event types
  AgentSessionEvent,
  ToolCallEvent,
  // Tool types
  ToolDefinition,
  ToolInfo,
} from "@mariozechner/pi-coding-agent";

import { Type } from "@sinclair/typebox";
import { StringEnum } from "@mariozechner/pi-ai";
import { Text, Container, SelectList } from "@mariozechner/pi-tui";

Next Steps

  • See Creating Skills for specialized task workflows
  • See Custom Providers for adding LLM providers
  • Check packages/coding-agent/examples/extensions/ for more examples

Build docs developers (and LLMs) love