Skip to content

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 /mycommand via pi.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 ​

text
~/.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适合快速试验.

bash
# Quick-test an extension
pi -e ./my-extension.ts
# After placing in the auto-discover位置; after改 code in interactive mode:
/reload

Permissions即 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;拆解逐段:

typescript
// ~/.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");
    },
  });
}

四个关键点:

  1. Default-export a factory function; the parameter is ExtensionAPI; the factory可以是异步的; pi awaits it完成后再继续启动;
  2. Tool parameter schemas use typebox's Type.Object(...); the description field becomes the模型理解参数的依据;
  3. Returning { block: true; reason } from the tool_call handler即可拦截本次工具执行;
  4. 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响应完整事件流 (节选主干):

text
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幂等地清理 in session_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:

typescript
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 + /reload during开发期;
  • Four capabilities: event subscriptions (pi.on); tool registration (registerTool + typebox); command registration (registerCommand); UI interaction (ctx.ui);
  • Returning {block:true; reason} from tool_call可拦截 dangerous operations;
  • Defer background resources to启动 at session_start;幂等清理 at session_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 ​

  1. Implement this chapter's my-extension.ts;验证 the /hello command; greet tool; and rm -rf interception三条路径都工作.
  2. Write a tool_result event handler that appends a line [inspected by ext] to the end of every bash output;观察其对模型的影响.
  3. Deliberately put an infinite-loop setTimeout print in the factory function; then对比 with the规范写法 (moved to session_start + session_shutdown cleanup).

After completing the exercises; proceed to Chapter 11: Extensions Advanced.