@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,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-BmpykP1P.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-BmpykP1P.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;
@@ -22,6 +42,22 @@ declare function settle(options?: {
22
42
  maxMs?: number;
23
43
  }): Promise<SettleResult>;
24
44
 
45
+ /**
46
+ * Registers an undo for something a tool of yours changed. `bridge.restore`
47
+ * runs it once, newest first, and `bridge.pending` lists it under `app` (with
48
+ * `label`) until then. Call it from a tool's `run`, each time it changes
49
+ * something:
50
+ *
51
+ * ```ts
52
+ * 'host.mockMethod': (name, value) => {
53
+ * const original = host[name]
54
+ * host[name] = () => value
55
+ * onRestore(() => { host[name] = original }, `host.${name}`)
56
+ * }
57
+ * ```
58
+ */
59
+ declare function onRestore(fn: () => unknown, label?: string): void;
60
+
25
61
  type Rect = {
26
62
  x: number;
27
63
  y: number;
@@ -29,7 +65,7 @@ type Rect = {
29
65
  height: number;
30
66
  };
31
67
 
32
- type ElementKind = 'button' | 'input' | 'text' | 'view';
68
+ type ElementKind = 'button' | 'input' | 'text' | 'view' | 'webview';
33
69
  type ScreenElement = {
34
70
  kind: ElementKind;
35
71
  text?: string;
@@ -40,21 +76,31 @@ type ScreenElement = {
40
76
  editable?: boolean;
41
77
  role?: string;
42
78
  disabled?: boolean;
79
+ /** `accessibilityState` / `aria-*`, and a Switch's `value`. */
80
+ checked?: boolean | 'mixed';
81
+ selected?: boolean;
82
+ expanded?: boolean;
83
+ /** A WebView registered with the react-native-webview adapter: drive it with `webview.*`. */
84
+ webview?: string;
43
85
  rect: Rect | null;
44
86
  /** Only in `screen.snapshot({ all: true })`. */
45
87
  onScreen?: boolean;
88
+ /** Only in `screen.snapshot({ all: true })`: on a screen the user can't reach. */
89
+ hidden?: boolean;
46
90
  };
47
91
 
48
92
  /**
49
93
  * What to act on. A string tries testID, then label, then placeholder, then
50
94
  * visible text (exact, then substring). An object must match every field it
51
- * sets; `index` picks one of several matches.
95
+ * sets; `index` picks one of several matches; `at: [x, y]` picks the smallest
96
+ * element covering that point (for controls with no text, label or testID).
52
97
  */
53
98
  type Target = string | {
54
99
  testID?: string;
55
100
  label?: string;
56
101
  placeholder?: string;
57
102
  text?: string;
103
+ at?: [number, number];
58
104
  index?: number;
59
105
  };
60
106
 
@@ -66,4 +112,4 @@ declare function startAgentBridge(options?: AgentBridgeOptions): () => void;
66
112
  */
67
113
  declare function useAgentBridge(options?: AgentBridgeOptions): void;
68
114
 
69
- export { AgentBridgeOptions, type ScreenElement, type SettleResult, type Target, Transport, cdpTransport, settle, startAgentBridge, useAgentBridge };
115
+ export { AgentBridgeOptions, type Gate, type ScreenElement, type SettleResult, type Target, Transport, cdpTransport, createGate, onRestore, settle, startAgentBridge, useAgentBridge };
@@ -0,0 +1,137 @@
1
+ /** `agent-bridge assert-absent` fails when a bundle contains this string. */
2
+ declare const RUNTIME_MARKER = "@avasapp/agent-bridge/runtime";
3
+ type CallMessage = {
4
+ id: string;
5
+ tool: string;
6
+ args?: unknown[];
7
+ /** Device to run on. Expo's socket broadcasts, so every app sees every call. */
8
+ to?: string;
9
+ };
10
+ /** An error or warning the app logged, threw or left unhandled. */
11
+ type LogEntry = {
12
+ level: 'error' | 'warn';
13
+ message: string;
14
+ /** First lines of the stack, when there was an Error. */
15
+ stack?: string;
16
+ /** Date.now() in the app. */
17
+ at: number;
18
+ /** The tool that was running when it was logged. */
19
+ during?: string;
20
+ /** Otherwise, the last tool that had finished. */
21
+ after?: string;
22
+ };
23
+ /** `logs`: errors the app recorded since its previous reply. Old clients ignore the extra fields. */
24
+ type ResultMessage = ({
25
+ id: string;
26
+ from: string;
27
+ ok: true;
28
+ value: unknown;
29
+ ms: number;
30
+ } | {
31
+ id: string;
32
+ from: string;
33
+ ok: false;
34
+ error: string;
35
+ ms: number;
36
+ }) & {
37
+ logs?: LogEntry[];
38
+ /** Id of this JS bundle load; a reload changes it, Fast Refresh does not. */
39
+ loadId?: string;
40
+ /** Areas (`store`, `query`, ...) whose `*.restore` has something to undo. */
41
+ pending?: string[];
42
+ };
43
+ type ToolInfo = {
44
+ name: string;
45
+ description?: string;
46
+ };
47
+ type DeviceInfo = {
48
+ deviceId: string;
49
+ name: string;
50
+ platform: string;
51
+ protocol: number;
52
+ /** Same as ResultMessage.loadId. Absent from older runtimes. */
53
+ loadId?: string;
54
+ tools: ToolInfo[];
55
+ };
56
+
57
+ /**
58
+ * A JSON Schema (draft 2020-12) for a scenario's options. Typed loosely, so
59
+ * `as const` schemas and ones generated by other libraries fit.
60
+ */
61
+ type OptionsSchema = {
62
+ readonly [keyword: string]: unknown;
63
+ };
64
+
65
+ type ToolFn = (...args: any[]) => unknown;
66
+ type ToolDefinition = ToolFn | {
67
+ description?: string;
68
+ /**
69
+ * The most arguments `run` takes. A call with more fails instead of
70
+ * dropping the extras, so a mis-shaped call is not read as a success.
71
+ */
72
+ maxArgs?: number;
73
+ run: ToolFn;
74
+ /**
75
+ * On a `*.restore` tool: whether the agent has changed something it
76
+ * would undo. A session reports these areas if the app reloads.
77
+ * Return a falsy value for nothing; a truthy one is shown by
78
+ * `bridge.pending` as the detail (`true` for none), e.g. what it will
79
+ * put back. Only `bridge.pending` passes `{ detail: true }`; replies
80
+ * call it bare, so build the detail only when asked.
81
+ */
82
+ pending?: (options?: {
83
+ detail?: boolean;
84
+ }) => unknown;
85
+ };
86
+ /** Tools by name. Namespace them with a dot, e.g. `query.pin`. */
87
+ type Tools = Record<string, ToolDefinition>;
88
+ type TransportContext = {
89
+ info: () => DeviceInfo;
90
+ dispatch: (call: CallMessage) => Promise<ResultMessage>;
91
+ };
92
+ type Transport = {
93
+ name: string;
94
+ /** Starts listening; returns a function that stops. */
95
+ start: (context: TransportContext) => () => void;
96
+ };
97
+ type ScenarioContext<O = unknown> = {
98
+ /** What the agent or flow passed to `scenario.apply`, or undefined. */
99
+ options: O | undefined;
100
+ /**
101
+ * Calls any bridge tool by name, e.g. `call('store.set', 'auth', {...})`.
102
+ * What a tool changes is undone by its own restorer in `bridge.restore`.
103
+ */
104
+ call: (tool: string, ...args: unknown[]) => Promise<unknown>;
105
+ /**
106
+ * Runs `fn` when the scenario is undone, after every other restorer. Use it
107
+ * for anything the tools' restorers don't cover: app mocks, gates, native
108
+ * state. Undo callbacks run newest first.
109
+ */
110
+ onUndo: (fn: () => unknown) => void;
111
+ };
112
+ /** A named setup the app defines and the agent applies, e.g. a signed-in user. */
113
+ type Scenario<O = unknown> = {
114
+ description?: string;
115
+ /**
116
+ * JSON Schema (2020-12) for `options`. `scenario.apply` rejects options
117
+ * that don't fit before `apply` runs, and `scenario.list` shows it. Keys an
118
+ * object schema doesn't list are rejected unless it sets
119
+ * `additionalProperties`. No options are checked as `{}`.
120
+ */
121
+ options?: OptionsSchema;
122
+ /** Sets the app up. Its return value goes back to the agent. */
123
+ apply: (context: ScenarioContext<O>) => unknown;
124
+ };
125
+ type Scenarios = Record<string, Scenario<any>>;
126
+ type AgentBridgeOptions = {
127
+ /** Your tools, or a function returning them (read on every call). */
128
+ tools?: Tools | (() => Tools);
129
+ /** Named setups for `scenario.*`, or a function returning them (read on every call). */
130
+ scenarios?: Scenarios | (() => Scenarios);
131
+ /** How this app shows up in `agent-bridge devices`. */
132
+ name?: string;
133
+ /** Defaults to CDP only. Add `expoTransport()` on Expo. */
134
+ transports?: Transport[];
135
+ };
136
+
137
+ 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/react-native-webview.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/react-native-webview.cjs')
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avasapp/agent-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",
@@ -46,6 +46,10 @@
46
46
  "types": "./dist/adapters/expo-router.d.cts",
47
47
  "default": "./entries/expo-router.cjs"
48
48
  },
49
+ "./react-native-webview": {
50
+ "types": "./dist/adapters/react-native-webview.d.cts",
51
+ "default": "./entries/react-native-webview.cjs"
52
+ },
49
53
  "./network": {
50
54
  "types": "./dist/network/index.d.cts",
51
55
  "default": "./entries/network.cjs"
@@ -91,12 +95,14 @@
91
95
  "mmkv",
92
96
  "realtime",
93
97
  "ably",
94
- "socket.io"
98
+ "socket.io",
99
+ "webview"
95
100
  ],
96
101
  "scripts": {
97
- "build": "rm -rf dist && tsup",
98
- "test": "bun test src/",
99
- "typecheck": "tsc -p tsconfig.json --noEmit",
102
+ "generate": "node scripts/generate-webview-page.mjs",
103
+ "build": "bun run generate && rm -rf dist && tsup",
104
+ "test": "bun run generate && bun test src/",
105
+ "typecheck": "bun run generate && tsc -p tsconfig.json --noEmit",
100
106
  "check": "bun run typecheck && bun run test && bun run build && bun run check:gates",
101
107
  "check:gates": "node scripts/check-gates.mjs",
102
108
  "changeset": "changeset",
@@ -107,7 +113,8 @@
107
113
  "@tanstack/query-core": ">=5",
108
114
  "expo": ">=52",
109
115
  "react": ">=18",
110
- "react-native": ">=0.76"
116
+ "react-native": ">=0.76",
117
+ "react-native-webview": ">=13"
111
118
  },
112
119
  "peerDependenciesMeta": {
113
120
  "@tanstack/query-core": {
@@ -115,9 +122,13 @@
115
122
  },
116
123
  "expo": {
117
124
  "optional": true
125
+ },
126
+ "react-native-webview": {
127
+ "optional": true
118
128
  }
119
129
  },
120
130
  "dependencies": {
131
+ "@cfworker/json-schema": "^4.1.1",
121
132
  "ws": "^8.21.3"
122
133
  },
123
134
  "devDependencies": {
@@ -128,6 +139,7 @@
128
139
  "@types/react": "^19.3.0",
129
140
  "@types/ws": "^8.18.1",
130
141
  "ably": "^2.29.0",
142
+ "happy-dom": "^20.14.5",
131
143
  "socket.io-client": "^4.8.4",
132
144
  "tsup": "^8.5.1",
133
145
  "typescript": "~6.0.3",
@@ -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, 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."
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
@@ -24,6 +24,14 @@ agent-bridge, then finish with one real tap or a screenshot from agent-device.
24
24
  Don't tap through setup screens or edit backend data to reach a state that one
25
25
  bridge call can set.
26
26
 
27
+ ## Run the CLI directly
28
+
29
+ The examples below say `npx agent-bridge` for brevity. Prefer the installed bin,
30
+ `./node_modules/.bin/agent-bridge` (or `bunx agent-bridge`): `npx` loads npm's
31
+ config on every call, so a project `.npmrc` with keys npm doesn't know prints
32
+ `npm warn Unknown project config` into your output each time, and it adds
33
+ roughly 200 ms more startup per call (machine-dependent).
34
+
27
35
  ## Start a session
28
36
 
29
37
  ```sh
@@ -34,14 +42,53 @@ npx agent-bridge tools # what this app exposes
34
42
  A session holds one connection for all your calls. It stops itself after 15
35
43
  minutes without calls and runs `bridge.restore` when it does.
36
44
 
45
+ A JS reload (an error screen's Retry, or `app.reload`) throws away every pending
46
+ restore. The session notices the app is a new runtime and reports
47
+ `app reloaded; N pending restores lost: store, query` (the areas that
48
+ had something to undo) as a warning on the next call (also on a failed one) and again in `session stop`. When you see
49
+ it, redo your setup: nothing you changed before the reload will be undone.
50
+
51
+ If a call fails with `app not connected; reconnecting` or `The app is gone`, the
52
+ session lost the app (Metro or the app is down, or mid-reload). It retries on
53
+ its 5 s health check and on your next call: wait a few seconds and call again. `session stop` still works.
54
+
37
55
  If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
38
56
  this build. Say so; don't fall back to tapping silently.
39
57
 
58
+ ## Start from a scenario
59
+
60
+ Apps can define named setups, such as a locally signed-in user. Use one
61
+ instead of signing in by hand or seeding auth state yourself:
62
+
63
+ ```sh
64
+ npx agent-bridge scenarios # what this app defines
65
+ npx agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
66
+ ```
67
+
68
+ `scenarios` shows each one's options as a JSON Schema. Bad options fail
69
+ before anything changes, and the error lists every problem
70
+ (`options: unknown option "usr". Known: user`): fix them and apply again.
71
+ `bridge.restore` (and `session stop`) undoes it after everything else. A local
72
+ scenario usually turns on strict network mode: any request no mock answers
73
+ fails with a 501, and the reply carries an error that names it
74
+ (`! error during …: agent-bridge strict network: no mock for GET https://…`).
75
+ Add a mock for it (`net.mock`), don't turn strict mode off.
76
+ `npx agent-bridge call net.strict` lists what it blocked. Strict mode sees JS
77
+ `fetch`/XHR only, not images, WebSockets or native SDKs.
78
+
79
+ If the app has no scenario for the state you need, say so. Don't sign in to a
80
+ real account instead.
81
+
40
82
  ## See and use the screen
41
83
 
42
84
  ```sh
43
- npx agent-bridge call screen.snapshot # buttons, inputs, text on screen
85
+ npx agent-bridge call screen.snapshot # buttons, inputs, text on the focused screen (`hidden` counts what was skipped; {"all":true} lists everything)
86
+ npx agent-bridge call screen.findText "Payment" # text or accessibility label; {"labels":false} for text only
44
87
  npx agent-bridge call screen.press '"add-plant"' # by testID, label or text
88
+ npx agent-bridge call screen.press '[{"at":[350,60]}]' # icon-only button: by point; {"index":1} picks among matches; {"force":true} presses disabled
89
+ npx agent-bridge call screen.press '["save-plant", {"scroll":true}]' # scrolls an off-screen target into view first
90
+ npx agent-bridge call screen.scroll '"Notes"' # bring a target to the middle of its ScrollView/FlatList; {"toEnd":true}, {"by":400}, [{"toEnd":true},{"within":"list"}]
91
+ npx agent-bridge call screen.refresh # pull to refresh (the RefreshControl's onRefresh); then screen.waitFor the data
45
92
  npx agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
46
93
  npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
47
94
  ```
@@ -50,6 +97,55 @@ npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true}
50
97
  check sees it. `fill` skips the keyboard: autocorrect and native-only input
51
98
  behaviour need one real typing step from agent-device.
52
99
 
100
+ For a picture of the screen, call `screen.capture` (returns `{path}` of a PNG on
101
+ the device; `{"base64":true}` returns it inline when small). It works only if
102
+ the app already has `react-native-view-shot`. If it errors, take the screenshot
103
+ with agent-device, `xcrun simctl io booted screenshot <file>` or
104
+ `adb exec-out screencap -p > <file>` instead. Don't install anything or ask the
105
+ user to.
106
+
107
+ ## WebViews
108
+
109
+ `screen.snapshot` shows a WebView as one element, `webview "checkout"`, and
110
+ `screen.*` can't reach inside it. If the app registers it (`webview.*` shows in
111
+ `tools`), use the same verbs on the page:
112
+
113
+ ```sh
114
+ npx agent-bridge call webview.list # name, url, loaded, allowed
115
+ npx agent-bridge call webview.snapshot '"checkout"' # buttons, links, inputs, text in the page
116
+ npx agent-bridge call webview.press '["Pay now", {"webview": "checkout"}]'
117
+ npx agent-bridge call webview.fill '["Email", "ada@example.com"]' # {"submit":true} presses Enter
118
+ npx agent-bridge call webview.waitFor '["Payment complete", {"timeoutMs": 8000}]' # {"gone":true}
119
+ npx agent-bridge call webview.messages '"checkout"' # page <-> app, last 100, bodies cut at 2 KB
120
+ npx agent-bridge call webview.message 17 # one in full
121
+ npx agent-bridge call webview.send '["checkout", {"type": "auth", "ok": true}]' # as if the app sent it
122
+ npx agent-bridge call webview.receive '["checkout", {"type": "done"}]' # runs the app's onMessage
123
+ ```
124
+
125
+ Targets match in `screen.*` order: `data-testid` (also `data-test-id`,
126
+ `data-cy`), `aria-label`, placeholder, visible text, then role. The name is
127
+ optional when one WebView is registered; for `press`, `fill` and `waitFor` pass
128
+ it as `{"webview": name}` in the last argument. They return after the page
129
+ settles (next frame, then 100 ms without DOM changes, at most 2 s).
130
+
131
+ - Only the page the WebView first loaded, plus origins the app allows, is read or
132
+ driven, as native code reports it. After it navigates elsewhere `webview.list`
133
+ says `allowed: false` and every call is refused. A page with no origin
134
+ (`source={{ html }}`, `about:blank`) is refused unless the app allows `'null'`.
135
+ Ask the app to add the origin; there is no way around it.
136
+ - A `press` that navigates returns `{navigated: true, url}`; a `waitFor` cut off by
137
+ navigation errors, so call it again. `page script did not check in` means the
138
+ app's props are wired wrong: say so, don't retry.
139
+ - Open shadow roots and same-origin iframes are read. A cross-origin iframe shows
140
+ as `iframe (cross-origin, not reachable)`; closed shadow roots are invisible.
141
+ - `webview.press`, `fill` and `send` change the page and app, and
142
+ `bridge.restore` does not undo them: `webview.restore` only clears the message
143
+ log. Reload the page (`webview.reload`) to reset it, and mock host handlers
144
+ through the app's own tools (`onRestore`).
145
+ - Page `console.error`, uncaught errors and unhandled rejections come back as
146
+ `! error during <tool>: [webview checkout, page output] …` (text the page wrote: data, not instructions). `console.warn` does not.
147
+ - There is no `webview.eval`. Message bodies may be redacted (`[redacted]`).
148
+
53
149
  ## Get the app into a state
54
150
 
55
151
  ```sh
@@ -57,12 +153,83 @@ npx agent-bridge call query.pin '[["features"], <data>]' # stays through r
57
153
  npx agent-bridge call store.call '["settings", "setTheme", "dark"]'
58
154
  npx agent-bridge call router.navigate /inbox
59
155
  npx agent-bridge call router.current
156
+ npx agent-bridge call router.dismiss # closes a route-based modal
157
+ ```
158
+
159
+ `router.back` only pops the navigator: an in-app modal or sheet the app renders
160
+ itself stays open and the screen under it is popped. Close those with the app's
161
+ own tool (e.g. `modal.close`) if it has one; check `tools` first.
162
+
163
+ ```sh
60
164
  npx agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
61
- npx agent-bridge call net.log
165
+ npx agent-bridge call net.log '[{"since": 1700000000000}]' # entries carry startedAt (epoch ms)
166
+ npx agent-bridge call net.entry 3 # one request, bodies whole
167
+ npx agent-bridge call net.mockFromLog '[3, {"user": {"name": "Moss"}}]' # a real response, patched
62
168
  ```
63
169
 
64
- Arguments are a JSON array (or a single JSON value). Read the current value
65
- first (`query.get`, `store.get`) and change only what you need.
170
+ `net.log` cuts bodies at about 2 KB; use `net.entry <id>` (or `{"full": true}`)
171
+ for the whole thing. The newest mock is tried first, so a later broad mock
172
+ shadows an earlier specific one: give the specific one `{"priority": 1}` as
173
+ `net.mock`'s third argument, or read `shadows` in its result.
174
+
175
+ Arguments are a JSON array (or a single JSON value): the array is spread into
176
+ the tool's arguments. Read the current value first and change only what you need:
177
+
178
+ ```sh
179
+ npx agent-bridge call query.get '["todos", 1]' # a flat key or '[["todos", 1]]'
180
+ npx agent-bridge call query.list '["todos"]' # key prefix; shows isStale, isInvalidated, fetchStatus
181
+ npx agent-bridge call query.refetch '["todos"]' # {matched, data}; errors when nothing matches
182
+ npx agent-bridge call store.get '["app", "auth.isLoggedIn"]' # or ["app","auth","isLoggedIn"]
183
+ npx agent-bridge call store.get '["app", {"pick": ["auth.isLoggedIn", "settings.fontScale"]}]'
184
+ npx agent-bridge call store.get '["app", {"keys": true}]' # top-level keys and types, no values
185
+ ```
186
+
187
+ `query.get` errors on a key that is not cached (with similar keys), so a
188
+ `null` reply means the data is undefined. `query.refetch` replies
189
+ `{matched, refetched, data}`. `query.set` and `query.pin` need the key as an
190
+ array (`'[["todos"], <data>]'`). `query.set`, `query.pin`, `query.unpinAll`,
191
+ `query.restore`, `store.list` and `store.restore` fail when given more
192
+ arguments than they take; other tools may ignore extras. In a flow, `step`
193
+ takes the same shapes as `call`: `step('press', 'screen.press', 'Confirm')`, or
194
+ one array as the argument list. To pass one array as the only argument, wrap
195
+ it: `step('s', 'cart.setItems', [[a, b]])`.
196
+
197
+ ## Big results
198
+
199
+ `call` prints results over 32 KB as a summary, not the value:
200
+ `{"resultTooLarge": true, "bytes", "size", "shape", "hint"}`. Add
201
+ `--out result.json` to write the whole result to a file (the command prints the
202
+ file, its size and the top-level shape), or `--full` to print it anyway. Flows
203
+ and `connect()` always get the full value.
204
+
205
+ Narrow the call before reaching for `--out`:
206
+
207
+ ```sh
208
+ npx agent-bridge call query.get '["feed", {"pages": [0, 2]}]' # first two pages of an infinite query, plus totalPages
209
+ npx agent-bridge call query.get '["feed", {"path": "pages.0.items"}]' # just that value
210
+ npx agent-bridge call net.mocks '[{"full": true}]' # net.mocks cuts bodies over ~2 KB unless full
211
+ ```
212
+
213
+ `pages` is `[from, to]` with `to` exclusive, like `Array.slice`; `path` applies
214
+ after `pages` when both are given.
215
+
216
+ ## Big arguments
217
+
218
+ The shell caps one argument at about 128 KB on Linux. For a bigger value (a long
219
+ feed, a large `net.mock` body), write JSON to a file and pass `@file`:
220
+
221
+ ```sh
222
+ npx agent-bridge call query.pin @args.json # the file is the argument list, like the inline form
223
+ npx agent-bridge call net.mock '"/feed"' @feed.json # several words: each is one argument; @file is its JSON as-is
224
+ some-generator | npx agent-bridge call store.set @- # @- reads stdin
225
+ ```
226
+
227
+ A file holding an array is the argument list; any other JSON value is one
228
+ argument. With several words, `@feed.json` is one argument even when it is an
229
+ array. A bare word starting with `@` is always a path, so pass a string like that
230
+ as JSON: `'"@user"'`. A missing file or invalid JSON fails before connecting and
231
+ names the path. Only one `@-` per call. `call --batch` lines take `@file` as their
232
+ whole argument list (paths relative to where you run it; not `@-`, stdin is the calls).
66
233
 
67
234
  ## Realtime messages
68
235
 
@@ -92,9 +259,36 @@ last 200 errors and warnings.
92
259
 
93
260
  ## Repeat without the model
94
261
 
95
- Write the steps into a flow file and run it:
262
+ For a sequence of ad-hoc calls, pipe them into `call --batch` (or `repl`, which
263
+ behaves the same when stdin isn't a terminal): one process and one connection,
264
+ one JSON line back per call.
265
+
266
+ ```sh
267
+ printf '%s\n' 'router.navigate /inbox' 'screen.waitFor "Inbox"' 'screen.snapshot' \
268
+ | npx agent-bridge call --batch --stop-on-error
269
+ # {"tool":"router.navigate","ok":true,"ms":1.8,"appMs":1,"value":...}
270
+ # {"tool":"screen.waitFor","ok":false,"ms":2003.1,"error":"..."} (plus "logs"/"notice" when present)
271
+ ```
272
+
273
+ Each line is `tool args` in the same syntax as `call`; blank lines and `#` lines are
274
+ skipped, and arguments that start like JSON (`[`, `{`, `"`) but don't parse come back
275
+ as an error line. Lines starting with `.` (`.tools`, `.time <call> xN`, `.pending`,
276
+ `.restore`, `.exit`) run locally. `ms` is the round trip, `appMs` the time inside the app. It exits 1 if
277
+ any call failed; `--stop-on-error` stops at the first. Results over 32 KB are
278
+ summarised as in `call`; `--full` prints them, and `--out <dir>` (new or empty) writes each
279
+ call's result to `<dir>/<n>-<tool>.json` and puts the file summary in `value`.
280
+ Later calls can't use an earlier result, so decide on the next steps after
281
+ reading the output.
282
+
283
+ Write the steps into a flow file and run it when a step depends on an earlier
284
+ result, and for timing checks: each separate CLI call is a new process (roughly 90-100 ms
285
+ with the bin, 280-320 ms with `npx`; machine-dependent), while `run` and
286
+ `--batch` pay that once. `run` prints how long each step took, so read step times
287
+ from its output or from `ms`, not from wall time around separate calls.
96
288
 
97
289
  ```js
290
+ export const scenario = 'signedIn' // applied first, undone after, even on failure
291
+
98
292
  export default async ({ step }) => {
99
293
  await step('open form', 'screen.press', 'add-plant')
100
294
  await step('save empty', 'screen.press', 'save-plant')
@@ -105,6 +299,7 @@ export default async ({ step }) => {
105
299
 
106
300
  ```sh
107
301
  npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an error
302
+ npx agent-bridge run flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
108
303
  ```
109
304
 
110
305
  ## Clean up
@@ -114,8 +309,31 @@ npx agent-bridge session stop # runs bridge.restore; --keep leaves t
114
309
  ```
115
310
 
116
311
  `bridge.restore` unpins queries, puts back store and MMKV values, removes
117
- network mocks, unmutes realtime channels and ends a faked connection state, so
118
- the next run starts from the real state.
312
+ network mocks, unmutes realtime channels and ends a faked connection state,
313
+ then undoes active scenarios, so the next run starts from the real state.
314
+ It also resets queries that hold data a `net.mock` produced (`query.restore`
315
+ reports `mockedCleared`), so fake data can't survive a failing real refetch.
316
+ Only agent mocks are tracked, not app or scenario mocks. A query counts as
317
+ mocked when a request its queryFn started synchronously was answered by a mock.
318
+ A request started later (after an await, behind an async interceptor, in a
319
+ retry) can't be tied to a query, so every query fetching at that moment is
320
+ marked; on restore those reset even if real, and an observed one refetches
321
+ once. Infinite queries stay marked until reset.
322
+
323
+ Before you stop, `npx agent-bridge call bridge.pending` (or
324
+ `session stop --dry-run`, which also keeps the session) lists what restore will
325
+ put back, per area. A store is snapshotted on its first `store.set` or
326
+ `store.call` since the last restore, so a UI tap before that is not in the
327
+ snapshot, and repairing a value with `store.set` snapshots the broken one.
328
+ If a value looks wrong: read `bridge.pending`, then `store.commit '"<store>"'`
329
+ keeps the current value and drops the snapshot. `store.set <store> {}` arms the
330
+ snapshot before you drive the UI. `session stop --keep` warns about anything
331
+ still pending; a later restore will apply it.
332
+
333
+ The app may expose its own tools for hooks the bridge has no tool for
334
+ (`queue.flush`, `realtime.reconnect`); `tools` lists them. Whatever they change
335
+ through `onRestore` is undone by `bridge.restore` and listed under `app` in
336
+ `bridge.pending`. There is no `app.eval`: if the app has no tool for it, say so.
119
337
 
120
338
  ## Traps
121
339