做出第一个 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/:
mkdir -p .pi/extensions创建 .pi/extensions/where.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 步:用临时参数测试
显式加载文件:
pi -e ./.pi/extensions/where.ts进入 Pi 后执行:
/where你应该看到当前目录、会话 ID 和信任状态。这个可见结果证明:
- Extension 文件被解析。
- 默认导出工厂已执行。
/where已注册。- 命令上下文和 TUI 通知可用。
如果启动时报错,先检查文件路径和导入名称。不要把文件移入全局目录来“碰碰运气”,那只会扩大影响范围。
第 3 步:启用自动发现与热重载
.pi/extensions/ 属于项目资源。确认源码可信后,在交互模式中信任项目并重启 Pi。
之后修改 where.ts,执行:
/reload再次运行 /where。自动发现位置中的 Extension 支持 /reload;pi -e 更适合一次性试验。
常见位置:
| 位置 | 作用域 |
|---|---|
~/.pi/agent/extensions/*.ts | 当前用户所有项目 |
~/.pi/agent/extensions/*/index.ts | 当前用户,多文件目录 |
.pi/extensions/*.ts | 当前项目 |
.pi/extensions/*/index.ts | 当前项目,多文件目录 |
-e <path/npm/git> | 当前进程临时加载 |
Extension 工厂的生命周期
默认导出接收 ExtensionAPI:
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 和结构化结果:
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 或策略沙箱。完整说明见权限与安全边界。
多文件与依赖
当单文件变得难以维护时,改成目录入口:
.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。
命令可用,但模型不知道工具
registerCommand 和 registerTool 是两种入口。命令属于用户界面;要让模型调用,必须注册工具并写清 description 与参数 schema。
Extension 在交互模式工作,在 Print 模式报错
检查是否无条件调用自定义 TUI、确认框或编辑器。用 ctx.mode 和 ctx.hasUI 做模式分支,并为无 UI 模式提供确定性行为。
下一步:
- 需要进程边界和自定义 UI:查看自动化与集成。
- 只需要工作说明:退回更轻的 Skill。
- 完整事件与 API:Pi Extensions 官方文档。