为什么需要给 Pi Agent 写扩展?
如果你已经用过 Pi Coding Agent,就会发现它是一个"小而美"的终端 AI 编程助手——核心只做一件事:把 LLM 的能力串进终端工作流。但真正让它变得"专属"的,是它的扩展系统(Extensions)。
Pi 的扩展是 TypeScript 模块,可以订阅生命周期事件、注册 LLM 可调用的自定义工具、添加斜杠命令、甚至自定义 TUI 组件。官方生态里已经有 pi-hosts(23 pts)、Parallel Pi agents(8 pts)等社区项目,但中文深度技术内容仍然稀缺。
如果你还没装 Pi,建议先看这篇入门:2026 年 5 大开源 AI 编程代理横评,里面有 Pi 的定位对比。本文则专注扩展开发,带你从零写出 4 个实战扩展。
Pi Agent 架构解析
核心组件
Pi 的架构可以用一句话概括:最小内核 + 可扩展边界。
┌─────────────────────────────────────────────┐
│ Pi 核心内核 │
│ ├─ LLM Provider 层(Anthropic/OpenAI/本地) │
│ ├─ Session 管理(会话、压缩、分支) │
│ ├─ 内置工具(bash、read、write、search…) │
│ └─ TUI 渲染(Ink/React) │
└─────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
Extensions Skills Packages
(TypeScript) (Markdown) (npm/git)
- Extensions:TypeScript 模块,运行时加载,能拦截事件、注册工具、添加命令。
- Skills:Markdown 文件,按需加载到上下文,定义工作流和提示词。
- Packages:通过 npm 或 git 分发的扩展集合。
插件系统原理
Pi 的扩展加载基于 jiti,无需编译即可直接运行 TypeScript。扩展的入口是一个默认导出的工厂函数,接收 ExtensionAPI 实例:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// 在这里订阅事件、注册工具、添加命令
}
扩展可以放在两个位置被自动发现:
| 位置 | 作用域 |
|---|---|
~/.pi/agent/extensions/*.ts |
全局(所有项目) |
.pi/extensions/*.ts |
项目本地(需信任项目) |
API 接口说明
ExtensionAPI 提供三类能力:
- 事件订阅:
pi.on("event_name", handler)— 覆盖从session_start到tool_call的完整生命周期。 - 工具注册:
pi.registerTool({ name, description, parameters, execute })— 让 LLM 能调用你的自定义函数。 - 命令注册:
pi.registerCommand("name", { handler })— 添加/name斜杠命令。
开发环境搭建
前置条件
- Node.js 18+(推荐 20 LTS)
- npm 或 pnpm
- Pi Coding Agent 已安装:
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
创建扩展项目
# 创建全局扩展目录
mkdir -p ~/.pi/agent/extensions/my-first-ext
cd ~/.pi/agent/extensions/my-first-ext
# 初始化 package.json
npm init -y
# 安装类型定义
npm install @earendil-works/pi-coding-agent typebox
# 创建入口文件
touch index.ts
目录结构
my-first-ext/
├── package.json
├── node_modules/
└── src/
└── index.ts # 入口
package.json 需要声明扩展入口:
{
"name": "my-first-ext",
"dependencies": {
"@earendil-works/pi-coding-agent": "^1.0.0",
"typebox": "^0.34.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}
第一个扩展:Hello World
基础版:通知 + 命令
// src/index.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// 会话启动时弹出通知
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Hello World 扩展已加载!", "info");
});
// 注册 /hello 命令
pi.registerCommand("hello", {
description: "向 Pi 打个招呼",
handler: async (args, ctx) => {
const name = args?.trim() || "World";
ctx.ui.notify(`Hello, ${name}! 👋`, "info");
},
});
}
测试运行
# 方式 1:自动发现(放在 ~/.pi/agent/extensions/ 下)
pi
# 方式 2:显式指定(临时测试)
pi -e ./src/index.ts
进入 Pi 后输入 /hello Kevin,你应该看到通知:Hello, Kevin! 👋。
进阶:拦截危险命令
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: "用户取消" };
}
});
这个扩展展示了 tool_call 事件的拦截能力——你可以在 LLM 执行工具前做权限检查。
实战项目一:自定义代码模板生成器
需求
团队有固定的代码模板(React 组件、Express 路由、Python 脚本等),每次新建文件都要复制粘贴。我们写一个扩展,让 LLM 能调用 generate_template 工具自动生成。
实现
// src/template-generator.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import * as fs from "node:fs";
import * as path from "node:path";
const TEMPLATES: Record<string, string> = {
"react-component": `import React from 'react';
interface {{Name}}Props {
title: string;
}
export const {{Name}}: React.FC<{{Name}}Props> = ({ title }) => {
return <div className="{{name}}">{title}</div>;
};
`,
"express-route": `import { Router, Request, Response } from 'express';
const router = Router();
router.get('/', (req: Request, res: Response) => {
res.json({ message: '{{Name}} endpoint' });
});
export default router;
`,
"python-script": `#!/usr/bin/env python3
"""{{Name}} module."""
import argparse
import sys
def main(args: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="{{Name}}")
parser.add_argument("--name", default="World")
parsed = parser.parse_args(args)
print(f"Hello, {parsed.name}!")
return 0
if __name__ == "__main__":
raise SystemExit(main())
`,
};
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "generate_template",
label: "模板生成器",
description: "根据模板名和组件名生成代码文件。可选模板:react-component, express-route, python-script",
parameters: Type.Object({
template: Type.String({ description: "模板名称" }),
name: Type.String({ description: "组件/模块名(PascalCase)" }),
outputPath: Type.String({ description: "输出文件路径" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const tpl = TEMPLATES[params.template];
if (!tpl) {
return {
content: [{ type: "text", text: `❌ 未知模板: ${params.template}` }],
details: {},
};
}
const pascalName = params.name.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase());
const kebabName = pascalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
const code = tpl
.replace(/\{\{Name\}\}/g, pascalName)
.replace(/\{\{name\}\}/g, kebabName);
const outPath = path.resolve(params.outputPath);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, code, "utf-8");
return {
content: [{ type: "text", text: `✅ 已生成 ${outPath}\n\n${code}` }],
details: { path: outPath, template: params.template },
};
},
});
}
使用方式
在 Pi 中直接说:
用 react-component 模板生成一个 UserProfile 组件,放到 src/components/UserProfile.tsx
Pi 会调用 generate_template 工具,自动完成变量替换和文件写入。
实战项目二:Git 工作流自动化
需求
每次提交前自动跑 lint、生成 commit message、推送后通知团队。我们用扩展拦截 bash 工具,在检测到 git commit 时注入前置检查。
实现
// src/git-workflow.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { execSync } from "node:child_process";
export default function (pi: ExtensionAPI) {
// 拦截 git commit,前置 lint
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return;
const cmd: string = event.input.command || "";
if (cmd.startsWith("git commit")) {
ctx.ui.setStatus("git-workflow", "🔍 运行 pre-commit 检查...");
try {
execSync("npm run lint --if-present", { stdio: "pipe" });
execSync("npm run test --if-present", { stdio: "pipe" });
ctx.ui.setStatus("git-workflow", "✅ 检查通过,继续提交");
} catch (err) {
ctx.ui.notify("❌ Lint/Test 失败,已阻止提交", "error");
return { block: true, reason: "pre-commit checks failed" };
}
}
// 拦截 git push,发送通知
if (cmd.startsWith("git push")) {
ctx.ui.setStatus("git-workflow", "📤 推送中...");
// 可以在这里调用 webhook 通知团队
}
});
// 注册 /git-summary 命令
pi.registerCommand("git-summary", {
description: "显示当前分支的提交摘要",
handler: async (_args, ctx) => {
try {
const log = execSync(
"git log --oneline -10 --format='%h %s (%an, %ar)'",
{ encoding: "utf-8" }
);
ctx.ui.notify(`最近 10 条提交:\n${log}`, "info");
} catch {
ctx.ui.notify("❌ 无法读取 git 历史", "error");
}
},
});
}
效果
- 当 Pi 尝试执行
git commit时,扩展自动跑 lint 和测试,失败则阻止提交。 /git-summary命令快速查看最近提交。
实战项目三:代码质量检查集成
需求
让 LLM 在写代码时自动调用外部质量检查工具(如 eslint、pylint、semgrep),并在结果中附带修复建议。
实现
// src/quality-checker.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { execSync } from "node:child_process";
type Linter = {
cmd: string;
pattern: RegExp;
};
const LINTERS: Linter[] = [
{ cmd: "npx eslint --format json", pattern: /\.(ts|tsx|js|jsx)$/ },
{ cmd: "pylint --output-format=json", pattern: /\.py$/ },
{ cmd: "semgrep --json", pattern: /.*/ },
];
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "check_code_quality",
label: "代码质量检查",
description: "对指定文件运行 lint/静态分析,返回问题列表和修复建议",
parameters: Type.Object({
filePath: Type.String({ description: "要检查的文件路径" }),
linter: Type.Optional(
Type.String({ description: "指定 linter(eslint/pylint/semgrep),留空自动检测" })
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const file = params.filePath;
let linter = LINTERS.find((l) => l.pattern.test(file));
if (params.linter) {
linter = LINTERS.find((l) => l.cmd.startsWith(params.linter!));
}
if (!linter) {
return {
content: [{ type: "text", text: "⚠️ 未找到匹配的 linter" }],
details: {},
};
}
ctx.ui.setStatus("quality", `🔍 运行 ${linter.cmd.split(" ")[0]}...`);
try {
const output = execSync(`${linter.cmd} ${file}`, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return {
content: [{ type: "text", text: `✅ 无问题\n${output}` }],
details: { file, linter: linter.cmd },
};
} catch (err: any) {
// linter 通常以非零退出码返回问题
const output = err.stdout || err.stderr || "";
return {
content: [{ type: "text", text: `⚠️ 发现问题:\n${output.slice(0, 3000)}` }],
details: { file, linter: linter.cmd, hasIssues: true },
};
}
},
});
}
使用方式
检查一下 src/auth/login.ts 的代码质量
Pi 会调用 check_code_quality,返回 lint 结果,然后自动给出修复建议。
实战项目四:自定义搜索工具集成
需求
让 Pi 能搜索团队内部知识库(比如 Notion、Confluence、或本地 Markdown 文件)。这里以本地 Markdown 知识库为例。
实现
// src/knowledge-search.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import * as fs from "node:fs";
import * as path from "node:path";
const KB_ROOT = process.env.KB_ROOT || "~/knowledge-base";
interface SearchResult {
file: string;
line: number;
context: string;
}
function searchMarkdown(query: string, root: string): SearchResult[] {
const results: SearchResult[] = [];
const terms = query.toLowerCase().split(/\s+/);
function walk(dir: string) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.name.endsWith(".md")) {
const content = fs.readFileSync(full, "utf-8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const lower = lines[i].toLowerCase();
if (terms.every((t) => lower.includes(t))) {
results.push({
file: path.relative(root, full),
line: i + 1,
context: lines.slice(Math.max(0, i - 1), i + 2).join("\n"),
});
}
}
}
}
}
walk(path.resolve(root));
return results.slice(0, 10);
}
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "search_knowledge_base",
label: "知识库搜索",
description: "在本地 Markdown 知识库中搜索相关内容",
parameters: Type.Object({
query: Type.String({ description: "搜索关键词" }),
kbRoot: Type.Optional(Type.String({ description: "知识库根目录,默认 ~/knowledge-base" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const root = params.kbRoot || KB_ROOT;
const resolved = path.resolve(root.replace("~/", process.env.HOME + "/"));
if (!fs.existsSync(resolved)) {
return {
content: [{ type: "text", text: `❌ 知识库目录不存在: ${resolved}` }],
details: {},
};
}
const results = searchMarkdown(params.query, resolved);
if (results.length === 0) {
return {
content: [{ type: "text", text: "🔍 未找到相关内容" }],
details: { query: params.query },
};
}
const formatted = results
.map((r) => `📄 ${r.file}:${r.line}\n${r.context}`)
.join("\n\n");
return {
content: [{ type: "text", text: `找到 ${results.length} 处匹配:\n\n${formatted}` }],
details: { query: params.query, count: results.length },
};
},
});
}
使用方式
设置环境变量 KB_ROOT=~/my-docs,然后在 Pi 中:
搜一下知识库里关于 API 认证的内容
Pi 会调用 search_knowledge_base,返回匹配的文件片段。
高级主题
多 Agent 协作
Pi 支持通过扩展实现多 Agent 模式。核心思路是:主 Agent 接收任务,拆分后委托给子 Agent(通过 bash 工具启动新的 pi 进程,使用 --resume 或 session 文件共享上下文)。
pi.registerTool({
name: "delegate_to_specialist",
label: "委托专家",
description: "将子任务委托给专门的 Agent 处理",
parameters: Type.Object({
task: Type.String({ description: "子任务描述" }),
specialist: Type.String({ description: "专家类型:frontend/backend/test" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const sessionFile = `/tmp/pi-${params.specialist}-${Date.now()}.json`;
const prompt = encodeURIComponent(params.task);
ctx.ui.setStatus("delegate", `🤖 委托 ${params.specialist} 处理...`);
try {
const output = execSync(
`pi --session ${sessionFile} --print "${params.task}"`,
{ encoding: "utf-8", timeout: 120_000 }
);
return {
content: [{ type: "text", text: output }],
details: { specialist: params.specialist, session: sessionFile },
};
} catch (err: any) {
return {
content: [{ type: "text", text: `❌ 委托失败: ${err.message}` }],
details: {},
};
}
},
});
性能优化
- 延迟加载:不要在工厂函数里启动后台进程,推迟到
session_start或首次工具调用。 - 缓存结果:对频繁调用的工具(如 lint),用 Map 缓存文件哈希和结果,避免重复执行。
- 流式更新:用
onUpdate回调实时反馈进度,避免 UI 卡死。
// 缓存示例
const cache = new Map<string, string>();
async execute(_toolCallId, params) {
const hash = crypto.createHash("md5").update(fs.readFileSync(params.filePath)).digest("hex");
const key = `${params.filePath}:${hash}`;
if (cache.has(key)) return { content: [{ type: "text", text: cache.get(key)! }], details: { cached: true } };
// ... 执行检查 ...
cache.set(key, result);
return { content: [{ type: "text", text: result }], details: {} };
}
发布和分享扩展
Pi 扩展可以通过 npm 或 git 仓库分发为 pi packages:
// package.json
{
"name": "pi-ext-my-tools",
"version": "1.0.0",
"pi": {
"extensions": ["./src/index.ts"]
}
}
用户安装:
# 从 npm
pi install npm:pi-ext-my-tools@1.0.0
# 从 git
pi install git:github.com/user/repo@v1
也可以在 pi.dev/packages 发布到官方包目录。
社区优秀扩展推荐
| 扩展 | 功能 | 链接 |
|---|---|---|
| pi-hosts | 管理 /etc/hosts,快速切换环境 | GitHub |
| Parallel Pi agents | 多 Agent 并行执行任务 | GitHub |
| pi-extensions (narumiruna) | 自动化、规划、浏览器控制、Git 工作流集合 | GitHub |
| pi-extensions-skill (Dwsy) | 渐进式扩展开发学习指南 | GitHub |
| pi-extension-builder (LobeHub) | 扩展脚手架生成器 | LobeHub |
常见问题 FAQ
Pi Agent 扩展和 Skills 有什么区别?
Extensions 是 TypeScript 模块,运行时加载,能拦截事件、注册工具、添加命令,拥有完整的系统权限。Skills 是 Markdown 文件,按需加载到 LLM 上下文,定义工作流和提示词,不能直接执行代码。简单说:Extension 是"代码级扩展",Skill 是"提示词级扩展"。
扩展开发需要会 TypeScript 吗?
需要基础 TypeScript 能力。Pi 用 jiti 直接加载 .ts 文件,无需编译步骤。如果你只会 JavaScript,也能写——把文件后缀改成 .js 即可,类型定义是可选的。
如何调试扩展?
用 pi -e ./my-ext.ts 启动,在代码里加 console.log,输出会显示在 Pi 的 TUI 底部。也可以用 ctx.ui.notify() 弹出通知。复杂逻辑建议写单元测试,用 Node.js 直接跑。
扩展会影响 Pi 的启动速度吗?
同步加载的扩展会阻塞启动。建议把耗时操作(网络请求、文件扫描)推迟到 session_start 事件或首次工具调用时执行。工厂函数里只做 pi.on() 和 pi.registerTool() 这类轻量注册。
可以在扩展里调用外部 API 吗?
可以。扩展运行在 Node.js 环境,能用 fetch、http、任何 npm 包。记得在 package.json 的 dependencies 里声明依赖。
总结
Pi Agent 的扩展系统给了开发者"改造 AI 助手"的完整能力。从拦截危险命令到注册自定义工具,从 Git 自动化到知识库搜索,TypeScript + 事件驱动 的组合让扩展既灵活又可控。
本文的 4 个实战项目覆盖了最常见的场景:模板生成、Git 工作流、代码质量、知识搜索。你可以直接拿去用,也可以作为模板改造出更多扩展。
下一步建议:
- 读官方 Extensions 文档
- 看 examples/extensions/ 里的示例
- 在 pi.dev/packages 找灵感,或发布你的第一个扩展