Pi Extensions 入门指南

Pi 的 Extension 是什么

Extension(扩展)是 Pi 的 TypeScript 插件系统,也是它可扩展性体系中最核心的一环。你可以把它理解成给 Pi 写"插件",用来定制它的行为、添加新能力,或者接入外部系统。

Pi 的哲学是"不把工作流强加给你",所以像权限控制、子代理、plan mode 这些其他工具内置的功能,Pi 全都不做——而是把能力交给 extension,让你自己决定怎么做。写一个 extension、或者装一个别人写好的 Pi Package,就能把 Pi 调整成你想要的形状,不需要 fork 源码。

具体来说,extension 能做这些事情:

它还覆盖了 Pi 运行的完整生命周期——从 session 启动、LLM 对话开始、每个 turn、每次工具调用、到 session 关闭,几乎所有环节都可以插入逻辑。所以 extension 不光能"加功能",也能"改行为"。

快速看一眼代码

一个最简 extension 长这样。三件事:拦截危险命令、注册一个工具、注册一个命令:

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

export default function (pi: ExtensionAPI) {
  // 拦截危险 bash 命令
  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
      const ok = await ctx.ui.confirm("危险操作!", "允许 rm -rf 吗?");
      if (!ok) return { block: true, reason: "用户拒绝" };
    }
  });

  // 注册一个 LLM 可调用的工具
  pi.registerTool({
    name: "greet",
    description: "向某人打招呼",
    parameters: Type.Object({ name: Type.String() }),
    async execute(_id, params) {
      return { content: [{ type: "text", text: `Hello, ${params.name}!` }] };
    },
  });

  // 注册一个用户可用的命令
  pi.registerCommand("hello", {
    description: "打招呼",
    handler: async (args, ctx) => {
      ctx.ui.notify(`Hello ${args || "world"}!`, "info");
    },
  });
}

放在 ~/.pi/agent/extensions/my-ext.ts,或者用 pi -e ./my-ext.ts 立即测试。

官方有哪些示例

Pi 官方提供了 70 多个 示例 extension,都在 <pi 安装目录>/examples/extensions/ 下面。覆盖范围极广——从 8 行的装饰性小插件,到几百行、带有独立子目录的完整功能系统。

按复杂度,可以分成六个层级

从写一行就能跑,到需要理解完整系统架构:

L0 — 装饰级(< 50 行):hello 工具、模型名显示在状态栏、桌面通知、设置 session 名、编辑器上下放点东西…基本就是调一个 API,看一下效果,非常适合上手。

L1 — 拦截级(30-60 行):开始有实际功用。比如拦截危险 bash 弹窗确认、阻止写入 .env 文件、切换/fork 会话前确认、切换前检查 git 有没有未提交改动、退出时自动 git commit、动态修改系统提示词让 LLM 变成海盗口吻…

这一类是最实用的层级—— 核心模式就是 监听事件 → 判断条件 → { block: true } 或 { cancel: true }。

L2 — 命令 & 输入级(40-120 行):开始处理用户输入。比如 ?quick xxx 自动变成"简要回答"、!{git branch} 内联展开成命令输出、运行时动态注册工具、发送用户消息、扩展间通信…

这里还出现了 input 事件的三元组返回值:continue(放行)、transform(改写)、handled(不交给 LLM,自己处理掉)。

L3 — 自定义渲染级(80-200 行):用 @earendil-works/pi-tui 组件库做自定义 UI。比如给消息套自己的样式、构建交互式选择界面、用 loader 动画调用 LLM 做总结、自定义底部状态栏显示 git 分支和 token 用量、设置页头 ASCII art…

这一层的核心是 ctx.ui.custom() —— 你提供一个 render + handleInput + invalidate 的组件,Pi 把它嵌入 TUI。

L4 — 高级工具 & 编辑器级(120-350 行):开始涉及状态持久化和编辑器替换。最典型的是 todo.ts——完整 CRUD 工具,数据存进 session entry 的 details 字段,session 加载时自动从历史重建状态,带自定义渲染和独立的 /todos 命令查看界面。还有自定义编辑器(彩虹动画、vim 模态模式)、git stash 自动检查点、交互式 shell(vim/htop)、覆写内置 read 工具做访问审计…

L5 — 系统级(200-800+ 行单文件):单文件承载完整系统。会话交接(handoff)、完全自定义压缩策略(用 Gemini Flash 来总结)、井字棋/贪吃蛇/太空侵略者游戏、把全部工具代理到远程 SSH…

L6 — 目录级扩展(多文件 + 依赖):独立的子目录,可能带 package.json 和 npm 依赖。包括 Claude Code 风格的 plan mode、子代理系统、DOOM 游戏渲染、OS 级沙箱、自定义 Provider、微 VM 路由…

按用途,大致分这几类

有几个已经被 Pi 主线集成了(如 /quit、/name、/tree 的标签功能),但它们的源码仍然值得看——展示的是底层 API 用法。

挑几个简单的细讲

从简单到稍微复杂,每条都在 50 行以内,但覆盖了 extension 开发的核心模式。

1. model-status.ts(24 行)—— 状态栏显示当前模型

export default function (pi: ExtensionAPI) {
  pi.on("model_select", async (event, ctx) => {
    const { model } = event;
    ctx.ui.notify(`Model: ${model.provider}/${model.id}`, "info");
    ctx.ui.setStatus("model", `🤖 ${model.id}`);
  });
}

做的事很简单——监听模型切换事件,切换时弹一条通知,并且在底部状态栏常驻显示当前模型名。这是最纯粹的"监听 → 更新 UI"模式。

2. notify.ts(30 行)—— 系统桌面通知

export default function (pi: ExtensionAPI) {
  pi.on("agent_end", async () => {
    notify("Pi", "Ready for input");
  });
}

LLM 处理完一次请求后,自动发一条系统通知。它内部自动检测终端类型(Ghostty/iTerm2/Kitty/Windows Terminal),走对应的协议——你不需要关心这些。展示了 agent_end 事件和 Node.js 原生模块的组合。

3. confirm-destructive.ts —— 破坏性操作前确认

pi.on("session_before_switch", async (event, ctx) => {
  if (event.reason === "new") {
    const ok = await ctx.ui.confirm("清除会话?", "这将删除所有消息。");
    if (!ok) return { cancel: true };   // 取消操作
  }
});

pi.on("session_before_fork", async (event, ctx) => {
  const choice = await ctx.ui.select("确认 fork?", [
    "Yes, create fork", "No, stay in current session"
  ]);
  if (choice !== "Yes, create fork") return { cancel: true };
});

展示了 “在操作发生前拦截” 的模式——session_before_* 事件让你在 /new、/resume、/fork 之前插手。confirm() 返回布尔,select() 返回选中的字符串,return { cancel: true } 取消操作。

4. dirty-repo-guard.ts —— 有 Git 未提交改动时阻止切换

const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
if (code !== 0) return;                    // 不是 git 仓库,放过
if (!stdout.trim()) return;                // 干净的,放过

const changedFiles = stdout.trim().split("\n").filter(Boolean).length;
const choice = await ctx.ui.select(
  `你有 ${changedFiles} 个未提交文件,继续?`,
  ["Yes, proceed anyway", "No, let me commit first"]
);
if (choice !== "Yes") return { cancel: true };

新增的知识点:pi.exec() 在 extension 中执行 shell 命令,以及 ctx.hasUI 判断当前是否交互模式(非交互模式可以直接 block 而不弹窗)。

5. git-checkpoint.ts —— 自动 Git 检查点

const checkpoints = new Map<string, string>();  // entryId → stash ref

pi.on("turn_start", async () => {
  const { stdout } = await pi.exec("git", ["stash", "create"]);
  if (stdout.trim() && currentEntryId) {
    checkpoints.set(currentEntryId, stdout.trim());
  }
});

pi.on("session_before_fork", async (event, ctx) => {
  const ref = checkpoints.get(event.entryId);
  if (ref) {
    await pi.exec("git", ["stash", "apply", ref]);
    ctx.ui.notify("代码已恢复到检查点", "info");
  }
});

关键模式:turn_start 在 LLM 开始改代码前触发,创建 git stash 快照;event.entryId 把 stash ref 和 session entry 关联起来;fork 时通过 event.entryId 找回对应的快照恢复代码。

6. tool-override.ts —— 覆写内置 read 工具

pi.registerTool({
  name: "read",   // 同名 = 覆写内置 read
  async execute(_id, params, _s, _u, ctx) {
    const abs = resolve(ctx.cwd, params.path);

    if (isBlockedPath(abs))       // 1. 检查黑名单
      return { content: [{ type: "text", text: "Access denied!" }] };

    await logAccess(abs, true);   // 2. 记录日志
    const content = await readFile(abs, "utf-8");  // 3. 真正读取
    return { content: [{ type: "text", text: content }] };
  },
});

// 同时注册一个查看日志的命令
pi.registerCommand("read-log", { ... });

三个知识点:同名注册就是覆写——pi.registerTool({ name: "read" }) 替换内置 read;不写 renderCall/renderResult 的话,自动复用内置的渲染器(语法高亮、行号等);工具 + 命令组合——工具给 LLM 用,命令给用户用。

7. todo.ts —— 完整 Todo 工具(进阶必经之路)

这个稍大一些,但它包含了 extension 开发几乎所有该知道的东西。建议看完上面六个之后直接读这个:

怎么学

不需要把 75 个全看完。按这个路线筛选几个关键示例就够了:

hello.ts + model-status.ts    ——  上手,了解工具注册和事件监听
    ↓
permission-gate.ts + pirate.ts ——  拦截事件 + 修改行为,最实用的日常模式
    ↓
input-transform.ts + inline-bash.ts ——  接管用户输入
    ↓
question.ts + summarize.ts    ——  学会 ctx.ui.custom 做交互 UI
    ↓
todo.ts                       ——  理解状态持久化和完整工具系统
    ↓
handoff.ts + custom-compaction.ts ——  掌握会话控制和自定义压缩
    ↓
plan-mode/ + subagent/        ——  搭建完整功能系统

每层理解 1-2 个示例的核心模式就能写出同层级的 extension。从 hello.ts 开始,半小时到事件拦截,一个上午做到自定义 UI 交互。

附:放置位置和运行方式

位置 作用范围
~/.pi/agent/extensions/*.ts 所有项目
~/.pi/agent/extensions/*/index.ts 所有项目(子目录)
.pi/extensions/*.ts 当前项目(需 trust)
.pi/extensions/*/index.ts 当前项目(子目录)
pi -e ./my-ext.ts           # 快速测试
cp my-ext.ts ~/.pi/agent/extensions/  # 安装为全局

修改后 /reload 即可热重载(themes 修改后自动热重载,无需手动 reload)。