remote-pi 0.1.2 → 0.2.0
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.
- package/README.md +195 -36
- package/dist/bin/supervisord.d.ts +2 -0
- package/dist/bin/supervisord.js +44 -0
- package/dist/bin/supervisord.js.map +1 -0
- package/dist/config.d.ts +49 -5
- package/dist/config.js +73 -9
- package/dist/config.js.map +1 -1
- package/dist/daemon/client.d.ts +20 -0
- package/dist/daemon/client.js +128 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/control_protocol.d.ts +100 -0
- package/dist/daemon/control_protocol.js +63 -0
- package/dist/daemon/control_protocol.js.map +1 -0
- package/dist/daemon/id.d.ts +18 -0
- package/dist/daemon/id.js +30 -0
- package/dist/daemon/id.js.map +1 -0
- package/dist/daemon/install.d.ts +132 -0
- package/dist/daemon/install.js +312 -0
- package/dist/daemon/install.js.map +1 -0
- package/dist/daemon/registry.d.ts +47 -0
- package/dist/daemon/registry.js +123 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/rpc_child.d.ts +76 -0
- package/dist/daemon/rpc_child.js +130 -0
- package/dist/daemon/rpc_child.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +38 -0
- package/dist/daemon/supervisor.js +301 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/index.d.ts +62 -8
- package/dist/index.js +1232 -304
- package/dist/index.js.map +1 -1
- package/dist/mesh/canonical.d.ts +30 -0
- package/dist/mesh/canonical.js +61 -0
- package/dist/mesh/canonical.js.map +1 -0
- package/dist/mesh/client.d.ts +31 -0
- package/dist/mesh/client.js +56 -0
- package/dist/mesh/client.js.map +1 -0
- package/dist/mesh/encoding.d.ts +36 -0
- package/dist/mesh/encoding.js +53 -0
- package/dist/mesh/encoding.js.map +1 -0
- package/dist/mesh/self_revoke.d.ts +111 -0
- package/dist/mesh/self_revoke.js +182 -0
- package/dist/mesh/self_revoke.js.map +1 -0
- package/dist/mesh/siblings.d.ts +62 -0
- package/dist/mesh/siblings.js +95 -0
- package/dist/mesh/siblings.js.map +1 -0
- package/dist/mesh/types.d.ts +34 -0
- package/dist/mesh/types.js +11 -0
- package/dist/mesh/types.js.map +1 -0
- package/dist/mesh/verify.d.ts +17 -0
- package/dist/mesh/verify.js +77 -0
- package/dist/mesh/verify.js.map +1 -0
- package/dist/pairing/qr.d.ts +16 -5
- package/dist/pairing/qr.js +27 -8
- package/dist/pairing/qr.js.map +1 -1
- package/dist/pairing/storage.d.ts +41 -0
- package/dist/pairing/storage.js +158 -21
- package/dist/pairing/storage.js.map +1 -1
- package/dist/protocol/types.d.ts +23 -0
- package/dist/session/broker.d.ts +74 -0
- package/dist/session/broker.js +142 -4
- package/dist/session/broker.js.map +1 -1
- package/dist/session/broker_remote.d.ts +110 -0
- package/dist/session/broker_remote.js +397 -0
- package/dist/session/broker_remote.js.map +1 -0
- package/dist/session/cwd_lock.d.ts +28 -0
- package/dist/session/cwd_lock.js +89 -0
- package/dist/session/cwd_lock.js.map +1 -0
- package/dist/session/global_config.d.ts +9 -0
- package/dist/session/global_config.js +9 -0
- package/dist/session/global_config.js.map +1 -1
- package/dist/session/leader_election.d.ts +16 -0
- package/dist/session/leader_election.js +22 -0
- package/dist/session/leader_election.js.map +1 -1
- package/dist/session/local_config.d.ts +12 -5
- package/dist/session/local_config.js +24 -3
- package/dist/session/local_config.js.map +1 -1
- package/dist/session/peer.d.ts +28 -1
- package/dist/session/peer.js +69 -2
- package/dist/session/peer.js.map +1 -1
- package/dist/session/peer_inventory.d.ts +13 -0
- package/dist/session/peer_inventory.js +48 -0
- package/dist/session/peer_inventory.js.map +1 -0
- package/dist/session/setup_wizard.d.ts +32 -8
- package/dist/session/setup_wizard.js +45 -33
- package/dist/session/setup_wizard.js.map +1 -1
- package/dist/session/tools.d.ts +15 -7
- package/dist/session/tools.js +145 -31
- package/dist/session/tools.js.map +1 -1
- package/dist/transport/pi_forward_client.d.ts +29 -0
- package/dist/transport/pi_forward_client.js +62 -0
- package/dist/transport/pi_forward_client.js.map +1 -0
- package/dist/ui/footer.js +8 -6
- package/dist/ui/footer.js.map +1 -1
- package/docs/daemon.md +289 -0
- package/package.json +10 -3
- package/service-templates/launchd.plist.template +35 -0
- package/service-templates/systemd.service.template +19 -0
- package/skills/agent-network/SKILL.md +273 -294
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/jacobaraujo7/remote_pi/main/branding/logo-full.svg" width="160" alt="Remote Pi logo" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Remote Pi</h1>
|
|
2
6
|
|
|
3
7
|
> Extend the [Pi coding agent](https://github.com/earendil-works/pi) with two
|
|
4
8
|
> superpowers: agents that talk to each other on the same machine, and a mobile
|
|
@@ -9,6 +13,13 @@
|
|
|
9
13
|
`/remote-pi` is a single slash command that wires both at once. Run it; the
|
|
10
14
|
first time it asks a couple of questions and you are done.
|
|
11
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
|
+
|
|
12
23
|
---
|
|
13
24
|
|
|
14
25
|
## Quick start
|
|
@@ -86,19 +97,17 @@ over — the failover is invisible to the LLMs.
|
|
|
86
97
|
|
|
87
98
|
The companion mobile app lets you send prompts to Pi and read its responses
|
|
88
99
|
from your phone. The phone and the Pi process find each other through a
|
|
89
|
-
**relay**: a small WebSocket server that ferries
|
|
90
|
-
|
|
100
|
+
**relay**: a small WebSocket server that ferries messages between them.
|
|
101
|
+
Pairing is one-time and per device, via QR code.
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
The relay sees
|
|
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.
|
|
94
106
|
|
|
95
|
-
App
|
|
107
|
+
**Get the app** — all current download options (Google Play, App Store, and
|
|
108
|
+
direct builds while public releases roll out):
|
|
96
109
|
|
|
97
|
-
-
|
|
98
|
-
- **App Store** — *coming soon*
|
|
99
|
-
|
|
100
|
-
Until the public releases land, follow
|
|
101
|
-
[the repo](https://github.com/jacobaraujo7/remote_pi) for build/beta info.
|
|
110
|
+
<https://remote-pi.jacobmoura.work/#get-the-app>
|
|
102
111
|
|
|
103
112
|
---
|
|
104
113
|
|
|
@@ -193,8 +202,10 @@ You have two options:
|
|
|
193
202
|
|
|
194
203
|
### Option A — Use the community relay
|
|
195
204
|
|
|
196
|
-
`
|
|
197
|
-
out or for casual use.
|
|
205
|
+
`https://relay-rp1.jacobmoura.work` (default). Zero setup. Good for trying
|
|
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.)
|
|
198
209
|
|
|
199
210
|
Caveats:
|
|
200
211
|
|
|
@@ -224,22 +235,27 @@ docker run -d \
|
|
|
224
235
|
```
|
|
225
236
|
|
|
226
237
|
Bind the container to your VPN interface, terminate TLS in a reverse proxy,
|
|
227
|
-
and point both your Pi and your phone at the resulting `
|
|
238
|
+
and point both your Pi and your phone at the resulting `https://…` URL.
|
|
228
239
|
|
|
229
240
|
### Pointing Pi at your own relay
|
|
230
241
|
|
|
231
242
|
Once your relay is reachable, tell the extension:
|
|
232
243
|
|
|
233
244
|
```text
|
|
234
|
-
/remote-pi relay url
|
|
245
|
+
/remote-pi relay url https://relay.yourdomain.tld
|
|
235
246
|
```
|
|
236
247
|
|
|
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.
|
|
252
|
+
|
|
237
253
|
This writes `~/.pi/remote/config.json` with `{ "relay": "..." }`. Resolution
|
|
238
254
|
order (highest precedence first):
|
|
239
255
|
|
|
240
256
|
1. `REMOTE_PI_RELAY` environment variable (CI / one-off overrides)
|
|
241
257
|
2. `~/.pi/remote/config.json`
|
|
242
|
-
3. The built-in default (`
|
|
258
|
+
3. The built-in default (`https://relay-rp1.jacobmoura.work`)
|
|
243
259
|
|
|
244
260
|
Verify the active URL and its source with:
|
|
245
261
|
|
|
@@ -299,34 +315,149 @@ real name to the peer.
|
|
|
299
315
|
|
|
300
316
|
## Command reference
|
|
301
317
|
|
|
318
|
+
### Local session (one Pi, one terminal)
|
|
319
|
+
|
|
302
320
|
| Command | Description |
|
|
303
321
|
|---|---|
|
|
304
|
-
| `/remote-pi` | Connect (join
|
|
322
|
+
| `/remote-pi` | Connect (join local mesh + start relay), or run setup on first use |
|
|
305
323
|
| `/remote-pi setup` | Run the setup wizard and update local config |
|
|
306
|
-
| `/remote-pi
|
|
307
|
-
| `/remote-pi
|
|
308
|
-
| `/remote-pi
|
|
309
|
-
| `/remote-pi
|
|
310
|
-
| `/remote-pi relay` | Toggle the relay connection on/off |
|
|
311
|
-
| `/remote-pi relay start` | Connect to the relay |
|
|
312
|
-
| `/remote-pi relay stop` | Disconnect from the relay |
|
|
313
|
-
| `/remote-pi relay status` | Show current relay status |
|
|
314
|
-
| `/remote-pi relay url <url>` | Set the relay URL (alias of `/remote-pi set-relay`) |
|
|
315
|
-
| `/remote-pi pair` | Show a QR code to pair a new mobile device |
|
|
316
|
-
| `/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) |
|
|
317
328
|
| `/remote-pi revoke <shortid>` | Revoke a paired device by its shortid |
|
|
318
|
-
| `/remote-pi set-relay <url>` | Persist a new relay URL
|
|
319
|
-
| `/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://) |
|
|
320
330
|
|
|
321
|
-
|
|
331
|
+
### Daemon fleet (one supervisor, N background Pis — see [Daemon mode](#daemon-mode))
|
|
322
332
|
|
|
323
|
-
|
|
324
|
-
|
|
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)
|
|
325
354
|
- `🟡 relay waiting for pairing` — relay connected, no device paired yet
|
|
326
355
|
- `📱 <shortid>` — a mobile device is actively connected right now
|
|
327
356
|
|
|
328
|
-
|
|
329
|
-
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.
|
|
330
461
|
|
|
331
462
|
---
|
|
332
463
|
|
|
@@ -343,7 +474,7 @@ your terminals apart at a glance.
|
|
|
343
474
|
Override the relay for a single run without persisting:
|
|
344
475
|
|
|
345
476
|
```bash
|
|
346
|
-
REMOTE_PI_RELAY=
|
|
477
|
+
REMOTE_PI_RELAY=https://staging.example.tld pi
|
|
347
478
|
```
|
|
348
479
|
|
|
349
480
|
---
|
|
@@ -372,6 +503,34 @@ other terminal first.
|
|
|
372
503
|
|
|
373
504
|
---
|
|
374
505
|
|
|
506
|
+
## Branding
|
|
507
|
+
|
|
508
|
+
Official brand assets live in
|
|
509
|
+
[`/branding`](https://github.com/jacobaraujo7/remote_pi/tree/main/branding) —
|
|
510
|
+
SVG sources for the logo (full, foreground, background, monochrome) plus a
|
|
511
|
+
banner. See the
|
|
512
|
+
[branding README](https://github.com/jacobaraujo7/remote_pi/blob/main/branding/README.md)
|
|
513
|
+
for palette and export sizes.
|
|
514
|
+
|
|
515
|
+
<table>
|
|
516
|
+
<tr>
|
|
517
|
+
<td align="center">
|
|
518
|
+
<img src="https://raw.githubusercontent.com/jacobaraujo7/remote_pi/main/branding/logo-full.svg" width="96" alt="logo-full" /><br/>
|
|
519
|
+
<sub><code>logo-full</code></sub>
|
|
520
|
+
</td>
|
|
521
|
+
<td align="center">
|
|
522
|
+
<img src="https://raw.githubusercontent.com/jacobaraujo7/remote_pi/main/branding/logo-foreground.svg" width="96" alt="logo-foreground" /><br/>
|
|
523
|
+
<sub><code>logo-foreground</code></sub>
|
|
524
|
+
</td>
|
|
525
|
+
<td align="center">
|
|
526
|
+
<img src="https://raw.githubusercontent.com/jacobaraujo7/remote_pi/main/branding/logo-monochrome.svg" width="96" alt="logo-monochrome" /><br/>
|
|
527
|
+
<sub><code>logo-monochrome</code></sub>
|
|
528
|
+
</td>
|
|
529
|
+
</tr>
|
|
530
|
+
</table>
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
375
534
|
## Links
|
|
376
535
|
|
|
377
536
|
- Homepage: <https://remote-pi.jacobmoura.work>
|
|
@@ -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,4 +1,11 @@
|
|
|
1
|
-
|
|
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
|
+
*/
|
|
8
|
+
export declare const kDefaultRelayUrl = "https://relay-rp1.jacobmoura.work";
|
|
2
9
|
export type RemotePiConfig = {
|
|
3
10
|
relay?: string;
|
|
4
11
|
};
|
|
@@ -9,10 +16,47 @@ export type RelayResolution = {
|
|
|
9
16
|
source: "env" | "config" | "default";
|
|
10
17
|
};
|
|
11
18
|
/**
|
|
12
|
-
* Resolves the effective relay URL
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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;
|
|
31
|
+
/**
|
|
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.
|
|
39
|
+
*/
|
|
18
40
|
export declare function isValidRelayUrl(url: string): boolean;
|
|
41
|
+
/**
|
|
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.
|
|
61
|
+
*/
|
|
62
|
+
export declare function toHttpUrl(url: string): string;
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,14 @@ 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
|
-
|
|
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
|
+
*/
|
|
13
|
+
export const kDefaultRelayUrl = "https://relay-rp1.jacobmoura.work";
|
|
7
14
|
export function loadConfig() {
|
|
8
15
|
try {
|
|
9
16
|
const raw = fs.readFileSync(CONFIG_FILE, "utf8");
|
|
@@ -23,22 +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
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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: 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: cfg.relay, source: "config" };
|
|
38
|
-
return { url: kDefaultRelayUrl, source: "default" };
|
|
50
|
+
return { url: toHttpUrl(cfg.relay), source: "config" };
|
|
51
|
+
return { url: toHttpUrl(kDefaultRelayUrl), source: "default" };
|
|
39
52
|
}
|
|
53
|
+
/**
|
|
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.
|
|
61
|
+
*/
|
|
40
62
|
export function isValidRelayUrl(url) {
|
|
41
|
-
if (!url
|
|
63
|
+
if (!url)
|
|
64
|
+
return false;
|
|
65
|
+
const lower = url.toLowerCase();
|
|
66
|
+
if (!lower.startsWith("http://") && !lower.startsWith("https://"))
|
|
42
67
|
return false;
|
|
43
68
|
try {
|
|
44
69
|
new URL(url);
|
|
@@ -48,4 +73,43 @@ export function isValidRelayUrl(url) {
|
|
|
48
73
|
return false;
|
|
49
74
|
}
|
|
50
75
|
}
|
|
76
|
+
/**
|
|
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)://)
|
|
93
|
+
*/
|
|
94
|
+
export function toWebSocketUrl(url) {
|
|
95
|
+
const lower = url.toLowerCase();
|
|
96
|
+
if (lower.startsWith("https://"))
|
|
97
|
+
return "wss://" + url.slice("https://".length);
|
|
98
|
+
if (lower.startsWith("http://"))
|
|
99
|
+
return "ws://" + url.slice("http://".length);
|
|
100
|
+
return url;
|
|
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
|
+
}
|
|
51
115
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAC5D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAEzD,MAAM,CAAC,MAAM,gBAAgB,GAAG,
|
|
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>;
|