reolink-cli Β· diff

git:20260904.b462e9d to git:20260904.43ec62d

1 added, 1 removed. Audit B to B.

---
name: reolink-cli
description: "Provides the only correct mechanism for operating Reolink network cameras locally β€” the installed `reolink-cli` binary plus its gateway. **You MUST consult this skill whenever the user wants to do anything with an IP / network / surveillance / doorbell camera on their LAN, even if they never say \"Reolink\"** β€” e.g. \"front-door cam\", \"garage camera\", \"IP camera at 192.168.x.x\", \"make it brighter\", \"stream URL\", \"too dark\", \"upside down\", \"sound the siren\", \"scare them off\", \"arm the house\", \"i am leaving\", \"set it to away mode\", \"disarm\", \"lower the bitrate\", \"change resolution\", \"switch to h265\", \"upgrade firmware\", \"flash firmware\". Covers discovery, login, snapshots, live preview, PTZ, IR/spotlight/LEDs, image tuning, OSD, the manual siren, scene / arming modes on a hub, video encoder settings (resolution / frame rate / bit rate / H.264-H.265), motion + AI detection (person/vehicle/pet/package), recording + SD-card status, VOD search/download, alarm events, user accounts, reboot, firmware upgrade, and RTSP/RTMP/FLV stream URLs (Frigate / Home Assistant / go2rtc / VLC). Do NOT trigger when the user names a competing brand (Nest, Ring, Wyze, Eufy, Amcrest, Hikvision, Arlo) or asks a generic networking / Frigate-config question not tied to operating a specific camera."
metadata:
openclaw:
requires:
bins: ["reolink-cli", "reolink-gateway"]
emoji: "πŸ“·"
always: false
---
# Reolink Camera Operator
## Overview
Primary surface: `reolink-cli` (JSON stdout by default). Don't start the MCP server unless asked. When the user needs non-CLI access (browser, curl, other languages), point them at the gateway's `POST /api`.
**Slash commands (Claude Code only)** β€” prefer these for fixed/single-action intents; skip the intent-mapping overhead:
| Slash | Equivalent | When to use |
|---|---|---|
| `/reolink-cli:status` | `reolink-cli status` | user asks about current state / dashboard / "how are things" |
| `/reolink-cli:features` | `reolink-cli features` | user asks what the plugin can do / "what can it do" |
| `/reolink-cli:scan` | `reolink-cli discover` | scan local network / "scan" |
| `/reolink-cli:devices` | `reolink-cli device list` | list registered cameras |
| `/reolink-cli:cache-clean` | `reolink-cli cache clean` dry-run→apply | clear old snapshots / "clear cache" |
| `/reolink-cli:update` | `reolink-cli self-update` | upgrade to the latest release (checks GitHub, no-op if current) |
| `/reolink-cli:uninstall` | `reolink-cli setup --uninstall --purge --no-interactive` | complete uninstall |
**Claude Code runtime:** **Must** use the slash command when the user's intent matches one of these seven exactly β€” faster (no LLM latency), deterministic, and discoverable via `/` autocompletion.
**Other agents (Codex / Cursor / Copilot / Gemini):** these surfaces don't support plugin-defined slash commands, so the "slash-first" rule doesn't apply β€” skip straight to running the equivalent `reolink-cli …` CLI command directly. Skill-driven intent mapping handles everything else.
**All agents:** **Fall back to skill-driven CLI chaining** for compound / fuzzy intents ("announce when a person is detected", "make it brighter") that aren't covered by the slash list / seven fixed actions above.
**The gateway is mandatory for almost every control command.** The CLI routes through `127.0.0.1:9000` by default (set in `config.toml` under the platform config dir β€” `~/.config/reolink-cli/config.toml` on Linux, `~/Library/Application Support/reolink-cli/config.toml` on macOS, `%APPDATA%\reolink-cli\config.toml` on Windows β€” as `gateway-addr`). If you see `gateway connect failed: Connection refused`, the gateway isn't running β€” start it once:
```bash
reolink-cli gateway start --addr 127.0.0.1:9000 &
```
Works **without** the gateway: `device add|list|update|remove|resolve|show`, `config init`, `discover`, `features`, `doctor`, `cache status|clean` β€” everything that reads local config or talks to the network directly. (Verified by running each with no gateway listening.) Everything else (`ping`, `login`, `info`, `config get/set`, `image`, `osd`, `ptz`, `detect`, `light`, `audio`, `preview`, `snapshot`, `vod`, `events`, `users`, `system reboot`, `system upgrade`) needs the gateway up.
**Envelopes** β€” parse `.ok` on CLI, `.status` on gateway:
| Surface | Success | Error |
|---|---|---|
| CLI | `{ok:true, command, protocol, data}` | `{ok:false, error:{code, message, retryable}}` (stderr) |
| CLI batch | `{command, summary, results:[{camera, host, ok, data, error}]}` | per-target `ok` |
| Gateway `/api` | `{status:"success", data}` | `{status:"error", code, message}` (HTTP status == code) |
`references/` has topic-scoped recipe files. **Must** load only the one matching your task β€” `references/index.md` is the router. `references/troubleshooting.md` has diagnosis decision trees for failures.
## Ground Truth (Binding)
**Forbidden** stating or implying as factual: device reachability, login state, configuration values, capabilities, detection/AI types, event counts, VOD file existence, user accounts, or any observable camera state β€” **except** when derived from the JSON output of an **executed** `reolink-cli` command or gateway `POST /api` response within the current task.
**Forbidden**: demo-style lists, guessed layouts, synthetic values, fake success after errors/timeouts, prose/JSON mimicking CLI output without a real response, or "the device has X" when no `get`/`info`/`inventory`/`capabilities` has run.
**Must** when data is missing: state what's missing and the cause (no camera registered, gateway down, auth failed, device offline), then take one concrete recovery step (from Error Recovery table) or ask a single clarifying question. **Forbidden** padding with plausible-sounding defaults.
## Intent Interpretation
Resolve ambiguity before picking a command. If still unclear, **ask with options**.
| User says | Command | Why / caveat |
|---|---|---|
| add a user | `users add` | Device account, NOT a CLI camera entry |
| add a camera | `device add` | CLI camera entry (`~/.config/reolink-cli/aliases.toml`), NOT a device account |
| change password | `users passwd` (default) OR `device update` (CLI creds) | Ask which |
| blinking light | `light statusled` | Body LED |
| night vision | `light ir` | Invisible IR LED |
| spotlight | `light spotlight` (manual) OR `light whiteled` (alarm-triggered) | Same bulb, different wiring |
| too dark | `image tune --bright` first, check `light ir get` if night | Image tuning, not spotlight |
| upside down | `image flip set --flip` | Vertical |
| mirrored | `image flip set --mirror` | Horizontal |
| mask | `privacy mask` | Read current regions first |
| rename | `osd set --name` (on-screen) OR `config set device-name` (app) | Ask which |
| restart / reboot | `system reboot` | 30–60s offline |
| upgrade / flash firmware / OTA | `system upgrade <file.pak>` | ⚠️ bricking risk. Match model+hw_ver via `info`; GATEWAY reads the LOCAL file; auto windowed/stop-and-wait. Detail in `references/admin.md` |
| factory reset | β€” standalone; `system upgrade --factory-reset` resets *during* a flash | Standalone reset not in CLI; ask user to use Reolink app |
| kick off | `users remove` (permanent) OR `users passwd` + `system reboot` (evict session) | Active sessions survive `users remove` until reboot |
| snapshot | `snapshot [--file PATH]` | JPEG. **Parent directories are auto-created** by the binary (since v0.5.0) β€” do NOT pre-`mkdir -p` the destination. Same for `vod download` and `preview capture --file`. |
| **slow / how long / time taken / performance / latency / benchmark** | **`benchmark [--iterations N] [--phases ...] [--reuse-session]`** | Per-phase p50/p95/p99: connect, login, info, snapshot. `--reuse-session` for warm-path. Detail in `references/index.md`. |
| **cpu / memory / device load / is the camera overloaded / stuttering** | **`config get performance`** | Live reading from the device: `cpuUsedPercent`, `codeRate`, `netDataRate`. Read-only and instantaneous β€” two calls a second apart legitimately differ. Not every model implements it; those answer a device-level rejection. Distinct from `benchmark`, which times the **client** round trip, not the camera. |
| **rtsp / rtmp / flv address / stream URL / HA / Frigate / go2rtc / VLC** | **`stream url [--kind rtsp,rtmp,flv] [--stream main,sub,ext] [--with-auth]`** | Default `--kind rtsp --stream main`. `--with-auth` only when user explicitly wants one-shot pasteable URL. NVR: `device expand` then `--tag <nvr>`. Detail in `references/media.md`. |
| nvr with 8 channels / RLN sub-cameras / channel N | `device expand <nvr-name> [--yes \| --names A,B,C]` | **NVR only**. Registers one entry per channel, tagged with parent name. After: `--tag <nvr-name>` fans out. |
| command rejected 400 / "model doesn't support it" / battery camera on a hub / first command after a pause | Nothing β€” the gateway already retries. `attempts` in the answer says which try got through (3 is normal after a pause, 1 during a burst) | A battery child behind a hub sleeps and then rejects commands with a bare 400, indistinguishable from unsupported. **When** it sleeps is not predictable from idle time alone (one day's readings said ~2 min; later overnight windows found it awake), so do not try to pre-empt it. What is consistent: a sleeping camera costs a flat 3 attempts (~1.6 s) to wake, and the refused request is itself the wake. If it still fails after the budget, `info` β†’ `channel.loginState` = `standby` explains it β€” but **that field lags**, so it explains a failure, it cannot pre-flight one. |
| dual-lens / two lenses / bullet+PTZ in one camera / wide and telephoto / second lens | `info` first (`channel.views`, `channel.dualLens`), then `--view N` on config commands | `--view` is a **third** axis: `--channel` picks the camera, `--view` picks the lens, `--stream` picks that lens's encoding profile. Verified per-view: `encode`, `privacy mask`, `detect motion`, PTZ. **Not** per-view: `snapshot` (device answers 400 for viewβ‰₯1), `preview`/`stream url` (protocol has no selector β€” these now **reject** `--view N` rather than silently handing back view 0), image/ISP (shared). Default 0 = the only view an ordinary camera has. |
| **preview / take a look / watch N minutes / watch live** | **`preview play`** (opens ffplay window) | **DEFAULT to `play`, not `capture`.** User wants a live window, not a file. |
| record N seconds / save a clip / save it | `preview capture --packets β‰ˆNΓ—25` | Only when user explicitly says save / record / export. Packet count, not seconds; ~25 fps main, ~10 fps sub |
| 60-second preview (ambiguous!) | If user says "preview for N seconds/minutes" β†’ `preview play --packets NΓ—fps`. If user says "record N seconds" β†’ `preview capture`. Ask if truly unclear. | Default to `play` when the verb is "preview/watch", `capture` only when the verb is "record/save/export" |
| volume | `audio volume set` (master) | Ask if they mean mute: `audio mute` |
| mute alarm | `audio mute` | Alarm audio only, not preview |
| **siren / alarm sound / sound the alarm / scare them off / make it beep** | **`audio siren play [--duration N \| --times N] [--wait]`** | Loud + side-effectful β€” confirm first. cmd 263 manual ring-down (the app's siren button); device self-stops after `--duration` s, or `audio siren stop`. `--wait` holds for the duration then stops explicitly and reports `stoppedAt` + `stopAcknowledged` β€” use it when you must be able to say the siren stopped, not just that it started. **A 400 from a battery camera behind a hub is not evidence about the hardware** β€” a sleeping child answers 400 exactly like an unimplemented command. (Separately: on one Home Hub 2 the siren was measured not to sound at all even when the command succeeds β€” see `verified: false` below. Never claim it sounded.) The gateway retries (measured: a camera idle >2 min needs 3 attempts, ~1.6 s), and `attempts` in the answer tells you which try got through. If it still fails, `info` β†’ `channel.loginState` = `standby` explains it β€” but that field lags, so it is for explaining a failure, not for checking before one. Never tell a user their camera lacks a feature on the strength of a 400; `capabilities` is what answers that. Same rule for `light spotlight`. "siren when motion is detected" is different β€” that is `audio siren task` linkage (often 405 on newer models). |
| **arm / disarm / away / at home / i'm leaving / scene mode / stop recording while i'm in** | **`scene set <id>`** (`scene list` for the ids, `scene set --schedule` to hand back to the timetable) | Hub/NVR only. One switch re-arms every channel β€” it does **not** touch per-camera detection config. `scene show` first: `currentSceneId` 0 means the timetable is driving. Detail in `references/scene-mode.md`. |
| **resolution / bitrate / fps / frame rate / h265 / h264 / stream quality / make the stream smaller** | **`encode set --stream main\|sub\|third ...`** | **Run `encode capability` first** β€” a value the device does not advertise comes back as a bare 400. `stream url` is unrelated (that only builds a playback URL). |
| person detection | `detect ai --type person` | Check `device inventory --capabilities` first |
| vehicle | `detect ai --type vehicle` | Not all models |
| turn on recording | `record schedule set --enable` | |
| record config / recording params / pre-record seconds / clip size | `record config get` | Read-only; cycle / pre/post record / package time |
| SD card / storage card / capacity / free space | `storage status` | Read-only; totalGB / remainGB / formatted / mounted |
| manual record / record now / start recording | β€” | **Not supported** on Reolink IPCs; fall back to `record schedule set --enable/--disable` |
| download yesterday / download recording | `vod search --since 24h` β†’ `vod download NAME` | |
| download 09:45–09:55 / just that clip / a time range | `vod download --from 2026-09-02T09:45:00 --to 2026-09-02T09:55:00` | Camera-side cut β€” do NOT download the whole hour and trim |
| any alarms / any alarms today | `events query --since 24h` | Requires gateway |
| event history further back than the live buffer / what happened last week / hub event log | `--channel N events history --since 7d [--types people,motion]` (hub/NVR only) | Reads the device's recorded event log (cmd 516/517), not the ~500-entry live ring. Standalone IPCs 400. Empty list = no matching events; a hub can go briefly quiet after heavy use, retry after a pause. |
| voice alert / voice announcement / announce when someone arrives / play voice when a person is detected | `audio talk` (see `references/voice-alert.md`) | PCM16 LE mono only; needs `capabilities.audioTalk=1` |
| detect X do Y / automation / event trigger / detect-then-do | **Must** `events monitor init/check/run` with TOML rule. **Forbidden** ad-hoc bash `while + events query` loops β€” see `references/event-monitor.md`. |
| is the monitor running / has a rule fired / monitor status / what fired recently | `events monitor status` (pid + cursor + lastFires + per-rule `expired`/`secondsRemaining`) or `events monitor history --last N [--rule NAME] [--since 30m]` | Inspect the running daemon without restarting. Filters apply **before** `--last`, so `--rule watch --last 20` is that rule's last 20 fires, not whatever survives the global tail. |
| watch this camera for the next 30 minutes / temporary rule / bounded monitoring task | add a rule with `expires_at = "<RFC 3339 or Unix second>"`, `events monitor reload`, then read back with `events monitor history --rule NAME` | Absolute time only β€” a relative `"30m"` would restart its countdown on every reload. Expiry stops the rule firing; it does **not** rewrite the rules file, and captured history is kept. |
| apply edited rules / reload rules | `events monitor reload` (Unix SIGHUP) | Re-reads TOML without dropping cursor. Invalid TOML keeps old rules + logs warn. On Windows, stop + run again β€” SIGHUP is Unix-only. |
| what can it do / what features / what's supported / `what can this CLI do` / what's new | `reolink-cli features` (add `--output text` for human view) | Lists installed commands + highlights since v0.2.8 + XDG paths. **Must** run this first to gate which subcommands exist on the user's binary |
| how much disk used / cache size / cache usage | `reolink-cli cache status` | Inspect `~/.cache/reolink-cli/` by category (snapshots/audio/captures/downloads/logs) |
| current status / how are things / system health / overview / dashboard | `reolink-cli status` | Fleet + gateway + events-monitor + cache. `--camera X` to include recent events. |
| is the gateway running / gateway status | `reolink-cli gateway status` | 500ms TCP probe; on `[DOWN]` prints exact `gateway start` to run. |
| check / diagnose / doctor / what's wrong / sanity check | `reolink-cli doctor` | 9 offline checks (binary, dirs, config, registry, perms, gateway TCP). Use FIRST when user says "broken". |
| tab completion / shell completion | `reolink-cli completions {bash\|zsh\|fish\|powershell\|elvish} > <path>` | Re-run after `self-update`. |
| **first-time setup / configure camera / set up reolink / how to use after install / how to get started** | **`reolink-cli config init`** then **`device add`** | `config init` writes default gateway addr; `device add <alias> --host <ip> --user admin` registers the first camera (password via prompt or `--password-stdin`). Then `--alias <name> login` to verify. |
| **uninstall / delete / remove reolink** | **`reolink-cli setup --uninstall --purge --no-interactive`** | **Confirm with the user before running this β€” `--purge` deletes their camera registry and cannot be undone.** Without a terminal on stdin β€” which is every command you launch β€” the uninstall refuses unless `--no-interactive` is present; that flag is the declaration that the deletion was intended, so do not add it reflexively to make an error go away. Removes binaries + config/cache/state + cross-agent skill dirs + Claude Code plugin registry. Drop `--purge` to keep config/cache/state (alias list preserved) β€” there is no `--keep-config` flag. The agent skill dirs are global (`~/.claude/skills/…`, `~/.agents/skills/…`): they are removed regardless of `REOLINK_PREFIX`, so uninstalling one copy of a side-by-side install unlinks the skill for both. If the customer still has the extracted tarball, `./uninstall.sh` in there is the symmetric alternative (it forwards straight to this same command; the uninstall flow only honours `--purge`, `--no-interactive` and `--prefix`). (Note: `npx skills remove reolink-cli` is rarely needed β€” `reolink-cli setup --uninstall` already wipes the agent skill dirs.) |
| skill stale / plugin cache stale / refresh skill | `reolink-cli plugin refresh` | Auto-detects agent (Claude Code / Codex / Cursor / Copilot / Gemini). Run when `features` reports `cache_state != in_sync`. |
| clean up / clear cache / delete old snapshots / clean cache | `cache clean [--older-than 7d] [--category X] [--apply]` | Dry-run by default β€” always preview first, run `--apply` only after the user sees the list |
## Workflow
**Default: just run the command.** The gateway daemon caches auth across CLI invocations, so the agent doesn't pay ping+login per command. Most user intents map to **one** CLI call (`info`, `get`, `apply`, `snapshot`, `stream url`, …).
**Forbidden** pre-fetching `<subcmd> --help` to "check what flags exist." The *Intent Interpretation* and *Command Reference* sections below, plus `references/<topic>.md`, already list every subcommand and its key flags; `reolink-cli features` enumerates the installed surface at runtime. **Must** dispatch the operative command directly. Only read `--help` if the command actually errored with an unknown-flag clap message. Each unnecessary `--help` round adds one full agent turn (~2–3 s of perceived user latency) for zero information gain β€” the user feels it, the CLI doesn't.
**Must** batch a single user intent into a single shell invocation when it needs multiple CLI commands. For sequences like "PTZ full sweep" (rightβ†’downβ†’leftβ†’upβ†’stop), "snapshot before/after a move", "info + capabilities + storage status", **or "health check + benchmark" / "doctor + benchmark" pairs**, chain them with `&&` (or `;` if you want continue-on-error) inside one Bash call β€” do **not** spawn N separate tool calls. Each extra tool call adds one full agent turn (~2–3 s). N=5 commands as one Bash = ~3 s perceived; N=5 commands as five tool calls = ~12 s perceived. The rule applies even when the commands are semantically distinct (e.g. `doctor` is local, `benchmark` hits the device) β€” the user asked for "both", so dispatch both in one shell line: `reolink-cli doctor && reolink-cli --camera X benchmark --iterations 3`. Independent prep checks for *unrelated* devices can still be parallel tool calls; the rule is about *one intent β†’ one invocation*.
**MCP server is the fastest path for repeated calls.** `reolink-cli mcp-server` speaks JSON-RPC 2.0 over stdio (MCP protocol 2025-11-25, ~39 tools): identity (`camera_{ping,login,info,capabilities,discover}`), PTZ (`camera_ptz_{move,stop,presets,preset_goto,preset_set,preset_delete}`), light (`camera_light_{ir,statusled}_{get,set}`, `camera_light_spotlight_set`), audio (`camera_audio_{volume_get,volume_set,mute}`), detection (`camera_detect_{motion,ai}_{get,set}`), recording/storage (`camera_record_config_get`, `camera_storage_status`, `camera_vod_search`), events (`camera_events_query`), image/OSD (`camera_image_{flip,tune}_get`, `camera_image_flip_set`, `camera_osd_{get,set}`), users (`camera_users_list`), system (`camera_system_reboot`), plus `camera_config_{get,set}`, `camera_preview_capture`, `camera_snapshot` (JPEG to a file), and `camera_raw`. Benchmarked locally: 5 sequential `camera_info` calls take 52 ms via MCP vs 108 ms via 5 Bash spawns β€” ~52% faster, saving ~11 ms per call by avoiding process startup. **When to suggest MCP wire-up**: the user is going to do >3 ops in one conversation, or runs the agent in a tight loop. **How to wire it in Claude Code** (claude.json or settings):
```json
"mcpServers": {
"reolink-cli": {
"command": "reolink-cli",
"args": ["mcp-server"],
"env": { "REOLINK_GATEWAY_ADDR": "127.0.0.1:9000" }
}
}
```
Pass `alias`, `host`, or `uid` per tool call to target a specific camera; the server validates input schemas and returns `{structuredContent, isError}` payloads. **Caveat**: the gateway must be running separately (MCP routes through it the same way the CLI does).
**`apply` recipes are one-shot.** For "set X to Y" intents, the `*** apply` subcommand does get β†’ compare β†’ (skip if idempotent) β†’ set β†’ verify internally and returns `{before, after, changed, verified}`. **Forbidden** running `get β†’ set β†’ get` manually, and **forbidden** running a separate `get` after an `apply` β€” the verify is already inside.
| Recipe | Replaces |
|---|---|
| `light ir apply --state auto\|on\|off` | manual ir get/set/get |
| `image flip apply [--flip\|--no-flip] [--mirror\|--no-mirror]` | manual flip get/set/get |
| `osd apply [--name] [--datetime] [--name-overlay]` | manual osd get/set/get |
| `detect motion apply [--enable] [--sensitivity N]` | manual motion get/set/get |
| `detect ai apply --type T [--sensitivity N]` | manual ai get/set/get |
**`ping` and `login` are NOT pre-steps. They are diagnostics.** The first command you actually need (`info`, `get`, `apply`, `snapshot`, …) does its own connect + auth via the gateway daemon. If that command's JSON has `error.code == "auth_required"`, re-run `login` once and retry. If it has `error.code == "connection_refused"` / `"timeout"` / `"no_route"`, that IS the same signal `ping` would give β€” no need to run `ping` again. **Forbidden** running `ping` followed by `login` followed by the real command on every turn; that's 3 round-trips for the work of 1. Only call `ping` standalone when the user explicitly asks "is camera X reachable?" or "why can't I connect?".
**Verify only when needed.** Re-`get` after a write **only** on capability-gated config SETs that can silently keep the old state (`light ir/whiteled/spotlight set`, `image flip/tune set`, `audio config`, `detect motion/ai set`, `osd set`, `encode set`, generic `config set`). **Forbidden** verifying after `apply` (already verified), `ptz move/stop/preset`, `system reboot`, `system upgrade` (confirm via `info` after the device reboots, not a re-`get`), `snapshot`, `preview *`, `stream url`, `audio mute/unmute/talk`, `audio siren play/stop` (a one-shot action, nothing to re-read), `vod download`, `users passwd`, `privacy mask set`. Full table in `references/troubleshooting.md#verify-rules`.
**Other safety rules** (kept verbatim β€” these break things if violated):
- **Target selectors are global options** (pre-subcommand). **Forbidden** positional. Priority: `--camera` > `--host` > `--uid` > env. Batch: `--tag`, `--cameras A,B`, `--all-devices`. **Must** pass `--channel N` for NVR.
- **Credential safety**: **Forbidden** `--password PLAIN` on argv. **Must** use `--camera <name>` (from `aliases.toml` 0600), `REOLINK_PASSWORD` env, or `--password-stdin`. If no camera registered and op isn't trivially read-only, ask user to run `device add` first.
- **Stored passwords are encrypted** (`RLENC1:…`, AES-256-GCM) with the key in `credentials.key` **beside the file that holds the password** β€” beside `aliases.toml` in every layout, including a registry redirected with `--cameras-file`/`REOLINK_CAMERAS_FILE`, and beside a redirected `config.toml` if a password lives there. Separate registry files therefore have separate keys. A plaintext config is converted automatically on first use. Do **not** try to read a password out of `aliases.toml` β€” it is ciphertext, and there is no command that reveals it. Backing up or moving a config means copying the file **and the `credentials.key` next to it** as a pair, from the same directory; with only one of them the passwords are unrecoverable and must be re-entered via `device update <camera> --password-stdin`. If a command reports a password that "cannot be decrypted", the key file is missing or mismatched β€” that is not a wrong-password problem, so do not retry with guesses. (Installs from before 0.12.4 kept a single key in the default config directory; it is still read as a fallback until the next write moves it beside its file.)
- **Pre-write read**: **Must** `get` current value before any write that's NOT covered by an `apply` recipe; confirm side effects from the table below; verify NVR channel.
- **Protocol**: leave it unset β€” it defaults to `v20`, which the whole supported fleet speaks. A v30 camera declares `protocol = "v30"` in its entry (or `--protocol v30` for one call). `--protocol auto` asks the gateway to detect it from the device's response magic and costs an extra TCP round trip; use it only to re-detect a camera whose entry names the wrong protocol.
- **Battery devices on LAN (transparent wake)**: when `--uid` is given without `--host`, the gateway automatically runs the BC3.0 Β§F.2 wake handshake on UDP 2026 before login (in customer / no-P2P builds). **Forbidden** calling `reolink-cli wake` as a precondition β€” it is a hidden diagnostic command. **Must** just call `info` / `preview` / `ptz` / etc. with `--uid` and accept that the **first** call may take 2–8 s while the device boots; subsequent calls within the gateway session TTL reuse the live session and have no wake overhead.
- **Events monitor exclusivity**: when intent pairs trigger + action ("announce when a person is detected", "car detected β†’ log"), **Must** use `events monitor init/check/run` with a TOML rule. **Forbidden** writing `while true; do events query…; done` shell loops β€” the rule engine already has debounce, parallel dispatch, SIGTERM draining, cursor bootstrapping, action-timeout, retry. See `references/event-monitor.md`.
## Global Options
`--host`/`--uid`/`--camera`/`--cameras A,B`/`--tag`/`--all-devices`, `--user`/`--password`/`--channel`/`--view`, `--protocol auto`, `--output json|text`, `--gateway-addr HOST:PORT`, `--config-file`/`--cameras-file`. Env equivalents: `REOLINK_HOST/UID/ALIAS/USER/PASSWORD/CHANNEL/VIEW/PROTOCOL/GATEWAY_ADDR/CONFIG_FILE/ALIASES_FILE`. Run `reolink-cli --help` or `<subcmd> --help` for exact flag shapes.
## Batch Operations
- **Must** keep the same selector throughout a workflow β€” the batch framework handles fan-out.
- **Forbidden** enumerating with `--tag` then switching to `--camera` inside the same workflow.
- Batch output has per-target `ok`; a single failure doesn't fail the batch. **Must** check `summary.failed > 0` and iterate `results[]`.
**Degenerate case:** when a batch selector (`--tag X`, `--cameras A`, `--all-devices`) resolves to *exactly one* device, the CLI emits the **single-target envelope** (`{ok, command, protocol, data}`), not the batch report. **Must** test `"summary" in response` to detect which shape you got. **Forbidden** assuming `results[]` exists.
## Error Recovery
| Error | Cause | Next step |
|---|---|---|
| `reolink-cli: command not found` | Binary not installed (installing the skill does not install the binary) | Run the install snippet in `references/setup.md` β†’ **Install the `reolink-cli` binary**, then retry |
| `gateway connect failed: Connection refused` | Gateway not running | `reolink-cli gateway start --addr 127.0.0.1:9000 &` then retry |
| `ping` returns `reachable:false, reason:"timeout"` | IP not routable (cross-subnet / VPN not up / host off / firewall) | Tell user: verify VPN / check subnet; **do not** try `login` β€” it would hit the same timeout |
| `ping` returns `reachable:false, reason:"refused"` | Host reachable but port 9000 closed | Wrong port / wrong IP / gateway service not running on device |
| `ping` returns `reachable:false, reason:"no_route"` | No routing entry at all | Check `netstat -rn` / VPN |
| `ping` returns `skipped:true, reason:"uid only..."` | UID-only target | Expected β€” proceed directly to `login` |
| `device unreachable` (retryable) | Net drop / device off / wrong port | Re-`ping`; check sibling |
| `auth required` / `invalid credentials` | Wrong creds | `device update`; admin-reset via Reolink app if locked out |
| `invalid params` | Caller bug | Fix request, don't retry |
| `unsupported` | Firmware lacks feature | `device inventory --capabilities` to confirm; skip |
| `Connection reset` after `system reboot` | Expected | CLI swallows this one; re-ping after 30–60s |
| Remote (UID) login timeout | First-connect warm-up | Retry with `--timeout-secs 15+` |
| `wake timed out after 3 attempts: no response from device with uid X` | Battery device not reachable on the local broadcast domain | Verify device is on the same /24 as the gateway host, UDP 2026 not firewalled, device not depleted; **do not** retry tightly β€” each attempt already takes ~3 s and the device may genuinely be off-network |
| `device acknowledged wake (booting) but did not report ready within 30s` | MCU woke but Linux main failed to come up | Physical-side issue (battery low, firmware stuck); user must check the device |
| `no active session` (gateway) | Token expired | Re-login via `set auth.login` |
| HTTP 401 (gateway) | Missing `Authorization: Bearer` | Add header |
| HTTP 403 (gateway) | Token invalid/expired | Re-login |
| HTTP 410 on `/api/login` or `/api/request` | Old v0.1.1 endpoint | Use `POST /api` + Bearer |
| `users remove admin` fails | Device refuses removing last admin | Don't |
| `users passwd` permission error | Non-admin changing someone else's | Log in as admin |
## Command Reference
Signatures only β€” run `<cmd> --help` for flag details; see `references/<topic>.md` for per-feature examples.
**Discovery / Registry:** `discover`, `device list|resolve|show|add|update|remove|import|inventory|analyze|expand`, `config init`
**NVR multi-channel:** `device expand <nvr-name> [--yes | --names A,B,C] [--drop-parent]`. RLN-series only; one entry per channel auto-tagged with parent name.
**Identity:** `ping`, `login`, `info`, `capabilities`
**Config paths** (`config get/set PATH` with merge semantics):
`led`, `device-name`, `language`, `time-zone`, `time-format`, `network`, `osd`, `osd-format`, `system-general` (get-only).
*(The paths `image` / `audio` / `alarm` / `alarm-policy` / `ai` were removed β€” they were stubs. Use dedicated commands below.)*
**Preview / Snapshot:** `preview capture|play|start|stop`, `snapshot [-o FILE] [--stream]`. `--stream sub` (the default) is the fast path β€” a small ~900x512 JPEG, ~2s on a woken camera; `--stream main` is a full-resolution frame at ~5s on a 4K model. **A battery camera behind a hub sleeps (when is not predictable from idle time β€” see the 400 row above), and the first capture after it does waits for its whole video pipeline to boot: measured 5s (sub) to 23s (4K, three channels waking at once) before the device sends a single byte.** That wait is device-side and unavoidable β€” it is not a hang, and the timeout budget covers it. A capture during a burst is sub-second; treat the first one after any gap as a warm-up rather than trying to predict which one pays the wake.
**Benchmark:** `benchmark [--iterations N] [--phases connect,login,info,snapshot] [--reuse-session] [--warmup M]` β€” per-phase p50/p95/p99/mean/stddev. Read-only.
**Doctor:** `doctor` β€” 9 sanity checks (binary, dirs, config.toml, registry, perms 0600, legacy cleanup, gateway TCP). Non-zero exit on fail. **Use first** for "broken" reports.
**Completions:** `completions {bash|zsh|fish|powershell|elvish}` β€” emits to stdout; redirect into shell's completion dir. Re-run after `self-update`.
**Gateway status:** `gateway status` β€” 500ms TCP probe of the resolved gateway-addr. Text shows `[LISTENING] / [DOWN] / [UNCONFIGURED]` + the `gateway start` command on down.
**Gateway log files (since 0.12.5):** the gateway always writes a compact single-line log to `<cache-dir>/logs/gateway-<pid>.log` (`~/.cache/reolink-cli/logs/` on Linux, `~/Library/Caches/reolink-cli/logs/` on macOS, `%LOCALAPPDATA%\reolink-cli\logs\` on Windows) at `info` level; files older than 14 days are pruned at startup, and `cache clean --category logs` manages them like any other cache category. **To collect evidence after a failed run: zip the `logs/` directory β€” no pre-configuration needed.** For deep traces re-run with `RUST_LOG=debug` (adds per-round LAN probe results, every TCP connect attempt with address+outcome, uid-cache hits with age). `REOLINK_LOG_FILE=off` disables the file; `REOLINK_LOG_FILE=<path>` redirects it (user-managed, no pruning). Credentials never appear in logs; entries do contain hosts/UIDs/SSIDs, so redact before posting publicly.
**Stream URLs:** `stream url [--kind rtsp,rtmp,flv] [--stream main,sub,ext] [--with-auth]` β€” defaults to `rtsp main`. `--with-auth` embeds creds (use only when user wants pasteable URL); without it ships sibling `user`/`password` JSON fields. NVR: `device expand` then `--tag <nvr> stream url`. RTSP path is `/Preview_{NN}_{main|sub|ext}`. Detail in `references/media.md`.
**Light:** `light {ir|statusled} {get|set --state auto|on|off}`, `light ir apply --state auto|on|off` (recipe; prefer for "set X"), `light spotlight set --enable|--disable [--duration]`, `light whiteled {get|set [--enable|--disable] [--brightness 0-100] [--alarm-mode] [--detect-type T,...]}`. *Caveat:* E-series can `ok:true` on `light ir set` while silently keeping old state β€” `apply` recipe surfaces this in `verified:false`; check `device inventory --capabilities` if write ignored.
**Image:** `image flip {get|set|apply [--flip|--no-flip] [--mirror|--no-mirror]}` (apply is the idempotent recipe), `image tune {get|set [--bright N] [--contrast N] [--saturation N] [--hue N] [--sharpen N]}` β€” all 0–255, 128 = neutral. If a user says "60%" that's `~160`.
**OSD:** `osd {get|set|apply [--name NAME] [--datetime|--no-datetime] [--name-overlay|--no-name-overlay]}` (apply is the idempotent recipe)
**System:** `system reboot`; `system upgrade <file.pak> [--factory-reset] [-y]` β€” flash firmware (gateway reads the LOCAL file; ⚠️ bricking risk, match model+hw_ver; auto windowed/stop-and-wait by capability; detail in `references/admin.md`)
**Users (device accounts):** `users list`, `users add NAME --level admin|user`, `users remove NAME`, `users passwd NAME`. Password via TTY prompt or `--password-stdin` β€” never `--password PLAIN`. Name + password each 1–31 chars. Non-admin users can only change their own password.
**Audio:** `audio config {get|set}`, `audio volume {get|set LEVEL}` (0–100), `audio mute`, `audio unmute`, `audio replies`, `audio talk {--file PATH | --stdin} [--sample-rate N]`. `audio volume` is master; `audio config.volume` is per-profile β€” ask which. `audio talk` pushes PCM16 LE mono to camera speaker via talkback (cmd 201 open + 202 stream). Gate on `capabilities.audioTalk=1`.
**Siren:** `audio siren play [--duration 1-600 | --times 1-100] [--wait]` (default 2 repetitions β‰ˆ 6s; device self-stops after the last one), `audio siren stop` (silence now). The device sounds in whole repetitions of ~3.2s, so `--duration` rounds **up** to `--times` and the answer reports both, plus `speakerVolume` β€” **if the user says they heard nothing, read that first** (a measured case sat at 7/100 and was inaudible; the command succeeded regardless). **Up to 0.14.3 the siren never actually sounded** β€” the CLI sent `playMode 1` (by duration), which this firmware accepts with a 200 and ignores; `playMode 0` (by times) is the one that makes noise, confirmed against the camera's own microphone. `--wait` blocks for the duration, then sends an explicit stop and adds `waited`/`stoppedAt`/`stopAcknowledged` to the answer; the firmware timer is still sent, so a killed CLI does not leave the siren running. Without `--wait` the answer is `accepted: true, verified: false` and nothing more β€” v20 has no GET for siren state, so **never** report that the siren sounded on the strength of it. This is the app's manual-siren button = **v20 cmd 263** `NET_MANUAL_RING_DOWN` (`<audioPlayInfo>` continuous/switch mode), verified on real RLC-823/810. Gate on `capabilities` / JSON `GetAbility.supportAudioAlarm`. Also `audio siren task {get|set --on|--off}` (which alarms trigger it) + `audio siren enable {get|set [--on|--off] [--channel-on|--channel-off]}` β€” **two** switches: device-wide `mainSwitch` and a per-channel `channelMask` bit. A hub can report master-on with every channel bit zero; `--on` alone cannot fix that, which is why `--channel-on` exists β€” **v20 cmds 306/307, 316/317, but this whole v3.x generation returns 405 for them**; only manual play/stop is universal. A sounding device pushes cmd 547 β†’ `events stream` shows `siren.on`/`siren.off` (no GET for siren state). NB: do NOT confuse with cmd 349 (play a *stored* audio file) β€” it 400s on cameras with an empty audio store, i.e. almost all of them.
**Scene mode (hub arming profiles):** `scene show` (active scene + which task bits this hub honours + the ids), `scene list` (every scene with its per-channel tasks), `scene status`, `scene set <id>` / `scene set --schedule`, `scene edit <id> [--tasks record,ftp,email,push,audio,linkage,speaker,track] [--channel N] [--iot-tasks linkage,device] [--name ...] [--delay N]`, `scene schedule get|set --scene <id> [--days 0,1] [--from H] [--to H]`, `scene alarm get|set --task REC --types people,vehicle [--channel N]`, `scene options [--key-enable|--key-disable] [--private-mode|--no-private-mode]`.
A scene is a named set of per-channel task bits, so switching scenes re-arms the whole hub at once β€” that is the app's Home / Away / Disarm control. `audio` in the task list is the **siren**, not the microphone. Scene id `0` is not a scene: it hands control to the weekly schedule, and `scene show` reports that as `followSchedule: true`. `--tasks` **replaces** the scene's set rather than adding to it, and without `--channel` it applies to every channel in the scene. The schedule is 7 days Γ— 2 half-hour slots Γ— 24 hours; `--days` takes the device's own 0-6 numbering.
**Encoder:** `encode get` (all three streams), `encode capability` (what the device accepts β€” **read this before setting**), `encode set --stream main|sub|third [--resolution WxH] [--fps N] [--bitrate KBPS] [--codec h264|h265] [--rate-control vbr|cbr] [--profile default|baseline|main|high] [--gop N] [--smart-h265|--no-smart-h265] [--smart-h265-level 0-2]`. The wire SET (cmd 57) rewrites all three streams at once, so the CLI always re-reads first and patches only the flags you pass. Changing resolution restarts the stream β€” any live preview/RTSP consumer drops. GOP is pinned by firmware when H.265+ is on (level 0/1 β†’ 2, level 2 β†’ 4), so `--gop` is ignored in that case.
**Privacy:** `privacy mask {get|set [--enable|--disable] [--json JSON]}`. Get first to see current regions (device-pixel `{x,y,width,height}`).
**PTZ:** `ptz move DIR [--speed 1-64] [--duration-ms]`, `ptz stop`, `ptz preset {list|goto ID|set ID NAME|delete ID}`, `ptz {zoom|focus} {get|set --pos N}`, `ptz focus auto --enable|--disable`, `ptz patrol {list|start|stop}`, `ptz guard {get|set|snapshot|goto}`, `ptz autotrack {get|set [--enable|--disable] [--mode N] [--detect-type T,...] [--priority T,...]}`. Directions: `left|right|up|down|left-up|left-down|right-up|right-down`. Speed 1–64 (not %). If user says "speed 50" ask if it means % or literal.
**Recording:** `record schedule {get|set [--enable|--disable] [--fps 1-15] [--pre-time] [--plan-type daily|weekly] [--week-table BITMAP] [--start-hour H --start-min M --end-hour H --end-min M]}`, `record config get` (read-only: cycle / pre-record / post-record / packageTime). `--week-table` is 7-bit bitmap (Mon=bit0 … Sun=bit6); weekdays=31, Mon-Sat=63, all=127. **Manual start/stop not supported** β€” cmd 277/278/587/588 return 405.
**Storage:** `storage status` (read-only SD/HDD capacity + mount state). **Forbidden** format/init ops β€” direct users to the Reolink app if they need to format.
- **VOD:** `vod search [--from ISO --to ISO | --since DURATION] [--type T,...] [--stream main|sub] [--limit N]`, `vod download NAME [-o FILE]`, `vod download --from ISO --to ISO` (time range, the **camera** cuts it β€” one request per recording segment, joined; output is an Annex-B `.hevc`/`.h264` elementary stream, not MP4; unsupported models answer 400). Time must be **naive local ISO** (`YYYY-MM-DDTHH:MM:SS`, no TZ, no ms). Cross-month windows are handled for you (the gateway splits at month boundaries and merges); `limit` applies to the merged list, and `truncated` tells you it cut short. Filenames case-sensitive.
+ **VOD:** `vod search [--from ISO --to ISO | --since DURATION] [--type T,...] [--stream main|sub] [--limit N]`, `vod download NAME [-o FILE]`, `vod download --from ISO --to ISO` (time range, the **camera** cuts it β€” one request per recording segment, joined; output is an Annex-B `.hevc`/`.h264` elementary stream; add `--audio clip.aac` for the sound β€” it is a second request and a second file, ADTS AAC, so `ffmpeg -i clip.hevc -i clip.aac -c copy clip.mp4` remuxes losslessly; unsupported models answer 400). Time must be **naive local ISO** (`YYYY-MM-DDTHH:MM:SS`, no TZ, no ms). Cross-month windows are handled for you (the gateway splits at month boundaries and merges); `limit` applies to the merged list, and `truncated` tells you it cut short. Filenames case-sensitive.
Types: `manual|sched|io|md|people|vehicle|face|dog_cat|visitor|other|package`
**Detection:** `detect motion {get|set|apply [--enable|--disable] [--sensitivity 0-100] [--use-pir|--disable-pir]}`, `detect ai {get|set|apply} --type TYPE [--sensitivity 0-100] [--stay-time SECS]`. `apply` is the idempotent recipe β€” prefer it for "set sensitivity to N"-style intent. AI types: `person|vehicle|dog_cat|package|cry` (subset varies by model).
**Notifications:** `notify push {get|set [--interval SECS] [--rich N] [--consent N]}`
**Events (gateway required):** `gateway start [--addr HOST:PORT]` once, then `events {query [--last N] [--after] [--since] [--types T,...] | history [--from ISO --to ISO | --since] [--types] [--limit N] | stream [--timeout] [--types] | monitor {init|check|run|reload|status|history}}` with `--gateway-addr HOST:PORT`. `monitor` is the declarative rule engine β€” see `references/event-monitor.md`. **Event `--types` vocabulary is different from VOD `--type` vocabulary** β€” events use `motion|people|vehicle|face|dog_cat|visitor|package|cry` (no `md`, `sched`, `manual`, `io`, `other`). `md` is VOD-only. `--since` on `events query` accepts `<N>m|h|d` only (NOT seconds) β€” use `1m` minimum. **`events history` is a separate store from `events query`**: `query`/`stream` read the gateway's in-memory ring of live pushes (recent ~500, lost on restart), `history` asks the device (hub/NVR) for its recorded event log by time window (cmd 516/517, per-sub-device UID resolved from `--channel`); standalone IPCs 400.
**WiFi:** `wifi get` β€” diagnostic: read current SSID / authMode / encryptType / channel / countryCode. The `key` field is always redacted to `""` by the device firmware. Use to confirm a previous `wifi set` actually applied (sometimes Phase 4 rediscover times out cross-subnet but the SET succeeded β€” `wifi get` confirms). `wifi set --ssid <SSID> {--psk <PSK> | --psk-stdin} [--no-test] [--no-update-registry] [--rediscover-timeout-secs <N> (default 90)] [--hidden]` β€” push new SSID+PSK; 4-phase orchestrator: (1) capability probe, (2) WifiTest pre-check, (3) commit, (4) BCDI rediscover + registry auto-update. Exit codes follow the CLI-wide error classes: 0 success / 1 input / 2 network (device unreachable, P2P/TCP timeout β€” retryable) / 3 auth (device rejected credentials or commit) / 4 device error / 5 protocol (includes "wifi did NOT switch" read-back verify failure). Branch on the JSON `error.code` string when possible; the numeric exit is the class, not a wifi-set-specific code. Safety: device on wireless with `wifiTestAtWireless=0` cap will refuse without `--no-test`; the device itself also enforces this server-side. **Recommended path**: wired device β†’ `wifi set --ssid X --psk-stdin` then echo PSK in. **Credential safety**: **Must** use `--psk-stdin` (pipe PSK via stdin) rather than `--psk PLAIN` on argv β€” same rule as passwords.
**Wake (diagnostic, hidden):** `wake --uid <UID> [--host <HOST>]` β€” send BC3.0 Β§F.2 UDP wake packet without logging in. Broadcast if `--host` is omitted. Marked `hide = true` in top-level help β€” appears only when explicitly queried (`wake --help`). Normal workflow does **not** need this; `info`/`login`/`ptz`/etc. auto-wake battery devices via the gateway. **Forbidden** calling `wake` as a pre-step before any of those commands.
**Raw:** `raw CMD [--body-xml|--ext-xml|--body-file|--ext-file]`.
**Self-description:** `reolink-cli features [--output json|text]` β€” prints the installed version, the list of subcommands this binary supports, feature highlights with their `since` version, and the XDG file layout. **Must** run this (or rely on AGENTS.md Part 5) before dispatching subcommands you haven't confirmed exist on the user's binary β€” skill docs track master, the binary may lag.
**Cache management:** `reolink-cli cache {status|clean}`. `status` reports per-category size + oldest/newest file timestamps under `~/.cache/reolink-cli/`. `clean [--older-than 7d] [--category audio|snapshots|captures|downloads|logs|all] [--apply]` lists candidates by default; add `--apply` to delete. `--older-than 0` matches every file. Empty date-bucket subdirs under `snapshots/` are auto-pruned.
## Gateway HTTP API
Single entry `POST /api` with `Authorization: Bearer <token>` and body `{action, path, params}`. Login is itself a call: `{action:"set", path:"auth.login", params:{username, password, host, protocol?, channel?}}` β†’ `{status:"success", data:{token, info}}`. Streaming endpoints (`/api/events`, `/api/preview/video`, `/api/snapshot`, `/api/vod/download`) take `?token=` in the query for browser-native elements; the token supplies the device credentials server-side, so **never put `user`/`password` in a URL** (requests without a valid token are rejected 401/403). See `references/gateway-http.md` for curl recipes and the full spec.
## Side-effectful commands
**Must** confirm with the user before running any of these. **Forbidden** executing them opportunistically or as part of a read-only inspection flow:
| Command | Side effect |
|---|---|
| `ptz move|goto|patrol start|guard goto` | Physical motion |
| `light spotlight --enable`, `light whiteled --enable` | Bright visible light |
| `audio volume set` (high), `audio replies` triggers | Loud output |
| `audio siren play` | Very loud alarm audible outside the property |
| `scene set` / `scene edit` | Re-arms every channel on the hub at once. `scene edit --tasks` **replaces** the task set, so a scene edited down to nothing records and notifies nothing (`references/scene-mode.md`) |
| `encode set` (resolution change) | Restarts the encoder β€” live preview / RTSP / NVR consumers drop |
| `privacy mask --disable` | Unmasks previously hidden regions |
| `system reboot` | 30–60s downtime; aborts in-flight |
| `users remove|passwd` (others) | Affects login; session persists until reboot |
| `record schedule --disable` | No new recordings |
| `detect motion|ai --disable` | No push / no white-LED trigger / no md-rule recording |
| `cache clean --apply` | Permanent file deletion under `~/.cache/reolink-cli/` β€” always preview with a dry-run first |
## Gotchas
- **NVR:** **Must** pass `--channel N`; wrong channel silently returns wrong config. **Must** verify via `device resolve` when uncertain.
- **Capability variance:** `detect ai --type` support differs per model. Battery cams may be `person` only. **Must** run `device inventory --capabilities` before scripting AI calls across devices.
- Empty `{}` response β‰  error on paths the device doesn't expose β€” do not treat as failure.
- Argv wins over env when both are set for `--password`.
- Remote (UID) first connect is 10–15 s; **Must** bump `--timeout-secs 15+` when first-connecting a UID target.
- `preview capture` writes BC framing, not raw H.264 β€” ffplay needs `-f hevc|-f h264`. `preview play` already handles that.
- **Forbidden** running `events query` / `events stream` without `--gateway-addr` β€” it will fail fast with a clear error, but don't attempt it.
- Gateway tokens don't auto-refresh. **Must** re-login after long pauses or on HTTP 403.
- Batch cannot prompt mid-run. **Must** register cameras (with stored creds) before any batch operation.
- **Siren = `audio siren play` (cmd 263), never anything about "audio files".** The manual siren is a one-shot ring-down; a device 400 here almost always means the model genuinely lacks a siren, not a bad call. `audio siren task/enable` (the alarm-linkage config) is **405 on most current v3.x models** β€” if config-get 405s, manual `play`/`stop` still works; don't conclude "no siren".
- **`encode set` is a full-stream rewrite (cmd 57).** The CLI already does GET→patch→SET so untouched streams are safe, but a value the device doesn't list is rejected with a bare 400 — **Must** run `encode capability` first and pick a listed resolution/fps/bitrate. Changing resolution restarts the encoder, dropping any live preview/RTSP/NVR consumer.
## File locations (XDG layout)
Single parent per category β€” easy to purge, easy to find. **Must** prefer these paths in rules, examples, and command suggestions. **Forbidden** scattering artifacts across `/tmp/*.pcm`, `/var/reolink/...`, `/etc/reolink/...`.
| Category | Path | Managed by |
|---|---|---|
| Persistent config (CLI, aliases, monitor rules) | `~/.config/reolink-cli/` | CLI (`config init`, `device add`, `events monitor init`) β€” 0600 perms |
| App state (pidfile, cursor, debounce, trigger history) | `~/.local/state/reolink-cli/` | events monitor daemon β€” `monitor-state.json` + `monitor-history.jsonl` + `monitor.pid` |
| Disposable artifacts (snapshots, PCM clips, captures, VOD) | `~/.cache/reolink-cli/{audio,snapshots/<date>,captures,downloads}/` | user / rule authors β€” **Must** use this prefix |
| Claude Code plugin cache | `~/.claude/plugins/cache/reolink-cli/` | Claude Code (`/plugin update` refreshes) |
events monitor expands `~/` in `path` and `pcm` fields and auto-`mkdir -p`'s the snapshot parent dir β€” safe to write rules with `~/.cache/reolink-cli/snapshots/{date}/...` without pre-creating anything. Bulk purge: `rm -rf ~/.cache/reolink-cli`.
## Launcher
Prefer the installed binary: `reolink-cli …`. Plugin-bundled:
`${CLAUDE_PLUGIN_ROOT}/scripts/run-cli.{sh,cmd}`.
## References
Topic-scoped recipe files. **Must** load only the one matching your task; **Forbidden** reading the full set.
| File | Read when |
|---|---|
| `references/setup.md` | Discovery, registry, identity, generic `config get/set` |
| `references/media.md` | Preview / snapshot / VOD |
| `references/controls.md` | Light / image / OSD / audio / privacy |
| `references/ptz.md` | Any `ptz …` |
| `references/detection.md` | Motion / AI detection (`detect motion`, `detect ai`) |
| `references/recording.md` | Record schedule + core config (`record schedule`, `record config get`) |
| `references/storage.md` | SD card / HDD read-only status (`storage status`) |
| `references/notifications.md` | Mobile push (`notify push`) |
| `references/events.md` | Gateway event bus β€” `events query` / `events stream`, event shape |
| `references/event-monitor.md` | `events monitor` rule engine β€” TOML-driven automation (preferred over shell watchers) |
| `references/voice-alert.md` | Detection-triggered voice announcement via `audio talk` (shell-watcher legacy) |
| `references/admin.md` | Device user accounts, `system reboot`, raw debug |
| `references/gateway-http.md` | Gateway `POST /api` from curl or non-CLI clients |
| `references/troubleshooting.md` | Something failed; or full intent map |
Other docs (in the repo, not this skill):
- `docs/cli-reference.md` β€” generated flag reference