Example

Register a tool and a command from an extension

Load a TypeScript extension that registers a model-callable tool and a slash command, and watch the tool run through Pi's real pipeline.

An extension is a TypeScript module whose default export receives ExtensionAPI. Registering a tool makes it active and declared to the model, while registering a command adds a / command beside the built-ins. This page loads both the smallest command extension and the running example’s review_scope tool; the only substitution is the model, a faux provider replaying a script.

Run it

Pi 1.0.4 loads local TypeScript with jiti, so there is no build step. The command extension below is the whole file, reproduced exactly:

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

export default function (pi: ExtensionAPI) {
	pi.registerCommand("hello", {
		description: "Show a greeting",
		handler: async (name, ctx) => {
			ctx.ui.notify(`Hello, ${name || "world"}!`, "info");
		},
	});
}

Load it with pi --extension ./hello.ts and run /hello.

The running example is a tool instead of a command. review_scope is declared with defineTool and Type, and registered with pi.registerTool. Its base parameter is Type.Optional(Type.String(...)), so a call with no arguments is valid and defaults to the working tree; passing { base: "main" } names git diff --name-only main.

Registered with What it adds How it appears
pi.registerCommand("hello", …) a /hello command listed beside the built-ins
pi.registerTool(reviewScopeTool) a model-callable tool active and declared to the model

Expected outcome

This is observed: first-extension.test.ts loads each extension into a real AgentSession with a scripted faux provider and asserts the following.

Extension Step Result
command getRegisteredCommands() includes hello, beside the built-ins
tool getActiveToolNames() includes review_scope — a direct tool activates on registration
tool model calls review_scope with { base: "main" } isError: false; text names git diff --name-only main; details is { base: "main", diffCmd: "git diff --name-only main" }
tool model calls review_scope with {} isError: false; text names git status --short

Mechanism and limitations

Documented: the exported ExtensionAPI type defines the default-factory contract, pi.registerCommand, pi.registerTool and defineTool. A direct tool (the default exposure) is activated on registration and declared to the model, and a tool result carries model-facing content plus a details field.

Observed: first-extension.test.ts runs the extension through the real runtime — real extension loader, real built-in tools, an in-memory session — and asserts that a registered tool is active, that a scripted call produces a toolResult whose content and details match what execute() returned, and that the command is registered. It also pins a defect: base was first declared as required Type.String(...) while its description called it optional, so a call with no arguments failed the schema. It is now Type.Optional(Type.String(...)), with a test for the empty call. The parameter description and the parameter schema are two statements, and only the schema is checked.

Limitations: registering a tool makes it exist and be declared; it is not permission to run, and the model may still ignore it (Chapter 17). A project extension under .pi/extensions/ does not load at all until the project is trusted (Chapter 8), so until then a skill fires without the tool. The test uses a faux model, so it shows the code around the model, not that a real model would choose review_scope.

Full source and test .

Understand this example

Copy this prompt into your AI tool. No code runs here.

Apply this example

Copy this prompt into your AI tool. No code runs here.