@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
@@ -34,23 +34,30 @@ __export(client_exports, {
34
34
  connect: () => connect,
35
35
  connectSession: () => connectSession,
36
36
  listDevices: () => listDevices,
37
- listSessions: () => listSessions
37
+ listSessions: () => listSessions,
38
+ runFlow: () => runFlow
38
39
  });
39
40
  module.exports = __toCommonJS(client_exports);
40
41
 
41
42
  // src/client/discover.ts
43
+ var within = (ms, signal) => signal ? AbortSignal.any([AbortSignal.timeout(ms), signal]) : AbortSignal.timeout(ms);
42
44
  var DEFAULT_METRO = "localhost:8081";
45
+ var stripScheme = (metro) => metro.replace(/^https?:\/\//, "").replace(/\/+$/, "");
46
+ function explicitMetro(metro) {
47
+ const given = metro ?? process.env.AGENT_BRIDGE_METRO;
48
+ return given ? stripScheme(given) : void 0;
49
+ }
43
50
  function metroHost(metro) {
44
- return (metro ?? process.env.AGENT_BRIDGE_METRO ?? DEFAULT_METRO).replace(/^https?:\/\//, "").replace(/\/+$/, "");
51
+ return stripScheme(metro ?? process.env.AGENT_BRIDGE_METRO ?? DEFAULT_METRO);
45
52
  }
46
- async function expoHostUri(metro) {
53
+ async function expoHostUri(metro, signal) {
47
54
  try {
48
55
  const res = await fetch(`http://${metro}/`, {
49
56
  headers: {
50
57
  "expo-platform": "ios",
51
58
  accept: "application/expo+json,application/json"
52
59
  },
53
- signal: AbortSignal.timeout(3e3)
60
+ signal: within(3e3, signal)
54
61
  });
55
62
  if (!res.ok) return null;
56
63
  const manifest = await res.json();
@@ -59,9 +66,9 @@ async function expoHostUri(metro) {
59
66
  return null;
60
67
  }
61
68
  }
62
- async function listCdpTargets(metro) {
69
+ async function listCdpTargets(metro, signal) {
63
70
  const res = await fetch(`http://${metro}/json/list`, {
64
- signal: AbortSignal.timeout(3e3)
71
+ signal: within(3e3, signal)
65
72
  });
66
73
  if (!res.ok)
67
74
  throw new Error(
@@ -100,21 +107,43 @@ function toAsciiJson(value) {
100
107
 
101
108
  // src/client/connection.ts
102
109
  var import_ws = __toESM(require("ws"), 1);
103
- function openSocket(url, headers) {
104
- return new Promise((resolve, reject) => {
110
+ var OPEN_SOCKET_MS = 5e3;
111
+ var OpenGaveUp = class extends Error {
112
+ };
113
+ function openSocket(url, headers, signal, timeoutMs = OPEN_SOCKET_MS) {
114
+ return new Promise((resolve2, reject) => {
105
115
  const ws = new import_ws.default(url, headers ? { headers } : void 0);
106
116
  let opened = false;
117
+ const giveUp = (why) => {
118
+ if (opened) return;
119
+ reject(new OpenGaveUp(`Could not open ${url}: ${why}`));
120
+ ws.terminate();
121
+ };
122
+ const timer = setTimeout(
123
+ () => giveUp(`no answer within ${timeoutMs} ms`),
124
+ timeoutMs
125
+ );
126
+ const onAbort = () => giveUp("aborted");
127
+ if (signal?.aborted) onAbort();
128
+ else signal?.addEventListener("abort", onAbort, { once: true });
129
+ const settled = () => {
130
+ clearTimeout(timer);
131
+ signal?.removeEventListener("abort", onAbort);
132
+ };
107
133
  ws.on("error", (event) => {
108
134
  if (opened) return;
135
+ settled();
109
136
  const message = event?.message ?? String(event);
110
137
  reject(new Error(`Could not open ${url}: ${message}`));
111
138
  });
112
139
  ws.once("close", () => {
140
+ settled();
113
141
  if (!opened) reject(new Error(`${url} closed before opening`));
114
142
  });
115
143
  ws.once("open", () => {
144
+ settled();
116
145
  opened = true;
117
- resolve(ws);
146
+ resolve2(ws);
118
147
  });
119
148
  });
120
149
  }
@@ -123,7 +152,7 @@ function createPending() {
123
152
  const waiting = /* @__PURE__ */ new Map();
124
153
  return {
125
154
  wait(id, tool, timeoutMs) {
126
- return new Promise((resolve, reject) => {
155
+ return new Promise((resolve2, reject) => {
127
156
  const timer = setTimeout(() => {
128
157
  waiting.delete(id);
129
158
  reject(new Error(`No reply to "${tool}" within ${timeoutMs} ms`));
@@ -131,7 +160,7 @@ function createPending() {
131
160
  waiting.set(id, (result) => {
132
161
  clearTimeout(timer);
133
162
  waiting.delete(id);
134
- resolve(result);
163
+ resolve2(result);
135
164
  });
136
165
  });
137
166
  },
@@ -148,10 +177,15 @@ function createPending() {
148
177
 
149
178
  // src/client/expo.ts
150
179
  var deviceLabel = (d) => `${d.name} (${d.platform}, ${d.deviceId})`;
151
- async function openBroadcast(metro, discoveryMs) {
152
- const ws = await openSocket(`ws://${metro}/expo-dev-plugins/broadcast`);
180
+ async function openBroadcast(metro, discoveryMs, signal) {
181
+ const ws = await openSocket(
182
+ `ws://${metro}/expo-dev-plugins/broadcast`,
183
+ void 0,
184
+ signal
185
+ );
153
186
  const devices = /* @__PURE__ */ new Map();
154
187
  const pending = createPending();
188
+ const announced = [];
155
189
  ws.on("message", (data, isBinary) => {
156
190
  if (isBinary) return;
157
191
  let frame;
@@ -163,7 +197,9 @@ async function openBroadcast(metro, discoveryMs) {
163
197
  if (frame.messageKey?.pluginName !== PLUGIN_NAME) return;
164
198
  if (frame.messageKey.method === "hello:reply") {
165
199
  const info = frame.payload;
200
+ const fresh = !devices.has(info.deviceId);
166
201
  devices.set(info.deviceId, info);
202
+ if (fresh) for (const listener of announced) listener(info);
167
203
  } else if (frame.messageKey.method === "result") {
168
204
  pending.settle(frame.payload);
169
205
  }
@@ -176,16 +212,32 @@ async function openBroadcast(metro, discoveryMs) {
176
212
  })
177
213
  );
178
214
  send("hello", {});
179
- await new Promise((r) => setTimeout(r, discoveryMs));
180
- return { ws, devices, pending, send };
215
+ await new Promise((resolve2, reject) => {
216
+ const onAbort = () => {
217
+ clearTimeout(timer);
218
+ ws.terminate();
219
+ reject(new Error("aborted while waiting for hello replies"));
220
+ };
221
+ const timer = setTimeout(() => {
222
+ signal?.removeEventListener("abort", onAbort);
223
+ resolve2();
224
+ }, discoveryMs);
225
+ if (signal?.aborted) onAbort();
226
+ else signal?.addEventListener("abort", onAbort, { once: true });
227
+ });
228
+ return { ws, devices, pending, send, announced };
181
229
  }
182
230
  async function listExpoDevices(metro, discoveryMs = 600) {
183
231
  const { ws, devices } = await openBroadcast(metro, discoveryMs);
184
232
  ws.close();
185
233
  return [...devices.values()];
186
234
  }
187
- async function connectExpo(metro, device, discoveryMs = 600) {
188
- const { ws, devices, pending, send } = await openBroadcast(metro, discoveryMs);
235
+ async function connectExpo(metro, device, discoveryMs = 600, signal) {
236
+ const { ws, devices, pending, send, announced } = await openBroadcast(
237
+ metro,
238
+ discoveryMs,
239
+ signal
240
+ );
189
241
  let info;
190
242
  try {
191
243
  info = pickOne([...devices.values()], device, deviceLabel);
@@ -193,10 +245,25 @@ async function connectExpo(metro, device, discoveryMs = 600) {
193
245
  ws.close();
194
246
  throw error;
195
247
  }
248
+ let superseded = null;
249
+ announced.push((next) => {
250
+ if (superseded || next.name !== info.name || next.platform !== info.platform)
251
+ return;
252
+ superseded = `${info.name} announced a new bridge (${next.deviceId}); the app reloaded`;
253
+ pending.failAll(superseded);
254
+ });
196
255
  return {
197
256
  transport: "expo",
198
257
  device: info,
199
258
  call(tool, args, timeoutMs) {
259
+ if (superseded)
260
+ return Promise.resolve({
261
+ id: "",
262
+ from: "",
263
+ ok: false,
264
+ error: superseded,
265
+ ms: 0
266
+ });
200
267
  const call = {
201
268
  id: newCallId(),
202
269
  tool,
@@ -216,17 +283,26 @@ var import_ws2 = __toESM(require("ws"), 1);
216
283
  var CannotEvaluate = class extends Error {
217
284
  };
218
285
  var targetLabel = (t) => `${t.title}${t.deviceName ? ` [${t.deviceName}]` : ""}`;
219
- async function connectCdp(metro, device) {
220
- const target = pickOne(await listCdpTargets(metro), device, targetLabel);
221
- const hostUri = await expoHostUri(metro);
286
+ async function connectCdp(metro, device, signal) {
287
+ const target = pickOne(
288
+ await listCdpTargets(metro, signal),
289
+ device,
290
+ targetLabel
291
+ );
292
+ const hostUri = await expoHostUri(metro, signal);
222
293
  const port = metro.split(":")[1] ?? "8081";
223
294
  const origin = hostUri ? `http://${hostUri}` : `http://localhost:${port}`;
224
295
  const closedEarly = `Metro closed the debugger socket. Origin sent: ${origin}. Is that the host Metro advertises?`;
225
- const ws = await openSocket(target.webSocketDebuggerUrl, {
226
- Origin: origin
227
- }).catch(() => {
228
- throw new Error(closedEarly);
296
+ const ws = await openSocket(
297
+ target.webSocketDebuggerUrl,
298
+ { Origin: origin },
299
+ signal
300
+ ).catch((error) => {
301
+ throw error instanceof OpenGaveUp ? error : new Error(closedEarly);
229
302
  });
303
+ const dropOnAbort = () => ws.terminate();
304
+ if (signal?.aborted) dropOnAbort();
305
+ else signal?.addEventListener("abort", dropOnAbort, { once: true });
230
306
  let seq = 0;
231
307
  let answered = false;
232
308
  const commands = /* @__PURE__ */ new Map();
@@ -247,13 +323,13 @@ async function connectCdp(metro, device) {
247
323
  commands.clear();
248
324
  pending.failAll(reason);
249
325
  });
250
- const send = (method, params = {}) => new Promise((resolve, reject) => {
326
+ const send = (method, params = {}) => new Promise((resolve2, reject) => {
251
327
  if (ws.readyState !== import_ws2.default.OPEN) {
252
328
  reject(new Error(answered ? "The debugger socket closed" : closedEarly));
253
329
  return;
254
330
  }
255
331
  const id = ++seq;
256
- commands.set(id, { resolve, reject });
332
+ commands.set(id, { resolve: resolve2, reject });
257
333
  ws.send(JSON.stringify({ id, method, params }));
258
334
  });
259
335
  const evaluate = async (expression) => {
@@ -269,25 +345,35 @@ async function connectCdp(metro, device) {
269
345
  }
270
346
  return message.result?.result?.value;
271
347
  };
272
- await send("Runtime.enable");
273
- await send("Runtime.addBinding", { name: CDP_REPLY_BINDING });
348
+ try {
349
+ await send("Runtime.enable");
350
+ await send("Runtime.addBinding", { name: CDP_REPLY_BINDING });
351
+ } catch (error) {
352
+ ws.terminate();
353
+ signal?.removeEventListener("abort", dropOnAbort);
354
+ throw error;
355
+ }
274
356
  let info;
275
357
  try {
276
358
  info = JSON.parse(
277
359
  String(await evaluate(`${CDP_GLOBAL}.info()`))
278
360
  );
279
361
  } catch (error) {
280
- ws.close();
362
+ ws.terminate();
363
+ signal?.removeEventListener("abort", dropOnAbort);
281
364
  throw new Error(
282
365
  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?`
283
366
  );
284
367
  }
368
+ signal?.removeEventListener("abort", dropOnAbort);
285
369
  return {
286
370
  transport: "cdp",
287
371
  device: { ...info, name: `${info.name} (${targetLabel(target)})` },
288
372
  async call(tool, args, timeoutMs) {
289
373
  const call = { id: newCallId(), tool, args };
290
374
  const reply = pending.wait(call.id, tool, timeoutMs);
375
+ reply.catch(() => {
376
+ });
291
377
  try {
292
378
  await evaluate(
293
379
  `${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
@@ -308,37 +394,186 @@ async function connectCdp(metro, device) {
308
394
  }
309
395
 
310
396
  // src/client/open.ts
397
+ var OPEN_MS = 15e3;
398
+ var abortable = (work, signal) => new Promise((resolve2, reject) => {
399
+ const onAbort = () => reject(new Error("aborted"));
400
+ signal.addEventListener("abort", onAbort, { once: true });
401
+ work.then(resolve2, reject).finally(() => {
402
+ signal.removeEventListener("abort", onAbort);
403
+ });
404
+ });
405
+ function phase(options, run) {
406
+ const ms = options.timeoutMs ?? OPEN_MS;
407
+ const timeout = AbortSignal.timeout(ms);
408
+ const signal = options.signal ? AbortSignal.any([timeout, options.signal]) : timeout;
409
+ const attempt = run(signal);
410
+ attempt.then(
411
+ (late) => {
412
+ if (signal.aborted) late.close();
413
+ },
414
+ () => {
415
+ }
416
+ );
417
+ return abortable(attempt, signal).catch((error) => {
418
+ if (!signal.aborted) throw error;
419
+ throw new OpenGaveUp(
420
+ timeout.aborted ? `Timed out after ${ms} ms opening a connection to Metro` : "Connecting was aborted"
421
+ );
422
+ });
423
+ }
311
424
  async function openConnection(options = {}) {
312
425
  const metro = metroHost(options.metro);
313
426
  const want = options.transport ?? "auto";
314
427
  let expoError;
315
428
  if (want !== "cdp") {
316
429
  try {
317
- return await connectExpo(metro, options.device);
430
+ return await phase(
431
+ options,
432
+ (signal) => connectExpo(metro, options.device, void 0, signal)
433
+ );
318
434
  } catch (error) {
319
- if (want === "expo") throw error;
435
+ if (want === "expo" || options.signal?.aborted) throw error;
320
436
  expoError = error;
321
437
  }
322
438
  }
323
439
  try {
324
- return await connectCdp(metro, options.device);
440
+ return await phase(
441
+ options,
442
+ (signal) => connectCdp(metro, options.device, signal)
443
+ );
325
444
  } catch (error) {
326
445
  const expoNote = expoError ? ` (Expo socket: ${String(expoError)})` : "";
446
+ const text = `${error instanceof Error ? error.message : String(error)}${expoNote}`;
447
+ throw error instanceof OpenGaveUp ? new OpenGaveUp(text) : new Error(text);
448
+ }
449
+ }
450
+
451
+ // src/client/log-lines.ts
452
+ function logLine(entry) {
453
+ const where = entry.during ? ` during ${entry.during}` : entry.after ? ` after ${entry.after}` : "";
454
+ const first = entry.message.split("\n")[0] ?? "";
455
+ const text = first.length > 300 ? `${first.slice(0, 300)}...` : first;
456
+ return `! ${entry.level}${where}: ${text}`;
457
+ }
458
+
459
+ // src/client/flow.ts
460
+ function flowScenarios(flow) {
461
+ const declared = [
462
+ ...flow.scenario === void 0 ? [] : [flow.scenario],
463
+ ...flow.scenarios ?? []
464
+ ];
465
+ return declared.map((s) => {
466
+ if (typeof s === "string") return { name: s };
467
+ if (s && typeof s === "object" && typeof s.name === "string") return s;
468
+ throw new Error(
469
+ `A flow's scenario is a name or { name, options }, not ${JSON.stringify(s)}`
470
+ );
471
+ });
472
+ }
473
+ function mergeScenarios(declared, extra) {
474
+ const names = new Set(extra.map((s) => s.name));
475
+ return [...declared.filter((s) => !names.has(s.name)), ...extra];
476
+ }
477
+ var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
478
+ var failedNotice = (error) => error instanceof AgentBridgeCallError ? error.notice : void 0;
479
+ async function runFlow(bridge, flow, options = {}) {
480
+ const print = options.print ?? console.log;
481
+ const needs = mergeScenarios(flowScenarios(flow), options.scenarios ?? []);
482
+ let n = 0;
483
+ let total = 0;
484
+ let errors = 0;
485
+ const t0 = performance.now();
486
+ const notices = [];
487
+ const report = (logs, notice) => {
488
+ errors += logs.length;
489
+ for (const e of logs) print(` ${logLine(e)}`);
490
+ if (notice) {
491
+ notices.push(notice);
492
+ print(` ! ${notice}`);
493
+ }
494
+ };
495
+ const timed = async (tool, ...args) => {
496
+ try {
497
+ const result = await bridge.timed(tool, ...args);
498
+ report(result.logs, result.notice);
499
+ return result;
500
+ } catch (error) {
501
+ report(failedLogs(error), failedNotice(error));
502
+ throw error;
503
+ }
504
+ };
505
+ const call = async (tool, ...args) => (await timed(tool, ...args)).value;
506
+ const step = async (label, tool, ...given) => {
507
+ const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
508
+ const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
509
+ report(failedLogs(error), failedNotice(error));
510
+ throw error;
511
+ });
512
+ total += ms;
513
+ print(
514
+ `${String(++n).padStart(2, "0")} ${label.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
515
+ );
516
+ report(logs, notice);
517
+ return value;
518
+ };
519
+ const restoreErrors = [];
520
+ const applied = {};
521
+ if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
327
522
  throw new Error(
328
- `${error instanceof Error ? error.message : String(error)}${expoNote}`
523
+ `The flow needs scenario "${needs[0].name}", but the app defines none: pass \`scenarios\` to useAgentBridge.`
329
524
  );
525
+ try {
526
+ for (const { name, options: options2 } of needs) {
527
+ const args = options2 === void 0 ? [name] : [name, options2];
528
+ applied[name] = await step(`scenario ${name}`, "scenario.apply", ...args);
529
+ }
530
+ await flow.default({
531
+ bridge: { ...bridge, timed, call },
532
+ call,
533
+ step,
534
+ scenarios: applied
535
+ });
536
+ } finally {
537
+ if (needs.length) {
538
+ try {
539
+ const result = await step("restore", "bridge.restore");
540
+ for (const [name, value] of Object.entries(result ?? {})) {
541
+ const error = value?.error;
542
+ if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
543
+ }
544
+ } catch (error) {
545
+ restoreErrors.push(
546
+ `bridge.restore: ${error instanceof Error ? error.message : String(error)}`
547
+ );
548
+ }
549
+ for (const e of restoreErrors) print(` ! restore failed: ${e}`);
550
+ }
330
551
  }
552
+ print(
553
+ `${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" : ""}`
554
+ );
555
+ return { steps: n, errors, restoreErrors, notices };
331
556
  }
332
557
 
333
558
  // src/client/session/client.ts
334
559
  var import_node_net = require("net");
335
560
  var import_node_readline = require("readline");
336
561
 
562
+ // src/client/session/protocol.ts
563
+ var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16)}`)}
564
+ `;
565
+
337
566
  // src/client/session/state.ts
338
567
  var import_node_crypto = require("crypto");
339
568
  var import_node_fs = require("fs");
340
569
  var import_node_os = require("os");
341
570
  var import_node_path = require("path");
571
+ function projectRoot(from = process.cwd()) {
572
+ for (let dir = (0, import_node_path.resolve)(from); ; dir = (0, import_node_path.dirname)(dir)) {
573
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
574
+ if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
575
+ }
576
+ }
342
577
  var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
343
578
  function privateDir(dir) {
344
579
  (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
@@ -419,15 +654,21 @@ function pickSession(filter) {
419
654
  return named;
420
655
  }
421
656
  const device = filter.device?.toLowerCase();
657
+ const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
658
+ const project = projectRoot();
422
659
  const matching = listSessions().filter(
423
- (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)
660
+ (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)
424
661
  );
425
- return matching.length === 1 ? matching[0] : null;
662
+ if (matching.length > 1)
663
+ throw new Error(
664
+ `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`
665
+ );
666
+ return matching[0] ?? null;
426
667
  }
427
668
 
428
669
  // src/client/session/client.ts
429
670
  function openSessionLink(state) {
430
- return new Promise((resolve, reject) => {
671
+ return new Promise((resolve2, reject) => {
431
672
  const socket = (0, import_node_net.createConnection)(state.socket);
432
673
  const waiting = /* @__PURE__ */ new Map();
433
674
  let seq = 0;
@@ -435,7 +676,7 @@ function openSessionLink(state) {
435
676
  const gone = `Session "${state.name}" closed the connection`;
436
677
  socket.once("connect", () => {
437
678
  open = true;
438
- resolve({
679
+ resolve2({
439
680
  request: (req) => new Promise((done, fail) => {
440
681
  if (socket.destroyed || socket.writableEnded) {
441
682
  fail(new Error(gone));
@@ -443,8 +684,7 @@ function openSessionLink(state) {
443
684
  }
444
685
  const id = ++seq;
445
686
  waiting.set(id, done);
446
- socket.write(`${JSON.stringify({ ...req, id })}
447
- `);
687
+ socket.write(encodeLine({ ...req, id }));
448
688
  }),
449
689
  close: () => socket.end()
450
690
  });
@@ -471,7 +711,7 @@ function openSessionLink(state) {
471
711
  async function connectSession(options = {}) {
472
712
  const state = pickSession({
473
713
  name: options.name ?? process.env.AGENT_BRIDGE_SESSION,
474
- metro: metroHost(options.metro),
714
+ metro: explicitMetro(options.metro),
475
715
  device: options.device,
476
716
  transport: options.transport
477
717
  });
@@ -493,12 +733,32 @@ async function connectSession(options = {}) {
493
733
  args,
494
734
  timeoutMs: options.timeoutMs
495
735
  });
496
- if (res.error || !res.result) throw new Error(res.error ?? "No result");
736
+ if (res.error || !res.result) {
737
+ const why = res.error ?? "No result";
738
+ throw res.notice ? new AgentBridgeCallError(tool, why, [], res.notice) : new Error(why);
739
+ }
497
740
  const result = res.result;
498
741
  const logs = result.logs ?? [];
499
- if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
500
- const { id: _id, from: _from, ok: _ok, ms: appMs, value, ...extra } = result;
501
- return { ...extra, value, ms: res.ms ?? 0, appMs, logs };
742
+ if (!result.ok)
743
+ throw new AgentBridgeCallError(tool, result.error, logs, res.notice);
744
+ const {
745
+ id: _id,
746
+ from: _from,
747
+ ok: _ok,
748
+ ms: appMs,
749
+ value,
750
+ loadId: _loadId,
751
+ pending: _pending,
752
+ ...extra
753
+ } = result;
754
+ return {
755
+ ...extra,
756
+ value,
757
+ ms: res.ms ?? 0,
758
+ appMs,
759
+ logs,
760
+ ...res.notice && { notice: res.notice }
761
+ };
502
762
  };
503
763
  return {
504
764
  session: info.state ?? state,
@@ -513,14 +773,17 @@ async function connectSession(options = {}) {
513
773
 
514
774
  // src/client/index.ts
515
775
  var AgentBridgeCallError = class extends Error {
516
- constructor(tool, message, logs = []) {
517
- super(`${tool}: ${message}`);
776
+ constructor(tool, message, logs = [], notice) {
777
+ super(`${tool}: ${message}${notice ? `
778
+ Warning: ${notice}` : ""}`);
518
779
  this.tool = tool;
519
780
  this.logs = logs;
781
+ this.notice = notice;
520
782
  this.name = "AgentBridgeCallError";
521
783
  }
522
784
  tool;
523
785
  logs;
786
+ notice;
524
787
  };
525
788
  async function connect(options = {}) {
526
789
  const timeoutMs = options.timeoutMs ?? 1e4;
@@ -567,5 +830,6 @@ async function listDevices(options = {}) {
567
830
  connect,
568
831
  connectSession,
569
832
  listDevices,
570
- listSessions
833
+ listSessions,
834
+ runFlow
571
835
  });