remote-pi 0.1.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/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +1285 -0
- package/dist/index.js.map +1 -0
- package/dist/pairing/crypto.d.ts +8 -0
- package/dist/pairing/crypto.js +22 -0
- package/dist/pairing/crypto.js.map +1 -0
- package/dist/pairing/handshake.d.ts +28 -0
- package/dist/pairing/handshake.js +113 -0
- package/dist/pairing/handshake.js.map +1 -0
- package/dist/pairing/noise-sha256.d.ts +16 -0
- package/dist/pairing/noise-sha256.js +103 -0
- package/dist/pairing/noise-sha256.js.map +1 -0
- package/dist/pairing/qr.d.ts +41 -0
- package/dist/pairing/qr.js +96 -0
- package/dist/pairing/qr.js.map +1 -0
- package/dist/pairing/storage.d.ts +10 -0
- package/dist/pairing/storage.js +65 -0
- package/dist/pairing/storage.js.map +1 -0
- package/dist/protocol/codec.d.ts +7 -0
- package/dist/protocol/codec.js +46 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/types.d.ts +119 -0
- package/dist/protocol/types.js +2 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/rooms.d.ts +9 -0
- package/dist/rooms.js +22 -0
- package/dist/rooms.js.map +1 -0
- package/dist/session/agent_bridge.d.ts +55 -0
- package/dist/session/agent_bridge.js +146 -0
- package/dist/session/agent_bridge.js.map +1 -0
- package/dist/session/broker.d.ts +37 -0
- package/dist/session/broker.js +206 -0
- package/dist/session/broker.js.map +1 -0
- package/dist/session/envelope.d.ts +24 -0
- package/dist/session/envelope.js +89 -0
- package/dist/session/envelope.js.map +1 -0
- package/dist/session/global_config.d.ts +14 -0
- package/dist/session/global_config.js +51 -0
- package/dist/session/global_config.js.map +1 -0
- package/dist/session/leader_election.d.ts +16 -0
- package/dist/session/leader_election.js +78 -0
- package/dist/session/leader_election.js.map +1 -0
- package/dist/session/local_config.d.ts +18 -0
- package/dist/session/local_config.js +47 -0
- package/dist/session/local_config.js.map +1 -0
- package/dist/session/peer.d.ts +80 -0
- package/dist/session/peer.js +268 -0
- package/dist/session/peer.js.map +1 -0
- package/dist/session/setup_wizard.d.ts +32 -0
- package/dist/session/setup_wizard.js +60 -0
- package/dist/session/setup_wizard.js.map +1 -0
- package/dist/session/tool_gate.d.ts +5 -0
- package/dist/session/tool_gate.js +11 -0
- package/dist/session/tool_gate.js.map +1 -0
- package/dist/session/tools.d.ts +16 -0
- package/dist/session/tools.js +123 -0
- package/dist/session/tools.js.map +1 -0
- package/dist/session/wizard.d.ts +13 -0
- package/dist/session/wizard.js +20 -0
- package/dist/session/wizard.js.map +1 -0
- package/dist/settings.d.ts +15 -0
- package/dist/settings.js +52 -0
- package/dist/settings.js.map +1 -0
- package/dist/transport/peer_channel.d.ts +37 -0
- package/dist/transport/peer_channel.js +85 -0
- package/dist/transport/peer_channel.js.map +1 -0
- package/dist/transport/relay_client.d.ts +81 -0
- package/dist/transport/relay_client.js +154 -0
- package/dist/transport/relay_client.js.map +1 -0
- package/dist/ui/footer.d.ts +32 -0
- package/dist/ui/footer.js +32 -0
- package/dist/ui/footer.js.map +1 -0
- package/package.json +77 -0
- package/skills/agent-network/SKILL.md +429 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jacob Moura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Remote Pi
|
|
2
|
+
|
|
3
|
+
> Extend the [Pi coding agent](https://github.com/earendil-works/pi) with two
|
|
4
|
+
> superpowers: agents that talk to each other on the same machine, and a mobile
|
|
5
|
+
> app that drives Pi from your phone.
|
|
6
|
+
|
|
7
|
+
`/remote-pi` is a single slash command that wires both at once. Run it; the
|
|
8
|
+
first time it asks a couple of questions and you are done.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
Install the extension (one-time):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:@jacobmoura7/remote-pi
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then in any Pi terminal:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
/remote-pi
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The first run shows a short interactive wizard (agent name, default session,
|
|
27
|
+
whether to auto-start the relay). On every following run, `/remote-pi` joins
|
|
28
|
+
the local agent session and starts the relay automatically — no extra typing.
|
|
29
|
+
|
|
30
|
+
### Try the agent network in 30 seconds
|
|
31
|
+
|
|
32
|
+
Open **two** Pi terminals in the same directory and run `/remote-pi` in each.
|
|
33
|
+
Both join the same session. Now just talk to the LLM — it has the tools.
|
|
34
|
+
|
|
35
|
+
In terminal A (say it ended up named `agent-A`):
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
Who else is connected in our agent session? List them.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The LLM calls `agent_send` to `broker` with `{ type: "list_peers" }` and
|
|
42
|
+
replies with the names it sees.
|
|
43
|
+
|
|
44
|
+
Then, still in terminal A:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
Send a ping to agent-B and wait for a reply.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Pi calls `agent_request({ to: "agent-B", body: { type: "ping" } })`. The
|
|
51
|
+
message arrives in terminal B as a user-facing turn — terminal B's LLM
|
|
52
|
+
answers, and the reply lands back in terminal A. Two agents, one prompt
|
|
53
|
+
each, full round trip.
|
|
54
|
+
|
|
55
|
+
(Replace `agent-B` with whatever name terminal B reports for itself — the
|
|
56
|
+
wizard's default is the directory name plus a `#N` suffix on collision.)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## What it does
|
|
61
|
+
|
|
62
|
+
Remote Pi adds two independent layers on top of Pi. You can use either, or
|
|
63
|
+
both:
|
|
64
|
+
|
|
65
|
+
### 1) Agent network (local, same machine)
|
|
66
|
+
|
|
67
|
+
Several Pi instances running side-by-side in different terminals can discover
|
|
68
|
+
each other and exchange messages. Each instance is a peer in a named
|
|
69
|
+
*session* and gets two tools the LLM can call directly:
|
|
70
|
+
|
|
71
|
+
- `agent_send` — fire-and-forget message to another agent
|
|
72
|
+
- `agent_request` — send and await a reply (correlated by message id)
|
|
73
|
+
|
|
74
|
+
This is purely local: the agents talk over a Unix domain socket at
|
|
75
|
+
`~/.pi/remote/sessions/<session-name>/broker.sock`. No network involved.
|
|
76
|
+
Useful for splitting work across roles (`backend`, `frontend`, `tests`,
|
|
77
|
+
`orchestrator`, …) and letting them coordinate.
|
|
78
|
+
|
|
79
|
+
The first agent to enter a session becomes the *leader* (hosts the broker);
|
|
80
|
+
the rest are *followers*. If the leader exits, a follower automatically takes
|
|
81
|
+
over — the failover is invisible to the LLMs.
|
|
82
|
+
|
|
83
|
+
### 2) Mobile app (over the relay)
|
|
84
|
+
|
|
85
|
+
The companion mobile app lets you send prompts to Pi and read its responses
|
|
86
|
+
from your phone. The phone and the Pi process find each other through a
|
|
87
|
+
**relay**: a small WebSocket server that ferries end-to-end encrypted
|
|
88
|
+
messages between them. Pairing is one-time and per device, via QR code.
|
|
89
|
+
|
|
90
|
+
Encryption uses Curve25519 key agreement + ChaCha20-Poly1305 (libsodium).
|
|
91
|
+
The relay sees only ciphertext.
|
|
92
|
+
|
|
93
|
+
App downloads:
|
|
94
|
+
|
|
95
|
+
- **Google Play** — *coming soon*
|
|
96
|
+
- **App Store** — *coming soon*
|
|
97
|
+
|
|
98
|
+
Until the public releases land, follow
|
|
99
|
+
[the repo](https://github.com/jacobmoura7/remote-pi) for build/beta info.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Install
|
|
104
|
+
|
|
105
|
+
Requirements: Node 20+, Pi (the host coding agent).
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pi install npm:@jacobmoura7/remote-pi
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The extension self-registers the `/remote-pi` slash command and deploys an
|
|
112
|
+
agent skill that teaches the LLM how to use `agent_send` / `agent_request`.
|
|
113
|
+
|
|
114
|
+
To verify:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
/remote-pi config
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It should print the effective relay URL and where it came from
|
|
121
|
+
(`env` / `config` / `default`).
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Using `/remote-pi`
|
|
126
|
+
|
|
127
|
+
The bare command is the everyday entry point:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
/remote-pi
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Behavior depends on whether there's a local config for this directory:
|
|
134
|
+
|
|
135
|
+
| State | What happens |
|
|
136
|
+
|---|---|
|
|
137
|
+
| First run (no `.pi/remote-pi/config.json`) | Interactive wizard → saves config → joins agent session → starts relay (if you opted in) |
|
|
138
|
+
| Returning user, auto-start enabled | Joins agent session + starts relay automatically, then prints status |
|
|
139
|
+
| Returning user, auto-start disabled | Prints status only; join/relay must be run manually |
|
|
140
|
+
|
|
141
|
+
The wizard asks three questions:
|
|
142
|
+
|
|
143
|
+
1. **Agent name** — how other agents will address you in `agent_send` /
|
|
144
|
+
`agent_request`. Defaults to the directory name.
|
|
145
|
+
2. **Default session** — the name of the agent-network room for this
|
|
146
|
+
directory. Multiple terminals in the same directory join the same session.
|
|
147
|
+
3. **Auto-start relay (for mobile app access)?** — `Yes` if you want
|
|
148
|
+
`/remote-pi` to also connect to the relay so the mobile app can reach this
|
|
149
|
+
Pi. `No` for local-only use (agent network without mobile access).
|
|
150
|
+
|
|
151
|
+
Re-run the wizard later with `/remote-pi setup`.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Pairing a mobile device
|
|
156
|
+
|
|
157
|
+
Once the relay is up (`/remote-pi relay status` shows `started` or `paired`):
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
/remote-pi pair
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A QR code is printed in the terminal. Scan it with the Remote Pi mobile app.
|
|
164
|
+
Pairing is **per machine** — once a device is paired, every Pi process on
|
|
165
|
+
this machine accepts it (it lives in `~/.pi/remote/peers.json`).
|
|
166
|
+
|
|
167
|
+
To list paired devices:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
/remote-pi devices
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
To remove one:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
/remote-pi revoke <shortid>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The shortid is the first 8 chars shown by `devices`.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## The relay
|
|
184
|
+
|
|
185
|
+
The relay is the only network-touching piece of Remote Pi. It does **not**
|
|
186
|
+
read messages — payloads are end-to-end encrypted between the Pi and the
|
|
187
|
+
paired device — but it sees connection metadata: which keypair is online,
|
|
188
|
+
which room/cwd identifiers exist, message timing, sizes.
|
|
189
|
+
|
|
190
|
+
You have two options:
|
|
191
|
+
|
|
192
|
+
### Option A — Use the community relay
|
|
193
|
+
|
|
194
|
+
`wss://relay.remote-pi.dev` (default). Zero setup. Good for trying things
|
|
195
|
+
out or for casual use.
|
|
196
|
+
|
|
197
|
+
Caveats:
|
|
198
|
+
|
|
199
|
+
- Shared infrastructure — availability is best-effort.
|
|
200
|
+
- The operator could observe connection metadata as described above.
|
|
201
|
+
- TLS + per-message encryption is the only protection; **there is no IP
|
|
202
|
+
allow-listing or VPN gating**.
|
|
203
|
+
|
|
204
|
+
### Option B — Self-host (recommended for privacy)
|
|
205
|
+
|
|
206
|
+
Run the relay yourself in Docker and put it behind a VPN like
|
|
207
|
+
[Tailscale](https://tailscale.com), [WireGuard](https://www.wireguard.com),
|
|
208
|
+
or your own VPC. Because the relay's network-level protection is just TLS +
|
|
209
|
+
keypair authentication, layering a VPN on top means **only your devices** can
|
|
210
|
+
even reach the WebSocket port — defense in depth.
|
|
211
|
+
|
|
212
|
+
Quick Docker outline (see the
|
|
213
|
+
[relay README](https://github.com/jacobmoura7/remote-pi/blob/main/relay/README.md#self-hosted-relay-recommended-for-privacy)
|
|
214
|
+
for the full setup, environment variables, and reverse-proxy guidance):
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
docker run -d \
|
|
218
|
+
--name remote-pi-relay \
|
|
219
|
+
-p 3000:3000 \
|
|
220
|
+
--restart unless-stopped \
|
|
221
|
+
ghcr.io/jacobmoura7/remote-pi-relay:latest
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Bind the container to your VPN interface, terminate TLS in a reverse proxy,
|
|
225
|
+
and point both your Pi and your phone at the resulting `wss://…` URL.
|
|
226
|
+
|
|
227
|
+
### Pointing Pi at your own relay
|
|
228
|
+
|
|
229
|
+
Once your relay is reachable, tell the extension:
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
/remote-pi relay url wss://relay.yourdomain.tld
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This writes `~/.pi/remote/config.json` with `{ "relay": "..." }`. Resolution
|
|
236
|
+
order (highest precedence first):
|
|
237
|
+
|
|
238
|
+
1. `REMOTE_PI_RELAY` environment variable (CI / one-off overrides)
|
|
239
|
+
2. `~/.pi/remote/config.json`
|
|
240
|
+
3. The built-in default (`wss://relay.remote-pi.dev`)
|
|
241
|
+
|
|
242
|
+
Verify the active URL and its source with:
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
/remote-pi config
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
If you change the URL while connected, run `/remote-pi relay stop` then
|
|
249
|
+
`/remote-pi relay start` (or `/remote-pi relay` to toggle).
|
|
250
|
+
|
|
251
|
+
The mobile app has its own relay-URL setting in its preferences pane — keep
|
|
252
|
+
both pointing at the same relay.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Agent network: deeper look
|
|
257
|
+
|
|
258
|
+
Each session is one Unix-domain-socket broker plus N peers. The broker
|
|
259
|
+
multiplexes messages by `to` name and broadcasts system events
|
|
260
|
+
(`peer_joined`, `peer_left`).
|
|
261
|
+
|
|
262
|
+
Inside the LLM, the agent skill registers two tools:
|
|
263
|
+
|
|
264
|
+
```jsonc
|
|
265
|
+
// Fire-and-forget
|
|
266
|
+
agent_send({
|
|
267
|
+
to: "backend", // peer name (or array for multicast)
|
|
268
|
+
body: { task: "add /healthz endpoint" },
|
|
269
|
+
re: "<id>" // optional — set when replying to a previous request
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Send + await reply (default 30s timeout)
|
|
273
|
+
agent_request({
|
|
274
|
+
to: "backend",
|
|
275
|
+
body: { question: "is the migration applied?" }
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The wire format is a 5-field envelope `{ from, to, id, re, body }` serialized
|
|
280
|
+
as one JSON line per message. The leader's broker writes an `audit.jsonl`
|
|
281
|
+
log at `~/.pi/remote/sessions/<name>/audit.jsonl` for postmortem inspection.
|
|
282
|
+
|
|
283
|
+
Useful commands:
|
|
284
|
+
|
|
285
|
+
| Command | What it does |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `/remote-pi join [name]` | Join (or create) a session — only needed manually if `auto_start_relay=false` |
|
|
288
|
+
| `/remote-pi leave` | Leave the current session |
|
|
289
|
+
| `/remote-pi sessions` | List local sessions and which are live |
|
|
290
|
+
| `/remote-pi rename <new>` | Rename this agent in the current session |
|
|
291
|
+
|
|
292
|
+
Name collisions inside a session get a numeric suffix automatically
|
|
293
|
+
(`backend`, `backend#2`, `backend#3`). The broker assigns it and returns the
|
|
294
|
+
real name to the peer.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Command reference
|
|
299
|
+
|
|
300
|
+
| Command | Description |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `/remote-pi` | Connect (join session + start relay), or run setup on first use |
|
|
303
|
+
| `/remote-pi setup` | Run the setup wizard and update local config |
|
|
304
|
+
| `/remote-pi join [name]` | Join (or create) a local agent session |
|
|
305
|
+
| `/remote-pi leave` | Leave the current agent session |
|
|
306
|
+
| `/remote-pi rename <name>` | Rename this agent in the current session |
|
|
307
|
+
| `/remote-pi sessions` | List local agent sessions |
|
|
308
|
+
| `/remote-pi relay` | Toggle the relay connection on/off |
|
|
309
|
+
| `/remote-pi relay start` | Connect to the relay |
|
|
310
|
+
| `/remote-pi relay stop` | Disconnect from the relay |
|
|
311
|
+
| `/remote-pi relay status` | Show current relay status |
|
|
312
|
+
| `/remote-pi relay url <url>` | Set the relay URL (alias of `/remote-pi set-relay`) |
|
|
313
|
+
| `/remote-pi pair` | Show a QR code to pair a new mobile device |
|
|
314
|
+
| `/remote-pi devices` | List paired mobile devices |
|
|
315
|
+
| `/remote-pi revoke <shortid>` | Revoke a paired device by its shortid |
|
|
316
|
+
| `/remote-pi set-relay <url>` | Persist a new relay URL to user config |
|
|
317
|
+
| `/remote-pi config` | Show the effective relay URL and its source |
|
|
318
|
+
|
|
319
|
+
The footer in the Pi TUI reflects state live:
|
|
320
|
+
|
|
321
|
+
- `📡 <session> (N)` — current agent session and peer count
|
|
322
|
+
- `🟢 relay` — relay connected, at least one device paired
|
|
323
|
+
- `🟡 relay waiting for pairing` — relay connected, no device paired yet
|
|
324
|
+
- `📱 <shortid>` — a mobile device is actively connected right now
|
|
325
|
+
|
|
326
|
+
The window title becomes `<agent-name> · <session> · relay` so you can tell
|
|
327
|
+
your terminals apart at a glance.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Configuration files
|
|
332
|
+
|
|
333
|
+
| Path | Scope | What's in it |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| `<cwd>/.pi/remote-pi/config.json` | Per-directory | `agent_name`, `session_name`, `auto_start_relay` |
|
|
336
|
+
| `~/.pi/remote/config.json` | Per-user | `relay` URL |
|
|
337
|
+
| `~/.pi/remote/peers.json` | Per-machine | Paired mobile devices |
|
|
338
|
+
| `~/.pi/remote/sessions/<name>/` | Per-session | Broker socket + `audit.jsonl` |
|
|
339
|
+
| `~/.pi/remote/skills/agent-network/SKILL.md` | Per-user | Agent skill the LLM reads |
|
|
340
|
+
|
|
341
|
+
Override the relay for a single run without persisting:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
REMOTE_PI_RELAY=wss://staging.example.tld pi
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Troubleshooting
|
|
350
|
+
|
|
351
|
+
**Footer says `🟡 relay waiting for pairing` even though I paired a device.**
|
|
352
|
+
The icon reflects whether *any* device has been paired on this machine, not
|
|
353
|
+
whether one is connected right now. If you really have a paired device in
|
|
354
|
+
`/remote-pi devices`, restart Pi — the cache may be stale (fixed in current
|
|
355
|
+
release; report a bug if it recurs).
|
|
356
|
+
|
|
357
|
+
**Mobile app times out connecting.** Verify the same relay URL is configured
|
|
358
|
+
on both sides. If you self-host behind a VPN, your phone must also be on the
|
|
359
|
+
VPN (Tailscale on iOS/Android works fine).
|
|
360
|
+
|
|
361
|
+
**`agent_request` keeps timing out.** Default timeout is 30 s. For tasks
|
|
362
|
+
that legitimately take longer, the receiver should reply with `agent_send`
|
|
363
|
+
including `re: "<original-id>"` so the requester can correlate. The skill
|
|
364
|
+
explains this to the LLM automatically.
|
|
365
|
+
|
|
366
|
+
**Multiple terminals in the same directory.** Supported. They share the same
|
|
367
|
+
agent-network session (UDS broker) and the relay handles each Pi process
|
|
368
|
+
independently. If the relay refuses with `RoomAlreadyOpenError`, stop the
|
|
369
|
+
other terminal first.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Links
|
|
374
|
+
|
|
375
|
+
- Source: <https://github.com/jacobmoura7/remote-pi>
|
|
376
|
+
- Pi coding agent: <https://github.com/earendil-works/pi>
|
|
377
|
+
- Relay (self-hosting guide): <https://github.com/jacobmoura7/remote-pi/blob/main/relay/README.md>
|
|
378
|
+
- Issues / bugs: <https://github.com/jacobmoura7/remote-pi/issues>
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## License
|
|
383
|
+
|
|
384
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const kDefaultRelayUrl = "wss://relay.remote-pi.dev";
|
|
2
|
+
export type RemotePiConfig = {
|
|
3
|
+
relay?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function loadConfig(): RemotePiConfig;
|
|
6
|
+
export declare function saveConfig(patch: Partial<RemotePiConfig>): void;
|
|
7
|
+
export type RelayResolution = {
|
|
8
|
+
url: string;
|
|
9
|
+
source: "env" | "config" | "default";
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
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)
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveRelayUrl(): RelayResolution;
|
|
18
|
+
export declare function isValidRelayUrl(url: string): boolean;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".pi", "remote");
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
export const kDefaultRelayUrl = "wss://relay.remote-pi.dev";
|
|
7
|
+
export function loadConfig() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (!parsed || typeof parsed !== "object")
|
|
12
|
+
return {};
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function saveConfig(patch) {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
const current = loadConfig();
|
|
22
|
+
const next = { ...current, ...patch };
|
|
23
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
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)
|
|
30
|
+
*/
|
|
31
|
+
export function resolveRelayUrl() {
|
|
32
|
+
const env = process.env["REMOTE_PI_RELAY"];
|
|
33
|
+
if (env && env.length > 0)
|
|
34
|
+
return { url: env, source: "env" };
|
|
35
|
+
const cfg = loadConfig();
|
|
36
|
+
if (cfg.relay && cfg.relay.length > 0)
|
|
37
|
+
return { url: cfg.relay, source: "config" };
|
|
38
|
+
return { url: kDefaultRelayUrl, source: "default" };
|
|
39
|
+
}
|
|
40
|
+
export function isValidRelayUrl(url) {
|
|
41
|
+
if (!url || (!url.startsWith("ws://") && !url.startsWith("wss://")))
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
new URL(url);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +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,2BAA2B,CAAC;AAI5D,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QACrD,OAAO,MAAwB,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAA8B;IACvD,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;IACtC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC/D,CAAC;AAID;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC3C,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC9D,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnF,OAAO,EAAE,GAAG,EAAE,gBAAgB,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAClF,IAAI,CAAC;QAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;AAC5D,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-extension — remote-pi slash commands + AgentBridge wiring
|
|
3
|
+
*
|
|
4
|
+
* Exported as ExtensionFactory (default export) to be loaded by Pi SDK:
|
|
5
|
+
* pi -e $(pwd)/dist/index.js
|
|
6
|
+
*
|
|
7
|
+
* State machine: idle → started → paired
|
|
8
|
+
* /remote-pi start connects to relay (idle → started)
|
|
9
|
+
* /remote-pi pair shows QR for new peers (started, async → paired via auto-listener)
|
|
10
|
+
* /remote-pi stop closes everything (any → idle)
|
|
11
|
+
*
|
|
12
|
+
* Pairing (post plano 06 — sem Noise XX):
|
|
13
|
+
* App envia inner `pair_request` (id, token, device_name) sobre canal opaco.
|
|
14
|
+
* Pi valida o token via qrSession.consumeToken, salva peer em peers.json
|
|
15
|
+
* {name, remote_epk, paired_at} e responde com `pair_ok` (ou `pair_error`).
|
|
16
|
+
* `ct` é base64(JSON.stringify(inner)) — sem cifra, sem MAC.
|
|
17
|
+
*
|
|
18
|
+
* Reconexão de peer conhecido:
|
|
19
|
+
* Se uma mensagem chega em estado `started` vinda de um epk presente em
|
|
20
|
+
* peers.json, o auto-listener promove direto pra `paired` sem novo
|
|
21
|
+
* pair_request, criando o PlainPeerChannel e roteando a mensagem.
|
|
22
|
+
*
|
|
23
|
+
* Architecture note — why we don't use AgentBridge directly here:
|
|
24
|
+
* AgentBridge.beforeToolCallHook is designed to be passed to createAgentSession().
|
|
25
|
+
* Inside an extension Pi already owns the AgentSession, so we can't re-bind
|
|
26
|
+
* beforeToolCall after the fact. The equivalent is pi.on("tool_call", …) which
|
|
27
|
+
* fires BEFORE execution and supports { block: true }.
|
|
28
|
+
* AgentBridge (src/session/agent_bridge.ts) remains the tested, mockable unit
|
|
29
|
+
* for integration tests.
|
|
30
|
+
*/
|
|
31
|
+
import type { ExtensionContext, ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
32
|
+
import type { ClientMessage, SessionHistoryEvent } from "./protocol/types.js";
|
|
33
|
+
export type RemoteState = "idle" | "started" | "paired";
|
|
34
|
+
type BufferMsg = {
|
|
35
|
+
role: "user" | "assistant" | "toolResult" | string;
|
|
36
|
+
content?: unknown;
|
|
37
|
+
timestamp?: number;
|
|
38
|
+
toolCallId?: string;
|
|
39
|
+
toolName?: string;
|
|
40
|
+
isError?: boolean;
|
|
41
|
+
usage?: {
|
|
42
|
+
input?: number;
|
|
43
|
+
output?: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** Test-only override of the message buffer. */
|
|
47
|
+
export declare function _setMessageBufferForTest(msgs: unknown[]): void;
|
|
48
|
+
/** Test-only accessor: returns a defensive copy of the buffer. */
|
|
49
|
+
export declare function _getMessageBufferForTest(): unknown[];
|
|
50
|
+
/** Test-only override of session started timestamp. */
|
|
51
|
+
export declare function _setSessionStartedAtForTest(ts: number | null): void;
|
|
52
|
+
/** Test-only: reset the cached model name (between tests). */
|
|
53
|
+
export declare function _setCurrentModelForTest(name: string | undefined): void;
|
|
54
|
+
/** Test-only: exposes pending reconnect timer state. */
|
|
55
|
+
export declare function _hasPendingReconnect(): boolean;
|
|
56
|
+
/** Exported for tests. */
|
|
57
|
+
export declare function _getState(): RemoteState;
|
|
58
|
+
/**
|
|
59
|
+
* App-level peer disconnect (relay still up).
|
|
60
|
+
* Transitions paired → started and re-installs the auto-listener.
|
|
61
|
+
* Exported so tests can trigger it directly; in production it will be
|
|
62
|
+
* called when the relay sends a peer-disconnect notification (future).
|
|
63
|
+
*/
|
|
64
|
+
export declare function _onPeerDisconnect(): void;
|
|
65
|
+
declare const extension: ExtensionFactory;
|
|
66
|
+
export default extension;
|
|
67
|
+
export declare function routeClientMessage(msg: ClientMessage, ctx: Pick<ExtensionContext, "abort">): void;
|
|
68
|
+
/**
|
|
69
|
+
* Maps SDK AgentMessage[] (UserMessage / AssistantMessage / ToolResultMessage)
|
|
70
|
+
* into the flat SessionHistoryEvent[] shape consumed by the app.
|
|
71
|
+
*
|
|
72
|
+
* Caveats (see report): in_reply_to of agent_message is the *last* user_input
|
|
73
|
+
* id seen in a linear scan — fine for typical conversational flow but not
|
|
74
|
+
* a perfect reconstruction of multi-turn ordering when tools interleave.
|
|
75
|
+
* Stable id for user_input is `sync_<timestamp>`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function _mapAgentMessagesToEvents(messages: BufferMsg[]): SessionHistoryEvent[];
|