Metadata-Version: 2.5
Name: cyberwave-cli
Version: 0.12.7
Summary: The official command-line interface for Cyberwave
Project-URL: Homepage, https://cyberwave.com
Project-URL: Documentation, https://docs.cyberwave.com
Project-URL: Repository, https://github.com/cyberwave-os/cyberwave-cli
Project-URL: Issues, https://github.com/cyberwave-os/cyberwave-cli/issues
Author-email: Cyberwave <info@cyberwave.com>
License: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: <4.0,>=3.10
Requires-Dist: click==8.3.3
Requires-Dist: cyberwave>=0.6.4
Requires-Dist: eclipse-zenoh==1.9.0
Requires-Dist: httpx==0.28.1
Requires-Dist: packaging==26.2
Requires-Dist: pyserial>=3.5
Requires-Dist: rich==15.0.0
Provides-Extra: build
Requires-Dist: pyinstaller>=6.0.0; (python_version < '3.15') and extra == 'build'
Provides-Extra: dev
Requires-Dist: mypy>=1.5.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

<p align="center">
  <a href="https://cyberwave.com">
    <img src="https://cyberwave.com/cyberwave-logo-black.svg" alt="Cyberwave logo" width="240" />
  </a>
</p>

# Cyberwave CLI

This module is part of **Cyberwave: Making the physical world programmable**.

The official command-line interface for Cyberwave. Authenticate and bootstrap robotics projects from your terminal.

[![License](https://img.shields.io/badge/License-MIT-orange.svg)](https://github.com/cyberwave-os/cyberwave-cli/blob/main/LICENSE)
[![Documentation](https://img.shields.io/badge/Documentation-docs.cyberwave.com-orange)](https://docs.cyberwave.com)
[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=orange)](https://discord.gg/dfGhNrawyF)
[![PyPI version](https://img.shields.io/pypi/v/cyberwave-cli.svg)](https://pypi.org/project/cyberwave-cli/)
[![PyPI Python versions](https://img.shields.io/pypi/pyversions/cyberwave-cli.svg)](https://pypi.org/project/cyberwave-cli/)
[![Release to PyPI](https://github.com/cyberwave-os/cyberwave-cli/actions/workflows/release-pypi.yml/badge.svg)](https://github.com/cyberwave-os/cyberwave-cli/actions/workflows/release-pypi.yml)

## Installation

### From PyPI (pip)

```bash
pip install cyberwave-cli
```

### From APT (Debian/Ubuntu)

```bash
curl -fsSL https://cyberwave.com/install.sh | bash
```

### From Source

```bash
git clone https://github.com/cyberwave-os/cyberwave-cli
cd cyberwave-cli
pip install -e .
```

## Quick Start - Edge

### 1. SSH into your edge device

```bash
ssh yourhost@your-ip
```

### 2. Set up your Edge device

Once you are in your edge device, set it up by:

```bash
cyberwave pair
```

`cyberwave pair` is the recommended alias for `cyberwave edge install` — both run the same first-time edge setup (workspace, environment, twin selection, package install, and boot service registration).

## Global Options

| Flag        | Description                          |
| ----------- | ------------------------------------ |
| `--version` | Print the CLI version and exit       |
| `--help`    | Show help for any command            |

```bash
cyberwave --version
# cyberwave, version 0.x.y
```

## Commands

| Command       | Description                                                   |
| ------------- | ------------------------------------------------------------- |
| `login`       | Authenticate with Cyberwave                                   |
| `logout`      | Remove stored credentials                                     |
| `configure`   | Set API token and URL directly (without login flow)           |
| `config-dir`  | Print the active configuration directory                      |
| `completion`  | Generate/install shell autocomplete                           |
| `pair`        | Pair this device as an edge node (alias for `edge install`)   |
| `twin`        | Create, pair, list, show, and delete digital twins            |
| `edge`        | Manage edge node (install, start, stop, drivers, sync, etc.)  |
| `compute`     | Manage cloud node (install, start, stop, status, logs)        |
| `workflow`    | Manage workflows (list, create, sync, activate, etc.)         |
| `worker`      | Manage local worker files for edge inference                   |
| `model`       | Manage ML model bindings on edge nodes                        |
| `plugin`      | Manage edge node plugins                                      |
| `environment` | List and inspect environments                                 |
| `scan`        | Discover IP cameras and NVRs on the local network             |
| `camera`      | Bootstrap a camera edge project                               |
| `so101`       | Bootstrap an SO-101 robot arm project                         |
| `manifest`    | Validate `cyberwave.yml` manifests                            |
| `edge bench`  | Benchmark Zenoh SDK hot paths vs a per-device baseline        |

## `cyberwave twin`

Create and manage digital twins — the virtual representation of your physical devices.

| Subcommand | Description                                           |
| ---------- | ----------------------------------------------------- |
| `create`   | Create a new twin from an asset                       |
| `pair`     | Pair this device with an existing twin                |
| `list`     | List all digital twins                                |
| `show`     | Show details of a specific twin                       |
| `delete`   | Delete a digital twin                                 |

### `cyberwave twin create`

Create a twin from an asset identifier. The ASSET argument can be a registry ID (`unitree/go2`, `cyberwave/standard-cam`), an alias (`go2`, `camera`), a local JSON file, or a URL.

```bash
cyberwave twin create camera
cyberwave twin create go2 --name "My Robot"
cyberwave twin create go2 -e <env-uuid>                      # target Edge Environment, mic, etc.
cyberwave twin create go2 --pick-environment                 # interactive environment picker
cyberwave twin create camera --pair                          # create + pair in one step
cyberwave twin create camera --pair --source "rtsp://..." --fps 15
```

**Environment selection:** without `-e`, the CLI reuses (or creates) a `Quickstart Environment` in your logged-in workspace — the same default as `client.twin()` in the Python SDK. Use `-e` for a specific environment or `--pick-environment` to choose interactively.

**Options:**

- `-n, --name`: Twin name
- `-e, --environment`: Environment UUID to create the twin in
- `--pick-environment`: Interactively pick an environment instead of the Quickstart default
- `--pair`: Also pair this device to the new twin
- `-d, --target-dir`: Directory to save the `.env` file (when `--pair` is used)
- `-y, --yes`: Skip confirmation prompts
- `--<field> <value>`: Any additional field from the asset's `edge_config_schema`

### `cyberwave twin pair`

Register this device and bind it to an existing twin. After pairing, run `cyberwave edge start` to begin streaming.

```bash
cyberwave twin pair <TWIN_UUID>
cyberwave twin pair abc-123 --camera-source "rtsp://..." --fps 15
```

### `cyberwave twin list`

```bash
cyberwave twin list                    # table view
cyberwave twin list --json             # JSON output
cyberwave twin list -e <ENV_UUID>      # filter by environment
```

### `cyberwave twin show / delete`

```bash
cyberwave twin show <UUID>
cyberwave twin delete <UUID>
cyberwave twin delete <UUID> --yes     # skip confirmation
```

## Worker Management

Edge workers are Python modules that run inside the edge worker container and
process sensor data using the Cyberwave SDK hooks API.

There are two kinds of workers:
- **Custom** workers: handwritten files you manage directly.
- **Generated** (`wf_*`) workers: auto-generated from backend workflow definitions,
  synced by edge-core. Do not edit these directly — [eject](#eject-a-workflow-worker) them first.

### `cyberwave worker list`

```bash
cyberwave worker list           # table view
cyberwave worker list --json    # JSON output
```

Lists all installed workers with their origin (`custom` or `workflow`).

### `cyberwave worker add`

```bash
cyberwave worker add ./detect_people.py
cyberwave worker add ~/workers/my_model.py --name renamed.py
```

Copies a Python worker file into `{CONFIG_DIR}/workers/`. After adding a worker,
restart the edge worker container:

```bash
cyberwave-edge-core worker restart
```

### `cyberwave worker remove`

```bash
cyberwave worker remove detect_people        # without .py extension
cyberwave worker remove detect_people.py    # with .py extension
cyberwave worker remove wf_abc123 --yes     # skip confirmation
```

Removes a worker file. Generated (`wf_*`) workers warn you that they will be
re-created on the next edge sync unless the originating workflow is deactivated.

### `cyberwave worker status`

```bash
cyberwave worker status
```

Shows the list of installed worker files (with origin labels) and the status of
the edge worker container (requires Docker).

### `cyberwave worker logs`

```bash
cyberwave worker logs             # follow logs (default)
cyberwave worker logs --tail 100  # show last 100 lines
cyberwave worker logs --no-follow # print and exit
```

Streams logs from the edge worker container (requires Docker).

### `cyberwave worker doctor`

```bash
cyberwave worker doctor                 # static + runtime checks (default)
cyberwave worker doctor --verbose       # show hints for passing checks too
cyberwave worker doctor --no-runtime    # skip the live-bus probe
cyberwave worker doctor --window 6      # longer runtime probe (seconds)
```

Diagnoses the common silent failure modes where a worker container looks
healthy but hooks report `frames: 0`. The doctor runs two groups of checks.

**Static ("paperwork") checks:**

- `cyberwave-edge-core` is installed;
- worker files in `{CONFIG_DIR}/workers/` are world-readable (UID 1001 needs to read them);
- at least one `cyberwave-driver-*` container is running on this host;
- `CYBERWAVE_ENVIRONMENT`, `ZENOH_CONNECT`, `CYBERWAVE_DATA_BACKEND` and
  `ZENOH_SHARED_MEMORY` agree between the running driver and worker containers;
- no known legacy env-var spellings slip through — currently
  `ZENOH_SHM_ENABLED` (superseded by `ZENOH_SHARED_MEMORY`). Other
  legacy names may be added as they're discovered; the check is
  deliberately conservative and only flags names we've explicitly
  mapped.

**Runtime checks** — open a short Zenoh subscription to `**` and compare
live traffic against every `@cw.on_*(<twin>)` hook declared in the worker
files. Three distinct diagnoses are emitted:

- **Sensor mismatch** — hook listens on `sensor="default"` but the driver
  publishes on another sensor name (or vice-versa).
- **Wrong twin** — the channel is flowing, but under a different twin
  UUID than the hook expects.
- **Unscoped keys** — a publisher is putting to e.g. `frames/color_camera`
  without the canonical `cw/<twin>/data/...` prefix; twin-scoped hooks
  silently drop these.

Requires `eclipse-zenoh` on the host (`pip install --user eclipse-zenoh`).
When missing, the runtime section degrades to an info message.

`cyberwave worker start` still invokes only the static checks as a
pre-flight (no bus probe, to keep startup fast); pass `--skip-preflight`
to bypass them.

### `cyberwave worker monitor`

```bash
cyberwave worker monitor               # live dashboard (default 2s refresh)
cyberwave worker monitor --update 1    # refresh every 1 second
cyberwave worker monitor -c <name>     # target a specific container
```

Opens a live-updating dashboard showing:

- **Resource usage** — CPU %, memory, network I/O (via `docker stats`)
- **GPU** — utilization, memory, temperature (Linux + NVIDIA only; shows N/A on macOS)
- **Zenoh throughput** — per-channel messages/second and totals
- **Worker hooks** — per-hook frame counts and drop rates
- **Model inference** — per-model latency (avg / P95 / P99) and inference count

The dashboard connects to the worker's Zenoh data bus for runtime metrics.

**Options:**

- `-u, --update`: Refresh interval in seconds (default: `2.0`)
- `-c, --container`: Explicit container name (auto-detected if omitted)

### Eject a Workflow Worker

Workflow-generated workers (`wf_*.py`) can be **ejected** into custom workers
when you need to customise their logic:

```bash
# 1. Copy the generated worker to a custom name
cp ~/.cyberwave/workers/wf_a1b2c3d4.py \
   ~/.cyberwave/workers/my_detector.py

# 2. Edit your copy
nano ~/.cyberwave/workers/my_detector.py

# 3. Deactivate the originating workflow in the UI
#    The wf_a1b2c3d4.py file will be removed on the next edge sync.
```

After ejection, `my_detector.py` is yours to edit freely. Edge sync never
touches files that do not start with `wf_`.

## `cyberwave workflow`

Manage workflows for automation — list, create, show, activate, sync to edge, and delete.

| Subcommand        | Description                                                     |
| ----------------- | --------------------------------------------------------------- |
| `list`            | List workflows (table or `--json`)                              |
| `templates`       | List available workflow templates                               |
| `create`          | Create a workflow (`--name` or `--template`)                    |
| `show`            | Show workflow details, nodes, and target twins                  |
| `sync`            | Sync a workflow to its edge node(s) via MQTT                    |
| `compile`         | Compile a workflow on the backend and report the emitted artifact |
| `compile-source`  | Download the generated `wf_*.py` worker source                  |
| `activate`        | Activate a workflow                                             |
| `deactivate`      | Deactivate a workflow                                           |
| `delete`          | Delete a workflow (`--yes` to skip confirm)                     |

All subcommands accept `--base-url` / `-u` to override the API URL (e.g. `http://192.168.10.101:8000`). When a UUID argument is omitted, an interactive arrow-key selector is shown.

```bash
# List workflows with target twin(s) column
cyberwave workflow list
cyberwave workflow list --json

# Create from template
cyberwave workflow create --template motion-detection

# Show details (interactive if UUID omitted)
cyberwave workflow show
cyberwave workflow show e7f1856c

# Sync workflow to edge device(s)
cyberwave workflow sync
cyberwave workflow sync --edge-active                               # only show active edge workflows in the selector
cyberwave workflow sync e7f1856c --base-url http://192.168.10.101:8000

# Activate / deactivate
cyberwave workflow activate
cyberwave workflow deactivate

# Inspect what the edge compiler emitted for a workflow (and why)
cyberwave workflow compile e7f1856c
cyberwave workflow compile e7f1856c --json     # raw API response (compiled_payload, worker_source, ...)

# Download the generated worker source
cyberwave workflow compile-source e7f1856c             # to stdout
cyberwave workflow compile-source e7f1856c -o ./wf.py  # to a file
cyberwave workflow compile-source e7f1856c -o /tmp/    # appends wf_<uuid>.py

# Delete
cyberwave workflow delete --yes
```

The `sync` command reads the workflow's nodes to find target twin(s), then sends `sync_workflows` MQTT commands to each twin's edge node.

`compile` wraps `/api/v1/workflows/{uuid}/compile` and reports the artifact shape the unified edge compiler emitted (`perception` for camera-frame graphs, `navigation` for graphs with a Move Twin node), the auto-derived target twin, model requirements, and any compiler warnings — including the diagnostic from the backend's `diagnose_compilation_dispatch` helper when the workflow's graph shape isn't compilable. Reach for it when `cyberwave workflow sync` reports the cloud isn't shipping a workflow to your edge. `compile-source` is the companion download for the generated `wf_*.py`.

## Shell Autocompletion

Enable persistent autocompletion in one step:

```bash
cyberwave completion install
```

This auto-detects your shell (`bash` or `zsh`), writes an idempotent completion block
into your shell RC file, and tells you which file to source.

### Explicit shell setup

```bash
# Bash
cyberwave completion install --shell bash

# Zsh
cyberwave completion install --shell zsh
```

### Generate scripts manually

```bash
cyberwave completion generate --shell bash
cyberwave completion generate --shell zsh
```

### Troubleshooting

- **Shell not detected**: run `cyberwave completion install --shell bash` or `--shell zsh`.
- **Permission denied writing RC file**: re-run with a writable `--rc-file` path, then source it.
- **Already installed**: the installer is idempotent and will report when completion is already configured.

## `cyberwave login`

Authenticates with Cyberwave using your email and password.

```bash
cyberwave login                                              # interactive
cyberwave login --email you@example.com --password yourpass   # non-interactive
```

**Options:**

- `-e, --email`: Email address
- `-p, --password`: Password (will prompt if not provided)

## `cyberwave config-dir`

Prints the resolved configuration directory path. Useful in scripts to locate credentials and config files without hardcoding paths.

```bash
cyberwave config-dir
# ~/.cyberwave

CONFIG_DIR=$(cyberwave config-dir)
cat "$CONFIG_DIR/credentials.json"
```

See [Configuration](#configuration) for how the directory is resolved.

## `cyberwave edge`

Manage the edge node service lifecycle, configuration, and monitoring.

| Subcommand       | Description                                              |
| ---------------- | -------------------------------------------------------- |
| `install`        | Install cyberwave-edge-core and register systemd service |
| `uninstall`      | Stop and remove the systemd service                      |
| `start`          | Start the edge node                                      |
| `stop`           | Stop the edge node                                       |
| `restart`        | Restart the edge node (systemd or process)               |
| `status`         | Check if the edge node is running                        |
| `pull`           | Pull edge configuration from backend                     |
| `whoami`         | Show device fingerprint and info                         |
| `health`         | Check edge health status via MQTT                        |
| `remote-status`  | Check edge status from twin metadata (heartbeat)         |
| `logs`           | Show edge node logs                                      |
| `install-deps`   | Install edge ML dependencies                             |
| `sync-workflows` | Trigger workflow sync on the edge node                   |
| `list-models`    | List model bindings loaded on the edge node              |
| `driver`         | Manage edge driver containers (subgroup — see [below](#cyberwave-edge-driver)) |

## `cyberwave pair`

Top-level alias for [`cyberwave edge install`](#cyberwave-edge-install). Shares the same options and behavior — use whichever reads better in your docs and scripts.

```bash
sudo cyberwave pair
sudo cyberwave pair -y
sudo cyberwave pair -e acme/envs/production-floor
sudo cyberwave pair --token cw_XXXX -e acme/envs/production-floor -y   # scripted
sudo cyberwave pair --channel dev
sudo cyberwave pair --reconfigure-camera
sudo cyberwave pair --reconfigure-microphone
```

### `cyberwave edge install`

Installs the `cyberwave-edge-core` package (via apt-get on Debian/Ubuntu) and creates a systemd service so it starts on boot. Guides you through workspace, environment, and twin selection.

**Workspace:** API tokens are bound to a single workspace, so the installer uses that workspace rather than asking you to pick one. If `CYBERWAVE_WORKSPACE_SLUG` names a different workspace, a first-time install stops and tells you to create a token for that workspace instead; a device that is already paired warns that the slug is being ignored and continues with the token's workspace. Previously either case continued against the wrong workspace and reported that no environments existed.

If the workspace cannot be determined — the API is unreachable, or returns an error — the install now says so and stops rather than picking a workspace at random and creating an empty environment in it.

**Environment:** pass `-e/--environment` to pin the environment up front and skip the selection prompt. Accepts an environment UUID or its full slug (`acme/envs/production-floor`) — the environment name on its own is not enough. The environment must already exist in this device's workspace — the one its API token is bound to, or the one selected at `cyberwave login`: `-e` selects, it never creates, and an unknown environment or one from another workspace stops the install before anything is installed rather than continuing against the wrong target. To attach a device to a different workspace, pass a `--token` for it or run `cyberwave login` again. Without `-e`, `-y` attaches to the most recently created environment in the workspace, which is rarely what you want when provisioning devices. `-e` pins the environment only — twin selection still prompts, and adding `-y` to skip that prompt selects the first edge-compatible twin, so a fully scripted install is currently only reliable for an environment with a single twin.

**Authentication:** by default the installer reuses the credentials saved by `cyberwave login`, falls back to `CYBERWAVE_API_KEY`, and prompts for email and password if neither is available. Pass `--token` to authenticate with a specific API token instead. Unlike `CYBERWAVE_API_KEY`, which only applies when no valid credentials are stored, `--token` always wins — so re-pairing a device onto a different workspace does not silently reuse the old token. A rejected `--token` stops the install rather than prompting for a password. `--token` changes the credential, not the backend: `CYBERWAVE_BASE_URL` still selects that, and a device already paired against a dev or staging stack keeps it.

Because the token appears in your shell history and in `ps` output, prefer `CYBERWAVE_API_KEY` on shared machines and `--token` in provisioning scripts that already handle secrets.

**Prerequisites — Linux:** Docker is auto-installed via `apt-get` when missing (Ubuntu / Debian / Raspbian, amd64 / arm64). No manual setup required.

**Prerequisites — macOS:**

- **Docker Desktop** is required for driver and ML worker containers. The installer pre-flights `docker info` and aborts with a copy-pasteable hint (`brew install --cask docker` if Homebrew is present, otherwise the [Docker Desktop install URL](https://docs.docker.com/desktop/install/mac-install/)) if it is missing or the daemon is not running. Open Docker Desktop once so the daemon starts, then re-run `cyberwave edge install`.
- **Rust toolchain** is needed only to compile the USB/IP host server. If `cargo` is missing the installer offers to run the official `rustup` one-liner non-interactively (`-y --profile minimal`); pass `-y` to `cyberwave edge install` to accept without prompting. `~/.cargo/bin` is added to the in-process `PATH` so the build proceeds in the same run.
- **Xcode command-line tools** (`git`) are still expected; the installer prints `xcode-select --install` if they are missing.

On **macOS**, the installer also sets up an MJPEG camera stream bridge (using `ffmpeg` and AVFoundation) and prompts you to select which camera to use. The selected camera is stored by **device name** (not index) so it persists across USB reconnections and reboots. With multiple camera-bearing twins and multiple AVFoundation devices, the installer walks you through a per-twin mapping and launches one `ffmpeg` MJPEG service per distinct camera (sequential ports starting at `8091`). Assignments are persisted in `~/.cyberwave/camera_streams.json` under `twin_to_stream_url` and honored by edge-core when it wires each driver container.

For **microphone** twins, the installer sets up a PCM audio stream bridge on the same pattern: `ffmpeg` captures from AVFoundation on the host and serves raw PCM over HTTP (ports starting at `8101`). Assignments are persisted in `~/.cyberwave/audio_streams.json` and injected as `CYBERWAVE_METADATA_AUDIO_DEVICE` when edge-core starts each generic-microphone driver container.

On **Linux** with multiple camera-bearing twins, the installer walks you through a per-twin mapping so each twin is bound to a specific `/dev/video*` device. The mapping is saved to the backend as `Edge.metadata.cameras` and mirrored locally in `~/.cyberwave/edge.json`. Edge-core reads camera config from `edge.json` at startup; `~/.cyberwave/cameras.json` is kept as a legacy fallback but is deprecated.

A twin that already pins its camera through `metadata.serial_number` is **not** prompted for: the installer matches that serial against the serials reported by the attached cameras and binds the twin automatically. Twins without a serial are asked as before, and the camera you choose is written back to the twin as its `serial_number` — so the prompt appears once per camera instead of on every install, and the binding survives a replug or a reboot that renumbers `/dev/video*`. If a pinned serial matches none of the attached cameras, the installer warns and asks rather than binding a different unit, because the driver refuses to substitute hardware for a pinned twin.

Some cameras have no serial to pin — the Logitech C920, for instance, reports an empty USB serial descriptor, so `metadata.serial_number` can never identify it. For those, set `metadata.video_device` to a udev stable name instead and the twin is skipped in the same way:

```
/dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920-video-index0      # follows the camera across ports
/dev/v4l/by-path/platform-...-usb-0:1.3:1.0-video-index0     # follows the port, not the camera
```

Pick `by-id` when the model is unique on the host, `by-path` when you may attach a second identical camera and care which port it is in. An `rtsp://` or `http://` URL in the same field marks the twin as a network camera, which is likewise never asked about and consumes no local device. A positional value (`0`, `/dev/video6`) is **not** identity — it means "whatever enumerated there this boot" — so those twins are still prompted. `serial_number` wins when a twin carries both.

```bash
sudo cyberwave edge install
sudo cyberwave edge install -y   # skip prompts
sudo cyberwave edge install -e acme/envs/production-floor   # pin the environment
sudo cyberwave edge install --token cw_XXXX -e acme/envs/production-floor -y   # scripted
sudo cyberwave edge install --reconfigure-camera   # re-select camera without full reinstall
sudo cyberwave edge install --reconfigure-microphone   # re-select microphone without full reinstall
```

**Options:**

- `-e`, `--environment`: Environment to attach this device to, as a UUID or full slug. Skips the environment prompt. Must already exist in the token's workspace.
- `--token`: API token to authenticate with. Overrides stored credentials, `CYBERWAVE_API_KEY`, and the interactive login. Has no short form.
- `--reconfigure-camera`: Re-run the camera selection prompt and restart the camera stream and edge-core service. Useful when switching between cameras (e.g. laptop webcam to external USB camera).
- `--reconfigure-microphone`: Re-run the microphone selection prompt and restart the audio stream bridge and edge-core service. Useful when switching microphones or fixing a broken PCM bridge.
- `--force-reinstall`: Force reinstall of the USB/IP server on macOS. Camera stream and edge-core setup are always forced during install.
- `--channel {stable,dev,staging,pr}`: Which build of `cyberwave-edge-core` to install. Defaults to `stable`. `pr` installs an unreviewed build of a single pull request and **requires `--version`** — PR builds are not ordered against each other, so there is no latest one to pick; copy the version from the pull request's build comment. It is meant for testing a change on hardware before it merges, not for running a device.
- `--version`: Exact package version to install instead of the channel's latest. Required with `--channel pr`.
- `-y`: Skip interactive confirmation prompts.

### `cyberwave edge uninstall`

Stops the systemd service, removes the unit file, and optionally uninstalls the package.

Uninstalling also releases the twins this device was paired to, so they stop
appearing under it in **Workspace settings → Edges** and are free to pair
elsewhere. Twins in other workspaces are never touched.

```bash
sudo cyberwave edge uninstall
sudo cyberwave edge uninstall --channel dev   # keep Docker images/containers
```

**Options:**

- `--channel {stable,dev,staging,pr}`: When set to `dev`, `staging` or `pr`, Docker images and containers are preserved so that a subsequent reinstall does not need to re-download them. This speeds up QA and development cycles and avoids unnecessary wear on SD cards.
- `-y`: Skip interactive confirmation prompts.

### `cyberwave edge start / stop / restart`

```bash
cyberwave edge start                        # background
cyberwave edge start -f                     # foreground

cyberwave edge stop

sudo cyberwave edge restart                 # systemd
cyberwave edge restart                      # process mode fallback
```

On **macOS**, `cyberwave edge restart` also self-heals the camera bridge: after reloading the edge-core LaunchAgent it inspects every installed `com.cyberwave.camera-stream*` service and runs `launchctl kickstart -k` on any whose MJPEG port is silent (e.g. ffmpeg crashed, the camera was unplugged and replugged, or launchd's spawn-throttle disabled the slot). Healthy slots are left alone so a working stream isn't briefly interrupted. If `~/.cyberwave/camera_streams.json` still references a port no installed slot serves — typical after adding a camera-bearing twin without re-running camera selection — a one-line warning per orphan twin points at `sudo cyberwave edge install --reconfigure-camera`.

The same restart path self-heals the microphone PCM bridge (`com.cyberwave.audio-stream*`, ports from `8101`) and warns when `~/.cyberwave/audio_streams.json` references a port with no installed slot — recover with `sudo cyberwave edge install --reconfigure-microphone`.

### `cyberwave edge status`

Checks whether the edge node process is running.

```bash
cyberwave edge status
```

### `cyberwave edge pull`

Pulls edge configuration from the backend using the discovery API (or legacy twin/environment lookup).

```bash
cyberwave edge pull                              # auto-discover via fingerprint
cyberwave edge pull --twin-uuid <UUID>           # single twin (legacy)
cyberwave edge pull --environment-uuid <UUID>    # all twins in environment (legacy)
cyberwave edge pull -d ./my-edge                 # custom output directory
```

### `cyberwave edge whoami`

Displays the unique hardware fingerprint for this device, used to identify the edge when connecting to twins.

```bash
cyberwave edge whoami
```

### `cyberwave edge health`

Queries real-time health status via MQTT (stream states, FPS, WebRTC connections).

```bash
cyberwave edge health -t <TWIN_UUID>
cyberwave edge health -t <TWIN_UUID> --watch     # continuous
cyberwave edge health -t <TWIN_UUID> --timeout 10
```

### `cyberwave edge remote-status`

Checks the last heartbeat stored in twin metadata to determine online/offline status without MQTT.

```bash
cyberwave edge remote-status -t <TWIN_UUID>
```

### `cyberwave edge logs`

```bash
cyberwave edge logs              # last 50 lines
cyberwave edge logs -n 100       # last 100 lines
cyberwave edge logs -f           # follow (tail -f)
```

### `cyberwave edge install-deps`

Installs common ML runtimes needed by edge plugins.

```bash
cyberwave edge install-deps                       # ultralytics + opencv
cyberwave edge install-deps -r onnx -r tflite     # specific runtimes
```

### `cyberwave edge sync-workflows / list-models`

```bash
cyberwave edge sync-workflows --twin-uuid <UUID>  # re-sync model bindings
cyberwave edge list-models --twin-uuid <UUID>      # show loaded models
```

### `cyberwave edge driver`

Manage edge driver containers.

| Subcommand | Description                                              |
| ---------- | -------------------------------------------------------- |
| `list`     | List running driver containers (`--all` includes exited) |
| `start`    | Start a stopped driver container |
| `stop`     | Stop a running driver container  |

```bash
cyberwave edge driver list           # running containers only
cyberwave edge driver list --all     # include exited containers

cyberwave edge driver start                             # interactive picker (stopped containers)
cyberwave edge driver start cyberwave-driver-624d7fe2   # directly by name

cyberwave edge driver stop                              # interactive picker (running containers)
cyberwave edge driver stop cyberwave-driver-624d7fe2    # directly by name
```

`start` restarts an existing stopped container and sets a `--restart=on-failure` Docker policy, so the driver automatically retries if it exits due to a transient error (e.g. robot not yet reachable on the network). To launch a brand-new driver, use the edge-core service which manages image selection and environment configuration.

`stop` disables the Docker restart policy before stopping, so the container does not come back automatically. If the driver is managed by a systemd service, stop that instead:

```bash
sudo systemctl stop cyberwave-video-grabber.service
```

### `cyberwave edge bench`

Micro-benchmark the Zenoh SDK hot paths (header pack, sample decode, stats
accounting, sequence numbering) and compare every metric against a per-device
baseline shipped with the CLI. Prints a device fingerprint, a results table
with `Baseline ops/s` / `Delta` / `Status` columns, and a pass/fail report
card. Exits with code `2` when any metric regresses beyond the threshold, so
CI on a reference device can gate merges.

```bash
# Default run (~30s on a modern laptop): 100k rounds, 2k warmup, 3 repeats,
# 15% regression threshold, auto-selected baseline.
cyberwave edge bench

# Tighter threshold + capture the full run result for later diffing.
cyberwave edge bench --threshold 0.10 --output /tmp/bench.json

# Pin to CPU 0 on Linux for quieter numbers.
cyberwave edge bench --pin

# Skip baseline comparison (just print raw numbers).
cyberwave edge bench --no-compare
```

**How the "device" is resolved**

The command auto-detects a `device_class` slug (e.g. `jetson-orin-nano`,
`rpi-5`, `x86-laptop`, `apple-silicon-m4`) from `/proc/device-tree/model`,
`/etc/nv_tegra_release`, `sysctl -n machdep.cpu.brand_string` (macOS),
`platform.machine()`, and (on Linux x86_64) whether a battery is present.
On Apple Silicon the chip generation is parsed from the brand string
(`"Apple M1"`, `"Apple M1 Pro"`, `"Apple M4 Max"`, …) and emitted as
`apple-silicon-m1`, `apple-silicon-m2`, `apple-silicon-m3`,
`apple-silicon-m4`; Pro/Max/Ultra variants share a tier.

It then loads `bench_baselines/{device_class}.json` from the CLI package,
falling back through parent slugs and finally `generic-{arch}.json`. The
chain is built by stripping trailing `-segment` suffixes, so
`apple-silicon-m4` tries `apple-silicon-m4.json` → `apple-silicon.json` →
`generic-arm64.json`. Missing files are silently skipped, so a newer
generation without a dedicated file still gets a sensible comparison. Use
`--baseline <path>` to override.

**Blessing a baseline for a new device class**

Ship-level baselines live inside the CLI package. To replace a provisional
baseline with real numbers captured on reference hardware:

```bash
# On the reference device (stable load, active cooling / MAXN power mode):
cyberwave edge bench \
    --rounds 500000 --warmup 20000 --repeat 5 --pin \
    --save-baseline ./jetson-orin-nano.json

# Then move the file into the package and commit.
```

The saved file uses the same schema the bench consumes. Set
`"provisional": false` once you are happy with the numbers. The file name
must match the `device_class` slug the fingerprint reports (e.g.
`apple-silicon-m4.json` on an M4 / M4 Pro / M4 Max MacBook). On macOS the
`--pin` flag is a no-op; plug in power, disable Low Power Mode, and close
background apps before blessing to avoid thermal throttling.

**Options**

| Flag | Default | Purpose |
| ---- | ------- | ------- |
| `-n, --rounds` | `100000` | Iterations per timed pass. |
| `--warmup` | `2000` | Un-timed warmup iterations before timing. |
| `--repeat` | `3` | Timed passes per benchmark; the median is reported. |
| `--threshold` | `0.15` | Regression threshold (fraction). |
| `--baseline PATH` | - | Override the auto-selected baseline file. |
| `--save-baseline PATH` | - | Write this run's metrics as a baseline file. |
| `--output PATH` | - | Write the full run result (fingerprint + metrics + deltas) as JSON. |
| `--pin` | `false` | Pin the bench to CPU 0 (Linux). |
| `--no-compare` | `false` | Skip baseline lookup and comparison. |

**Exit codes**: `0` when every metric is within threshold (or no baseline is
available), `2` when one or more metrics regress.

<!-- End of edge bench -->

## `cyberwave compute`

Manage the cloud node service — a GPU-powered companion that runs ML workloads in the cloud.

| Subcommand  | Description                                              |
| ----------- | -------------------------------------------------------- |
| `install`   | Install cyberwave-cloud-node and register a boot service |
| `uninstall` | Stop and remove the boot service                         |
| `start`     | Start the cloud node                                     |
| `stop`      | Stop the cloud node                                      |
| `restart`   | Restart the cloud node                                   |
| `status`    | Check if the cloud node is running                       |
| `logs`      | Show cloud node logs                                     |

```bash
sudo cyberwave compute install
cyberwave compute start --slug my-gpu-node --profile gpu-a100
cyberwave compute status
cyberwave compute logs -f
cyberwave compute stop
```

## `cyberwave model`

Manage local ML model bindings on edge nodes. For most use cases, configure models via UI Workflows instead; these commands are for local/offline configuration.

| Subcommand | Description                           |
| ---------- | ------------------------------------- |
| `list`     | List available edge-compatible models |
| `bind`     | Configure a model for local inference |
| `show`     | Show current model configuration      |
| `remove`   | Remove a model binding                |

```bash
cyberwave model list
cyberwave model bind --model yolov8n --twin-uuid <UUID>
cyberwave model show
cyberwave model remove yolov8n
```

## `cyberwave plugin`

Manage edge node plugins (advanced — most users should use `cyberwave model` or UI Workflows).

| Subcommand  | Description           |
| ----------- | --------------------- |
| `list`      | List available plugins |
| `info`      | Show plugin details    |
| `install`   | Install/enable a plugin |
| `uninstall` | Uninstall a plugin     |

```bash
cyberwave plugin list
cyberwave plugin info yolo
cyberwave plugin install yolo
cyberwave plugin uninstall yolo
```

## `cyberwave environment`

Browse and inspect environments.

| Subcommand | Description                         |
| ---------- | ----------------------------------- |
| `list`     | List environments (table or `--json`) |
| `show`     | Show environment details and twins  |

```bash
cyberwave environment list
cyberwave environment list --json
cyberwave environment show <UUID>
```

## `cyberwave scan`

Discover IP cameras and NVRs on the local network using TCP port scanning, ONVIF WS-Discovery, and UPnP/SSDP.

```bash
cyberwave scan                        # auto-detect subnet
cyberwave scan -s 10.0.0              # specific subnet
cyberwave scan --json                 # JSON output
cyberwave scan --no-ports             # discovery protocols only
cyberwave scan -t 2.0                 # increase timeout
```

## `cyberwave camera`

Bootstrap a camera edge project — creates an environment, twin, and edge config for an IP camera.

```bash
cyberwave camera
cyberwave camera -u "rtsp://192.168.1.100:554/stream"
```

## `cyberwave so101`

Bootstrap a new SO-101 robot arm project. Clones the starter template and runs setup scripts.

```bash
cyberwave so101                       # default directory (./so101-project)
cyberwave so101 ~/projects/my-robot   # custom path
```

## `cyberwave manifest`

Validate `cyberwave.yml` manifest files.

```bash
cyberwave manifest validate                          # default: ./cyberwave.yml
cyberwave manifest validate path/to/cyberwave.yml
cyberwave manifest validate --lenient                # unknown fields as warnings
```

## `cyberwave configure`

Set API credentials directly without the interactive login flow. Useful when you already have a token from the dashboard.

```bash
cyberwave configure --token YOUR_TOKEN
cyberwave configure --token YOUR_TOKEN --base-url http://localhost:8000
cyberwave configure --show
```

## Configuration

Configuration is stored in a single directory shared by the CLI and the edge-core service. The directory is resolved as follows:

1. **`CYBERWAVE_EDGE_CONFIG_DIR`** env var — explicit override
2. **`~/.cyberwave`** — per-user directory (owner-only permissions `0700`)

Legacy installs that used `/etc/cyberwave` are automatically migrated on first CLI invocation.

Run `cyberwave config-dir` to see which directory is active.

**Files inside the config directory:**

- `credentials.json` — API token, workspace info, and runtime env overrides (permissions `600`)
- `environment.json` — selected workspace, environment, and twin bindings
- `fingerprint.json` — unique edge device identifier
- `edge.json` — cached edge device record from the backend, including `metadata.cameras` (device list and twin-to-device mapping); written during install and refreshed by edge-core
- `cameras.json` — **deprecated**; legacy camera config retained for backward compatibility with older edge-core builds

Other environment variables:

- `CYBERWAVE_BASE_URL`: Override the API URL (default: `https://api.cyberwave.com`)
- `CYBERWAVE_ENVIRONMENT`: Environment name (for example `dev`, defaults to `production`)
- `CYBERWAVE_MQTT_HOST`: MQTT broker host (for example `dev.mqtt.cyberwave.com` for dev; defaults to `mqtt.cyberwave.com`)
- `CYBERWAVE_MQTT_TOPIC_PREFIX`: Override the MQTT topic prefix (rarely needed; the CLI derives it from `CYBERWAVE_ENVIRONMENT`/credentials)

When credentials are written, the CLI also persists these `CYBERWAVE_*` values into
`credentials.json` so `cyberwave-edge-core` can reuse them in service mode.

### MQTT topic prefix safety check

Commands that publish over MQTT (for example `cyberwave workflow sync`) need to
hit the same topic prefix `cyberwave-edge-core` is subscribed to — `dev` for
the dev broker, `staging` for staging, empty for production. The CLI derives
that prefix automatically from `CYBERWAVE_ENVIRONMENT`/credentials and
cross-checks it against the broker host.

If you log in against `https://api-dev.cyberwave.com` but your saved
`CYBERWAVE_ENVIRONMENT` says `production`, the CLI will refuse to publish:

```
Error: MQTT topic prefix doesn't match the broker host.

  broker host:       dev.mqtt.cyberwave.com
  inferred env:      dev
  expected prefix:   'dev'
  resolved prefix:   '' (production)
```

Fix it by re-running `cyberwave login` (or `cyberwave configure`) so the
saved environment matches the broker, or set `CYBERWAVE_ENVIRONMENT=dev` in
your shell. As a last-resort override use `CYBERWAVE_MQTT_TOPIC_PREFIX`.

## Building for Distribution

### PyInstaller (standalone binary)

```bash
pip install -e ".[build]"
pyinstaller --onefile --name cyberwave cyberwave_cli/main.py
```

### Debian Package

See `debian/` directory for packaging scripts.

## Contributing

Contributions are welcome. Please open an issue for bugs or feature requests, and submit a pull request for improvements.

## Support

- **Documentation**: [docs.cyberwave.com](https://docs.cyberwave.com)
- **Issues**: [GitHub Issues](https://github.com/cyberwave-os/cyberwave-cli/issues)
- **Community**: [Discord](https://discord.gg/dfGhNrawyF)
