Skip to content

Commands

Updated View as Markdown

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 14

Why 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 probe

Checks 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.

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> --yes

Removes 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 mcp

Nothing 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 --stop

One 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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close