Skip to content

Creating Extensions

Extensions are the building blocks of tallow. Each extension provides specific functionality that ships bundled with the application.

Each extension lives in the extensions/ directory:

extensions/my-extension/
├── extension.json # Manifest (required)
├── index.ts # Entry point
└── README.md # Documentation (optional)

Every extension needs an extension.json manifest. This file is the source of truth for extension metadata.

{
"name": "my-extension",
"version": "0.1.0",
"description": "What the extension does in one line",
"category": "tool",
"tags": ["utility"],
"files": ["index.ts"],
"relationships": [],
"npmDependencies": {}
}
FieldRequiredDescription
nameYesUnique extension name. Must match the directory name.
versionYesSemver version string.
descriptionYesOne-line description.
categoryNoOne of tool, ui, utility, command, integration, language-support, context, alias.
tagsNoSearchable tags.
filesYesFiles included with the extension. Glob patterns supported. extension.json is always included automatically.
relationshipsNoDependencies and integrations (see below).
npmDependenciesNonpm packages needed at runtime.
configFilesNoConfig files to copy or merge into the install root.
piVersionNoMinimum pi version required.

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. Must be present.
  • enhances, Soft link. If both are present, they integrate.
  • conflicts, Cannot coexist.
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
});
}

After creating or modifying an extension:

Terminal window
# Typecheck extensions
npm run typecheck:extensions
# Build the project
npm run build
# Test by running tallow
tallow

Extensions are typechecked against the pi API. The project includes a dedicated tsconfig.extensions.json and the pre-commit hook validates all extensions before allowing commits.

Terminal window
# Run extension typecheck manually
npm run typecheck:extensions
  • Tool return types, execute() must return AgentToolResult<T> with content and details. Use as const on content type literals (type: "text" as const).
  • Widget render callbacks, setWidget factories receive (tui, theme), but viewport width comes from the render(width) method, not from tui.
  • globalThis state, If your extension stores cross-reload state on globalThis, declare the property in extensions/global.d.ts:
extensions/global.d.ts
declare global {
var __piMyExtensionState: Map<string, unknown> | undefined;
}
export {};

These are available as devDependencies for import:

  • @mariozechner/pi-coding-agent, ExtensionAPI, ExtensionContext, tools
  • @mariozechner/pi-tui, TUI, Container, Text, Key
  • @mariozechner/pi-ai, Model, TextContent, ImageContent
  • @mariozechner/pi-agent-core, AgentToolResult, AgentToolUpdateCallback
  • @sinclair/typebox, Type for parameter schemas
  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