@avasapp/agent-bridge 0.1.0 → 0.3.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.
@@ -22,6 +22,7 @@ var runtime_exports = {};
22
22
  __export(runtime_exports, {
23
23
  RUNTIME_MARKER: () => RUNTIME_MARKER,
24
24
  cdpTransport: () => cdpTransport,
25
+ createGate: () => createGate,
25
26
  settle: () => settle,
26
27
  startAgentBridge: () => startAgentBridge,
27
28
  useAgentBridge: () => useAgentBridge
@@ -73,14 +74,15 @@ function logTools(logs) {
73
74
  // src/runtime/tools/restore.ts
74
75
  var SUFFIX = ".restore";
75
76
  var SELF = "bridge.restore";
77
+ var LAST = "scenario.restore";
76
78
  function restoreTools(getTools) {
77
79
  return {
78
80
  [SELF]: {
79
- description: "Undo what the agent changed: runs every *.restore tool in name order and returns each result or error.",
81
+ description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
80
82
  run: async () => {
81
83
  const tools = getTools();
82
84
  const results = {};
83
- const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort();
85
+ const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort((a, b) => Number(a === LAST) - Number(b === LAST) || a.localeCompare(b));
84
86
  for (const name of names) {
85
87
  const definition = tools[name];
86
88
  const run = typeof definition === "function" ? definition : definition.run;
@@ -98,6 +100,159 @@ function restoreTools(getTools) {
98
100
  };
99
101
  }
100
102
 
103
+ // src/runtime/options-schema.ts
104
+ var import_json_schema = require("@cfworker/json-schema");
105
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
106
+ var CASCADE = /* @__PURE__ */ new Set([
107
+ "properties",
108
+ "patternProperties",
109
+ "additionalProperties",
110
+ "unevaluatedProperties",
111
+ "items",
112
+ "prefixItems",
113
+ "unevaluatedItems",
114
+ "allOf",
115
+ "if",
116
+ "then",
117
+ "else",
118
+ "dependentSchemas",
119
+ "$ref",
120
+ "false"
121
+ ]);
122
+ var path = (instanceLocation) => instanceLocation === "#" ? "options" : `options${instanceLocation.slice(1)}`;
123
+ function unknownKeys(schema, value, at, out) {
124
+ if (!isObject(schema)) return;
125
+ if (Array.isArray(value)) {
126
+ if (isObject(schema.items))
127
+ value.forEach((item, i) => unknownKeys(schema.items, item, `${at}/${i}`, out));
128
+ return;
129
+ }
130
+ if (!isObject(value) || !isObject(schema.properties)) return;
131
+ const props = schema.properties;
132
+ const open = schema.additionalProperties !== void 0 && schema.additionalProperties !== false || schema.patternProperties !== void 0 || // Composed schemas split their properties across branches; leave them to the validator.
133
+ ["allOf", "anyOf", "oneOf", "$ref"].some((k) => schema[k] !== void 0);
134
+ for (const [key, child] of Object.entries(value)) {
135
+ if (key in props) unknownKeys(props[key], child, `${at}/${key}`, out);
136
+ else if (!open)
137
+ out.push(`${path(at)}: unknown option "${key}". Known: ${Object.keys(props).join(", ") || "none"}`);
138
+ }
139
+ }
140
+ var validators = /* @__PURE__ */ new WeakMap();
141
+ function checkOptions(schema, options) {
142
+ let validator = validators.get(schema);
143
+ if (!validator) {
144
+ validator = new import_json_schema.Validator(schema, "2020-12", false);
145
+ validators.set(schema, validator);
146
+ }
147
+ const value = options === void 0 ? {} : options;
148
+ const problems = [];
149
+ unknownKeys(schema, value, "#", problems);
150
+ const result = validator.validate(value);
151
+ if (!result.valid) {
152
+ const leaves = result.errors.filter(
153
+ (e) => !CASCADE.has(e.keyword) && !/\/(anyOf|oneOf)\/\d/.test(e.keywordLocation)
154
+ );
155
+ const shown = leaves.length || problems.length ? leaves : result.errors;
156
+ for (const e of shown) problems.push(`${path(e.instanceLocation)}: ${e.error}`);
157
+ }
158
+ return [...new Set(problems)];
159
+ }
160
+
161
+ // src/runtime/tools/scenario.ts
162
+ var active = /* @__PURE__ */ new Map();
163
+ var message = (error) => error instanceof Error ? error.message : String(error);
164
+ async function runUndos(entry) {
165
+ const errors = [];
166
+ for (const undo2 of entry.undos.splice(0).reverse()) {
167
+ try {
168
+ await undo2();
169
+ } catch (error) {
170
+ errors.push(message(error));
171
+ }
172
+ }
173
+ return errors;
174
+ }
175
+ async function undo(name) {
176
+ const entry = active.get(name);
177
+ if (!entry) return [];
178
+ active.delete(name);
179
+ return (await runUndos(entry)).map((e) => `${name}: ${e}`);
180
+ }
181
+ function scenarioTools(getScenarios, getTools) {
182
+ const find = (name) => {
183
+ const scenarios = getScenarios();
184
+ const scenario = scenarios[name];
185
+ if (!scenario)
186
+ throw new Error(
187
+ `Unknown scenario "${name}". Known: ${Object.keys(scenarios).join(", ") || "none"}`
188
+ );
189
+ return scenario;
190
+ };
191
+ const call = async (tool, ...args) => {
192
+ const definition = getTools()[tool];
193
+ if (!definition) throw new Error(`Unknown tool "${tool}"`);
194
+ const run = typeof definition === "function" ? definition : definition.run;
195
+ return run(...args);
196
+ };
197
+ return {
198
+ "scenario.list": {
199
+ description: "Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.",
200
+ run: () => Object.entries(getScenarios()).map(([name, s]) => ({
201
+ name,
202
+ description: s.description,
203
+ options: s.options,
204
+ active: active.has(name)
205
+ })).sort((a, b) => a.name.localeCompare(b.name))
206
+ },
207
+ "scenario.apply": {
208
+ description: "Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.",
209
+ run: async (name, options) => {
210
+ const scenario = find(name);
211
+ if (scenario.options) {
212
+ const problems = checkOptions(scenario.options, options);
213
+ if (problems.length)
214
+ throw new Error(
215
+ `Scenario "${name}" got bad options, nothing was applied:
216
+ - ${problems.join("\n- ")}
217
+ scenario.list shows its options schema.`
218
+ );
219
+ }
220
+ const previous = await undo(name);
221
+ if (previous.length)
222
+ throw new Error(`Undoing the active "${name}" failed: ${previous.join("; ")}`);
223
+ const entry = { options, undos: [] };
224
+ active.set(name, entry);
225
+ const context = {
226
+ options,
227
+ call,
228
+ onUndo: (fn) => {
229
+ if (typeof fn !== "function")
230
+ throw new Error("onUndo takes a function");
231
+ entry.undos.push(fn);
232
+ }
233
+ };
234
+ try {
235
+ return await scenario.apply(context);
236
+ } catch (error) {
237
+ const errors = await undo(name);
238
+ const extra = errors.length ? ` (undo: ${errors.join("; ")})` : "";
239
+ throw new Error(`Scenario "${name}" failed: ${message(error)}${extra}`);
240
+ }
241
+ }
242
+ },
243
+ "scenario.restore": {
244
+ description: "Undo every active scenario, newest first. bridge.restore runs this after every other restorer.",
245
+ run: async () => {
246
+ const names = [...active.keys()].reverse();
247
+ const errors = [];
248
+ for (const name of names) errors.push(...await undo(name));
249
+ if (errors.length) throw new Error(errors.join("; "));
250
+ return names;
251
+ }
252
+ }
253
+ };
254
+ }
255
+
101
256
  // src/runtime/tools/screen.ts
102
257
  var import_react_native = require("react-native");
103
258
 
@@ -639,12 +794,14 @@ function screenTools() {
639
794
  }
640
795
 
641
796
  // src/runtime/builtin-tools.ts
642
- function builtinTools(listTools, getTools, logs) {
797
+ function builtinTools(listTools, getTools, logs, getScenarios) {
643
798
  return {
644
799
  ...bridgeTools(listTools),
645
800
  ...logTools(logs),
646
801
  ...restoreTools(getTools),
647
- ...screenTools()
802
+ ...screenTools(),
803
+ // Only when the app defines scenarios, so `tools` doesn't list empty ones.
804
+ ...getScenarios && scenarioTools(getScenarios, getTools)
648
805
  };
649
806
  }
650
807
 
@@ -673,6 +830,38 @@ function cdpTransport() {
673
830
  };
674
831
  }
675
832
 
833
+ // src/runtime/gate.ts
834
+ function createGate(name) {
835
+ let holds = 0;
836
+ const listeners = /* @__PURE__ */ new Set();
837
+ const notify = () => {
838
+ for (const fn of [...listeners]) fn(holds > 0);
839
+ };
840
+ return {
841
+ name,
842
+ get closed() {
843
+ return holds > 0;
844
+ },
845
+ close: () => {
846
+ let released = false;
847
+ holds += 1;
848
+ if (holds === 1) notify();
849
+ return () => {
850
+ if (released) return;
851
+ released = true;
852
+ holds -= 1;
853
+ if (holds === 0) notify();
854
+ };
855
+ },
856
+ subscribe: (fn) => {
857
+ listeners.add(fn);
858
+ return () => {
859
+ listeners.delete(fn);
860
+ };
861
+ }
862
+ };
863
+ }
864
+
676
865
  // src/runtime/logs.ts
677
866
  var CAPACITY = 200;
678
867
  var STACK_LINES = 10;
@@ -735,9 +924,9 @@ function formatArgs(args) {
735
924
  }
736
925
  parts.push(...rest.map(describe2));
737
926
  const error = args.find((a) => a instanceof Error);
738
- const message = parts.join(" ");
927
+ const message2 = parts.join(" ");
739
928
  return {
740
- message: message.length > MESSAGE_CHARS ? `${message.slice(0, MESSAGE_CHARS)}...` : message,
929
+ message: message2.length > MESSAGE_CHARS ? `${message2.slice(0, MESSAGE_CHARS)}...` : message2,
741
930
  ...error?.stack ? { stack: trimStack(error.stack) } : {}
742
931
  };
743
932
  }
@@ -756,9 +945,9 @@ function describe2(value) {
756
945
  var trimStack = (stack) => stack.split("\n").slice(0, STACK_LINES).join("\n");
757
946
  function installLogHooks(capture, env) {
758
947
  let quiet = 0;
759
- let active = true;
948
+ let active2 = true;
760
949
  const record = (level, args) => {
761
- if (!active || quiet) return;
950
+ if (!active2 || quiet) return;
762
951
  quiet++;
763
952
  try {
764
953
  capture.record(level, args);
@@ -801,7 +990,7 @@ function installLogHooks(capture, env) {
801
990
  env.addEventListener?.("unhandledrejection", onRejection);
802
991
  }
803
992
  return () => {
804
- active = false;
993
+ active2 = false;
805
994
  if (console.error === wrappers.error) console.error = originals.error;
806
995
  if (console.warn === wrappers.warn) console.warn = originals.warn;
807
996
  if (errorUtils && originalHandler && errorUtils.getGlobalHandler() === handler2)
@@ -877,8 +1066,8 @@ function createRegistry(getTools, logs) {
877
1066
  const value = await unwrap(definition).run(...call.args ?? []);
878
1067
  return { id: call.id, from, ok: true, value: toJson(value), ms: ms() };
879
1068
  } catch (error) {
880
- const message = error instanceof Error ? error.message : String(error);
881
- return { id: call.id, from, ok: false, error: message, ms: ms() };
1069
+ const message2 = error instanceof Error ? error.message : String(error);
1070
+ return { id: call.id, from, ok: false, error: message2, ms: ms() };
882
1071
  }
883
1072
  }
884
1073
  return { list, dispatch };
@@ -889,9 +1078,11 @@ var randomId = () => Math.random().toString(36).slice(2, 10);
889
1078
  function startAgentBridge(options = {}) {
890
1079
  const deviceId = randomId();
891
1080
  const userTools = () => typeof options.tools === "function" ? options.tools() : options.tools ?? {};
1081
+ const { scenarios } = options;
1082
+ const getScenarios = scenarios ? () => typeof scenarios === "function" ? scenarios() : scenarios : void 0;
892
1083
  const logs = startLogCapture();
893
1084
  const allTools = () => ({
894
- ...builtinTools(() => registry.list(), allTools, logs.capture),
1085
+ ...builtinTools(() => registry.list(), allTools, logs.capture, getScenarios),
895
1086
  ...userTools()
896
1087
  });
897
1088
  const registry = createRegistry(allTools, logs.capture);
@@ -927,7 +1118,12 @@ function useAgentBridge(options = {}) {
927
1118
  tools: () => {
928
1119
  const { tools } = latest.current;
929
1120
  return typeof tools === "function" ? tools() : tools ?? {};
930
- }
1121
+ },
1122
+ // Decided at mount: scenario.* tools are there or not for the bridge's life.
1123
+ scenarios: latest.current.scenarios ? () => {
1124
+ const { scenarios } = latest.current;
1125
+ return typeof scenarios === "function" ? scenarios() : scenarios ?? {};
1126
+ } : void 0
931
1127
  });
932
1128
  }, []);
933
1129
  }
@@ -1,5 +1,5 @@
1
- import { a as Transport, A as AgentBridgeOptions } from '../types-C6DUUHnB.cjs';
2
- export { C as CallMessage, D as DeviceInfo, L as LogEntry, R as RUNTIME_MARKER, b as ResultMessage, c as ToolDefinition, d as ToolFn, e as ToolInfo, T as Tools } from '../types-C6DUUHnB.cjs';
1
+ import { a as Transport, A as AgentBridgeOptions } from '../types-6kKQsLC1.cjs';
2
+ export { C as CallMessage, D as DeviceInfo, L as LogEntry, O as OptionsSchema, R as RUNTIME_MARKER, b as ResultMessage, S as Scenario, c as ScenarioContext, d as Scenarios, e as ToolDefinition, f as ToolFn, g as ToolInfo, T as Tools } from '../types-6kKQsLC1.cjs';
3
3
 
4
4
  /**
5
5
  * Works in any React Native app served by Metro. The client evaluates one fixed
@@ -8,6 +8,26 @@ export { C as CallMessage, D as DeviceInfo, L as LogEntry, R as RUNTIME_MARKER,
8
8
  */
9
9
  declare function cdpTransport(): Transport;
10
10
 
11
+ type Gate = {
12
+ name: string;
13
+ /** True while anything holds the gate closed. */
14
+ readonly closed: boolean;
15
+ /**
16
+ * Closes the gate until the returned function is called. Closes are
17
+ * counted, so the gate opens when the last one is released. Hand the
18
+ * result to a scenario's `onUndo`.
19
+ */
20
+ close: () => () => void;
21
+ /** Calls `fn(closed)` when the gate closes or opens. Returns an unsubscribe. */
22
+ subscribe: (fn: (closed: boolean) => void) => () => void;
23
+ };
24
+ /**
25
+ * A switch app code checks before a side effect a scenario must hold off,
26
+ * e.g. connecting a realtime client with a fake token. Always open in a
27
+ * release build.
28
+ */
29
+ declare function createGate(name: string): Gate;
30
+
11
31
  type SettleResult = {
12
32
  commits: number;
13
33
  ms: number;
@@ -66,4 +86,4 @@ declare function startAgentBridge(options?: AgentBridgeOptions): () => void;
66
86
  */
67
87
  declare function useAgentBridge(options?: AgentBridgeOptions): void;
68
88
 
69
- export { AgentBridgeOptions, type ScreenElement, type SettleResult, type Target, Transport, cdpTransport, settle, startAgentBridge, useAgentBridge };
89
+ export { AgentBridgeOptions, type Gate, type ScreenElement, type SettleResult, type Target, Transport, cdpTransport, createGate, settle, startAgentBridge, useAgentBridge };
@@ -48,6 +48,14 @@ type DeviceInfo = {
48
48
  tools: ToolInfo[];
49
49
  };
50
50
 
51
+ /**
52
+ * A JSON Schema (draft 2020-12) for a scenario's options. Typed loosely, so
53
+ * `as const` schemas and ones generated by other libraries fit.
54
+ */
55
+ type OptionsSchema = {
56
+ readonly [keyword: string]: unknown;
57
+ };
58
+
51
59
  type ToolFn = (...args: any[]) => unknown;
52
60
  type ToolDefinition = ToolFn | {
53
61
  description?: string;
@@ -64,13 +72,44 @@ type Transport = {
64
72
  /** Starts listening; returns a function that stops. */
65
73
  start: (context: TransportContext) => () => void;
66
74
  };
75
+ type ScenarioContext<O = unknown> = {
76
+ /** What the agent or flow passed to `scenario.apply`, or undefined. */
77
+ options: O | undefined;
78
+ /**
79
+ * Calls any bridge tool by name, e.g. `call('store.set', 'auth', {...})`.
80
+ * What a tool changes is undone by its own restorer in `bridge.restore`.
81
+ */
82
+ call: (tool: string, ...args: unknown[]) => Promise<unknown>;
83
+ /**
84
+ * Runs `fn` when the scenario is undone, after every other restorer. Use it
85
+ * for anything the tools' restorers don't cover: app mocks, gates, native
86
+ * state. Undo callbacks run newest first.
87
+ */
88
+ onUndo: (fn: () => unknown) => void;
89
+ };
90
+ /** A named setup the app defines and the agent applies, e.g. a signed-in user. */
91
+ type Scenario<O = unknown> = {
92
+ description?: string;
93
+ /**
94
+ * JSON Schema (2020-12) for `options`. `scenario.apply` rejects options
95
+ * that don't fit before `apply` runs, and `scenario.list` shows it. Keys an
96
+ * object schema doesn't list are rejected unless it sets
97
+ * `additionalProperties`. No options are checked as `{}`.
98
+ */
99
+ options?: OptionsSchema;
100
+ /** Sets the app up. Its return value goes back to the agent. */
101
+ apply: (context: ScenarioContext<O>) => unknown;
102
+ };
103
+ type Scenarios = Record<string, Scenario<any>>;
67
104
  type AgentBridgeOptions = {
68
105
  /** Your tools, or a function returning them (read on every call). */
69
106
  tools?: Tools | (() => Tools);
107
+ /** Named setups for `scenario.*`, or a function returning them (read on every call). */
108
+ scenarios?: Scenarios | (() => Scenarios);
70
109
  /** How this app shows up in `agent-bridge devices`. */
71
110
  name?: string;
72
111
  /** Defaults to CDP only. Add `expoTransport()` on Expo. */
73
112
  transports?: Transport[];
74
113
  };
75
114
 
76
- export { type AgentBridgeOptions as A, type CallMessage as C, type DeviceInfo as D, type LogEntry as L, RUNTIME_MARKER as R, type Tools as T, type Transport as a, type ResultMessage as b, type ToolDefinition as c, type ToolFn as d, type ToolInfo as e };
115
+ export { type AgentBridgeOptions as A, type CallMessage as C, type DeviceInfo as D, type LogEntry as L, type OptionsSchema as O, RUNTIME_MARKER as R, type Scenario as S, type Tools as T, type Transport as a, type ResultMessage as b, type ScenarioContext as c, type Scenarios as d, type ToolDefinition as e, type ToolFn as f, type ToolInfo as g };
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ // Metro inlines NODE_ENV and drops the dead branch, so a release bundle
4
+ // never includes the bridge, the same way react/index.js works.
5
+ if (process.env.NODE_ENV === 'production') {
6
+ module.exports = require('../dist/noop/ably.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/ably.cjs')
9
+ }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ // Metro inlines NODE_ENV and drops the dead branch, so a release bundle
4
+ // never includes the bridge, the same way react/index.js works.
5
+ if (process.env.NODE_ENV === 'production') {
6
+ module.exports = require('../dist/noop/realtime.cjs')
7
+ } else {
8
+ module.exports = require('../dist/realtime/index.cjs')
9
+ }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ // Metro inlines NODE_ENV and drops the dead branch, so a release bundle
4
+ // never includes the bridge, the same way react/index.js works.
5
+ if (process.env.NODE_ENV === 'production') {
6
+ module.exports = require('../dist/noop/socket-io.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/socket-io.cjs')
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avasapp/agent-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Let coding agents drive a running React Native app directly: seed state, flip flags, navigate and check the screen in about a millisecond per call, with no taps.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,8 +13,7 @@
13
13
  "url": "https://github.com/avas-app/agent-bridge/issues"
14
14
  },
15
15
  "publishConfig": {
16
- "access": "public",
17
- "provenance": true
16
+ "access": "public"
18
17
  },
19
18
  "engines": {
20
19
  "node": ">=20"
@@ -51,6 +50,18 @@
51
50
  "types": "./dist/network/index.d.cts",
52
51
  "default": "./entries/network.cjs"
53
52
  },
53
+ "./realtime": {
54
+ "types": "./dist/realtime/index.d.cts",
55
+ "default": "./entries/realtime.cjs"
56
+ },
57
+ "./ably": {
58
+ "types": "./dist/adapters/ably.d.cts",
59
+ "default": "./entries/ably.cjs"
60
+ },
61
+ "./socket.io": {
62
+ "types": "./dist/adapters/socket-io.d.cts",
63
+ "default": "./entries/socket.io.cjs"
64
+ },
54
65
  "./client": {
55
66
  "types": "./dist/client/index.d.ts",
56
67
  "import": "./dist/client/index.js",
@@ -77,7 +88,10 @@
77
88
  "cdp",
78
89
  "tanstack-query",
79
90
  "zustand",
80
- "mmkv"
91
+ "mmkv",
92
+ "realtime",
93
+ "ably",
94
+ "socket.io"
81
95
  ],
82
96
  "scripts": {
83
97
  "build": "rm -rf dist && tsup",
@@ -104,6 +118,7 @@
104
118
  }
105
119
  },
106
120
  "dependencies": {
121
+ "@cfworker/json-schema": "^4.1.1",
107
122
  "ws": "^8.21.3"
108
123
  },
109
124
  "devDependencies": {
@@ -113,6 +128,8 @@
113
128
  "@types/bun": "^1.4.2",
114
129
  "@types/react": "^19.3.0",
115
130
  "@types/ws": "^8.18.1",
131
+ "ably": "^2.29.0",
132
+ "socket.io-client": "^4.8.4",
116
133
  "tsup": "^8.5.1",
117
134
  "typescript": "~6.0.3",
118
135
  "zustand": "^5.0.15"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agent-bridge
3
- description: "Drive a running React Native or Expo dev build directly through @avasapp/agent-bridge: see what's on screen, fill in forms, press buttons, seed query data, flip feature flags, set store state, navigate, mock the network and undo it all, in milliseconds per call instead of tapping through the app. Use whenever you need the app in a particular state to test a change, or need to verify what a screen shows."
3
+ description: "Drive a running React Native or Expo dev build directly through @avasapp/agent-bridge: start from an app-defined scenario such as a locally signed-in user, see what's on screen, fill in forms, press buttons, seed query data, flip feature flags, set store state, navigate, mock the network, fake realtime messages and undo it all, in milliseconds per call instead of tapping through the app. Pairs with agent-device, which opens the app and covers native UI, real taps and screenshots. Use whenever you need the app in a particular state to test a change, or need to verify what a screen shows."
4
4
  ---
5
5
 
6
6
  # Driving an app with agent-bridge
@@ -8,6 +8,22 @@ description: "Drive a running React Native or Expo dev build directly through @a
8
8
  The app mounts `useAgentBridge({ tools })` in a dev build. You call those tools
9
9
  by name. Nothing is tapped, so there is no setup to click through.
10
10
 
11
+ ## Pair it with agent-device
12
+
13
+ agent-bridge works inside the app's JavaScript; agent-device works the device.
14
+ Use both, each for what it's good at:
15
+
16
+ | agent-bridge | agent-device |
17
+ | --- | --- |
18
+ | Put the app in a state: flags, query data, store values, network mocks, realtime messages | Install and open the app, reload, relaunch |
19
+ | Navigate, fill forms, press buttons, check text on screen | System alerts, permission prompts, native sheets, the keyboard |
20
+ | Undo it all with `bridge.restore` | Screenshots, recordings, one real tap to prove the UI responds |
21
+
22
+ A typical run: `agent-device open` the app, set up and check the screen with
23
+ agent-bridge, then finish with one real tap or a screenshot from agent-device.
24
+ Don't tap through setup screens or edit backend data to reach a state that one
25
+ bridge call can set.
26
+
11
27
  ## Start a session
12
28
 
13
29
  ```sh
@@ -21,6 +37,30 @@ minutes without calls and runs `bridge.restore` when it does.
21
37
  If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
22
38
  this build. Say so; don't fall back to tapping silently.
23
39
 
40
+ ## Start from a scenario
41
+
42
+ Apps can define named setups, such as a locally signed-in user. Use one
43
+ instead of signing in by hand or seeding auth state yourself:
44
+
45
+ ```sh
46
+ npx agent-bridge scenarios # what this app defines
47
+ npx agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
48
+ ```
49
+
50
+ `scenarios` shows each one's options as a JSON Schema. Bad options fail
51
+ before anything changes, and the error lists every problem
52
+ (`options: unknown option "usr". Known: user`): fix them and apply again.
53
+ `bridge.restore` (and `session stop`) undoes it after everything else. A local
54
+ scenario usually turns on strict network mode: any request no mock answers
55
+ fails with a 501, and the reply carries an error that names it
56
+ (`! error during …: agent-bridge strict network: no mock for GET https://…`).
57
+ Add a mock for it (`net.mock`), don't turn strict mode off.
58
+ `npx agent-bridge call net.strict` lists what it blocked. Strict mode sees JS
59
+ `fetch`/XHR only, not images, WebSockets or native SDKs.
60
+
61
+ If the app has no scenario for the state you need, say so. Don't sign in to a
62
+ real account instead.
63
+
24
64
  ## See and use the screen
25
65
 
26
66
  ```sh
@@ -32,7 +72,7 @@ npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true}
32
72
 
33
73
  `press` and `fill` return once the app has rendered the result, so the next
34
74
  check sees it. `fill` skips the keyboard: autocorrect and native-only input
35
- behaviour need one real typing step from your device tool.
75
+ behaviour need one real typing step from agent-device.
36
76
 
37
77
  ## Get the app into a state
38
78
 
@@ -48,6 +88,25 @@ npx agent-bridge call net.log
48
88
  Arguments are a JSON array (or a single JSON value). Read the current value
49
89
  first (`query.get`, `store.get`) and change only what you need.
50
90
 
91
+ ## Realtime messages
92
+
93
+ If the app has `realtime.*` tools, you can see its realtime messages and fake
94
+ them instead of waiting for a backend to send one:
95
+
96
+ ```sh
97
+ npx agent-bridge call realtime.channels # what the app listens on
98
+ npx agent-bridge call realtime.log '{"channel": "chat"}' # recent messages, real and faked
99
+ npx agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
100
+ npx agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
101
+ npx agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
102
+ ```
103
+
104
+ Copy the shape of a real message from `realtime.log` before you emit one. With
105
+ Ably, pass `{ name, data }`; with socket.io, pass the arguments after the
106
+ channel (the event name). Muting still lets your emitted messages through.
107
+ `emit` fails and lists the live channels when nothing listens on yours: open
108
+ the screen that subscribes first.
109
+
51
110
  ## Watch for errors
52
111
 
53
112
  Every reply carries errors the app logged or threw since the previous reply,
@@ -60,6 +119,8 @@ last 200 errors and warnings.
60
119
  Write the steps into a flow file and run it:
61
120
 
62
121
  ```js
122
+ export const scenario = 'signedIn' // applied first, undone after, even on failure
123
+
63
124
  export default async ({ step }) => {
64
125
  await step('open form', 'screen.press', 'add-plant')
65
126
  await step('save empty', 'screen.press', 'save-plant')
@@ -70,6 +131,7 @@ export default async ({ step }) => {
70
131
 
71
132
  ```sh
72
133
  npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an error
134
+ npx agent-bridge run flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
73
135
  ```
74
136
 
75
137
  ## Clean up
@@ -78,12 +140,13 @@ npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an
78
140
  npx agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
79
141
  ```
80
142
 
81
- `bridge.restore` unpins queries, puts back store and MMKV values and removes
82
- network mocks, so the next run starts from the real state.
143
+ `bridge.restore` unpins queries, puts back store and MMKV values, removes
144
+ network mocks, unmutes realtime channels and ends a faked connection state,
145
+ then undoes active scenarios, so the next run starts from the real state.
83
146
 
84
147
  ## Traps
85
148
 
86
149
  - Run next to the simulator. From another machine every call pays the network.
87
150
  - It can't see native UI (system alerts, permission prompts, native sheets) or
88
- overlap. Finish a flow with one real UI check or screenshot from your device tool.
151
+ overlap. Finish a flow with one real UI check or screenshot from agent-device.
89
152
  - With several apps on one Metro, pass `--device`.