How the CLI decides
The CLI treats you as a human only when all of these hold:- The requested output format is
table(notjsonoryaml— if you passed--json, you’re treated as an agent even at a terminal). stdinis a TTY — an interactive terminal, not a pipe or a redirected file.stdoutis a TTY, the same way.- None of
CLI_AGENT,CLAUDECODE, orCIis set in the environment (checked with a truthy test: unset, empty,0andfalseall count as not set).
CLI_AGENT=1: it’s the one signal an agent can set for itself when its shell does look like a terminal.
What each audience gets
- Human
- Agent
- Tables instead of JSON, colored by the shared theme (see below).
- A picker when a required argument is missing —
edstem threadswith no unit prompts aselectpopulated from your own units, not a “missing required argument” error. threads sendprompts for--titleand opens$EDITORfor the body when neither--bodynor--body-fileis given.- A
y/Nconfirmation before any mutation, instead of requiring--yes. - Spinners for long-running steps, drawn on stderr so stdout stays clean even if you’re piping this particular run for some other reason.
- A guided first-run sign-in the first time a command needs a session and finds none — moodle shows a wordmark, explains what’s about to happen, and offers a menu of sign-in methods (your own browser, a CLI-controlled browser, or pasting a cookie) that comes back after a failed attempt instead of exiting.
The shared color roles
When color is on, every CLI paints four roles consistently, so the meaning transfers between tools:
Status words (
overdue, graded, not started, announcement, and similar) are colored by meaning rather than by column: overdue reads as danger (red), graded as success (green), not started as muted (dim), announcement as accent (magenta), regardless of which product or which column they appear in. A negated word like “not graded” is checked before the positive form it contains, so it doesn’t get misread as success.
None of this shows up in a pipe or with NO_COLOR set — the exact same text renders with no escape codes at all, so grepping a piped table works the same as parsing its JSON.
Escapes for a prompt that can’t run
Some interactive prompts have a non-interactive equivalent built in, for exactly the case where an agent needs the data a prompt would collect but can’t answer one:- A masked password prompt, used for a token, throws without a terminal; the calling command offers a
--token-stdinstyle flag instead —edstem auth login --token-stdinreads the token from the first line of stdin. - An editor prompt, used for a post body, throws without a terminal; the calling command offers a
--body-filestyle flag instead —edstem replies send ... --body-file -reads the body from stdin, or a path reads from that file.$VISUALor$EDITORis what opens when a person runs the same command interactively without either flag.
Ctrl+C
Cancelling an interactive prompt — Ctrl+C or Escape — is treated as the person’s decision, not a failure: it produces acancelled error and exit code 130, the same convention a shell uses for a process killed by a signal.
Errors and exit codes
The full exit-code table, including
130 for cancellation.