chore(naming): remove Iris codename
parent
790079c3b6
commit
d182f7e4b2
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
## 2.0.0-beta1 — 2025-12-14
|
## 2.0.0-beta1 — 2025-12-14
|
||||||
|
|
||||||
First Clawdis release post rebrand. This is a semver-major because we dropped legacy providers/agents and moved defaults to new paths while adding a full macOS companion app, a WebSocket Gateway, and an iOS node (Iris).
|
First Clawdis release post rebrand. This is a semver-major because we dropped legacy providers/agents and moved defaults to new paths while adding a full macOS companion app, a WebSocket Gateway, and an iOS node.
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
- Renamed to **Clawdis**: defaults now live under `~/.clawdis` (sessions in `~/.clawdis/sessions/`, IPC at `~/.clawdis/clawdis.sock`, logs in `/tmp/clawdis`). Launchd labels and config filenames follow the new name; legacy stores are copied forward on first run.
|
- Renamed to **Clawdis**: defaults now live under `~/.clawdis` (sessions in `~/.clawdis/sessions/`, IPC at `~/.clawdis/clawdis.sock`, logs in `/tmp/clawdis`). Launchd labels and config filenames follow the new name; legacy stores are copied forward on first run.
|
||||||
|
|
@ -19,7 +19,7 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
|
||||||
### Gateway, nodes, and automation
|
### Gateway, nodes, and automation
|
||||||
- New typed Gateway WS protocol (JSON schema validated) with `clawdis gateway {health,status,send,agent,call}` helpers and structured presence/instance updates for all clients.
|
- New typed Gateway WS protocol (JSON schema validated) with `clawdis gateway {health,status,send,agent,call}` helpers and structured presence/instance updates for all clients.
|
||||||
- Optional LAN-facing bridge (`tcp://0.0.0.0:18790`) keeps the Gateway loopback-only while enabling direct Bonjour-discovered connections for paired nodes.
|
- Optional LAN-facing bridge (`tcp://0.0.0.0:18790`) keeps the Gateway loopback-only while enabling direct Bonjour-discovered connections for paired nodes.
|
||||||
- Node pairing + management via `clawdis nodes {pending,approve,reject,invoke}` (used by the iOS node “Iris” and future remote nodes).
|
- Node pairing + management via `clawdis nodes {pending,approve,reject,invoke}` (used by the iOS node and future remote nodes).
|
||||||
- Cron jobs are Gateway-owned (`clawdis cron …`) with run history stored as JSONL and support for “isolated summary” posting into the main session.
|
- Cron jobs are Gateway-owned (`clawdis cron …`) with run history stored as JSONL and support for “isolated summary” posting into the main session.
|
||||||
|
|
||||||
### macOS companion app
|
### macOS companion app
|
||||||
|
|
@ -29,10 +29,10 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
|
||||||
- **Browser control**: manage clawd’s dedicated Chrome/Chromium with tab listing/open/focus/close, screenshots, DOM query/dump, and “AI snapshots” (aria/domSnapshot/ai) via `clawdis browser …` and UI controls.
|
- **Browser control**: manage clawd’s dedicated Chrome/Chromium with tab listing/open/focus/close, screenshots, DOM query/dump, and “AI snapshots” (aria/domSnapshot/ai) via `clawdis browser …` and UI controls.
|
||||||
- **Remote gateway control**: Bonjour discovery for local masters plus SSH-tunnel fallback for remote control when multicast is unavailable.
|
- **Remote gateway control**: Bonjour discovery for local masters plus SSH-tunnel fallback for remote control when multicast is unavailable.
|
||||||
|
|
||||||
### iOS node (Iris)
|
### iOS node
|
||||||
- New iOS companion app that pairs to the Gateway bridge, reports presence as a node, and exposes a WKWebView “Canvas” for agent-driven UI.
|
- New iOS companion app that pairs to the Gateway bridge, reports presence as a node, and exposes a WKWebView “Canvas” for agent-driven UI.
|
||||||
- `clawdis nodes invoke` supports `canvas.eval` and `canvas.snapshot` to drive and verify the iOS Canvas (fails fast when Iris is backgrounded).
|
- `clawdis nodes invoke` supports `canvas.eval` and `canvas.snapshot` to drive and verify the iOS Canvas (fails fast when the iOS node is backgrounded).
|
||||||
- Voice wake words are configurable in-app; Iris reconnects to the last bridge when credentials are still present in Keychain.
|
- Voice wake words are configurable in-app; the iOS node reconnects to the last bridge when credentials are still present in Keychain.
|
||||||
|
|
||||||
### WhatsApp & agent experience
|
### WhatsApp & agent experience
|
||||||
- Group chats fully supported: mention-gated triggers (including media-only captions), sender attribution, session primer with subject/member roster, allowlist bypass when you’re @‑mentioned, and safer handling of view-once/ephemeral media.
|
- Group chats fully supported: mention-gated triggers (including media-only captions), sender attribution, session primer with subject/member roster, allowlist bypass when you’re @‑mentioned, and safer handling of view-once/ephemeral media.
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -30,7 +30,7 @@ WhatsApp / Telegram
|
||||||
├─ CLI (clawdis …)
|
├─ CLI (clawdis …)
|
||||||
├─ WebChat (loopback UI)
|
├─ WebChat (loopback UI)
|
||||||
├─ macOS app (Clawdis.app)
|
├─ macOS app (Clawdis.app)
|
||||||
└─ iOS node (Iris) via Bridge + pairing
|
└─ iOS node via Bridge + pairing
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why "CLAWDIS"?
|
## Why "CLAWDIS"?
|
||||||
|
|
@ -53,7 +53,7 @@ Because every space lobster needs a time-and-space machine. The Doctor has a TAR
|
||||||
- 🎤 **Voice & transcription hooks** — Voice Wake (macOS/iOS) + optional transcription pipeline
|
- 🎤 **Voice & transcription hooks** — Voice Wake (macOS/iOS) + optional transcription pipeline
|
||||||
- 🔧 **Tool Streaming** — Real-time display (💻📄✍️📝)
|
- 🔧 **Tool Streaming** — Real-time display (💻📄✍️📝)
|
||||||
- 🖥️ **macOS Companion (Clawdis.app)** — Menu bar controls, Voice Wake, WebChat, onboarding, remote gateway control
|
- 🖥️ **macOS Companion (Clawdis.app)** — Menu bar controls, Voice Wake, WebChat, onboarding, remote gateway control
|
||||||
- 📱 **iOS Node (Iris)** — Pairs as a node, exposes a Canvas surface, forwards voice wake transcripts
|
- 📱 **iOS node** — Pairs as a node, exposes a Canvas surface, forwards voice wake transcripts
|
||||||
|
|
||||||
Only the Pi CLI is supported now; legacy Claude/Codex/Gemini paths have been removed.
|
Only the Pi CLI is supported now; legacy Claude/Codex/Gemini paths have been removed.
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ Only the Pi CLI is supported now; legacy Claude/Codex/Gemini paths have been rem
|
||||||
|
|
||||||
- **TypeScript (ESM)**: CLI + Gateway live in `src/` and run on Node ≥ 22.
|
- **TypeScript (ESM)**: CLI + Gateway live in `src/` and run on Node ≥ 22.
|
||||||
- **macOS app (Swift)**: menu bar companion lives in `apps/macos/`.
|
- **macOS app (Swift)**: menu bar companion lives in `apps/macos/`.
|
||||||
- **iOS app (Swift)**: Iris node prototype lives in `apps/ios/`.
|
- **iOS app (Swift)**: iOS node prototype lives in `apps/ios/`.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -118,9 +118,9 @@ If delivery fails (e.g. WhatsApp disconnected / Telegram token missing), Clawdis
|
||||||
|
|
||||||
Build/run the mac app with `./scripts/restart-mac.sh` (packages, installs, and launches), or `swift build --package-path apps/macos && open dist/Clawdis.app`.
|
Build/run the mac app with `./scripts/restart-mac.sh` (packages, installs, and launches), or `swift build --package-path apps/macos && open dist/Clawdis.app`.
|
||||||
|
|
||||||
### iOS Node (Iris) (internal)
|
### iOS node (internal)
|
||||||
|
|
||||||
Iris is an internal/prototype iOS app that connects as a **remote node**:
|
The iOS node app is an internal/prototype app that connects as a **remote node**:
|
||||||
|
|
||||||
- **Voice trigger:** forwards transcripts into the Gateway (agent runs + wakeups).
|
- **Voice trigger:** forwards transcripts into the Gateway (agent runs + wakeups).
|
||||||
- **Canvas screen:** a WKWebView + `<canvas>` surface the agent can control (via `canvas.eval` / `canvas.snapshot` over `node.invoke`).
|
- **Canvas screen:** a WKWebView + `<canvas>` surface the agent can control (via `canvas.eval` / `canvas.snapshot` over `node.invoke`).
|
||||||
|
|
@ -164,7 +164,7 @@ Optional: enable/configure clawd’s dedicated browser control (defaults are alr
|
||||||
- [Troubleshooting](./docs/troubleshooting.md)
|
- [Troubleshooting](./docs/troubleshooting.md)
|
||||||
- [The Lore](./docs/lore.md) 🦞
|
- [The Lore](./docs/lore.md) 🦞
|
||||||
- [Telegram (Bot API)](./docs/telegram.md)
|
- [Telegram (Bot API)](./docs/telegram.md)
|
||||||
- [iOS node runbook (Iris)](./docs/ios/connect.md)
|
- [iOS node runbook](./docs/ios/connect.md)
|
||||||
- [macOS app spec](./docs/clawdis-mac.md)
|
- [macOS app spec](./docs/clawdis-mac.md)
|
||||||
|
|
||||||
## Clawd
|
## Clawd
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
## Clawdis Node (Android) (internal)
|
## Clawdis Node (Android) (internal)
|
||||||
|
|
||||||
Modern Android “node” app (Iris parity): connects to the **Gateway-owned bridge** (`_clawdis-bridge._tcp`) over TCP and exposes **Canvas + Chat + Camera**.
|
Modern Android node app: connects to the **Gateway-owned bridge** (`_clawdis-bridge._tcp`) over TCP and exposes **Canvas + Chat + Camera**.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- The node keeps the connection alive via a **foreground service** (persistent notification with a Disconnect action).
|
- The node keeps the connection alive via a **foreground service** (persistent notification with a Disconnect action).
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import Testing
|
||||||
@Suite struct BridgeEndpointIDTests {
|
@Suite struct BridgeEndpointIDTests {
|
||||||
@Test func stableIDForServiceDecodesAndNormalizesName() {
|
@Test func stableIDForServiceDecodesAndNormalizesName() {
|
||||||
let endpoint = NWEndpoint.service(
|
let endpoint = NWEndpoint.service(
|
||||||
name: "Clawdis\\032Bridge \\032 Iris\n",
|
name: "Clawdis\\032Bridge \\032 Node\n",
|
||||||
type: "_clawdis-bridge._tcp",
|
type: "_clawdis-bridge._tcp",
|
||||||
domain: "local.",
|
domain: "local.",
|
||||||
interface: nil)
|
interface: nil)
|
||||||
|
|
||||||
#expect(BridgeEndpointID.stableID(endpoint) == "_clawdis-bridge._tcp|local.|Clawdis Bridge Iris")
|
#expect(BridgeEndpointID.stableID(endpoint) == "_clawdis-bridge._tcp|local.|Clawdis Bridge Node")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func stableIDForNonServiceUsesEndpointDescription() {
|
@Test func stableIDForNonServiceUsesEndpointDescription() {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import Testing
|
||||||
nodes: [
|
nodes: [
|
||||||
ControlRequestHandler.GatewayNodeListPayload.Node(
|
ControlRequestHandler.GatewayNodeListPayload.Node(
|
||||||
nodeId: "n1",
|
nodeId: "n1",
|
||||||
displayName: "Iris",
|
displayName: "Node",
|
||||||
platform: "iOS",
|
platform: "iOS",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
deviceFamily: "iPad",
|
deviceFamily: "iPad",
|
||||||
|
|
@ -36,10 +36,10 @@ import Testing
|
||||||
#expect(res.pairedNodeIds.sorted() == ["n1", "n2"])
|
#expect(res.pairedNodeIds.sorted() == ["n1", "n2"])
|
||||||
#expect(res.connectedNodeIds == ["n1"])
|
#expect(res.connectedNodeIds == ["n1"])
|
||||||
|
|
||||||
let iris = res.nodes.first { $0.nodeId == "n1" }
|
let node = res.nodes.first { $0.nodeId == "n1" }
|
||||||
#expect(iris?.remoteAddress == "192.168.0.88")
|
#expect(node?.remoteAddress == "192.168.0.88")
|
||||||
#expect(iris?.deviceFamily == "iPad")
|
#expect(node?.deviceFamily == "iPad")
|
||||||
#expect(iris?.modelIdentifier == "iPad14,5")
|
#expect(node?.modelIdentifier == "iPad14,5")
|
||||||
#expect(iris?.capabilities?.sorted() == ["camera", "canvas"])
|
#expect(node?.capabilities?.sorted() == ["camera", "canvas"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Clawdis uses Bonjour (mDNS / DNS-SD) as a **LAN-only convenience** to discover a
|
||||||
|
|
||||||
## Wide-Area Bonjour (Unicast DNS-SD) over Tailscale
|
## Wide-Area Bonjour (Unicast DNS-SD) over Tailscale
|
||||||
|
|
||||||
If you want Iris/iPad auto-discovery while the Gateway is on another network (e.g. Vienna ⇄ London), you can keep the `NWBrowser` UX but switch discovery from multicast mDNS (`local.`) to **unicast DNS-SD** (“Wide-Area Bonjour”) over Tailscale.
|
If you want iOS node auto-discovery while the Gateway is on another network (e.g. Vienna ⇄ London), you can keep the `NWBrowser` UX but switch discovery from multicast mDNS (`local.`) to **unicast DNS-SD** (“Wide-Area Bonjour”) over Tailscale.
|
||||||
|
|
||||||
High level:
|
High level:
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ In the Tailscale admin console:
|
||||||
- Add a nameserver pointing at the gateway’s tailnet IP (UDP/TCP 53).
|
- Add a nameserver pointing at the gateway’s tailnet IP (UDP/TCP 53).
|
||||||
- Add split DNS so the domain `clawdis.internal` uses that nameserver.
|
- Add split DNS so the domain `clawdis.internal` uses that nameserver.
|
||||||
|
|
||||||
Once clients accept tailnet DNS, Iris can browse `_clawdis-bridge._tcp` in `clawdis.internal.` without multicast.
|
Once clients accept tailnet DNS, iOS nodes can browse `_clawdis-bridge._tcp` in `clawdis.internal.` without multicast.
|
||||||
|
|
||||||
### Bridge listener security (recommended)
|
### Bridge listener security (recommended)
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ Only the **Node Gateway** (`clawd` / `clawdis gateway`) advertises Bonjour beaco
|
||||||
## Service types
|
## Service types
|
||||||
|
|
||||||
- `_clawdis-master._tcp` — “master gateway” discovery beacon (primarily for macOS remote-control UX).
|
- `_clawdis-master._tcp` — “master gateway” discovery beacon (primarily for macOS remote-control UX).
|
||||||
- `_clawdis-bridge._tcp` — bridge transport beacon (used by Iris/iOS nodes).
|
- `_clawdis-bridge._tcp` — bridge transport beacon (used by iOS/Android nodes).
|
||||||
|
|
||||||
## TXT keys (non-secret hints)
|
## TXT keys (non-secret hints)
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ The Gateway advertises small non-secret hints to make UI flows convenient:
|
||||||
- `sshPort=<port>` (defaults to 22 when not overridden)
|
- `sshPort=<port>` (defaults to 22 when not overridden)
|
||||||
- `gatewayPort=<port>` (informational; the Gateway WS is typically loopback-only)
|
- `gatewayPort=<port>` (informational; the Gateway WS is typically loopback-only)
|
||||||
- `bridgePort=<port>` (only when bridge is enabled)
|
- `bridgePort=<port>` (only when bridge is enabled)
|
||||||
- `canvasPort=<port>` (only when the optional canvas host is enabled; default `18793`)
|
- `canvasPort=<port>` (only when the canvas host is running; enabled by default; default `18793`)
|
||||||
- `tailnetDns=<magicdns>` (optional hint; may be absent)
|
- `tailnetDns=<magicdns>` (optional hint; may be absent)
|
||||||
|
|
||||||
## Debugging on macOS
|
## Debugging on macOS
|
||||||
|
|
@ -119,9 +119,9 @@ Look for `bonjour:` lines, especially:
|
||||||
- `bonjour: ... name conflict resolved` / `hostname conflict resolved`
|
- `bonjour: ... name conflict resolved` / `hostname conflict resolved`
|
||||||
- `bonjour: watchdog detected non-announced service; attempting re-advertise ...` (self-heal attempt after sleep/interface churn)
|
- `bonjour: watchdog detected non-announced service; attempting re-advertise ...` (self-heal attempt after sleep/interface churn)
|
||||||
|
|
||||||
## Debugging on iOS (Iris)
|
## Debugging on iOS node
|
||||||
|
|
||||||
Iris discovers bridges via `NWBrowser` browsing `_clawdis-bridge._tcp`.
|
The iOS node app discovers bridges via `NWBrowser` browsing `_clawdis-bridge._tcp`.
|
||||||
|
|
||||||
To capture what the browser is doing:
|
To capture what the browser is doing:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,9 +178,9 @@ When enabled, the server:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `bridge` (Iris/node bridge server)
|
### `bridge` (node bridge server)
|
||||||
|
|
||||||
The Gateway can expose a simple TCP bridge for nodes (iOS/Android “Iris”), typically on port `18790`.
|
The Gateway can expose a simple TCP bridge for nodes (iOS/Android), typically on port `18790`.
|
||||||
|
|
||||||
Defaults:
|
Defaults:
|
||||||
- enabled: `true`
|
- enabled: `true`
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ read_when:
|
||||||
Clawdis has two distinct problems that look similar on the surface:
|
Clawdis has two distinct problems that look similar on the surface:
|
||||||
|
|
||||||
1) **Operator remote control**: the macOS menu bar app controlling a “master” gateway running elsewhere.
|
1) **Operator remote control**: the macOS menu bar app controlling a “master” gateway running elsewhere.
|
||||||
2) **Node pairing**: Iris/iOS (and future nodes) finding a gateway and pairing securely.
|
2) **Node pairing**: iOS/Android (and future nodes) finding a gateway and pairing securely.
|
||||||
|
|
||||||
The design goal is to keep all network discovery/advertising in the **Node Gateway** (`clawd` / `clawdis gateway`) and keep clients (mac app, iOS) as consumers.
|
The design goal is to keep all network discovery/advertising in the **Node Gateway** (`clawd` / `clawdis gateway`) and keep clients (mac app, iOS) as consumers.
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ Troubleshooting and beacon details: `docs/bonjour.md`.
|
||||||
- `sshPort=22` (or whatever is advertised)
|
- `sshPort=22` (or whatever is advertised)
|
||||||
- `gatewayPort=18789` (loopback WS port; informational)
|
- `gatewayPort=18789` (loopback WS port; informational)
|
||||||
- `bridgePort=18790` (when bridge is enabled)
|
- `bridgePort=18790` (when bridge is enabled)
|
||||||
- `canvasPort=18793` (when the optional canvas host is enabled)
|
- `canvasPort=18793` (when the canvas host is running; enabled by default)
|
||||||
- `tailnetDns=<magicdns>` (optional hint)
|
- `tailnetDns=<magicdns>` (optional hint)
|
||||||
|
|
||||||
Disable/override:
|
Disable/override:
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,15 @@ WhatsApp / Telegram
|
||||||
▼
|
▼
|
||||||
┌──────────────────────────┐
|
┌──────────────────────────┐
|
||||||
│ Gateway │ ws://127.0.0.1:18789 (loopback-only)
|
│ Gateway │ ws://127.0.0.1:18789 (loopback-only)
|
||||||
│ (single source) │ tcp://0.0.0.0:18790 (optional Bridge)
|
│ (single source) │ tcp://0.0.0.0:18790 (Bridge)
|
||||||
│ │ http://0.0.0.0:18793 (optional Canvas host)
|
│ │ http://0.0.0.0:18793 (Canvas host)
|
||||||
└───────────┬───────────────┘
|
└───────────┬───────────────┘
|
||||||
│
|
│
|
||||||
├─ Pi agent (RPC)
|
├─ Pi agent (RPC)
|
||||||
├─ CLI (clawdis …)
|
├─ CLI (clawdis …)
|
||||||
├─ Chat UI (SwiftUI)
|
├─ Chat UI (SwiftUI)
|
||||||
├─ macOS app (Clawdis.app)
|
├─ macOS app (Clawdis.app)
|
||||||
└─ iOS node (Iris) via Bridge + pairing
|
└─ iOS node via Bridge + pairing
|
||||||
```
|
```
|
||||||
|
|
||||||
Most operations flow through the **Gateway** (`clawdis gateway`), a single long-running process that owns provider connections and the WebSocket control plane.
|
Most operations flow through the **Gateway** (`clawdis gateway`), a single long-running process that owns provider connections and the WebSocket control plane.
|
||||||
|
|
@ -52,7 +52,7 @@ Most operations flow through the **Gateway** (`clawdis gateway`), a single long-
|
||||||
- **One Gateway per host**: it is the only process allowed to own the WhatsApp Web session.
|
- **One Gateway per host**: it is the only process allowed to own the WhatsApp Web session.
|
||||||
- **Loopback-first**: Gateway WS is `ws://127.0.0.1:18789` (not exposed on the LAN).
|
- **Loopback-first**: Gateway WS is `ws://127.0.0.1:18789` (not exposed on the LAN).
|
||||||
- **Bridge for nodes**: optional LAN/tailnet-facing bridge on `tcp://0.0.0.0:18790` for paired nodes (Bonjour-discoverable).
|
- **Bridge for nodes**: optional LAN/tailnet-facing bridge on `tcp://0.0.0.0:18790` for paired nodes (Bonjour-discoverable).
|
||||||
- **Canvas host (optional)**: LAN/tailnet HTTP file server (default `18793`) for node WebViews; see `docs/configuration.md` (`canvasHost`).
|
- **Canvas host**: LAN/tailnet HTTP file server (default `18793`) for node WebViews; see `docs/configuration.md` (`canvasHost`).
|
||||||
- **Remote use**: SSH tunnel or tailnet/VPN; see `docs/remote.md` and `docs/discovery.md`.
|
- **Remote use**: SSH tunnel or tailnet/VPN; see `docs/remote.md` and `docs/discovery.md`.
|
||||||
|
|
||||||
## Features (high level)
|
## Features (high level)
|
||||||
|
|
@ -65,7 +65,7 @@ Most operations flow through the **Gateway** (`clawdis gateway`), a single long-
|
||||||
- 📎 **Media Support** — Send and receive images, audio, documents
|
- 📎 **Media Support** — Send and receive images, audio, documents
|
||||||
- 🎤 **Voice notes** — Optional transcription hook
|
- 🎤 **Voice notes** — Optional transcription hook
|
||||||
- 🖥️ **WebChat + macOS app** — Local UI + menu bar companion for ops and voice wake
|
- 🖥️ **WebChat + macOS app** — Local UI + menu bar companion for ops and voice wake
|
||||||
- 📱 **iOS node (Iris)** — Pairs as a node and exposes a Canvas surface
|
- 📱 **iOS node** — Pairs as a node and exposes a Canvas surface
|
||||||
|
|
||||||
Note: legacy Claude/Codex/Gemini/Opencode paths have been removed; Pi is the only coding-agent path.
|
Note: legacy Claude/Codex/Gemini/Opencode paths have been removed; Pi is the only coding-agent path.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ read_when:
|
||||||
This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a host (e.g., your Mac Studio) and connecting clients to it.
|
This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a host (e.g., your Mac Studio) and connecting clients to it.
|
||||||
|
|
||||||
- For **operators (you / the macOS app)**: SSH tunneling is the universal fallback.
|
- For **operators (you / the macOS app)**: SSH tunneling is the universal fallback.
|
||||||
- For **nodes (Iris/iOS and future devices)**: prefer the Gateway **Bridge** when on the same LAN/tailnet (see `docs/discovery.md`).
|
- For **nodes (iOS/Android and future devices)**: prefer the Gateway **Bridge** when on the same LAN/tailnet (see `docs/discovery.md`).
|
||||||
|
|
||||||
## The core idea
|
## The core idea
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ Who receives it:
|
||||||
- Uses the global list to gate `VoiceWakeRuntime` triggers.
|
- Uses the global list to gate `VoiceWakeRuntime` triggers.
|
||||||
- Editing “Trigger words” in Voice Wake settings calls `voicewake.set` and then relies on the broadcast to keep other clients in sync.
|
- Editing “Trigger words” in Voice Wake settings calls `voicewake.set` and then relies on the broadcast to keep other clients in sync.
|
||||||
|
|
||||||
### iOS node (Iris)
|
### iOS node
|
||||||
|
|
||||||
- Uses the global list for `VoiceWakeManager` trigger detection.
|
- Uses the global list for `VoiceWakeManager` trigger detection.
|
||||||
- Editing Wake Words in Settings calls `voicewake.set` (over the bridge) and also keeps local wake-word detection responsive.
|
- Editing Wake Words in Settings calls `voicewake.set` (over the bridge) and also keeps local wake-word detection responsive.
|
||||||
|
|
@ -59,4 +59,3 @@ Who receives it:
|
||||||
|
|
||||||
- Exposes a Wake Words editor in Settings.
|
- Exposes a Wake Words editor in Settings.
|
||||||
- Calls `voicewake.set` over the bridge so edits sync everywhere.
|
- Calls `voicewake.set` over the bridge so edits sync everywhere.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export type BridgeConfig = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
port?: number;
|
port?: number;
|
||||||
/**
|
/**
|
||||||
* Bind address policy for the Iris bridge server.
|
* Bind address policy for the node bridge server.
|
||||||
* - auto: prefer tailnet IP when present, else LAN (0.0.0.0)
|
* - auto: prefer tailnet IP when present, else LAN (0.0.0.0)
|
||||||
* - lan: 0.0.0.0 (reachable on local network + any forwarded interfaces)
|
* - lan: 0.0.0.0 (reachable on local network + any forwarded interfaces)
|
||||||
* - tailnet: bind only to the Tailscale interface IP (100.64.0.0/10)
|
* - tailnet: bind only to the Tailscale interface IP (100.64.0.0/10)
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,7 @@ describe("gateway server", () => {
|
||||||
type: "req",
|
type: "req",
|
||||||
id: "pair-req-1",
|
id: "pair-req-1",
|
||||||
method: "node.pair.request",
|
method: "node.pair.request",
|
||||||
params: { nodeId: "n1", displayName: "Iris" },
|
params: { nodeId: "n1", displayName: "Node" },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const res1 = await onceMessage<{
|
const res1 = await onceMessage<{
|
||||||
|
|
@ -432,7 +432,7 @@ describe("gateway server", () => {
|
||||||
type: "req",
|
type: "req",
|
||||||
id: "pair-req-2",
|
id: "pair-req-2",
|
||||||
method: "node.pair.request",
|
method: "node.pair.request",
|
||||||
params: { nodeId: "n1", displayName: "Iris" },
|
params: { nodeId: "n1", displayName: "Node" },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const res2 = await onceMessage<{
|
const res2 = await onceMessage<{
|
||||||
|
|
@ -827,7 +827,7 @@ describe("gateway server", () => {
|
||||||
(p) =>
|
(p) =>
|
||||||
typeof p === "object" &&
|
typeof p === "object" &&
|
||||||
p !== null &&
|
p !== null &&
|
||||||
(p as { instanceId?: unknown }).instanceId === "iris-1" &&
|
(p as { instanceId?: unknown }).instanceId === "node-1" &&
|
||||||
(p as { reason?: unknown }).reason === reason,
|
(p as { reason?: unknown }).reason === reason,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -835,20 +835,20 @@ describe("gateway server", () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const presenceConnectedP = waitPresenceReason("iris-connected");
|
const presenceConnectedP = waitPresenceReason("node-connected");
|
||||||
await bridgeCall?.onAuthenticated?.({
|
await bridgeCall?.onAuthenticated?.({
|
||||||
nodeId: "iris-1",
|
nodeId: "node-1",
|
||||||
displayName: "Iris",
|
displayName: "Node",
|
||||||
platform: "ios",
|
platform: "ios",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
remoteIp: "10.0.0.10",
|
remoteIp: "10.0.0.10",
|
||||||
});
|
});
|
||||||
await presenceConnectedP;
|
await presenceConnectedP;
|
||||||
|
|
||||||
const presenceDisconnectedP = waitPresenceReason("iris-disconnected");
|
const presenceDisconnectedP = waitPresenceReason("node-disconnected");
|
||||||
await bridgeCall?.onDisconnected?.({
|
await bridgeCall?.onDisconnected?.({
|
||||||
nodeId: "iris-1",
|
nodeId: "node-1",
|
||||||
displayName: "Iris",
|
displayName: "Node",
|
||||||
platform: "ios",
|
platform: "ios",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
remoteIp: "10.0.0.10",
|
remoteIp: "10.0.0.10",
|
||||||
|
|
|
||||||
|
|
@ -1263,7 +1263,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
thinking: p.thinking,
|
thinking: p.thinking,
|
||||||
deliver: p.deliver,
|
deliver: p.deliver,
|
||||||
timeout: Math.ceil(timeoutMs / 1000).toString(),
|
timeout: Math.ceil(timeoutMs / 1000).toString(),
|
||||||
surface: `Iris(${nodeId})`,
|
surface: `Node(${nodeId})`,
|
||||||
abortSignal: abortController.signal,
|
abortSignal: abortController.signal,
|
||||||
},
|
},
|
||||||
defaultRuntime,
|
defaultRuntime,
|
||||||
|
|
@ -1367,7 +1367,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
sessionId,
|
sessionId,
|
||||||
thinking: "low",
|
thinking: "low",
|
||||||
deliver: false,
|
deliver: false,
|
||||||
surface: "Iris",
|
surface: "Node",
|
||||||
},
|
},
|
||||||
defaultRuntime,
|
defaultRuntime,
|
||||||
deps,
|
deps,
|
||||||
|
|
@ -1442,7 +1442,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
typeof link?.timeoutSeconds === "number"
|
typeof link?.timeoutSeconds === "number"
|
||||||
? link.timeoutSeconds.toString()
|
? link.timeoutSeconds.toString()
|
||||||
: undefined,
|
: undefined,
|
||||||
surface: "Iris",
|
surface: "Node",
|
||||||
},
|
},
|
||||||
defaultRuntime,
|
defaultRuntime,
|
||||||
deps,
|
deps,
|
||||||
|
|
@ -1508,7 +1508,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
const platform = node.platform?.trim() || undefined;
|
const platform = node.platform?.trim() || undefined;
|
||||||
const deviceFamily = node.deviceFamily?.trim() || undefined;
|
const deviceFamily = node.deviceFamily?.trim() || undefined;
|
||||||
const modelIdentifier = node.modelIdentifier?.trim() || undefined;
|
const modelIdentifier = node.modelIdentifier?.trim() || undefined;
|
||||||
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason iris-connected`;
|
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason node-connected`;
|
||||||
upsertPresence(node.nodeId, {
|
upsertPresence(node.nodeId, {
|
||||||
host,
|
host,
|
||||||
ip,
|
ip,
|
||||||
|
|
@ -1517,7 +1517,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
deviceFamily,
|
deviceFamily,
|
||||||
modelIdentifier,
|
modelIdentifier,
|
||||||
mode: "remote",
|
mode: "remote",
|
||||||
reason: "iris-connected",
|
reason: "node-connected",
|
||||||
lastInputSeconds: 0,
|
lastInputSeconds: 0,
|
||||||
instanceId: node.nodeId,
|
instanceId: node.nodeId,
|
||||||
text,
|
text,
|
||||||
|
|
@ -1554,7 +1554,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
const platform = node.platform?.trim() || undefined;
|
const platform = node.platform?.trim() || undefined;
|
||||||
const deviceFamily = node.deviceFamily?.trim() || undefined;
|
const deviceFamily = node.deviceFamily?.trim() || undefined;
|
||||||
const modelIdentifier = node.modelIdentifier?.trim() || undefined;
|
const modelIdentifier = node.modelIdentifier?.trim() || undefined;
|
||||||
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason iris-disconnected`;
|
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason node-disconnected`;
|
||||||
upsertPresence(node.nodeId, {
|
upsertPresence(node.nodeId, {
|
||||||
host,
|
host,
|
||||||
ip,
|
ip,
|
||||||
|
|
@ -1563,7 +1563,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
deviceFamily,
|
deviceFamily,
|
||||||
modelIdentifier,
|
modelIdentifier,
|
||||||
mode: "remote",
|
mode: "remote",
|
||||||
reason: "iris-disconnected",
|
reason: "node-disconnected",
|
||||||
lastInputSeconds: 0,
|
lastInputSeconds: 0,
|
||||||
instanceId: node.nodeId,
|
instanceId: node.nodeId,
|
||||||
text,
|
text,
|
||||||
|
|
@ -1589,7 +1589,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||||
if (started.port > 0) {
|
if (started.port > 0) {
|
||||||
bridge = started;
|
bridge = started;
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
`bridge listening on tcp://${bridgeHost}:${bridge.port} (Iris)`,
|
`bridge listening on tcp://${bridgeHost}:${bridge.port} (node)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ export async function startGatewayBonjourAdvertiser(
|
||||||
svc: master as unknown as BonjourService,
|
svc: master as unknown as BonjourService,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optional bridge beacon (same type used by Iris/iOS today).
|
// Optional bridge beacon (same type used by iOS/Android nodes today).
|
||||||
if (typeof opts.bridgePort === "number" && opts.bridgePort > 0) {
|
if (typeof opts.bridgePort === "number" && opts.bridgePort > 0) {
|
||||||
const bridge = responder.createService({
|
const bridge = responder.createService({
|
||||||
name: safeServiceName(instanceName),
|
name: safeServiceName(instanceName),
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ describe("node bridge server", () => {
|
||||||
sendLine(socket, {
|
sendLine(socket, {
|
||||||
type: "pair-request",
|
type: "pair-request",
|
||||||
nodeId: "n4",
|
nodeId: "n4",
|
||||||
displayName: "Iris",
|
displayName: "Node",
|
||||||
platform: "ios",
|
platform: "ios",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
deviceFamily: "iPad",
|
deviceFamily: "iPad",
|
||||||
|
|
@ -315,7 +315,7 @@ describe("node bridge server", () => {
|
||||||
|
|
||||||
expect(lastAuthed?.nodeId).toBe("n4");
|
expect(lastAuthed?.nodeId).toBe("n4");
|
||||||
// Prefer paired metadata over hello payload (token verifies the stored node record).
|
// Prefer paired metadata over hello payload (token verifies the stored node record).
|
||||||
expect(lastAuthed?.displayName).toBe("Iris");
|
expect(lastAuthed?.displayName).toBe("Node");
|
||||||
expect(lastAuthed?.platform).toBe("ios");
|
expect(lastAuthed?.platform).toBe("ios");
|
||||||
expect(lastAuthed?.version).toBe("1.0");
|
expect(lastAuthed?.version).toBe("1.0");
|
||||||
expect(lastAuthed?.deviceFamily).toBe("iPad");
|
expect(lastAuthed?.deviceFamily).toBe("iPad");
|
||||||
|
|
@ -425,7 +425,7 @@ describe("node bridge server", () => {
|
||||||
sendLine(socket, {
|
sendLine(socket, {
|
||||||
type: "pair-request",
|
type: "pair-request",
|
||||||
nodeId: "n-caps",
|
nodeId: "n-caps",
|
||||||
displayName: "Iris",
|
displayName: "Node",
|
||||||
platform: "ios",
|
platform: "ios",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
deviceFamily: "iPad",
|
deviceFamily: "iPad",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue