Chapter 10 · Extensions Intro: TypeScript Extension API
Chapter goals: Understand what extensions can do; where to place them; how to load and hot-reload; master the全景 of extension lifecycle events; and完整走读 a minimal extension example.
10.1 Extensions Are pi's "Superpower Layer"
If skills are "说明书 for the model"; extensions are "surgery on pi itself". Extensions are TypeScript modules that can:
- Register custom tools —
pi.registerTool(); let the LLM调用 your functions; - Intercept events — block or修改 tool calls (e.g. popup confirmation before
rm -rf); inject context; customize compaction; - User interaction — popup select/confirm/input/notify via
ctx.ui; - Custom UI components — build TUI组件 with keyboard interaction via
ctx.ui.custom(); - Custom commands — register
/mycommandviapi.registerCommand(); - Session persistence — store cross-restart状态 via
pi.appendEntry().
The official列举的典型用例: dangerous command权限门; git auto checkpoint; protecting .env from being written; custom compaction; file listening/webhook integration … someone even写了贪吃蛇 to play while等待. pi刻意不内置 sub-agents and plan mode;就是要靠这个生态来长.
10.2 Where to Place and How to Load
~/.pi/agent/extensions/*.ts ← global auto-discover
~/.pi/agent/extensions/*/index.ts ← global (子目录形式)
.pi/extensions/*.ts ← project-level (受 Project Trust门控)
.pi/extensions/*/index.ts ← project-level (子目录形式)补充通道: the extensions array in settings.json can指定 local paths/directories; the packages array can加载 npm/git-distributed packages; the CLI uses -e ./path.ts for临时挂载.
Use -e during开发期; move to the auto-discover directory when stable
The官方建议: only extensions放在 the auto-discover位置支持 /reload热重载; pi -e ./x.ts only适合快速试验.
# Quick-test an extension
pi -e ./my-extension.ts
# After placing in the auto-discover位置; after改 code in interactive mode:
/reloadPermissions即 User Permissions
Extensions run with your完整系统权限 and can执行任意代码 — this is why project-level extensions are受 Project Trust门控. Only install来源 you trust.
10.3 Walkthrough: Minimal Extension Example
Below is an官方风格的示例 covering four core capabilities;拆解逐段:
// ~/.pi/agent/extensions/my-extension.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// ① Subscribe to lifecycle events: notify at session start
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
// ② Intercept tool calls: ask the user before dangerous bash commands
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: greet callable by the LLM
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 custom command /hello
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}四个关键点:
- Default-export a factory function; the parameter is
ExtensionAPI; the factory可以是异步的; pi awaits it完成后再继续启动; - Tool parameter schemas use typebox's
Type.Object(...); thedescriptionfield becomes the模型理解参数的依据; - Returning
{ block: true; reason }from thetool_callhandler即可拦截本次工具执行; - Extensions are loaded via jiti; write TypeScript directly; no编译步骤 needed.
Dependency管理: place a package.json旁 the extension and after npm install; packages in node_modules are自动可用; Node built-in modules ( node:fs etc.) are天然可用.
10.4 Lifecycle Events Panorama
The官方文档给出的启动 to响应完整事件流 (节选主干):
pi启动
├─ project_trust (received only by user/global/CLI extensions;早于 project resources load)
├─ session_start { reason: "startup" }
└─ resources_discover
User sends a prompt
├─ input (可拦截/改写/接管 input)
├─ before_agent_start (可inject messages; modify the system prompt)
├─ agent_start
│ ┌─ turn loop (repeat if the LLM calls tools)─────────┐
│ ├─ turn_start → context (可改 message list)
│ ├─ tool_execution_start
│ ├─ tool_call (可 block)
│ ├─ tool_result (可 modify the result)
│ └─ turn_end
├─ agent_end
└─ agent_settled (when there are no retries/compaction/follow-up messages)
/new or /resume switching sessions
└─ session_before_switch → session_shutdown → session_start{reason}两个对初学者最重要的实践规则:
- Do not启动 background resources in the factory function (processes; sockets; timers) — the factory可能运行 in calls without a session;推迟这类初始化 to
session_start; and幂等地清理 insession_shutdown; project_trust早于 project resources: logic needing to感知 the trust state should挂 on user/global extensions.
A真实用途 for an异步 factory — pulling the model list from a local service at startup and注册 a provider:
export default async function (pi: ExtensionAPI) {
const res = await fetch("http://localhost:1234/v1/models");
const payload = await res.json();
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((m) => ({
id: m.id,
name: m.name ?? m.id,
reasoning: false,
input: ["text"],
cost: { input: 0; output: 0; cacheRead: 0; cacheWrite: 0 },
contextWindow: m.context_window ?? 128000,
maxTokens: m.max_tokens ?? 4096,
})),
});
}调试方法: ctx.ui.notify()打点最简单; type definitions可直接查看安装目录 node_modules/@earendil-works/pi-coding-agent/dist/.
Chapter Summary
- Extensions = a TypeScript module default-exporting a factory function; loaded directly by jiti without编译;
- Auto-discover位置: global
~/.pi/agent/extensions/; project.pi/extensions/(the latter needs trust); use-e+/reloadduring开发期; - Four capabilities: event subscriptions (
pi.on); tool registration (registerTool+ typebox); command registration (registerCommand); UI interaction (ctx.ui); - Returning
{block:true; reason}fromtool_call可拦截 dangerous operations; - Defer background resources to启动 at
session_start;幂等清理 atsession_shutdown.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What is the correct导出形式 for an extension file?
2. To popup a confirmation框 before the LLM calls bash to执行 rm -rf; which event should you订阅?
3. Regarding extension loading and hot-reload; which is correct?
4. An extension needs to维护 a file watcher during a session. What is the正确做法?
🛠️ Hands-on Practice
- Implement this chapter's my-extension.ts;验证 the
/hellocommand;greettool; andrm -rfinterception三条路径都工作. - Write a
tool_resultevent handler that appends a line[inspected by ext]to the end of every bash output;观察其对模型的影响. - Deliberately put an infinite-loop
setTimeoutprint in the factory function; then对比 with the规范写法 (moved to session_start + session_shutdown cleanup).
After completing the exercises; proceed to Chapter 11: Extensions Advanced.