One binary. Run with no arguments it is the app — a menu bar item and the
session browser behind it; run with arguments it stays a CLI, so a single
signed executable serves both. The window is the daily loop now: show,
name, diarize and export all have a control in it, and this page stays a
complete reference because scripting and debugging still go through the verbs.
A verb it does not recognise is not an error: ambient wibble prints the usage banner on stdout
and exits 0, exactly as asking for help would, so a mistyped verb reads as
success to whatever is scripting it.
Launching the verbs that need system audio
tap and record create a Core Audio process tap, and a tap only returns audio
when the process was launched through LaunchServices. Run the bare binary from a
terminal and the tap is created, delivers buffers at the right rate, and every
sample is zero — TCC attributes the request to the terminal, which has no audio
permission and does not prompt.
./setup-signing.sh # once — stable identity
./make-app.sh # bundle + sign
open -a "$PWD/build/Ambient.app" --args tap /tmp/out.wav 14Why this is the only launch that works, and why a stale signing identity fails differently from a shell launch, is in when it does not work.
doctor
ambient doctor [--json]Runs ten checks, always in the same order: the four model files under the
models root, the config file, and four checks against the sessions
directory (writable, sessions awaiting transcription, a live session, a
stale lock). A sessions directory that does not exist yet passes the
writable check when its nearest existing parent is writable, since the
first recording creates it. Prints ok or FAIL with a detail for each, exits non-zero
when any check failed so a script can branch on it, and --json prints the
same ten checks as one array of {name, ok, detail} objects.
probe
ambient probeChecks the machine is viable: enumerates audio processes through the Core Audio
bindings, then reports the ONNX Runtime execution-provider state. Needs no
permission — enumeration is not capture — so a clean probe says nothing about
whether a tap will return audio.
tap
ambient tap <out.wav> <secs> [bundle-id...] [--no-mic] [--via-child]Records both tracks for <secs> seconds and writes them as two files,
<out>.room.wav and <out>.call.wav, at their own device rates — the .wav
suffix on the argument is stripped to form the base name. With no bundle IDs it
falls back to the apps setting, and if that is empty taps all system audio;
an argument beats the setting, and the settings case is marked (from settings)
in the log. <secs> defaults to 10 if the argument is missing or unparsable.
Each track is reported with its peak, its total length and its real device
seconds, so a stalled tap prints 20.1s, 17.4s real, 2.7s padded rather than
passing for a healthy one — see two tracks, two
clocks. If the call track is flat while
something was demonstrably rendering output, it names the likely cause instead
of exiting quietly.
--no-mic records the call track only. --via-child is a diagnostic: it
re-execs this binary as a child process running tap <out> <secs> — dropping
any bundle IDs and --no-mic — to answer whether a spawned process inherits the
bundle’s audio-capture grant.
record
ambient record [--name <s>] [--app <bundle-id>]... [--model <dir>] [--seconds <n>]Captures until stopped and publishes completed speech turns during recording.
After Stop, it finishes the remaining text and speaker labels, then prints the
session directory on stdout. --app may be repeated; with no --app
the apps setting decides. --model overrides the ASR directory, which
otherwise defaults to sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8 under the
models root; a missing ASR or VAD model is a hard error before any audio is
captured. --seconds bounds an unattended run.
Turn segmentation here is Vad::turns, not the chunking transcribe uses —
one record per turn, because a record spanning two people cannot carry a
speaker. See sessions and voice activity
detection.
stop
ambient stop [<session-dir>]With no argument it finds the recording in flight under the sessions folder.
It writes a STOP file that the capture loop notices on its next 200 ms tick,
echoes the session’s status line to stderr, and prints the directory. A file
rather than a signal because the launch that gets system audio has no terminal
to Ctrl-C and no pid a user can see; Ctrl-C still works when there is a
terminal. With two recordings in flight at once, a bare stop writes STOP
into whichever live session directory sorts last by name — the newest, when the
two started in different minutes — and leaves the other running; name the
directory to reach a specific one. When it does not
work has the cases where sort order and start order come
apart. See capture.
sessions
ambient sessions [--json]Lists every session in the sessions folder, newest first, one line each: id,
start time, duration, name and state. The state is live while a capture is in
flight, transcribing while a transcriber holds the lock, awaiting transcript for audio no transcript has been written for yet, done once
transcript.md exists, and broken when session.json cannot be read.
It lists what is on disk rather than what is finished. A session still being
captured has no session.json yet — it is written once the scratch audio
closes — and one interrupted mid-write has a session.json that will not
parse; both appear, the first with empty metadata columns and the second as
broken, because a session that has gone wrong is the one worth seeing.
--json prints the same rows as a JSON array, each with id, dir, name,
started_at, duration_s, transcribed, live, transcribing, error,
tags and pinned. See sessions.
search
ambient search <query> [--json]Case-insensitive substring search for query over every session’s tidied
transcript, newest session first. Prints a header row followed by one row per
hit — session id, mm:ss, speaker and the matching line — and exits 0 with
just the header when nothing on disk matches, or the sessions folder is
empty. --json prints the same hits as a JSON array instead, each with
session, index, track, start_ms, speaker and text; index is the
line’s position in that session’s appended order, the same one api::search
reports. See the session API.
show
ambient show <session-dir> [--verbatim] [--json]Prints the session with edits.jsonl folded over raw.jsonl. --verbatim
skips the fold and shows what the recogniser actually produced. Nothing is
mutated. The window’s Tidied/Verbatim toggle is the same pair of views. See
sessions.
--json prints the same lines as a JSON array instead of the aligned text, for
piping into jq or a script. Each element carries track ("room" or
"call"), start_ms, end_ms, speaker (null until something has named
it) and text. It composes with --verbatim, in either order, and a session
with no lines in it prints [] rather than a message on stderr.
name
ambient name <session-dir> <label> <name>Renames every line currently carrying <label>, e.g. call-1 Priya, reports
how many lines moved, then prints the session. It appends a speaker edit like
anything else, so it is undoable, and a later diarize leaves a human-assigned
name alone. The window’s naming strip appends the identical edit, on whichever
session is selected.
Asking for a label nothing carries is how you find out which labels exist. It
refuses and lists them: ambient name <dir> call-2 Priya on a session holding
two speakers answers no lines labelled "call-2" (available: call-1, room-1),
and on a session that has not been through diarize the list reads none — run `ambient diarize` first. What it lists is the labels as they stand now, so
once call-1 has been named Priya it is Priya that comes back, not the label
she replaced. See diarization.
undo
ambient undo <session-dir> [--seq <n>]Takes back the newest naming and reports reverted <n> edits. name appends
one speaker edit per line, so undo reverts that whole batch — every edit
sharing the newest timestamp among the ones you typed — and whatever speaker
edit is still live for those lines surfaces again. Undoing appends:
edits.jsonl only ever grows, and raw.jsonl is untouched, so a second undo
takes back the naming before it rather than redoing anything.
What surfaces is usually diarization’s label, but not after a retune: the
diarize below reverts its own previous labels and writes no replacement for a
line somebody has named, so on diarize → name → diarize --threshold there
is nothing left underneath and undoing the name leaves the line with no speaker
at all — which also drops it out of the naming strip and out of name’s list
of labels. Running diarize once more labels it again.
Only names a person typed are candidates; taking back diarization’s own labels
is what re-running diarize does. A session where nothing has been named
answers nothing to undo — naming the directory it looked in. A mistyped path
fails earlier, at the lock every undo now claims around its call, saying it
could not write there.
--seq <n> reverts one edit instead, addressed by its zero-based line in
edits.jsonl — the way to reach a repair, or one line of a naming. Asking for
a line that does not exist answers no edit <n>, a line already reverted
answers already reverted, and a line that is itself a revert is refused:
undo the edit it names.
meta
ambient meta <session-dir> name|notes|pinned|tag|untag <value>Sets the session’s name or notes, pins or unpins it, or adds/removes one tag
— whichever field you name — and prints the resulting name and tag list.
pinned takes true/false (also yes/no, on/off, 1/0); tag and
untag add or remove one tag at a time, so adding a tag that is already there
or removing one that is not is a no-op rather than an error. Rewrites
session.json only. A session still being captured has no session.json yet
and refuses, saying so. See the session API.
delete
ambient delete <session-dir> --yesRemoves the session directory and its audio for good. Without --yes it
refuses and tells you to add it. Refuses a session that is still recording,
and one a transcriber currently holds the lock on. A directory with no
session.json and no fresh audio — a failed capture — deletes like any other.
See the session API.
diarize
ambient diarize <session-dir> [--threshold <f>]Assigns speakers to a session that already has a transcript, appends the edits
and prints the result. --threshold defaults to 0.5 — this path never reads
the config file, so the threshold setting does not apply here; it governs only
the automatic diarization at the end of record. Re-running appends
revert records for the previous run’s labels first, so a bad threshold costs a
revert rather than a recording. It refuses on a session whose audio has been
swept by retention, and on one with no records in raw.jsonl — the window’s
Separate voices button disables itself and says why in both cases rather than
letting you ask. See
diarization.
export
ambient export <session-dir> [--format <f>] [--out <path>]Writes a transcript — into the session directory unless --out says
otherwise — and prints the path. --format is one of markdown, text,
json, srt, vtt or assistant and defaults to markdown; the default
filename follows the format (transcript.md, .txt, .json, .srt, .vtt,
or .assistant.md). The file is derived; raw.jsonl plus edits.jsonl
remain the only source of truth, and record, diarize and name each
regenerate the markdown one. See sessions.
The window’s session header has an Export menu with the same formats plus “Copy for an assistant”, which copies straight to the clipboard instead of writing a file.
transcribe
ambient transcribe <model-dir> <a.wav>Transcribes one file to stdout, with timing on stderr. It accepts any sample
rate and resamples to 16 kHz rather than insisting on 16 kHz input. If
models/silero_vad.onnx loads it chunks on VAD boundaries in 30 s windows and
reports the speech-to-total split; if that model is missing it silently falls
back to unchunked long-form decoding, which costs memory linear in length. See
speech recognition.
vad
ambient vad <a.wav>Prints each detected speech segment with its start, end and duration, then the
segment count, total speech and the percentage skipped. Unlike transcribe it
requires a 16 kHz wav — it does not resample — and it loads the VAD model from
the relative path models/silero_vad.onnx. See voice activity
detection.
config
ambient config [<key> <value>]With no arguments it prints the resolved settings, where sessions will actually
go, the config file path, and the input devices it can currently see — and says
so explicitly when AMBIENT_HOME is set and overriding sessions_dir. With a
key and a value it sets one setting and writes the file. A key with no value is
an error rather than a read. Anything after the value is dropped without
complaint — ambient config threshold 0.4 wibble sets the threshold and says
nothing about wibble — where record, export and diarize bail on an
argument they do not recognise. Keys and defaults are in
settings.
roster
ambient roster [add <name> | rm <name>]With no arguments it lists the people you record with, one per line. add is a
no-op on a name already present (case-insensitively) and keeps the list sorted;
rm errors if the name is not there. As with config, anything past the name
is dropped in silence: ambient roster add Priya wibble adds Priya and never
mentions the rest. Removing someone does not unname them in
past recordings — those names live in each session’s edits.jsonl. The roster
holds names and nothing else; see what is kept.
mcp
ambient mcp [--no-play] [--save-audio <dir>]Serves the Model Context Protocol on stdin and stdout so another program can
read sessions as data: three read-only tools, sessions, transcript and
status, and newline-delimited JSON-RPC 2.0 with one reply per request. It
also serves the live assistant’s tools and its watch prompt.
--no-play runs the assistant’s voice without playing it, and
--save-audio <dir> writes each utterance to <dir>/reply-<n>.wav. It is
not meant to be typed at — a client starts it, and it exits when that client
closes its stdin — but a piped request answers on stdout, which is how you
check a registration:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"ping"}' | ambient mcpNothing but protocol goes to stdout; diagnostics go to stderr. The session
tools never write: there is no tool here to record, name or delete. The
assistant’s tools write only its heartbeat and the session’s assistant.jsonl. Registration, the tool
arguments and the cursor for following a session as it records are in
reading sessions from an assistant, and the reasoning is
ADR 0016.
talk
ambient talk --wav <a.wav> [--dry-run] [--no-play] [--home <dir>] [--timeout <s>]
ambient talk --pcm [--rate <hz>] [...] audio on stdin
ambient talk --stopOne spoken turn with firstmate. It trims the audio with the VAD and
transcribes it with Parakeet on this Mac; less than 0.4 s of speech is “didn’t
catch that” and nothing is sent. The text goes to firstmate as a note through
fm-inbox.sh note --request-id ambient-<id>, in the home named by
talk.firstmate_home or --home, and the turn polls fm-inbox.sh receipts
about once a second for the reply to that note’s id, for up to --timeout
seconds (300 by default). The reply is read aloud through the assistant’s
voice helper with its markdown stripped and cut to 600 characters, while
holding speaking.lock beside the config file so the live assistant never
talks over it. A voice that fails, a lock still held after ten seconds, or a
recording that started meanwhile leaves the reply as text.
The audio is never written to disk or sent anywhere: only the transcript
leaves the process. Talking is refused while a recording is in progress,
because the microphone is shared. --dry-run sends the note and waits for
the reply but prints what it would say instead of starting the voice, so the
whole loop runs headless against a stand-in fm-inbox.sh. --pcm reads raw
32-bit float little-endian mono until end of file, which is how the app hands
over a push-to-talk hold. --stop, or SIGTERM to the turn, silences a reply
mid-word.
Stdout is one JSON event per line for the program that started it (heard,
nothing, queued, sent, reply, spoken, text_only, dry_run,
no_reply); src/talk.rs documents each.
Environment
| Variable | Read by | Effect |
|---|---|---|
AMBIENT_HOME |
session::home |
Sessions directory. Wins over the sessions_dir setting. |
AMBIENT_CONFIG |
config::path |
Path to the config file, so a test can point somewhere harmless. |
AMBIENT_ROSTER |
roster::path |
Path to roster.json, which otherwise sits beside the config. |
AMBIENT_MODELS |
session::models_root |
Models directory. Otherwise models/ relative to the working directory, then searched upward from the executable. |
AMBIENT_VOICE_CACHE |
voice helper | Where the helper downloads Kokoro and caches designed voices. Default ~/Library/Caches/Ambient/voice. |
AMBIENT_DEBUG_DIAR |
diarize |
Set to anything: adds the per-window powerset class histogram. |
AMBIENT_DEBUG_TAP |
capture |
Set to anything: extra tap-level diagnostics during capture. |
HOME |
config::path, session::home, settings |
Falls back to . if unset, so both paths become relative. |