@avasapp/agent-bridge 0.4.2 → 0.6.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 +36 -14
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +13 -4
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
- package/dist/cli.js +2370 -103
- package/dist/client/index.cjs +441 -136
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +838 -128
- package/dist/runtime/index.d.cts +4 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +34 -2
- package/skills/agent-bridge/SKILL.md +70 -22
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +19 -8
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +25 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +25 -7
- package/skills/agent-bridge/references/screen.md +25 -11
- package/skills/agent-bridge/references/state.md +31 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
package/dist/client/index.cjs
CHANGED
|
@@ -35,10 +35,26 @@ __export(client_exports, {
|
|
|
35
35
|
connectSession: () => connectSession,
|
|
36
36
|
listDevices: () => listDevices,
|
|
37
37
|
listSessions: () => listSessions,
|
|
38
|
-
runFlow: () => runFlow
|
|
38
|
+
runFlow: () => runFlow,
|
|
39
|
+
runFlows: () => runFlows
|
|
39
40
|
});
|
|
40
41
|
module.exports = __toCommonJS(client_exports);
|
|
41
42
|
|
|
43
|
+
// src/client/call-timeout.ts
|
|
44
|
+
var WAITERS = {
|
|
45
|
+
"screen.waitFor": 2e3,
|
|
46
|
+
// The page helper adds 5 s to timeoutMs before it gives up.
|
|
47
|
+
"webview.waitFor": 7e3
|
|
48
|
+
};
|
|
49
|
+
var MAX_MS = 10 * 6e4;
|
|
50
|
+
function callTimeout(base, tool, args) {
|
|
51
|
+
const margin = WAITERS[tool];
|
|
52
|
+
const t = args[1]?.timeoutMs;
|
|
53
|
+
if (margin === void 0 || typeof t !== "number" || !(t > 0) || !Number.isFinite(t))
|
|
54
|
+
return base;
|
|
55
|
+
return Math.max(base, Math.min(t + margin, MAX_MS));
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
// src/client/discover.ts
|
|
43
59
|
var within = (ms, signal) => signal ? AbortSignal.any([AbortSignal.timeout(ms), signal]) : AbortSignal.timeout(ms);
|
|
44
60
|
var DEFAULT_METRO = "localhost:8081";
|
|
@@ -76,11 +92,11 @@ async function listCdpTargets(metro, signal) {
|
|
|
76
92
|
);
|
|
77
93
|
return await res.json();
|
|
78
94
|
}
|
|
79
|
-
function pickOne(items, filter,
|
|
95
|
+
function pickOne(items, filter, label2) {
|
|
80
96
|
const matching = filter ? items.filter(
|
|
81
|
-
(item) =>
|
|
97
|
+
(item) => label2(item).toLowerCase().includes(filter.toLowerCase())
|
|
82
98
|
) : items;
|
|
83
|
-
const connected = items.map(
|
|
99
|
+
const connected = items.map(label2).join("; ") || "none";
|
|
84
100
|
if (matching.length === 0) {
|
|
85
101
|
throw new Error(
|
|
86
102
|
filter ? `No app matches "${filter}". Connected: ${connected}` : "No app is connected to Metro."
|
|
@@ -93,11 +109,11 @@ function pickOne(items, filter, label) {
|
|
|
93
109
|
}
|
|
94
110
|
if (matching.length > 1) {
|
|
95
111
|
const exact = matching.filter(
|
|
96
|
-
(item) =>
|
|
112
|
+
(item) => label2(item).toLowerCase() === filter?.toLowerCase()
|
|
97
113
|
);
|
|
98
114
|
if (exact.length === 1) return exact[0];
|
|
99
115
|
throw new Error(
|
|
100
|
-
`${matching.length} apps match "${filter}"; narrow --device. Matching: ${matching.map(
|
|
116
|
+
`${matching.length} apps match "${filter}"; narrow --device. Matching: ${matching.map(label2).join("; ")}`
|
|
101
117
|
);
|
|
102
118
|
}
|
|
103
119
|
return matching[0];
|
|
@@ -272,7 +288,7 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
|
272
288
|
transport: "expo",
|
|
273
289
|
device: info,
|
|
274
290
|
pin: { name: info.name, platform: info.platform },
|
|
275
|
-
call(tool, args, timeoutMs) {
|
|
291
|
+
call(tool, args, timeoutMs, extra) {
|
|
276
292
|
if (superseded)
|
|
277
293
|
return Promise.resolve({
|
|
278
294
|
id: "",
|
|
@@ -285,7 +301,8 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
|
285
301
|
id: newCallId(),
|
|
286
302
|
tool,
|
|
287
303
|
args,
|
|
288
|
-
to: info.deviceId
|
|
304
|
+
to: info.deviceId,
|
|
305
|
+
...extra
|
|
289
306
|
};
|
|
290
307
|
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
291
308
|
send("call", call);
|
|
@@ -295,6 +312,219 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
|
295
312
|
};
|
|
296
313
|
}
|
|
297
314
|
|
|
315
|
+
// src/client/session/state.ts
|
|
316
|
+
var import_node_crypto = require("crypto");
|
|
317
|
+
var import_node_fs = require("fs");
|
|
318
|
+
var import_node_os = require("os");
|
|
319
|
+
var import_node_path = require("path");
|
|
320
|
+
function projectRoot(from = process.cwd()) {
|
|
321
|
+
for (let dir = (0, import_node_path.resolve)(from); ; dir = (0, import_node_path.dirname)(dir)) {
|
|
322
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
323
|
+
if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
var isPackage = (dir) => (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"));
|
|
327
|
+
function inProject(session, project) {
|
|
328
|
+
if (!session) return false;
|
|
329
|
+
if (session === project) return true;
|
|
330
|
+
if (!session.startsWith(project + import_node_path.sep) || !isPackage(project)) return false;
|
|
331
|
+
for (let dir = session; dir !== project; dir = (0, import_node_path.dirname)(dir)) {
|
|
332
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, ".git"))) return false;
|
|
333
|
+
if (dir !== session && isPackage(dir)) return false;
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
|
|
338
|
+
function privateDir(dir) {
|
|
339
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
340
|
+
const stat = (0, import_node_fs.lstatSync)(dir);
|
|
341
|
+
if (!stat.isDirectory())
|
|
342
|
+
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
343
|
+
if (process.getuid && stat.uid !== process.getuid())
|
|
344
|
+
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
345
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
346
|
+
(0, import_node_fs.chmodSync)(dir, 448);
|
|
347
|
+
return dir;
|
|
348
|
+
}
|
|
349
|
+
function stateDir() {
|
|
350
|
+
return privateDir(
|
|
351
|
+
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? (0, import_node_path.join)(process.env.XDG_RUNTIME_DIR, "agent-bridge") : (0, import_node_path.join)((0, import_node_os.tmpdir)(), `agent-bridge-${uid()}`))
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
var shortHash = (text) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
355
|
+
function socketPath(name, dir) {
|
|
356
|
+
if (process.platform === "win32")
|
|
357
|
+
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
358
|
+
const path = (0, import_node_path.join)(dir, `${name}.sock`);
|
|
359
|
+
if (Buffer.byteLength(path) <= 100) return path;
|
|
360
|
+
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
361
|
+
return (0, import_node_path.join)(short, `${shortHash(path)}.sock`);
|
|
362
|
+
}
|
|
363
|
+
var sessionFiles = (name, dir = stateDir()) => ({
|
|
364
|
+
state: (0, import_node_path.join)(dir, `${name}.json`),
|
|
365
|
+
error: (0, import_node_path.join)(dir, `${name}.error`),
|
|
366
|
+
log: (0, import_node_path.join)(dir, `${name}.log`),
|
|
367
|
+
socket: socketPath(name, dir)
|
|
368
|
+
});
|
|
369
|
+
function checkName(name) {
|
|
370
|
+
if (!/^[\w.-]{1,40}$/.test(name))
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
373
|
+
);
|
|
374
|
+
return name;
|
|
375
|
+
}
|
|
376
|
+
function isAlive(pid) {
|
|
377
|
+
try {
|
|
378
|
+
process.kill(pid, 0);
|
|
379
|
+
return true;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return error.code === "EPERM";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function removeSessionFiles(name, dir = stateDir()) {
|
|
385
|
+
const files = sessionFiles(name, dir);
|
|
386
|
+
(0, import_node_fs.rmSync)(files.state, { force: true });
|
|
387
|
+
if (process.platform !== "win32") (0, import_node_fs.rmSync)(files.socket, { force: true });
|
|
388
|
+
}
|
|
389
|
+
function readState(file) {
|
|
390
|
+
try {
|
|
391
|
+
return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function listSessions(dir = stateDir()) {
|
|
397
|
+
const live = [];
|
|
398
|
+
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
399
|
+
if (!entry.endsWith(".json")) continue;
|
|
400
|
+
const state = readState((0, import_node_path.join)(dir, entry));
|
|
401
|
+
if (!state) continue;
|
|
402
|
+
if (isAlive(state.pid)) live.push(state);
|
|
403
|
+
else removeSessionFiles(state.name, dir);
|
|
404
|
+
}
|
|
405
|
+
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
406
|
+
}
|
|
407
|
+
function readSession(name, dir = stateDir()) {
|
|
408
|
+
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
409
|
+
}
|
|
410
|
+
function pickSession(filter) {
|
|
411
|
+
if (filter.name) {
|
|
412
|
+
const named = readSession(checkName(filter.name));
|
|
413
|
+
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
414
|
+
return named;
|
|
415
|
+
}
|
|
416
|
+
const device = filter.device?.toLowerCase();
|
|
417
|
+
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
418
|
+
const project = projectRoot();
|
|
419
|
+
const matching = listSessions().filter(
|
|
420
|
+
(s) => !filter.skip?.(s) && (explicit || inProject(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)
|
|
421
|
+
);
|
|
422
|
+
if (!explicit && matching.length > 1 && matching.every((s) => s.project !== project))
|
|
423
|
+
return null;
|
|
424
|
+
if (matching.length > 1)
|
|
425
|
+
throw new Error(
|
|
426
|
+
`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`
|
|
427
|
+
);
|
|
428
|
+
return matching[0] ?? null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/client/metro-hint.ts
|
|
432
|
+
var range = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
433
|
+
var probe = {
|
|
434
|
+
ports: [...range(8081, 8090), ...range(8100, 8199)],
|
|
435
|
+
eachMs: 300,
|
|
436
|
+
totalMs: 1e3
|
|
437
|
+
};
|
|
438
|
+
var hostOf = (metro) => {
|
|
439
|
+
const colon = metro.lastIndexOf(":");
|
|
440
|
+
return colon > 0 && !metro.endsWith("]") ? metro.slice(0, colon) : metro;
|
|
441
|
+
};
|
|
442
|
+
async function answers(metro, signal) {
|
|
443
|
+
try {
|
|
444
|
+
const res = await fetch(`http://${metro}/status`, { signal });
|
|
445
|
+
return (await res.text()).includes("packager-status:running");
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function appsOn(metro, signal) {
|
|
451
|
+
try {
|
|
452
|
+
const res = await fetch(`http://${metro}/json/list`, { signal });
|
|
453
|
+
const targets = await res.json();
|
|
454
|
+
const names = targets.map(
|
|
455
|
+
(t) => `${t.title ?? "?"}${t.deviceName ? ` [${t.deviceName}]` : ""}`
|
|
456
|
+
);
|
|
457
|
+
return [...new Set(names)];
|
|
458
|
+
} catch {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function findMetros(metro) {
|
|
463
|
+
const host = hostOf(metro);
|
|
464
|
+
const candidates = new Set(probe.ports.map((port) => `${host}:${port}`));
|
|
465
|
+
try {
|
|
466
|
+
for (const s of listSessions()) candidates.add(s.metro);
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
candidates.delete(metro);
|
|
470
|
+
const deadline = AbortSignal.timeout(probe.totalMs);
|
|
471
|
+
const within2 = () => AbortSignal.any([deadline, AbortSignal.timeout(probe.eachMs)]);
|
|
472
|
+
const running = (await Promise.all(
|
|
473
|
+
[...candidates].map(async (m) => await answers(m, within2()) ? m : null)
|
|
474
|
+
)).filter((m) => m !== null);
|
|
475
|
+
return Promise.all(
|
|
476
|
+
running.map(async (m) => ({ metro: m, apps: await appsOn(m, within2()) }))
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
var label = (found) => `${found.metro} (${found.apps.length ? `${found.apps.length === 1 ? "app" : "apps"}: ${found.apps.join(", ")}` : "no app connected"})`;
|
|
480
|
+
var howToSet = (metro) => `Pass --metro ${metro} or set AGENT_BRIDGE_METRO=${metro}.`;
|
|
481
|
+
function elsewhere(found) {
|
|
482
|
+
const [only] = found;
|
|
483
|
+
if (found.length === 1 && only)
|
|
484
|
+
return `Metro is running on ${label(only)}. ${howToSet(only.metro)}`;
|
|
485
|
+
return `Metro is running on ${found.map(label).join("; ")}. ${howToSet("<host:port>")}`;
|
|
486
|
+
}
|
|
487
|
+
var errorText = (error) => {
|
|
488
|
+
if (!(error instanceof Error)) return String(error);
|
|
489
|
+
const code = error.code ?? "";
|
|
490
|
+
return `${error.message} ${code} ${error.cause ? errorText(error.cause) : ""}`;
|
|
491
|
+
};
|
|
492
|
+
var isRefused = (error) => /ECONNREFUSED|ConnectionRefused/.test(errorText(error));
|
|
493
|
+
var NO_APP = /No app is connected to Metro|No app matches/;
|
|
494
|
+
async function refusedMessage(metro) {
|
|
495
|
+
const found = await findMetros(metro);
|
|
496
|
+
if (found.length)
|
|
497
|
+
return `Nothing is listening on ${metro}. ${elsewhere(found)}`;
|
|
498
|
+
const host = hostOf(metro);
|
|
499
|
+
return `Nothing is listening on ${metro}, and no Metro answered on ${host} ports 8081-8090 or 8100-8199. Start Metro, or if it runs elsewhere, pass --metro <host:port> or set AGENT_BRIDGE_METRO=<host:port>.`;
|
|
500
|
+
}
|
|
501
|
+
async function noAppHint(metro, device) {
|
|
502
|
+
const causes = `Likely: the app isn't open or hasn't reloaded since the bridge was mounted, it uses another Metro${device ? `, or --device "${device}" doesn't match` : ""}.`;
|
|
503
|
+
const withApps = (await findMetros(metro)).filter((f) => f.apps.length);
|
|
504
|
+
return withApps.length ? `${causes} ${elsewhere(withApps)}` : causes;
|
|
505
|
+
}
|
|
506
|
+
async function refused(metro) {
|
|
507
|
+
try {
|
|
508
|
+
await fetch(`http://${metro}/status`, {
|
|
509
|
+
signal: AbortSignal.timeout(probe.eachMs)
|
|
510
|
+
});
|
|
511
|
+
return false;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
return isRefused(error);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function explainConnectError(error, metro, device) {
|
|
517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
518
|
+
if (NO_APP.test(message))
|
|
519
|
+
return new Error(`${message}
|
|
520
|
+
${await noAppHint(metro, device)}`, {
|
|
521
|
+
cause: error
|
|
522
|
+
});
|
|
523
|
+
if (await refused(metro))
|
|
524
|
+
return new Error(await refusedMessage(metro), { cause: error });
|
|
525
|
+
return error;
|
|
526
|
+
}
|
|
527
|
+
|
|
298
528
|
// src/client/cdp.ts
|
|
299
529
|
var import_ws2 = __toESM(require("ws"), 1);
|
|
300
530
|
var CannotEvaluate = class extends Error {
|
|
@@ -399,8 +629,8 @@ async function connectCdp(metro, device, signal, pin) {
|
|
|
399
629
|
target: targetLabel(target)
|
|
400
630
|
},
|
|
401
631
|
device: { ...info, name: `${info.name} (${targetLabel(target)})` },
|
|
402
|
-
call(tool, args, timeoutMs) {
|
|
403
|
-
const call = { id: newCallId(), tool, args };
|
|
632
|
+
call(tool, args, timeoutMs, extra) {
|
|
633
|
+
const call = { id: newCallId(), tool, args, ...extra };
|
|
404
634
|
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
405
635
|
evaluate(
|
|
406
636
|
`${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
|
|
@@ -483,6 +713,40 @@ function logLine(entry) {
|
|
|
483
713
|
}
|
|
484
714
|
|
|
485
715
|
// src/client/flow.ts
|
|
716
|
+
function deepMatches(actual, expected) {
|
|
717
|
+
if (expected instanceof RegExp) return typeof actual === "string" && expected.test(actual);
|
|
718
|
+
if (Array.isArray(expected))
|
|
719
|
+
return Array.isArray(actual) && actual.length === expected.length && expected.every((e, i) => deepMatches(actual[i], e));
|
|
720
|
+
if (expected && typeof expected === "object") {
|
|
721
|
+
const proto = Object.getPrototypeOf(expected);
|
|
722
|
+
if (proto !== Object.prototype && proto !== null)
|
|
723
|
+
throw new Error(
|
|
724
|
+
`A match can hold plain objects, arrays, values and RegExps, not a ${expected.constructor?.name ?? "class instance"}`
|
|
725
|
+
);
|
|
726
|
+
return !!actual && typeof actual === "object" && Object.entries(expected).every(
|
|
727
|
+
([k, v]) => deepMatches(actual[k], v)
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
return Object.is(actual, expected);
|
|
731
|
+
}
|
|
732
|
+
var MAX_SCREEN_LINES = 40;
|
|
733
|
+
function compactScreen(snapshot) {
|
|
734
|
+
const elements = snapshot?.elements;
|
|
735
|
+
if (!Array.isArray(elements)) return [];
|
|
736
|
+
const lines = elements.slice(0, MAX_SCREEN_LINES).map((raw) => {
|
|
737
|
+
const e = raw;
|
|
738
|
+
const parts = [String(e.kind ?? "element")];
|
|
739
|
+
if (e.testID) parts.push(`#${String(e.testID)}`);
|
|
740
|
+
for (const key of ["text", "label", "placeholder", "value"])
|
|
741
|
+
if (typeof e[key] === "string" && e[key]) parts.push(JSON.stringify(e[key]));
|
|
742
|
+
if (e.disabled) parts.push("disabled");
|
|
743
|
+
if (e.checked) parts.push(`checked=${String(e.checked)}`);
|
|
744
|
+
return parts.join(" ");
|
|
745
|
+
});
|
|
746
|
+
if (elements.length > MAX_SCREEN_LINES)
|
|
747
|
+
lines.push(`... ${elements.length - MAX_SCREEN_LINES} more`);
|
|
748
|
+
return lines;
|
|
749
|
+
}
|
|
486
750
|
function flowScenarios(flow) {
|
|
487
751
|
const declared = [
|
|
488
752
|
...flow.scenario === void 0 ? [] : [flow.scenario],
|
|
@@ -510,6 +774,10 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
510
774
|
let errors = 0;
|
|
511
775
|
const t0 = performance.now();
|
|
512
776
|
const notices = [];
|
|
777
|
+
let lost = false;
|
|
778
|
+
const noteOutcome = (error) => {
|
|
779
|
+
lost = error !== void 0 && !(error instanceof AgentBridgeCallError);
|
|
780
|
+
};
|
|
513
781
|
const report = (logs, notice) => {
|
|
514
782
|
errors += logs.length;
|
|
515
783
|
for (const e of logs) print(` ${logLine(e)}`);
|
|
@@ -521,28 +789,82 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
521
789
|
const timed = async (tool, ...args) => {
|
|
522
790
|
try {
|
|
523
791
|
const result = await bridge.timed(tool, ...args);
|
|
792
|
+
noteOutcome();
|
|
524
793
|
report(result.logs, result.notice);
|
|
525
794
|
return result;
|
|
526
795
|
} catch (error) {
|
|
796
|
+
noteOutcome(error);
|
|
527
797
|
report(failedLogs(error), failedNotice(error));
|
|
528
798
|
throw error;
|
|
529
799
|
}
|
|
530
800
|
};
|
|
531
801
|
const call = async (tool, ...args) => (await timed(tool, ...args)).value;
|
|
532
|
-
const
|
|
533
|
-
const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
|
|
534
|
-
const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
|
|
535
|
-
report(failedLogs(error), failedNotice(error));
|
|
536
|
-
throw error;
|
|
537
|
-
});
|
|
802
|
+
const record = (label2, ms) => {
|
|
538
803
|
total += ms;
|
|
539
804
|
print(
|
|
540
|
-
`${String(++n).padStart(2, "0")} ${
|
|
805
|
+
`${String(++n).padStart(2, "0")} ${label2.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
|
|
541
806
|
);
|
|
542
|
-
|
|
543
|
-
|
|
807
|
+
};
|
|
808
|
+
const step = async (label2, tool, ...given) => {
|
|
809
|
+
const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
|
|
810
|
+
const result = await timed(tool, ...args);
|
|
811
|
+
record(label2, result.ms);
|
|
812
|
+
return result.value;
|
|
813
|
+
};
|
|
814
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
815
|
+
const expectApi = {
|
|
816
|
+
text: async (target, o) => {
|
|
817
|
+
await step(`expect text ${JSON.stringify(target)}`, "screen.waitFor", target, o ?? {}).catch(
|
|
818
|
+
(error) => {
|
|
819
|
+
throw new Error(`expected ${JSON.stringify(target)} on screen: ${message(error)}`);
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
},
|
|
823
|
+
gone: async (target, o) => {
|
|
824
|
+
await step(`expect gone ${JSON.stringify(target)}`, "screen.waitFor", target, {
|
|
825
|
+
...o,
|
|
826
|
+
gone: true
|
|
827
|
+
}).catch((error) => {
|
|
828
|
+
throw new Error(`expected ${JSON.stringify(target)} to be off screen: ${message(error)}`);
|
|
829
|
+
});
|
|
830
|
+
},
|
|
831
|
+
route: async (pathname, o) => {
|
|
832
|
+
const label2 = `expect route ${pathname}`;
|
|
833
|
+
const t02 = performance.now();
|
|
834
|
+
const deadline = t02 + (o?.timeoutMs ?? 2e3);
|
|
835
|
+
for (; ; ) {
|
|
836
|
+
const route = await call("router.current").catch((error) => {
|
|
837
|
+
record(label2, performance.now() - t02);
|
|
838
|
+
throw error;
|
|
839
|
+
});
|
|
840
|
+
if (route?.pathname === pathname) return record(label2, performance.now() - t02);
|
|
841
|
+
if (performance.now() >= deadline) {
|
|
842
|
+
record(label2, performance.now() - t02);
|
|
843
|
+
throw new Error(`expected route ${pathname}, got ${route?.pathname ?? JSON.stringify(route)}`);
|
|
844
|
+
}
|
|
845
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
result: async (tool, args, match) => {
|
|
849
|
+
const value = await step(`expect ${tool}`, tool, args);
|
|
850
|
+
if (!deepMatches(value, match))
|
|
851
|
+
throw new Error(
|
|
852
|
+
`expected ${tool} to match ${JSON.stringify(match)}, got ${JSON.stringify(value)}`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
const failureState = async () => {
|
|
857
|
+
const has = (name) => bridge.tools().some((t) => t.name === name);
|
|
858
|
+
const state = {};
|
|
859
|
+
if (has("screen.snapshot"))
|
|
860
|
+
state.screen = await call("screen.snapshot").then(compactScreen).catch(() => void 0);
|
|
861
|
+
if (has("router.current") && !lost)
|
|
862
|
+
state.route = await call("router.current").catch(() => void 0);
|
|
863
|
+
return state;
|
|
544
864
|
};
|
|
545
865
|
const restoreErrors = [];
|
|
866
|
+
let flowErrors = 0;
|
|
867
|
+
let unreachable = false;
|
|
546
868
|
const applied = {};
|
|
547
869
|
if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
|
|
548
870
|
throw new Error(
|
|
@@ -557,10 +879,22 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
557
879
|
bridge: { ...bridge, timed, call },
|
|
558
880
|
call,
|
|
559
881
|
step,
|
|
560
|
-
scenarios: applied
|
|
882
|
+
scenarios: applied,
|
|
883
|
+
expect: expectApi
|
|
561
884
|
});
|
|
885
|
+
} catch (error) {
|
|
886
|
+
const state = lost ? {} : await failureState();
|
|
887
|
+
if (state.route !== void 0) print(` route: ${JSON.stringify(state.route)}`);
|
|
888
|
+
if (state.screen?.length) {
|
|
889
|
+
print(" on screen:");
|
|
890
|
+
for (const line of state.screen) print(` ${line}`);
|
|
891
|
+
}
|
|
892
|
+
options.onFailure?.(state);
|
|
893
|
+
throw error;
|
|
562
894
|
} finally {
|
|
563
|
-
|
|
895
|
+
flowErrors = errors;
|
|
896
|
+
unreachable = lost;
|
|
897
|
+
if (!lost && !options.keep && (needs.length || bridge.tools().some((t) => t.name === "bridge.restore"))) {
|
|
564
898
|
try {
|
|
565
899
|
const result = await step("restore", "bridge.restore");
|
|
566
900
|
for (const [name, value] of Object.entries(result ?? {})) {
|
|
@@ -568,17 +902,70 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
568
902
|
if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
|
|
569
903
|
}
|
|
570
904
|
} catch (error) {
|
|
905
|
+
unreachable = lost;
|
|
571
906
|
restoreErrors.push(
|
|
572
907
|
`bridge.restore: ${error instanceof Error ? error.message : String(error)}`
|
|
573
908
|
);
|
|
574
909
|
}
|
|
575
910
|
for (const e of restoreErrors) print(` ! restore failed: ${e}`);
|
|
576
911
|
}
|
|
912
|
+
options.onFinish?.({ steps: n, errors: flowErrors, restoreErrors, notices, unreachable });
|
|
577
913
|
}
|
|
578
914
|
print(
|
|
579
|
-
`${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${
|
|
915
|
+
`${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${flowErrors ? `, ${flowErrors} error${flowErrors === 1 ? "" : "s"}` : ""}${notices.length ? ", the app reloaded" : ""}`
|
|
580
916
|
);
|
|
581
|
-
return { steps: n, errors, restoreErrors, notices };
|
|
917
|
+
return { steps: n, errors: flowErrors, restoreErrors, notices, unreachable };
|
|
918
|
+
}
|
|
919
|
+
async function runFlows(bridge, entries, options = {}) {
|
|
920
|
+
const print = options.print ?? console.log;
|
|
921
|
+
const printError = options.single ? options.printError ?? console.error : print;
|
|
922
|
+
const runs = [];
|
|
923
|
+
let unreachable = false;
|
|
924
|
+
for (const entry of entries) {
|
|
925
|
+
const base = { file: entry.file, name: entry.name, restoreErrors: [], notices: [] };
|
|
926
|
+
const skip = unreachable ? "skipped: the app stopped answering in an earlier flow" : options.bail && runs.some((r) => r.status === "failed") ? "skipped: an earlier flow failed (--bail)" : void 0;
|
|
927
|
+
if (skip) {
|
|
928
|
+
runs.push({ ...base, status: "skipped", ms: 0, steps: 0, errors: 0, error: skip });
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (!options.single) print(`
|
|
932
|
+
${entry.name}`);
|
|
933
|
+
const t0 = performance.now();
|
|
934
|
+
let state;
|
|
935
|
+
let done = { steps: 0, errors: 0, restoreErrors: [], notices: [], unreachable: false };
|
|
936
|
+
const reasons = [];
|
|
937
|
+
try {
|
|
938
|
+
await runFlow(bridge, entry.module, {
|
|
939
|
+
...options,
|
|
940
|
+
onFailure: (s) => {
|
|
941
|
+
state = s;
|
|
942
|
+
},
|
|
943
|
+
onFinish: (r) => {
|
|
944
|
+
done = r;
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
} catch (error) {
|
|
948
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
949
|
+
printError(` FAILED: ${why}`);
|
|
950
|
+
reasons.push(why);
|
|
951
|
+
}
|
|
952
|
+
if (done.restoreErrors.length) reasons.push(`restore failed: ${done.restoreErrors.join("; ")}`);
|
|
953
|
+
if (options.strict && done.errors)
|
|
954
|
+
reasons.push(`the app logged ${done.errors} error${done.errors === 1 ? "" : "s"}`);
|
|
955
|
+
if (done.notices.length)
|
|
956
|
+
print(" ! the app reloaded: changes made before the reload could not be undone");
|
|
957
|
+
const { unreachable: gone, ...result } = done;
|
|
958
|
+
runs.push({
|
|
959
|
+
...base,
|
|
960
|
+
...result,
|
|
961
|
+
status: reasons.length ? "failed" : "passed",
|
|
962
|
+
ms: performance.now() - t0,
|
|
963
|
+
...reasons.length && { error: reasons.join("; ") },
|
|
964
|
+
...state && { state }
|
|
965
|
+
});
|
|
966
|
+
if (gone) unreachable = true;
|
|
967
|
+
}
|
|
968
|
+
return runs;
|
|
582
969
|
}
|
|
583
970
|
|
|
584
971
|
// src/client/session/client.ts
|
|
@@ -589,123 +976,27 @@ var import_node_readline = require("readline");
|
|
|
589
976
|
var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16)}`)}
|
|
590
977
|
`;
|
|
591
978
|
|
|
592
|
-
// src/client/session/state.ts
|
|
593
|
-
var import_node_crypto = require("crypto");
|
|
594
|
-
var import_node_fs = require("fs");
|
|
595
|
-
var import_node_os = require("os");
|
|
596
|
-
var import_node_path = require("path");
|
|
597
|
-
function projectRoot(from = process.cwd()) {
|
|
598
|
-
for (let dir = (0, import_node_path.resolve)(from); ; dir = (0, import_node_path.dirname)(dir)) {
|
|
599
|
-
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
600
|
-
if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
|
|
604
|
-
function privateDir(dir) {
|
|
605
|
-
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
606
|
-
const stat = (0, import_node_fs.lstatSync)(dir);
|
|
607
|
-
if (!stat.isDirectory())
|
|
608
|
-
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
609
|
-
if (process.getuid && stat.uid !== process.getuid())
|
|
610
|
-
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
611
|
-
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
612
|
-
(0, import_node_fs.chmodSync)(dir, 448);
|
|
613
|
-
return dir;
|
|
614
|
-
}
|
|
615
|
-
function stateDir() {
|
|
616
|
-
return privateDir(
|
|
617
|
-
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? (0, import_node_path.join)(process.env.XDG_RUNTIME_DIR, "agent-bridge") : (0, import_node_path.join)((0, import_node_os.tmpdir)(), `agent-bridge-${uid()}`))
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
var shortHash = (text) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
621
|
-
function socketPath(name, dir) {
|
|
622
|
-
if (process.platform === "win32")
|
|
623
|
-
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
624
|
-
const path = (0, import_node_path.join)(dir, `${name}.sock`);
|
|
625
|
-
if (Buffer.byteLength(path) <= 100) return path;
|
|
626
|
-
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
627
|
-
return (0, import_node_path.join)(short, `${shortHash(path)}.sock`);
|
|
628
|
-
}
|
|
629
|
-
var sessionFiles = (name, dir = stateDir()) => ({
|
|
630
|
-
state: (0, import_node_path.join)(dir, `${name}.json`),
|
|
631
|
-
error: (0, import_node_path.join)(dir, `${name}.error`),
|
|
632
|
-
log: (0, import_node_path.join)(dir, `${name}.log`),
|
|
633
|
-
socket: socketPath(name, dir)
|
|
634
|
-
});
|
|
635
|
-
function checkName(name) {
|
|
636
|
-
if (!/^[\w.-]{1,40}$/.test(name))
|
|
637
|
-
throw new Error(
|
|
638
|
-
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
639
|
-
);
|
|
640
|
-
return name;
|
|
641
|
-
}
|
|
642
|
-
function isAlive(pid) {
|
|
643
|
-
try {
|
|
644
|
-
process.kill(pid, 0);
|
|
645
|
-
return true;
|
|
646
|
-
} catch (error) {
|
|
647
|
-
return error.code === "EPERM";
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
function removeSessionFiles(name, dir = stateDir()) {
|
|
651
|
-
const files = sessionFiles(name, dir);
|
|
652
|
-
(0, import_node_fs.rmSync)(files.state, { force: true });
|
|
653
|
-
if (process.platform !== "win32") (0, import_node_fs.rmSync)(files.socket, { force: true });
|
|
654
|
-
}
|
|
655
|
-
function readState(file) {
|
|
656
|
-
try {
|
|
657
|
-
return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
658
|
-
} catch {
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
function listSessions(dir = stateDir()) {
|
|
663
|
-
const live = [];
|
|
664
|
-
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
665
|
-
if (!entry.endsWith(".json")) continue;
|
|
666
|
-
const state = readState((0, import_node_path.join)(dir, entry));
|
|
667
|
-
if (!state) continue;
|
|
668
|
-
if (isAlive(state.pid)) live.push(state);
|
|
669
|
-
else removeSessionFiles(state.name, dir);
|
|
670
|
-
}
|
|
671
|
-
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
672
|
-
}
|
|
673
|
-
function readSession(name, dir = stateDir()) {
|
|
674
|
-
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
675
|
-
}
|
|
676
|
-
function pickSession(filter) {
|
|
677
|
-
if (filter.name) {
|
|
678
|
-
const named = readSession(checkName(filter.name));
|
|
679
|
-
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
680
|
-
return named;
|
|
681
|
-
}
|
|
682
|
-
const device = filter.device?.toLowerCase();
|
|
683
|
-
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
684
|
-
const project = projectRoot();
|
|
685
|
-
const matching = listSessions().filter(
|
|
686
|
-
(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)
|
|
687
|
-
);
|
|
688
|
-
if (matching.length > 1)
|
|
689
|
-
throw new Error(
|
|
690
|
-
`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`
|
|
691
|
-
);
|
|
692
|
-
return matching[0] ?? null;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
979
|
// src/client/session/client.ts
|
|
980
|
+
var SessionLinkError = class extends Error {
|
|
981
|
+
constructor(message) {
|
|
982
|
+
super(message);
|
|
983
|
+
this.name = "SessionLinkError";
|
|
984
|
+
}
|
|
985
|
+
};
|
|
696
986
|
function openSessionLink(state) {
|
|
697
987
|
return new Promise((resolve2, reject) => {
|
|
698
988
|
const socket = (0, import_node_net.createConnection)(state.socket);
|
|
699
989
|
const waiting = /* @__PURE__ */ new Map();
|
|
700
990
|
let seq = 0;
|
|
701
991
|
let open = false;
|
|
702
|
-
|
|
992
|
+
let lastError = "";
|
|
993
|
+
const gone = () => `Session "${state.name}" closed the connection${lastError && `: ${lastError}`}`;
|
|
703
994
|
socket.once("connect", () => {
|
|
704
995
|
open = true;
|
|
705
996
|
resolve2({
|
|
706
997
|
request: (req) => new Promise((done, fail) => {
|
|
707
998
|
if (socket.destroyed || socket.writableEnded) {
|
|
708
|
-
fail(new
|
|
999
|
+
fail(new SessionLinkError(gone()));
|
|
709
1000
|
return;
|
|
710
1001
|
}
|
|
711
1002
|
const id = ++seq;
|
|
@@ -716,18 +1007,22 @@ function openSessionLink(state) {
|
|
|
716
1007
|
});
|
|
717
1008
|
});
|
|
718
1009
|
socket.on("error", (error) => {
|
|
1010
|
+
lastError = error.message;
|
|
719
1011
|
if (!open)
|
|
720
1012
|
reject(
|
|
721
|
-
new
|
|
1013
|
+
new SessionLinkError(
|
|
722
1014
|
`Session "${state.name}" isn't answering on ${state.socket}: ${error.message}`
|
|
723
1015
|
)
|
|
724
1016
|
);
|
|
725
1017
|
});
|
|
726
1018
|
socket.on("close", () => {
|
|
727
|
-
for (const [id, done] of waiting) done({ id, error: gone });
|
|
1019
|
+
for (const [id, done] of waiting) done({ id, error: gone(), linkLost: true });
|
|
728
1020
|
waiting.clear();
|
|
729
1021
|
});
|
|
730
|
-
(0, import_node_readline.createInterface)({ input: socket })
|
|
1022
|
+
const lines = (0, import_node_readline.createInterface)({ input: socket });
|
|
1023
|
+
lines.on("error", () => {
|
|
1024
|
+
});
|
|
1025
|
+
lines.on("line", (line) => {
|
|
731
1026
|
const res = JSON.parse(line);
|
|
732
1027
|
waiting.get(res.id)?.(res);
|
|
733
1028
|
waiting.delete(res.id);
|
|
@@ -741,10 +1036,16 @@ async function connectSession(options = {}) {
|
|
|
741
1036
|
device: options.device,
|
|
742
1037
|
transport: options.transport
|
|
743
1038
|
});
|
|
744
|
-
if (!state)
|
|
1039
|
+
if (!state) {
|
|
1040
|
+
const where = [
|
|
1041
|
+
explicitMetro(options.metro) && `on Metro ${explicitMetro(options.metro)}`,
|
|
1042
|
+
options.device && `on a device matching "${options.device}"`,
|
|
1043
|
+
options.transport && options.transport !== "auto" && `using ${options.transport}`
|
|
1044
|
+
].filter(Boolean);
|
|
745
1045
|
throw new Error(
|
|
746
|
-
|
|
1046
|
+
`No single session matches (looked for sessions ${where.length ? where.join(", ") : `started in ${projectRoot()}, or in an app folder under it`}). Start one with \`agent-bridge session start\`, or name it.`
|
|
747
1047
|
);
|
|
1048
|
+
}
|
|
748
1049
|
const link = await openSessionLink(state);
|
|
749
1050
|
const info = await link.request({ op: "info" });
|
|
750
1051
|
if (info.error || !info.device || !info.transport) {
|
|
@@ -761,6 +1062,7 @@ async function connectSession(options = {}) {
|
|
|
761
1062
|
});
|
|
762
1063
|
if (res.error || !res.result) {
|
|
763
1064
|
const why = res.error ?? "No result";
|
|
1065
|
+
if (res.linkLost) throw new SessionLinkError(why);
|
|
764
1066
|
throw res.notice ? new AgentBridgeCallError(tool, why, [], res.notice) : new Error(why);
|
|
765
1067
|
}
|
|
766
1068
|
const result = res.result;
|
|
@@ -813,10 +1115,12 @@ Warning: ${notice}` : ""}`);
|
|
|
813
1115
|
};
|
|
814
1116
|
async function connect(options = {}) {
|
|
815
1117
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
816
|
-
const conn = await openConnection(options)
|
|
1118
|
+
const conn = await openConnection(options).catch(async (error) => {
|
|
1119
|
+
throw await explainConnectError(error, metroHost(options.metro), options.device);
|
|
1120
|
+
});
|
|
817
1121
|
const timed = async (tool, ...args) => {
|
|
818
1122
|
const t0 = performance.now();
|
|
819
|
-
const result = await conn.call(tool, args, timeoutMs);
|
|
1123
|
+
const result = await conn.call(tool, args, callTimeout(timeoutMs, tool, args));
|
|
820
1124
|
const ms = performance.now() - t0;
|
|
821
1125
|
const logs = result.logs ?? [];
|
|
822
1126
|
if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
|
|
@@ -857,5 +1161,6 @@ async function listDevices(options = {}) {
|
|
|
857
1161
|
connectSession,
|
|
858
1162
|
listDevices,
|
|
859
1163
|
listSessions,
|
|
860
|
-
runFlow
|
|
1164
|
+
runFlow,
|
|
1165
|
+
runFlows
|
|
861
1166
|
});
|