`!ai` sends the question to a language model. `!archy` never does: it reads the same status caches the /bitcoin-status and /electrs-status endpoints serve, so answers are deterministic, cost no tokens, and stay available when the assistant is switched off. Sub-commands: status (default), btc, electrs, version; anything else returns a usage hint. Replies are single-frame terse to respect airtime. Reuses the assistant's trust gate (is_sender_allowed) and reply routing unchanged — blocked, allowlist, trusted_only and federation-Trusted all behave identically. The one deliberate asymmetry is that !archy does not require assistant_enabled, since it never calls a model. Wired into both entry paths: plain radio text (decode.rs) and typed 1:1 chat (dispatch.rs). A command hooked into only one of them would work from a phone but not the UI, or vice versa. Adds docs/COMMANDS.md — the user-facing surface for mesh, voice and HTTP, which had no reference until now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
7.3 KiB
Markdown
142 lines
7.3 KiB
Markdown
# Talking to your node
|
|
|
|
Every way to ask an Archipelago node a question: over the LoRa mesh, by voice, and over HTTP.
|
|
|
|
Two rules worth internalising before the tables:
|
|
|
|
- **`!ai` asks a language model. `!archy` never does.** `!archy` reads the same status caches the HTTP endpoints serve, so its answers are deterministic, cost nothing, and keep working with the assistant switched off.
|
|
- **Mesh command prefixes are exact strings** (case-insensitive). **Voice phrases are not** — they are matched by Home Assistant's intent parser, so the examples below are representative, not literal.
|
|
|
|
---
|
|
|
|
## 1. Mesh commands
|
|
|
|
Sent as ordinary text over the mesh — either as plain channel/DM text from a stock meshcore or Meshtastic client, or typed into a 1:1 chat in the Archipelago UI.
|
|
|
|
### `!archy` — node status, no AI
|
|
|
|
| Command | Aliases | Answers with |
|
|
|---|---|---|
|
|
| `!archy` | `!archy status` | OS version, chain tip, peer count, electrum progress |
|
|
| `!archy btc` | `bitcoin`, `node`, `sync` | Sync state, block height, peer count |
|
|
| `!archy electrs` | `electrum` | Electrum index progress |
|
|
| `!archy version` | `ver` | Archipelago OS version |
|
|
| anything else | — | A one-line usage hint |
|
|
|
|
Examples of what comes back:
|
|
|
|
```
|
|
!archy → Archipelago OS v1.7.99-alpha: BTC synced 957295 (12p), electrum 86%.
|
|
!archy btc → BTC: synced, block 957295, 12 peers.
|
|
!archy electrs → Electrum: syncing 86.2% (824785/957295).
|
|
!archy version → Archipelago OS v1.7.99-alpha
|
|
!archy wat → archy: !archy [status|btc|electrs|version]. Node status, no AI.
|
|
```
|
|
|
|
`!archyfoo` is not a command — the prefix must be followed by whitespace or end-of-message.
|
|
|
|
### `!ai` / `!ask` — language model
|
|
|
|
```
|
|
!ai what is the halving schedule
|
|
!ask how do I open a lightning channel
|
|
```
|
|
|
|
Requires the assistant to be **enabled** (`assistant_enabled` in `mesh-config.json`). Backend is `ollama` (default, `qwen2.5-coder`) or `claude` (`claude-haiku-4-5-20251001`), set by `assistant_backend`. The model is told to reply in at most two short sentences, because airtime is scarce.
|
|
|
|
### Who is allowed to ask
|
|
|
|
Both commands share one gate — `is_sender_allowed()` in `mesh/listener/assist.rs`. Evaluated in order:
|
|
|
|
1. **Blocked contact** → always denied.
|
|
2. **On `assistant_allowed_contacts`** → allowed, even without a signature. This is the deliberate opt-in for keyless phone clients.
|
|
3. **`assistant_trusted_only == false`** → anyone on the mesh may ask.
|
|
4. **`assistant_trusted_only == true`** → the asker must be *authenticated* **and** carry federation `Trusted` status.
|
|
|
|
"Authenticated" means the message carried an Ed25519 signature that verified against the sender's known identity key, **or** it arrived over the federation (Tor) transport, which verifies upstream. **Bare plain-text radio messages are never authenticated** — so on a `trusted_only` node, a stock meshcore client can only get an answer by being on the allowlist.
|
|
|
|
Denials are silent on the wire (no airtime is spent saying "no"); the asker is recorded so an operator can allow them from the UI.
|
|
|
|
The one asymmetry: **`!ai` additionally requires `assistant_enabled`; `!archy` does not**, because it never calls a model. Turning the LLM off should not take node status with it.
|
|
|
|
### Where the answer goes
|
|
|
|
| You asked from | Reply arrives as |
|
|
|---|---|
|
|
| Plain radio text, your pubkey is known | A private unicast DM (not the public channel) |
|
|
| Plain radio text, pubkey unresolvable | A broadcast on channel 0 |
|
|
| 1:1 chat in the Archipelago UI | A chat bubble in the same thread |
|
|
| The `AssistQuery` widget | Ordered, reassembled `AssistResponse` chunks |
|
|
|
|
Size caps: 480 characters per answer overall, 200 for plain-text channel/DM replies, and a hard 160-byte LoRa frame limit underneath both.
|
|
|
|
---
|
|
|
|
## 2. Voice
|
|
|
|
A [PineVoice](https://pine64.org) satellite speaker, wake word **"Hey Jarvis"** (or press the centre button). Speech-to-text is Whisper, text-to-speech is Piper — both run locally on the node. Nothing leaves the box.
|
|
|
|
Phrases are matched by intent, so wording is flexible. These are examples, not exact strings:
|
|
|
|
| Ask something like | You hear |
|
|
|---|---|
|
|
| "What is Archipelago OS?" · "What is this node running?" | A one-sentence description plus the running version |
|
|
| "What version am I running?" | Version and uptime |
|
|
| "Is my node synced?" · "How is my Bitcoin node doing?" · "Bitcoin node status" | Sync state, block height, peer count |
|
|
| "What's the current block height?" · "How many blocks do we have?" | The chain tip |
|
|
| "Is the electrum server synced?" · "Electrum status" | Index progress |
|
|
|
|
Optional and alternative words are part of the templates, so "how is *the* node doing" and "how is *my* Bitcoin node" both land on the same intent.
|
|
|
|
### How voice is wired
|
|
|
|
The speaker is a Wyoming satellite. Home Assistant runs it through an Assist pipeline: wake word on-device → audio streamed to Whisper → intent matched → Piper speaks the answer.
|
|
|
|
| Piece | Location (on the node) |
|
|
|---|---|
|
|
| Whisper + Piper services | `~/.config/containers/systemd/wyoming-{whisper,piper}.container` |
|
|
| Wyoming entries, Assist pipeline | `home-assistant/.storage/{core.config_entries,assist_pipeline.pipelines}` |
|
|
| Sensors + spoken answers | `home-assistant/configuration.yaml` (`rest:` and `intent_script:`) |
|
|
| Phrasings | `home-assistant/custom_sentences/en/archipelago.yaml` |
|
|
|
|
Home Assistant reaches the node's own HTTP API at `host.containers.internal` — **not** the node's LAN IP, which under rootless podman's pasta networking resolves back to the container itself.
|
|
|
|
Adding a phrase means editing `custom_sentences/en/archipelago.yaml`; adding an *answer* means adding an `intent_script` entry (and a `rest:` sensor if it needs new data).
|
|
|
|
---
|
|
|
|
## 3. HTTP
|
|
|
|
Served by nginx on port 80, proxying the backend on `127.0.0.1:5678`.
|
|
|
|
**No authentication required** (5-second cache):
|
|
|
|
| Endpoint | Returns |
|
|
|---|---|
|
|
| `GET /health` | Status, uptime, version, services |
|
|
| `GET /bitcoin-status` | `getblockchaininfo` + `getnetworkinfo` + `getindexinfo` |
|
|
| `GET /electrs-status` | Index height, progress, onion address |
|
|
|
|
```bash
|
|
curl -s http://<node>/bitcoin-status | jq '.blockchain_info.blocks'
|
|
```
|
|
|
|
**Session required** — everything else goes through JSON-RPC at `POST /rpc/v1`:
|
|
|
|
```bash
|
|
curl -s http://<node>/rpc/v1 -H 'Content-Type: application/json' \
|
|
-d '{"method":"auth.login","params":{"password":"…"}}' -c jar.txt
|
|
curl -s http://<node>/rpc/v1 -b jar.txt -H 'Content-Type: application/json' \
|
|
-d '{"method":"system.stats","params":{}}'
|
|
```
|
|
|
|
Login returns a `session` cookie. Read-only methods (`system.stats`, `system.get-metrics`, `bitcoin.getinfo`, `monitoring.current`, `bitcoin.relay-status`, `tor.status`) are CSRF-exempt, so the cookie alone is enough; state-changing calls also need the `X-CSRF-Token` header. If TOTP is enabled, follow the login with `auth.login.totp`.
|
|
|
|
---
|
|
|
|
## Adding a command
|
|
|
|
- **New `!archy` sub-command** — add a variant to `NodeCmd` and a match arm in `run_node_cmd`, both in `mesh/listener/node_cmd.rs`. Keep answers under 200 characters so a stock client sees the whole thing in one frame.
|
|
- **New mesh prefix** — add a `strip_*_trigger` alongside `strip_archy_trigger`, then hook it in `decode.rs` (plain radio text) and `dispatch.rs` (typed 1:1 chat). Both paths must be wired or the command only works from one of them.
|
|
- **New voice phrase or answer** — see the voice table above.
|