跳转至

60. CLI 入口与三种运行模式

这个模块干什么

cli.ts 把 Node 进程变成 pi:设置进程标题和 PI_CODING_AGENT,预先配置 HTTP dispatcher,再把去掉 node/脚本名的参数交给 main()packages/coding-agent/src/cli.ts:8-20

main() 先处理包管理和配置子命令,再解析参数、创建会话运行时,最后在 interactive、print/json、rpc 三条执行路径中选一条。packages/coding-agent/src/main.ts:473-518 packages/coding-agent/src/main.ts:811-863

三种模式共用同一个 AgentSessionRuntime,差别主要是输入面、输出协议、扩展获得的 UI 能力,以及进程何时结束。packages/coding-agent/src/main.ts:741-748 packages/coding-agent/src/modes/print-mode.ts:32-158 packages/coding-agent/src/modes/rpc/rpc-mode.ts:53-61

index.ts 既是 SDK 的公共门面,也重新导出 main 和三个模式的程序化 API;命令行入口本身并不要求外部调用者直接 import 模式内部文件。packages/coding-agent/src/index.ts:194-221 packages/coding-agent/src/index.ts:326-344

核心文件速查

文件 一句话职责 必读优先级
packages/coding-agent/src/cli.ts CLI shebang 入口:初始化进程环境和 HTTP dispatcher,随后调用 main(process.argv.slice(2))packages/coding-agent/src/cli.ts:1-20 ⭐⭐⭐
packages/coding-agent/src/main.ts 决定模式、解析启动参数、构造 runtime、准备首条消息并分派到三种模式。packages/coding-agent/src/main.ts:100-115 packages/coding-agent/src/main.ts:473-863 ⭐⭐⭐
packages/coding-agent/src/cli/args.ts 将 argv 拆成已知选项、消息、@file 和可留给扩展的未知长选项。packages/coding-agent/src/cli/args.ts:10-55 packages/coding-agent/src/cli/args.ts:63-209 ⭐⭐⭐
packages/coding-agent/src/cli/initial-message.ts 合并 stdin、文件文本和第一条 CLI 消息,并从 messages 中消费这第一项。packages/coding-agent/src/cli/initial-message.ts:16-42 ⭐⭐
packages/coding-agent/src/cli/file-processor.ts @file 变成 XML 风格文本片段或图片附件,并在失败时终止。packages/coding-agent/src/cli/file-processor.ts:23-86 ⭐⭐
packages/coding-agent/src/cli/startup-ui.ts 在正式 InteractiveMode 之前提供首装、选择和输入所需的小型 TUI。packages/coding-agent/src/cli/startup-ui.ts:77-100 packages/coding-agent/src/cli/startup-ui.ts:134-238 ⭐⭐
packages/coding-agent/src/modes/interactive/interactive-mode.ts 构建常驻 TUI,绑定扩展、渲染现有会话,并循环等待用户输入。packages/coding-agent/src/modes/interactive/interactive-mode.ts:679-813 packages/coding-agent/src/modes/interactive/interactive-mode.ts:832-920 ⭐⭐⭐
packages/coding-agent/src/modes/print-mode.ts 单次发送 prompt;text 只打印最终 assistant 文本,json 则输出会话事件,随后 dispose。packages/coding-agent/src/modes/print-mode.ts:32-158 ⭐⭐⭐
packages/coding-agent/src/modes/rpc/rpc-mode.ts 以 JSONL 常驻服务处理 RPC 命令、转发事件,并把扩展 UI 映射为协议请求。packages/coding-agent/src/modes/rpc/rpc-mode.ts:53-61 packages/coding-agent/src/modes/rpc/rpc-mode.ts:312-363 ⭐⭐⭐
packages/coding-agent/src/rpc-entry.tsmodes/index.tsindex.ts 前者强制 RPC,后两者把模式类型和实现组织成内部与公共出口。packages/coding-agent/src/rpc-entry.ts:1-12 packages/coding-agent/src/modes/index.ts:1-15 packages/coding-agent/src/index.ts:326-344 ⭐⭐

精读

1. cli.ts:真正的进程入口很薄

文件以 #!/usr/bin/env node 开头,因此发布后的可执行脚本由 Node 解释。packages/coding-agent/src/cli.ts:1-1

它导入的业务入口只有 main,其余两个依赖是应用名和 HTTP dispatcher。packages/coding-agent/src/cli.ts:8-10

process.title = APP_NAME 设置进程标题,PI_CODING_AGENT = "true" 给子进程一个“正在 Pi 内部”的标记。packages/coding-agent/src/cli.ts:12-13

它还把 process.emitWarning 换成空函数;这是入口级的全局行为,不是某一种模式的配置。packages/coding-agent/src/cli.ts:14-14

configureHttpDispatcher()main() 前运行,注释说明目的是在 provider SDK 发请求前配置 undici 的全局 dispatcher。packages/coding-agent/src/cli.ts:16-18

简化调用是 main(args: string[]): Promise<void>,其中 args 正是 process.argv.slice(2)packages/coding-agent/src/cli.ts:20-20 packages/coding-agent/src/main.ts:473-473

容易漏掉的是这里没有 await 或顶层错误处理;启动后的控制权直接交给异步 main()packages/coding-agent/src/cli.ts:20-20

2. cli/args.ts:先保留输入形状,再解释参数

Mode 只允许 "text" | "json" | "rpc",而 Args 同时装模式、模型、会话、资源、消息、文件参数、未知 flag 与诊断。packages/coding-agent/src/cli/args.ts:10-55

parseArgs(args) 先初始化空 messagesfileArgsunknownFlagsdiagnostics,再按顺序扫描 argv。packages/coding-agent/src/cli/args.ts:63-72

--mode 只在值属于三个字面量时写入 result.mode;无效值不会被这里改写成默认模式。packages/coding-agent/src/cli/args.ts:78-82

--thinkingisValidThinkingLevel() 检验;非法级别会产生 warning 而不是立刻退出。packages/coding-agent/src/cli/args.ts:57-61 packages/coding-agent/src/cli/args.ts:130-139

-p/--print 可顺带消费紧随其后的一个非 flag、非 @ 值为第一条消息,但以 --- 开头的值例外地仍可当消息。packages/coding-agent/src/cli/args.ts:140-146

@path 去掉 @ 后进入 fileArgs;普通非 flag 参数则按出现顺序进入 messagespackages/coding-agent/src/cli/args.ts:186-187 packages/coding-agent/src/cli/args.ts:202-206

未识别的 --name=value 保存为字符串,未识别的 --name value 会消费普通值,否则保存 true;这正是扩展 flag 能延迟到 runtime 再解释的入口。packages/coding-agent/src/cli/args.ts:188-201

短形式的未知 -x 不走扩展保留通道,而是追加 error 诊断。packages/coding-agent/src/cli/args.ts:202-204

资源开关的解析只是把值放进 Args-e 收集 extensions,--skill 收集 skills,--prompt-template--theme 同理,--no-* 设置禁用布尔值。packages/coding-agent/src/cli/args.ts:149-170

printHelp(extensionFlags?) 会在静态帮助之后附加已加载扩展注册的 flag,因此完整帮助必须在资源加载之后才能生成。packages/coding-agent/src/cli/args.ts:212-222 packages/coding-agent/src/main.ts:752-758

3. main.ts(前半):模式判定与启动前工作

resolveAppMode(parsed, stdinIsTTY, stdoutIsTTY) 的优先级是显式 rpc、显式 json、--print 或任一端不是 TTY 的 print,否则才是 interactive。packages/coding-agent/src/main.ts:100-111

toPrintOutputMode() 只把最终 AppMode 映射为 "text""json",因此 json 不是单独的运行器。packages/coding-agent/src/main.ts:113-115

main() 合并内建扩展和调用者传入的 extensionFactories;SDK 嵌入方可以通过 MainOptions 把内联扩展插入同一启动流程。packages/coding-agent/src/main.ts:469-476

--offline 或真值 PI_OFFLINE 会同时设定 PI_OFFLINE=1PI_SKIP_VERSION_CHECK=1packages/coding-agent/src/main.ts:95-98 packages/coding-agent/src/main.ts:475-480

启动先用“不信任项目”的 SettingsManager 读取全局代理设置并配置 dispatcher;包管理子命令和 config 子命令在一般参数解析前短路返回。packages/coding-agent/src/main.ts:486-507

parseArgs() 的 diagnostics 会逐项写 stderr,含 error 时退出码为 1;版本和 export 也在创建 runtime 前退出。packages/coding-agent/src/main.ts:509-538

非交互且不是纯 help/list-models 元数据命令时,main() 接管 stdout,避免普通日志混入机器可读的输出。packages/coding-agent/src/main.ts:540-544 packages/coding-agent/src/main.ts:117-119

RPC 明确拒绝 @file 参数,因为该模式已经把 stdin 留给 JSON-RPC。packages/coding-agent/src/main.ts:546-549 packages/coding-agent/src/main.ts:766-773

迁移在 session/runtime 创建前执行;首装向导只在 interactive、非 help/list-models 且条件满足时运行。packages/coding-agent/src/main.ts:554-566

createSessionManager()--no-session、help、list-models 变成内存会话;其余情况再处理 fork、指定 session、resume、continue 或新建。packages/coding-agent/src/main.ts:264-354

恢复到另一个 cwd 的会话前,main() 会检查缺失 cwd;interactive 可以用启动 TUI 选择继续,非交互则报错退出。packages/coding-agent/src/main.ts:568-590 packages/coding-agent/src/cli/startup-ui.ts:134-163

4. main.ts(中段):把 argv 变成可运行 session

createRuntime 闭包接收最终 cwd、agentDir、sessionManager 与可选 sessionStartEvent,并据此创建 cwd 绑定的服务。packages/coding-agent/src/main.ts:615-621

它把 CLI 给出的 extension、skill、prompt-template、theme 路径先按 cwd 解析,再连同 --no-* 资源开关传给 createAgentSessionServices()packages/coding-agent/src/main.ts:611-614 packages/coding-agent/src/main.ts:633-677

资源加载错误会被转成 runtime diagnostics;extension 的加载错误还会获得“用 -ne 重试”的提示。packages/coding-agent/src/main.ts:679-687 packages/coding-agent/src/main.ts:790-797

模型候选优先取 CLI --models,否则取 settings;resolveModelScope() 之后,buildSessionOptions() 才处理 CLI 模型、thinking 和工具筛选。packages/coding-agent/src/main.ts:689-703 packages/coding-agent/src/main.ts:357-452

--model 支持单独 provider 或 provider/model 的解析,并可能从模型模式里的 :thinking 得到 thinking 值;显式 --thinking 最后覆盖它。packages/coding-agent/src/main.ts:372-397 packages/coding-agent/src/main.ts:421-424

如果没有 CLI 模型、又是新会话且存在 scoped models,代码优先复用仍在 scope 内的已保存默认模型,否则取 scope 第一个。packages/coding-agent/src/main.ts:399-418

--api-key 不是写入设置:它在 session model 已选定时调用 setRuntimeApiKey(),随后刷新可用模型。packages/coding-agent/src/main.ts:705-715

服务和选项最终交给 createAgentSessionFromServices();外层再由 createAgentSessionRuntime() 承担 session 切换/重绑等 runtime 生命周期。packages/coding-agent/src/main.ts:717-745

帮助和列模型虽然也要加载资源/runtime:前者要收集扩展 flags,后者要依赖 modelRuntime 的可用模型。packages/coding-agent/src/main.ts:752-764 packages/coding-agent/src/cli/list-models.ts:29-58

5. cli/initial-message.tsfile-processor.ts:首条 prompt 的拼装规则

prepareInitialMessage() 没有 @file 时直接调用 buildInitialMessage();有文件时先 processFileArguments(),再将文字和图片一并传入。packages/coding-agent/src/main.ts:121-140

buildInitialMessage({ parsed, fileText, fileImages, stdinContent }) 的拼接顺序是 stdin、文件文本、第一条 CLI 消息。packages/coding-agent/src/cli/initial-message.ts:20-40

第一条 CLI 消息被 shift() 移走,后续消息留在 parsed.messages,供 interactive 或 print 继续逐条发送。packages/coding-agent/src/cli/initial-message.ts:34-37 packages/coding-agent/src/main.ts:820-827 packages/coding-agent/src/main.ts:851-856

没有任何文字部分时,initialMessageundefined;图片数组同样只有非空时才保留。packages/coding-agent/src/cli/initial-message.ts:39-42

文件处理先经 resolveReadPath()resolve() 定位,再检查存在性与空文件;找不到文件会写错误并退出。packages/coding-agent/src/cli/file-processor.ts:29-46

可识别图像经过 processImage() 后进入 ImageContent[],同时在文本中加一个 <file ...> 引用或处理提示。packages/coding-agent/src/cli/file-processor.ts:48-72

普通文件以 UTF-8 读取,包装成带绝对路径的 <file name="..."> 块;读取失败同样退出。packages/coding-agent/src/cli/file-processor.ts:73-82

读取 piped stdin 只在非 RPC 下发生;若确实读到内容且原判定是 interactive,main() 会把最终模式改成 print。packages/coding-agent/src/main.ts:58-74 packages/coding-agent/src/main.ts:766-781

6. InteractiveMode:常驻终端界面如何接上 session

构造函数保存 runtime、注册 session 无效化和重绑回调,并创建 TUI、各容器、默认编辑器、footer 与 theme controller。packages/coding-agent/src/modes/interactive/interactive-mode.ts:448-495

init() 只做一次:先注册信号处理器、确保 fd/rg,然后按 header、资源、聊天、待发送、状态、widgets、editor、footer 的顺序加入根 TUI。packages/coding-agent/src/modes/interactive/interactive-mode.ts:679-720

键处理和 editor 提交处理在 ui.start() 前安装;TUI 启动后才把 isInitialized 设为 true。packages/coding-agent/src/modes/interactive/interactive-mode.ts:722-727 packages/coding-agent/src/modes/interactive/interactive-mode.ts:2551-2617 packages/coding-agent/src/modes/interactive/interactive-mode.ts:2644-2664

首帧的关键顺序是:应用主题和 header,requestRender(),再 rebindCurrentSession(),随后 renderInitialMessages()packages/coding-agent/src/modes/interactive/interactive-mode.ts:729-813

rebindCurrentSession({ renderBeforeBind: true }) 会先清旧订阅、应用设置、渲染已有 session、订阅 agent,再 bind extensions;这让扩展的资源列表出现在已有消息之前。packages/coding-agent/src/modes/interactive/interactive-mode.ts:1732-1747 packages/coding-agent/src/modes/interactive/interactive-mode.ts:793-797

TUI 绑定传给 session.bindExtensions() 的是 mode: "tui" 和完整的 uiContext,并提供 new session、fork、tree、switch、reload、shutdown 等 command actions。packages/coding-agent/src/modes/interactive/interactive-mode.ts:1629-1698

绑定完成后会注册资源主题、重建自动补全、安装扩展快捷键并显示资源/启动提示。packages/coding-agent/src/modes/interactive/interactive-mode.ts:1700-1707

renderInitialMessages()sessionManager.buildContextEntries() 取得可渲染条目,渲染时同时更新 footer 和编辑器历史,再补充未信任项目与 compacted 状态。packages/coding-agent/src/modes/interactive/interactive-mode.ts:3463-3478

run()init() 后异步刷新模型、检查版本与包更新、检查 tmux;这些后台任务不阻塞首次输入。packages/coding-agent/src/modes/interactive/interactive-mode.ts:832-869

它先发送启动传来的 initial message 与剩余 messages,之后无限循环 getUserInput()session.prompt()packages/coding-agent/src/modes/interactive/interactive-mode.ts:871-920

getUserInput() 先消费已排队的文本;没有时返回一个在 editor submit 后 resolve 的 Promise。packages/coding-agent/src/modes/interactive/interactive-mode.ts:3500-3512

7. print-mode.ts:一次会话、两种输出编码

runPrintMode(runtimeHost, options): Promise<number> 接收 text/json、首条消息、图片和后续消息;它保留可变 session 是为了 runtime 重绑后更新引用。packages/coding-agent/src/modes/print-mode.ts:17-26 packages/coding-agent/src/modes/print-mode.ts:32-38

重绑时它以 mode: "print""json" bind extensions,提供非 TUI 的 command actions;无论哪种模式都能 new、fork、tree、switch、reload。packages/coding-agent/src/modes/print-mode.ts:67-109

json 模式订阅全部 session event 并逐条 JSON.stringify(event) + "\n" 写 raw stdout;开始时还会输出 session header。packages/coding-agent/src/modes/print-mode.ts:103-117

它先发送 initialMessage(可带图片),再按数组顺序 await 后续每一条消息。packages/coding-agent/src/modes/print-mode.ts:119-127

text 模式只检查最后一条消息:正常时打印其中所有 text content,erroraborted stopReason 则写 stderr 并返回 1。packages/coding-agent/src/modes/print-mode.ts:129-148

无论成功、异常还是信号,finally 都卸载信号、dispose runtime 并 flush raw stdout;所以它是一次性运行器而不是常驻服务。packages/coding-agent/src/modes/print-mode.ts:40-65 packages/coding-agent/src/modes/print-mode.ts:149-158

8. rpc-entry.tsrpc-mode.tsjsonl.ts:stdin/stdout 都是协议

rpc-entry.ts 和普通入口做同样的标题、环境、dispatcher 初始化,但调用 main(["--mode", "rpc", ...argv]) 强制 RPC 优先。packages/coding-agent/src/rpc-entry.ts:1-12

runRpcMode() 一开始再次接管 stdout,随后把任意输出对象序列化为一条 JSONL。packages/coding-agent/src/modes/rpc/rpc-mode.ts:53-61

协议的输入是带可选 id 的 discriminated RpcCommand;其类型覆盖 prompt、状态、模型、thinking、队列、compact、bash、session 和命令枚举。packages/coding-agent/src/modes/rpc/rpc-types.ts:20-73

每个常规命令可返回统一的 { type: "response", command, success } 形状;失败响应携带字符串 errorpackages/coding-agent/src/modes/rpc/rpc-mode.ts:63-76 packages/coding-agent/src/modes/rpc/rpc-types.ts:114-231

RPC 重绑把 extension 的 mode 设为 "rpc",订阅 session event 写入协议输出,并额外等待 raw stdout 背压。packages/coding-agent/src/modes/rpc/rpc-mode.ts:312-363

prompt 不等待整轮 agent 完成:预检成功就输出成功 response,真正的 agent event 继续异步流出。packages/coding-agent/src/modes/rpc/rpc-mode.ts:393-415

RPC 的 get_commands 把 extension、prompt template、skill 汇合成带 source/sourceInfo 的命令清单,而不是假定客户端知道本地资源目录。packages/coding-agent/src/modes/rpc/rpc-mode.ts:662-693

扩展需要 select、confirm、input 或 editor 时,RPC 将请求放入 pendingExtensionRequests 并写出 extension_ui_request;客户端必须用 extension_ui_response 对应 id。packages/coding-agent/src/modes/rpc/rpc-mode.ts:78-130 packages/coding-agent/src/modes/rpc/rpc-mode.ts:748-762

只有字符串数组 widget 被转发;component factory、footer、header、编辑器组件和终端原始输入都因没有 TUI 而不支持。packages/coding-agent/src/modes/rpc/rpc-mode.ts:162-207 packages/coding-agent/src/modes/rpc/rpc-mode.ts:209-310

attachJsonlLineReader() 只按 LF 查找记录边界,故意不用 readline,因为 U+2028/U+2029 可以合法地出现在 JSON 字符串内部。packages/coding-agent/src/modes/rpc/jsonl.ts:4-21 packages/coding-agent/src/modes/rpc/jsonl.ts:29-57

stdin 结束会触发 shutdown;正常初始化后函数返回一个永不 resolve 的 Promise,直到 shutdown 调用 process.exit()packages/coding-agent/src/modes/rpc/rpc-mode.ts:708-725 packages/coding-agent/src/modes/rpc/rpc-mode.ts:784-800

9. modes/index.ts 与根 index.ts:内部桶与公开 SDK 面

modes/index.ts 只导出 InteractiveModerunPrintModeRpcClientrunRpcMode 及相应类型,是 main.ts 的模式桶。packages/coding-agent/src/modes/index.ts:1-15 packages/coding-agent/src/main.ts:46-46

index.ts 不只暴露 main,还暴露 InteractiveMode、print/rpc 运行器和 RpcClient,所以嵌入者可在不走 CLI 的情况下复用这些层。packages/coding-agent/src/index.ts:326-344

同一公共入口还导出 parseArgsSettingsManagerModelRuntimeDefaultResourceLoader、extension API 和 session/runtime 工厂;这解释了为什么模式文件尽量依赖内部抽象而非 argv。packages/coding-agent/src/index.ts:3-3 packages/coding-agent/src/index.ts:52-193 packages/coding-agent/src/index.ts:194-267

数据流

下面这条链描述默认交互启动到第一帧;--mode rpc 与 print/json 只在最后的分派节点换轨。packages/coding-agent/src/main.ts:100-115 packages/coding-agent/src/main.ts:811-863

node pi
  -> cli.ts: 设置 process.title / PI_CODING_AGENT / dispatcher
  -> main(argv): package/config 子命令短路,parseArgs
  -> resolveAppMode: rpc > json > print/非TTY > interactive
  -> migrations + session manager + createAgentSessionServices
  -> createAgentSessionRuntime
  -> read stdin + @file,prepareInitialMessage,initTheme
  -> new InteractiveMode(runtime, initial messages)
  -> InteractiveMode.init()
       -> 构造容器并 ui.start()
       -> 首次 requestRender():空骨架/header 可画出
       -> rebindCurrentSession({ renderBeforeBind: true })
            -> renderCurrentSessionState() / renderInitialMessages()
            -> subscribeToAgent() / bindExtensions(mode: "tui")
            -> 显示资源、补全与启动提示
  -> InteractiveMode.run(): 可选首条 prompt,随后 getUserInput() -> session.prompt()

链中 cli.ts 的环境设置和 dispatcher 初始化发生在 main() 之前。packages/coding-agent/src/cli.ts:12-20

参数解析、模式判定和一般运行时的建造位置分别是 main() 的 509-544、554-745 与 766-798 行。packages/coding-agent/src/main.ts:509-544 packages/coding-agent/src/main.ts:554-745 packages/coding-agent/src/main.ts:766-798

interactive 分支在 main() 中创建 InteractiveMode 并 await run();print 分支 await runPrintMode() 后恢复 stdout;rpc 分支 await 不会自然返回的 runRpcMode()packages/coding-agent/src/main.ts:816-863 packages/coding-agent/src/modes/rpc/rpc-mode.ts:799-800

首个可见布局由 init() 中的根 children、ui.start()、主题/header 和第一次 requestRender() 建立,随后才开始 session/extension 的重绑。packages/coding-agent/src/modes/interactive/interactive-mode.ts:707-727 packages/coding-agent/src/modes/interactive/interactive-mode.ts:729-797

自测

  1. resolveAppMode() 为什么把“stdin 或 stdout 不是 TTY”判为 print,而不是 interactive?packages/coding-agent/src/main.ts:100-111

  2. 一个未知的 --plan=fast 怎样穿过 parseArgs(),最后在哪里被交给 runtime 服务?packages/coding-agent/src/cli/args.ts:188-201 packages/coding-agent/src/main.ts:633-639

  3. cat file | pi "问题" 中,stdin、@file、第一条 message 的拼接顺序是什么,为什么最后模式会变为 print?packages/coding-agent/src/cli/initial-message.ts:20-42 packages/coding-agent/src/main.ts:766-781

  4. print 的 text 与 json 输出各自订阅/打印什么,哪个路径负责 runtime dispose?packages/coding-agent/src/modes/print-mode.ts:103-158

  5. RPC 客户端为什么不能用普通按行读取器,且它如何回复 extension 的输入请求?packages/coding-agent/src/modes/rpc/jsonl.ts:14-21 packages/coding-agent/src/modes/rpc/rpc-mode.ts:78-130 packages/coding-agent/src/modes/rpc/rpc-mode.ts:748-762