跳转到正文

做出第一个 Pi Extension

Extension 是运行在 Pi 进程中的 TypeScript 模块。它可以注册命令和工具、监听事件、拦截工具调用、保存会话状态,以及扩展终端 UI。

这份教程先实现一个没有外部副作用的 /where 命令。它显示当前工作目录、会话 ID 和项目是否受信任,让你先跑通完整加载链路。

先确认 Extension 是正确选择

Extension 能力很强,也意味着更高风险:

需求先选什么
告诉 Agent 项目规则AGENTS.md
复用一段提示词Prompt Template
执行一套按需步骤Skill
注册命令、工具或事件处理器Extension

Extension 与 Pi 同进程运行,拥有启动用户的文件、进程、网络和环境变量权限。不要用 Extension 解决一份 Markdown 就能解决的问题。

第 1 步:创建单文件 Extension

项目级 Extension 放在 .pi/extensions/

bash
mkdir -p .pi/extensions

创建 .pi/extensions/where.ts

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

export default function whereExtension(pi: ExtensionAPI) {
  pi.registerCommand("where", {
    description: "显示当前 Pi 会话的目录、ID 和项目信任状态",
    handler: async (_args, ctx) => {
      const sessionId = ctx.sessionManager.getSessionId();
      const trust = ctx.isProjectTrusted() ? "trusted" : "not trusted";

      ctx.ui.notify(
        [`cwd: ${ctx.cwd}`, `session: ${sessionId}`, `project: ${trust}`].join(
          "\n",
        ),
        "info",
      );
    },
  });
}

Pi 使用运行时 TypeScript 加载器,因此这个文件不需要预编译。

第 2 步:用临时参数测试

显式加载文件:

bash
pi -e ./.pi/extensions/where.ts

进入 Pi 后执行:

text
/where

你应该看到当前目录、会话 ID 和信任状态。这个可见结果证明:

  • Extension 文件被解析。
  • 默认导出工厂已执行。
  • /where 已注册。
  • 命令上下文和 TUI 通知可用。

如果启动时报错,先检查文件路径和导入名称。不要把文件移入全局目录来“碰碰运气”,那只会扩大影响范围。

第 3 步:启用自动发现与热重载

.pi/extensions/ 属于项目资源。确认源码可信后,在交互模式中信任项目并重启 Pi。

之后修改 where.ts,执行:

text
/reload

再次运行 /where。自动发现位置中的 Extension 支持 /reloadpi -e 更适合一次性试验。

常见位置:

位置作用域
~/.pi/agent/extensions/*.ts当前用户所有项目
~/.pi/agent/extensions/*/index.ts当前用户,多文件目录
.pi/extensions/*.ts当前项目
.pi/extensions/*/index.ts当前项目,多文件目录
-e <path/npm/git>当前进程临时加载

Extension 工厂的生命周期

默认导出接收 ExtensionAPI

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

export default function myExtension(pi: ExtensionAPI) {
  pi.on("session_start", async (_event, ctx) => {
    // 会话已经准备好
  });

  pi.registerCommand("example", {
    description: "示例命令",
    handler: async (_args, ctx) => {
      ctx.ui.notify("ready", "info");
    },
  });
}

工厂也可以是 async,Pi 会等待它完成再继续启动。异步工厂只适合一次性初始化,例如读取本地配置或发现模型。

不要在工厂中直接启动永不结束的计时器、socket、文件监听器或子进程。部分 Pi 命令只加载资源但不会开始会话。长期资源应在 session_start 或真正需要它的命令中启动,并在 session_shutdown 中幂等关闭。

从命令升级为自定义工具

命令由用户显式调用;工具可以由模型请求调用。注册工具时必须提供名称、说明、TypeBox 参数 schema 和结构化结果:

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

export default function wordCountExtension(pi: ExtensionAPI) {
  pi.registerTool({
    name: "count_words",
    label: "Count words",
    description: "Count whitespace-separated words in provided text",
    parameters: Type.Object({
      text: Type.String({ description: "Text to count" }),
    }),
    async execute(_toolCallId, params) {
      const count = params.text.trim()
        ? params.text.trim().split(/\s+/u).length
        : 0;

      return {
        content: [{ type: "text", text: String(count) }],
        details: { count },
      };
    },
  });
}

先从纯函数或只读能力开始。一个生产工具还需要:

  • 严格输入 schema 和业务校验。
  • AbortSignal 取消传播。
  • 超时、输出大小和并发限制。
  • 明确的错误结果。
  • 权限与租户检查。
  • 写操作确认和幂等键。

工具失败时抛出错误或返回明确的错误结果,不要把失败包装成“成功”的文本。

用事件做保护时要知道边界

Extension 可以监听 tool_call,并返回 { block: true, reason } 阻止某次工具调用。这适合团队工作流中的额外护栏,但不是操作系统安全边界:

  • 其他 Extension 仍是任意代码。
  • shell 子进程仍继承系统权限。
  • 规则可能漏掉等价命令、符号链接或编码变体。
  • 自定义工具可能绕开你只针对内置工具写的拦截。

真正处理不可信仓库、无人值守任务或高价值凭据时,使用容器、VM、微型 VM 或策略沙箱。完整说明见权限与安全边界

多文件与依赖

当单文件变得难以维护时,改成目录入口:

text
.pi/extensions/project-tools/
├── index.ts
├── commands.ts
├── tools.ts
└── policy.ts

需要第三方 npm 包时,可以在 Extension 目录或父目录放置 package.json 并安装依赖。

如果要分发成 Pi Package,运行时依赖必须放在 dependencies,不能只放在 devDependencies。Pi 安装 Package 时默认执行生产依赖安装。

验证清单

完成一个 Extension 后至少检查:

  • [ ] pi -e <path> 能启动,终端没有加载错误。
  • [ ] 用户命令或工具的理想路径可见且正确。
  • [ ] 非法输入、取消和异常路径有明确结果。
  • [ ] /reload 后没有重复注册或重复后台任务。
  • [ ] Print、JSON 或 RPC 模式下不会调用只存在于 TUI 的交互。
  • [ ] 不会在日志或工具结果中泄露凭据。
  • [ ] 项目不受信任时,行为符合预期。

可以通过 ctx.mode 判断 "tui""rpc""json""print",通过 ctx.hasUI 判断是否存在可对话的 UI。

常见问题

/reload 后看不到修改

  • 确认文件位于自动发现目录,而不是只通过旧的 -e 路径加载。
  • 查看启动时的 Extensions 列表和报错。
  • 如果修改了依赖或包结构,完全重启 Pi。

命令可用,但模型不知道工具

registerCommandregisterTool 是两种入口。命令属于用户界面;要让模型调用,必须注册工具并写清 description 与参数 schema。

Extension 在交互模式工作,在 Print 模式报错

检查是否无条件调用自定义 TUI、确认框或编辑器。用 ctx.modectx.hasUI 做模式分支,并为无 UI 模式提供确定性行为。

下一步:

非官方中文工程指南,内容以 Pi 上游文档与源码为准。