@avasapp/agent-bridge 0.5.0 → 0.6.1

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 (61) hide show
  1. package/README.md +20 -6
  2. package/dist/adapters/ably.cjs +79 -3
  3. package/dist/adapters/ably.d.cts +2 -1
  4. package/dist/adapters/async-storage.cjs +158 -0
  5. package/dist/adapters/async-storage.d.cts +14 -0
  6. package/dist/adapters/device.cjs +1110 -0
  7. package/dist/adapters/device.d.cts +49 -0
  8. package/dist/adapters/expo-router.cjs +112 -6
  9. package/dist/adapters/expo-router.d.cts +1 -1
  10. package/dist/adapters/jotai.cjs +266 -0
  11. package/dist/adapters/jotai.d.cts +19 -0
  12. package/dist/adapters/react-native-mmkv.cjs +80 -0
  13. package/dist/adapters/react-native-mmkv.d.cts +3 -1
  14. package/dist/adapters/react-native-webview.d.cts +1 -1
  15. package/dist/adapters/react-navigation.cjs +293 -0
  16. package/dist/adapters/react-navigation.d.cts +55 -0
  17. package/dist/adapters/redux.cjs +322 -0
  18. package/dist/adapters/redux.d.cts +28 -0
  19. package/dist/adapters/secure-store.cjs +169 -0
  20. package/dist/adapters/secure-store.d.cts +28 -0
  21. package/dist/adapters/socket-io.cjs +77 -3
  22. package/dist/adapters/socket-io.d.cts +2 -1
  23. package/dist/adapters/tanstack-query.d.cts +1 -1
  24. package/dist/adapters/zustand.cjs +193 -22
  25. package/dist/adapters/zustand.d.cts +11 -15
  26. package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
  27. package/dist/cli.js +1383 -255
  28. package/dist/client/index.cjs +233 -27
  29. package/dist/client/index.d.ts +67 -4
  30. package/dist/client/index.js +5 -3
  31. package/dist/expo/index.d.cts +1 -1
  32. package/dist/network/index.cjs +320 -73
  33. package/dist/network/index.d.cts +18 -3
  34. package/dist/noop/async-storage.cjs +26 -0
  35. package/dist/noop/device.cjs +26 -0
  36. package/dist/noop/jotai.cjs +26 -0
  37. package/dist/noop/react-navigation.cjs +26 -0
  38. package/dist/noop/realtime.cjs +6 -1
  39. package/dist/noop/redux.cjs +28 -0
  40. package/dist/noop/secure-store.cjs +26 -0
  41. package/dist/realtime/index.cjs +71 -4
  42. package/dist/realtime/index.d.cts +21 -2
  43. package/dist/runtime/index.cjs +822 -140
  44. package/dist/runtime/index.d.cts +2 -2
  45. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  46. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  47. package/entries/async-storage.cjs +9 -0
  48. package/entries/device.cjs +9 -0
  49. package/entries/jotai.cjs +9 -0
  50. package/entries/react-navigation.cjs +9 -0
  51. package/entries/redux.cjs +9 -0
  52. package/entries/secure-store.cjs +9 -0
  53. package/package.json +32 -1
  54. package/skills/agent-bridge/SKILL.md +39 -8
  55. package/skills/agent-bridge/references/device.md +21 -0
  56. package/skills/agent-bridge/references/flows.md +13 -3
  57. package/skills/agent-bridge/references/network.md +19 -1
  58. package/skills/agent-bridge/references/realtime.md +1 -1
  59. package/skills/agent-bridge/references/restore.md +21 -3
  60. package/skills/agent-bridge/references/screen.md +8 -2
  61. package/skills/agent-bridge/references/state.md +21 -1
@@ -1,4 +1,5 @@
1
- import { T as Tools } from '../types-BmpykP1P.cjs';
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
+ import { R as Redact } from '../store-common-j0cHHvBr.cjs';
2
3
 
3
4
  /** Anything with zustand's store shape. */
4
5
  type StoreLike = {
@@ -8,24 +9,19 @@ type StoreLike = {
8
9
  * syntax, so zustand's overloads (replace: false | true) still fit.
9
10
  */
10
11
  setState(partial: Record<string, unknown>, replace?: boolean): void;
12
+ /** Present on stores made with zustand's `persist` middleware. */
13
+ persist?: {
14
+ getOptions?: () => {
15
+ partialize?: (state: never) => unknown;
16
+ };
17
+ hasHydrated?: () => boolean;
18
+ onFinishHydration?: (fn: () => void) => () => void;
19
+ };
11
20
  };
12
- /**
13
- * Keeps secrets out of what store.* prints. Either dotted paths per store
14
- * (`{ auth: ['accessToken', 'user.phone'] }`), or a hook called for the store
15
- * value and every nested value with its dotted path; what it returns is
16
- * printed instead.
17
- *
18
- * Paths are store paths. A `store.call` result that is a piece of the store's
19
- * state (same object, e.g. `() => get().user`) is matched by where it lives; a
20
- * copy or derived value (`() => ({ ...get().user })`) is not, so its paths are
21
- * matched relative to the result and store paths like `user.phone` won't apply.
22
- * Use the hook, or don't return secrets from actions.
23
- */
24
- type Redact = Record<string, string[]> | ((store: string, path: string, value: unknown) => unknown);
25
21
  type StoreToolsOptions = {
26
22
  redact?: Redact;
27
23
  };
28
24
  /** Read, merge into, and call actions on named stores. */
29
25
  declare function storeTools(stores: Record<string, StoreLike>, options?: StoreToolsOptions): Tools;
30
26
 
31
- export { type Redact, type StoreLike, type StoreToolsOptions, storeTools };
27
+ export { Redact, type StoreLike, type StoreToolsOptions, storeTools };
@@ -1,3 +1,18 @@
1
+ // src/client/call-timeout.ts
2
+ var WAITERS = {
3
+ "screen.waitFor": 2e3,
4
+ // The page helper adds 5 s to timeoutMs before it gives up.
5
+ "webview.waitFor": 7e3
6
+ };
7
+ var MAX_MS = 10 * 6e4;
8
+ function callTimeout(base, tool, args) {
9
+ const margin = WAITERS[tool];
10
+ const t = args[1]?.timeoutMs;
11
+ if (margin === void 0 || typeof t !== "number" || !(t > 0) || !Number.isFinite(t))
12
+ return base;
13
+ return Math.max(base, Math.min(t + margin, MAX_MS));
14
+ }
15
+
1
16
  // src/client/discover.ts
2
17
  var within = (ms, signal) => signal ? AbortSignal.any([AbortSignal.timeout(ms), signal]) : AbortSignal.timeout(ms);
3
18
  var DEFAULT_METRO = "localhost:8081";
@@ -232,7 +247,7 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
232
247
  transport: "expo",
233
248
  device: info,
234
249
  pin: { name: info.name, platform: info.platform },
235
- call(tool, args, timeoutMs) {
250
+ call(tool, args, timeoutMs, extra) {
236
251
  if (superseded)
237
252
  return Promise.resolve({
238
253
  id: "",
@@ -245,7 +260,8 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
245
260
  id: newCallId(),
246
261
  tool,
247
262
  args,
248
- to: info.deviceId
263
+ to: info.deviceId,
264
+ ...extra
249
265
  };
250
266
  const reply = pending.wait(call.id, tool, timeoutMs);
251
267
  send("call", call);
@@ -268,13 +284,24 @@ import {
268
284
  writeFileSync
269
285
  } from "fs";
270
286
  import { tmpdir, userInfo } from "os";
271
- import { dirname, join, resolve } from "path";
287
+ import { dirname, join, resolve, sep } from "path";
272
288
  function projectRoot(from = process.cwd()) {
273
289
  for (let dir = resolve(from); ; dir = dirname(dir)) {
274
290
  if (existsSync(join(dir, "package.json"))) return dir;
275
291
  if (dirname(dir) === dir) return resolve(from);
276
292
  }
277
293
  }
294
+ var isPackage = (dir) => existsSync(join(dir, "package.json"));
295
+ function inProject(session, project) {
296
+ if (!session) return false;
297
+ if (session === project) return true;
298
+ if (!session.startsWith(project + sep) || !isPackage(project)) return false;
299
+ for (let dir = session; dir !== project; dir = dirname(dir)) {
300
+ if (existsSync(join(dir, ".git"))) return false;
301
+ if (dir !== session && isPackage(dir)) return false;
302
+ }
303
+ return true;
304
+ }
278
305
  var uid = () => process.getuid?.() ?? userInfo().username;
279
306
  function privateDir(dir) {
280
307
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -374,8 +401,10 @@ function pickSession(filter) {
374
401
  const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
375
402
  const project = projectRoot();
376
403
  const matching = listSessions().filter(
377
- (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)
404
+ (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)
378
405
  );
406
+ if (!explicit && matching.length > 1 && matching.every((s) => s.project !== project))
407
+ return null;
379
408
  if (matching.length > 1)
380
409
  throw new Error(
381
410
  `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`
@@ -587,8 +616,8 @@ async function connectCdp(metro, device, signal, pin) {
587
616
  target: targetLabel(target)
588
617
  },
589
618
  device: { ...info, name: `${info.name} (${targetLabel(target)})` },
590
- call(tool, args, timeoutMs) {
591
- const call = { id: newCallId(), tool, args };
619
+ call(tool, args, timeoutMs, extra) {
620
+ const call = { id: newCallId(), tool, args, ...extra };
592
621
  const reply = pending.wait(call.id, tool, timeoutMs);
593
622
  evaluate(
594
623
  `${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
@@ -671,6 +700,40 @@ function logLine(entry) {
671
700
  }
672
701
 
673
702
  // src/client/flow.ts
703
+ function deepMatches(actual, expected) {
704
+ if (expected instanceof RegExp) return typeof actual === "string" && expected.test(actual);
705
+ if (Array.isArray(expected))
706
+ return Array.isArray(actual) && actual.length === expected.length && expected.every((e, i) => deepMatches(actual[i], e));
707
+ if (expected && typeof expected === "object") {
708
+ const proto = Object.getPrototypeOf(expected);
709
+ if (proto !== Object.prototype && proto !== null)
710
+ throw new Error(
711
+ `A match can hold plain objects, arrays, values and RegExps, not a ${expected.constructor?.name ?? "class instance"}`
712
+ );
713
+ return !!actual && typeof actual === "object" && Object.entries(expected).every(
714
+ ([k, v]) => deepMatches(actual[k], v)
715
+ );
716
+ }
717
+ return Object.is(actual, expected);
718
+ }
719
+ var MAX_SCREEN_LINES = 40;
720
+ function compactScreen(snapshot) {
721
+ const elements = snapshot?.elements;
722
+ if (!Array.isArray(elements)) return [];
723
+ const lines = elements.slice(0, MAX_SCREEN_LINES).map((raw) => {
724
+ const e = raw;
725
+ const parts = [String(e.kind ?? "element")];
726
+ if (e.testID) parts.push(`#${String(e.testID)}`);
727
+ for (const key of ["text", "label", "placeholder", "value"])
728
+ if (typeof e[key] === "string" && e[key]) parts.push(JSON.stringify(e[key]));
729
+ if (e.disabled) parts.push("disabled");
730
+ if (e.checked) parts.push(`checked=${String(e.checked)}`);
731
+ return parts.join(" ");
732
+ });
733
+ if (elements.length > MAX_SCREEN_LINES)
734
+ lines.push(`... ${elements.length - MAX_SCREEN_LINES} more`);
735
+ return lines;
736
+ }
674
737
  function flowScenarios(flow) {
675
738
  const declared = [
676
739
  ...flow.scenario === void 0 ? [] : [flow.scenario],
@@ -711,6 +774,10 @@ async function runFlow(bridge, flow, options = {}) {
711
774
  let errors = 0;
712
775
  const t0 = performance.now();
713
776
  const notices = [];
777
+ let lost = false;
778
+ const noteOutcome = (error) => {
779
+ lost = error !== void 0 && !(error instanceof AgentBridgeCallError);
780
+ };
714
781
  const report = (logs, notice) => {
715
782
  errors += logs.length;
716
783
  for (const e of logs) print(` ${logLine(e)}`);
@@ -722,28 +789,82 @@ async function runFlow(bridge, flow, options = {}) {
722
789
  const timed = async (tool, ...args) => {
723
790
  try {
724
791
  const result = await bridge.timed(tool, ...args);
792
+ noteOutcome();
725
793
  report(result.logs, result.notice);
726
794
  return result;
727
795
  } catch (error) {
796
+ noteOutcome(error);
728
797
  report(failedLogs(error), failedNotice(error));
729
798
  throw error;
730
799
  }
731
800
  };
732
801
  const call = async (tool, ...args) => (await timed(tool, ...args)).value;
733
- const step = async (label2, tool, ...given) => {
734
- const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
735
- const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
736
- report(failedLogs(error), failedNotice(error));
737
- throw error;
738
- });
802
+ const record = (label2, ms) => {
739
803
  total += ms;
740
804
  print(
741
805
  `${String(++n).padStart(2, "0")} ${label2.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
742
806
  );
743
- report(logs, notice);
744
- return value;
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;
745
864
  };
746
865
  const restoreErrors = [];
866
+ let flowErrors = 0;
867
+ let unreachable = false;
747
868
  const applied = {};
748
869
  if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
749
870
  throw new Error(
@@ -758,10 +879,22 @@ async function runFlow(bridge, flow, options = {}) {
758
879
  bridge: { ...bridge, timed, call },
759
880
  call,
760
881
  step,
761
- scenarios: applied
882
+ scenarios: applied,
883
+ expect: expectApi
762
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;
763
894
  } finally {
764
- if (needs.length) {
895
+ flowErrors = errors;
896
+ unreachable = lost;
897
+ if (!lost && !options.keep && (needs.length || bridge.tools().some((t) => t.name === "bridge.restore"))) {
765
898
  try {
766
899
  const result = await step("restore", "bridge.restore");
767
900
  for (const [name, value] of Object.entries(result ?? {})) {
@@ -769,17 +902,70 @@ async function runFlow(bridge, flow, options = {}) {
769
902
  if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
770
903
  }
771
904
  } catch (error) {
905
+ unreachable = lost;
772
906
  restoreErrors.push(
773
907
  `bridge.restore: ${error instanceof Error ? error.message : String(error)}`
774
908
  );
775
909
  }
776
910
  for (const e of restoreErrors) print(` ! restore failed: ${e}`);
777
911
  }
912
+ options.onFinish?.({ steps: n, errors: flowErrors, restoreErrors, notices, unreachable });
778
913
  }
779
914
  print(
780
- `${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" : ""}`
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" : ""}`
781
916
  );
782
- 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;
783
969
  }
784
970
 
785
971
  // src/client/session/client.ts
@@ -791,19 +977,26 @@ var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]
791
977
  `;
792
978
 
793
979
  // src/client/session/client.ts
980
+ var SessionLinkError = class extends Error {
981
+ constructor(message) {
982
+ super(message);
983
+ this.name = "SessionLinkError";
984
+ }
985
+ };
794
986
  function openSessionLink(state) {
795
987
  return new Promise((resolve2, reject) => {
796
988
  const socket = createConnection(state.socket);
797
989
  const waiting = /* @__PURE__ */ new Map();
798
990
  let seq = 0;
799
991
  let open = false;
800
- const gone = `Session "${state.name}" closed the connection`;
992
+ let lastError = "";
993
+ const gone = () => `Session "${state.name}" closed the connection${lastError && `: ${lastError}`}`;
801
994
  socket.once("connect", () => {
802
995
  open = true;
803
996
  resolve2({
804
997
  request: (req) => new Promise((done, fail) => {
805
998
  if (socket.destroyed || socket.writableEnded) {
806
- fail(new Error(gone));
999
+ fail(new SessionLinkError(gone()));
807
1000
  return;
808
1001
  }
809
1002
  const id = ++seq;
@@ -814,18 +1007,22 @@ function openSessionLink(state) {
814
1007
  });
815
1008
  });
816
1009
  socket.on("error", (error) => {
1010
+ lastError = error.message;
817
1011
  if (!open)
818
1012
  reject(
819
- new Error(
1013
+ new SessionLinkError(
820
1014
  `Session "${state.name}" isn't answering on ${state.socket}: ${error.message}`
821
1015
  )
822
1016
  );
823
1017
  });
824
1018
  socket.on("close", () => {
825
- for (const [id, done] of waiting) done({ id, error: gone });
1019
+ for (const [id, done] of waiting) done({ id, error: gone(), linkLost: true });
826
1020
  waiting.clear();
827
1021
  });
828
- createInterface({ input: socket }).on("line", (line) => {
1022
+ const lines = createInterface({ input: socket });
1023
+ lines.on("error", () => {
1024
+ });
1025
+ lines.on("line", (line) => {
829
1026
  const res = JSON.parse(line);
830
1027
  waiting.get(res.id)?.(res);
831
1028
  waiting.delete(res.id);
@@ -847,10 +1044,16 @@ async function connectSession(options = {}) {
847
1044
  device: options.device,
848
1045
  transport: options.transport
849
1046
  });
850
- if (!state)
1047
+ if (!state) {
1048
+ const where = [
1049
+ explicitMetro(options.metro) && `on Metro ${explicitMetro(options.metro)}`,
1050
+ options.device && `on a device matching "${options.device}"`,
1051
+ options.transport && options.transport !== "auto" && `using ${options.transport}`
1052
+ ].filter(Boolean);
851
1053
  throw new Error(
852
- "No single session matches. Start one with `agent-bridge session start`, or name it."
1054
+ `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.`
853
1055
  );
1056
+ }
854
1057
  const link = await openSessionLink(state);
855
1058
  const info = await link.request({ op: "info" });
856
1059
  if (info.error || !info.device || !info.transport) {
@@ -867,6 +1070,7 @@ async function connectSession(options = {}) {
867
1070
  });
868
1071
  if (res.error || !res.result) {
869
1072
  const why = res.error ?? "No result";
1073
+ if (res.linkLost) throw new SessionLinkError(why);
870
1074
  throw res.notice ? new AgentBridgeCallError(tool, why, [], res.notice) : new Error(why);
871
1075
  }
872
1076
  const result = res.result;
@@ -924,7 +1128,7 @@ async function connect(options = {}) {
924
1128
  });
925
1129
  const timed = async (tool, ...args) => {
926
1130
  const t0 = performance.now();
927
- const result = await conn.call(tool, args, timeoutMs);
1131
+ const result = await conn.call(tool, args, callTimeout(timeoutMs, tool, args));
928
1132
  const ms = performance.now() - t0;
929
1133
  const logs = result.logs ?? [];
930
1134
  if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
@@ -960,6 +1164,7 @@ async function listDevices(options = {}) {
960
1164
  }
961
1165
 
962
1166
  export {
1167
+ callTimeout,
963
1168
  explicitMetro,
964
1169
  metroHost,
965
1170
  RUNTIME_MARKER,
@@ -983,7 +1188,9 @@ export {
983
1188
  flowScenarios,
984
1189
  parseScenarioFlag,
985
1190
  runFlow,
1191
+ runFlows,
986
1192
  encodeLine,
1193
+ SessionLinkError,
987
1194
  sessionRequest,
988
1195
  connectSession,
989
1196
  AgentBridgeCallError,