CLI Modes & Dispatch
From bin entry to interactive/print/rpc/json modes
“main.ts dispatches the Mode; project trust gates before the prompt”
Deep Dive
main.ts dispatches the Mode; project trust gates before the prompt
Learning Objectives
After this chapter you will be able to:
- Explain the "thin bin + reusable main" layering of cli.ts → main.ts
- Describe how parseArgs resolves
openai/gpt-4o:highshorthand into a ScopedModel - Explain why project trust gating must precede extension loading
- Understand how four modes (interactive/print/rpc/json) share AgentSession
Key Source Files
| File | Lines | Responsibility |
|---|---|---|
packages/coding-agent/src/cli.ts | 20 | bin entry: set process.title, env vars, delegate to main |
packages/coding-agent/src/main.ts | 916 | Main logic: arg parsing, trust, mode dispatch |
packages/coding-agent/src/cli/args.ts | 400 | parseArgs: shorthand resolution, subcommand detection |
packages/coding-agent/src/config.ts | 566 | Config merge: env + file + CLI flags |
Design Motivation: Why Is bin Only 20 Lines?
Problem: pi has multiple launch paths — CLI direct run, SDK programmatic invocation, RPC server mode. If all initialization logic lives in the bin entry, SDK/RPC cannot reuse it.
Analogy: Like Unix's main() vs libmain() separation — the thin wrapper does only process-level setup (title, signals, env vars); real logic lives in a main function callable by anyone.
Design principle:
- cli.ts: process-level initialization (non-reusable parts)
- main.ts: business logic (callable by SDK/RPC/tests directly)
cli.ts: The 20-Line Thin Entry
#!/usr/bin/env node
// cli.ts — process-level setup only
process.title = "pi"
process.env.PI_CODING_AGENT = "true"
process.removeAllListeners("warning") // suppress Node deprecation warnings
// Configure undici dispatcher (proxy, connection pool)
import { setGlobalDispatcher } from "undici"
setGlobalDispatcher(buildDispatcher())
// Delegate to main — all real logic lives here
import { main } from "./main"
main(process.argv.slice(2))
Why suppress warnings? Some of pi's dependencies trigger Node deprecation warnings. For users these are noise — pi's developers track them in CI, not in end-user terminals.
main.ts: The Dispatch Pipeline
main(argv):
① parseArgs(argv)
├── --model openai/gpt-4o:high → ScopedModel { provider, model, thinking }
├── --mode interactive|print|rpc|json
└── subcommand detection: install/update/list/config → short-circuit to packageManagerCli
② Package manager subcommand? → packageManagerCli(args) → return (short-circuit)
③ resolveProjectTrusted(cwd)
├── trusted → continue
└── untrusted → refuse to load extensions/skills, restrict toolset
④ Load config (config.ts merges env + ~/.pi/config + .pi/config)
⑤ switch (mode):
├── interactive → InteractiveMode (TUI, → Chapter 16)
├── print → runPrintMode (single output, no TUI)
├── rpc → runRpcMode (JSON-RPC server, → Chapter 19)
└── json → JSON mode (structured output)
⑥ All modes → createAgentSession() → shared orchestration (→ Chapter 11)
Arg Parsing: Shorthand Syntax
// args.ts resolution logic (simplified)
function resolveModelFlag(spec: string): ScopedModel {
// "openai/gpt-4o:high" → { provider: "openai", model: "gpt-4o", thinking: "high" }
const [fullName, thinking] = spec.split(":")
const [provider, model] = fullName.split("/")
return { provider, model, thinking: thinking as ThinkingLevel }
}
Supported shorthands:
anthropic/claude-sonnet-4-5→ provider + modelopenai/gpt-4o:high→ + thinking levelgpt-4o→ auto-infer provider (lookup from registered model catalog)
Project Trust: Security Gate
async function resolveProjectTrusted(cwd: string): Promise<boolean> {
// ① Check ~/.pi/trusted-projects.json
if (isExplicitlyTrusted(cwd)) return true
// ② Check .pi/trust.json (project self-declaration — requires user confirmation)
if (hasTrustFile(cwd) && await confirmWithUser()) return true
// ③ Untrusted → restricted mode
return false
}
Why must trust precede extension loading?
- Extensions can register tools, subscribe to events, execute arbitrary code
- An untrusted project could place malicious extensions in
.pi/extensions/ - Verify trust first, then decide whether to load extensions — gate before capability
Analogy: Like a browser's "Allow this site to run scripts?" — ask permission first, grant capability second.
Four Modes Share Orchestration
interactive ─┐
print ───────┼──→ createAgentSession() ──→ AgentSession (→ Chapter 11)
rpc ─────────┤ │
json ────────┘ └── same prompt() entry
same event stream
same session persistence
Differences are I/O layer only:
- interactive: TUI rendering + keyboard input
- print: stdout plain text + stdin read-once
- rpc: WebSocket/stdio JSON-RPC
- json: structured JSON output
Core logic (agent loop, tool execution, compaction, session) implemented once.
Engineering Insights
-
916 lines in main.ts: Looks large, but includes arg parsing, trust verification, config merging, version checks, error reporting. The actual "dispatch" is only ~30 lines of switch — complexity lives in "preparation before dispatch."
-
config.ts 566 lines: Config comes from 5 layers (defaults → env → ~/.pi/config → .pi/config → CLI flags), each overriding the previous. Merge logic handles nested objects, array append vs replace semantics.
-
Subcommand short-circuit:
pi install/pi updatedon't go through the agent path — straight to the package manager (→ Chapter 15). This avoids the absurd experience of "initializing model connections just to install a package."
Cross-Chapter Links
| Direction | Chapter | Relationship |
|---|---|---|
| Prerequisite | Ch01 Agent Loop | All modes ultimately drive agentLoop |
| Follow-up | Ch11 AgentSession | Shared orchestration layer for all four modes |
| Follow-up | Ch15 Package Manager | Subcommand short-circuits to package management |
| Follow-up | Ch16 Interactive TUI | Concrete implementation of interactive mode |
| Follow-up | Ch18 Settings & Trust | Complete trust mechanism design |
| Follow-up | Ch19 RPC Server | Concrete implementation of rpc mode |
Check Your Understanding
- If trust verification were moved after extension loading, what security risk would arise?
- Why do package manager subcommands "short-circuit" instead of going through full agent initialization?
- What's the benefit of four modes sharing AgentSession? What problems would arise if each mode implemented its own?
Takeaways
- Thin bin (20 lines), reusable main — SDK/RPC/tests call main directly
- Trust gate precedes extension loading — gate before capability
- Shorthand syntax
provider/model:thinkingparsed in one pass - Four modes share AgentSession; differences are I/O layer only
This chapter is based on the main-branch source of earendil-works/pi.