@avasapp/agent-bridge 0.2.0 → 0.4.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 +220 -20
- package/dist/adapters/ably.cjs +22 -4
- package/dist/adapters/ably.d.cts +8 -1
- package/dist/adapters/expo-router.cjs +22 -1
- package/dist/adapters/expo-router.d.cts +5 -1
- package/dist/adapters/react-native-mmkv.cjs +1 -0
- package/dist/adapters/react-native-mmkv.d.cts +1 -1
- package/dist/adapters/react-native-webview.cjs +672 -0
- package/dist/adapters/react-native-webview.d.cts +79 -0
- package/dist/adapters/socket-io.cjs +3 -1
- package/dist/adapters/socket-io.d.cts +1 -1
- package/dist/adapters/tanstack-query.cjs +256 -17
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +152 -12
- package/dist/adapters/zustand.d.cts +19 -3
- package/dist/{chunk-LBYZ5JHF.js → chunk-AJD3XTNT.js} +329 -46
- package/dist/cli.js +848 -98
- package/dist/client/index.cjs +311 -47
- package/dist/client/index.d.ts +64 -3
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +434 -66
- package/dist/network/index.d.cts +53 -3
- package/dist/noop/index.cjs +12 -0
- package/dist/noop/network.cjs +7 -1
- package/dist/noop/react-native-webview.cjs +31 -0
- package/dist/realtime/index.cjs +2 -1
- package/dist/realtime/index.d.cts +3 -1
- package/dist/runtime/index.cjs +1067 -111
- package/dist/runtime/index.d.cts +51 -5
- package/dist/types-BmpykP1P.d.cts +137 -0
- package/entries/react-native-webview.cjs +9 -0
- package/package.json +18 -6
- package/skills/agent-bridge/SKILL.md +226 -8
- package/dist/types-C6DUUHnB.d.cts +0 -76
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// src/client/discover.ts
|
|
2
|
+
var within = (ms, signal) => signal ? AbortSignal.any([AbortSignal.timeout(ms), signal]) : AbortSignal.timeout(ms);
|
|
2
3
|
var DEFAULT_METRO = "localhost:8081";
|
|
4
|
+
var stripScheme = (metro) => metro.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
5
|
+
function explicitMetro(metro) {
|
|
6
|
+
const given = metro ?? process.env.AGENT_BRIDGE_METRO;
|
|
7
|
+
return given ? stripScheme(given) : void 0;
|
|
8
|
+
}
|
|
3
9
|
function metroHost(metro) {
|
|
4
|
-
return (metro ?? process.env.AGENT_BRIDGE_METRO ?? DEFAULT_METRO)
|
|
10
|
+
return stripScheme(metro ?? process.env.AGENT_BRIDGE_METRO ?? DEFAULT_METRO);
|
|
5
11
|
}
|
|
6
|
-
async function expoHostUri(metro) {
|
|
12
|
+
async function expoHostUri(metro, signal) {
|
|
7
13
|
try {
|
|
8
14
|
const res = await fetch(`http://${metro}/`, {
|
|
9
15
|
headers: {
|
|
10
16
|
"expo-platform": "ios",
|
|
11
17
|
accept: "application/expo+json,application/json"
|
|
12
18
|
},
|
|
13
|
-
signal:
|
|
19
|
+
signal: within(3e3, signal)
|
|
14
20
|
});
|
|
15
21
|
if (!res.ok) return null;
|
|
16
22
|
const manifest = await res.json();
|
|
@@ -19,9 +25,9 @@ async function expoHostUri(metro) {
|
|
|
19
25
|
return null;
|
|
20
26
|
}
|
|
21
27
|
}
|
|
22
|
-
async function listCdpTargets(metro) {
|
|
28
|
+
async function listCdpTargets(metro, signal) {
|
|
23
29
|
const res = await fetch(`http://${metro}/json/list`, {
|
|
24
|
-
signal:
|
|
30
|
+
signal: within(3e3, signal)
|
|
25
31
|
});
|
|
26
32
|
if (!res.ok)
|
|
27
33
|
throw new Error(
|
|
@@ -61,21 +67,43 @@ function toAsciiJson(value) {
|
|
|
61
67
|
|
|
62
68
|
// src/client/connection.ts
|
|
63
69
|
import WebSocket from "ws";
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
var OPEN_SOCKET_MS = 5e3;
|
|
71
|
+
var OpenGaveUp = class extends Error {
|
|
72
|
+
};
|
|
73
|
+
function openSocket(url, headers, signal, timeoutMs = OPEN_SOCKET_MS) {
|
|
74
|
+
return new Promise((resolve2, reject) => {
|
|
66
75
|
const ws = new WebSocket(url, headers ? { headers } : void 0);
|
|
67
76
|
let opened = false;
|
|
77
|
+
const giveUp = (why) => {
|
|
78
|
+
if (opened) return;
|
|
79
|
+
reject(new OpenGaveUp(`Could not open ${url}: ${why}`));
|
|
80
|
+
ws.terminate();
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(
|
|
83
|
+
() => giveUp(`no answer within ${timeoutMs} ms`),
|
|
84
|
+
timeoutMs
|
|
85
|
+
);
|
|
86
|
+
const onAbort = () => giveUp("aborted");
|
|
87
|
+
if (signal?.aborted) onAbort();
|
|
88
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
89
|
+
const settled = () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
signal?.removeEventListener("abort", onAbort);
|
|
92
|
+
};
|
|
68
93
|
ws.on("error", (event) => {
|
|
69
94
|
if (opened) return;
|
|
95
|
+
settled();
|
|
70
96
|
const message = event?.message ?? String(event);
|
|
71
97
|
reject(new Error(`Could not open ${url}: ${message}`));
|
|
72
98
|
});
|
|
73
99
|
ws.once("close", () => {
|
|
100
|
+
settled();
|
|
74
101
|
if (!opened) reject(new Error(`${url} closed before opening`));
|
|
75
102
|
});
|
|
76
103
|
ws.once("open", () => {
|
|
104
|
+
settled();
|
|
77
105
|
opened = true;
|
|
78
|
-
|
|
106
|
+
resolve2(ws);
|
|
79
107
|
});
|
|
80
108
|
});
|
|
81
109
|
}
|
|
@@ -84,7 +112,7 @@ function createPending() {
|
|
|
84
112
|
const waiting = /* @__PURE__ */ new Map();
|
|
85
113
|
return {
|
|
86
114
|
wait(id, tool, timeoutMs) {
|
|
87
|
-
return new Promise((
|
|
115
|
+
return new Promise((resolve2, reject) => {
|
|
88
116
|
const timer = setTimeout(() => {
|
|
89
117
|
waiting.delete(id);
|
|
90
118
|
reject(new Error(`No reply to "${tool}" within ${timeoutMs} ms`));
|
|
@@ -92,7 +120,7 @@ function createPending() {
|
|
|
92
120
|
waiting.set(id, (result) => {
|
|
93
121
|
clearTimeout(timer);
|
|
94
122
|
waiting.delete(id);
|
|
95
|
-
|
|
123
|
+
resolve2(result);
|
|
96
124
|
});
|
|
97
125
|
});
|
|
98
126
|
},
|
|
@@ -109,10 +137,15 @@ function createPending() {
|
|
|
109
137
|
|
|
110
138
|
// src/client/expo.ts
|
|
111
139
|
var deviceLabel = (d) => `${d.name} (${d.platform}, ${d.deviceId})`;
|
|
112
|
-
async function openBroadcast(metro, discoveryMs) {
|
|
113
|
-
const ws = await openSocket(
|
|
140
|
+
async function openBroadcast(metro, discoveryMs, signal) {
|
|
141
|
+
const ws = await openSocket(
|
|
142
|
+
`ws://${metro}/expo-dev-plugins/broadcast`,
|
|
143
|
+
void 0,
|
|
144
|
+
signal
|
|
145
|
+
);
|
|
114
146
|
const devices = /* @__PURE__ */ new Map();
|
|
115
147
|
const pending = createPending();
|
|
148
|
+
const announced = [];
|
|
116
149
|
ws.on("message", (data, isBinary) => {
|
|
117
150
|
if (isBinary) return;
|
|
118
151
|
let frame;
|
|
@@ -124,7 +157,9 @@ async function openBroadcast(metro, discoveryMs) {
|
|
|
124
157
|
if (frame.messageKey?.pluginName !== PLUGIN_NAME) return;
|
|
125
158
|
if (frame.messageKey.method === "hello:reply") {
|
|
126
159
|
const info = frame.payload;
|
|
160
|
+
const fresh = !devices.has(info.deviceId);
|
|
127
161
|
devices.set(info.deviceId, info);
|
|
162
|
+
if (fresh) for (const listener of announced) listener(info);
|
|
128
163
|
} else if (frame.messageKey.method === "result") {
|
|
129
164
|
pending.settle(frame.payload);
|
|
130
165
|
}
|
|
@@ -137,16 +172,32 @@ async function openBroadcast(metro, discoveryMs) {
|
|
|
137
172
|
})
|
|
138
173
|
);
|
|
139
174
|
send("hello", {});
|
|
140
|
-
await new Promise((
|
|
141
|
-
|
|
175
|
+
await new Promise((resolve2, reject) => {
|
|
176
|
+
const onAbort = () => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
ws.terminate();
|
|
179
|
+
reject(new Error("aborted while waiting for hello replies"));
|
|
180
|
+
};
|
|
181
|
+
const timer = setTimeout(() => {
|
|
182
|
+
signal?.removeEventListener("abort", onAbort);
|
|
183
|
+
resolve2();
|
|
184
|
+
}, discoveryMs);
|
|
185
|
+
if (signal?.aborted) onAbort();
|
|
186
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
187
|
+
});
|
|
188
|
+
return { ws, devices, pending, send, announced };
|
|
142
189
|
}
|
|
143
190
|
async function listExpoDevices(metro, discoveryMs = 600) {
|
|
144
191
|
const { ws, devices } = await openBroadcast(metro, discoveryMs);
|
|
145
192
|
ws.close();
|
|
146
193
|
return [...devices.values()];
|
|
147
194
|
}
|
|
148
|
-
async function connectExpo(metro, device, discoveryMs = 600) {
|
|
149
|
-
const { ws, devices, pending, send } = await openBroadcast(
|
|
195
|
+
async function connectExpo(metro, device, discoveryMs = 600, signal) {
|
|
196
|
+
const { ws, devices, pending, send, announced } = await openBroadcast(
|
|
197
|
+
metro,
|
|
198
|
+
discoveryMs,
|
|
199
|
+
signal
|
|
200
|
+
);
|
|
150
201
|
let info;
|
|
151
202
|
try {
|
|
152
203
|
info = pickOne([...devices.values()], device, deviceLabel);
|
|
@@ -154,10 +205,25 @@ async function connectExpo(metro, device, discoveryMs = 600) {
|
|
|
154
205
|
ws.close();
|
|
155
206
|
throw error;
|
|
156
207
|
}
|
|
208
|
+
let superseded = null;
|
|
209
|
+
announced.push((next) => {
|
|
210
|
+
if (superseded || next.name !== info.name || next.platform !== info.platform)
|
|
211
|
+
return;
|
|
212
|
+
superseded = `${info.name} announced a new bridge (${next.deviceId}); the app reloaded`;
|
|
213
|
+
pending.failAll(superseded);
|
|
214
|
+
});
|
|
157
215
|
return {
|
|
158
216
|
transport: "expo",
|
|
159
217
|
device: info,
|
|
160
218
|
call(tool, args, timeoutMs) {
|
|
219
|
+
if (superseded)
|
|
220
|
+
return Promise.resolve({
|
|
221
|
+
id: "",
|
|
222
|
+
from: "",
|
|
223
|
+
ok: false,
|
|
224
|
+
error: superseded,
|
|
225
|
+
ms: 0
|
|
226
|
+
});
|
|
161
227
|
const call = {
|
|
162
228
|
id: newCallId(),
|
|
163
229
|
tool,
|
|
@@ -177,17 +243,26 @@ import WebSocket2 from "ws";
|
|
|
177
243
|
var CannotEvaluate = class extends Error {
|
|
178
244
|
};
|
|
179
245
|
var targetLabel = (t) => `${t.title}${t.deviceName ? ` [${t.deviceName}]` : ""}`;
|
|
180
|
-
async function connectCdp(metro, device) {
|
|
181
|
-
const target = pickOne(
|
|
182
|
-
|
|
246
|
+
async function connectCdp(metro, device, signal) {
|
|
247
|
+
const target = pickOne(
|
|
248
|
+
await listCdpTargets(metro, signal),
|
|
249
|
+
device,
|
|
250
|
+
targetLabel
|
|
251
|
+
);
|
|
252
|
+
const hostUri = await expoHostUri(metro, signal);
|
|
183
253
|
const port = metro.split(":")[1] ?? "8081";
|
|
184
254
|
const origin = hostUri ? `http://${hostUri}` : `http://localhost:${port}`;
|
|
185
255
|
const closedEarly = `Metro closed the debugger socket. Origin sent: ${origin}. Is that the host Metro advertises?`;
|
|
186
|
-
const ws = await openSocket(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
256
|
+
const ws = await openSocket(
|
|
257
|
+
target.webSocketDebuggerUrl,
|
|
258
|
+
{ Origin: origin },
|
|
259
|
+
signal
|
|
260
|
+
).catch((error) => {
|
|
261
|
+
throw error instanceof OpenGaveUp ? error : new Error(closedEarly);
|
|
190
262
|
});
|
|
263
|
+
const dropOnAbort = () => ws.terminate();
|
|
264
|
+
if (signal?.aborted) dropOnAbort();
|
|
265
|
+
else signal?.addEventListener("abort", dropOnAbort, { once: true });
|
|
191
266
|
let seq = 0;
|
|
192
267
|
let answered = false;
|
|
193
268
|
const commands = /* @__PURE__ */ new Map();
|
|
@@ -208,13 +283,13 @@ async function connectCdp(metro, device) {
|
|
|
208
283
|
commands.clear();
|
|
209
284
|
pending.failAll(reason);
|
|
210
285
|
});
|
|
211
|
-
const send = (method, params = {}) => new Promise((
|
|
286
|
+
const send = (method, params = {}) => new Promise((resolve2, reject) => {
|
|
212
287
|
if (ws.readyState !== WebSocket2.OPEN) {
|
|
213
288
|
reject(new Error(answered ? "The debugger socket closed" : closedEarly));
|
|
214
289
|
return;
|
|
215
290
|
}
|
|
216
291
|
const id = ++seq;
|
|
217
|
-
commands.set(id, { resolve, reject });
|
|
292
|
+
commands.set(id, { resolve: resolve2, reject });
|
|
218
293
|
ws.send(JSON.stringify({ id, method, params }));
|
|
219
294
|
});
|
|
220
295
|
const evaluate = async (expression) => {
|
|
@@ -230,25 +305,35 @@ async function connectCdp(metro, device) {
|
|
|
230
305
|
}
|
|
231
306
|
return message.result?.result?.value;
|
|
232
307
|
};
|
|
233
|
-
|
|
234
|
-
|
|
308
|
+
try {
|
|
309
|
+
await send("Runtime.enable");
|
|
310
|
+
await send("Runtime.addBinding", { name: CDP_REPLY_BINDING });
|
|
311
|
+
} catch (error) {
|
|
312
|
+
ws.terminate();
|
|
313
|
+
signal?.removeEventListener("abort", dropOnAbort);
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
235
316
|
let info;
|
|
236
317
|
try {
|
|
237
318
|
info = JSON.parse(
|
|
238
319
|
String(await evaluate(`${CDP_GLOBAL}.info()`))
|
|
239
320
|
);
|
|
240
321
|
} catch (error) {
|
|
241
|
-
ws.
|
|
322
|
+
ws.terminate();
|
|
323
|
+
signal?.removeEventListener("abort", dropOnAbort);
|
|
242
324
|
throw new Error(
|
|
243
325
|
error instanceof CannotEvaluate ? `The debugger for ${targetLabel(target)} can't run code (no Runtime.evaluate, as in Expo Go on Android). Use --transport expo, or a dev build.` : `agent-bridge isn't running in ${targetLabel(target)}. Is useAgentBridge mounted in a dev build?`
|
|
244
326
|
);
|
|
245
327
|
}
|
|
328
|
+
signal?.removeEventListener("abort", dropOnAbort);
|
|
246
329
|
return {
|
|
247
330
|
transport: "cdp",
|
|
248
331
|
device: { ...info, name: `${info.name} (${targetLabel(target)})` },
|
|
249
332
|
async call(tool, args, timeoutMs) {
|
|
250
333
|
const call = { id: newCallId(), tool, args };
|
|
251
334
|
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
335
|
+
reply.catch(() => {
|
|
336
|
+
});
|
|
252
337
|
try {
|
|
253
338
|
await evaluate(
|
|
254
339
|
`${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
|
|
@@ -269,36 +354,193 @@ async function connectCdp(metro, device) {
|
|
|
269
354
|
}
|
|
270
355
|
|
|
271
356
|
// src/client/open.ts
|
|
357
|
+
var OPEN_MS = 15e3;
|
|
358
|
+
var abortable = (work, signal) => new Promise((resolve2, reject) => {
|
|
359
|
+
const onAbort = () => reject(new Error("aborted"));
|
|
360
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
361
|
+
work.then(resolve2, reject).finally(() => {
|
|
362
|
+
signal.removeEventListener("abort", onAbort);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
function phase(options, run) {
|
|
366
|
+
const ms = options.timeoutMs ?? OPEN_MS;
|
|
367
|
+
const timeout = AbortSignal.timeout(ms);
|
|
368
|
+
const signal = options.signal ? AbortSignal.any([timeout, options.signal]) : timeout;
|
|
369
|
+
const attempt = run(signal);
|
|
370
|
+
attempt.then(
|
|
371
|
+
(late) => {
|
|
372
|
+
if (signal.aborted) late.close();
|
|
373
|
+
},
|
|
374
|
+
() => {
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
return abortable(attempt, signal).catch((error) => {
|
|
378
|
+
if (!signal.aborted) throw error;
|
|
379
|
+
throw new OpenGaveUp(
|
|
380
|
+
timeout.aborted ? `Timed out after ${ms} ms opening a connection to Metro` : "Connecting was aborted"
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
272
384
|
async function openConnection(options = {}) {
|
|
273
385
|
const metro = metroHost(options.metro);
|
|
274
386
|
const want = options.transport ?? "auto";
|
|
275
387
|
let expoError;
|
|
276
388
|
if (want !== "cdp") {
|
|
277
389
|
try {
|
|
278
|
-
return await
|
|
390
|
+
return await phase(
|
|
391
|
+
options,
|
|
392
|
+
(signal) => connectExpo(metro, options.device, void 0, signal)
|
|
393
|
+
);
|
|
279
394
|
} catch (error) {
|
|
280
|
-
if (want === "expo") throw error;
|
|
395
|
+
if (want === "expo" || options.signal?.aborted) throw error;
|
|
281
396
|
expoError = error;
|
|
282
397
|
}
|
|
283
398
|
}
|
|
284
399
|
try {
|
|
285
|
-
return await
|
|
400
|
+
return await phase(
|
|
401
|
+
options,
|
|
402
|
+
(signal) => connectCdp(metro, options.device, signal)
|
|
403
|
+
);
|
|
286
404
|
} catch (error) {
|
|
287
405
|
const expoNote = expoError ? ` (Expo socket: ${String(expoError)})` : "";
|
|
406
|
+
const text = `${error instanceof Error ? error.message : String(error)}${expoNote}`;
|
|
407
|
+
throw error instanceof OpenGaveUp ? new OpenGaveUp(text) : new Error(text);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/client/log-lines.ts
|
|
412
|
+
function logLine(entry) {
|
|
413
|
+
const where = entry.during ? ` during ${entry.during}` : entry.after ? ` after ${entry.after}` : "";
|
|
414
|
+
const first = entry.message.split("\n")[0] ?? "";
|
|
415
|
+
const text = first.length > 300 ? `${first.slice(0, 300)}...` : first;
|
|
416
|
+
return `! ${entry.level}${where}: ${text}`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/client/flow.ts
|
|
420
|
+
function flowScenarios(flow) {
|
|
421
|
+
const declared = [
|
|
422
|
+
...flow.scenario === void 0 ? [] : [flow.scenario],
|
|
423
|
+
...flow.scenarios ?? []
|
|
424
|
+
];
|
|
425
|
+
return declared.map((s) => {
|
|
426
|
+
if (typeof s === "string") return { name: s };
|
|
427
|
+
if (s && typeof s === "object" && typeof s.name === "string") return s;
|
|
288
428
|
throw new Error(
|
|
289
|
-
|
|
429
|
+
`A flow's scenario is a name or { name, options }, not ${JSON.stringify(s)}`
|
|
290
430
|
);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function parseScenarioFlag(value) {
|
|
434
|
+
const at = value.indexOf("=");
|
|
435
|
+
if (at === -1) return { name: value };
|
|
436
|
+
const name = value.slice(0, at);
|
|
437
|
+
const raw = value.slice(at + 1);
|
|
438
|
+
try {
|
|
439
|
+
return { name, options: JSON.parse(raw) };
|
|
440
|
+
} catch {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`--scenario ${name}: the options after "=" must be JSON, got ${raw}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function mergeScenarios(declared, extra) {
|
|
447
|
+
const names = new Set(extra.map((s) => s.name));
|
|
448
|
+
return [...declared.filter((s) => !names.has(s.name)), ...extra];
|
|
449
|
+
}
|
|
450
|
+
var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
|
|
451
|
+
var failedNotice = (error) => error instanceof AgentBridgeCallError ? error.notice : void 0;
|
|
452
|
+
async function runFlow(bridge, flow, options = {}) {
|
|
453
|
+
const print = options.print ?? console.log;
|
|
454
|
+
const needs = mergeScenarios(flowScenarios(flow), options.scenarios ?? []);
|
|
455
|
+
let n = 0;
|
|
456
|
+
let total = 0;
|
|
457
|
+
let errors = 0;
|
|
458
|
+
const t0 = performance.now();
|
|
459
|
+
const notices = [];
|
|
460
|
+
const report = (logs, notice) => {
|
|
461
|
+
errors += logs.length;
|
|
462
|
+
for (const e of logs) print(` ${logLine(e)}`);
|
|
463
|
+
if (notice) {
|
|
464
|
+
notices.push(notice);
|
|
465
|
+
print(` ! ${notice}`);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const timed = async (tool, ...args) => {
|
|
469
|
+
try {
|
|
470
|
+
const result = await bridge.timed(tool, ...args);
|
|
471
|
+
report(result.logs, result.notice);
|
|
472
|
+
return result;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
report(failedLogs(error), failedNotice(error));
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const call = async (tool, ...args) => (await timed(tool, ...args)).value;
|
|
479
|
+
const step = async (label, tool, ...given) => {
|
|
480
|
+
const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
|
|
481
|
+
const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
|
|
482
|
+
report(failedLogs(error), failedNotice(error));
|
|
483
|
+
throw error;
|
|
484
|
+
});
|
|
485
|
+
total += ms;
|
|
486
|
+
print(
|
|
487
|
+
`${String(++n).padStart(2, "0")} ${label.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
|
|
488
|
+
);
|
|
489
|
+
report(logs, notice);
|
|
490
|
+
return value;
|
|
491
|
+
};
|
|
492
|
+
const restoreErrors = [];
|
|
493
|
+
const applied = {};
|
|
494
|
+
if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
|
|
495
|
+
throw new Error(
|
|
496
|
+
`The flow needs scenario "${needs[0].name}", but the app defines none: pass \`scenarios\` to useAgentBridge.`
|
|
497
|
+
);
|
|
498
|
+
try {
|
|
499
|
+
for (const { name, options: options2 } of needs) {
|
|
500
|
+
const args = options2 === void 0 ? [name] : [name, options2];
|
|
501
|
+
applied[name] = await step(`scenario ${name}`, "scenario.apply", ...args);
|
|
502
|
+
}
|
|
503
|
+
await flow.default({
|
|
504
|
+
bridge: { ...bridge, timed, call },
|
|
505
|
+
call,
|
|
506
|
+
step,
|
|
507
|
+
scenarios: applied
|
|
508
|
+
});
|
|
509
|
+
} finally {
|
|
510
|
+
if (needs.length) {
|
|
511
|
+
try {
|
|
512
|
+
const result = await step("restore", "bridge.restore");
|
|
513
|
+
for (const [name, value] of Object.entries(result ?? {})) {
|
|
514
|
+
const error = value?.error;
|
|
515
|
+
if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
restoreErrors.push(
|
|
519
|
+
`bridge.restore: ${error instanceof Error ? error.message : String(error)}`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
for (const e of restoreErrors) print(` ! restore failed: ${e}`);
|
|
523
|
+
}
|
|
291
524
|
}
|
|
525
|
+
print(
|
|
526
|
+
`${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}${notices.length ? ", the app reloaded" : ""}`
|
|
527
|
+
);
|
|
528
|
+
return { steps: n, errors, restoreErrors, notices };
|
|
292
529
|
}
|
|
293
530
|
|
|
294
531
|
// src/client/session/client.ts
|
|
295
532
|
import { createConnection } from "net";
|
|
296
533
|
import { createInterface } from "readline";
|
|
297
534
|
|
|
535
|
+
// src/client/session/protocol.ts
|
|
536
|
+
var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16)}`)}
|
|
537
|
+
`;
|
|
538
|
+
|
|
298
539
|
// src/client/session/state.ts
|
|
299
540
|
import { createHash } from "crypto";
|
|
300
541
|
import {
|
|
301
542
|
chmodSync,
|
|
543
|
+
existsSync,
|
|
302
544
|
lstatSync,
|
|
303
545
|
mkdirSync,
|
|
304
546
|
readFileSync,
|
|
@@ -307,7 +549,13 @@ import {
|
|
|
307
549
|
writeFileSync
|
|
308
550
|
} from "fs";
|
|
309
551
|
import { tmpdir, userInfo } from "os";
|
|
310
|
-
import { join } from "path";
|
|
552
|
+
import { dirname, join, resolve } from "path";
|
|
553
|
+
function projectRoot(from = process.cwd()) {
|
|
554
|
+
for (let dir = resolve(from); ; dir = dirname(dir)) {
|
|
555
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
556
|
+
if (dirname(dir) === dir) return resolve(from);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
311
559
|
var uid = () => process.getuid?.() ?? userInfo().username;
|
|
312
560
|
function privateDir(dir) {
|
|
313
561
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
@@ -404,15 +652,21 @@ function pickSession(filter) {
|
|
|
404
652
|
return named;
|
|
405
653
|
}
|
|
406
654
|
const device = filter.device?.toLowerCase();
|
|
655
|
+
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
656
|
+
const project = projectRoot();
|
|
407
657
|
const matching = listSessions().filter(
|
|
408
|
-
(s) => s.metro === filter.metro && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
|
|
658
|
+
(s) => (explicit || s.project === project) && (!filter.metro || s.metro === filter.metro) && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
|
|
409
659
|
);
|
|
410
|
-
|
|
660
|
+
if (matching.length > 1)
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Several sessions match (${matching.map((s) => `${s.name}: ${s.device.name} on ${s.metro}`).join("; ")}); pick one by name (--session, or --name for session stop/status), or narrow with --metro/--device`
|
|
663
|
+
);
|
|
664
|
+
return matching[0] ?? null;
|
|
411
665
|
}
|
|
412
666
|
|
|
413
667
|
// src/client/session/client.ts
|
|
414
668
|
function openSessionLink(state) {
|
|
415
|
-
return new Promise((
|
|
669
|
+
return new Promise((resolve2, reject) => {
|
|
416
670
|
const socket = createConnection(state.socket);
|
|
417
671
|
const waiting = /* @__PURE__ */ new Map();
|
|
418
672
|
let seq = 0;
|
|
@@ -420,7 +674,7 @@ function openSessionLink(state) {
|
|
|
420
674
|
const gone = `Session "${state.name}" closed the connection`;
|
|
421
675
|
socket.once("connect", () => {
|
|
422
676
|
open = true;
|
|
423
|
-
|
|
677
|
+
resolve2({
|
|
424
678
|
request: (req) => new Promise((done, fail) => {
|
|
425
679
|
if (socket.destroyed || socket.writableEnded) {
|
|
426
680
|
fail(new Error(gone));
|
|
@@ -428,8 +682,7 @@ function openSessionLink(state) {
|
|
|
428
682
|
}
|
|
429
683
|
const id = ++seq;
|
|
430
684
|
waiting.set(id, done);
|
|
431
|
-
socket.write(
|
|
432
|
-
`);
|
|
685
|
+
socket.write(encodeLine({ ...req, id }));
|
|
433
686
|
}),
|
|
434
687
|
close: () => socket.end()
|
|
435
688
|
});
|
|
@@ -464,7 +717,7 @@ async function sessionRequest(state, req) {
|
|
|
464
717
|
async function connectSession(options = {}) {
|
|
465
718
|
const state = pickSession({
|
|
466
719
|
name: options.name ?? process.env.AGENT_BRIDGE_SESSION,
|
|
467
|
-
metro:
|
|
720
|
+
metro: explicitMetro(options.metro),
|
|
468
721
|
device: options.device,
|
|
469
722
|
transport: options.transport
|
|
470
723
|
});
|
|
@@ -486,12 +739,32 @@ async function connectSession(options = {}) {
|
|
|
486
739
|
args,
|
|
487
740
|
timeoutMs: options.timeoutMs
|
|
488
741
|
});
|
|
489
|
-
if (res.error || !res.result)
|
|
742
|
+
if (res.error || !res.result) {
|
|
743
|
+
const why = res.error ?? "No result";
|
|
744
|
+
throw res.notice ? new AgentBridgeCallError(tool, why, [], res.notice) : new Error(why);
|
|
745
|
+
}
|
|
490
746
|
const result = res.result;
|
|
491
747
|
const logs = result.logs ?? [];
|
|
492
|
-
if (!result.ok)
|
|
493
|
-
|
|
494
|
-
|
|
748
|
+
if (!result.ok)
|
|
749
|
+
throw new AgentBridgeCallError(tool, result.error, logs, res.notice);
|
|
750
|
+
const {
|
|
751
|
+
id: _id,
|
|
752
|
+
from: _from,
|
|
753
|
+
ok: _ok,
|
|
754
|
+
ms: appMs,
|
|
755
|
+
value,
|
|
756
|
+
loadId: _loadId,
|
|
757
|
+
pending: _pending,
|
|
758
|
+
...extra
|
|
759
|
+
} = result;
|
|
760
|
+
return {
|
|
761
|
+
...extra,
|
|
762
|
+
value,
|
|
763
|
+
ms: res.ms ?? 0,
|
|
764
|
+
appMs,
|
|
765
|
+
logs,
|
|
766
|
+
...res.notice && { notice: res.notice }
|
|
767
|
+
};
|
|
495
768
|
};
|
|
496
769
|
return {
|
|
497
770
|
session: info.state ?? state,
|
|
@@ -506,14 +779,17 @@ async function connectSession(options = {}) {
|
|
|
506
779
|
|
|
507
780
|
// src/client/index.ts
|
|
508
781
|
var AgentBridgeCallError = class extends Error {
|
|
509
|
-
constructor(tool, message, logs = []) {
|
|
510
|
-
super(`${tool}: ${message}`
|
|
782
|
+
constructor(tool, message, logs = [], notice) {
|
|
783
|
+
super(`${tool}: ${message}${notice ? `
|
|
784
|
+
Warning: ${notice}` : ""}`);
|
|
511
785
|
this.tool = tool;
|
|
512
786
|
this.logs = logs;
|
|
787
|
+
this.notice = notice;
|
|
513
788
|
this.name = "AgentBridgeCallError";
|
|
514
789
|
}
|
|
515
790
|
tool;
|
|
516
791
|
logs;
|
|
792
|
+
notice;
|
|
517
793
|
};
|
|
518
794
|
async function connect(options = {}) {
|
|
519
795
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
@@ -556,9 +832,16 @@ async function listDevices(options = {}) {
|
|
|
556
832
|
}
|
|
557
833
|
|
|
558
834
|
export {
|
|
835
|
+
explicitMetro,
|
|
559
836
|
metroHost,
|
|
560
837
|
RUNTIME_MARKER,
|
|
561
838
|
openConnection,
|
|
839
|
+
logLine,
|
|
840
|
+
flowScenarios,
|
|
841
|
+
parseScenarioFlag,
|
|
842
|
+
runFlow,
|
|
843
|
+
encodeLine,
|
|
844
|
+
projectRoot,
|
|
562
845
|
stateDir,
|
|
563
846
|
sessionFiles,
|
|
564
847
|
checkName,
|