-
Notifications
You must be signed in to change notification settings - Fork 3
Cli
The src/cli area is every command-line surface Clio Coder ships as the
clio-coder binary. It owns argument parsing, global-flag extraction, and the
subcommand dispatch table in src/cli/index.ts, then delegates to one module
per command: the interactive boot hand-off, the headless single-turn runner,
the configure wizard and target setup, target listing and role selection,
fleet authoring and execution, the read-only fleet run viewer, and the
cross-session usage analyzer. It does not own the domain logic those commands
drive; it parses, preflights, and hands control to the composition root and the
domain modules.
The area's central design decision is cold-start cost. package.json maps the
clio-coder bin to dist/cli/index.js, whose entry is main at
src/cli/index.ts:168. main sets process.title, sets the AI_AGENT
environment marker, parses only the top-level flags statically, and then routes
every subcommand through a literal dynamic import(...) inside the
COMMAND_HANDLERS map. The comment at src/cli/index.ts:168 states this is the
"highest-value cut of the cold module-load tax," and the map doubles as the
command-recognition source used by the global value flags.
| Surface | Entry symbol | File |
|---|---|---|
| Process entry + dispatch | main |
src/cli/index.ts:168 |
| Headless turn command | runClioRun |
src/cli/run.ts:309 |
| Headless main-agent runner | runHeadlessMainAgent |
src/cli/modes/print.ts:624 |
| Configure wizard + settings | runConfigureCommand |
src/cli/configure.ts:2281 |
| First-run onboarding steps | runOnboardingWizard |
src/cli/configure-onboarding.ts:974 |
| Target list/use/profile | runTargetsCommand |
src/cli/targets.ts:178 |
| Fleet authoring + execution | runFleetCommand |
src/cli/fleet.ts:829 |
| Fleet run viewer | runFleetView |
src/cli/fleet-view.ts:681 |
| Usage analyzer | runUsageCommand |
src/cli/usage.ts:303 |
The dispatch table COMMAND_HANDLERS (a Map<string, CommandHandler>) in
src/cli/index.ts holds one handler per subcommand; each handler is a closure
whose body is a literal await import("./<name>.js"). extensions and ext
share one handler; export and import share one. A dev prefix resolves to
the same handlers as the unprefixed names via a second dispatch call. Retired
names (context-init, context-index, context-clear) stay in the
RETIRED_SUBCOMMANDS set so the top-level value flags cannot swallow them as a
flag value and boot a different mode.
The full path is:
-
maininsrc/cli/index.ts:168extracts global flags (--api-key,--no-context-files,--no-skills,--skill,--with-panes,--no-panes,--autonomy,--demo) and dispatchesrun. -
run's handler callsenableBootCompileCache, then dynamically importssrc/cli/run.tsand invokesrunClioRun(subArgs, bootOptions). -
runClioRun(src/cli/run.ts:309) parses the run flags, arms--timeoutas a coordinated-shutdown deadline, assembles the prompt from message tokens, piped stdin, and@filereferences, and runs a headless slash preflight that refuses interactive commands and answers display-only templates. - For the main-agent path it calls
takeOverStdout, thenrunClioCommand(src/cli/clio.ts) with aheadless: { prompt, mode, jsonEvents, ... }bundle.runClioCommandboots the orchestrator viabootOrchestrator(src/entry/orchestrator.ts), which loads the domain modules and resolves the model target. - The orchestrator calls
runHeadlessMainAgent(src/cli/modes/print.ts:624) on the created chat loop. That function subscribes tochat.onEvent, folds tool outcomes and usage into aHeadlessMainAgentReceiptStats, submits the prompt, and seals a receipt into the run ledger throughrecordHeadlessMainAgentReceipt.
runHeadlessMainAgent is the wrapper that owns the run's accounting; the
ChatLoop it receives does the turn. The HeadlessShutdownHooks interface it
accepts defaults to the process termination coordinator, so --timeout, a
SIGTERM, and a normal settle all seal the same receipt through one path.
runFleetView (src/cli/fleet-view.ts:681) is the read-only surface an
operator uses over SSH or inside a workers-view pane:
- It parses
--follow,--watch,--all, and four--*-dir overrides, thenapplyViewDirspins the already-resolved layout intoprocess.envbefore any read. - It computes
fleetInspectionScope(all)so--allwidens the inspection across projects; without it the view is scoped to this project. - For a run id it calls
resolveRunId(exact ledger id first, then a unique prefix), thenloadRunViewModel, which reads the ledger envelope, the run event journal at<state>/runs/<runId>/events.ndjson, and the sealed receipt. The transcript is capped atTRANSCRIPT_LIMIT = 400(src/cli/fleet-view.ts:78). -
renderRunViewis a pure function overRunViewModel; the snapshot path joins its lines onto stdout and exits. - With
--follow(or--watch <selection-file>) the command enters an alternate-screen loop that re-reads the journal atPOLL_MS = 250(src/cli/fleet-view.ts:76) until the terminal line lands, then stays open untilq. The--watchloop re-reads the selection file each poll, so the TUI retargets this process through one atomic file write with no socket traffic.
A fleet root id (fleet-<hex>, prefixed by FLEET_ROOT_PREFIX at
src/cli/fleet-view.ts:406) is not a run: it renders the step index from the
durable record under <state>/fleet-runs/<rootId>.json instead, one line per
step naming the run id to view next.
runTargetsCommand (src/cli/targets.ts:178) routes the use subcommand to
runUse, which:
- Parses the target id and the role flags (
--model,--orchestrator-model,--background-model,--fleet-model,--fleet-target). - Calls
unusedSharedModelReason, which refuses--modelwhen the role flags select a specific role and name its own model — the focused test intests/contracts/targets-use-unused-model.test.tspins the exact message--model 'dynamo/new' would not be usedand the remediation--orchestrator-model 'dynamo/new'. - Calls
useTargetInSettings(src/core/config.ts) to mutate a draft copy of the settings, returning the roles it touched. - For each selected role, verifies the chosen model against the runtime's live
inventory through
resolveSupportedWireModels, refusing a model the target does not advertise (exit 1) and warning when it cannot verify offline. - Commits the draft with one
updateSettingscall, so a refusal cannot partially update another role.
runConfigureCommand (src/cli/configure.ts:2281) branches on TTY. On a
terminal with no targets it launches runConfigLauncher, which offers
Quick Connect, Settings, and Diagnostics. The first-run path instead runs
runOnboardingWizard (src/cli/configure-onboarding.ts:974).
The wizard is a list of steps (STEPS at src/cli/configure-onboarding.ts:924):
category, runtime, target-id, credential, credential-value, url,
detected-runtime, model, thinking, context-window,
antigravity-colleague, and review. Each step draws its prompt, erases it
once answered, and leaves one row on a lifecycle rail saying what was chosen.
Escape moves the cursor back one step and writer.rewindTo(mark) erases the
rail rows the step left behind, so the screen is the answers so far plus the
question at hand. Steps that do not apply (applies(answers) false) are skipped
in whichever direction the cursor is moving.
Nothing persists until finish runs: applyAnswers mutates a preview of
readSettings(), validateSettings throws a SettingsValidationError if the
preview is invalid, and only then does updateSettings write. The focused test
tests/contracts/settings-target-wizard.test.ts demonstrates this with
ScriptedWizard — a subclass of TargetWizardSurface that drives the same
select/text prompts — asserting that a cancel at the review step leaves
readSettings() unchanged and that a late probe cancel cannot save a target.
BOOT_OPTION_COMMANDS (src/cli/index.ts:136) is the set {"run", "acp"}.
unhonoredBootFlag (src/cli/index.ts:140) returns a refusal for the first
boot flag a subcommand cannot honor. The effect is that --api-key K fleet run
fails with a message naming the flag and the command, rather than running on
the configured key with no word said. --with-panes/--no-panes
(PANES_FLAGS) are refused for every subcommand because they apply only to the
interactive session.
runHeadlessMainAgent (src/cli/modes/print.ts:624) owns the stdout contract
for the main-agent path. In text mode the session id is written to stderr the
moment it exists (writeTextSessionId), and the final answer — either the
assistant's text or, for a terminating tool, the tool's result line — is the
only thing written to stdout. The comment states that printing mid-workflow
chatter instead would hand the operator a dangling "let me try..." for a turn
that succeeded. The focused test
tests/extended-smoke/cli-core.test.ts asserts this with --json-events terminal, checking the event stream carries session, turn_start,
agent_end, and turn_end frames and that the receipt seals with outcome: "succeeded".
The onboarding wizard's Answers object is the only place an answer lives until
Save. The repo compiles with exactOptionalPropertyTypes, so each field is
explicitly | undefined; going back a step clears the answers that depended on
the one being changed by assigning undefined. The focused test
tests/contracts/settings-target-wizard.test.ts proves that a cancel at the
review step and a cancel during a probe both leave settings unchanged, and that
the "Add remains a draft until Save" assertion holds across the whole flow.
runFleetCommand (src/cli/fleet.ts:829) for the run subcommand performs a
preflight that validates the fleet graph, resolves every step's target and
model, and rejects missing or unresolvable targets before any write or model
call. The help text in fleet.ts states the preflight is "zero side effects":
a validation failure exits 2 and touches nothing.
evidenceLine in src/cli/fleet-view.ts authenticates the receipt against the
ledger envelope through inspectRunReceiptTrustStatus before any of its fields
are shown. A receipt that fails integrity contributes its failure reason and
nothing else; the task text and journal event details are sanitized and bounded
through sanitizeBounded because they cross the operator/worker trust seam.
-
New CLI subcommand: add an entry to
COMMAND_HANDLERSinsrc/cli/index.tswhose body is a literal dynamicimport("./<name>.js"). The literal form is load-bearing — the comment at the map states that "every dynamic import stays a literal so tsup can split command graphs" — and that adding a command in one place cannot leave a global flag free to swallow it as a value. -
New configure wizard step: add a
Stepobject to theSTEPSarray atsrc/cli/configure-onboarding.ts:924. The step'sappliesclosure decides whether it shows, and itsrunclosure must return one of theStepOutcomevalues (next,back,quit,cancel,credential). The order in the array is the forward direction; going back rewinds the rail to the step's row. -
New headless mode: the headless runner is
src/cli/modes/print.ts, alongsidejson-stream.tsandjsonl.ts. A new mode that needs the chat loop but different output (e.g. a different JSON projection) would live in this directory and be called fromsrc/cli/run.ts. -
New fleet view projection:
loadRunViewModelbuilds theRunViewModel, andrenderRunViewis the pure renderer. A new view (e.g. a combined transcript or a different column layout) adds a projection function over the same model, following the pattern ofrenderWatchViewandrenderFleetRunView. -
New fleet inspection scope:
fleetInspectionScope(imported fromsrc/cli/fleet-project-scope.ts) returns a scope object withseesRunandseesRoot. Widening or narrowing whatfleet viewandfleet statuscan see means changing that scope, which both the run resolver and the fleet-root resolver consume.
-
tests/extended-smoke/cli-core.test.tsspawns the built binary (dist/cli/index.js) against a mock OpenAI-compatible server. It asserts--versionprints the package version,--helproutes usage to stdout with exit 0, an unknown subcommand exits 2 with the error on stderr,doctor --fixcreates a fresh home,configure --runtime openai-compatwithout--idexits 2,configurewithqinput exits 130, and a full--json-events terminalrun seals a receipt withoutcome: "succeeded"andagentId: "main-agent". It also asserts thetargets userole flags update only their own role and name only that role, and that rejecting an unadvertised model is atomic (no partial write). -
tests/contracts/targets-use-unused-model.test.tscallsrunTargetsCommandanduseTargetInSettingsdirectly. It demonstrates that--modelcombined with--fleet-target/--fleet-modelis refused with a message naming the flag that would be unused and the remediation, and that the same call works once--modelis moved to--orchestrator-model. -
tests/contracts/settings-target-wizard.test.tsdrives the wizard through aScriptedWizardsubclass ofTargetWizardSurface. It demonstrates the save-once rule (cancel leaves settings unchanged), the late-probe-cancel guard (cancel during a probe cannot save), the text-prompt validation and back navigation, and that the settings overlay mounts Add and Edit as docked prompts. -
tests/boundaries/check-boundaries.tsis the import-boundary checker. Rule 6 specifically governs the CLI: a CLI file may reachsrc/interactive/**andsrc/engine/**only through a seam declared inSTAGE0_SEAMS. The declared seams for the CLI includesrc/engine/tui-primitives.ts(used bysrc/cli/fleet-view.ts),src/interactive/slash-commands.ts(used bysrc/cli/run.ts), andsrc/engine/oauth.ts(used byconfigureandauth).
-
The literal dynamic import is the code-splitting contract. Changing a
COMMAND_HANDLERSentry from a literalimport("./x.js")to a variable or computed path breaks tsup's chunk splitting and defeats the cold-start design. The comment at the map insrc/cli/index.tsstates this explicitly. -
runHeadlessMainAgentowns the receipt seal. The seal is idempotent:sealedis set to the promise on first call, and both the turn's completion path and the coordinated-shutdown drain hook race to it. Reordering the seal or adding a second seal path will lose usage on interrupted runs. The comment atsrc/cli/modes/print.ts:624explains that the drain hook is registered after the composition root's chat drain hook so the stats folded are final. -
The onboarding wizard's rail is a line-count contract.
railWritercounts lines that went through it and rewinds with a terminal erase sequence. A step that writes to the output stream without going through thewriter.streamproxy breaks the rewind math, and the next back-navigation will leave stale text on screen. -
fleet view's import boundary is load-bearing. The file importsProcessTerminalandTuiAltScreenfromsrc/engine/tui-primitives.ts, never from thesrc/engine/tui.tsbarrel. The comment at the top ofsrc/cli/fleet-view.tsstates that an edge to the barrel breaks the Stage 0 instant-shell chunk budget. Adding a TUI primitive import from the barrel will fail the boundary check. -
The headless stdout contract is asserted by tests.
tests/extended-smoke/cli-core.test.tschecks the exact event stream and receipt fields for a--json-events terminalrun. Changing the text-mode stdout behavior (e.g. printing the session id to stdout, or printing mid-workflow chatter) will break the test and the contract the comment atsrc/cli/modes/print.ts:624describes. -
targets usevalidation is atomic by design. The model validation loop inrunUse(src/cli/targets.ts) checks every selected role against the runtime's live inventory before callingupdateSettings. A failure in any role exits 1 and writes nothing. The focused testtests/contracts/targets-use-unused-model.test.tsasserts this atomicity withstrictEqual(readFileSync(path, "utf8"), before).
Source and generation metadata
title: "Command-line surfaces"
summary: "The argument-parsing and subcommand-dispatch entry point for every Clio Coder CLI surface, the configure wizard and target-selection flows, the headless main-agent runner, and the fleet authoring/execution and read-only run-viewer commands."
sources:
- "src/cli/index.ts"
- "src/cli/run.ts"
- "src/cli/modes/print.ts"
- "src/cli/configure.ts"
- "src/cli/configure-onboarding.ts"
- "src/cli/targets.ts"
- "src/cli/fleet.ts"
- "src/cli/fleet-view.ts"
- "src/cli/usage.ts"
tests:
- "tests/extended-smoke/cli-core.test.ts"
- "tests/contracts/targets-use-unused-model.test.ts"
- "tests/contracts/settings-target-wizard.test.ts"
- "tests/boundaries/check-boundaries.ts"
invariants:
- "Every subcommand is a literal dynamic import through the `COMMAND_HANDLERS` map in `src/cli/index.ts`, so a bare `clio-coder --version` pays for its own module graph and nothing else."
- "`--api-key`, `--no-context-files`, `--no-skills`, and `--skill` are honored only by the `run` and `acp` subcommands; `--with-panes`/`--no-panes` are honored by no subcommand at all."
- "The headless `clio-coder run` text mode keeps stdout the assistant's answer alone; the session id and any failure go to stderr."
- "The configure onboarding wizard persists nothing until the operator chooses Save at the review step; every earlier answer lives only in the in-memory `Answers` object."
validate:
- "node --import tsx tests/boundaries/check-boundaries.ts"
- "pnpm test:file tests/extended-smoke/cli-core.test.ts"Clio Coder · Repository · Website · Documentation
Wiki v0.1 · Developing implementation reference · Source snapshot: 657dce13d. Authored architecture documents define the product contracts.
- Clio Coder GUI Client
- apps / clio-coder-gui
- Apps clio coder gui server
- Apps clio coder gui tests
- apps
- Architecture
- Command-line surfaces
- Core
- Domains agents
- Config Domain
- Context Domain
- Dispatch domain
- Domains evidence
- Domains extensions
- Domains gateway
- domains
- Domains interop
- Domains lifecycle
- Domains memory
- Middleware Domain
- Domains mux
- Domains observability
- Domains plugins
- Prompt Compiler
- Domains providers
- Domains quota
- Domains resources
- Domains safety
- Domains scheduling
- Domains session
- Vendored Tool Registry and Resolution
- Engine
- Engine acp
- Engine apis
- engine
- Entry point
- Interactive
- interactive
- Interactive overlays
- Interactive renderers
- clio-coder wiki
- Scripts
- Contract tests
- Tests extended
- tests
- Tools
- Tools data
- tools
- Tools verify
- Worker runtime