A manual for the command line, and two rules it caught us breaking

docs/cli.md documents every command and option, what each one prints, and
what it returns -- with the output conventions and the exit-status table
up front, since those are the two things a script is written against.
Every command, option and flag in it was checked against the running app;
the sample output is captured from the simulator rather than typed out.

Writing it turned up two places the CLI still did the thing the last
commit was about. A destructive command refused for want of --yes printed
its refusal in red on *stdout* and exited 1: a colour and a sentence that
is not the command's answer, in whatever was reading the command's answer.
A missing confirmation is a bad invocation, so it now goes through the
parser like any other -- nothing on stdout, the reason on stderr, exit 2.

The README keeps its cookbook and its summary of the conventions, and
points at the manual for the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-09-08 01:17:26 -04:00
1 parent da05dd402f
commit 4a91845651
7 files changed
+868 -16

No files matched your search

+806
View File
@@ -0,0 +1,806 @@
# The MeshTerm command line
MeshTerm has two front ends over one codebase. The **menu** — what you get by running
`meshterm` with no arguments — is a full-screen session meant to be read by a person. The
**CLI** is meant to be read by `awk`.
This is the manual for the second one: what it prints, what it returns, and every command
it answers to.
- [Running it](#running-it)
- [Output conventions](#output-conventions)
- [Exit status](#exit-status)
- [Command reference](#command-reference)
- [Finding a device](#finding-a-device)
- [This node](#this-node)
- [The mesh around you](#the-mesh-around-you)
- [Messaging](#messaging)
- [Tracing](#tracing)
- [Other nodes](#other-nodes)
- [MeshTerm itself](#meshterm-itself)
- [Recipes](#recipes)
- [What has no command](#what-has-no-command)
---
## Running it
```
meshterm [GLOBAL OPTIONS] COMMAND [ARGS]
```
With no command, MeshTerm launches the interactive menu instead. Global options go
**before** the command:
| Option | What it does |
| --- | --- |
| `-p`, `--profile NAME` | Use a named device profile from `config.toml`. |
| `--port PORT` | Serial port to connect to (`COM5`, `/dev/ttyACM0`), overriding the profile. |
| `--ble ADDRESS` | Bluetooth address of a companion; selects the BLE transport. |
| `--ble-pin PIN` | Pairing PIN, if the Bluetooth companion asks for one. |
| `--tcp HOST[:PORT]` | Network address of a TCP companion; selects the TCP transport. Default port 5000. |
| `--mock` | Use the built-in simulator instead of real hardware. Nothing transmits. |
| `--db PATH` | Use this SQLite database instead of `~/.meshterm/meshterm.db`. |
| `--json` | Machine-readable output. Only `devices` and `tx-optimize` honour it; everything else is already parseable as plain text. |
| `-q`, `--quiet` | Suppress console logging entirely (the log file still records). |
| `--platform NAME` | Force the UI flavour (`regular`\|`picocalc`) instead of detecting it. |
Commands that need a radio open one, do their work, and close it. Commands that only read
stored history (`records`) or MeshTerm's own state (`preferences`, `about`) need no device
at all and work with nothing attached.
> **One transmission per invocation.** A trace transmits exactly once — repeaters
> penalise, and can blacklist, nodes that burst traffic. To sample more, run the command
> again with your own pacing between runs.
---
## Output conventions
The CLI prints like a standard Unix utility, and every rule below exists so its output can
be piped somewhere without being cleaned up first.
**No colour.** Not a hue, not a bold, not a dim — stdout carries no escape sequence at all,
whether or not it is a terminal. A colour that survives into a file is noise in it.
**No wrapping.** A record is a line. Wrapping would put half a record's fields under the
wrong headings, so nothing folds: a long line runs off the right and your terminal (or
`less -S`, or your pipe) decides what to do about it.
**No frames.** No borders, no boxes, no rules, no titles, no legends. The command you typed
is the title.
**Listings** are an uppercase header line followed by space-aligned records, the shape `ps`
and `df` print. Numeric columns right-align.
```console
$ meshterm contacts
NAME TYPE HEARD PKTS KEY
"Alice" node 2026-09-07T19:58:54-04:00 12 d4e5f6a700000000000000000000000000000000000000000000000000000000
"Yagi-Repeater" repeater 2026-09-07T19:58:53-04:00 97 a1b2c3d400000000000000000000000000000000000000000000000000000000
```
**Node names are quoted.** A name can hold a space, a comma, even a quote, so a bare name
is not one field — it is however many fields the name happens to split into. Quotes and
backslashes inside a name are escaped the ordinary way (`"He said \"hi\""`), so a quoted
value reads back unambiguously.
**Key/value blocks** are a key, the gutter, and the rest of the line as its value — the
shape `sysctl -a` prints. There is no header, and the value is *not* quoted: a line has
exactly two fields, so the value is whatever follows the key.
```console
$ meshterm info
name MockCompanion
public_key 0000000000000000000000000000000000000000000000000000000000000000
role companion
battery_v 4.10
uptime_s 93784
noise_floor_dbm -110
```
A `get` prints the bare value alone, ready for `$(...)`:
```console
$ meshterm config get tx_power
20
```
**Paths** are quoted node names, each with its hash in parentheses *outside* the quotes,
joined by a bare comma — the same comma `--path` takes:
```
"MockCompanion" (00),"Yagi-Repeater" (a1),"Alice" (d4),"Yagi-Repeater" (a1),"MockCompanion" (00)
```
Your own node is a hop like any other, named and hashed — never a `★`. The hash is there
because the CLI has no colour: on screen a route's hops are told apart by their key-derived
hues, and in plain text the hash is what carries that identity. It is also what joins a
route line to the per-hop table underneath it.
**Times are absolute** local ISO-8601 to the second — `2026-09-07T18:22:41-04:00` — never
`3h ago`. A relative age is for someone watching a screen; a script wants something it can
sort and subtract.
**`-` is the one token for nothing** — absent, unknown, or not applicable. You learn it
once; there is no `—`, no `n/a`, no `never`, no empty cell.
**stdout is the answer; everything else is stderr.** Errors print as
`meshterm: what went wrong`. Progress bars draw on stderr too, and draw nothing at all when
stderr is not a terminal. So `meshterm contacts > contacts.txt` puts contacts in the file
and any complaint on your screen.
---
## Exit status
The return value is half the report.
| Code | Meaning |
| --- | --- |
| `0` | Success. |
| `1` | Failure with no more specific code — a bad value, an unreadable file, an unhandled fault. |
| `2` | Usage error: an unknown flag, a missing argument, a value the parser rejected. |
| `3` | No companion device could be selected: none attached, none matching the connection flags, or the choice was ambiguous. **Nothing was transmitted.** |
| `4` | A device was reached but the operation failed — a command error, a timeout, a link lost mid-run. Retrying is reasonable. |
| `5` | The command completed and there was **nothing to report**: an empty list, a target that never answered, a conversation with no messages. stdout is empty. This is not an error. |
The table is printed under `meshterm --help` as well.
`5` is the one worth building on. `grep` conflates "found nothing" with "worked", and a
caller that has to count output lines to tell an empty mesh from a full one is parsing when
it could be branching:
```bash
if meshterm contacts > contacts.txt; then
echo "$(($(wc -l < contacts.txt) - 1)) contacts" # minus the header
elif [ $? -eq 5 ]; then
echo "no contacts yet"
else
echo "could not reach the radio" >&2
fi
```
`3` and `4` split the failures a retry might fix from the ones it never will:
```bash
meshterm trace --target Alice
case $? in
0) echo "reachable" ;;
5) echo "no reply — the walk ran, nothing came home" ;;
3) echo "no radio attached" >&2; exit 1 ;;
4) echo "radio trouble — worth retrying" >&2; exit 1 ;;
*) echo "failed" >&2; exit 1 ;;
esac
```
---
## Command reference
### Finding a device
#### `meshterm devices`
List every attached serial device and in-range Bluetooth companion. Opens no radio — it
only enumerates what is attached or advertising.
| Option | What it does |
| --- | --- |
| `--ble` / `--no-ble` | Include a Bluetooth LE scan. On by default; skipping it saves a few seconds. |
```console
$ meshterm devices --no-ble
TARGET TRANSPORT NAME HARDWARE MESHCORE SERIAL ACTIVE
COM12 serial "USB Serial Device (COM12)" "Adafruit" maybe D030D2CCD00F08EB no
COM16 serial "USB Serial Device (COM16)" "Adafruit" maybe D8D7D4BB5E4546E0 no
COM1 serial "Communications Port (COM1)" "(Standard port types)" no - no
```
`TARGET` is what `--port` and `--ble` take, verbatim. `MESHCORE` is three-valued and stays
that way: `yes` only once a connection has proved the device speaks the protocol, `maybe`
for a USB vendor ID that suggests a LoRa board or a bridge chip, `no` for anything else — a
vendor ID is a hint, and the column would be lying if it rounded one up.
Returns `5` when nothing is found.
---
### This node
#### `meshterm info`
The connected companion's live status: which radio this is, and how it is doing.
```console
$ meshterm info
name MockCompanion
public_key 0000000000000000000000000000000000000000000000000000000000000000
role companion
model MeshCore Simulator
firmware mock mock
battery_v 4.10
storage_used_kb 128
storage_total_kb 1024
clock 2026-09-08T01:05:09-04:00
clock_drift_s -125
uptime_s 93784
noise_floor_dbm -110
last_rssi_dbm -62
last_snr_db +9.5
tx_air_s 42
rx_air_s 360
packets_sent 210
packets_received 1234
receive_errors 3
```
The unit is in the key, so nothing has to be pulled back out of prose. A reading the
firmware does not answer for is **absent** rather than `-`: a missing key says "this device
does not report it", where a dash would claim it reported nothing.
What the radio is *set to* is `config show`'s answer, not this one.
#### `meshterm config`
View and change every device setting. With no subcommand it runs `show`.
| Subcommand | What it does |
| --- | --- |
| `show` | Print every setting as `key value`. |
| `get KEY` | Print one setting's value, bare. |
| `set KEY VALUE` | Change one setting. |
| `backup PATH` | Write every setting to a TOML file. |
| `restore PATH [--dry-run]` | Apply settings from a TOML backup. `--dry-run` prints the plan and changes nothing. |
| `custom KEY VALUE` | Set an experimental custom variable. |
| `channel INDEX NAME [--secret HEX]` | Configure a channel slot (see also the `channels` group). |
| `advert [--flood]` | Broadcast an advertisement. Zero-hop unless `--flood`. |
| `advert-cadence HOURS [--flood]` | How often to auto-advertise in the background. `0` disables. |
| `share` | Print this node's contact card as a `meshcore://` URI. |
| `sync-clock` | Set the device clock from this computer. |
| `export-key [--out PATH]` | Export the private key. **Sensitive.** |
| `import-key KEY_HEX --yes` | Import a private key, overwriting this node's identity. |
| `reboot --yes` | Reboot the device. |
| `factory-reset --yes` | Erase all data and reset to defaults. |
```console
$ meshterm config show
name MockCompanion
adv_lat 0.0
adv_lon 0.0
device_pin ••••••
radio_freq 869.618
radio_bw 62.5
radio_sf 8
radio_cr 5
tx_power 20
manual_add_contacts false
flood_scope ""
adv_loc_policy 0
```
`show` names each setting by the key `get` and `set` take, and prints a value they will
take back — an enum's **number**, not the reader's label; `""` for an empty string; `-` for
something the firmware never reported. So a line read out of `show` can be typed straight
back in:
```console
$ meshterm config set radio_sf 9
$ meshterm config get radio_sf
9
```
The pairing PIN stays masked here, because this is the whole-device dump — the thing that
gets redirected into a file and pasted into a bug report. Name it deliberately with
`meshterm config get device_pin`.
Custom variables are namespaced `custom.*` in the output, since they have no spec and
`config set` will not take them (use `config custom`).
Destructive operations are gated behind `--yes` rather than a prompt, so nothing surprising
happens in a script. Without it the command is refused as a **usage error** — nothing on
stdout, the reason on stderr, exit `2`.
---
### The mesh around you
#### `meshterm contacts`
The contacts your device knows.
| Option | What it does |
| --- | --- |
| `-s`, `--sort ORDER` | `heard` (default), `name`, or `packets`. |
```console
$ meshterm contacts --sort name
NAME TYPE HEARD PKTS KEY
"Alice" node 2026-09-07T19:58:54-04:00 12 d4e5f6a700000000000000000000000000000000000000000000000000000000
"Local-Repeater" repeater 2026-09-07T19:58:53-04:00 48 b2c3d4e500000000000000000000000000000000000000000000000000000000
```
`TYPE` is the node's advertised role in words — `node`, `repeater`, `room`, `sensor`, or
`unknown`. `PKTS` is how many packets passive monitoring has overheard from it. `KEY` is
the full public key, never elided: a truncated key is not something you can hand back to
`--to` or `--path`.
This lists *contacts only*. Your own node is not a contact — `meshterm info` reports it, in
far more detail than a row could hold.
Returns `5` when the device knows no contacts yet.
#### `meshterm monitor`
Capture overheard packets in the foreground for a bounded window, then summarise it.
Transmits nothing; it only listens. Recording to history is always on anyway — this adds
the live view.
| Option | What it does |
| --- | --- |
| `-s`, `--seconds N` | How long to capture. `0` (the default) runs until Ctrl-C. |
```console
$ meshterm monitor --seconds 3
TIME NODE NAME SNR_DB RSSI_DBM LAT LON
2026-09-08T01:07:25-04:00 a1b2c3d4 "Yagi-Repeater" +7.0 -101 45.50190 -73.56740
2026-09-08T01:07:25-04:00 c3d4e5f6 "Observer-Bot" +4.9 -93 - -
NODE NAME PKTS MEDIAN_SNR_DB BEST_SNR_DB RSSI_DBM LAST_HEARD
c3d4e5f6 "Observer-Bot" 54 +5.2 +12.4 -108 2026-09-08T01:07:33-04:00
a1b2c3d4 "Yagi-Repeater" 48 +5.1 +12.8 -100 2026-09-08T01:07:33-04:00
```
Two blocks, separated by a blank line. The **stream** is a header and then a record per
packet as it arrives; it cannot be column-aligned, because the widths are not known until
it ends, so its fields are gutter-separated and the name carries its quotes. The
**summary** is the aggregate the stream cannot give — a count, a median, a best per node —
most recently heard first.
Returns `5` when the window heard nothing.
#### `meshterm records`
The record-setting walks every trace has been scored against. Reads the database; needs no
device and never transmits.
| Option | What it does |
| --- | --- |
| `-c`, `--category ID` | One discipline: `long_haul`, `far_point`, `long_leg`, `grand_tour`, `clean_trail`, `thin_thread`, `big_loop`. |
| `-w`, `--width N` | Only records set at this per-hop hash width (`1`, `2`, or `4`). |
```console
$ meshterm records --category long_haul --width 2
CATEGORY WIDTH SCORE UNIT RECORDED VERSION ROUTE
long_haul 2 273.0 km 2026-08-17T18:14:28-04:00 0.2.8 "Yagi-Repeater" (a1b2),"Alice" (d4e5),"Yagi-Repeater" (a1b2)
long_haul 2 112.0 km 2026-09-03T17:46:10-04:00 0.2.8 "Local-Repeater" (b2c3),"Alice" (d4e5),"Local-Repeater" (b2c3)
```
`SCORE` is a bare number and `UNIT` is its own column, so the scores in a column are
comparable. A Longest-distance score prefixed `>=` is a **lower bound**: some hop on that
walk has no known position, so the distance is a floor rather than a measurement.
Records are kept per hash width, because the width bounds both a walk's maximum length and
its collision odds — boards at different widths are measuring different games.
Returns `5` when no records match.
---
### Messaging
#### `meshterm chat`
| Subcommand | What it does |
| --- | --- |
| `send TEXT --to NAME` | Send a direct message. |
| `send TEXT --channel N` | Broadcast on a channel slot. |
| `history [--to NAME \| --channel N] [--limit N]` | Print a conversation's stored transcript. Default limit 50. |
| `list` | Every channel and contact with its unread count and last message. |
| `listen [-s SECONDS] [--debug]` | Tail inbound messages live. |
Exactly one of `--to` and `--channel` is required on `send` and `history`.
```console
$ meshterm chat send "on my way" --to Alice
acked yes
```
A direct message prints its `acked` state, which is the one thing the send does not already
tell you — the radio accepted it either way, and whether the peer answered is a separate
fact. A **channel** broadcast prints nothing: there is no acknowledgement on a channel, so
the exit status is the whole answer.
```console
$ meshterm chat history --to Alice
TIME DIRECTION PEER SNR_DB TEXT
2026-09-08T01:07:41-04:00 out "Alice" - on my way
2026-09-08T01:09:02-04:00 in "Alice" +6.4 see you there
```
`DIRECTION` is `in` or `out`; `PEER` is always the *other* party. `TEXT` is last and
unquoted — it is the rest of the line, and a message body is the one field that can hold
absolutely anything.
```console
$ meshterm chat list
CONVERSATION KIND UNREAD LAST_TIME LAST_TEXT
"Public" channel 0 - -
"Alice" direct 2 2026-09-07T19:58:54-04:00 see you there
```
`listen` prints a header and then a record per message as it arrives, the same shape
`monitor`'s stream takes. `--debug` routes the message-pull logging to stderr, for working
out whether messages are being pulled from the companion at all.
`history`, `list` and `listen` return `5` when there is nothing to report.
#### `meshterm channels`
| Subcommand | What it does |
| --- | --- |
| `list` | The configured channel slots. |
| `add INDEX NAME [--secret HEX]` | Add a channel. A leading `#` makes it public (the firmware derives the key from the name); otherwise it is private, with a random key unless you give one. |
| `join INDEX NAME SECRET` | Join a channel from its name and 32-hex-character key. |
| `import INDEX URL` | Import a `meshcore://channel/add` link. |
| `share INDEX` | Print a slot's share link. |
| `clear INDEX --yes` | Clear a slot, removing the channel from the device. |
```console
$ meshterm channels list
SLOT NAME TYPE HASH
0 "Public" public 8f
1 "brigade" private 2c
```
`share` prints the `meshcore://` URI alone. (The menu draws a QR code beside it; redirected
into a file that is a block of block characters wrapped around the one thing that is
actually the answer.)
`clear` is gated behind `--yes` because a private channel's key is lost with the slot unless
it is saved elsewhere.
`list` returns `5` when no slots are configured.
#### `meshterm courier`
A store-and-forward outbox: queue a message for a contact that is not reachable right now,
and it goes out the moment the contact is next heard — or at a time you name.
| Subcommand | What it does |
| --- | --- |
| `queue CONTACT TEXT… [--at HH:MM]` | Queue a message. `--at` holds it until the next occurrence of that local time. |
| `list` | The outbox: waiting entries then finished ones. |
| `send ID` | Force one delivery attempt now, skipping the "wait until heard" check. |
| `cancel ID` | Remove a waiting entry. |
| `clear` | Drop every finished (delivered / given-up) entry. |
```console
$ meshterm courier queue Alice "ping me when you are back" --at 18:30
id 10
$ meshterm courier list
ID STATE NODE SCHEDULED FINISHED TEXT
10 waiting "Alice" 2026-09-08T18:30:00-04:00 - ping me when you are back
8 delivered "Bob" - 2026-08-11T18:10:17-04:00 Courier test!
```
`queue` prints the entry's **id**, which is the one fact you have to keep: it is what `send`
and `cancel` take. An entry with no `SCHEDULED` time goes as soon as the contact is heard.
```console
$ meshterm courier send 10
outcome delivered
```
`outcome` is one of `delivered`, `no ack`, `gave up`, `unknown contact`, `busy`, `gone`.
The *sending* is done by a running interactive session's courier; a queued message sits in
the outbox until one runs.
`list` returns `5` on an empty outbox; `cancel` and `clear` return `5` when there was
nothing to act on.
---
### Tracing
#### `meshterm trace`
Walk the path to a target once and report what came back.
| Option | What it does |
| --- | --- |
| `-t`, `--target NAME` | **Required.** Target node name or key prefix. |
| `-p`, `--path SPEC` | Force a route: comma-separated contact names and/or hex key prefixes, mixed freely (`3d,f2,3d`). Omit it and the device routes. |
```console
$ meshterm trace --target Alice
target Alice
path auto
success yes
hops 3
min_snr_db +2.6
rtt_ms 459
route "MockCompanion" (00),"Yagi-Repeater" (a1),"Alice" (d4),"MockCompanion" (00)
HOP FROM TO SNR_DB
0 00 a1 +6.0
1 a1 d4 +2.6
2 d4 00 +5.1
```
Two blocks. The first is what the walk did, `route` among the facts. The second is a record
per hop, naming its two ends by the **hash** rather than repeating the names: the route line
above is where the names are, and the hash is what joins the two blocks. `path` reads `auto`
when the device routed it — every real value there is comma-separated hex.
A trace that never came home is **not** a failure of the command: the radio transmitted and
the walk ran. It reports `success no` and returns `5`.
#### `meshterm trace-path`
The other half of tracing: no target, just a route you compose by hand — out and back
whichever way you choose. It only has to end within earshot of this node.
| Option | What it does |
| --- | --- |
| `-p`, `--path SPEC` | **Required.** The whole walk, comma-separated. |
```console
$ meshterm trace-path --path "a1,d4,a1"
target -
path a1,d4,a1
success yes
hops 4
min_snr_db +2.0
rtt_ms 458
route "MockCompanion" (00),"Yagi-Repeater" (a1),"Alice" (d4),"Yagi-Repeater" (a1),"MockCompanion" (00)
HOP FROM TO SNR_DB
0 00 a1 +6.0
1 a1 d4 +2.0
2 d4 a1 +2.4
3 a1 00 +6.0
```
`target` is `-` because a path walk has none. Walks record under a sentinel so they stay out
of the target picker's history.
#### `meshterm tx-optimize`
Sweep a remote node's transmit power against a target and pick the lowest level that still
gets through reliably.
| Option | What it does |
| --- | --- |
| `-p`, `--path SPEC` | **Required.** Forced path ending at the target (`Repeater,Target`, or `3d,f2`). The hop before the target is the node being tuned. |
| `-n`, `--samples N` | Traces per TX level. Default 3. |
| `--step N` | Coarse sweep step. Default 3. |
| `--min N` / `--max N` | Bound the sweep (else the `tx_opt_min` / `tx_opt_max` preferences). |
| `--password TEXT` | Admin password for the tuned node; else the remembered one, else prompted. |
| `--apply` / `--no-apply` | Set the winner on the node. On by default. |
```console
$ meshterm tx-optimize --path "Yagi-Repeater,Alice" --samples 5
tuning_node Yagi-Repeater
target Alice
path a1,d4
optimal_tx_dbm 22
target_snr_db +4.1
reliability 1.00
previous_tx_dbm 27
applied yes
TX_DBM TARGET_SNR_DB SUCCESSES SAMPLES
18 -1.2 2 5
21 +2.8 5 5
22 +4.1 5 5
```
The winner comes first because it is what you asked for; the per-level records follow so the
choice can be checked against the measurements it was made from. This transmits many times
(paced by the `trace_cooldown_s` preference) — it is the one command that is not a single
transmission.
Returns `5` when no trace reached the target at any level: nothing was measured, and the
node was left at its original power.
---
### Other nodes
#### `meshterm repeater-admin`
Send one command to a remote repeater or room server over the mesh and print its reply.
```
meshterm repeater-admin NODE COMMAND... [--password TEXT]
```
| Argument / option | What it does |
| --- | --- |
| `NODE` | The remote contact's name. |
| `COMMAND...` | The text command to send, as the node's own CLI spells it. |
| `--password TEXT` | Admin password; else the one remembered from an interactive login. |
```console
$ meshterm repeater-admin Yagi-Repeater get name
Yagi-Repeater
```
The reply is printed as the node sent it — its own text, and the whole answer. A node that
does not answer is not a failure (the command may well have landed), but there is nothing to
report, so it returns `5`.
Logging in happens automatically from the remembered password; run the interactive flow once
to store one, or pass `--password`. A refused login clears the saved password and fails with
`4`.
---
### MeshTerm itself
#### `meshterm preferences`
How MeshTerm behaves, as opposed to how the radio is configured. Reads nothing from the
companion, transmits nothing, and works with no device attached.
| Subcommand | What it does |
| --- | --- |
| `show` | Every preference, its value, and its built-in default. |
| `get KEY` | One preference's value, bare. |
| `set KEY VALUE` | Change one preference. |
| `reset --yes` | Return every preference to its default. |
```console
$ meshterm preferences show
PREFERENCE VALUE DEFAULT
trace_cooldown_s 5 5
flood_advert_cooldown_s 5 60
map_view_fraction 0.5 0.5
history_days 365 365
```
Values print in the form `preferences set` accepts back — no unit suffix on a number,
`on`/`off` for a boolean. Where `VALUE` differs from `DEFAULT`, this install has an override.
Overrides live in `~/.meshterm/preferences.yaml`, which lists only what you have changed;
delete a line and the default takes over again.
#### `meshterm about`, `about-author`, `discord`, `support`
The four written pages, printed as plain text. `about` is what MeshTerm is and the terms it
ships under; `about-author` is who wrote it; `discord` is the community invite; `support` is
what keeps it going.
```console
$ meshterm discord
Join the Discord
Questions, ideas, bug reports, and mesh talk.
• https://discord.gg/AZwe5Uvb3S
```
The menu draws a QR code beside each link; the scripted face prints the link alone.
#### `meshterm platform`
A diagnostic for which UI flavour a given invocation resolves to, and why.
```console
$ meshterm platform
platform regular
platform_source default
flag -
env -
device_tree_model -
icons yes
icons_source no-console
```
`icons` is a separate question from the flavour, and the usual reason a Windows session looks
plainer than the screenshots.
#### `meshterm specimen`
Print the visual-language specimen — every mark, icon, colour scale and fold on one card.
**This is the one command that keeps its colour**, and it builds its own themed console to do
it. That is deliberate: the colour *is* the output. It is the font-and-palette acceptance
card on the PicoCalc console, and with `--platform picocalc` it previews that flavour from a
desktop. A monochrome specimen would test nothing.
---
## Recipes
**Watch a node and shout if it goes quiet.** `trace` returning `5` means the walk ran and
nothing came home.
```bash
#!/usr/bin/env bash
target=${1:?usage: watch NODE}
meshterm trace --target "$target" > /dev/null
status=$? # capture it first: `if ! cmd` would have reset $? to 0
if [ "$status" -eq 5 ]; then
echo "$target did not answer" | mail -s "mesh alert" me@example.com
fi
```
**Nightly config archive**, keeping the path the command actually wrote:
```bash
out=$(meshterm config backup "$HOME/mesh/$(date +%F).toml") && echo "archived $out"
```
`config backup` prints the file it wrote, bare and one per line, so the path comes back to
the caller.
**Fields out of a listing.** Names are quoted, so a CSV-aware reader is the safe way in;
`awk` works when you split on the quotes rather than on spaces.
```bash
# every repeater's key
meshterm contacts | awk 'NR > 1 && $0 ~ /repeater/ { print $NF }'
# names alone, quotes stripped
meshterm contacts | awk -F'"' 'NR > 1 { print $2 }'
```
**One value into a variable:**
```bash
sf=$(meshterm config get radio_sf)
[ "$sf" -lt 9 ] && meshterm config set radio_sf 9
```
**Capture a window of traffic to a file**, with the progress noise left on the terminal:
```bash
meshterm monitor --seconds 300 > /var/log/mesh-$(date +%s).tsv
```
**Sample a trace politely.** A trace transmits exactly once per invocation by design; pace
the repeats yourself:
```bash
for i in $(seq 5); do
meshterm trace --target Alice --path "3d,f2,3d"
sleep "$(meshterm preferences get trace_cooldown_s)"
done
```
**Queue for someone who is offline**, then chase it later:
```bash
id=$(meshterm courier queue Alice "call me" | awk '{print $2}')
# ... later ...
meshterm courier send "$id"
```
**Cron-friendly quiet:** `--quiet` suppresses console logging entirely, leaving only the
command's own output and any error line.
```cron
*/30 * * * * meshterm --quiet --profile yagi config advert --flood
```
---
## What has no command
Some features have no scripted face, and that is the honest answer rather than a degraded
one. Each of these is a *live picture* — its meaning is where things sit on a grid, how they
move, and what colour they are — and none of that survives being turned into records.
| Feature | Why not, and what to use instead |
| --- | --- |
| **Map** | A drawing of braille cells whose nodes are told apart by colour. Stripped of colour to match the rest of the CLI it would be unreadable; left coloured it could not be piped anywhere useful. `meshterm contacts` lists the same nodes, coordinates and all. |
| **Dashboard** | A live overview that repaints every second. `meshterm monitor` captures the same traffic as records. |
| **Live feed** | Every packet as it arrives, newest first, with a viewer behind each row. `meshterm monitor` is the scripted tail. |
| **Watchtower** | A background sentinel over starred nodes; it exists to *interrupt* a session. Build the same alarm from `meshterm trace`'s exit status (see [Recipes](#recipes)). |
| **Time machine** | Braille charts over a switchable window. The underlying history is in the database `--db` points at. |
| **Mesh walk** | An evidence graph walked one node at a time. `meshterm records` and `meshterm trace` cover the walks it is built from. |
Everything else in the menu has a command, listed above.