ππ Agent
Chapter 10 / 21

CLI Modes & Dispatch

From bin entry to interactive/print/rpc/json modes

main.ts dispatches the Mode; project trust gates before the prompt
No simulation scenario available for this chapter

Deep Dive

~7 min read·7084 chars

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:high shorthand 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

FileLinesResponsibility
packages/coding-agent/src/cli.ts20bin entry: set process.title, env vars, delegate to main
packages/coding-agent/src/main.ts916Main logic: arg parsing, trust, mode dispatch
packages/coding-agent/src/cli/args.ts400parseArgs: shorthand resolution, subcommand detection
packages/coding-agent/src/config.ts566Config 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 + model
  • openai/gpt-4o:high → + thinking level
  • gpt-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

  1. 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."

  2. 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.

  3. Subcommand short-circuit: pi install/pi update don'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

DirectionChapterRelationship
PrerequisiteCh01 Agent LoopAll modes ultimately drive agentLoop
Follow-upCh11 AgentSessionShared orchestration layer for all four modes
Follow-upCh15 Package ManagerSubcommand short-circuits to package management
Follow-upCh16 Interactive TUIConcrete implementation of interactive mode
Follow-upCh18 Settings & TrustComplete trust mechanism design
Follow-upCh19 RPC ServerConcrete implementation of rpc mode

Check Your Understanding

  1. If trust verification were moved after extension loading, what security risk would arise?
  2. Why do package manager subcommands "short-circuit" instead of going through full agent initialization?
  3. 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:thinking parsed 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.