@uuaid/pillar 0.2.0-alpha.6 → 0.2.0-alpha.8
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/bin/pillar.mjs +133 -32
- package/package.json +2 -2
- package/src/bridges/zestro.mjs +89 -17
package/bin/pillar.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* pillar — one-command CLI.
|
|
4
4
|
*
|
|
5
5
|
* pillar init [--home ~/.pillar] first-time setup: mint UUAID + keys
|
|
6
|
+
* pillar init --refresh [--directory URL] re-sync carriers + address book (keychain untouched)
|
|
6
7
|
* pillar id [--home ~/.pillar] print our UUAID + PeerId + addrs
|
|
7
8
|
* pillar send <recipient> <kind> <json> send a signed envelope
|
|
8
9
|
* pillar recv [--home ~/.pillar] [--follow] listen for envelopes and print them
|
|
@@ -89,9 +90,77 @@ function writeRuntimeFile(pillar, home) {
|
|
|
89
90
|
writeFileSync(join(home, "runtime.json"), JSON.stringify(rt, null, 2));
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// Carrier list + address book merge from a fetched directory — shared by
|
|
94
|
+
// `init --directory` (first contact) and `init --refresh` (re-sync without
|
|
95
|
+
// touching the keychain).
|
|
96
|
+
function carriersFromDirectory(d) {
|
|
97
|
+
return [d.carrier, ...(Array.isArray(d.seeds) ? d.seeds.map((s) => s.url) : [])].filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mergeDirectoryAgents(home, d) {
|
|
101
|
+
if (!Array.isArray(d.agents)) return null;
|
|
102
|
+
const fleetPath = join(home, "fleet.json");
|
|
103
|
+
const prev = existsSync(fleetPath) ? JSON.parse(readFileSync(fleetPath, "utf-8")).fleet ?? {} : {};
|
|
104
|
+
const fleet = { ...prev };
|
|
105
|
+
let added = 0;
|
|
106
|
+
for (const a of d.agents) {
|
|
107
|
+
if (!a.uuaid || !a.publicKey) continue;
|
|
108
|
+
const key = typeof a.role === "string" && a.role ? a.role : a.uuaid;
|
|
109
|
+
fleet[key] = {
|
|
110
|
+
uuaid: a.uuaid,
|
|
111
|
+
publicKey: a.publicKey,
|
|
112
|
+
...(prev[key]?.addrs ? { addrs: prev[key].addrs } : {}),
|
|
113
|
+
...(prev[key]?.peerId ? { peerId: prev[key].peerId } : {}),
|
|
114
|
+
};
|
|
115
|
+
if (!prev[key]) added++;
|
|
116
|
+
}
|
|
117
|
+
writeFileSync(fleetPath, JSON.stringify({ fleet }, null, 2));
|
|
118
|
+
return { count: Object.keys(fleet).length, added };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function fetchDirectory(url) {
|
|
122
|
+
const res = await fetch(url);
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
console.error(`directory fetch failed: ${url} -> ${res.status}`);
|
|
125
|
+
exit(1);
|
|
126
|
+
}
|
|
127
|
+
return res.json();
|
|
128
|
+
}
|
|
129
|
+
|
|
92
130
|
async function cmdInit(args) {
|
|
93
131
|
const home = homePath(args);
|
|
94
132
|
mkdirSync(home, { recursive: true });
|
|
133
|
+
|
|
134
|
+
// --refresh re-syncs carriers + address book from a directory WITHOUT
|
|
135
|
+
// touching the keychain (init --force would mint a NEW identity and break
|
|
136
|
+
// every enrolled sender). Requires an existing identity.
|
|
137
|
+
if (args.refresh) {
|
|
138
|
+
const kcPath = join(home, "keychain.json");
|
|
139
|
+
if (!existsSync(kcPath)) {
|
|
140
|
+
console.error(`refresh: no keychain at ${kcPath} — run pillar init first (refresh never mints keys)`);
|
|
141
|
+
exit(1);
|
|
142
|
+
}
|
|
143
|
+
if (typeof args.directory !== "string") {
|
|
144
|
+
console.error("refresh: --directory <url> is required");
|
|
145
|
+
exit(1);
|
|
146
|
+
}
|
|
147
|
+
const d = await fetchDirectory(args.directory);
|
|
148
|
+
const list = carriersFromDirectory(d);
|
|
149
|
+
if (!list.length) {
|
|
150
|
+
console.error(`refresh: directory ${args.directory} lists no carrier and no seeds`);
|
|
151
|
+
exit(1);
|
|
152
|
+
}
|
|
153
|
+
const cfgPath = join(home, "config.json");
|
|
154
|
+
const cfg = existsSync(cfgPath) ? JSON.parse(readFileSync(cfgPath, "utf-8")) : {};
|
|
155
|
+
cfg.carriers = list;
|
|
156
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
157
|
+
console.log(`carriers refreshed from ${args.directory}: ${list.join(", ")}`);
|
|
158
|
+
const ab = mergeDirectoryAgents(home, d);
|
|
159
|
+
if (ab) console.log(`address book: ${ab.count} peers in ${join(home, "fleet.json")} (${ab.added} new from directory)`);
|
|
160
|
+
console.log(`refresh complete at ${home} (keychain untouched)`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
95
164
|
const kcPath = join(home, "keychain.json");
|
|
96
165
|
if (existsSync(kcPath) && !args.force) {
|
|
97
166
|
console.error(`keychain already exists at ${kcPath}. use --force to overwrite.`);
|
|
@@ -110,13 +179,8 @@ async function cmdInit(args) {
|
|
|
110
179
|
// the primary carrier plus every seeds[] entry as carriers (the LimeWire
|
|
111
180
|
// seed model — poll all, replicate to all); --carriers sets them directly.
|
|
112
181
|
if (typeof args.directory === "string") {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
console.error(`init: directory fetch failed: ${args.directory} -> ${res.status}`);
|
|
116
|
-
exit(1);
|
|
117
|
-
}
|
|
118
|
-
const d = await res.json();
|
|
119
|
-
const list = [d.carrier, ...(Array.isArray(d.seeds) ? d.seeds.map(s => s.url) : [])].filter(Boolean);
|
|
182
|
+
const d = await fetchDirectory(args.directory);
|
|
183
|
+
const list = carriersFromDirectory(d);
|
|
120
184
|
if (!list.length) {
|
|
121
185
|
console.error(`init: directory ${args.directory} lists no carrier and no seeds`);
|
|
122
186
|
exit(1);
|
|
@@ -126,25 +190,8 @@ async function cmdInit(args) {
|
|
|
126
190
|
// Seed the address book from the directory's agents[] so a fresh agent can
|
|
127
191
|
// `pillar ask`/`pillar send` with zero key plumbing (keys required for E2E).
|
|
128
192
|
// Merge — never clobber a hand-curated fleet.json.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const prev = existsSync(fleetPath) ? JSON.parse(readFileSync(fleetPath, "utf-8")).fleet ?? {} : {};
|
|
132
|
-
const fleet = { ...prev };
|
|
133
|
-
let added = 0;
|
|
134
|
-
for (const a of d.agents) {
|
|
135
|
-
if (!a.uuaid || !a.publicKey) continue;
|
|
136
|
-
const key = typeof a.role === "string" && a.role ? a.role : a.uuaid;
|
|
137
|
-
fleet[key] = {
|
|
138
|
-
uuaid: a.uuaid,
|
|
139
|
-
publicKey: a.publicKey,
|
|
140
|
-
...(prev[key]?.addrs ? { addrs: prev[key].addrs } : {}),
|
|
141
|
-
...(prev[key]?.peerId ? { peerId: prev[key].peerId } : {}),
|
|
142
|
-
};
|
|
143
|
-
if (!prev[key]) added++;
|
|
144
|
-
}
|
|
145
|
-
writeFileSync(fleetPath, JSON.stringify({ fleet }, null, 2));
|
|
146
|
-
console.log(`address book: ${Object.keys(fleet).length} peers in ${fleetPath} (${added} new from directory)`);
|
|
147
|
-
}
|
|
193
|
+
const ab = mergeDirectoryAgents(home, d);
|
|
194
|
+
if (ab) console.log(`address book: ${ab.count} peers in ${join(home, "fleet.json")} (${ab.added} new from directory)`);
|
|
148
195
|
} else if (args.carriers) {
|
|
149
196
|
cfg.carriers = args.carriers.split(",").map(s => s.trim()).filter(Boolean);
|
|
150
197
|
}
|
|
@@ -238,6 +285,19 @@ async function cmdGroups(args) {
|
|
|
238
285
|
}
|
|
239
286
|
}
|
|
240
287
|
|
|
288
|
+
// Extract human-readable text from a zestro command_result. The server-side
|
|
289
|
+
// payload fields are unverified — defensively try the common shapes, cap at
|
|
290
|
+
// 8192 bytes (pillar-responder's MAX_REPLY_BYTES convention).
|
|
291
|
+
function replyTextFromResult(result) {
|
|
292
|
+
const text = result?.text ?? result?.result ?? result?.output ?? result?.response ?? result?.message;
|
|
293
|
+
if (typeof text === "string" && text.trim()) return text.slice(0, 8192);
|
|
294
|
+
if (typeof text !== "string" && text !== undefined && text !== null) {
|
|
295
|
+
const s = JSON.stringify(text);
|
|
296
|
+
if (s) return s.slice(0, 8192);
|
|
297
|
+
}
|
|
298
|
+
return JSON.stringify(result ?? {}).slice(0, 8192);
|
|
299
|
+
}
|
|
300
|
+
|
|
241
301
|
async function cmdRecv(args) {
|
|
242
302
|
const p = loadPillar(args);
|
|
243
303
|
await p.start();
|
|
@@ -263,6 +323,10 @@ async function cmdRecv(args) {
|
|
|
263
323
|
url: process.env.PILLAR_ZESTRO_URL ?? args["zestro-url"],
|
|
264
324
|
token: process.env.PILLAR_ZESTRO_TOKEN ?? args["zestro-token"],
|
|
265
325
|
sessionId: process.env.PILLAR_ZESTRO_AGENT ?? args["zestro-agent"],
|
|
326
|
+
tabId: process.env.PILLAR_ZESTRO_TAB ?? args["zestro-tab"],
|
|
327
|
+
// Opt-in: force on a busy tab spawns a SECOND agent process resuming the
|
|
328
|
+
// same transcript (handle-spawn.js has no live-process guard). Default off.
|
|
329
|
+
force: /^1|true|yes$/i.test(process.env.PILLAR_ZESTRO_FORCE ?? "") || !!args["zestro-force"],
|
|
266
330
|
});
|
|
267
331
|
await bridge.connect();
|
|
268
332
|
// Never log the full URL — it embeds the auth token.
|
|
@@ -273,16 +337,53 @@ async function cmdRecv(args) {
|
|
|
273
337
|
const line = { at: new Date().toISOString(), from: env.sender, kind: env.kind, payload: env.payload };
|
|
274
338
|
if (env.payload?._group?.name) line.group = env.payload._group.name;
|
|
275
339
|
console.log(JSON.stringify(line));
|
|
276
|
-
if (bridge) {
|
|
340
|
+
if (bridge && env.kind === "request") {
|
|
341
|
+
// Interaction-convention return leg: ack FIRST — pillar-ask allows 120s
|
|
342
|
+
// per phase but ZestroBridge times out at 30s, so the ack goes out before
|
|
343
|
+
// the bridged agent turn starts. Reply statuses (verified against app code):
|
|
344
|
+
// "ok" = command_result received = renderer consumed / spawn initiated
|
|
345
|
+
// (NOT turn-complete — the turn's answer returns via a separate
|
|
346
|
+
// pillar send from the agent, if the prompt instructs one)
|
|
347
|
+
// "busy" = target tab busy, server rejected (fast, no 30s hang)
|
|
348
|
+
// "pending" = bridge timeout — the request was never confirmed
|
|
349
|
+
// "error" = bridge failure
|
|
350
|
+
const thread = env.payload?.thread ?? env.id;
|
|
351
|
+
const senderKey = env.transportSignature.publicKey; // open() F1: sender derives from this key
|
|
352
|
+
try {
|
|
353
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "ack", payload: { inReplyTo: env.id } });
|
|
354
|
+
const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
|
|
355
|
+
const hasText = result && typeof result === "object" &&
|
|
356
|
+
["text", "result", "output", "response", "message"].some(k => k in result);
|
|
357
|
+
const text = hasText
|
|
358
|
+
? replyTextFromResult(result)
|
|
359
|
+
: "zestro command_result received (server accepted) — agent turn answer, if any, returns via separate pillar send";
|
|
360
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "ok", text } });
|
|
361
|
+
console.log(` → zestro reply sent (${text.length} bytes)`);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// ZestroServerError SESSION_BUSY = server rejected: target tab busy and
|
|
364
|
+
// force off. Immediate distinct status — the sender can retry or drop.
|
|
365
|
+
if (e.code === "SESSION_BUSY") {
|
|
366
|
+
console.log(` → zestro: busy (${e.message})`);
|
|
367
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "busy", reason: e.message.slice(0, 300), text: `zestro agent busy: ${e.message.slice(0, 300)}` } });
|
|
368
|
+
} else if (/timed out/.test(e.message)) {
|
|
369
|
+
// Bridge timeout = the server never confirmed delivery (renderer drop,
|
|
370
|
+
// cli-server gone, etc.) — accepted-unconfirmed, not turn-running.
|
|
371
|
+
console.log(` → zestro: unconfirmed (bridge timeout)`);
|
|
372
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "pending", text: "request forwarded to zestro agent — delivery unconfirmed" } });
|
|
373
|
+
} else {
|
|
374
|
+
console.error(` → zestro bridge error: ${e.message}`);
|
|
375
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "error", reason: e.message.slice(0, 300), text: `zestro bridge error: ${e.message.slice(0, 300)}` } });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} else if (bridge) {
|
|
277
379
|
try {
|
|
278
380
|
const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
|
|
279
381
|
console.log(` → zestro result: ${JSON.stringify(result).slice(0, 200)}`);
|
|
280
382
|
} catch (e) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
console.log(` → zestro: queued (command_result pending — agent busy)`);
|
|
383
|
+
if (e.code === "SESSION_BUSY") {
|
|
384
|
+
console.log(` → zestro: busy (${e.message})`);
|
|
385
|
+
} else if (/timed out/.test(e.message)) {
|
|
386
|
+
console.log(` → zestro: unconfirmed (bridge timeout)`);
|
|
286
387
|
} else {
|
|
287
388
|
console.error(` → zestro bridge error: ${e.message}`);
|
|
288
389
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uuaid/pillar",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.8",
|
|
4
4
|
"description": "IAASO-3050 reference implementation. Every agent runs its own PDP pillar, discovers peers via a Kademlia DHT, and exchanges cryptographically signed + Noise-encrypted messages over libp2p. One command to install and join the network.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -56,4 +56,4 @@
|
|
|
56
56
|
"uint8arrays": "^5.1.1",
|
|
57
57
|
"ws": "^8.21.3"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|
package/src/bridges/zestro.mjs
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zestro WebSocket bridge.
|
|
3
3
|
*
|
|
4
|
-
* Wire protocol
|
|
4
|
+
* Wire protocol — verified against the OFFICIAL client contract
|
|
5
|
+
* (Zestro app.asar: dist/cli/maestro-client.ts, sourcesContent from
|
|
6
|
+
* maestro-cli.js.map) and the app's own server/renderer handlers:
|
|
5
7
|
* ws://127.0.0.1:53522/<token>/ws
|
|
6
|
-
* -> { type: "send_command",
|
|
7
|
-
*
|
|
8
|
+
* -> { type: "send_command", requestId: "req_...", sessionId: <agentId>,
|
|
9
|
+
* command: <str>, inputMode: "ai", tabId?: <uuid>, force?: true }
|
|
10
|
+
* <- { type: "command_result", success, sessionId, tabId?, requestId } // echoes requestId
|
|
11
|
+
* <- { type: "error", message, sessionId } // NO requestId echo
|
|
12
|
+
*
|
|
13
|
+
* Correlation is on `requestId`, NOT `id` — the server echoes only
|
|
14
|
+
* `requestId`, so a client correlating on `id` can never match a reply.
|
|
15
|
+
*
|
|
16
|
+
* Semantics (verified in app code, all four layers):
|
|
17
|
+
* - command_result arrives IMMEDIATELY after the renderer receives the
|
|
18
|
+
* command — it means "consumed by the renderer / spawn initiated", NOT
|
|
19
|
+
* "the agent's turn completed". The turn answer returns out-of-band.
|
|
20
|
+
* - A busy target session WITHOUT force is rejected by the server
|
|
21
|
+
* ({type:"error"}), and the renderer has TWO more busy drops of its own.
|
|
22
|
+
* Errors carry no requestId, so they must be handled single-flight.
|
|
23
|
+
* - force=true on a BUSY tab spawns a SECOND agent process resuming the
|
|
24
|
+
* same session transcript (handle-spawn.js: no live-process guard,
|
|
25
|
+
* --resume <agentSessionId>). Concurrent, not queued. Opt-in only.
|
|
8
26
|
*
|
|
9
27
|
* Token lives in ~/Library/Application Support/zestro/cli-server.json.
|
|
10
28
|
* Agent id for ZZ was verified to be 7fbce535-3a2f-4e6b-96e6-64926403efa5.
|
|
11
|
-
*
|
|
12
|
-
* Correlation on `id` so multiple concurrent sends don't race.
|
|
13
29
|
*/
|
|
14
30
|
import { WebSocket } from "ws";
|
|
15
|
-
|
|
31
|
+
|
|
32
|
+
/** Server rejected the command (busy session, bad payload, ...). */
|
|
33
|
+
export class ZestroServerError extends Error {
|
|
34
|
+
constructor(message, code = "ZESTRO_SERVER_ERROR") {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "ZestroServerError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
16
40
|
|
|
17
41
|
export class ZestroBridge {
|
|
18
|
-
constructor({ url, token, sessionId, timeoutMs = 30000 }) {
|
|
42
|
+
constructor({ url, token, sessionId, tabId, timeoutMs = 30000, force = false }) {
|
|
19
43
|
if (!url || !token || !sessionId) {
|
|
20
44
|
throw new Error("ZestroBridge requires { url, token, sessionId }");
|
|
21
45
|
}
|
|
@@ -26,7 +50,9 @@ export class ZestroBridge {
|
|
|
26
50
|
}
|
|
27
51
|
this.url = url;
|
|
28
52
|
this.sessionId = sessionId;
|
|
53
|
+
this.tabId = tabId ?? null;
|
|
29
54
|
this.timeoutMs = timeoutMs;
|
|
55
|
+
this.force = force;
|
|
30
56
|
this._ws = null;
|
|
31
57
|
this._pending = new Map();
|
|
32
58
|
}
|
|
@@ -44,35 +70,81 @@ export class ZestroBridge {
|
|
|
44
70
|
ws.on("message", (buf) => {
|
|
45
71
|
let msg;
|
|
46
72
|
try { msg = JSON.parse(buf.toString()); } catch { return; }
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
|
|
74
|
+
// `echo` = the app did not recognize the message type (older build).
|
|
75
|
+
if (msg.type === "echo") {
|
|
76
|
+
const original = msg.data ?? {};
|
|
77
|
+
const reqId = original.requestId ?? msg.originalRequestId;
|
|
78
|
+
if (reqId && this._pending.has(reqId)) {
|
|
79
|
+
this._settle(reqId, p => p.reject(new ZestroServerError(
|
|
80
|
+
`Zestro app does not support '${original.type ?? "unknown"}' (older build?)`,
|
|
81
|
+
"ZESTRO_UNSUPPORTED"
|
|
82
|
+
)));
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Exact correlation: server echoes requestId on command_result.
|
|
88
|
+
if (msg.requestId && this._pending.has(msg.requestId)) {
|
|
89
|
+
this._settle(msg.requestId, p => p.resolve(msg));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Single-flight: server `error` replies carry NO requestId (verified:
|
|
94
|
+
// sendError sends {type:'error', message, ...extra}). The bridge keeps
|
|
95
|
+
// at most one request in flight, so route orphan replies to it.
|
|
96
|
+
if (msg.type === "error") {
|
|
97
|
+
const message = msg.message ?? "zestro server error";
|
|
98
|
+
const code = /busy/i.test(message) ? "SESSION_BUSY" : "ZESTRO_SERVER_ERROR";
|
|
99
|
+
this._settleOldest(p => p.reject(new ZestroServerError(message, code)));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (msg.type === "command_result") {
|
|
103
|
+
this._settleOldest(p => p.resolve(msg));
|
|
51
104
|
}
|
|
52
105
|
});
|
|
53
106
|
});
|
|
54
107
|
}
|
|
55
108
|
|
|
56
|
-
|
|
109
|
+
_settle(requestId, fn) {
|
|
110
|
+
const p = this._pending.get(requestId);
|
|
111
|
+
if (p) {
|
|
112
|
+
this._pending.delete(requestId);
|
|
113
|
+
fn(p);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_settleOldest(fn) {
|
|
118
|
+
const first = this._pending.entries().next();
|
|
119
|
+
if (!first.done) {
|
|
120
|
+
const [requestId, p] = first.value;
|
|
121
|
+
this._pending.delete(requestId);
|
|
122
|
+
fn(p);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
sendCommand({ command, inputMode = "ai", force } = {}) {
|
|
57
127
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
58
128
|
return Promise.reject(new Error("zestro bridge not connected"));
|
|
59
129
|
}
|
|
60
|
-
const
|
|
130
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
61
131
|
return new Promise((resolve, reject) => {
|
|
62
132
|
const timer = setTimeout(() => {
|
|
63
|
-
this._pending.delete(
|
|
64
|
-
reject(new Error(`zestro send_command ${
|
|
133
|
+
this._pending.delete(requestId);
|
|
134
|
+
reject(new Error(`zestro send_command ${requestId} timed out after ${this.timeoutMs}ms`));
|
|
65
135
|
}, this.timeoutMs);
|
|
66
|
-
this._pending.set(
|
|
136
|
+
this._pending.set(requestId, {
|
|
67
137
|
resolve: (msg) => { clearTimeout(timer); resolve(msg); },
|
|
68
138
|
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
69
139
|
});
|
|
70
140
|
this._ws.send(JSON.stringify({
|
|
71
141
|
type: "send_command",
|
|
72
|
-
|
|
142
|
+
requestId,
|
|
73
143
|
sessionId: this.sessionId,
|
|
74
144
|
command,
|
|
75
145
|
inputMode,
|
|
146
|
+
...(this.tabId ? { tabId: this.tabId } : {}),
|
|
147
|
+
...((force ?? this.force) ? { force: true } : {}),
|
|
76
148
|
}));
|
|
77
149
|
});
|
|
78
150
|
}
|