Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pi Packages 与包管理

TL;DR:Pi Package 是把扩展、skills、prompt 模板、主题打成一个单元分发的普通目录或 npm 包,来源可以是 npm、git 或本地路径。安装只做两件事:把包下载到 Pi 自己管理的目录,并在 settings 的 packages 里记一条声明;之后每次启动或 /reload,PackageManager.resolve() 把所有声明(加上顶层路径和自动发现目录)翻译成一份带来源、作用域和启用状态的资源路径清单,交给 ResourceLoader 去加载。包内资源靠 package.json 的 pi 字段或约定目录声明,用户还能在 settings 里按资源类型过滤。包等于可执行代码,所以项目级包要等项目信任之后才安装和加载。

它解决什么问题

一个好用的定制往往不止一个文件:一个权限扩展、配套的 review skill、几个 prompt 模板、一个主题。如果只能手动把文件拷进 ~/.pi/agent/extensions/,分享、升级、在团队里统一版本都很麻烦;扩展依赖的 npm 包也没人装。Pi Package 解决的就是分发与版本管理:复用 npm 和 git 现成的生态,而不是自建 registry。

运行形态要先讲清楚:包管理不是常驻服务。它分两部分,都在 Pi 进程内执行:

  • pi install / remove / update / list / config 命令(package-manager-cli.ts):解析参数、做信任检查,然后调用 DefaultPackageManager 完成安装并写回 settings,命令结束进程就退出。
  • 启动和 /reload 时的解析(DefaultPackageManager.resolve()):由 ResourceLoader 调用,把配置翻译成资源文件清单。缺失的包会在这时自动补装。

数据最终存在哪里:settings 里的声明才是唯一权威(全局 ~/.pi/agent/settings.json,项目 .pi/settings.json);下载下来的包目录只是可重建的缓存,删掉后下次启动会按声明重新安装。

一个包长什么样

最简单的包就是按约定放目录:

my-pi-package/
├── package.json
├── extensions/   # .ts / .js 扩展
├── skills/       # skill 目录
├── prompts/      # .md 模板
└── themes/       # .json 主题

资源不在约定位置,或者需要筛选时,用 package.json 里的 pi 字段(manifest)显式声明,路径相对包根目录,支持 glob 和 ! 排除:

{
  "name": "my-pi-package",
  "keywords": ["pi-package"],
  "pi": {
    "extensions": ["./src/extension.ts"],
    "skills": ["./resources/skills"],
    "prompts": ["./resources/prompts/*.md"],
    "changelogPath": "./docs/CHANGELOG.md"
  }
}

manifest 的解析非常克制(pi-manifest.ts 只有几十行):只认 extensions / skills / prompts / themes 四个字符串数组和 changelogPath。pi-package 关键字让 npm 包出现在 pi.dev 的包展示页上。

依赖声明有一个容易踩的坑。扩展运行时需要的包放进 dependencies,安装时 Pi 会一并安装(devDependencies 不装)。但 @earendil-works/pi-ai、pi-agent-core、pi-coding-agent、pi-tui、typebox 这些由宿主提供,应该写进 peerDependencies 并用 "*",不能写进 dependencies。原因和扩展系统的设计直接相关:Pi 把这些 import 映射到宿主自己的模块,扩展才能和宿主共享同一套类和注册表;包里如果带了一份物理拷贝,就可能绕过映射,出现重复的类、重复的注册表和重复初始化。Pi 检测到这种配置会报扩展警告。另外,每个安装的包有独立的模块根,不能指望两个包共享同一个依赖实例。

一次 pi install 到资源生效

以团队成员执行 pi install npm:@acme/pi-guard@1.2.0 为例。

第一步:解析来源。parseSource() 把字符串分成三类:npm: 前缀是 npm 源,git: 前缀或 https:// URL 是 git 源,其余按本地路径处理。带精确版本的 npm 规格、git 的 tag 或 commit 视为固定版本(pinned)。

第二步:安装到受管目录。npm 包装到 ~/.pi/agent/npm/,git 仓库 clone 到 ~/.pi/agent/git/<host>/<path>;加 --local(-l)则改为项目下的 .pi/npm/、.pi/git/,并且要求项目已被信任。本地路径不复制、不安装,只校验存在,所以它的依赖树由作者自己负责。

第三步:写回 settings。installAndPersist() 在安装成功后把来源追加到对应 settings 的 packages 数组。这一步才让安装"持久":下次启动是 settings 驱动解析,而不是扫描安装目录。

第四步:启动时解析。之后每次启动或 /reload,ResourceLoader 先解决项目信任,再调用 packageManager.resolve():

resolve()
  ├─ 读取全局和项目 settings
  ├─ 合并两边的 packages,按包身份去重(项目优先)
  ├─ 逐个包:缺失就补装 → 进入包目录收集资源
  │     settings 里的过滤器 > 包内 pi manifest > 约定目录扫描
  ├─ 解析顶层的 extensions / skills / prompts / themes 路径
  ├─ 自动发现 ~/.pi/agent/* 与 .pi/* 下的资源
  ├─ 加入内置扩展(mcp、codemode 等)及其启用状态
  └─ 输出 ResolvedPaths:每类资源一个列表,每项带 path、enabled、metadata

metadata 记录资源的来源(哪个包、本地配置、自动发现、CLI、内置)、作用域(user / project / temporary)和来源方式(包内还是顶层配置)。所以包管理器输出的不只是路径,而是可追溯的路径:/reload、pi config、冲突报告和项目信任判断都依赖这些元数据。同一资源被多个来源命中时,项目级优先于用户级,显式配置优先于自动发现,再往后是包内资源,内置扩展排在最后。

第五步:交给 ResourceLoader 加载。ResourceLoader 只取 enabled 的条目:扩展交给扩展 loader 用 jiti 加载(见 扩展系统),skills、prompts、themes 各自交给对应的加载器(见 资源发现、项目信任与系统提示词)。pi -e npm:xxx 走的是同一套解析,只是作用域为 temporary,包缓存在 ~/.pi/agent/tmp/extensions/ 下,不写 settings。

这里体现了职责划分:PackageManager 负责"从哪里拿到哪些文件",ResourceLoader 负责"把文件变成运行时对象"。包管理器不执行任何扩展代码。

一句话:settings 声明是权威,安装目录是缓存,resolve() 把声明翻译成带元数据的资源路径清单,加载交给 ResourceLoader。

作用域、身份与过滤

同一个包可以同时出现在全局和项目 settings 里。为了避免重复加载,Pi 按"包身份"去重:npm 按包名(忽略版本),git 按仓库地址(忽略 ref),本地按解析后的绝对路径。默认项目条目替换全局条目;如果项目条目写了 autoload: false,它就不再替换,而是作为对全局包的过滤增量。

settings 里可以用对象形式,按资源类型缩小加载范围:

{
  "packages": [
    {
      "source": "npm:@example/pi-tools",
      "extensions": ["extensions/*.ts", "!extensions/legacy.ts"],
      "skills": [],
      "prompts": ["prompts/review.md"]
    }
  ]
}

省略某类表示加载包允许的全部,[] 表示一个都不加载,!pattern 排除 glob 匹配,+path 强制包含某个确切路径,-path 强制排除。关键约束是过滤只能收窄 manifest,不能暴露包本身没声明的资源。pi config 提供交互界面来启用或禁用已发现的资源和内置扩展,Tab 在全局和项目范围间切换。

命令行

任务命令
安装pi install <source> [-l]
列出已配置的包pi list
删除包及其 settings 条目pi remove <source> [-l](别名 uninstall)
配置加载哪些资源pi config [--local]
更新所有包pi update --extensions
更新单个包pi update <source>
更新 Pi 本身pi update;--all 同时更新 Pi 和所有包

注意 pi update 不带参数时更新的是 Pi 自己,不是包。包更新的语义是"按声明对齐":npm 包检查是否有更新版本,固定版本不动;git 包 fetch 后 reset 到声明的分支或 ref,tag 和 commit 不会被移动。1.5 起 pi update 也可以通过 PI_SELF_UPDATE_COMMAND 或 selfUpdateCommand 设置,经由包装器或其他包管理器更新 Pi 本身。

容错策略是分场景的:启动时某个已配置的包装不上(比如没网),Pi 只发警告、跳过这个包的资源、继续运行,下次启动或 /reload 再试;而显式的 pi install 和 pi update --extensions 失败会以非零退出码报错。理由是启动路径不应该因为一个可选组件坏了就整体不可用,而用户主动发起的命令需要明确的失败信号。

更新后显示 changelog(1.5)

1.5 新增:包更新后,下次全新启动时 Pi 会读取包的 changelog,在"Extension Updates"区域显示比上次看到的版本更新的条目,和 Pi 自己的更新说明一样。默认读包根目录的 CHANGELOG.md,pi.changelogPath 可以指向其他文件,设为 "" 则关闭。版本匹配看的是 package.json 的 version 字段,所以 git 包只有在这个字段增加时才会显示。第一次见到某个包时只记录版本,不显示。只有至少加载了一个扩展的包才显示 changelog;collapseChangelog 设置可以把每个包压缩成一行"Updated xxx to v1.2.0"。

这个功能解决的是一个真实痛点:扩展会随更新悄悄改变行为(新的 hook、新的权限检查),用户需要知道变了什么。

信任与安全

包本质上是可执行代码:扩展在 Pi 进程内以用户权限运行,skill 还可以指示模型去执行程序。所以安全模型和扩展一致,靠的是"信任来源",而不是隔离:

  • 全局包由用户自己安装,等同于用户主动信任;
  • 项目包的声明写在 .pi/settings.json 里,只有在项目被信任之后,Pi 才会读取这些声明、补装缺失的包、加载项目包里的扩展。在 --local 下执行 install / remove 时,包管理器同样会先检查项目是否已被信任。

这一点很重要:克隆一个陌生仓库后直接启动 Pi,仓库里声明的包不会被自动下载执行,必须先经过项目信任决策(交互确认、project_trust 扩展、trust.json 里保存的决定,或 defaultProjectTrust 设置)。1.5 还修复了一个相关问题:保存过"不信任"的项目,即使目录里只有 AGENTS.md 这类无需信任的资源,也不会再被自动信任,包命令据此判断。

但项目信任只决定"加载什么",不限制"加载后能做什么"。要真正限制影响范围,仍然要靠容器、虚拟机等隔离手段(见 扩展系统 的安全边界部分)。安装第三方包前审阅源码,在信任项目前审阅它声明的包,是官方给出的建议。

常见追问

为什么不自建 registry,而是复用 npm 和 git? 包的作者和用户本来就在这两个生态里,版本、依赖、私有源、鉴权都现成可用;Pi 只需要定义一个很小的 manifest 约定。代价是要处理 npm 和 git 各自的边角问题(锁文件、代理、Windows 文件占用等),CHANGELOG 里相当一部分修复都与此相关。

为什么 settings 是权威,而不是扫描安装目录? 声明可以提交进项目仓库、在团队中共享,并且可以表达版本固定和资源过滤;安装目录只是缓存,丢了可以按声明重建。反过来扫描目录就无法区分"用户想要的"和"残留的"。

本地路径的包和直接写 extensions 路径有什么区别? 本地包会按包规则发现四类资源,并参与按包身份去重和过滤;顶层 extensions 路径只加载一个文件或目录里的扩展。两者都不复制文件,也不安装依赖。

两个包依赖同一个库的不同版本会冲突吗? 不会互相影响,每个安装的包有独立的模块根。但也意味着不能依赖它们共享单例;需要共享状态时,应通过宿主提供的机制(如 pi.events)。

源码入口(v1.5):packages/coding-agent/src/core/package-manager.ts、packages/coding-agent/src/package-manager-cli.ts、packages/coding-agent/src/core/pi-manifest.ts、packages/coding-agent/src/core/resource-loader.ts、packages/coding-agent/docs/packages.md