跳到主内容
创建时间:最后更新:作者:白川

安装、命令行与运行模式

装上 Pi、接上第一个模型,并搞清楚命令行和四种运行模式

Pi 装完就能跑,但「装上」这一步有三种做法,它们在传递依赖是否被锁定和**pi update 能不能自更新上有实质差别;pi 跑起来之后有八个子命令和四种运行模式**,选错模式是最常见的效率损失来源。本文把这三件事一次讲清。

先给三条结论,后面逐条展开:

  1. Node.js 下限是 22.19.0,不是 20,也不是「22.x 就行」。
  2. 子命令只有八个:install、remove/uninstall、update、list、config、auth、mcp。chat、run、doctor 这三个都不存在。
  3. 默认是全屏 TUI,-p 才是「跑一次就退出」,--mode json 是「跑一次但输出结构化事件」,--mode rpc 是「常驻进程」。

2.1 环境要求与三种安装方式

Node.js 的下限写在 package.json 里

packages/coding-agent/package.json 的 engines 字段只有一条约束:

json
"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 是你的责任。

三种安装方式

方式命令传递依赖自更新适合
官方脚本
curl -fsSL https://pi.dev/install.sh | sh
锁定(install-lock/)
支持 pi update
绝大多数人
npm
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
不锁
支持 pi update
需要纳管全局工具链
Nix
nix profile add github:earendil-works/pi/stable
源码构建,离线
不支持,用 nix profile upgrade pi
已有的 Nix 工作流

三者装的是同一个包:@earendil-works/pi-coding-agent。区别在安装机制,不在产物。

官方脚本:锁文件在 install-lock/

安装脚本做的关键一件事,是从一个独立的 lockfile root 安装,而不是从发布到 npm 的 tarball 安装。仓库里这个目录是 packages/coding-agent/install-lock/,里面只有两个文件:

json
{
  "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
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

bash
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" 一节把整套做法摊开讲:

做法内容
直接依赖钉死
精确版本号,不用范围
.npmrc
save-exact=true、min-release-age=2
lockfile 地位
package-lock.json 是依赖的唯一真相,pre-commit 拦截意外改动
CI 安装
npm ci --ignore-scripts
install-lock 生成
对依赖生命周期脚本有显式白名单,新依赖未审查前过不了检查
审计
定时任务跑 npm audit --omit=dev 与签名校验

你用 npm 装 Pi 时不需要复现这一整套,但值得知道它存在——因为它解释了官方文档在两处都坚持加 --ignore-scripts:文档里的安装命令,和 README 的本地 release 安装。

bin 入口是 dist/bundle/cli.js

写脚本、算路径、排查 which pi 时用得上:

json
"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:从源码构建,模型数据离线

bash
nix profile add github:earendil-works/pi/stable

stable 指向最新 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 叠加更新的目录,所以正常联网时你不会感知到这个区别;只有完全离线时才能看出目录有多旧。

验证

bash
pi --version

--version 打印版本号后立刻退出(main.ts 里是 console.log(VERSION); process.exit(0)),不走会话初始化,所以即使配置坏了它也能跑通。

怎么选

你的处境选理由
macOS / Linux,想省事
官方脚本
锁依赖 + pi update 自更新,除此之外没有理由选别的
需要把 Pi 纳入团队的全局工具链
npm
版本跟着 lockfile 走;代价是传递依赖不锁
已经在用 Nix 管一切开发环境
Nix
版本可复现;代价是 pi update 对它无效
Windows 原生终端
PowerShell 脚本
docs/windows.md 有专门的原生 Windows 说明
Android / Termux
见 docs/termux.md
有 Bionic libc 的兼容分支,不能套用桌面流程

2.2 接入第一个模型

Pi 不带模型。装完之后进到工作目录、跑起 pi,第一件事是 /login。

bash
cd /path/to/your/project
pi

工作目录不是装饰:它决定 Pi 从哪里往上找配置文件、上下文文件和资源,也决定会话归到哪一组。这条在 交互式会话 里会展开。

三种接入方式

方式命令凭据落在哪
订阅
会话里 /login,选供应商,走浏览器授权
~/.pi/agent/auth.json
API key
会话里 /login,或直接设环境变量
环境变量,或 ~/.pi/agent/auth.json
本地模型
/login 里选本地路由,或 --model 直接指定
同上

凭据文件是 ~/.pi/agent/auth.json,写入时用 mode: 0o600。认证在 Pi 的模型层只有两种类型——api_key 和 oauth,供应商差异都在这之下。供应商和认证的完整版在 模型与供应商。

凭据有没有生效,脚本怎么判断

交互式界面里 /login 会直接告诉你结果。脚本里没法交互,所以 Pi 提供了一条专门给自动化用的通道:

bash
pi auth check --provider openai

它的输出和退出码是一一对应的,这在 shell 里可以直接用:

输出退出码含义
ready
0
凭据已就绪
not_ready
1
没配凭据,或供应商找不到
invalid
2
认证状态本身是坏的

映射关系在源码里是一行三元表达式:process.exitCode = result.status === "ready" ? 0 : result.status === "not_ready" ? 1 : 2。这个设计比打印一段人类可读文本有用得多,因为它让 if pi auth check --provider x; then … 这种判断成为可能。

pi auth 还有两个把凭据打到 stdout 的命令,--provider 或 --model 至少要给一个:

命令做什么
pi auth check
打印就绪状态
pi auth print-api-key
打印解析出的 API key
pi auth print-bearer-token
打印解析出的 OAuth bearer token

print-bearer-token 支持 --min-expiry <duration>(单位 ms/s/m/h,例如 30m),要求 token 至少还剩这么久。check 支持 --credentials 在就绪时一并输出凭据,--no-refresh 禁止刷新过期的 OAuth 凭据(默认会刷新)。

这些命令存在的意义是把 Pi 当成另一个客户端的凭据来源。help 文本里的例子就是给外部程序用的——比如你想让某个不认 Pi 配置的工具复用 Pi 已登录的订阅凭据。

命令行上直接选模型

不想进 TUI 也能定模型:

bash
pi --model sonnet:high
开关语义
--provider <name>
把 --model 的查找限制在一个供应商内,必须配合 --model
--model <pattern>
精确 ID 或模糊匹配,接受 provider/id 和可选的 :<thinking> 后缀
--api-key <key>
一次性的 key 覆盖,必须配合 --model 或 --models
--thinking <level>
覆盖模型后缀里的思考等级,并按模型能力钳制
--models <patterns>
逗号分隔,限定 Ctrl+P 循环切换的范围
--list-models [search]
列出可用模型后退出,可选模糊搜索

思考等级一共七个值,源码里是一个字面量数组:

typescript
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 命令行结构:子命令与全局开关

顶层形状

text
pi [options] [--] [@files...] [messages...]

位置参数有两类:@path 把一个文本文件或图片塞进第一条消息,其余是消息本身。-- 之后全部按位置参数处理,用来让以 - 开头的提示词不至于被当成开关。

@path 从当前工作目录解析。同一个工作目录还决定了项目配置、资源发现和会话分组——同一件事在一句话里出现三次,因为它真的是一件事。

管道的标准输入会被前置到第一条提示词:

bash
git diff | pi --print "Review this change"

真实的子命令只有这些

子命令作用
install <source>
装一个扩展来源并写进配置
remove <source>
从配置里移除一个扩展来源
uninstall <source>
remove 的别名
update [source|self|pi]
更新 Pi、包,或模型目录
list
列出配置里已安装的扩展
config
打开一个 TUI,勾选哪些包资源生效
auth <command>
打印凭据,或检查供应商就绪状态
mcp <command>
检查 MCP 服务器,OAuth 登录/登出

需要明确否定的三个:没有 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
npm:@example/pi-tools@1.0.0
装到 Pi 的 npm 目录
git
git:github.com/example/pi-tools@v1
clone 并对齐到指定 ref
URL
https://github.com/example/pi-tools
当作 git 来源处理
本地
./pi-tools
按解析出的路径加载,不复制

带版本的 npm spec 会被钉住;git 的 tag 和 commit 也会被钉住,但包更新只做 reconcile,不会移动你配置的 ref。

全局开关按主题分组

模型

开关一句话
--provider / --model / --api-key
见 2.2
--thinking <level>
七个等级之一
--models <patterns>
限定循环切换范围,支持 glob 和模糊匹配
--list-models [search]
列出后退出

会话

开关一句话
-c, --continue
继续当前项目最近的一次会话
-r, --resume
打开会话选择器
--session <path|id>
按文件路径、精确 ID 或部分 ID 打开
--session-id <id>
按项目会话 ID 打开,不存在就建
--fork <path|id>
从既有会话分叉出新会话
--session-dir <dir>
覆盖会话存储与查找位置
--no-session
用内存会话,不落盘
-n, --name <name>
设置会话显示名

这里有三条组合约束,源码和文档都写了:

  • 会话 ID 必须以字母或数字开头结尾。
  • --fork 不能和 --session、--continue、--resume、--no-session 同时用。
  • --session-id 不能和 --session、--continue、--resume 同时用;要选新 ID 就配 --fork。

工具

开关语义
-t, --tools <list>
逗号分隔的允许列表,替换默认选择
-xt, --exclude-tools <list>
逗号分隔的拒绝列表,在所有其他选择之后生效,包含 MCP 工具
-nbt, --no-builtin-tools
关掉默认内置工具,保留扩展和自定义工具
-nt, --no-tools
所有内置、扩展、自定义、MCP 工具全关

--tools 的纯名字形式是白名单(得把想要的都写上),纯 +name/-name 形式是对默认选择的增量修改,两者不能混用;增量形式只接受精确名字,不接受 * 通配,想按模式禁用用 --exclude-tools。工具选择这层机制的完整讨论在 工具系统与信任边界。

资源

开关语义
-e, --extension <path>
加载扩展文件或目录,或 builtin:<name>,可重复
-ne, --no-extensions
关掉发现到的、配置的、内置的扩展;显式 -e 路径仍然加载
--no-mcp
关掉内置 MCP:不连服务器、没有 MCP 工具、没有 /mcp
--skill <path>
加载 skill 文件或目录,可重复
-ns, --no-skills
关掉 skill 发现与加载
--prompt-template <path>
加载 prompt template,可重复
-np, --no-prompt-templates
关掉 template 发现与加载
--theme <path>
加载主题,可重复
--use-theme <name[/name]>
本次运行的初始主题
--no-themes
关掉主题发现与加载
-nc, --no-context-files
关掉 AGENTS.md 与 CLAUDE.md 的发现

资源路径只对当前进程生效,相对路径从当前工作目录解析。

提示与进程

开关语义
--system-prompt <text|path>
替换默认系统提示,接受文本或文件内容
--append-system-prompt <text|path>
追加,可重复
--tui-mode <mode>
fullscreen(默认)或 regular
--verbose
强制详细启动信息,覆盖 quietStartup
-a, --approve
本次运行信任项目本地文件
-na, --no-approve
本次运行忽略受信任门禁的项目本地文件
--offline
关掉自动网络活动,含模型目录刷新
--export <file> [output]
把会话文件导出成 HTML 后退出
-h, --help / -v, --version
帮助 / 版本

--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)" 就是这个机制的例子。

未知短选项直接报错。 源码里这一分支没有扩展通道:

typescript
} 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、同一套会话、同一套资源和同一套工具。模式只决定三件事:输入怎么进来、输出怎么暴露、进程是否继续留着接受更多命令。

模式是怎么选出来的,源码里就是一个五行的函数:

typescript
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,实际上它处理完就退出了。

模式入口生命周期适合
交互式
pi
到用户退出为止
人直接用
Print
pi -p "…"
一次调用
脚本只要最终文本
JSON
pi --mode json "…" > events.jsonl
一次调用
进程需要结构化进度
RPC
pi --mode rpc
常驻
进程需要双向控制

一个容易搞混的点:--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.

所以:

你想要正确写法
文本输出,跑一次退出
pi -p "…"
文本输出,开交互式界面
pi --mode text(等价于 pi)
JSON 事件,跑一次退出
pi --mode json "…"
常驻进程
pi --mode rpc
bash
pi --print "Summarize the changes in this repository"

它执行完提示词、把最终 assistant 文本写到 stdout、然后退出。中间事件不暴露。适合命令替换、管道和一次性任务。

退出码是有语义的,值得写进脚本:

情况stdoutstderr退出码
正常
最终文本
——
0
assistant 以 error 或 aborted 结束
——
错误
非 0
撞上模型输出上限(length)
部分文本照样打印
警告
非 0

第三行是重点:被截断的响应仍然把已有的部分文本打到 stdout。这让「先拿个大概,够不够再调模型重试」的写法可行,但你必须同时看退出码,否则会把半截输出当成完整答案。

JSON 模式

bash
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 模式

bash
pi --mode rpc --no-session

Pi 保持运行,命令从 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 上跑结构化协议,但方向不同:

JSONRPC
方向
只出不进
双向
生命周期
一次调用
常驻
中途换模型/管会话
不行
行
跑 shell 命令、回答扩展对话框
不行
行
典型消费者
CI、批处理管道
编辑器插件、自建客户端

选择依据只有一条:你需要在运行途中改变 Pi 的状态吗? 不需要就 JSON,需要就 RPC。

2.5 更新与卸载

更新

pi update 不带目标时更新 Pi 自己。

目标命令
更新 Pi
pi update
更新所有已安装的包
pi update --extensions
更新某一个包
pi update <source>
刷新模型目录
pi update --models
更新 Pi 和所有包
pi update --all

pi update --self、pi update self、pi update pi 都是 pi update 的别名;pi update --extension <source> 是 pi update <source> 的别名。别名存在的唯一理由是防止你在批量脚本里手滑。

四种安装方式在更新上的差异是这一节最值得记住的部分:

你的安装方式pi update 能不能更新 Pi用什么更新
官方脚本(受管安装)
能,且不需要包管理器
内部走安装器;不接受 --force
npm
能
内部调 npm;Windows 上只有 npm 和 pnpm 支持自更新
pnpm / yarn / bun(包管理器装的)
能
各自包管理器的全局安装命令
bun 编译出的独立二进制
不能
从 GitHub Releases 下载
Nix
不能
nix profile upgrade pi

包管理器这一支还有两个前提,源码里是三个条件的与运算:包目录必须落在全局包管理器根目录下(本地装的 pi 自更新不了),目标路径必须可写。任一不满足,Pi 就打印一条手工命令让你自己来,而不是静默失败。

npm 自更新成功后,Pi 会主动提示你迁移:

text
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 可以跳过这个请求。

卸载

安装方式卸载命令
npm
npm uninstall -g @earendil-works/pi-coding-agent
官方脚本
重跑安装脚本,在菜单里选 Uninstall Pi
Nix
nix profile remove pi

三条命令都不删配置、凭据、会话和已安装的 Pi 包——它们全在 ~/.pi/agent/ 下,要自己动手清理。

这一条值得单独强调,因为它是个安全设计:卸载 CLI 不等于注销你的模型订阅。auth.json 里的 OAuth 凭据留在磁盘上,任何能读这个文件的程序都还能用它。

2.6 排障

先分清是哪一类问题

Pi 的故障可以粗暴地分成三类,各自的排查入口不同:

症状类别典型表现从哪查起
装不上 / 跑不起来
pi: command not found、版本要求报错
Node 版本、PATH、bin 入口
起来了但没模型
报没有凭据、模型列表空
/login、pi auth check、--list-models
卡在启动
问要不要信任这个项目
--approve / --no-approve
行为不对但能跑
输出乱、快捷键失灵、卡在某个扩展
-ne 隔离扩展、--verbose、/debug

Node 版本

bash
node --version

低于 22.19.0 就装不了 npm 版本。官方脚本能替你装 Node,但如果你是 npm 装的,就得自己处理。注意 macOS 自带的 /usr/bin/node 通常很旧,which -a node 看清楚 shell 实际用的是哪一个。

PATH

确认 pi 到底指向哪里:

bash
which -a pi
pi --version

npm 全局装的二进制通常在 npm prefix -g 下的 bin 里。如果 pi --version 报的是找不到文件而不是「不是内部或外部命令」,那问题在 Pi 自己的入口,而不在 PATH——回去核对是不是有人把路径写成了 dist/cli.js。

模型和凭据

bash
pi auth check --provider openai

退出码 0/1/2 直接对应就绪/未配置/状态损坏。1 基本都是「这个供应商的凭据压根没配」。想确认 Pi 眼里到底有哪些模型:

bash
pi --list-models
pi --list-models sonnet

--list-models 只读运行时元数据,不进入会话初始化,所以它在配置部分损坏时仍然能跑。

怀疑是网络问题(比如内网、代理、离线环境)时:

bash
pi --offline

--offline 关掉所有自动网络活动,包括模型目录刷新,等价于 PI_OFFLINE=1。它同时也跳过版本检查——源码里检测到离线模式时会顺手设上 PI_SKIP_VERSION_CHECK=1。

代理走环境变量:HTTP_PROXY、HTTPS_PROXY。

启动卡在项目信任

在一个带受保护项目资源的目录里首次启动 Pi,交互式界面会问要不要信任。非交互模式下弹不出这个问题:defaultProjectTrust 是 "ask" 时,它等价于 "never"——没人能回答。所以 CI 里要么显式传开关:

bash
pi -p --approve "Summarize this repo"

要么显式否定:

bash
pi -p --no-approve "Summarize this repo"

不要靠改全局设置来解决自动化场景——那是把一次性的意图写成了永久的。信任机制本身、它的优先级四层、以及它管不到什么,在 工具系统与信任边界 里展开。

怀疑是扩展的问题

最快的隔离办法是关掉所有扩展:

bash
pi -ne

-ne 关掉发现到的、配置的、内置的扩展,但显式写的 -e 路径仍然加载。所以如果你怀疑某个具体扩展,可以留着它:

bash
pi -ne -e ./my-extension.ts

只想关 MCP 的话有单独的开关:pi --no-mcp 不连接任何 MCP 服务器,也没有 MCP 工具和 /mcp 命令,但它不影响一个替换了内置 MCP 实现的第三方扩展。

启动信息不够

bash
pi --verbose

--verbose 强制详细启动,覆盖 quietStartup 设置。启动头部本来就会列出 Pi 加载了哪些指令和资源,verbose 会把它们说得更细。

终端渲染或会话状态出问题

会话里跑 /debug。它把两样东西写进 agent 目录下的 pi-debug.log:

  1. 全部渲染出来的终端行,每行带它计算出的可见宽度;
  2. 当前会话的全部消息,JSONL 格式。
text
~/.pi/agent/pi-debug.log

宽度那一项对诊断很有用——大部分渲染错乱最后都能归因到「这个字符串的显示宽度和它的 length 不一样」,也就是 ANSI 转义、宽字符、emoji 或组合字符。TUI 库里量宽度用的是可见列数而不是字符串长度。

分享这个文件之前先读一遍。 它会包含提示词、模型回复、工具输出、文件内容。

一张环境变量速查

排障时最有用的一张表:

变量作用
PI_CODING_AGENT_DIR
改配置目录,默认 ~/.pi/agent
PI_CODING_AGENT_SESSION_DIR
改会话存储,被 --session-dir 覆盖
PI_OFFLINE
关掉自动网络活动,含模型目录刷新
PI_SKIP_VERSION_CHECK
关掉 pi.dev 最新版本请求
PI_TELEMETRY
覆盖安装/更新遥测和供应商归因头
PI_PACKAGE_DIR
覆盖包目录,Nix/Guix store 路径用
AI_AGENT=pi、PI_CODING_AGENT=true
进程标记,子进程据此认出自己跑在 Pi 里

供应商凭据是另一套环境变量(ANTHROPIC_API_KEY、OPENAI_API_KEY 等),pi --help 底部有一整张清单。

还有一个容易忽略的:bash 工具执行命令时会注入会话环境变量(PI_SESSION_ID、PI_SESSION_FILE、PI_PROVIDER、PI_MODEL、PI_REASONING_LEVEL),但你自己用 ! 敲的命令不会。所以「模型能看到当前是哪个模型,我敲的命令看不到」是正常的,不是 bug。

常见的错误用法

最后一张表,把最常见的几种误用和它们的正确写法并排放:

错误用法实际是什么
pi chat
不存在这个子命令。一次性问答用 pi -p "…"
pi run
不存在这个子命令。跑一次用 pi -p,要结构化输出用 --mode json
pi doctor
不存在这个子命令。没有内置的诊断命令;用 --verbose 和 /debug
pi config list
pi config 是交互式 TUI,没有 list 子命令。想看内容直接读 settings 文件
pi --debug
没有这个开关。调试用会话里的 /debug,启动信息用 --verbose
npx pi
包名是 @earendil-works/pi-coding-agent,pi 只是它暴露的 bin 名
/retry
不存在这个斜杠命令。重新加载用 /reload
/editor
不存在这个斜杠命令。外部编辑器是 Ctrl+G
/branch
不存在这个命令,会话分叉叫 /fork
/exit
不存在这个斜杠命令。退出叫 /quit(快捷键是 Ctrl+D)
--skill-dir
没有这个开关。加载 skill 用 --skill <path>

最后一条斜杠命令那组在 交互式会话 里会给出 24 个真实命令的完整清单。

2.7 事实来源

本文所有数字、开关名、路径和默认值都可在仓库里复算:

断言来源
Node.js 下限 >=22.19.0
packages/coding-agent/package.json engines
npm 包名 @earendil-works/pi-coding-agent
同上 name
bin 入口 dist/bundle/cli.js
同上 bin
没有任何 postinstall 生命周期脚本
同上 scripts(仓库根只有 prepare: husky,不发布)
安装器锁依赖、npm 不锁
根 README.md "Supply-chain hardening" 节;packages/coding-agent/install-lock/package.json
install-lock/ 由根 lockfile 生成
根 README.md;scripts/generate-coding-agent-install-lock.mjs
受管安装标记 managed-install.json
src/package-manager-cli.ts:51
受管安装不支持 --force
src/package-manager-cli.ts:1050-1057
npm 安装后提示迁移到受管安装,不迁移 pnpm/yarn/bun
src/package-manager-cli.ts:1118-1129 及上方注释
Windows 自更新只支持 npm 与 pnpm
src/package-manager-cli.ts:1080-1085
包管理器自更新的三个前提(全局安装、路径可写、有对应命令)
src/config.ts:313-335、getSelfUpdateCommandForMethod
bun 独立二进制不自更新,给出手工下载指引
src/config.ts:129-130、:344-346
pi update 不能更新 Nix 安装
docs/quickstart.md、docs/cli.md
Nix 模型目录 pin 与离线构建
根 README.md "Run with Nix" 节;nix/model-catalog.json
版本检查端点与 10 秒超时
src/utils/version-check.ts:5-6
PI_SKIP_VERSION_CHECK 跳过检查
同上 :98
pi config 是 TUI
src/cli/config-selector.ts;src/main.ts:613
八个顶层子命令
src/main.ts:585-620 的分发顺序;src/cli/args.ts:289-298 的 help 文本
--local / -l 写项目配置
src/package-manager-cli.ts:431,830;docs/packages.md
包来源四种形式
docs/packages.md "Choose a source" 表
--fork 与 --session-id 的组合约束
docs/cli.md Constraints 段
未知长选项交给扩展、未知短选项报错
src/cli/args.ts:249-264;args.ts:348 末行
-p 会吃掉下一个参数
src/cli/args.ts:177-183
auth.json 路径与 0o600 权限
src/config.ts:625、src/core/auth-storage.ts:37
pi auth check 三个状态与退出码 0/1/2
src/cli/auth-check.ts:8;src/main.ts:202
--min-expiry 单位 ms/s/m/h
docs/cli.md Credential commands 段
七个思考等级
src/cli/args.ts:61 VALID_THINKING_LEVELS
默认思考等级 medium
docs/settings.md
thinkingBudgets 只覆盖四个等级
docs/settings.md
--tools 两种形式不可混用、增量形式不接受 *
docs/cli.md Tools 段;src/core/settings-manager.ts:100-104
-ne 之后显式 -e 仍加载
docs/cli.md Resources 段
--no-mcp 不影响替换实现
同上
--offline 与 PI_OFFLINE 等价并顺带跳过版本检查
docs/environment-variables.md;src/main.ts:580-583
四种运行模式的判定顺序
src/main.ts:112-123 resolveAppMode
--mode text 不改变生命周期
同上(text 无分支);docs/cli.md Invocation 段
print 模式的三种退出情形
docs/cli-integration.md "Print to stdout" 段
JSON 模式 stdout 独占、增量事件、agent_settled
同上 "Stream JSON events" 段
RPC 模式拒绝 @file、prompt 成功不等于运行完成
docs/cli.md、docs/cli-integration.md
RpcClient 与示例路径
docs/cli-integration.md "Control Pi with RPC" 段
SDK 不是第五种模式
docs/cli-integration.md 开头段
pi update 各变体与别名
docs/cli.md "Update Pi or packages" 与 "Aliases" 段;src/package-manager-cli.ts
三种卸载方式都不清理 ~/.pi/agent/
docs/quickstart.md "Uninstall Pi" 末句
/debug 写 pi-debug.log,含渲染行与消息
src/modes/interactive/interactive-mode.ts:6959-6983;src/config.ts:654-656
非交互模式下 defaultProjectTrust: "ask" 等价 never
docs/security.md;src/core/trust-manager.ts
PI_* 会话变量不注入用户 ! 命令
docs/environment-variables.md
默认 TUI 模式是 fullscreen
src/core/settings-defaults.ts:62

完整事实表(含每条断言的推导命令)见专栏仓库的 research/pi-agent/FACTS.md。