AI Research

pi 扩展开发实战手册

2026-06-05 #pi#cli-agent#extension#handbook#tutorial

pi 扩展开发实战手册

摘要

这是一本面向开发者的 cookbook,把 pi(earendil-works/pi)的四层扩展系统(skill / prompt template / extension / package)拆成”先讲机制 → 给最小可运行示例 → 列常见坑 → 推荐进阶”四步式章节,目标是让你看完就能动手写并发布。

本手册覆盖的 pi 版本:@earendil-works/pi-coding-agent 0.74.x – 0.78.x(manifest 字段、生命周期事件、CLI 子命令均按这一带版本)。已有的两份姊妹笔记 pi-deep-dive.md(深度研究 925 行)和 pi-agent-vs-claude-code-codex.md(横向对比)作为背景,本手册不再重复综述内容。

撰写过程同步抓取了官方 docs(packages/coding-agent/docs/*.md)、src/core/extensions/types.ts、loader 源码,以及社区扩展 pi-mcp-adapter 与 pi-subagents 的真实源码,每节”机制”部分尽量贴官方原文,可粘贴可运行的示例则按 docs 与社区代码风格组合而成。

2026-06-05 复核(共 9 处):原稿中标注的不确定点已通过抓取 src/core/extensions/types.ts、src/core/package-manager.ts、src/core/slash-commands.ts、src/core/prompt-templates.ts、src/cli/args.ts、src/package-manager-cli.ts、packages/ai/src/api-registry.ts、docs/skills.md、docs/packages.md、docs/extensions.md、docs/containerization.md 进行 verbatim 核实。

第一轮 5 处关键修正:tool_call event 没有 replaceWith 字段(只能 block + reason + 直接改 event.input);pi pkg create / pi extension dev 子命令确认不存在;getSystemPromptOptions() 是真实 API 但只在 ExtensionCommandContext 上;ToolDefinition 用 executionMode: "sequential" | "parallel" 而不是分开两个字段。详见 §6.3 / §6.4 / §6.6 / §7.3。

第二轮 4 处补充修正:(1) prompt template 替换还支持 ${@:N} ${@:N:L} bash 切片语法,并按顺序替换防递归——详见 §5;(2) slash command 没有显式优先级常量,SlashCommandSource 仅有 "extension" | "prompt" | "skill" 三类来源 + 21 个内置命令;真正的 resolve 在调用链推断——详见 §5.5;(3) 自定义 provider 的真实 API 是 registerApiProvider 而非 registerProvider,定义在 packages/ai/src/api-registry.ts,用 Map 实现而非 class;详见 §11.x;(4) context-mode ≠ pi 扩展——它是 mksglu/claude-context-mode(Claude Code 的 MCP 插件,Elastic License 2.0),原稿 §8.3 把它当 pi 扩展拆解是错误前提,已修正。


1. 导言:本手册写给谁

读完你能:

  1. 在 30 分钟内写一个 skill(最简单的层),给 pi 增加一段”按需加载”的工作流。
  2. 写一个 prompt template,把”你常打的那段话”变成一个 /foo 斜杠命令。
  3. 写一个 extension,注册自定义工具、监听 agent 生命周期、画 TUI panel、加快捷键、做进程间 RPC。
  4. 把上述三者打成一个 npm package,发布给别人 pi install npm:your-package 一键装上。
  5. 调试上面这套东西、知道哪里看 log、哪里 hot reload。

不写:模型 / provider 选型,session 文件格式,theme 调色盘,CLI 内部架构(这些去看 pi-deep-dive.md)。

前置假设:

  • 你写过一点 TypeScript(不一定精,但能读 import type { ... } from "...")。
  • 你装过 npm 全局包,知道 ~/ 是 home 目录。
  • 你大致知道”agent loop = 模型生成 → 工具调用 → 把结果塞回去再生成”是怎么回事。

2. 预备知识与环境搭建

2.1 安装 pi

1
npm install -g --ignore-scripts @earendil-works/pi-coding-agent

--ignore-scripts 是官方 quickstart 推荐写法,避免依赖在 install 时跑生命周期脚本。装完直接 pi 启动。

2.2 登录 / 配 key

1
2
3
4
5
6
7
# 选项 1:subscription(启动后 /login 选 Claude Pro / ChatGPT Plus / Copilot)
pi
> /login

# 选项 2:API key(启动前注入环境变量)
export ANTHROPIC_API_KEY=sk-ant-...
pi

凭据落到 ~/.pi/agent/auth.json。

2.3 目录结构(你只需要记三个位置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
~/.pi/agent/                  # 全局
├── settings.json # 全局设置
├── keybindings.json # 全局快捷键
├── auth.json # 凭据
├── pi-debug.log # 调试日志(/debug 命令产出)
├── AGENTS.md # 全局 system 上下文
├── extensions/ # 全局扩展(自动发现)
├── skills/ # 全局 skills(自动发现)
├── prompts/ # 全局 prompt templates
├── themes/ # 全局主题
└── npm/ # pi install 装的 npm 包落地处

<project>/.pi/ # 项目级(覆盖全局)
├── settings.json
├── extensions/
├── skills/
├── prompts/
└── themes/

<project>/AGENTS.md # 项目 system 上下文
<project>/CLAUDE.md # 兼容 Claude Code 的项目上下文

优先级:项目级 > 全局;同名 skill / extension 项目级覆盖全局。源码 core/package-manager.ts 中的 dedupePackages() 实现了这条规则——见 §7.7。

2.4 IDE 配置建议

1
2
3
4
5
6
7
8
9
// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifier": "non-relative",
"files.associations": {
"SKILL.md": "markdown",
"AGENTS.md": "markdown"
}
}

TypeScript 版本:5.4+ 即可(pi 内部用 jiti 加载 .ts,所以不需要你自己 tsc 编译;jiti 直接吃源码)。

2.5 本地 dev loop(最重要的一节)

三种 loop,从快到慢:

场景 命令 何时用
单文件试 pi -e ./my-extension.ts 写第一版、还没决定要不要发布
项目内安装 放进 <project>/.pi/extensions/<name>/ 跟项目绑定的 ext
全局安装 放进 ~/.pi/agent/extensions/<name>/ 自己日常用的 ext

热重载:在 pi 里敲 /reload,会重新发现并加载所有 extensions / skills / prompts,不会丢当前 session。

没有 pi extension dev / pi pkg create 这种子命令。pi CLI 截至 v0.78.x 的全部子命令是 install / remove / uninstall / update / list / config(见 src/package-manager-cli.ts 的 PackageCommand 联合类型)。dev loop 就是 pi -e <path>、改文件、/reload。

2.6 调试

1
2
3
/debug              # 启用 debug 输出
# 输出落到 ~/.pi/agent/pi-debug.log
# 内容:渲染过的 TUI 行(含 ANSI 码)+ 最近发给模型的消息

console.log / console.error 在 extension 里是有效的,会进 pi 的 log 通道(具体落点取决于运行模式:TUI 模式下被 capture,--mode json / --mode rpc 下被框成事件)。


3. 四层扩展系统速查

层 文件类型 写起来要 何时用 不能做什么
skill Markdown(SKILL.md) 5 分钟 把”复杂任务的步骤、checklist、参考代码”按需投喂给模型 不能跑代码、不能拦事件
prompt template Markdown(*.md) 2 分钟 把”你常打的那一大段 prompt”变成 /foo arg1 arg2 不能拦事件、不能注册工具
extension TypeScript 模块 30 分钟~几天 注册新工具、拦截 agent loop、画 TUI、做 RPC 写起来重,不适合纯 prompt 工程
package npm package 1 小时 把上面 1~3 打包发布 它本身只是容器

口诀:能用 skill 就别写 extension;能用 prompt template 就别写 skill;能 hardcode 就别拆 package。


4. 写一个 Skill

4.1 机制

skill 走的是 progressive disclosure:

  1. pi 启动时扫描所有 skill 位置(~/.pi/agent/skills/、<project>/.pi/skills/、packages、settings、CLI flag),只读 SKILL.md 的 frontmatter(name + description)。
  2. 把所有 skill 的 name + description 以 XML 格式拼进 system prompt,让模型”看到目录”。
  3. 模型决定要用某个 skill 时,再加载完整的 SKILL.md body 与配套文件。
  4. 用户也可以用 /skill:<name> 显式触发。

这是为了省 token:100 个 skill 摘要 ≈ 10K token,全文加载 ≈ 1M token,差一个数量级。

4.2 文件位置与命名

1
2
3
4
5
6
7
~/.pi/agent/skills/
└── code-review-checklist/
├── SKILL.md # 必须,frontmatter + 主体
├── reference/ # 可选
│ └── owasp-top10.md
└── scripts/ # 可选,skill 引用的辅助脚本
└── check-secrets.sh

frontmatter 字段(来自 docs/skills.md 的 Frontmatter 表,verbatim):

字段 必填 约束
name ✅ Max 64 chars. Lowercase a-z, 0-9, hyphens. Pi does not require this to match the parent directory(标准要求一致,pi 故意放宽)。
description ✅ Max 1024 chars. What the skill does and when to use it.
license ❌ License name or reference to bundled file.
compatibility ❌ Max 500 chars. Environment requirements.
metadata ❌ Arbitrary key-value mapping.
allowed-tools ❌ Space-delimited list of pre-approved tools (experimental)。
disable-model-invocation ❌ When true, skill 从 system prompt 隐藏,必须 /skill:name 才能用。

metadata 字段的语义已核实:docs 写明 *”Arbitrary key-value mapping”*,源码(core/skills.ts 的 SkillFrontmatter 与 Skill 接口)把它存为 frontmatter 透传字段,未知字段统一忽略——意思是 pi 自身不读这块,留给 skill 作者 / 三方扩展自由用。如果你的 extension 想消费它,需要自己读 SKILL.md frontmatter。

pi 比 Agent Skills 标准更宽容:skill name 可以与目录名不同(虽然 docs 说 “suboptimal for shared skill directories”),description 缺失才会拒绝加载,其他违规只 warn。

4.3 最小示例:code review checklist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!-- ~/.pi/agent/skills/code-review-checklist/SKILL.md -->
---
name: code-review-checklist
description: |
Use this skill when the user asks for a code review, PR review, or
"look over my changes". Walks through correctness, security, style,
and test coverage in that order, and outputs findings as a numbered
list with severity tags.
license: MIT
---

# Code Review Checklist

When invoked, perform these passes **in order**, stopping at the first
pass that turns up blocking issues.

## Pass 1: Correctness

- [ ] Are off-by-one / boundary cases covered?
- [ ] Any unhandled error / exception path?
- [ ] Concurrency: shared state, race conditions?

## Pass 2: Security

Read `reference/owasp-top10.md` for the canonical list. At minimum:

- [ ] Untrusted input flowing into shell / SQL / template?
- [ ] Hardcoded secrets? Run `scripts/check-secrets.sh` to scan.
- [ ] AuthZ checks in place for new endpoints?

## Pass 3: Style & Tests

- [ ] Naming consistent with surrounding file?
- [ ] New behavior covered by tests?
- [ ] Public API change reflected in docs?

## Output format

Return findings as:

[severity] file:line — short description
why it matters
suggested fix

1
2

severity ∈ {blocker, major, minor, nit}.

运行:

1
2
3
pi --skill ~/.pi/agent/skills/code-review-checklist
> /skill:code-review-checklist
> Review the staged changes.

或者放进 .pi/skills/ 后直接:

1
2
3
pi
> Review my staged changes for security issues
# 模型会从 system prompt 看到 description,自动加载这个 skill

4.4 进阶:与 prompt template 组合

skill 是”参考资料”,prompt template 是”启动咒语”。把 /review template 的 body 写成 “Use the code-review-checklist skill to review $@“,就能把模型的入口点固定下来:

1
2
3
4
5
6
7
<!-- ~/.pi/agent/prompts/review.md -->
---
description: Review changes using the canonical checklist
argument-hint: [path...]
---
Use the `code-review-checklist` skill. Targets: $@
If no targets given, use `git diff --cached`.

调用:/review src/api/auth.ts src/api/users.ts。

4.5 常见坑

  1. 命名冲突:同名 skill,pi 只保留先扫到的那个并 warn。项目级先扫,所以项目级 wins。但若两个全局位置(~/.pi/agent/skills/ 与 ~/.agents/skills/)都有同名 skill,行为依赖发现顺序,最好别撞名。
  2. description 太长:>1024 字符 pi 会 warn,且摘要会被截断进 system prompt,一旦截在关键词中间,模型可能就不调你了。
  3. token 预算:每个 skill 摘要 ~50-200 token,装 200 个 skill 就吃掉一两万 token。考虑用 settings 的 glob filter 关掉不用的。
  4. 加载顺序:skill body 是按需加载的,但摘要在 startup 时就一次扫完。新加 skill 必须 /reload 或重启。
  5. disable-model-invocation 用错地方:如果你只是想让 skill 的描述不出现在 system prompt 里、节省 token,但还希望模型能间接调到——做不到。设了这个标志后必须 /skill:name 显式触发。

4.6 推荐进阶

  • 翻 Anthropic 的 agent-skills repo 与 pi 自己 collection 看真实模板。
  • 把”长 reference 文档”放进 skill 子目录而不是 SKILL.md 主体,让模型按需 read 而不是一次塞光。
  • 在 allowed-tools 字段里只暴露 read / grep,做”只读 skill”,避免 agent 写文件。

5. 写一个 Prompt Template

5.1 机制

prompt template 是纯文本宏:

  • Markdown 文件,文件名 = 命令名(review.md → /review)。
  • 调用时 pi 把 frontmatter 之外的 body 展开成一条 user message 送给模型。
  • 支持参数替换:$1、$2、…、$@(全部)、$ARGUMENTS(同 $@)、${@:N}(从第 N 个开始)、${@:N:L}(从第 N 个起 L 个)。
  • 没有任何拦截 / 注册能力,纯展开。

防注入机制(2026-06-05 实测自 core/prompt-templates.ts):substituteArgs() 是纯字符串顺序替换,没有 shell escape、没有 HTML escape。模块对”参数值里恰好包含 $1 这种 pattern 的字符串”做了顺序处理防递归——一次替换完不会再扫第二遍,所以参数值里的 $2 不会触发对第二个参数的二次替换。但对 prompt 注入完全无防护:如果用户把 "; rm -rf /" 当 $1 传进来,pi 原样塞给模型;写 template 时不要把 $@ 直接喂进 shell 类指令的反引号,要么自己加引号,要么改用 skill 让模型显式 bash 调用,由 bash 工具做参数转义。

参数解析也在 core/prompt-templates.ts 的 parseCommandArgs() 里做:支持 bash-style 单/双引号,按空白分词。所以 /refactor "user account" "too long" 会得到 $1 = "user account"、$2 = "too long",而不是 4 个参数。

5.2 加载位置

1
2
~/.pi/agent/prompts/*.md           # 全局,非递归
<project>/.pi/prompts/*.md # 项目,非递归

子目录里的 template 默认不会被发现,要在 settings 或 package manifest 里显式列出。

5.3 何时用 template、何时用 skill

场景 选
我要把”那段我每天打 5 次的 prompt”变成 /foo template
我要给模型一份 checklist / 参考资料、希望它自己决定何时用 skill
我要传参数(<path>、<branch>) template
我要附带辅助脚本 / reference 文档 skill
我要拦截事件 / 改 tool result / 画 UI extension

5.4 最小示例:/refactor <function-name>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- ~/.pi/agent/prompts/refactor.md -->
---
description: Refactor a function with safety checks
argument-hint: <function-name> [reason]
---
Refactor the function named `$1` in the current project.

Goals:
- Preserve external behavior (tests must still pass).
- Reduce cyclomatic complexity if it's over 10.
- Replace magic numbers with named constants.

Reason for refactoring (free-form, may be empty): ${@:2}

Workflow:
1. `grep` to find the definition.
2. `read` the file to understand context.
3. Read all callers (`grep -n "$1("`).
4. Propose a diff.
5. Apply with `edit`.
6. Run the project's test command.

调用:

1
/refactor parseConfig "too long, hard to test"

替换结果(送给模型的):

1
2
3
4
Refactor the function named `parseConfig` in the current project.
...
Reason for refactoring (free-form, may be empty): too long, hard to test
...

运行命令:

1
2
3
4
pi --prompt-template ~/.pi/agent/prompts/refactor.md
# 或者放进 .pi/prompts/ 后直接
pi
> /refactor parseConfig

5.5 常见坑

  1. 文件名带连字符 / 大写:Refactor-Func.md 会变成命令 /Refactor-Func,pi 不会自动 lower-case。建议全 lower-snake-or-kebab。
  2. frontmatter 缺 description:依然能加载,但 / 自动补全里没有提示,别人不知道这命令干嘛——docs/prompt-templates.md 写明 *”if omitted, the first non-empty line serves this purpose”*。
  3. $@ 没人传参:会展开成空字符串,模型可能困惑。给个 if empty, use ... 的 fallback 句子。
  4. 递归发现问题:把 template 塞 prompts/team/code/refactor.md 默认发现不到,要么平铺,要么在 settings.json 配 path。
  5. 跟 skill 重名 / 与内置斜杠命令同名时的优先级(2026-06-05 复核):core/slash-commands.ts 只定义 SlashCommandSource = "extension" | "prompt" | "skill" 三类来源 + 21 条内置命令固定清单(new/resume/clone/fork/tree/settings/model/scoped-models/login/logout/export/import/share/copy/session/changelog/hotkeys/reload/quit 等),但没有公开的 priority 数值常量——优先级是按”内置 → extension → prompt → skill (/skill: 命名空间)”的注册/查表顺序隐式决定的。/skill: 前缀因为是命名空间不会跟 template 直接撞名;同名 prompt template 与 extension command 同时存在时,后注册者覆盖前者(基于 Map.set 语义)。建议加项目前缀 /myproj-refactor 避免歧义——尤其是当你的项目命令撞上未来 pi 新增的内置命令时(21 条内置清单可能在 v0.79+ 扩张)。

5.6 推荐进阶

  • 把 template 放到 package 里跟 extension 一起发;package 的 pi.prompts 字段可以指 prompt 目录。
  • 用 template 做”对话状态启动器”:body 里写 “你现在是 X 角色,第一句话先问我 Y”。
  • 写一个 meta template /template-new 把生成新 template 的过程也自动化。

6. 写一个 Extension(核心章节)

这是 pi 扩展系统真正强大的地方。skill / template 都是给模型读的文本,extension 是 TypeScript 代码,能拦截 agent loop 的每一步、注册新工具、画 UI、跟外部进程做 RPC。本节会把官方 docs 与 src/core/extensions/types.ts 里的事件 / API 表完整列出,再给两个完整可运行例子。

6.1 Extension 的 3 种形态

形态 文件结构 何时用
单文件 ~/.pi/agent/extensions/foo.ts 写脚本式逻辑
目录 ~/.pi/agent/extensions/foo/index.ts 拆成多文件
带依赖的 npm 包 foo/package.json 里有 "pi": { "extensions": ["./index.ts"] } 要 import 第三方依赖

加载机制(来自 src/core/extensions/loader.ts):

  • 用 jiti 动态 import,所以不需要预编译 TS。
  • 入口必须 export default function (pi: ExtensionAPI) { ... }。
  • 三个发现位置:项目级 <cwd>/.pi/extensions/、全局 agentDir/extensions/、settings 配的 path。
  • 入口规则(来自 core/package-manager.ts 的 resolveExtensionEntries()):先看 package.json 里的 pi.extensions 字段;找不到就回退到 index.ts / index.js。

6.2 Manifest 字段(package.json pi 块)

源码 core/package-manager.ts 的 PiManifest interface(v0.78.x verbatim):

1
2
3
4
5
6
interface PiManifest {
extensions?: string[];
skills?: string[];
prompts?: string[];
themes?: string[];
}

社区扩展 pi-mcp-adapter 的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "pi-mcp-adapter",
"version": "x.y.z",
"keywords": ["pi-package", "pi", "mcp", "extension"],
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"dependencies": {
"@earendil-works/pi-ai": "^0.74.0",
"@earendil-works/pi-tui": "^0.74.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"typebox": "^1.1.24"
},
"pi": {
"extensions": ["./index.ts"],
"video": "https://github.com/.../pi-mcp.mp4"
}
}

pi-subagents 把三层都打包:

1
2
3
4
5
6
7
{
"pi": {
"extensions": ["./src/extension/index.ts"],
"skills": ["./skills"],
"prompts": ["./prompts"]
}
}

注意:PiManifest 源码里没有 video / image 字段——这两个是 gallery 元数据,被 npm 读到 pi.video / pi.image 即可,pi 自身的资源加载器忽略它们。

核心 deps 要放 peerDependencies:pi-coding-agent 自己管 @earendil-works/pi-coding-agent / pi-agent-core / pi-ai,extension 不要 bundle,否则版本错位时会出现两份运行时。

6.3 完整生命周期事件表(含返回值精确 TS 类型)

本节所有 return 类型直接抓自 packages/coding-agent/src/core/extensions/types.ts(v0.78.x 时点)。最重要的修正:

  • tool_call event 没有 replaceWith 字段!只能 block + reason;要修改工具入参的话,直接 mutate event.input in place(types.ts 注释原文:”event.input is mutable. Mutate it in place to patch tool arguments before execution”)。
  • tool_result event 的返回值是 { content?, details?, isError? },不是 replaceWith。
  • input event 用 tagged union { action: "continue" | "transform" | "handled" }。
  • user_bash event 是 { operations?, result? },没有 cancel。
  • message_end 用 { message? }(替换整条 message 必须保留原 role)。
事件 触发时机 精确 return 类型(types.ts) 行为
resources_discover 启动扫描 skills / prompts / themes 时 ResourcesDiscoverResult { skillPaths?, promptPaths?, themePaths? } 追加额外资源路径
session_start session 开始(含 fork、resume 后) — (handler 不返回) 只读
session_before_switch 准备切到另一个 session SessionBeforeSwitchResult { cancel?: boolean } 可阻断
session_before_fork 准备 fork SessionBeforeForkResult { cancel?, skipConversationRestore? } 可阻断
session_before_compact 准备压缩对话历史 SessionBeforeCompactResult { cancel?, compaction? } 可阻断 / 可整段替
session_compact compact 实际发生 — 只读
session_before_tree 进 session tree 视图前 SessionBeforeTreeResult { cancel?, summary?, customInstructions?, replaceInstructions?, label? } 可阻断 / 可改参
session_tree 进入后 — 只读
session_shutdown session 结束 — 做清理
context 准备发 LLM 时 ContextEventResult { messages? } 替换 messages 数组
before_provider_request 即将向 provider 发请求 BeforeProviderRequestEventResult = unknown 改请求体
after_provider_response provider 响应回来后 — 只读
before_agent_start agent loop 即将开始 BeforeAgentStartEventResult { message?, systemPrompt? } 注入 message / 替 system prompt(多扩展会被 chain)
agent_start / agent_end agent loop 起止 — 只读
turn_start / turn_end turn 起止 — 只读
message_start / message_update 模型流式 chunk — 只读
message_end 一条 message 流完 MessageEndEventResult { message? } 替换整条 message(必须保留 role)
tool_call 执行前模型决定调一个工具 ToolCallEventResult { block?, reason? } 改入参靠 mutate event.input,不靠 return
tool_result 工具执行完毕 ToolResultEventResult { content?, details?, isError? } 改写结果
tool_execution_start/update/end 工具实际执行的 3 个时点 — 只读
model_select 用户 / 系统选模型 — (无 ResultType;返回值被忽略) 只读 / 监听
thinking_level_select 调 thinking level — 同上
user_bash 用户 !cmd 跑 shell UserBashEventResult { operations?, result? } 换执行后端或直接给结果
input 用户提交 message(未送 agent) InputEventResult = { action: "continue" } | { action: "transform"; text; images? } | { action: "handled" } 拦 / 改 / 完全接管

要点:

  • 能阻断 + 能改写的事件主要是 tool_call(block)/ input(handled / transform)/ user_bash(result)/ before_agent_start(systemPrompt)。
  • 改 tool 入参必须用 in-place mutation:event.input.command = sanitize(event.input.command),不是 return { input: ... }。
  • 改 tool 输出用 tool_result 的 content / details / isError。
  • 上下文型事件(context / before_agent_start / resources_discover)的返回值是追加(多扩展会被 chain)而不是替换。

ToolCallEvent 是按工具名分支的 union(来自 types.ts):

1
2
3
4
5
6
7
8
9
export type ToolCallEvent =
| BashToolCallEvent
| ReadToolCallEvent
| EditToolCallEvent
| WriteToolCallEvent
| GrepToolCallEvent
| FindToolCallEvent
| LsToolCallEvent
| CustomToolCallEvent;

所以你做 event.toolName === "bash" 判断后能拿到带 typed event.input(如 BashToolCallEvent.input.command)。

6.4 ctx:你能拿到什么

来自 types.ts 的 ExtensionContext(v0.78.x,verbatim):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export interface ExtensionContext {
ui: ExtensionUIContext;
mode: ExtensionMode; // "tui" | "rpc" | "json" | "print"
hasUI: boolean; // dialog-capable UI(TUI / RPC 下 true)
cwd: string;
sessionManager: ReadonlySessionManager;
modelRegistry: ModelRegistry;
model: Model<any> | undefined;
isIdle(): boolean;
signal: AbortSignal | undefined;
abort(): void;
hasPendingMessages(): boolean;
shutdown(): void;
getContextUsage(): ContextUsage | undefined;
compact(options?: CompactOptions): void;
getSystemPrompt(): string; // 当前已组装好的 system prompt 文本
}

getSystemPromptOptions() 在哪? 它是真实存在的 API,但只挂在 ExtensionCommandContext 上(命令 handler 拿到的、ExtensionContext 的超集)。verbatim:

1
2
3
4
5
6
7
8
9
10
11
export interface ExtensionCommandContext extends ExtensionContext {
/** Get the current base system-prompt construction options. */
getSystemPromptOptions(): BuildSystemPromptOptions;

/** Wait for the agent to finish streaming */
waitForIdle(): Promise<void>;

/** Start a new session, optionally with initialization. */
newSession(options?: { ... }): Promise<{ cancelled: boolean }>;
// …还有 fork / switchSession / navigateTree / reload
}

差别:

  • getSystemPrompt() 给你已组装好的最终字符串,事件 handler 与命令 handler 都能调。
  • getSystemPromptOptions() 给你还没组装的结构化选项(BuildSystemPromptOptions 来自 core/system-prompt.ts),用来在 before_agent_start 之类的事件里看 / 改 system prompt 的”原料”——但只能在命令 handler 里调。在事件 handler 里要拿原料,看 BeforeAgentStartEvent.systemPromptOptions(事件直接给你了,types.ts 里这条 event 自带 systemPromptOptions: BuildSystemPromptOptions 字段)。

ExtensionCommandContext 比 ExtensionContext 多的 methods(命令 handler 专属):

  • getSystemPromptOptions(): BuildSystemPromptOptions
  • waitForIdle(): Promise<void>
  • newSession(options?) / fork(options?) / switchSession(...) / navigateTree(...)
  • reload() —— 重新发现 / 加载所有扩展

6.5 ExtensionAPI 注册接口(pi 参数)

来自 types.ts(v0.78.x verbatim 摘录):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export interface ExtensionAPI {
// —— 事件订阅 ——
on(event: "resources_discover",
handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
on(event: "tool_call",
handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
on(event: "input",
handler: ExtensionHandler<InputEvent, InputEventResult>): void;
on(event: "context",
handler: ExtensionHandler<ContextEvent, ContextEventResult>): void;
// …其余事件同模式

// —— 注册 ——
registerTool<TParams extends TSchema, TDetails, TState>(
tool: ToolDefinition<TParams, TDetails, TState>,
): void;
registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void;
registerShortcut(shortcut: KeyId, options: { /* ... */ }): void;
registerFlag(name: string, options: { /* ... */ }): void;
getFlag(name: string): boolean | string | undefined;
registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void;

// —— 行为 ——
sendMessage<T>(message, options?): void;
sendUserMessage(content, options?): void;
appendEntry<T>(customType: string, data?: T): void;

// —— session 元数据 ——
setSessionName(name: string): void;
getSessionName(): string | undefined;
setLabel(entryId: string, label: string | undefined): void;
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;

// —— tool 状态 ——
getActiveTools(): string[];
getAllTools(): ToolInfo[];
setActiveTools(toolNames: string[]): void;
getCommands(): SlashCommandInfo[];

// —— model / thinking ——
setModel(model: Model<any>): Promise<boolean>;
getThinkingLevel(): ThinkingLevel;
setThinkingLevel(level: ThinkingLevel): void;

// —— provider ——
// 注:这两个方法是 ExtensionContext 包装层。底层调的是
// packages/ai/src/api-registry.ts 的 `registerApiProvider(provider, sourceId?)`
// —— Map 实现,不是 class;详见 §11 的 ProviderConfig 备注。
registerProvider(name: string, config: ProviderConfig): void;
unregisterProvider(name: string): void;

// —— 共享 event bus ——
events: EventBus;
}

// 通用 handler 签名:
export type ExtensionHandler<E, R = undefined> =
(event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;

6.6 注册自定义工具

ToolDefinition 接口(v0.78.x verbatim 摘自 types.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export interface ToolDefinition<
TParams extends TSchema = TSchema,
TDetails = unknown,
TState = any,
> {
name: string;
label: string;
description: string;
promptSnippet?: string;
promptGuidelines?: string[];
parameters: TParams;
renderShell?: "default" | "self";
prepareArguments?: (args: unknown) => Static<TParams>;

/**
* Per-tool execution mode override.
* - "sequential": this tool must execute one at a time with other tool calls.
* - "parallel": this tool can execute concurrently with other tool calls.
*
* If omitted, the default execution mode applies.
*/
executionMode?: ToolExecutionMode;

execute(
toolCallId: string,
params: Static<TParams>,
signal: AbortSignal | undefined,
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
ctx: ExtensionContext,
): Promise<AgentToolResult<TDetails>>;

renderCall?: (args, theme, context) => Component;
renderResult?: (result, options, theme, context) => Component;
}

关键修正:并发控制是单一字段 executionMode: "sequential" | "parallel",不是两个分开的 sequential / parallel boolean。原稿里描述错了。

具体用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Type } from "typebox";

pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does",
promptSnippet: "100 char teaser",
parameters: Type.Object({
path: Type.String({ description: "Target file path" }),
dryRun: Type.Optional(Type.Boolean()),
}),
executionMode: "sequential", // 关键 IO 用 sequential;纯查询用 "parallel"

async execute(toolCallId, params, signal, onUpdate, ctx) {
onUpdate?.({ kind: "text", text: "Working..." });
if (signal?.aborted) throw new Error("aborted");
// ... 真实工作 ...
return {
content: [{ type: "text", text: "Done." }],
details: { /* 任意 JSON,给 renderResult 用 */ },
};
},

renderCall: (params, theme, ctx) => /* Component */,
renderResult: (result, options, theme, ctx) => /* Component */,
});

注意:

  • 参数 schema 用 TypeBox,不是 zod。pi 内部把 TypeBox schema 转成 JSON Schema 喂给 LLM。如果你需要 enum,用 StringEnum from @earendil-works/pi-ai 而不是 Type.Union(Type.Literal(...)),这是 docs 强调的兼容性细节(Google API 会拒绝某些 OneOf)。
  • 输出截断是必须的,docs 写明 50KB 或 2000 行上限——超过会破坏 compaction 逻辑、把 context 撑爆。
  • 工具与原子工具(read/write/edit/bash)共存,名字别冲突;要替换 read/write 行为推荐用 tool_call event 拦截而不是同名 register。
  • ctx 在 execute 里跟事件 handler 拿到的 ExtensionContext 同接口(不是 ExtensionCommandContext)。

6.7 添加 UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在 handler 或命令里
ctx.ui.notify("Saved!", "info"); // info | warning | error
const ok = await ctx.ui.confirm("Run prettier?", "This formats all .ts files");
const choice = await ctx.ui.select("Pick one", ["a", "b", "c"]);
const text = await ctx.ui.input("Branch name?", "main");

// 自定义 panel(来自 docs/tui.md + types.ts ExtensionUIContext)
ctx.ui.custom((tui, theme, keybindings, done) => ({
render: (width: number) => [
theme.fg("accent", "Custom Panel"),
theme.fg("muted", "Press q to close"),
],
invalidate: () => { /* 清缓存 */ },
handleInput: (data: string) => {
if (data === "q") done();
},
}), { overlay: true });

ExtensionUIContext 关键 method(types.ts 摘录):select / confirm / input / notify / setStatus / setWorkingMessage / setWidget / setFooter / setHeader / setTitle / custom / pasteToEditor / setEditorText / getEditorText / editor / addAutocompleteProvider / setEditorComponent / theme(只读)/ getAllThemes / setTheme / getToolsExpanded / setToolsExpanded。

@earendil-works/pi-tui 里能 import 的内置组件:Text、Box、Container、SelectList、SettingsList、Markdown、按键工具 matchesKey / Key、宽度工具 visibleWidth / truncateToWidth / wrapTextWithAnsi。

守则:自定义渲染必须用 callback 里的 theme,不要直接 import { theme },否则用户切主题你不会跟着变。

6.8 添加 slash commands & 快捷键

1
2
3
4
5
6
7
8
9
10
11
pi.registerCommand("hello", {
description: "Say hi",
handler: async (args, ctx) => {
ctx.ui.notify(`Hi ${args || "world"}!`);
},
});

pi.registerShortcut("ctrl+shift+h", {
description: "Greet",
handler: async (ctx) => { /* ... */ },
});

跟内置命令同名时用户级 keybindings.json 优先级最高,extension 注册的会被 shadow——这是 docs/keybindings.md 说的”用户可改任何 binding”的副作用。

6.9 RPC:扩展跟外部进程通信

pi 的 RPC 主要面向外部客户端控制 pi(headless mode,stdin/stdout 走 JSON),不是 extension 之间互相喊话。但 extension 在 RPC 模式下可以发起 extension_ui_request 让外部 UI 帮你画对话框(参见 docs/rpc.md)。

extension ↔ 外部进程的常见做法:

  1. extension 里 child_process.spawn 起一个本地进程(比如 MCP server、prettier daemon)。
  2. session 结束时在 session_shutdown handler 里 graceful kill。
  3. 跨 extension 共享状态用 pi.appendEntry() 写 session log,下次读回。

pi-mcp-adapter 的真实做法(节选自 index.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pi.on("session_start", async (_event, ctx) => {
const generation = ++lifecycleGeneration;
// 关掉上一次 session 留下的 MCP servers
await Promise.all([
shutdownState(previousState, "session_restart"),
shutdownOAuth(),
]);
// 启动新的
const promise = initializeMcp(pi, ctx);
initPromise = promise;
});

pi.on("session_shutdown", async () => {
await Promise.all([
shutdownState(state, "session_shutdown"),
shutdownOAuth(),
]);
});

lifecycleGeneration 这个 monotonic counter 是经典做法:在 session 反复重启时丢弃”已经过期”的初始化结果,避免 race。

6.10 完整最小示例:auto-format on save

需求:每次 agent 用 write 或 edit 修改 .ts / .tsx / .js / .jsx / .json / .md 文件后,自动跑 prettier --write,把格式化结果当成”额外的工具结果”显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// ~/.pi/agent/extensions/auto-format/index.ts
//
// Listens for write/edit tool results, runs prettier on the touched file,
// and appends a tiny notice. Skips files prettier doesn't support.
//
// Run standalone: pi -e ~/.pi/agent/extensions/auto-format/index.ts
// Or drop it into ~/.pi/agent/extensions/auto-format/ and `pi`.

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import * as path from "node:path";

const exec = promisify(execFile);

const FORMATTABLE = new Set([
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
".json", ".jsonc", ".md", ".mdx", ".yml", ".yaml",
".css", ".scss", ".html",
]);

export default function autoFormat(pi: ExtensionAPI) {
pi.registerFlag("auto-format-disabled", {
description: "Disable auto-format on save",
type: "boolean",
});

pi.on("tool_result", async (event, ctx) => {
if (pi.getFlag("auto-format-disabled")) return;

// Only react to write / edit
const toolName = event.toolName;
if (toolName !== "write" && toolName !== "edit") return;

// Find the touched path. write/edit conventionally take `file_path`.
const filePath: string | undefined =
(event as any).input?.file_path ?? (event as any).input?.path;
if (!filePath) return;

const ext = path.extname(filePath).toLowerCase();
if (!FORMATTABLE.has(ext)) return;

const abs = path.isAbsolute(filePath)
? filePath
: path.join(ctx.cwd, filePath);

try {
await exec("npx", ["--no-install", "prettier", "--write", abs], {
cwd: ctx.cwd,
signal: ctx.signal,
timeout: 15_000,
});
if (ctx.hasUI) {
ctx.ui.notify(`prettier: formatted ${path.basename(abs)}`, "info");
}
} catch (err) {
// prettier missing or failed — don't blow up the agent loop
if (ctx.hasUI) {
const msg = err instanceof Error ? err.message : String(err);
ctx.ui.notify(`prettier skipped: ${msg.slice(0, 120)}`, "warning");
}
}

// We don't need to return anything; tool_result returns can mutate
// result content via { content, details, isError }, but here we just observe.
});

pi.registerCommand("autoformat", {
description: "Toggle auto-format on save",
handler: async (_args, ctx) => {
ctx.ui.notify("Toggle via --auto-format-disabled flag for now", "info");
},
});
}

运行:

1
pi -e ~/.pi/agent/extensions/auto-format/index.ts

或者放进 ~/.pi/agent/extensions/auto-format/(把上面文件命名为 index.ts)然后 pi 直接启动。

测试:在 pi 里说 “create a file tmp.ts that prints ‘hi’ badly indented”,等模型 write 完,你会看到右下角 notify 弹”prettier: formatted tmp.ts”,文件被自动格式化。

6.11 进阶示例:简化版 plan-mode

需求:拦截所有写操作(write / edit / bash),先把”接下来要做什么”以 plan 形式打出来,等用户在 UI 里确认才放行。

修正(2026-06-05):原稿这里用了 tool_call 的 replaceWith 字段——但 ToolCallEventResult 没有这个字段(types.ts 只定义了 block?: boolean 和 reason?: string)。要给模型一个”被拦了为什么”的反馈,靠 reason 字符串。要把”被拦”显示成更复杂的 message,可以在拦截后调 pi.sendMessage() 注入一条 custom message。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ~/.pi/agent/extensions/plan-gate/index.ts
//
// A toy "plan mode": for every destructive tool call, ask the user
// (via TUI confirm) before letting it through. Skips read-only tools.
//
// Run: pi -e ~/.pi/agent/extensions/plan-gate/index.ts

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

const DESTRUCTIVE = new Set(["write", "edit", "bash"]);
let armed = false;

export default function planGate(pi: ExtensionAPI) {
pi.registerCommand("plan", {
description: "Toggle plan-gate (confirm before write/edit/bash)",
handler: async (_args, ctx) => {
armed = !armed;
ctx.ui.notify(`plan-gate: ${armed ? "ON" : "OFF"}`, "info");
},
});

pi.on("tool_call", async (event, ctx) => {
if (!armed) return;
if (!DESTRUCTIVE.has(event.toolName)) return; // read-only: pass

if (!ctx.hasUI) {
// Non-interactive run: just block.
return {
block: true,
reason: "plan-gate blocked: destructive tool calls require TUI confirmation.",
};
}

const summary = `${event.toolName}(${JSON.stringify(event.input).slice(0, 200)})`;
const ok = await ctx.ui.confirm(
`Approve: ${summary}?`,
"plan-gate is armed; press y to allow this tool call.",
);
if (!ok) {
return {
block: true,
reason: "User declined the action.",
};
}
// Returning nothing lets the call proceed.
});
}

运行:

1
2
3
4
pi -e ~/.pi/agent/extensions/plan-gate/index.ts
> /plan
> refactor src/api/auth.ts to use async/await
# 每次 write/edit/bash 都会弹确认

这就是为什么 pi 把”plan mode”故意没做成内置——50 行 extension 就能自己写一个,且行为完全可控。

顺手提一句:要改 tool 入参而不是拦截,直接 mutate event.input:

1
2
3
4
5
pi.on("tool_call", (event) => {
if (event.toolName === "bash") {
event.input.command = event.input.command.replace(/rm -rf \//g, "echo blocked");
}
});

6.12 常见坑

  1. handler 阻塞 agent loop:tool_call handler 是 await 的,handler 里 sleep 30 秒,agent 就卡 30 秒。长任务请 setImmediate(() => ...),把”通知 / 写日志”挪到后台。
  2. 拿 tool_call 的 replaceWith / cancel:没有这俩字段!只能 { block: true, reason: "..." }。要改入参:mutate event.input。要替换”被拦后给模型的反馈”:自己 pi.sendMessage 一条 custom message。
  3. ctx.signal 不监听:用户 ctrl-C,你的工具继续跑、UI 还在转圈。execute 长任务必须周期性 if (signal?.aborted) throw new Error("aborted");。
  4. session 切换时残留状态:如 6.9 节展示的 lifecycleGeneration 模式,在 session_start 里要 nuke 之前的状态、并防止”上次的 init 结果回来后污染本次”。
  5. tool_result 改写 size:能减小、不能增大太多,否则触发 token 截断逻辑出怪现象(docs 强调 50KB / 2000 行)。
  6. TS 编译陷阱:jiti 跑你的源码,不会做 strict 类型检查。意思是 as any 和 implicit any 都过;自己 IDE 配 tsc --noEmit 做静态检查,否则 bug 很难发现。
  7. 依赖装错位置:@earendil-works/pi-coding-agent 应该在 peerDependencies 而不是 dependencies;放 dependencies 用户装包时会拉一份独立 runtime。
  8. registerProvider 顺序:provider 必须在 session_start 之前注册;registerProvider 调用本身可以放在 factory 同步代码里,但生效要等 ctx 起来。
  9. getSystemPromptOptions() 在事件 handler 里报 undefined:那是因为它只挂在 ExtensionCommandContext 上。事件 handler 里要拿 system prompt 原料,看 BeforeAgentStartEvent.systemPromptOptions。

6.13 推荐进阶

  • 读 pi-mcp-adapter 的 index.ts(约 250 行)学:lifecycle generation、direct tool 注册、proxy tool 模式、CLI flag 注入。
  • 读 pi-subagents 的 src/extension/index.ts(约 22 KB)学:怎么把”subagent 任务”做成自定义 tool(一个 tool 名 subagent,靠参数分 single / chain / parallel / list / get / create / update / delete / status / interrupt / resume / doctor 多种动作)、注册三个 message renderer、监听 pi.events.on(SUBAGENT_ASYNC_*)、做 slash bridge。
  • 给你的 extension 加 ctx.appendEntry() 持久化,这样 pi 重启之后 todos / 计数器还在。

7. 写一个 Package

7.1 目录结构

一个 package 同时可以含 extensions / skills / prompt templates / themes:

1
2
3
4
5
6
7
8
9
10
11
12
13
my-pi-pack/
├── package.json
├── README.md
├── extensions/
│ └── auto-format/
│ └── index.ts
├── skills/
│ └── code-review-checklist/
│ └── SKILL.md
├── prompts/
│ └── refactor.md
└── themes/
└── solarized.json

package.json manifest(手写):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"name": "my-pi-pack",
"version": "0.1.0",
"description": "My personal pi pack: auto-format + code review",
"keywords": ["pi-package", "pi"], // 必须有 pi-package 才会被 gallery 收
"license": "MIT",
"peerDependencies": {
"@earendil-works/pi-coding-agent": ">=0.74.0"
},
"dependencies": {
"typebox": "^1.1.24"
},
"pi": {
"extensions": ["./extensions/auto-format/index.ts"],
"skills": ["./skills"], // 见 §7.7:指目录会扫子目录里的 SKILL.md
"prompts": ["./prompts"],
"themes": ["./themes/solarized.json"],
"video": "https://example.com/demo.mp4" // gallery 字段,资源加载器忽略
},
"files": [
"extensions",
"skills",
"prompts",
"themes",
"README.md"
]
}

如果你不写 pi 字段,pi 会自动发现 extensions/、skills/、prompts/、themes/ 这些约定目录(来自 core/package-manager.ts 的 collectPackageResources() fallback 分支)。所以小包可以直接省略 manifest。

7.2 4 种分发方式(直接抓自 docs/packages.md)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1) npm 注册表
pi install npm:@foo/bar@1.0.0
pi install npm:my-pi-pack # 不指定版本走 latest

# 2) git
pi install git:github.com/user/repo@v1
pi install https://github.com/user/repo
pi install ssh://git@github.com/user/repo
pi install git:git@github.com:user/repo@v1.0.0

# 3) 本地路径
pi install /absolute/path/to/package
pi install ./relative/path/to/package

# 4) 内置(pi 自带)
# 不需要装,extension 通过 settings.json 启用即可

加 -l flag 表示项目级安装(落 <project>/.pi/npm/),不加就是全局(落 ~/.pi/agent/npm/)。源码 core/package-manager.ts 的 getManagedNpmInstallPath():

1
2
3
4
5
6
7
8
9
private getManagedNpmInstallPath(source: NpmSource, scope: SourceScope): string {
if (scope === "temporary") {
return join(this.getTemporaryDir("npm"), "node_modules", source.name);
}
if (scope === "project") {
return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name);
}
return join(this.agentDir, "npm", "node_modules", source.name);
}

7.3 管理命令(v0.78.x verbatim 抓自 src/package-manager-cli.ts)

1
export type PackageCommand = "install" | "remove" | "update" | "list";

加上 alias uninstall 和独立的 config 命令,CLI 子命令的完整列表就是这些(args.ts 的 help 文本同步):

1
2
3
4
5
6
7
8
9
10
pi install <source> [-l]      # 装
pi remove <source> [-l] # 卸
pi uninstall <source> [-l] # remove 的 alias
pi list # 列已装
pi update # 升 pi 自己 + 所有 package
pi update --extensions # 只升 package
pi update --self # 只升 pi
pi update --self --force # 强制重装 pi
pi update <source> # 升单个
pi config # 交互式开/关 package 资源

经源码核实,pi 当前 (v0.78.x) 没有以下子命令:

  • ❌ pi pkg create —— 不存在。用 npm init -y 起项目,按 §7.1 写 package.json 的 pi 字段即可。
  • ❌ pi pkg publish —— 不存在。**用 npm publish**。
  • ❌ pi extension dev —— 不存在。**dev loop 是 pi -e <path> + /reload**(见 §2.5)。
  • ❌ pi pkg <anything> —— 整个 pkg 子命令族不存在。

这是用户 brief 里的笔误或外部资料过时;以本节为准。

7.4 临时加载(不安装)

1
2
3
4
5
6
pi -e npm:@foo/bar                    # 单次启动加载 npm 包里的 extension
pi -e git:github.com/user/repo
pi -e ./local/extension.ts
pi --skill ./my-skill # 加载 skill
pi --prompt-template ./tpl.md # 加载 template
pi --theme ./theme.json # 加载 theme

适合 demo / CI / 临时调试。

7.5 版本管理 & 兼容性

  • pi-coding-agent 主版本 0.x 阶段频繁 break:events 的 ctx shape、tool 注册字段、内置组件 API 都可能改名。把 peerDependencies 写成 >=0.74.0 这种宽松约束,CI 跑测试,挂了就出新 minor。
  • pi 安装 npm 包时 dependencies 会被装到 package 自己的 node_modules;bundledDependencies 里列出的会一起 publish 进 tarball(适合 fork 出来的子模块)。peerDependencies 不会被装——意思是 pi 自己作为 peer 提供。
  • pi-package 这个 keyword 是 gallery 收录的硬性条件(也是 docs 推荐的)。

7.6 最小示例:把第 6 节的 auto-format 发成 npm package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 1) 起项目
mkdir my-auto-format && cd my-auto-format
npm init -y
mkdir -p extensions/auto-format

# 2) 把第 6.10 节的 index.ts 拷进 extensions/auto-format/

# 3) 改 package.json
cat > package.json <<'EOF'
{
"name": "pi-auto-format",
"version": "0.1.0",
"description": "Auto-run prettier after pi writes/edits files",
"keywords": ["pi-package", "pi", "prettier", "extension"],
"license": "MIT",
"peerDependencies": {
"@earendil-works/pi-coding-agent": ">=0.74.0"
},
"pi": {
"extensions": ["./extensions/auto-format/index.ts"]
},
"files": ["extensions", "README.md"]
}
EOF

# 4) 本地测一下
pi -e .
# 或者 pi install ./
# 在 pi 里让模型 write 一个文件,看 prettier 有没有跑

# 5) 发布
npm login
npm publish --access public

# 6) 别人怎么装
pi install npm:pi-auto-format

7.7 加载顺序与覆盖规则(核心源码精确版)

本节所有结论直接来自 core/package-manager.ts 的 dedupePackages() / resourcePrecedenceRank() / collectPackageResources() / applyPatterns()。

Package 维度(同源覆盖):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// core/package-manager.ts dedupePackages()
private dedupePackages(packages) {
const seen = new Map();
for (const entry of packages) {
const identity = this.getPackageIdentity(sourceStr, entry.scope);
const existing = seen.get(identity);
if (!existing) {
seen.set(identity, entry);
} else if (entry.scope === "project" && existing.scope === "user") {
// Project wins over user
seen.set(identity, entry);
}
// 同 scope 同 identity:保留先扫到的
}
return Array.from(seen.values());
}

identity 的算法:

1
2
3
4
5
6
7
8
private getPackageIdentity(source, scope?) {
// npm: 包名(不含 version)
if (parsed.type === "npm") return `npm:${parsed.name}`;
// git: host/path(SSH 与 HTTPS 视为同一个)
if (parsed.type === "git") return `git:${parsed.host}/${parsed.path}`;
// local: 解析后的绝对路径
return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;
}

Resource 维度(同名资源覆盖):

1
2
3
4
5
6
// resourcePrecedenceRank(),越小越优先
// 0 project + settings entry (source: "local", scope: "project")
// 1 project + auto-discovered (source: "auto", scope: "project")
// 2 user + settings entry (source: "local", scope: "user")
// 3 user + auto-discovered (source: "auto", scope: "user")
// 4 package resource (origin: "package")

也就是说:

  • 项目 settings 显式列出 > 项目 auto > 全局 settings 显式列出 > 全局 auto > package。
  • 同 rank 同名时先扫到的 wins(loader.ts / skills.ts 里 “first wins” 的实现一致)。

Package filtering(pi config / settings 里的 packages 数组对象形式):

1
2
3
4
5
6
7
8
9
10
11
12
13
// settings.json
{
"packages": [
{
"source": "npm:my-pi-pack",
"extensions": ["./extensions/auto-format/index.ts"], // 仅这一个 ext 启用
"skills": ["!archive/*"], // 排除 archive/ 下
"prompts": [], // 全关
"themes": ["+special.json"] // 强制开
// 省略某 key → 该类型走默认(全开)
}
]
}

applyPatterns() 的精确规则(源码注释 verbatim):

Pattern types:

  • Plain patterns: include matching paths
  • !pattern: exclude matching paths
  • +path: force-include exact path (overrides exclusions)
  • -path: force-exclude exact path (overrides force-includes)

pi.skills manifest 指什么(来自 core/package-manager.ts 的 collectResourceFiles() → collectSkillEntries(dir, "pi")):

  • 指目录("./skills" 或 "./skills/foo"):递归扫描,遇到 SKILL.md 就停止下钻(把那个目录当作 skill root),同时根目录下的 .md 文件也直接当作单文件 skill 加载。
  • 直接指 SKILL.md 文件("./skills/foo/SKILL.md"):把那一个 skill 加进来。
  • 指中间目录但目录里没有 SKILL.md、只有 .md 文件:根目录的 .md 文件被当成 skill;若该目录仅是”装着多个子 skill 目录”的容器,行为正确——逐个递归找 SKILL.md。

简单说:指目录最稳,无论目录里是直接放 SKILL.md 还是嵌套了多个子 skill 目录都行。


8. 真实案例拆解

8.1 pi-mcp-adapter(月下载 99.2K)

职责:把任意 MCP server 接入 pi。

关键设计:

  1. proxy tool 模式:默认只注册一个 mcp 工具(~200 token),通过参数(tool / connect / describe / search)把上百个 MCP tool 隐藏在后面。
  2. direct tool 模式:用户在 .mcp.json 里把高频 tool 标记为 direct,启动时把这些注册成一等公民工具(每个 ~200 token)。MCP_DIRECT_TOOLS=__none__ 完全禁用、缺省自动选。
  3. lazy connect:MCP server 不在启动时全连,模型调到时才 spawn(节省内存、更快启动)。
  4. lifecycleGeneration:抗 session 反复重启的 race。
  5. OAuth flow:跟 MCP server 走 OAuth 时,需要 extension 自己起 callback 端口;在 session_start 调 initializeOAuth(),session_shutdown 里 shutdownOAuth()。

注册的工具与命令:

名字 类型 干嘛
mcp tool(proxy) 让 LLM 调任意 MCP tool
<prefix>__<tool> tool(direct) 高频 MCP tool 直接暴露
/mcp command 状态面板(reconnect / tools / setup / logout / status)
/mcp-auth command 触发 OAuth

怎么读它的源码:

  • 入口 index.ts 是粘合层。
  • 业务逻辑在 init.ts(启动)、commands.ts(slash 命令)、proxy-modes.ts(proxy tool 的几种模式)、direct-tools.ts(直接 tool 注册)、tool-result-renderer.ts(自定义渲染)。
  • 配置:config.ts 读 .mcp.json(兼容 Claude Desktop / Cursor 的格式)。

对你的启发:写”adapter 类”扩展(连外部协议进 pi)的标准模板就是它。

8.2 pi-subagents(月下载 103.2K)

职责:在 pi 里实现 sub-agent,能 sequential / parallel / background 跑多个内嵌 agent。

关键设计(从 src/extension/index.ts 实抓):

  1. 单工具多动作:只注册一个 subagent 工具,靠参数 schema 分支({ agent, task? } / { chain: [...] } / { tasks: [...] } 三种主模式 + list / get / create / update / delete / status / interrupt / resume / doctor 这套管理动作)。
  2. 三个 message renderer:分别处理 SLASH_RESULT_TYPE、subagent-notify、SUBAGENT_CONTROL_MESSAGE_TYPE,定制 TUI 显示。
  3. 基于 EventBus 的 async 框架:pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT) / SUBAGENT_ASYNC_COMPLETE_EVENT / SUBAGENT_CONTROL_EVENT,让 background subagent 能跨 turn 通知主 session。
  4. session bridge:registerSlashSubagentBridge、registerPromptTemplateDelegationBridge、registerFanoutChildSubagentExtension——把 slash command 和 prompt template 接进 subagent 体系。
  5. 包结构(package.json):
1
2
3
4
5
"pi": {
"extensions": ["./src/extension/index.ts"],
"skills": ["./skills"],
"prompts": ["./prompts"]
}

三层都用:

  • extension 注册 subagent 工具与 /subagent / /subagent-status 等命令。
  • skills 提供”如何使用 subagent 框架”的 progressive disclosure 文档。
  • prompts 提供 /scout、/plan 这类启动咒语。

对比 Claude Code 内置 subagent:

维度 Claude Code pi-subagents
实现位置 内核 第三方 extension
可定制性 受限于官方暴露的 hook 全部 TS 源码
chain 表达力 简单 parallel / collect / chain 三种
隔离粒度 子任务 子 session(含独立 context window)
持久化 跟主 session 绑 markdown / json artifact

这正体现 pi 的哲学:核心不内置,社区能写得比内置更好——因为你看得到全部代码。

8.3 context-mode(月下载 121.5K)— ⚠️ 不是 pi extension

2026-06-05 复核更正:原稿把 context-mode 当 pi 社区扩展拆解是错误前提。npm registry 显示该包的 repository 字段指向 mksglu/claude-context-mode,作者 Mert Koseoğlu (mksglu),Elastic License 2.0(不是 MIT),最新版 1.0.162。它实际是Claude Code / Gemini CLI / Cursor / Codex CLI 等多平台的 MCP 插件,安装方式是:

1
2
3
4
5
6
7
8
# Claude Code
/plugin marketplace add mksglu/context-mode
/plugin install context-mode@context-mode
/context-mode:ctx-doctor

# Gemini CLI / 其他平台
npm install -g context-mode
# 在 ~/.gemini/settings.json 或对应平台 config 里配 MCP server + 4 hooks

官方明确支持的平台清单(README 摘录):

  • Full hooks(routing enforcement + session continuity):Claude Code、Gemini CLI、VS Code Copilot、JetBrains Copilot、OpenCode、OpenClaw、Codex CLI、OMP
  • Partial / no hooks(约 60% compliance):Cursor、Antigravity、Zed、Kiro
  • 未提及 pi——所以”pi 月下载 121.5K 的最热扩展”这一说法源自 npm 全局下载量,不能等同于 pi extension 圈的真实份额。

功能(README 原文):解决”context window crisis”——把 raw data 隔离到 sandbox 跑代码、用 SQLite + FTS5 + BM25 做长程检索、强制模型生成分析脚本而非直接看数据。核心工具:

Tool 功能 Context 节省
ctx_execute 在 12 种语言的 sandbox 跑代码,只回 stdout 56 KB → 299 B
ctx_batch_execute 多命令 + 多查询合一调 986 KB → 62 KB
ctx_index / ctx_search FTS5 + BM25 检索 60 KB → 40 B
ctx_fetch_and_index 抓 URL 切片建索引(24h 缓存) 60 KB → 40 B
ctx_execute_file sandbox 内处理大文件 45 KB → 155 B

对 pi 用户的启示:

  1. 想要类似能力,可以通过 pi-mcp-adapter 把 context-mode 接进 pi——pi-mcp-adapter 本身就是 MCP 桥,理论上 context-mode 这个 MCP server 能挂上去(pi 文档与 README 都未明确测试,需自验)。
  2. context-mode 的”代码代替数据”范式仍然有借鉴价值:写真正的 pi extension 时,用 sandboxed bash + 临时文件路径回传(pi 的 bash 工具默认行为)已经能实现 70% 等效效果,无需引入新工具。
  3. License 差异:Elastic License 2.0 禁止”作为托管服务转售”,企业内部使用 OK;写到 pi-package 生态时不要复制 context-mode 源码——只能通过 MCP 调用。

9. 从 Claude Code / Codex 迁移

9.1 概念映射

Claude Code Codex pi
Skills (无原生) skills(同协议)
MCP servers MCP servers pi-mcp-adapter(社区)
Hooks(PreToolUse / PostToolUse) (Codex 内置较少) extension events(tool_call / tool_result)
Subagents(内置) (无原生) pi-subagents(社区)
/commands --prompt flag prompt templates
Plan mode(内置) (无) DIY,参考 6.11
Permissions sandbox(OpenShell) DIY + pi-OpenShell / gondolin

9.2 Skills:直接挪过来

Claude Code 的 skill 协议跟 pi 的几乎完全一致(pi 实现的就是 Agent Skills 标准),把 ~/.claude/skills/foo 复制到 ~/.pi/agent/skills/foo 多半能用。差异:

  • pi 允许 name 跟目录名不同。
  • pi 的 frontmatter 多了 compatibility / metadata / disable-model-invocation,少了 Claude 特有字段(如果有)。
  • allowed-tools 列表里写的工具名要对应 pi 的工具名(bash / read / write / edit),不是 Claude Code 的 Bash / Read(首字母大小写不同)。

docs/skills.md 给出现成的”借用其他 harness 的 skills”配置:

1
2
3
4
5
6
{
"skills": [
"~/.claude/skills",
"~/.codex/skills"
]
}

9.3 MCP:装个 adapter

1
pi install npm:pi-mcp-adapter

把 Claude Desktop / Cursor 的 mcp.json 直接放 <project>/.mcp.json 或 ~/.config/mcp/mcp.json,pi 自动读。OAuth flow 走 /mcp-auth。

9.4 Hooks → events

Claude Code Hook pi event
PreToolUse tool_call(return { block: true, reason } 等于拒绝;mutate event.input 等于改入参)
PostToolUse tool_result(return { content?, details?, isError? } 等于 transform)
UserPromptSubmit input(return { action: "transform", text, images? } 改写;{ action: "handled" } 完全接管)
Stop agent_end
SubagentStop (pi 里没 subagent 内置;用 pi-subagents 自己监听)

写法上 pi 是 TS 里的函数,Claude Code 是 JSON 配命令;pi 灵活得多但要写代码。

9.5 Codex sandbox → pi(来自 docs/containerization.md 抓取)

docs/containerization.md 列了三种容器化模式,verbatim 引用:

Pattern What is isolated Best for Notes
OpenShell Whole pi process in a policy-controlled sandbox Local or remote managed sandbox Requires an OpenShell gateway
Gondolin extension Built-in tools and ! commands Local micro-VM isolation while keeping auth on host See examples/extensions/gondolin/
Plain Docker Whole pi process in a local container Simple local isolation Provider API keys enter the container

OpenShell(NVIDIA OpenShell)

进 sandbox 内整体跑 pi:

1
2
3
openshell gateway add <gateway-url> --name <name>
openshell gateway select <name>
openshell sandbox create --name pi-sandbox --from pi -- pi

如果 gateway 是远端的,项目文件不会被 bind-mount,要么 sandbox 里 clone repo,要么用 openshell sandbox upload / download。OpenShell 还能做”原始 API key 不进 sandbox、由 gateway 注入”的 inference routing(沙箱里调 https://inference.local)。

Gondolin(earendil-works 自家本地 micro-VM)

1
2
3
4
5
6
cp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin
cd ~/.pi/agent/extensions/gondolin
npm install --ignore-scripts

cd /path/to/project
pi -e ~/.pi/agent/extensions/gondolin

行为:把 host cwd 挂到 VM 的 /workspace,override 内置 read / write / edit / bash / grep / find / ls 七个工具,并把 ! 命令也路由进 VM。文件改动通过 /workspace 写回 host。需要 Node ≥ 23.6.0 和 QEMU。

Plain Docker(最简单)

1
2
3
4
5
6
7
FROM node:24-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
WORKDIR /workspace
ENTRYPOINT ["pi"]
1
2
3
4
5
6
docker build -t pi-sandbox -f Dockerfile.pi .
docker run --rm -it \
-e ANTHROPIC_API_KEY \
-v "$PWD:/workspace" \
-v pi-agent-home:/root/.pi/agent \
pi-sandbox

关键差别:OpenShell / Plain Docker 是整 pi 进容器,凭据在容器内;Gondolin 是 pi 在 host、工具调用进 VM,auth 留在 host。要纯审计沙箱选 OpenShell,要本地隔离选 Gondolin,要 demo 选 Docker。


10. 调试与发布

10.1 本地 dev loop

1
2
3
# 写代码 → 跑 pi -e ./your-ext.ts
# 改代码 → 在 pi 里 /reload
# 看 log → /debug 然后 tail ~/.pi/agent/pi-debug.log

console.log / console.error 可用。在 TUI 模式下 stdout 被 capture,进 log 文件而不是终端。

10.2 单元测试

pi 没有官方 extension test harness(截至 v0.78.x 我看到的 docs / repo)。建议方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// tests/auto-format.test.ts
import { describe, it, expect, vi } from "vitest";
import autoFormat from "../extensions/auto-format/index.ts";

function makeFakePi() {
const handlers: Record<string, Function> = {};
return {
api: {
on: (name: string, fn: Function) => { handlers[name] = fn; },
registerTool: vi.fn(),
registerCommand: vi.fn(),
registerFlag: vi.fn(),
getFlag: vi.fn().mockReturnValue(false),
} as any,
fire: (name: string, event: any, ctx: any) =>
handlers[name]?.(event, ctx),
};
}

describe("auto-format", () => {
it("triggers prettier on .ts write", async () => {
const { api, fire } = makeFakePi();
autoFormat(api);
const ctx = {
cwd: process.cwd(),
hasUI: false,
signal: new AbortController().signal,
ui: { notify: vi.fn() },
};
await fire("tool_result",
{ toolName: "write", input: { file_path: "x.ts" } },
ctx);
// assert prettier got called — wire up a child_process spy
});
});

集成测试:跑 pi --mode json -p "...",把 JSON 事件流抓下来,断言事件序列。这条路 pi-subagents 项目里有 test:integration script,可以参考它的实现。

10.3 发布

1
2
3
4
5
6
# 普通 npm 流程
npm version patch # bump
npm publish --access public

# pi.dev gallery 自动从 npm 同步带 pi-package keyword 的包
# 不需要手动提交

发布之前 checklist:

  • package.json 有 pi-package keyword
  • peerDependencies 写明 pi 兼容版本范围
  • files 字段包含 extensions / skills / prompts / themes / README
  • README 里有「装 / 配 / 用 / 卸」四段
  • 至少跑过一次 pi -e . 不报错
  • 至少有一个 demo gif 或 mp4

11. 未来与限制

仍在 unstable / 短期可能变的:

  • ctx.mode 的取值集合(tui / rpc / json / print)—— SDK mode 是否会进来还不确定。
  • TypeBox 是 pi 选的,但 0.x 内随时可能换 schema 库(Claude Code / OpenAI 都各有偏好)。
  • Provider registration API:ExtensionContext.registerProvider(name, config) 是包装层;底层是 packages/ai/src/api-registry.ts 的 registerApiProvider(provider, sourceId?)(Map 实现,非 class,不存在 ApiRegistry 类)。ApiProvider<TApi, TOptions> 接口字段是 { api, stream, streamSimple }。**ProviderConfig 这个名字在 pi-ai/src/types.ts 中并不存在——extension docs 用 ProviderConfig 是把 ApiProvider 包了一层(具体包装在 core/extensions/wrapper.ts,本次未深入)。社区扩展自定义 OpenAI 兼容 endpoint 时,目前最可靠路径仍是走 pi-ai 内置的 openai-completions provider 加 baseURL 配置**,而不是手写 ApiProvider。
  • disable-model-invocation 这个 skill 字段的执行细节。
  • ToolCallEventResult 的字段集(block + reason)非常窄;社区有需求要 transformInput 这种正式字段(目前靠 mutate event.input)。

已知限制:

  • extension 之间没有标准化的 shared state 总线,要么 appendEntry、要么自己开 socket。
  • tool_call 不能在执行后改 result(要 tool_result 才能改)。
  • skill 的”按需加载”是模型自己决定,不是确定性触发——遇到模型偷懒不调,只能改 description 或显式 /skill:name。
  • prompt template 的参数替换没有自动 escape,命令注入风险靠开发者自己防。
  • pi 的 RPC mode 是 stdio JSON,没有 WebSocket(不适合跨主机集成;要远程就上 gondolin)。

12. 不确定性(2026-06-05 两轮复核后留下的)

12.1 第一轮 5 处复核(已闭合)

5 个原始不确定点的核实结果:

# 原疑问 结论
1 ctx.getSystemPromptOptions() ✅ 真实存在,但只挂在 ExtensionCommandContext(命令 handler 拿到的超集)。事件 handler 拿不到,要看 BeforeAgentStartEvent.systemPromptOptions。
2 pi pkg create / pi extension dev ❌ 确认不存在。CLI 子命令完整集合是 install / remove / uninstall / update / list / config(来自 src/package-manager-cli.ts 的 PackageCommand 联合类型 + args.ts 的 help 文本)。
3 tool_call 的 replaceWith schema ❌ 不存在!ToolCallEventResult 仅有 { block?, reason? }。改入参是 mutate event.input in place。改 tool 结果要在 tool_result 事件里返回 { content?, details?, isError? }。
4 skill metadata 字段 ✅ docs 写明 *”Arbitrary key-value mapping”*;pi 自身只透传不读,给三方 extension 作元数据用。
5 全局 + 项目同名 package、pi config 部分关闭时的覆盖规则 ✅ 见 §7.7:dedupePackages 项目 wins、resourcePrecedenceRank 给出 5 级精确顺序、applyPatterns 处理 plain / ! / + / - 四类 filter。

12.2 第二轮 4 处补充复核(已闭合)

# 原疑问 结论
6 slash command / prompt template / skill 同名优先级常量 ⚠️ 没有显式 priority 常量。core/slash-commands.ts 只定义 SlashCommandSource = "extension" | "prompt" | "skill" + 21 个内置命令固定清单(new/resume/clone/fork/tree/settings/model/scoped-models/login/logout/export/import/share/copy/session/changelog/hotkeys/reload/quit 等)。优先级靠”内置 → extension → prompt → skill /skill:“的注册顺序隐式决定,同源后注册者覆盖前者(Map.set 语义)。建议永远加项目前缀避免歧义。
7 prompt template 参数替换的 sanitization 边界 ⚠️ 完全无防护。core/prompt-templates.ts 的 substituteArgs() 是纯字符串顺序替换,只防递归不防注入;parseCommandArgs() 支持 bash-style 单/双引号,按空白分词。命令注入风险全部转嫁给 template 作者。
8 registerProvider 的 ProviderConfig schema ⚠️ **API 真名是 registerApiProvider**(packages/ai/src/api-registry.ts,Map 实现)。ApiProvider 接口是 { api, stream, streamSimple }。ProviderConfig 名字仅出现在 ExtensionContext.registerProvider 的包装层,未在 pi-ai/src/types.ts 中独立定义;自定义 OpenAI 兼容 endpoint 推荐走内置 openai-completions + baseURL 配置。详见 §6.4 注释与 §11。
9 context-mode 源码地址与 pi 关系 ❌ 不是 pi extension!npm 包 context-mode 的 repository 是 mksglu/claude-context-mode,Elastic License 2.0,是 Claude Code / Gemini CLI / Cursor / Codex CLI 的多平台 MCP 插件,README 明确未列 pi。原稿 §8.3 已重写。

12.3 仍未闭合的派生疑点

  1. ApiProvider 包装层 wrapper.ts:ExtensionContext.registerProvider(name, config) 把 name 与 ProviderConfig 转成 ApiProvider 的具体路径未抓 verbatim 源码。
  2. /skill:name 命名空间冲突:当一个 package 与一个 extension 同时注册同名 skill,/skill:name 解析到哪个,源码无明确测试。
  3. slash command 21 条内置清单的稳定性:core/slash-commands.ts 内置数组在 v0.79+ 是否会扩张/重命名(用户在 prompt template / extension 里撞上同名时会被静默覆盖)。
  4. registerApiProvider 的 sourceId 参数:用于 unregisterApiProviders(sourceId) 时按源批量清理,但 extension 调用 ctx.registerProvider 时这个 sourceId 是否自动塞为 extension name,未抓代码确认。

13. 来源

直接拉取的源码 / docs(raw URL,时点 main 分支 v0.78.x 附近,2026-06-05 抓取):

姊妹笔记(同 repo):

  • research/notes/pi-deep-dive.md
  • research/notes/pi-agent-vs-claude-code-codex.md
评论
分享