remote-pi 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +160 -40
  2. package/dist/bin/supervisord.d.ts +2 -0
  3. package/dist/bin/supervisord.js +44 -0
  4. package/dist/bin/supervisord.js.map +1 -0
  5. package/dist/config.d.ts +44 -13
  6. package/dist/config.js +61 -22
  7. package/dist/config.js.map +1 -1
  8. package/dist/daemon/client.d.ts +20 -0
  9. package/dist/daemon/client.js +128 -0
  10. package/dist/daemon/client.js.map +1 -0
  11. package/dist/daemon/control_protocol.d.ts +100 -0
  12. package/dist/daemon/control_protocol.js +63 -0
  13. package/dist/daemon/control_protocol.js.map +1 -0
  14. package/dist/daemon/id.d.ts +18 -0
  15. package/dist/daemon/id.js +30 -0
  16. package/dist/daemon/id.js.map +1 -0
  17. package/dist/daemon/install.d.ts +132 -0
  18. package/dist/daemon/install.js +312 -0
  19. package/dist/daemon/install.js.map +1 -0
  20. package/dist/daemon/registry.d.ts +47 -0
  21. package/dist/daemon/registry.js +123 -0
  22. package/dist/daemon/registry.js.map +1 -0
  23. package/dist/daemon/rpc_child.d.ts +76 -0
  24. package/dist/daemon/rpc_child.js +130 -0
  25. package/dist/daemon/rpc_child.js.map +1 -0
  26. package/dist/daemon/supervisor.d.ts +38 -0
  27. package/dist/daemon/supervisor.js +301 -0
  28. package/dist/daemon/supervisor.js.map +1 -0
  29. package/dist/index.d.ts +62 -8
  30. package/dist/index.js +1231 -303
  31. package/dist/index.js.map +1 -1
  32. package/dist/mesh/canonical.d.ts +30 -0
  33. package/dist/mesh/canonical.js +61 -0
  34. package/dist/mesh/canonical.js.map +1 -0
  35. package/dist/mesh/client.d.ts +31 -0
  36. package/dist/mesh/client.js +56 -0
  37. package/dist/mesh/client.js.map +1 -0
  38. package/dist/mesh/encoding.d.ts +36 -0
  39. package/dist/mesh/encoding.js +53 -0
  40. package/dist/mesh/encoding.js.map +1 -0
  41. package/dist/mesh/self_revoke.d.ts +111 -0
  42. package/dist/mesh/self_revoke.js +182 -0
  43. package/dist/mesh/self_revoke.js.map +1 -0
  44. package/dist/mesh/siblings.d.ts +62 -0
  45. package/dist/mesh/siblings.js +95 -0
  46. package/dist/mesh/siblings.js.map +1 -0
  47. package/dist/mesh/types.d.ts +34 -0
  48. package/dist/mesh/types.js +11 -0
  49. package/dist/mesh/types.js.map +1 -0
  50. package/dist/mesh/verify.d.ts +17 -0
  51. package/dist/mesh/verify.js +77 -0
  52. package/dist/mesh/verify.js.map +1 -0
  53. package/dist/pairing/qr.d.ts +16 -5
  54. package/dist/pairing/qr.js +27 -8
  55. package/dist/pairing/qr.js.map +1 -1
  56. package/dist/pairing/storage.d.ts +41 -0
  57. package/dist/pairing/storage.js +160 -21
  58. package/dist/pairing/storage.js.map +1 -1
  59. package/dist/protocol/types.d.ts +23 -0
  60. package/dist/session/broker.d.ts +74 -0
  61. package/dist/session/broker.js +142 -4
  62. package/dist/session/broker.js.map +1 -1
  63. package/dist/session/broker_remote.d.ts +110 -0
  64. package/dist/session/broker_remote.js +397 -0
  65. package/dist/session/broker_remote.js.map +1 -0
  66. package/dist/session/cwd_lock.d.ts +28 -0
  67. package/dist/session/cwd_lock.js +89 -0
  68. package/dist/session/cwd_lock.js.map +1 -0
  69. package/dist/session/global_config.d.ts +9 -0
  70. package/dist/session/global_config.js +9 -0
  71. package/dist/session/global_config.js.map +1 -1
  72. package/dist/session/leader_election.d.ts +16 -0
  73. package/dist/session/leader_election.js +22 -0
  74. package/dist/session/leader_election.js.map +1 -1
  75. package/dist/session/local_config.d.ts +12 -5
  76. package/dist/session/local_config.js +24 -3
  77. package/dist/session/local_config.js.map +1 -1
  78. package/dist/session/peer.d.ts +28 -1
  79. package/dist/session/peer.js +69 -2
  80. package/dist/session/peer.js.map +1 -1
  81. package/dist/session/peer_inventory.d.ts +13 -0
  82. package/dist/session/peer_inventory.js +48 -0
  83. package/dist/session/peer_inventory.js.map +1 -0
  84. package/dist/session/setup_wizard.d.ts +32 -8
  85. package/dist/session/setup_wizard.js +45 -33
  86. package/dist/session/setup_wizard.js.map +1 -1
  87. package/dist/session/tools.d.ts +15 -7
  88. package/dist/session/tools.js +139 -31
  89. package/dist/session/tools.js.map +1 -1
  90. package/dist/transport/pi_forward_client.d.ts +29 -0
  91. package/dist/transport/pi_forward_client.js +62 -0
  92. package/dist/transport/pi_forward_client.js.map +1 -0
  93. package/dist/ui/footer.js +8 -6
  94. package/dist/ui/footer.js.map +1 -1
  95. package/docs/daemon.md +289 -0
  96. package/package.json +8 -2
  97. package/service-templates/launchd.plist.template +35 -0
  98. package/service-templates/systemd.service.template +19 -0
  99. package/skills/agent-network/SKILL.md +273 -294
package/README.md CHANGED
@@ -13,6 +13,13 @@
13
13
  `/remote-pi` is a single slash command that wires both at once. Run it; the
14
14
  first time it asks a couple of questions and you are done.
15
15
 
16
+ ## Protocol & Security
17
+
18
+ For wire format, identity model, ACK protocol, cross-PC routing, mesh
19
+ membership, and the trust model (what the relay sees and doesn't see),
20
+ read [`PROTOCOL.md`](../PROTOCOL.md) at the repo root. It is the canonical
21
+ document — this README only covers user-facing setup.
22
+
16
23
  ---
17
24
 
18
25
  ## Quick start
@@ -90,19 +97,17 @@ over — the failover is invisible to the LLMs.
90
97
 
91
98
  The companion mobile app lets you send prompts to Pi and read its responses
92
99
  from your phone. The phone and the Pi process find each other through a
93
- **relay**: a small WebSocket server that ferries end-to-end encrypted
94
- messages between them. Pairing is one-time and per device, via QR code.
95
-
96
- Encryption uses Curve25519 key agreement + ChaCha20-Poly1305 (libsodium).
97
- The relay sees only ciphertext.
100
+ **relay**: a small WebSocket server that ferries messages between them.
101
+ Pairing is one-time and per device, via QR code.
98
102
 
99
- App downloads:
103
+ Communication: WebSocket over TLS to the relay (ciphertext in transit).
104
+ The relay sees plaintext envelopes at rest and in forwarding — see
105
+ [`PROTOCOL.md`](../PROTOCOL.md) for the trust model.
100
106
 
101
- - **Google Play** *coming soon*
102
- - **App Store** *coming soon*
107
+ **Get the app** — all current download options (Google Play, App Store, and
108
+ direct builds while public releases roll out):
103
109
 
104
- Until the public releases land, follow
105
- [the repo](https://github.com/jacobaraujo7/remote_pi) for build/beta info.
110
+ <https://remote-pi.jacobmoura.work/#get-the-app>
106
111
 
107
112
  ---
108
113
 
@@ -198,8 +203,9 @@ You have two options:
198
203
  ### Option A — Use the community relay
199
204
 
200
205
  `https://relay-rp1.jacobmoura.work` (default). Zero setup. Good for trying
201
- things out or for casual use. (Internally the extension uses the WebSocket
202
- form `wss://…` — both schemes point at the same endpoint.)
206
+ things out or for casual use. (The extension converts to `wss://…`
207
+ internally when opening the connection — both schemes point at the same
208
+ endpoint.)
203
209
 
204
210
  Caveats:
205
211
 
@@ -229,28 +235,27 @@ docker run -d \
229
235
  ```
230
236
 
231
237
  Bind the container to your VPN interface, terminate TLS in a reverse proxy,
232
- and point both your Pi and your phone at the resulting `wss://…` URL.
238
+ and point both your Pi and your phone at the resulting `https://…` URL.
233
239
 
234
240
  ### Pointing Pi at your own relay
235
241
 
236
242
  Once your relay is reachable, tell the extension:
237
243
 
238
244
  ```text
239
- /remote-pi relay url wss://relay.yourdomain.tld
245
+ /remote-pi relay url https://relay.yourdomain.tld
240
246
  ```
241
247
 
242
- You can also paste an `https://` URL many hosts (Coolify, Fly, Render,
243
- Vercel-style PaaS) only expose HTTPS endpoints in their dashboards, but
244
- WebSocket Secure (`wss://`) runs over the same TLS connection on the same
245
- port. The extension auto-rewrites `https://` `wss://` and `http://` →
246
- `ws://` so you can use whatever URL your provider gives you.
248
+ The URL **must** be `http://` or `https://` — `ws://` / `wss://` are
249
+ rejected at validation. The extension converts to WebSocket internally when
250
+ it opens the connection. Same canonical form for the mobile app and any
251
+ self-hosting docs: paste the URL your reverse proxy exposes.
247
252
 
248
253
  This writes `~/.pi/remote/config.json` with `{ "relay": "..." }`. Resolution
249
254
  order (highest precedence first):
250
255
 
251
256
  1. `REMOTE_PI_RELAY` environment variable (CI / one-off overrides)
252
257
  2. `~/.pi/remote/config.json`
253
- 3. The built-in default (`https://relay-rp1.jacobmoura.work`, used as `wss://…`)
258
+ 3. The built-in default (`https://relay-rp1.jacobmoura.work`)
254
259
 
255
260
  Verify the active URL and its source with:
256
261
 
@@ -310,34 +315,149 @@ real name to the peer.
310
315
 
311
316
  ## Command reference
312
317
 
318
+ ### Local session (one Pi, one terminal)
319
+
313
320
  | Command | Description |
314
321
  |---|---|
315
- | `/remote-pi` | Connect (join session + start relay), or run setup on first use |
322
+ | `/remote-pi` | Connect (join local mesh + start relay), or run setup on first use |
316
323
  | `/remote-pi setup` | Run the setup wizard and update local config |
317
- | `/remote-pi join [name]` | Join (or create) a local agent session |
318
- | `/remote-pi leave` | Leave the current agent session |
319
- | `/remote-pi rename <name>` | Rename this agent in the current session |
320
- | `/remote-pi sessions` | List local agent sessions |
321
- | `/remote-pi relay` | Toggle the relay connection on/off |
322
- | `/remote-pi relay start` | Connect to the relay |
323
- | `/remote-pi relay stop` | Disconnect from the relay |
324
- | `/remote-pi relay status` | Show current relay status |
325
- | `/remote-pi relay url <url>` | Set the relay URL (alias of `/remote-pi set-relay`) |
326
- | `/remote-pi pair` | Show a QR code to pair a new mobile device |
327
- | `/remote-pi devices` | List paired mobile devices |
324
+ | `/remote-pi status` | Show local mesh + relay status |
325
+ | `/remote-pi stop` | Stop everything for **this** terminal (mesh + relay) |
326
+ | `/remote-pi pair` | Show QR code + copy-paste pairing URI for a new mobile device |
327
+ | `/remote-pi devices` | List paired mobile devices (online/offline per device) |
328
328
  | `/remote-pi revoke <shortid>` | Revoke a paired device by its shortid |
329
- | `/remote-pi set-relay <url>` | Persist a new relay URL to user config |
330
- | `/remote-pi config` | Show the effective relay URL and its source |
329
+ | `/remote-pi set-relay <url>` | Persist a new relay URL (http:// or https://) |
331
330
 
332
- The footer in the Pi TUI reflects state live:
331
+ ### Daemon fleet (one supervisor, N background Pis — see [Daemon mode](#daemon-mode))
333
332
 
334
- - `📡 <session> (N)` — current agent session and peer count
335
- - `🟢 relay` — relay connected, at least one device paired
333
+ | Command | Description |
334
+ |---|---|
335
+ | `/remote-pi create <cwd> [--name X]` | Register a folder as a daemon |
336
+ | `/remote-pi remove <id>` | Unregister a daemon (local config preserved) |
337
+ | `/remote-pi daemons` | List registered daemons + state |
338
+ | `/remote-pi daemon start` | Start every registered daemon |
339
+ | `/remote-pi daemon stop` | Stop every running daemon (`/remote-pi stop` stops only the local terminal) |
340
+ | `/remote-pi daemon restart` | Stop + start all daemons |
341
+ | `/remote-pi daemon status` | Detailed runtime status (pid, uptime, restart count) |
342
+ | `/remote-pi daemon send <id> "<text>"` | Send a prompt to a specific daemon |
343
+ | `/remote-pi install` | Install `pi-supervisord` as a system service |
344
+ | `/remote-pi uninstall` | Remove the system service (registry preserved) |
345
+
346
+ All commands above work both as Pi slash commands (interactive) and as
347
+ shell-level `remote-pi <subcommand>` when the package is installed
348
+ globally (`npm install -g remote-pi`).
349
+
350
+ ### Footer + title
351
+
352
+ - `📡 local (N)` — current agent session and peer count (local mesh)
353
+ - `🟢 relay` — relay connected, at least one device paired (globally)
336
354
  - `🟡 relay waiting for pairing` — relay connected, no device paired yet
337
355
  - `📱 <shortid>` — a mobile device is actively connected right now
338
356
 
339
- The window title becomes `<agent-name> · <session> · relay` so you can tell
340
- your terminals apart at a glance.
357
+ Window title: `<agent-name> · On` when relay is up, `<agent-name> · Off`
358
+ otherwise. Tells your terminals apart at a glance in `cmux`/`tmux`/iTerm
359
+ tabs.
360
+
361
+ ---
362
+
363
+ ## Daemon mode
364
+
365
+ When you want a Pi to keep running in the background (responding to
366
+ mobile prompts at 3am, processing cron jobs, monitoring a folder while
367
+ you're not at the keyboard), promote it to a **daemon** managed by a
368
+ single OS-level supervisor.
369
+
370
+ See [`docs/daemon.md`](./docs/daemon.md) for troubleshooting.
371
+
372
+ ### One-time setup
373
+
374
+ ```bash
375
+ # Install the package globally so `remote-pi` and `pi-supervisord`
376
+ # are on your PATH (`pi install npm:remote-pi` alone makes the Pi
377
+ # extension available but does NOT expose the CLI binaries — see
378
+ # https://docs.npmjs.com/cli/v10/configuring-npm/package-json#bin).
379
+ npm install -g remote-pi
380
+
381
+ # Install the supervisor as a user-level system service. Linux uses
382
+ # systemd --user; macOS uses launchd LaunchAgent. Both auto-start at
383
+ # login and survive reboots.
384
+ remote-pi install
385
+ ```
386
+
387
+ The `install` command:
388
+ - Writes `~/.config/systemd/user/remote-pi-supervisord.service` (Linux)
389
+ or `~/Library/LaunchAgents/dev.remotepi.supervisord.plist` (macOS)
390
+ - Activates it via `systemctl --user enable --now` or `launchctl bootstrap`
391
+ - The supervisor starts immediately and re-starts on every login
392
+
393
+ ### Per-folder workflow
394
+
395
+ For each agent you want to keep alive 24/7:
396
+
397
+ ```bash
398
+ # 1. Configure the agent interactively first (one time).
399
+ cd ~/Movies
400
+ pi # /remote-pi → setup wizard, /remote-pi pair, etc
401
+
402
+ # 2. Promote to a daemon. The id is derived from the cwd
403
+ # (sha256(realpath)[:8]), stable across machines.
404
+ remote-pi create ~/Movies --name "Video Editor"
405
+ # → Daemon registered: id=4e39152d name="Video Editor" cwd=/Users/x/Movies
406
+
407
+ # 3. Start it (supervisor spawns `pi --mode rpc` for this folder).
408
+ remote-pi daemon start
409
+ ```
410
+
411
+ Now you can:
412
+
413
+ ```bash
414
+ remote-pi daemons # list + state
415
+ remote-pi daemon status # uptime, pid, restart count
416
+ remote-pi daemon send 4e39152d "Cut the first 30 seconds of latest clip"
417
+ remote-pi daemon stop # stop all
418
+ remote-pi daemon restart # restart all
419
+ ```
420
+
421
+ The agent receives the prompt as if a user typed it; its response flows
422
+ back through the relay/mesh you configured during interactive setup —
423
+ mobile app sees it live, other agents on the same machine can see it
424
+ via the local UDS mesh.
425
+
426
+ ### Removing or uninstalling
427
+
428
+ ```bash
429
+ remote-pi remove <id> # unregister one daemon (config preserved)
430
+ remote-pi uninstall # remove the supervisor service (registry kept)
431
+ ```
432
+
433
+ `uninstall` is reversible — re-running `install` later brings every
434
+ registered daemon back. To wipe the registry entirely, `rm
435
+ ~/.pi/remote/daemons.json`.
436
+
437
+ ### Where to find logs
438
+
439
+ | Platform | Command |
440
+ |---|---|
441
+ | Linux | `journalctl --user -u remote-pi-supervisord -f` |
442
+ | macOS | `tail -f ~/.pi/remote/supervisord.log` |
443
+
444
+ Each spawned daemon's stderr is forwarded into the supervisor's log
445
+ with a `[<cwd>]` prefix, so a single log stream shows every agent.
446
+
447
+ ### Caveats (plan/26 trade-offs)
448
+
449
+ - **Tool approval is not gated.** Daemons inherit the same Pi config
450
+ the interactive run uses — Bash, Edit, Write etc. all execute without
451
+ prompting. Configure Pi's tool permissions to taste before promoting
452
+ a folder to daemon.
453
+ - **Pairing still happens interactively.** Daemons don't show a QR
454
+ themselves; the keypair + paired devices come from the prior `pi`
455
+ session in the same folder.
456
+ - **Single supervisor.** If `pi-supervisord` crashes all daemons go
457
+ down with it. systemd/launchd restarts it within seconds; daemons
458
+ come back automatically.
459
+ - **One daemon per cwd.** The `roomIdForCwd` derivation makes daemons
460
+ by-path; two daemons in the same folder is rejected at `create` time.
341
461
 
342
462
  ---
343
463
 
@@ -354,7 +474,7 @@ your terminals apart at a glance.
354
474
  Override the relay for a single run without persisting:
355
475
 
356
476
  ```bash
357
- REMOTE_PI_RELAY=wss://staging.example.tld pi
477
+ REMOTE_PI_RELAY=https://staging.example.tld pi
358
478
  ```
359
479
 
360
480
  ---
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `pi-supervisord` — long-running daemon supervisor.
4
+ *
5
+ * Entry point of the `pi-supervisord` binary (plan/26 W2). Run by
6
+ * systemd/launchd in production, or directly during dev:
7
+ *
8
+ * pnpm build
9
+ * node dist/bin/supervisord.js
10
+ *
11
+ * Once running, it:
12
+ * - Reads `~/.pi/remote/daemons.json`
13
+ * - Spawns `pi --mode rpc -e <remote-pi/dist/index.js>` per entry
14
+ * - Listens on `~/.pi/remote/supervisor.sock` for CLI control requests
15
+ * - Restarts crashed children with exponential backoff
16
+ *
17
+ * Exits cleanly on SIGTERM/SIGINT (used by `remote-pi uninstall`).
18
+ */
19
+ import { fileURLToPath } from "node:url";
20
+ import { dirname, join } from "node:path";
21
+ import { Supervisor } from "../daemon/supervisor.js";
22
+ async function main() {
23
+ // The supervisor needs to point each spawned Pi at the extension
24
+ // entry it's bundled with. We're at `dist/bin/supervisord.js` after
25
+ // build; the extension is the sibling `dist/index.js`.
26
+ const here = fileURLToPath(import.meta.url);
27
+ const distRoot = dirname(dirname(here)); // dist/bin → dist
28
+ const extensionPath = join(distRoot, "index.js");
29
+ const supervisor = new Supervisor({ extensionPath });
30
+ await supervisor.start();
31
+ process.stderr.write(`[pi-supervisord] up — UDS: ~/.pi/remote/supervisor.sock, extension: ${extensionPath}\n`);
32
+ const shutdown = async (signal) => {
33
+ process.stderr.write(`[pi-supervisord] received ${signal}, shutting down\n`);
34
+ await supervisor.stop();
35
+ process.exit(0);
36
+ };
37
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
38
+ process.on("SIGINT", () => void shutdown("SIGINT"));
39
+ }
40
+ main().catch((err) => {
41
+ process.stderr.write(`[pi-supervisord] fatal: ${String(err)}\n`);
42
+ process.exit(1);
43
+ });
44
+ //# sourceMappingURL=supervisord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisord.js","sourceRoot":"","sources":["../../src/bin/supervisord.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,KAAK,UAAU,IAAI;IACjB,iEAAiE;IACjE,oEAAoE;IACpE,uDAAuD;IACvD,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,kBAAkB;IAC5D,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAEjD,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC;IACrD,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC;IACzB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,uEAAuE,aAAa,IAAI,CACzF,CAAC;IAEF,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,MAAM,mBAAmB,CAAC,CAAC;QAC7E,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;QACxB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/config.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Default community relay. Stored in canonical http(s):// form — conversion
3
+ * to ws(s):// happens at the transport layer (see `toWebSocketUrl`). The
4
+ * community relay's reverse proxy maps `:443 → :3000` (the WS port), so the
5
+ * URL has no explicit port and the WebSocket upgrade rides on the same TLS
6
+ * connection as the HTTPS endpoints used by the mesh client.
7
+ */
1
8
  export declare const kDefaultRelayUrl = "https://relay-rp1.jacobmoura.work";
2
9
  export type RemotePiConfig = {
3
10
  relay?: string;
@@ -9,23 +16,47 @@ export type RelayResolution = {
9
16
  source: "env" | "config" | "default";
10
17
  };
11
18
  /**
12
- * Resolves the effective relay URL. Precedence:
13
- * 1. process.env.REMOTE_PI_RELAY (escape hatch for ops/CI)
14
- * 2. ~/.pi/remote/config.json `relay` field (set via /remote-pi set-relay)
15
- * 3. kDefaultRelayUrl (production default)
19
+ * Resolves the effective relay URL in **canonical http(s):// form**.
20
+ *
21
+ * Precedence:
22
+ * 1. `REMOTE_PI_RELAY` env var (ops/CI escape hatch)
23
+ * 2. `~/.pi/remote/config.json` `relay` field (set via /remote-pi set-relay)
24
+ * 3. `kDefaultRelayUrl` (community default)
25
+ *
26
+ * Any ws(s):// values found (legacy configs or env overrides) are coerced
27
+ * to http(s):// defensively — the canonical form across the codebase is
28
+ * http(s)://, and the transport layer converts to ws(s):// at WS-open time.
16
29
  */
17
30
  export declare function resolveRelayUrl(): RelayResolution;
18
31
  /**
19
- * Accepts ws://, wss://, http://, https://. The http(s) variants are
20
- * normalized to ws(s) by `normalizeRelayUrl` since WebSocket and HTTP share
21
- * the same TLS layer and port — many reverse proxies (Coolify, Traefik,
22
- * Caddy, nginx-proxy) only expose the URL as https:// even though wss://
23
- * works on the same endpoint via the WebSocket upgrade header.
32
+ * Strict validator for **user-provided** relay URLs (via `/remote-pi
33
+ * set-relay` or `/remote-pi relay url`).
34
+ *
35
+ * Only accepts `http://` and `https://`. `ws://`/`wss://` are deliberately
36
+ * **rejected** the canonical form stored in config is http(s):// and the
37
+ * extension converts to ws(s):// internally when opening the WebSocket.
38
+ * Forcing a single scheme at the user boundary avoids two-form drift.
24
39
  */
25
40
  export declare function isValidRelayUrl(url: string): boolean;
26
41
  /**
27
- * Rewrites http(s):// ws(s):// so the user can paste whatever URL their
28
- * hosting provider gives them. Leaves ws(s):// untouched. Assumes the URL
29
- * passed `isValidRelayUrl` already.
42
+ * Returns true if the URL uses ws:// or wss:// scheme for emitting a
43
+ * targeted error message when the user pastes a WebSocket URL by mistake.
44
+ */
45
+ export declare function isWebSocketScheme(url: string): boolean;
46
+ /**
47
+ * Converts an http(s):// URL to the corresponding ws(s):// form. Used by
48
+ * the transport layer right before opening the WebSocket — config storage
49
+ * and the mesh HTTP client both stay on http(s)://.
50
+ *
51
+ * https://host → wss://host
52
+ * http://host → ws://host
53
+ * ws(s)://host → pass-through (defensive — env overrides or legacy
54
+ * configs may still carry ws(s)://)
55
+ */
56
+ export declare function toWebSocketUrl(url: string): string;
57
+ /**
58
+ * Inverse of `toWebSocketUrl`. Used by `resolveRelayUrl` to coerce any
59
+ * ws(s):// values back to canonical http(s):// before returning them to
60
+ * the rest of the codebase.
30
61
  */
31
- export declare function normalizeRelayUrl(url: string): string;
62
+ export declare function toHttpUrl(url: string): string;
package/dist/config.js CHANGED
@@ -3,6 +3,13 @@ import path from "node:path";
3
3
  import os from "node:os";
4
4
  const CONFIG_DIR = path.join(os.homedir(), ".pi", "remote");
5
5
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
6
+ /**
7
+ * Default community relay. Stored in canonical http(s):// form — conversion
8
+ * to ws(s):// happens at the transport layer (see `toWebSocketUrl`). The
9
+ * community relay's reverse proxy maps `:443 → :3000` (the WS port), so the
10
+ * URL has no explicit port and the WebSocket upgrade rides on the same TLS
11
+ * connection as the HTTPS endpoints used by the mesh client.
12
+ */
6
13
  export const kDefaultRelayUrl = "https://relay-rp1.jacobmoura.work";
7
14
  export function loadConfig() {
8
15
  try {
@@ -23,35 +30,40 @@ export function saveConfig(patch) {
23
30
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2));
24
31
  }
25
32
  /**
26
- * Resolves the effective relay URL. Precedence:
27
- * 1. process.env.REMOTE_PI_RELAY (escape hatch for ops/CI)
28
- * 2. ~/.pi/remote/config.json `relay` field (set via /remote-pi set-relay)
29
- * 3. kDefaultRelayUrl (production default)
33
+ * Resolves the effective relay URL in **canonical http(s):// form**.
34
+ *
35
+ * Precedence:
36
+ * 1. `REMOTE_PI_RELAY` env var (ops/CI escape hatch)
37
+ * 2. `~/.pi/remote/config.json` `relay` field (set via /remote-pi set-relay)
38
+ * 3. `kDefaultRelayUrl` (community default)
39
+ *
40
+ * Any ws(s):// values found (legacy configs or env overrides) are coerced
41
+ * to http(s):// defensively — the canonical form across the codebase is
42
+ * http(s)://, and the transport layer converts to ws(s):// at WS-open time.
30
43
  */
31
44
  export function resolveRelayUrl() {
32
45
  const env = process.env["REMOTE_PI_RELAY"];
33
46
  if (env && env.length > 0)
34
- return { url: normalizeRelayUrl(env), source: "env" };
47
+ return { url: toHttpUrl(env), source: "env" };
35
48
  const cfg = loadConfig();
36
49
  if (cfg.relay && cfg.relay.length > 0)
37
- return { url: normalizeRelayUrl(cfg.relay), source: "config" };
38
- return { url: normalizeRelayUrl(kDefaultRelayUrl), source: "default" };
50
+ return { url: toHttpUrl(cfg.relay), source: "config" };
51
+ return { url: toHttpUrl(kDefaultRelayUrl), source: "default" };
39
52
  }
40
53
  /**
41
- * Accepts ws://, wss://, http://, https://. The http(s) variants are
42
- * normalized to ws(s) by `normalizeRelayUrl` since WebSocket and HTTP share
43
- * the same TLS layer and port — many reverse proxies (Coolify, Traefik,
44
- * Caddy, nginx-proxy) only expose the URL as https:// even though wss://
45
- * works on the same endpoint via the WebSocket upgrade header.
54
+ * Strict validator for **user-provided** relay URLs (via `/remote-pi
55
+ * set-relay` or `/remote-pi relay url`).
56
+ *
57
+ * Only accepts `http://` and `https://`. `ws://`/`wss://` are deliberately
58
+ * **rejected** the canonical form stored in config is http(s):// and the
59
+ * extension converts to ws(s):// internally when opening the WebSocket.
60
+ * Forcing a single scheme at the user boundary avoids two-form drift.
46
61
  */
47
62
  export function isValidRelayUrl(url) {
48
63
  if (!url)
49
64
  return false;
50
65
  const lower = url.toLowerCase();
51
- if (!lower.startsWith("ws://") &&
52
- !lower.startsWith("wss://") &&
53
- !lower.startsWith("http://") &&
54
- !lower.startsWith("https://"))
66
+ if (!lower.startsWith("http://") && !lower.startsWith("https://"))
55
67
  return false;
56
68
  try {
57
69
  new URL(url);
@@ -62,15 +74,42 @@ export function isValidRelayUrl(url) {
62
74
  }
63
75
  }
64
76
  /**
65
- * Rewrites http(s):// ws(s):// so the user can paste whatever URL their
66
- * hosting provider gives them. Leaves ws(s):// untouched. Assumes the URL
67
- * passed `isValidRelayUrl` already.
77
+ * Returns true if the URL uses ws:// or wss:// scheme for emitting a
78
+ * targeted error message when the user pastes a WebSocket URL by mistake.
79
+ */
80
+ export function isWebSocketScheme(url) {
81
+ const lower = url.toLowerCase();
82
+ return lower.startsWith("ws://") || lower.startsWith("wss://");
83
+ }
84
+ /**
85
+ * Converts an http(s):// URL to the corresponding ws(s):// form. Used by
86
+ * the transport layer right before opening the WebSocket — config storage
87
+ * and the mesh HTTP client both stay on http(s)://.
88
+ *
89
+ * https://host → wss://host
90
+ * http://host → ws://host
91
+ * ws(s)://host → pass-through (defensive — env overrides or legacy
92
+ * configs may still carry ws(s)://)
68
93
  */
69
- export function normalizeRelayUrl(url) {
70
- if (url.toLowerCase().startsWith("https://"))
94
+ export function toWebSocketUrl(url) {
95
+ const lower = url.toLowerCase();
96
+ if (lower.startsWith("https://"))
71
97
  return "wss://" + url.slice("https://".length);
72
- if (url.toLowerCase().startsWith("http://"))
98
+ if (lower.startsWith("http://"))
73
99
  return "ws://" + url.slice("http://".length);
74
100
  return url;
75
101
  }
102
+ /**
103
+ * Inverse of `toWebSocketUrl`. Used by `resolveRelayUrl` to coerce any
104
+ * ws(s):// values back to canonical http(s):// before returning them to
105
+ * the rest of the codebase.
106
+ */
107
+ export function toHttpUrl(url) {
108
+ const lower = url.toLowerCase();
109
+ if (lower.startsWith("wss://"))
110
+ return "https://" + url.slice("wss://".length);
111
+ if (lower.startsWith("ws://"))
112
+ return "http://" + url.slice("ws://".length);
113
+ return url;
114
+ }
76
115
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type ControlReplyFor, type ControlRequest } from "./control_protocol.js";
2
+ export declare class SupervisorOfflineError extends Error {
3
+ readonly sockPath: string;
4
+ constructor(sockPath: string);
5
+ }
6
+ /**
7
+ * Sends a single request and returns the typed reply data.
8
+ *
9
+ * Throws:
10
+ * - `SupervisorOfflineError` when the supervisor isn't reachable.
11
+ * - `Error` from `parseReply` when the reply line is malformed.
12
+ * - The supervisor's own error string when `ok: false`.
13
+ */
14
+ export declare function callSupervisor<Op extends ControlRequest["op"]>(req: Extract<ControlRequest, {
15
+ op: Op;
16
+ }>): Promise<ControlReplyFor<Op>>;
17
+ /** Returns true when the supervisor is reachable. Used by `/remote-pi
18
+ * daemons` to decide whether to query runtime state or fall back to
19
+ * registry-only listing. */
20
+ export declare function supervisorOnline(): Promise<boolean>;