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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type Envelope } from "./envelope.js";
|
|
2
|
+
/**
|
|
3
|
+
* Symmetric peer-in-session API. Hides whether you are leader or follower;
|
|
4
|
+
* `send`, `request`, `onMessage`, `rename`, `leave` all work the same.
|
|
5
|
+
*
|
|
6
|
+
* Pending map demuxes parallel `request()` calls by message `id` → `re`.
|
|
7
|
+
*
|
|
8
|
+
* Failover: when the leader dies, follower socket emits `close`. Remaining
|
|
9
|
+
* peers re-run `joinOrLead`. One becomes the new leader; others reconnect.
|
|
10
|
+
*/
|
|
11
|
+
export type MessageHandler = (env: Envelope) => void;
|
|
12
|
+
export type ReconnectHandler = () => void;
|
|
13
|
+
export interface SessionPeerOptions {
|
|
14
|
+
sockPath: string;
|
|
15
|
+
name: string;
|
|
16
|
+
auditPath?: string;
|
|
17
|
+
/** Per-request default timeout (ms). Override per call if needed. */
|
|
18
|
+
defaultTimeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class SessionPeer {
|
|
21
|
+
private readonly opts;
|
|
22
|
+
/** Name actually assigned by the broker (may differ via #N collision suffix). */
|
|
23
|
+
private assignedName;
|
|
24
|
+
private role;
|
|
25
|
+
private broker;
|
|
26
|
+
private socket;
|
|
27
|
+
private buf;
|
|
28
|
+
/** Map of in-flight request ids → resolver. */
|
|
29
|
+
private readonly pending;
|
|
30
|
+
private readonly handlers;
|
|
31
|
+
private readonly reconnectHandlers;
|
|
32
|
+
private leftFlag;
|
|
33
|
+
constructor(opts: SessionPeerOptions);
|
|
34
|
+
/** Joins or leads the session at `sockPath`. Resolves with the assigned name. */
|
|
35
|
+
start(): Promise<string>;
|
|
36
|
+
/** Returns the name as assigned by the broker (after collision suffix). */
|
|
37
|
+
name(): string;
|
|
38
|
+
/** Returns "leader" or "follower" — current role. */
|
|
39
|
+
currentRole(): "leader" | "follower";
|
|
40
|
+
/**
|
|
41
|
+
* Fire-and-forget send. Doesn't await a reply.
|
|
42
|
+
*
|
|
43
|
+
* `re` (optional) lets the caller correlate this message as a reply to a
|
|
44
|
+
* previous request — when an LLM peer is *answering* a question from
|
|
45
|
+
* another agent, it must echo the original `id` here so the requester's
|
|
46
|
+
* pending map can resolve. Without `re`, the requester treats this as a
|
|
47
|
+
* new unsolicited message and its `request()` call times out.
|
|
48
|
+
*/
|
|
49
|
+
send(to: string | string[], body: unknown, re?: string | null): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Send + await reply. Resolves with the first inbound envelope whose `re`
|
|
52
|
+
* matches the outbound `id`. Rejects on timeout.
|
|
53
|
+
*/
|
|
54
|
+
request(to: string, body: unknown, timeoutMs?: number): Promise<Envelope>;
|
|
55
|
+
onMessage(handler: MessageHandler): () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Fires after the peer successfully (re)joins following a failover —
|
|
58
|
+
* leader died and we re-elected. NOT called for the initial `start()`,
|
|
59
|
+
* only for post-drop reconnects. Consumers use this to re-query state
|
|
60
|
+
* the broker may have lost in the transition (e.g., peer list).
|
|
61
|
+
*/
|
|
62
|
+
onReconnect(handler: ReconnectHandler): () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Requests a different display name from the broker. Returns the name
|
|
65
|
+
* actually assigned (may carry a #N suffix on collision). Implemented as
|
|
66
|
+
* a soft rejoin: leaves & rejoins with the new name.
|
|
67
|
+
*/
|
|
68
|
+
rename(newName: string): Promise<string>;
|
|
69
|
+
leave(): Promise<void>;
|
|
70
|
+
private _joinOrLead;
|
|
71
|
+
private _registerAsClient;
|
|
72
|
+
private _wireSocket;
|
|
73
|
+
private _registerOver;
|
|
74
|
+
private _preAckListener;
|
|
75
|
+
private _onData;
|
|
76
|
+
private _handleLine;
|
|
77
|
+
private _writeEnvelope;
|
|
78
|
+
private _onSocketClose;
|
|
79
|
+
private _teardownConn;
|
|
80
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { envelope, parse, serialize, EnvelopeError } from "./envelope.js";
|
|
3
|
+
import { joinOrLead } from "./leader_election.js";
|
|
4
|
+
import { Broker } from "./broker.js";
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
|
+
const FAILOVER_RETRY_MS = 100;
|
|
7
|
+
export class SessionPeer {
|
|
8
|
+
opts;
|
|
9
|
+
/** Name actually assigned by the broker (may differ via #N collision suffix). */
|
|
10
|
+
assignedName;
|
|
11
|
+
role = "follower";
|
|
12
|
+
broker = null;
|
|
13
|
+
socket = null;
|
|
14
|
+
buf = "";
|
|
15
|
+
/** Map of in-flight request ids → resolver. */
|
|
16
|
+
pending = new Map();
|
|
17
|
+
handlers = new Set();
|
|
18
|
+
reconnectHandlers = new Set();
|
|
19
|
+
leftFlag = false;
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.opts = opts;
|
|
22
|
+
this.assignedName = opts.name;
|
|
23
|
+
}
|
|
24
|
+
// ── public API ────────────────────────────────────────────────────────────
|
|
25
|
+
/** Joins or leads the session at `sockPath`. Resolves with the assigned name. */
|
|
26
|
+
async start() {
|
|
27
|
+
return this._joinOrLead();
|
|
28
|
+
}
|
|
29
|
+
/** Returns the name as assigned by the broker (after collision suffix). */
|
|
30
|
+
name() {
|
|
31
|
+
return this.assignedName;
|
|
32
|
+
}
|
|
33
|
+
/** Returns "leader" or "follower" — current role. */
|
|
34
|
+
currentRole() {
|
|
35
|
+
return this.role;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fire-and-forget send. Doesn't await a reply.
|
|
39
|
+
*
|
|
40
|
+
* `re` (optional) lets the caller correlate this message as a reply to a
|
|
41
|
+
* previous request — when an LLM peer is *answering* a question from
|
|
42
|
+
* another agent, it must echo the original `id` here so the requester's
|
|
43
|
+
* pending map can resolve. Without `re`, the requester treats this as a
|
|
44
|
+
* new unsolicited message and its `request()` call times out.
|
|
45
|
+
*/
|
|
46
|
+
async send(to, body, re = null) {
|
|
47
|
+
const env = envelope(this.assignedName, to, body, re);
|
|
48
|
+
await this._writeEnvelope(env);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Send + await reply. Resolves with the first inbound envelope whose `re`
|
|
52
|
+
* matches the outbound `id`. Rejects on timeout.
|
|
53
|
+
*/
|
|
54
|
+
async request(to, body, timeoutMs = this.opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) {
|
|
55
|
+
const env = envelope(this.assignedName, to, body, null);
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
this.pending.delete(env.id);
|
|
59
|
+
reject(new Error(`request to ${to} timed out after ${timeoutMs}ms`));
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
this.pending.set(env.id, { resolve, reject, timer });
|
|
62
|
+
this._writeEnvelope(env).catch((err) => {
|
|
63
|
+
const slot = this.pending.get(env.id);
|
|
64
|
+
if (!slot)
|
|
65
|
+
return;
|
|
66
|
+
clearTimeout(slot.timer);
|
|
67
|
+
this.pending.delete(env.id);
|
|
68
|
+
reject(err);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
onMessage(handler) {
|
|
73
|
+
this.handlers.add(handler);
|
|
74
|
+
return () => this.handlers.delete(handler);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fires after the peer successfully (re)joins following a failover —
|
|
78
|
+
* leader died and we re-elected. NOT called for the initial `start()`,
|
|
79
|
+
* only for post-drop reconnects. Consumers use this to re-query state
|
|
80
|
+
* the broker may have lost in the transition (e.g., peer list).
|
|
81
|
+
*/
|
|
82
|
+
onReconnect(handler) {
|
|
83
|
+
this.reconnectHandlers.add(handler);
|
|
84
|
+
return () => this.reconnectHandlers.delete(handler);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Requests a different display name from the broker. Returns the name
|
|
88
|
+
* actually assigned (may carry a #N suffix on collision). Implemented as
|
|
89
|
+
* a soft rejoin: leaves & rejoins with the new name.
|
|
90
|
+
*/
|
|
91
|
+
async rename(newName) {
|
|
92
|
+
await this._teardownConn();
|
|
93
|
+
this.opts.name = newName;
|
|
94
|
+
this.assignedName = newName;
|
|
95
|
+
return this._joinOrLead();
|
|
96
|
+
}
|
|
97
|
+
async leave() {
|
|
98
|
+
this.leftFlag = true;
|
|
99
|
+
await this._teardownConn();
|
|
100
|
+
}
|
|
101
|
+
// ── join / failover loop ──────────────────────────────────────────────────
|
|
102
|
+
async _joinOrLead() {
|
|
103
|
+
const result = await joinOrLead(this.opts.sockPath);
|
|
104
|
+
if (result.role === "leader") {
|
|
105
|
+
this.role = "leader";
|
|
106
|
+
this.broker = new Broker({
|
|
107
|
+
server: result.server,
|
|
108
|
+
auditPath: this.opts.auditPath,
|
|
109
|
+
});
|
|
110
|
+
// Leader also registers itself as a peer so other followers see it +
|
|
111
|
+
// can address it. We create a self-loopback socket via the broker's
|
|
112
|
+
// internal API: easiest is to open a real client connection back to
|
|
113
|
+
// our own server.
|
|
114
|
+
return this._registerAsClient();
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.role = "follower";
|
|
118
|
+
this._wireSocket(result.socket);
|
|
119
|
+
return this._registerOver(result.socket);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async _registerAsClient() {
|
|
123
|
+
const { createConnection } = await import("node:net");
|
|
124
|
+
const sock = createConnection(this.opts.sockPath);
|
|
125
|
+
await new Promise((resolve, reject) => {
|
|
126
|
+
sock.once("connect", () => resolve());
|
|
127
|
+
sock.once("error", reject);
|
|
128
|
+
});
|
|
129
|
+
this._wireSocket(sock);
|
|
130
|
+
return this._registerOver(sock);
|
|
131
|
+
}
|
|
132
|
+
_wireSocket(sock) {
|
|
133
|
+
this.socket = sock;
|
|
134
|
+
this.buf = "";
|
|
135
|
+
sock.setEncoding("utf8");
|
|
136
|
+
sock.on("data", (chunk) => this._onData(chunk));
|
|
137
|
+
sock.on("close", () => this._onSocketClose());
|
|
138
|
+
sock.on("error", () => { });
|
|
139
|
+
}
|
|
140
|
+
_registerOver(sock) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
// The first inbound line MUST be the register_ack. Buffer-aware.
|
|
143
|
+
const wait = setTimeout(() => reject(new Error("register_ack timeout")), 5_000);
|
|
144
|
+
const onceListener = (raw) => {
|
|
145
|
+
clearTimeout(wait);
|
|
146
|
+
const ack = raw;
|
|
147
|
+
if (ack && ack.type === "register_ack" && typeof ack.name_assigned === "string") {
|
|
148
|
+
this.assignedName = ack.name_assigned;
|
|
149
|
+
this._preAckListener = null;
|
|
150
|
+
resolve(ack.name_assigned);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
reject(new Error(`expected register_ack, got: ${JSON.stringify(raw)}`));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
this._preAckListener = onceListener;
|
|
157
|
+
const req = JSON.stringify({ type: "register", name: this.opts.name }) + "\n";
|
|
158
|
+
try {
|
|
159
|
+
sock.write(req);
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
clearTimeout(wait);
|
|
163
|
+
reject(e);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
_preAckListener = null;
|
|
168
|
+
_onData(chunk) {
|
|
169
|
+
this.buf += chunk;
|
|
170
|
+
let nl;
|
|
171
|
+
while ((nl = this.buf.indexOf("\n")) >= 0) {
|
|
172
|
+
const line = this.buf.slice(0, nl);
|
|
173
|
+
this.buf = this.buf.slice(nl + 1);
|
|
174
|
+
if (!line)
|
|
175
|
+
continue;
|
|
176
|
+
this._handleLine(line);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
_handleLine(line) {
|
|
180
|
+
// Before register_ack: parse loosely as an ack control message.
|
|
181
|
+
if (this._preAckListener) {
|
|
182
|
+
try {
|
|
183
|
+
const parsed = JSON.parse(line);
|
|
184
|
+
this._preAckListener(parsed);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Garbage during register window — ignore.
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Regular envelope.
|
|
192
|
+
let env;
|
|
193
|
+
try {
|
|
194
|
+
env = parse(line);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
if (e instanceof EnvelopeError)
|
|
198
|
+
return;
|
|
199
|
+
throw e;
|
|
200
|
+
}
|
|
201
|
+
// Correlate replies first.
|
|
202
|
+
if (env.re) {
|
|
203
|
+
const slot = this.pending.get(env.re);
|
|
204
|
+
if (slot) {
|
|
205
|
+
clearTimeout(slot.timer);
|
|
206
|
+
this.pending.delete(env.re);
|
|
207
|
+
slot.resolve(env);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Otherwise dispatch to subscribers.
|
|
212
|
+
for (const h of this.handlers) {
|
|
213
|
+
try {
|
|
214
|
+
h(env);
|
|
215
|
+
}
|
|
216
|
+
catch { /* handler errors don't break peer */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async _writeEnvelope(env) {
|
|
220
|
+
if (!this.socket || this.socket.destroyed) {
|
|
221
|
+
throw new Error("session peer not connected");
|
|
222
|
+
}
|
|
223
|
+
this.socket.write(serialize(env));
|
|
224
|
+
}
|
|
225
|
+
async _onSocketClose() {
|
|
226
|
+
if (this.leftFlag)
|
|
227
|
+
return; // intentional leave
|
|
228
|
+
// Attempt to re-elect once. New leader will bind sockPath; we either
|
|
229
|
+
// become leader ourselves or rejoin as follower.
|
|
230
|
+
await delay(FAILOVER_RETRY_MS);
|
|
231
|
+
if (this.leftFlag)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
await this._joinOrLead();
|
|
235
|
+
// The new broker's peers map starts fresh — consumers must re-query
|
|
236
|
+
// any cached state (peer count, etc.) that depended on the old broker.
|
|
237
|
+
for (const h of this.reconnectHandlers) {
|
|
238
|
+
try {
|
|
239
|
+
h();
|
|
240
|
+
}
|
|
241
|
+
catch { /* handler errors don't break peer */ }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch { /* election failed; peer stuck in disconnected state */ }
|
|
245
|
+
}
|
|
246
|
+
async _teardownConn() {
|
|
247
|
+
if (this.socket) {
|
|
248
|
+
try {
|
|
249
|
+
this.socket.destroy();
|
|
250
|
+
}
|
|
251
|
+
catch { /* ignored */ }
|
|
252
|
+
this.socket = null;
|
|
253
|
+
}
|
|
254
|
+
if (this.broker) {
|
|
255
|
+
try {
|
|
256
|
+
await this.broker.close();
|
|
257
|
+
}
|
|
258
|
+
catch { /* ignored */ }
|
|
259
|
+
this.broker = null;
|
|
260
|
+
}
|
|
261
|
+
for (const slot of this.pending.values()) {
|
|
262
|
+
clearTimeout(slot.timer);
|
|
263
|
+
slot.reject(new Error("peer leaving"));
|
|
264
|
+
}
|
|
265
|
+
this.pending.clear();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=peer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer.js","sourceRoot":"","sources":["../../src/session/peer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LocalConfig } from "./local_config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pi SDK UI surface needed by the wizard. Subset of `ExtensionUIContext` —
|
|
4
|
+
* declared inline so tests can mock cleanly without dragging the full
|
|
5
|
+
* ExtensionContext shape.
|
|
6
|
+
*/
|
|
7
|
+
export interface WizardUI {
|
|
8
|
+
/** Free-text prompt. Returns the entered string, or undefined if cancelled. */
|
|
9
|
+
input?: (title: string, options?: {
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
}) => Promise<string | undefined>;
|
|
12
|
+
/** Picker. Returns the picked option, or undefined if cancelled. */
|
|
13
|
+
select: (title: string, options: string[]) => Promise<string | undefined>;
|
|
14
|
+
/** Non-blocking notification. Used for inline validation feedback. */
|
|
15
|
+
notify?: (msg: string, kind: "info" | "warning" | "error") => void;
|
|
16
|
+
}
|
|
17
|
+
export interface WizardDefaults {
|
|
18
|
+
agent_name: string;
|
|
19
|
+
session_name: string;
|
|
20
|
+
auto_start_relay: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs the 3-question setup wizard. Returns the chosen config on confirm,
|
|
24
|
+
* or null when the user cancels any prompt.
|
|
25
|
+
*
|
|
26
|
+
* Prompts:
|
|
27
|
+
* 1. Agent name (default: basename of cwd)
|
|
28
|
+
* 2. Session name (default: basename of cwd)
|
|
29
|
+
* 3. Auto-start relay? (yes/no) — relay lets the mobile app connect to this Pi
|
|
30
|
+
* Final: review + confirm "Save and activate?" yes/no
|
|
31
|
+
*/
|
|
32
|
+
export declare function runSetupWizard(ui: WizardUI, defaults: WizardDefaults): Promise<LocalConfig | null>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const YES = "Yes";
|
|
2
|
+
const NO = "No";
|
|
3
|
+
const CANCEL_TOKEN = "__cancel__";
|
|
4
|
+
/**
|
|
5
|
+
* Runs the 3-question setup wizard. Returns the chosen config on confirm,
|
|
6
|
+
* or null when the user cancels any prompt.
|
|
7
|
+
*
|
|
8
|
+
* Prompts:
|
|
9
|
+
* 1. Agent name (default: basename of cwd)
|
|
10
|
+
* 2. Session name (default: basename of cwd)
|
|
11
|
+
* 3. Auto-start relay? (yes/no) — relay lets the mobile app connect to this Pi
|
|
12
|
+
* Final: review + confirm "Save and activate?" yes/no
|
|
13
|
+
*/
|
|
14
|
+
export async function runSetupWizard(ui, defaults) {
|
|
15
|
+
const agent_name = await _askText(ui, "Agent name:", defaults.agent_name);
|
|
16
|
+
if (agent_name === null)
|
|
17
|
+
return null;
|
|
18
|
+
const session_name = await _askText(ui, "Default session:", defaults.session_name);
|
|
19
|
+
if (session_name === null)
|
|
20
|
+
return null;
|
|
21
|
+
ui.notify?.("The relay lets the Remote Pi mobile app connect to this Pi over the network. Enable it to allow the app to send prompts and receive responses; disable for local-only use.", "info");
|
|
22
|
+
const autoChoice = await ui.select("Auto-start the relay (for mobile app access)?", defaults.auto_start_relay ? [YES, NO] : [NO, YES]);
|
|
23
|
+
if (!autoChoice)
|
|
24
|
+
return null;
|
|
25
|
+
const auto_start_relay = autoChoice === YES;
|
|
26
|
+
// Review + confirm
|
|
27
|
+
const summary = [
|
|
28
|
+
` Agent name: ${agent_name}`,
|
|
29
|
+
` Default session: ${session_name}`,
|
|
30
|
+
` Auto-start relay: ${auto_start_relay ? YES : NO}`,
|
|
31
|
+
].join("\n");
|
|
32
|
+
ui.notify?.(`Summary:\n${summary}`, "info");
|
|
33
|
+
const confirm = await ui.select("Save and activate?", [YES, NO]);
|
|
34
|
+
if (confirm !== YES)
|
|
35
|
+
return null;
|
|
36
|
+
return { agent_name, session_name, auto_start_relay };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Asks the user for free text. Falls back to `select` with the default as the
|
|
40
|
+
* only option when the SDK doesn't expose `input` (some Pi modes). Re-prompts
|
|
41
|
+
* once on empty/whitespace input; second blank counts as cancel.
|
|
42
|
+
*/
|
|
43
|
+
async function _askText(ui, title, defaultValue) {
|
|
44
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
45
|
+
const raw = ui.input
|
|
46
|
+
? await ui.input(title, { defaultValue })
|
|
47
|
+
: await ui.select(`${title} (default: ${defaultValue})`, [defaultValue, CANCEL_TOKEN]);
|
|
48
|
+
if (raw === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
if (raw === CANCEL_TOKEN)
|
|
51
|
+
return null;
|
|
52
|
+
const trimmed = raw.trim();
|
|
53
|
+
if (trimmed.length > 0)
|
|
54
|
+
return trimmed;
|
|
55
|
+
ui.notify?.("Value required — cannot be empty.", "warning");
|
|
56
|
+
}
|
|
57
|
+
// 2 blanks in a row → treat as cancel
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=setup_wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup_wizard.js","sourceRoot":"","sources":["../../src/session/setup_wizard.ts"],"names":[],"mappings":"AAsBA,MAAM,GAAG,GAAG,KAAK,CAAC;AAClB,MAAM,EAAE,GAAG,IAAI,CAAC;AAChB,MAAM,YAAY,GAAG,YAAY,CAAC;AAElC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAY,EACZ,QAAwB;IAExB,MAAM,UAAU,GAAG,MAAM,QAAQ,CAC/B,EAAE,EACF,aAAa,EACb,QAAQ,CAAC,UAAU,CACpB,CAAC;IACF,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,YAAY,GAAG,MAAM,QAAQ,CACjC,EAAE,EACF,kBAAkB,EAClB,QAAQ,CAAC,YAAY,CACtB,CAAC;IACF,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvC,EAAE,CAAC,MAAM,EAAE,CACT,4KAA4K,EAC5K,MAAM,CACP,CAAC;IACF,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,MAAM,CAChC,+CAA+C,EAC/C,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAClD,CAAC;IACF,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,gBAAgB,GAAG,UAAU,KAAK,GAAG,CAAC;IAE5C,mBAAmB;IACnB,MAAM,OAAO,GAAG;QACd,uBAAuB,UAAU,EAAE;QACnC,uBAAuB,YAAY,EAAE;QACrC,uBAAuB,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;KACrD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,EAAE,CAAC,MAAM,EAAE,CAAC,aAAa,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACjE,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAEjC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,QAAQ,CACrB,EAAY,EACZ,KAAa,EACb,YAAoB;IAEpB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK;YAClB,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,YAAY,EAAE,CAAC;YACzC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,KAAK,cAAc,YAAY,GAAG,EAAE,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;QACzF,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACnC,IAAI,GAAG,KAAK,YAAY;YAAE,OAAO,IAAI,CAAC;QACtC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC;QACvC,EAAE,CAAC,MAAM,EAAE,CAAC,mCAAmC,EAAE,SAAS,CAAC,CAAC;IAC9D,CAAC;IACD,sCAAsC;IACtC,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Tool names that execute automatically without prompting the user. */
|
|
2
|
+
export declare const AUTO_APPROVE_TOOLS: ReadonlySet<string>;
|
|
3
|
+
export type ApprovalDecision = "auto" | "ask";
|
|
4
|
+
/** Returns 'auto' for read-only tools, 'ask' for everything else. */
|
|
5
|
+
export declare function decide(toolName: string): ApprovalDecision;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Tool names that execute automatically without prompting the user. */
|
|
2
|
+
export const AUTO_APPROVE_TOOLS = new Set([
|
|
3
|
+
"Read",
|
|
4
|
+
"Glob",
|
|
5
|
+
"Grep",
|
|
6
|
+
]);
|
|
7
|
+
/** Returns 'auto' for read-only tools, 'ask' for everything else. */
|
|
8
|
+
export function decide(toolName) {
|
|
9
|
+
return AUTO_APPROVE_TOOLS.has(toolName) ? "auto" : "ask";
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=tool_gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool_gate.js","sourceRoot":"","sources":["../../src/session/tool_gate.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,CAAC,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC;IAC7D,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC,CAAC;AAIH,qEAAqE;AACrE,MAAM,UAAU,MAAM,CAAC,QAAgB;IACrC,OAAO,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { SessionPeer } from "./peer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers two native tools the Pi LLM can invoke to talk to other agents
|
|
5
|
+
* in the same UDS session (plano 19 transport):
|
|
6
|
+
* - `agent_send` — fire-and-forget delivery
|
|
7
|
+
* - `agent_request` — request/reply with `re` correlation + timeout
|
|
8
|
+
*
|
|
9
|
+
* Both tools are registered unconditionally. When this Pi is not currently
|
|
10
|
+
* joined to a session, the tool returns a structured error string instead
|
|
11
|
+
* of throwing, so the LLM can react and tell the user.
|
|
12
|
+
*
|
|
13
|
+
* `getSessionPeer` is a getter (not a captured value) so changes to the
|
|
14
|
+
* underlying `_sessionPeer` module variable are observed live.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerAgentTools(pi: ExtensionAPI, getSessionPeer: () => SessionPeer | null): void;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
const NOT_IN_SESSION = "Not in a session. Run /remote-pi join first";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
4
|
+
/**
|
|
5
|
+
* Registers two native tools the Pi LLM can invoke to talk to other agents
|
|
6
|
+
* in the same UDS session (plano 19 transport):
|
|
7
|
+
* - `agent_send` — fire-and-forget delivery
|
|
8
|
+
* - `agent_request` — request/reply with `re` correlation + timeout
|
|
9
|
+
*
|
|
10
|
+
* Both tools are registered unconditionally. When this Pi is not currently
|
|
11
|
+
* joined to a session, the tool returns a structured error string instead
|
|
12
|
+
* of throwing, so the LLM can react and tell the user.
|
|
13
|
+
*
|
|
14
|
+
* `getSessionPeer` is a getter (not a captured value) so changes to the
|
|
15
|
+
* underlying `_sessionPeer` module variable are observed live.
|
|
16
|
+
*/
|
|
17
|
+
export function registerAgentTools(pi, getSessionPeer) {
|
|
18
|
+
const SendParams = Type.Object({
|
|
19
|
+
to: Type.String({ description: "Recipient agent name (e.g. 'backend'), 'broadcast', or array of names." }),
|
|
20
|
+
body: Type.Unknown({ description: "Free-form JSON payload. String or object — your choice." }),
|
|
21
|
+
re: Type.Optional(Type.String({
|
|
22
|
+
description: "When you are REPLYING to a message from another agent, set this to that " +
|
|
23
|
+
"message's `id`. Required for `agent_request` correlation on the other side — " +
|
|
24
|
+
"without it, the sender's pending request times out. Skip for unsolicited sends.",
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
const RequestParams = Type.Object({
|
|
28
|
+
to: Type.String({ description: "Recipient agent name. Must be a single peer (not broadcast)." }),
|
|
29
|
+
body: Type.Unknown({ description: "Free-form JSON payload to send." }),
|
|
30
|
+
timeout_ms: Type.Optional(Type.Number({
|
|
31
|
+
description: "Optional override of the default 30s reply timeout. Per-request.",
|
|
32
|
+
})),
|
|
33
|
+
});
|
|
34
|
+
pi.registerTool({
|
|
35
|
+
name: "agent_send",
|
|
36
|
+
label: "Agent Send",
|
|
37
|
+
description: "Send a message to another Pi agent in the current local session. " +
|
|
38
|
+
"Two uses: (1) fire-and-forget announcements, status updates, or anything " +
|
|
39
|
+
"that doesn't need a reply — call without `re`. (2) REPLYING to a question " +
|
|
40
|
+
"another agent sent you via `agent_request` — call with `re` set to the " +
|
|
41
|
+
"incoming message's `id`. Without `re`, the requester times out.",
|
|
42
|
+
promptSnippet: "agent_send({to, body, re?}): send/reply to peers. Set `re` to incoming message id when answering an agent_request.",
|
|
43
|
+
parameters: SendParams,
|
|
44
|
+
execute: async (_toolCallId, params) => {
|
|
45
|
+
const peer = getSessionPeer();
|
|
46
|
+
if (!peer) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: NOT_IN_SESSION }],
|
|
49
|
+
details: { ok: false, error: NOT_IN_SESSION },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const { to, body, re } = params;
|
|
53
|
+
if (to === peer.name()) {
|
|
54
|
+
const msg = `Refused: cannot agent_send to yourself ("${to}"). Just do the work directly.`;
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: msg }],
|
|
57
|
+
details: { ok: false, error: msg },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await peer.send(to, body, re ?? null);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `Sent to ${to}${re ? ` (re=${re})` : ""}.` }],
|
|
64
|
+
details: { ok: true },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: `Failed to send: ${msg}` }],
|
|
71
|
+
details: { ok: false, error: msg },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
pi.registerTool({
|
|
77
|
+
name: "agent_request",
|
|
78
|
+
label: "Agent Request",
|
|
79
|
+
description: "Send a message to another Pi agent and wait for the reply. Resolves to the responder's " +
|
|
80
|
+
"body field. Use when you need an answer from a peer before continuing your turn. " +
|
|
81
|
+
"Default timeout 30s; override with `timeout_ms`.",
|
|
82
|
+
promptSnippet: "agent_request({to, body, timeout_ms?}): synchronous request/reply with a peer (returns peer's body or {error}).",
|
|
83
|
+
parameters: RequestParams,
|
|
84
|
+
execute: async (_toolCallId, params) => {
|
|
85
|
+
const peer = getSessionPeer();
|
|
86
|
+
if (!peer) {
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: NOT_IN_SESSION }],
|
|
89
|
+
details: { error: NOT_IN_SESSION },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const { to, body, timeout_ms } = params;
|
|
93
|
+
if (to === peer.name()) {
|
|
94
|
+
const msg = `Refused: cannot agent_request to yourself ("${to}"). Just do the work directly.`;
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: msg }],
|
|
97
|
+
details: { error: msg },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const timeout = typeof timeout_ms === "number" && timeout_ms > 0
|
|
101
|
+
? timeout_ms
|
|
102
|
+
: DEFAULT_TIMEOUT_MS;
|
|
103
|
+
try {
|
|
104
|
+
const reply = await peer.request(to, body, timeout);
|
|
105
|
+
const text = typeof reply.body === "string"
|
|
106
|
+
? reply.body
|
|
107
|
+
: JSON.stringify(reply.body);
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text }],
|
|
110
|
+
details: reply.body,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: `Request failed: ${msg}` }],
|
|
117
|
+
details: { error: msg },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/session/tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal wizard for `/remote-pi join` with no argument.
|
|
3
|
+
*
|
|
4
|
+
* The Pi SDK `ExtensionUIContext.select` accepts a title and string options
|
|
5
|
+
* and returns the chosen option. Wizard offers existing sessions + an "explicit
|
|
6
|
+
* create" sentinel. If user picks the sentinel, prompts for a fresh name via
|
|
7
|
+
* `select`-with-option-other (Pi's "Other" escape hatch). Returns the picked
|
|
8
|
+
* session name, or null if user cancelled.
|
|
9
|
+
*/
|
|
10
|
+
export interface WizardUI {
|
|
11
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
export declare function joinWizard(ui: WizardUI, defaultName: string): Promise<string | null>;
|