安装、命令行与运行模式
装上 Pi、接上第一个模型,并搞清楚命令行和四种运行模式
Pi 装完就能跑,但「装上」这一步有三种做法,它们在传递依赖是否被锁定和**pi update 能不能自更新上有实质差别;pi 跑起来之后有八个子命令和四种运行模式**,选错模式是最常见的效率损失来源。本文把这三件事一次讲清。
先给三条结论,后面逐条展开:
- Node.js 下限是 22.19.0,不是 20,也不是「22.x 就行」。
- 子命令只有八个:
install、remove/uninstall、update、list、config、auth、mcp。chat、run、doctor这三个都不存在。 - 默认是全屏 TUI,
-p才是「跑一次就退出」,--mode json是「跑一次但输出结构化事件」,--mode rpc是「常驻进程」。
2.1 环境要求与三种安装方式
Node.js 的下限写在 package.json 里
packages/coding-agent/package.json 的 engines 字段只有一条约束:
"engines": {
"node": ">=22.19.0"
}这是唯一的下限声明。Node 20 不满足它,Node 22.0 到 22.18 也不满足。这一点值得单独强调,因为大多数 Agent 工具的 README 只写「Node 18+」,读者会把 22.19 读成 22 开头的任意小版本。
官方安装脚本可以在需要时替你装 Node——根 README 的原话是 "The macOS, Linux, and Windows installers can install it if needed."。但走 npm 这条路的话,Node 是你的责任。
三种安装方式
三者装的是同一个包:@earendil-works/pi-coding-agent。区别在安装机制,不在产物。
官方脚本:锁文件在 install-lock/
安装脚本做的关键一件事,是从一个独立的 lockfile root 安装,而不是从发布到 npm 的 tarball 安装。仓库里这个目录是 packages/coding-agent/install-lock/,里面只有两个文件:
{
"name": "@earendil-works/pi-coding-agent-install",
"version": "1.3.0",
"private": true,
"description": "Lockfile root used by the Pi installer and updater.",
"dependencies": {
"@earendil-works/pi-coding-agent": "1.3.0"
},
"overrides": {
"protobufjs": "7.6.6"
},
"engines": {
"node": ">=22.19.0"
}
}注意 private: true——这个包从不发布,只存在于仓库里。根 README 把机制说得很直白:
The pi.dev installer installs from
packages/coding-agent/install-lock/, generated from the root lockfile, to pin transitive deps. The npm package does not pin transitive deps.
所以「官方脚本锁全部依赖」这句话的全部内容就是:它比 npm 多用了一个目录。这个目录由根 package-lock.json 生成,dependencies 把 Pi 钉到精确版本,overrides 把 protobufjs 钉到 7.6.6。
Windows 用 PowerShell 版脚本:
powershell -c "irm https://pi.dev/install.ps1 | iex"装完之后 Pi 是一个「受管安装」(managed install)。这个身份是可验证的:源码里 MANAGED_INSTALL_MARKER = "managed-install.json",pi update 启动时会读这个标记文件判断自己是不是受管安装。受管安装的自更新走的是重跑安装器,而不是调用包管理器,并且不接受 --force:
Managed pi installations do not support --force; rerun the installer to repair this installation.
也就是说,如果受管安装坏掉了,pi update --force 不会帮你修,正确做法是重新跑一遍安装脚本。
npm 安装:为什么推荐 --ignore-scripts
npm install -g --ignore-scripts @earendil-works/pi-coding-agent--ignore-scripts 之所以被官方写进推荐命令,是因为 Pi 的 npm 包没有 postinstall 生命周期脚本。package.json 的 scripts 里只有 clean、build、build:unbundled、build:binary、copy-assets、test、prepublishOnly 这些,没有 postinstall。根 README 也明说:
Pi does not require dependency lifecycle scripts for a normal npm installation.
推论很直接:加 --ignore-scripts 不会让安装缺任何东西。它只是不执行第三方依赖的安装脚本——而那恰恰是供应链攻击面。README 的 "Supply-chain hardening" 一节把整套做法摊开讲:
你用 npm 装 Pi 时不需要复现这一整套,但值得知道它存在——因为它解释了官方文档在两处都坚持加 --ignore-scripts:文档里的安装命令,和 README 的本地 release 安装。
bin 入口是 dist/bundle/cli.js
写脚本、算路径、排查 which pi 时用得上:
"bin": {
"pi": "dist/bundle/cli.js"
}dist/cli.js 也存在——它是 build:unbundled(tsc -p tsconfig.build.json)的直接产物,用于开发时从源码跑。发布出去的入口是 dist/bundle/cli.js,多了一层 bundle。如果你看到任何文档或脚本指向 dist/cli.js,那是开发路径,不是安装路径。
Nix:从源码构建,模型数据离线
nix profile add github:earendil-works/pi/stablestable 指向最新 release。要钉版本就用 tag,例如 github:earendil-works/pi/v1.0.0。
Nix 有两个值得知道的性质:
第一,pi update 不能更新 Nix 安装。 官方文档在三个地方重复了这件事(quickstart、cli.md、源码里的错误提示),因为踩坑的人不少。源码里的原文提示是 "pi update cannot update Pi when another package manager provides it, such as Nix"。用 nix profile upgrade pi。不过 pi update --extensions 和 pi update --models 仍然可用——包更新和模型目录刷新不依赖包管理器。
第二,Nix 构建是离线的。 所以发行包里带的模型数据来自 nix/model-catalog.json 里钉住的一次 pi.dev 模型目录修订。运行时 Pi 仍然照常从 pi.dev 叠加更新的目录,所以正常联网时你不会感知到这个区别;只有完全离线时才能看出目录有多旧。
验证
pi --version--version 打印版本号后立刻退出(main.ts 里是 console.log(VERSION); process.exit(0)),不走会话初始化,所以即使配置坏了它也能跑通。
怎么选
2.2 接入第一个模型
Pi 不带模型。装完之后进到工作目录、跑起 pi,第一件事是 /login。
cd /path/to/your/project
pi工作目录不是装饰:它决定 Pi 从哪里往上找配置文件、上下文文件和资源,也决定会话归到哪一组。这条在 交互式会话 里会展开。
三种接入方式
凭据文件是 ~/.pi/agent/auth.json,写入时用 mode: 0o600。认证在 Pi 的模型层只有两种类型——api_key 和 oauth,供应商差异都在这之下。供应商和认证的完整版在 模型与供应商。
凭据有没有生效,脚本怎么判断
交互式界面里 /login 会直接告诉你结果。脚本里没法交互,所以 Pi 提供了一条专门给自动化用的通道:
pi auth check --provider openai它的输出和退出码是一一对应的,这在 shell 里可以直接用:
映射关系在源码里是一行三元表达式:process.exitCode = result.status === "ready" ? 0 : result.status === "not_ready" ? 1 : 2。这个设计比打印一段人类可读文本有用得多,因为它让 if pi auth check --provider x; then … 这种判断成为可能。
pi auth 还有两个把凭据打到 stdout 的命令,--provider 或 --model 至少要给一个:
print-bearer-token 支持 --min-expiry <duration>(单位 ms/s/m/h,例如 30m),要求 token 至少还剩这么久。check 支持 --credentials 在就绪时一并输出凭据,--no-refresh 禁止刷新过期的 OAuth 凭据(默认会刷新)。
这些命令存在的意义是把 Pi 当成另一个客户端的凭据来源。help 文本里的例子就是给外部程序用的——比如你想让某个不认 Pi 配置的工具复用 Pi 已登录的订阅凭据。
命令行上直接选模型
不想进 TUI 也能定模型:
pi --model sonnet:high思考等级一共七个值,源码里是一个字面量数组:
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;会话里的默认值是 medium。注意 off 在这个列表里,但 thinkingBudgets 预算表只覆盖 minimal/low/medium/high 四个等级——off 和 xhigh 没有预算参数,max 也不在预算表里。这是两套不同的枚举,别混用。
--model 的一个实际后果值得记住:如果多个供应商提供同一个模型,Pi 优先选有凭据的那个;都没有凭据就问你 --provider。 这解释了为什么 --api-key 不能替代 --provider——它不参与「在多个供应商之间选择」这件事。
2.3 命令行结构:子命令与全局开关
顶层形状
pi [options] [--] [@files...] [messages...]位置参数有两类:@path 把一个文本文件或图片塞进第一条消息,其余是消息本身。-- 之后全部按位置参数处理,用来让以 - 开头的提示词不至于被当成开关。
@path 从当前工作目录解析。同一个工作目录还决定了项目配置、资源发现和会话分组——同一件事在一句话里出现三次,因为它真的是一件事。
管道的标准输入会被前置到第一条提示词:
git diff | pi --print "Review this change"真实的子命令只有这些
需要明确否定的三个:没有 chat,没有 run,也没有 doctor。想让 Pi 跑一次就用 -p;想检查健康状态没有专门子命令,用 /settings、pi auth check、pi mcp list 或 --verbose(细节在 2.6 节)。
pi config 也需要单独强调:它是交互式 TUI,不是查询命令。 没有 pi config list。源码里它构造的是 TuiMainScreen 加一个 ConfigSelectorComponent,界面里按 Tab 在全局配置和项目配置之间切换、勾选资源。想知道「当前启用了哪些资源」,要么在 TUI 里看,要么直接读 ~/.pi/agent/settings.json 和 .pi/settings.json 两个文件(配置)。
install / remove / uninstall / config 都接受 --local(短写 -l),意思是写进项目配置而不是全局配置。
包来源有四种形式:
带版本的 npm spec 会被钉住;git 的 tag 和 commit 也会被钉住,但包更新只做 reconcile,不会移动你配置的 ref。
全局开关按主题分组
模型
会话
这里有三条组合约束,源码和文档都写了:
- 会话 ID 必须以字母或数字开头结尾。
--fork不能和--session、--continue、--resume、--no-session同时用。--session-id不能和--session、--continue、--resume同时用;要选新 ID 就配--fork。
工具
--tools 的纯名字形式是白名单(得把想要的都写上),纯 +name/-name 形式是对默认选择的增量修改,两者不能混用;增量形式只接受精确名字,不接受 * 通配,想按模式禁用用 --exclude-tools。工具选择这层机制的完整讨论在 工具系统与信任边界。
资源
资源路径只对当前进程生效,相对路径从当前工作目录解析。
提示与进程
--tui-mode 的默认值是 fullscreen,源码在 settings-defaults.ts 里:tuiMode: "fullscreen"。这一点和很多人的预期相反,3.7 节专门讲。
参数解析的三条边界
未知长选项会被收集起来交给扩展。 解析器遇到 --foo bar 或 --foo=bar 时不会报错,而是放进一个 unknownFlags 映射,由已加载的扩展决定含义。help 文本里那句 "Extensions can register additional flags (e.g., --plan from plan-mode extension)" 就是这个机制的例子。
未知短选项直接报错。 源码里这一分支没有扩展通道:
} else if (arg.startsWith("-") && !arg.startsWith("--")) {
result.diagnostics.push({ type: "error", message: `Unknown option: ${arg}` });
}所以扩展只能注册长形式的开关。
-p 会顺手吃掉下一个参数。 这段解析逻辑让 pi -p "总结一下" 里的提示词不必再写一遍,但也带来一个坑:以 - 开头的提示词要写成 pi -p -- "- Summarize these points",用 -- 终止选项解析。
2.4 四种运行模式
四种模式共用同一套 agent、同一套会话、同一套资源和同一套工具。模式只决定三件事:输入怎么进来、输出怎么暴露、进程是否继续留着接受更多命令。
模式是怎么选出来的,源码里就是一个五行的函数:
function resolveAppMode(parsed: Args, stdinIsTTY: boolean, stdoutIsTTY: boolean): AppMode {
if (parsed.mode === "rpc") return "rpc";
if (parsed.mode === "json") return "json";
if (parsed.print || !stdinIsTTY || !stdoutIsTTY) return "print";
return "interactive";
}读法很简单:rpc 和 json 优先,其次「显式 -p 或 stdin/stdout 任一不是 TTY」判为 print,剩下才是交互式。 这解释了一个容易被忽略的行为——pi > out.txt 会静默进入 print 模式,你以为自己在跑交互式 TUI,实际上它处理完就退出了。
一个容易搞混的点:--mode text 不等于一次性
--mode text 只选输出形态,不选生命周期。resolveAppMode 里 text 根本没有分支——它落到了最后那个 return "interactive"。
结论就是文档里那句:--mode text does not force one-shot execution when stdin and stdout are terminals; use --print for that behavior.
所以:
Print 模式
pi --print "Summarize the changes in this repository"它执行完提示词、把最终 assistant 文本写到 stdout、然后退出。中间事件不暴露。适合命令替换、管道和一次性任务。
退出码是有语义的,值得写进脚本:
第三行是重点:被截断的响应仍然把已有的部分文本打到 stdout。这让「先拿个大概,够不够再调模型重试」的写法可行,但你必须同时看退出码,否则会把半截输出当成完整答案。
JSON 模式
pi --mode json "Review this repository" > events.jsonl它输出的是 JSONL 事件流,先是一个会话头,然后是 agent 和 session 事件。这不是「一个大 JSON 结果」,也不是「约束模型回复格式」——模型该回什么都行。
几条硬规则:
- 所有提示词在进程启动时一次性给完。 之后不接受新命令。所以 JSON 模式不适合交互式会话中途改变主意。
- stdout 被 JSONL 独占。 诊断信息和应用日志走 stderr。
- 流式
message_update记录里是增量,不是不断变大的整条消息快照。 要组装实时输出就用增量,最终结果以message_end里那条权威消息为准。 agent_end之后可能还有自动恢复或队列里的活。 判断「这次真的跑完了」要看agent_settled。- 失败的响应会出现在事件流里,但本身不产生非 0 退出码。 成功与否要自己从事件里判断。
事件类型和形状在 docs/json.md,本文只给判断规则。
RPC 模式
pi --mode rpc --no-sessionPi 保持运行,命令从 stdin 读,响应和事件往 stdout 写,每条记录占一行。
加 id 的命令会有配对的响应重复这个 ID;事件通常不带命令 ID,因为它们描述的是会话活动而不是某次请求。
最需要小心的一条:一次成功的 prompt 响应只意味着提示词被接受、排队或处理了,不代表这次运行完成了。要等完成就继续消费事件直到 agent_settled。
另外两条边界:
- RPC 模式拒绝
@file参数。 传了会报错。 - TUI-only 的扩展能力在非交互模式下不可用或降级。 扩展对话框是一套请求-响应子协议,其它 UI 更新是客户端可以显示也可以忽略的通知。
TypeScript / Node 集成不要手写协议:RpcClient 会启动子进程、关联请求、暴露有类型的方法、并把会话事件送给监听器。它需要一条可运行的 Pi CLI 路径,仓库里的示例指向 dist/cli.js,所以从 checkout 跑示例前得先 build。
SDK 不是第五种模式。 它把 agent 直接嵌进 Node.js 或 Bun 进程,没有进程边界。选择标准很简单:需要跨进程、语言无关、或者不想把 Node 引入依赖树,用 CLI 模式;已经在 Node 里、想要直接的类型访问,用 SDK。
JSON 和 RPC 为什么要分开
两种非交互模式都在 stdout 上跑结构化协议,但方向不同:
选择依据只有一条:你需要在运行途中改变 Pi 的状态吗? 不需要就 JSON,需要就 RPC。
2.5 更新与卸载
更新
pi update 不带目标时更新 Pi 自己。
pi update --self、pi update self、pi update pi 都是 pi update 的别名;pi update --extension <source> 是 pi update <source> 的别名。别名存在的唯一理由是防止你在批量脚本里手滑。
四种安装方式在更新上的差异是这一节最值得记住的部分:
包管理器这一支还有两个前提,源码里是三个条件的与运算:包目录必须落在全局包管理器根目录下(本地装的 pi 自更新不了),目标路径必须可写。任一不满足,Pi 就打印一条手工命令让你自己来,而不是静默失败。
npm 自更新成功后,Pi 会主动提示你迁移:
This npm installation of pi does not pin its dependencies.
Run the installer to migrate to a managed installation that does:这条提示在源码里的注释解释了它的边界:pi.dev 安装器会把全局 npm 安装迁移成受管安装,但不会迁移 pnpm、yarn 或 bun 的安装。 换句话说,官方只对 npm 这一条路提供了迁移。
自更新会去请求 https://pi.dev/api/latest-version 拿最新版本,默认超时 10 秒。设 PI_SKIP_VERSION_CHECK 可以跳过这个请求。
卸载
三条命令都不删配置、凭据、会话和已安装的 Pi 包——它们全在 ~/.pi/agent/ 下,要自己动手清理。
这一条值得单独强调,因为它是个安全设计:卸载 CLI 不等于注销你的模型订阅。auth.json 里的 OAuth 凭据留在磁盘上,任何能读这个文件的程序都还能用它。
2.6 排障
先分清是哪一类问题
Pi 的故障可以粗暴地分成三类,各自的排查入口不同:
Node 版本
node --version低于 22.19.0 就装不了 npm 版本。官方脚本能替你装 Node,但如果你是 npm 装的,就得自己处理。注意 macOS 自带的 /usr/bin/node 通常很旧,which -a node 看清楚 shell 实际用的是哪一个。
PATH
确认 pi 到底指向哪里:
which -a pi
pi --versionnpm 全局装的二进制通常在 npm prefix -g 下的 bin 里。如果 pi --version 报的是找不到文件而不是「不是内部或外部命令」,那问题在 Pi 自己的入口,而不在 PATH——回去核对是不是有人把路径写成了 dist/cli.js。
模型和凭据
pi auth check --provider openai退出码 0/1/2 直接对应就绪/未配置/状态损坏。1 基本都是「这个供应商的凭据压根没配」。想确认 Pi 眼里到底有哪些模型:
pi --list-models
pi --list-models sonnet--list-models 只读运行时元数据,不进入会话初始化,所以它在配置部分损坏时仍然能跑。
怀疑是网络问题(比如内网、代理、离线环境)时:
pi --offline--offline 关掉所有自动网络活动,包括模型目录刷新,等价于 PI_OFFLINE=1。它同时也跳过版本检查——源码里检测到离线模式时会顺手设上 PI_SKIP_VERSION_CHECK=1。
代理走环境变量:HTTP_PROXY、HTTPS_PROXY。
启动卡在项目信任
在一个带受保护项目资源的目录里首次启动 Pi,交互式界面会问要不要信任。非交互模式下弹不出这个问题:defaultProjectTrust 是 "ask" 时,它等价于 "never"——没人能回答。所以 CI 里要么显式传开关:
pi -p --approve "Summarize this repo"要么显式否定:
pi -p --no-approve "Summarize this repo"不要靠改全局设置来解决自动化场景——那是把一次性的意图写成了永久的。信任机制本身、它的优先级四层、以及它管不到什么,在 工具系统与信任边界 里展开。
怀疑是扩展的问题
最快的隔离办法是关掉所有扩展:
pi -ne-ne 关掉发现到的、配置的、内置的扩展,但显式写的 -e 路径仍然加载。所以如果你怀疑某个具体扩展,可以留着它:
pi -ne -e ./my-extension.ts只想关 MCP 的话有单独的开关:pi --no-mcp 不连接任何 MCP 服务器,也没有 MCP 工具和 /mcp 命令,但它不影响一个替换了内置 MCP 实现的第三方扩展。
启动信息不够
pi --verbose--verbose 强制详细启动,覆盖 quietStartup 设置。启动头部本来就会列出 Pi 加载了哪些指令和资源,verbose 会把它们说得更细。
终端渲染或会话状态出问题
会话里跑 /debug。它把两样东西写进 agent 目录下的 pi-debug.log:
- 全部渲染出来的终端行,每行带它计算出的可见宽度;
- 当前会话的全部消息,JSONL 格式。
~/.pi/agent/pi-debug.log宽度那一项对诊断很有用——大部分渲染错乱最后都能归因到「这个字符串的显示宽度和它的 length 不一样」,也就是 ANSI 转义、宽字符、emoji 或组合字符。TUI 库里量宽度用的是可见列数而不是字符串长度。
分享这个文件之前先读一遍。 它会包含提示词、模型回复、工具输出、文件内容。
一张环境变量速查
排障时最有用的一张表:
供应商凭据是另一套环境变量(ANTHROPIC_API_KEY、OPENAI_API_KEY 等),pi --help 底部有一整张清单。
还有一个容易忽略的:bash 工具执行命令时会注入会话环境变量(PI_SESSION_ID、PI_SESSION_FILE、PI_PROVIDER、PI_MODEL、PI_REASONING_LEVEL),但你自己用 ! 敲的命令不会。所以「模型能看到当前是哪个模型,我敲的命令看不到」是正常的,不是 bug。
常见的错误用法
最后一张表,把最常见的几种误用和它们的正确写法并排放:
最后一条斜杠命令那组在 交互式会话 里会给出 24 个真实命令的完整清单。
2.7 事实来源
本文所有数字、开关名、路径和默认值都可在仓库里复算:
完整事实表(含每条断言的推导命令)见专栏仓库的 research/pi-agent/FACTS.md。