CLI Entry and Dispatch
main.ts is the process entry point for the pi command. It parses command-line arguments into an AppMode, wires up an AgentSessionRuntime, and dispatches to one of three run modes: interactive TUI, one-shot print, or JSON-RPC. Beyond mode dispatch, it also handles migrations, session resolution, model selection, and file-argument preprocessing. Think of it as the front layer that translates a shell invocation into a runtime invocation.
Responsibilities
- Parse and dispatch:
parseArgsproducesArgs,resolveAppModecomputesAppMode, and that selectsrunRpcMode/InteractiveMode/runPrintMode. Seepackages/coding-agent/src/main.ts:98-109andpackages/coding-agent/src/main.ts:673-726. - Session resolution: conflict checks and path resolution among
--continue,--resume,--session <id|path>,--fork <id>,--no-session, locating the session identifier to its.jsonlon disk. Seepackages/coding-agent/src/main.ts:188-212andpackages/coding-agent/src/main.ts:147-172. - Service wiring: calls
createAgentSessionServices+createAgentSessionRuntime, threading together cwd, agentDir, authStorage, and extension/skill/theme paths. Seepackages/coding-agent/src/main.ts:522-560. - Pre-run short-circuits:
pi --version,pi --export,pi package ...,pi config ...bypass the runtime and callprocess.exitdirectly. Seepackages/coding-agent/src/main.ts:431-478.
Design Motivation
Why keep the entry point in its own file? CLI parsing, session selection, and migrations happen once, at process startup. Stuffing these into createAgentSession (the SDK entry) would hurt SDK reuse — SDK callers are usually programs that already have a runtime context, don't need to parse argv, and don't want process-level stdout takeover. So main.ts only takes on process-level chores: stdout takeover, signal handling, migrations, version checks, fork validation. Everything else is delegated to the SDK and runtime factories.
Key Files
packages/coding-agent/src/main.ts:54-71—readPipedStdin, reads piped stdin when not a TTY to use as the initial message for print mode.packages/coding-agent/src/main.ts:98-109—resolveAppMode, combinesArgs.mode/Args.printwithstdin.isTTYto compute the fourAppModevalues.packages/coding-agent/src/main.ts:423-478— start ofmain: migrations, offline, package/config short-circuits, argument diagnostics.packages/coding-agent/src/main.ts:522-560—createRuntimefactory closure, captures CLI-parsed extension/skill/theme paths and passes them tocreateAgentSessionServices.packages/coding-agent/src/main.ts:673-726— the three-way dispatch:runRpcMode/new InteractiveMode/runPrintMode.packages/coding-agent/src/cli/— CLI submodule directory, containingargs.ts,file-processor.ts,initial-message.ts,session-picker.ts,list-models.ts.
resolveAppMode checks the --mode flag first, then falls back to whether stdin is a TTY, so piped usage like echo x | pi automatically goes to print mode:
// packages/coding-agent/src/main.ts:98-109
function resolveAppMode(parsed: Args, stdinIsTTY: boolean): AppMode {
if (parsed.mode === "rpc") {
return "rpc";
}
if (parsed.mode === "json") {
return "json";
}
if (parsed.print || !stdinIsTTY) {
return "print";
}
return "interactive";
}The three-way dispatch uses appMode to pick the branch; print and json share runPrintMode, with the difference in toPrintOutputMode(appMode):
// packages/coding-agent/src/main.ts:673-726
if (appMode === "rpc") {
printTimings();
await runRpcMode(runtime);
} else if (appMode === "interactive") {
// ...
const interactiveMode = new InteractiveMode(runtime, {
migratedProviders,
modelFallbackMessage,
initialMessage,
initialImages,
initialMessages: parsed.messages,
verbose: parsed.verbose,
});
// ...
await interactiveMode.run();
} else {
printTimings();
const exitCode = await runPrintMode(runtime, {
mode: toPrintOutputMode(appMode),
messages: parsed.messages,
initialMessage,
initialImages,
});
// ...
}Data Flow
The dispatch path after argv arrives:
Edges and Failures
- Fork conflicts:
--forkcannot be combined with--session,--continue,--resume, or--no-session. Seepackages/coding-agent/src/main.ts:188-202. - stdout takeover: non-interactive modes call
takeOverStdout(), redirectingconsole.logto an internal buffer to keep TUI escapes from polluting print/rpc output;restoreStdoutruns on exit. - Missing session cwd: when
--sessionpoints to another project, interactive mode prompts the user to choose, while print/rpc fail outright. Seepackages/coding-agent/src/main.ts:502-514. - Benchmark mode: under
startupBenchmark, onlyinteractiveMode.init()runs beforestop()— the main loop is skipped.
Summary
main.ts is a thin shell: it translates argv and stdin into a runtime factory closure, then hands control to one of three modes. For the runtime factory internals, see createAgentSession Wiring; for interactive mode details, see TUI Interactive Mode; for non-interactive modes, see print and rpc Modes.