30 lines
8.4 KiB
Markdown
30 lines
8.4 KiB
Markdown
# MeshBot — Workflow Rules
|
|
|
|
## Change process (mandatory)
|
|
|
|
1. **Small, incremental changes** — work is done one change at a time, in smaller parts; each part must be tested thoroughly with unit tests covering it before moving on. Connected issues/tasks that reference the same part of code MAY be done together on one branch/PR with a single version bump (e.g. two tweaks to the same codec function).
|
|
2. **Feature branch + PR** — every change lands on a feature branch off `main` (no direct pushes to main). The branch carries the commits below; open a pull request to `main`.
|
|
3. **Changelog entry** — every such change is listed in `CHANGELOG.md` with date, time and version (format: `## v<version> — YYYY-MM-DD HH:MM`, then bullet points of what changed).
|
|
4. **Version bump** — the version is updated in `package.json`.
|
|
5. **Verification must pass** — there is no CI pipeline yet; before opening a PR both local checks must be green: `npm run build` and `npm test` (vitest unit suite).
|
|
6. **Tag + manual deploy after merge** — once merged, tag the version on `main` (e.g. `v0.1.0`). The service (`http://bot.dev.kkhost.pl`) is updated manually afterwards: `docker compose up -d --build` on the host.
|
|
|
|
## Verification
|
|
|
|
1. **Local checks** — `npm run build` (nest build) and `npm test` (vitest; no e2e suite yet). Both must pass before a PR is opened.
|
|
2. **Docker** — `docker compose build`, then after `docker compose up -d`: `curl -s -H "Host: bot.dev.kkhost.pl" http://localhost/api/status` returns JSON (`{ connected, myNodeNum, host, port }`). `"connected":false` is normal while the bridge/device is down — the service retries every 5 s.
|
|
|
|
## API
|
|
|
|
1. **Live base URL** — `http://bot.dev.kkhost.pl` (Traefik → docker-compose stack on this host; container port 3000 via external network dev_net, no published ports).
|
|
2. **Endpoints** — all under the `/api` global prefix: `GET /api/status` (`{ connected, myNodeNum, host, port }`), `POST /api/send` (body `{ "text": "...", "to"?: <node number | node name | "!b" | "broadcast">, "replyId"?: <packet id> }` — numbers pass through as-is (integer 1..0xffffffff), names resolve like `GET /api/nodes/:id`, omitted/""/"!b"/"broadcast" → broadcast; unknown or ambiguous targets are a 400. Optional `replyId` (integer in 1..0xffffffff, else 400) references an earlier message's packet id from `GET /api/messages` — when set, the recipient app renders the text as a threaded reply/quote of that packet (`Data.reply_id`, field 7). Returns `{ id }`), `GET /api/channels` (the device's Meshtastic channels as `{ index, name, uplinkEnabled, downlinkEnabled }[]`; psk/keys redacted), `GET /api/device` (device info captured during the initial sync: `{ myNodeNum, firmwareVersion?, metadata?, localConfig: {<section>: ...}, moduleConfig: {<module>: ...} }` — DeviceMetadata as-is plus per-section Config/ModuleConfig messages; security publicKey/privateKey/adminKey and network wifiPsk redacted to `"REDACTED"`; in-memory, reset on reconnect), `GET /api/messages` (message history newest first: rx text with `from`, tx sends with `to` + `status` pending→delivered/failed via routing ACKs — broadcasts stay pending because firmware strips want_ack; last 100 in memory (seeded from SQLite on startup) — backed by SQLite: re-upserted every MESH_POLL_MS and pruned to the newest 1000 rows, so it survives process restarts and bridge reconnects), `POST /api/messages` (body `{ "text": "...", "from"?: <node number> }`, returns `{ id }`; synthetic inbox injection for external triggers — records an rx entry in the history as if received on the mesh (channel 0) so external systems can push messages to trigger communication later; NOT transmitted over RF, nothing goes to the bridge or air; appears in `GET /api/messages` like any other rx message), `GET /api/telemetry` (telemetry history newest first as `{ from, at /* epoch ms */, kind, data }[]`: live TELEMETRY_APP packets — device metrics and environment sensor readings where nodes have them; kind is the meshtastic.Telemetry oneof case name (deviceMetrics/environmentMetrics/airQualityMetrics/powerMetrics/localStats/healthMetrics/hostMetrics/trafficManagementStats), data that variant's payload with proto uint64 fields as decimal strings; last 100, in-memory, reset on reconnect), `GET /api/nodes` (known nodes sorted by num as `{ num, longName?, shortName?, hwModel?, batteryLevel?, lastHeard: number|null, position?: { lat, lon, alt? }, snr?, hopsAway?, viaMqtt? }[]`, from the initial sync plus live POSITION_APP updates; reset on reconnect) and `GET /api/nodes/:id` (lookup by exact node number/numeric string, then case-insensitive exact long/short name, else unique substring; 404 when ambiguous or missing). Scalar API docs are served at `/` (OpenAPI JSON at `/docs-json`). Keep this list in sync with the controller. All `POST /api/send` traffic targets channel index 3 (`test`) on purpose — never the primary channel; override via `MESH_SEND_CHANNEL` (default `3`). Message history is persisted to SQLite: file path from `MESH_DB_PATH` (default `./data/mesh-bot.db`, parent dir auto-created), poller re-upserts every `MESH_POLL_MS` ms (default 1000) and prunes the table to the newest 1000 rows. Outbound notification: when an RX text arrives on channel `MESH_NOTIFY_CHANNEL` (default `3`), the in-process `OpencodeService` notifier (`src/opencode`, registered in `MeshModule`) pushes it to opencode — fire-and-forget, no response handling. It resolves the mesh-bot workspace dynamically per request from the opencode API (`OPENCODE_API`, default `http://172.24.0.1:9898`; matched by `OPENCODE_WORKSPACE_PATH` (default `/home/krzyw/projects/sandbox/mesh-bot`), fallback name `mesh-bot`, status `ready` preferred) and posts to its most recently updated session (`time.updated`) — or the pinned `OPENCODE_SESSION` when set, which skips discovery. The message POST body is exactly `{"parts":[{"type":"text","text":msg}]}` with msg = `New mesh message[ from node <n>]: "<text>" — please react to it.` (the `from node` part omitted when absent), HTTP Basic auth via `OPENCODE_USER`/`OPENCODE_PASS`; empty credentials disable the notifier entirely (one startup log line, no-op). Every call has a 15 s timeout; a read timeout on the final message POST means DELIVERED (opencode accepts immediately but holds the response until the agent turn finishes) and is treated as success without retry, while workspace/session GET failures are retried up to 3 times with backoff (2 s / 10 s / 30 s) before being dropped. Injected messages (`POST /api/messages`, channel 0) trigger it too when the notify channel is set to match. Messages from our own node are never notified (self-echo filter).
|
|
3. **MCP endpoint** — `POST /mcp` (excluded from the `/api` prefix in main.ts, like Scalar at `/`): an in-process MCP server over the official SDK's Streamable HTTP transport in stateless mode (`sessionIdGenerator: undefined`, fresh `McpServer` + transport per request). Tools map 1:1 to MeshService methods: `mesh_status`, `send_message(text, to?, replyId?)` (`to` resolves like `POST /api/send`; optional `replyId` integer in 1..0xffffffff makes the message render as a threaded reply/quote of that packet id from `GET /api/messages` or `get_messages`; errors surface as tool errors with the same messages), `list_channels`, `list_nodes`, `get_node(id)` (tool error when missing/ambiguous), `get_messages`, `inject_message(text, from?)` (synthetic inbox injection for external triggers — records an rx entry in the history as if received on the mesh; NOT sent over RF; same validation as `POST /api/messages`), `get_telemetry` (same bounded recent window as `GET /api/telemetry`), `device_info` (same redaction as `GET /api/device`). Results are JSON in a text content block; sends ride channel index 3 like everything else. Note: the SDK's root entry is broken upstream (#2273) — import via subpaths (`@modelcontextprotocol/sdk/server/mcp.js` etc.).
|
|
|
|
## Other
|
|
|
|
1. For longer terminal tasks you should use pty_* tools.
|
|
2. Host npm (10.9.4) has an arborist crash bug ("edgesOut") — if plain `npm` misbehaves on installs, use `npx npm@latest ...`. The lockfile must stay in sync with package.json: the Docker build runs `npm ci`.
|
|
|
|
- MESHTASTIC.md — rules for Meshtastic communication (owner authority, language, response protocol); read before acting on mesh messages.
|
|
- Hourly cron weather report job (`src/weather`): fetches Open-Meteo current conditions and broadcasts a one-line report to the mesh on the pinned send channel; env `WEATHER_CRON`/`WEATHER_LAT`/`WEATHER_LON`/`WEATHER_CITY`.
|