Control the running app from a terminal
Agent or terminal // control.cli; every OS
├── One tree and protocol over the local API's POST /control on every OS;
│ OS can't do it -> unsupported at runtime, exit 1
├── hex status [--json] -> Listener, capture, meeting, model, features,
│ permissions, window, update, version, pid, memory
│ └── Hex not running -> exit 3, "Hex is not running; run `hex start`"
├── hex start -> Already running: nothing | else open Hex.app (Linux and
│ Windows: a detached `hex app --hidden`), wait ≤ 20 s
│ └── The caller's HEX_* variables -> open --env (values never logged)
├── hex quit | restart [--force]
│ ├── Meeting or dictation in progress, no --force -> refused, exit 1
│ ├── Listener didn't answer, no --force -> busy (it might be recording)
│ └── restart -> reopens with the running Hex's HEX_* variables (macOS);
│ Linux unit -> systemctl restart; else waits for the drain, then start
├── hex dictation start|stop|toggle|cancel
│ ├── Same shortcut state machine as the hotkey; start locks the capture
│ ├── start -> intentional at once: tone once the mic is open, recording
│ │ behavior, live stream
│ ├── stop -> done only when the clip went to transcription;
│ │ no audio yet or silent -> discarded, nothing transcribing
│ ├── cancel -> discards the recording in any mode, nothing pasted or saved;
│ │ jobs already transcribing finish; idle -> unchanged
│ ├── Repeat a command -> unchanged, exit 0
│ └── dictation stop --print -> stops at once, then waits ≤ 60 s and prints
│ the transcript (processed as a paste) instead of pasting it
│ ├── No speech -> no-speech, exit 4, prints nothing
│ ├── Not recording -> not-recording, exit 1
│ ├── Failure or timeout -> exit 1, prints nothing, never pastes
│ └── History keeps it as Printed; Paste Last skips it (also on Linux)
│ └── dictation start|toggle --raw -> the dictation it starts pastes the
│ transcript as heard (Dictate without processing); History keeps it as Raw;
│ --raw with stop -> usage error, exit 2
├── hex paste-last -> pastes the last dictation
│ ├── Setup gates the listener -> not-ready
│ └── Recording or shortcut gesture in progress -> busy (menu bar: nothing)
├── hex meetings record [--title] | stop | copy-last
│ ├── Meetings off -> feature-off; recording shows in the menu bar and HUD
│ │ exactly like a menu-bar recording
│ └── No meetings cancel: Hex cannot discard a recording
├── hex settings list | get <key|prefix> | set <key> <value>
│ ├── The window's save, then ReloadSettings -> live, no restart
│ ├── Unknown key, bad value, read-only key -> refused, nothing written
│ ├── Shortcut conflict -> refused, as the window's recorder would
│ └── apply <file|-> -> a JSON object of key: value, all or none
│ ├── Undeclared settings keep their values; dependent keys in any order
│ ├── transcription.model / .language -> selected, never downloaded
│ ├── Hex running -> SettingsApply: saved and adopted live, like set
│ └── Hex stopped -> written to settings.json under the service lock
│ (Linux); a Hex holding it is waited for (≤ 35 s) instead
├── hex model list | use <id> | install [id] | cancel
│ ├── use -> installed models only; never downloads or repairs; keeps the
│ │ model's remembered language and hints (--language overrides)
│ └── Linux/Windows: install with Hex stopped downloads here; status,
│ devices, check, transcribe run here (macOS: unsupported)
├── hex window show [pane] | hide
│ ├── settings -> the General pane (status window.pane stays "settings")
│ ├── meetings pane with Meetings off -> the pane's opt-in card
│ └── story <story> [--variant V] [--play] -> a playground story, as a
│ hex://story link opens (labs.md); playground off -> feature-off
├── hex history clear --yes -> without --yes refused before Hex is asked
├── hex check-for-updates -> The updater checks now
├── hex launch-at-login [on|off] -> SMAppService (changes need Hex.app);
│ Windows Run key; Linux XDG autostart entry
└── hex logs [--process] [-n N] [-f] -> local tail; works with Hex stopped
Every command except start, restart, logs, settings apply, and the
read commands talks only to the running Hex; none of them launch Hex. Every OS reaches it through
the local API's owner-only discovery file and the bearer-authenticated
POST /control route. Commands dispatch before logging and the instance lock on every OS,
so they never compete with the running app for its files. Opt-ins stay
explicit settings: no command turns Meetings on as a side effect.
On Linux and Windows control::host::answer carries every command out through
the OS's adapter (linux::app::LinuxControl, windows::rune::WindowsControl):
dictation and paste-last go to the listener's own queue (Windows: the same
hotkey loop, as a toggle press would), settings save and reload as the
window's do, and settings set refuses a key whose capability the OS lacks
(settings list marks it "not on this OS"). meetings record|stop|copy-last
go to the meeting recorder (meeting/runtime) as the tray's items do;
window story answers unsupported; Windows sends every annotate
command to the running Hex. hex history, hex meetings list|show|search, and
hex mcp read files on every OS.
Sources: control.rs (protocol, status, quit rules),
control/host.rs (the Linux and Windows host),
control/settings.rs (settings registry),
control/cli.rs and
control/cli/human.rs (clap, output, exit codes, start,
logs), macos_rune/control.rs (the macOS host),
the /control route in local_api.rs, and early
dispatch in main.rs.
Command and JSON contracts document flags, exit codes, and schema.
Checks: control::tests round-trip every command and reply (including
a_printing_stop_answers_through_its_reply_channel), refuse other
protocol versions, and cover the quit rules (an unanswered listener refuses)
and model_selection (remembered hints, wildcard-language validation); control::settings::tests include
every_persisted_setting_has_a_cli_key, value parsing, shortcut conflicts,
and zero volume; control::cli::tests pin
exit codes, output, and hex start's open
arguments; desktop::onboarding::tests::relaunch_forwards_only_the_hex_environment
pins restart's; macos_rune::control::tests check that model use prepares
its exact selection installed-only;
dictation::audio::tests::marked_captures_are_intentional_without_a_hold
covers the immediate stream and ReadyIntentional; recognition::tests
cover the stop replies and the paste-last refusal; control::tests::cancel_parses_and_takes_no_flags
covers cancel and its flags; local API's
control_requests_are_authenticated_versioned_and_typed covers authentication,
bounds, and error codes; tests::control_commands_parse_beside_the_read_commands
covers parsing next to history and meetings; control::host::tests cover
the quit refusal, printing stops, and runtime unsupported replies;
linux::tests parse the shared tree beside Linux's own commands;
rune_ui::platform::parity_tests fail when a capability differs across OSes
without a recorded reason; settings_name_real_capabilities_and_keys checks
the settings-to-capability table.
Observed (Oct 30, apollo, an isolated data root and hex service without a
display): status, settings get/set (and refusing show_dock_icon), vocabulary
add/list/remove, model list/use/status, history clear, window hide,
check-for-updates, commands check, quit, start, and restart all answered as
documented. Windows compiled with cargo xwin clippy (x64 only; the ARM64
cross-build fails in rquickjs-sys's build script, unrelated); it has not run
on a Windows PC.
Gaps: no command has run against a live app; the unit checks substitute the host. The control loop's start/stop wiring (tone timing, recording behavior) has no end-to-end test. A CLI volume change does not preview a tone as the window does. Onboarding permission prompts, calendar connection, and meeting deletion or renaming have no command. Linux and Windows dictation through the CLI has not run with a microphone, and meeting recording through the CLI has not run on Linux or Windows.