@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.
Files changed (35) hide show
  1. package/README.md +220 -20
  2. package/dist/adapters/ably.cjs +22 -4
  3. package/dist/adapters/ably.d.cts +8 -1
  4. package/dist/adapters/expo-router.cjs +22 -1
  5. package/dist/adapters/expo-router.d.cts +5 -1
  6. package/dist/adapters/react-native-mmkv.cjs +1 -0
  7. package/dist/adapters/react-native-mmkv.d.cts +1 -1
  8. package/dist/adapters/react-native-webview.cjs +672 -0
  9. package/dist/adapters/react-native-webview.d.cts +79 -0
  10. package/dist/adapters/socket-io.cjs +3 -1
  11. package/dist/adapters/socket-io.d.cts +1 -1
  12. package/dist/adapters/tanstack-query.cjs +256 -17
  13. package/dist/adapters/tanstack-query.d.cts +1 -1
  14. package/dist/adapters/zustand.cjs +152 -12
  15. package/dist/adapters/zustand.d.cts +19 -3
  16. package/dist/{chunk-LBYZ5JHF.js → chunk-AJD3XTNT.js} +329 -46
  17. package/dist/cli.js +848 -98
  18. package/dist/client/index.cjs +311 -47
  19. package/dist/client/index.d.ts +64 -3
  20. package/dist/client/index.js +5 -3
  21. package/dist/expo/index.d.cts +1 -1
  22. package/dist/network/index.cjs +434 -66
  23. package/dist/network/index.d.cts +53 -3
  24. package/dist/noop/index.cjs +12 -0
  25. package/dist/noop/network.cjs +7 -1
  26. package/dist/noop/react-native-webview.cjs +31 -0
  27. package/dist/realtime/index.cjs +2 -1
  28. package/dist/realtime/index.d.cts +3 -1
  29. package/dist/runtime/index.cjs +1067 -111
  30. package/dist/runtime/index.d.cts +51 -5
  31. package/dist/types-BmpykP1P.d.cts +137 -0
  32. package/entries/react-native-webview.cjs +9 -0
  33. package/package.json +18 -6
  34. package/skills/agent-bridge/SKILL.md +226 -8
  35. 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).replace(/^https?:\/\//, "").replace(/\/+$/, "");
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: AbortSignal.timeout(3e3)
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: AbortSignal.timeout(3e3)
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
- function openSocket(url, headers) {
65
- return new Promise((resolve, reject) => {
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
- resolve(ws);
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((resolve, reject) => {
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
- resolve(result);
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(`ws://${metro}/expo-dev-plugins/broadcast`);
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((r) => setTimeout(r, discoveryMs));
141
- return { ws, devices, pending, send };
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(metro, discoveryMs);
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(await listCdpTargets(metro), device, targetLabel);
182
- const hostUri = await expoHostUri(metro);
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(target.webSocketDebuggerUrl, {
187
- Origin: origin
188
- }).catch(() => {
189
- throw new Error(closedEarly);
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((resolve, reject) => {
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
- await send("Runtime.enable");
234
- await send("Runtime.addBinding", { name: CDP_REPLY_BINDING });
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.close();
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 connectExpo(metro, options.device);
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 connectCdp(metro, options.device);
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
- `${error instanceof Error ? error.message : String(error)}${expoNote}`
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
- return matching.length === 1 ? matching[0] : null;
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((resolve, reject) => {
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
- resolve({
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(`${JSON.stringify({ ...req, id })}
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: metroHost(options.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) throw new Error(res.error ?? "No 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) throw new AgentBridgeCallError(tool, result.error, logs);
493
- const { id: _id, from: _from, ok: _ok, ms: appMs, value, ...extra } = result;
494
- return { ...extra, value, ms: res.ms ?? 0, appMs, logs };
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,