@avasapp/agent-bridge 0.4.2 → 0.6.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 (65) hide show
  1. package/README.md +36 -14
  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.cjs +3 -0
  24. package/dist/adapters/tanstack-query.d.cts +13 -4
  25. package/dist/adapters/zustand.cjs +193 -22
  26. package/dist/adapters/zustand.d.cts +11 -15
  27. package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
  28. package/dist/cli.js +2370 -103
  29. package/dist/client/index.cjs +441 -136
  30. package/dist/client/index.d.ts +67 -4
  31. package/dist/client/index.js +5 -3
  32. package/dist/expo/index.d.cts +1 -1
  33. package/dist/network/index.cjs +320 -73
  34. package/dist/network/index.d.cts +18 -3
  35. package/dist/noop/async-storage.cjs +26 -0
  36. package/dist/noop/device.cjs +26 -0
  37. package/dist/noop/jotai.cjs +26 -0
  38. package/dist/noop/react-navigation.cjs +26 -0
  39. package/dist/noop/realtime.cjs +6 -1
  40. package/dist/noop/redux.cjs +28 -0
  41. package/dist/noop/secure-store.cjs +26 -0
  42. package/dist/realtime/index.cjs +71 -4
  43. package/dist/realtime/index.d.cts +21 -2
  44. package/dist/runtime/index.cjs +838 -128
  45. package/dist/runtime/index.d.cts +4 -2
  46. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  47. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  48. package/entries/async-storage.cjs +9 -0
  49. package/entries/device.cjs +9 -0
  50. package/entries/jotai.cjs +9 -0
  51. package/entries/react-navigation.cjs +9 -0
  52. package/entries/redux.cjs +9 -0
  53. package/entries/secure-store.cjs +9 -0
  54. package/package.json +34 -2
  55. package/skills/agent-bridge/SKILL.md +70 -22
  56. package/skills/agent-bridge/references/device.md +21 -0
  57. package/skills/agent-bridge/references/flows.md +19 -8
  58. package/skills/agent-bridge/references/large-data.md +6 -6
  59. package/skills/agent-bridge/references/network.md +25 -7
  60. package/skills/agent-bridge/references/realtime.md +5 -5
  61. package/skills/agent-bridge/references/restore.md +25 -7
  62. package/skills/agent-bridge/references/screen.md +25 -11
  63. package/skills/agent-bridge/references/state.md +31 -11
  64. package/skills/agent-bridge/references/webview.md +9 -9
  65. package/skills/agent-bridge/references/writing-scenarios.md +3 -3
@@ -1,5 +1,5 @@
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';
1
+ import { a as Transport, A as AgentBridgeOptions } from '../types-qlwdcGjt.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-qlwdcGjt.cjs';
3
3
 
4
4
  /**
5
5
  * Works in any React Native app served by Metro. The client evaluates one fixed
@@ -82,6 +82,8 @@ type ScreenElement = {
82
82
  expanded?: boolean;
83
83
  /** A WebView registered with the react-native-webview adapter: drive it with `webview.*`. */
84
84
  webview?: string;
85
+ /** `accessibilityActions` it handles with `onAccessibilityAction`, e.g. `activate` on a swipe button. */
86
+ actions?: string[];
85
87
  rect: Rect | null;
86
88
  /** Only in `screen.snapshot({ all: true })`. */
87
89
  onScreen?: boolean;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Keeps secrets out of what store.* prints. Either dotted paths per store
3
+ * (`{ auth: ['accessToken', 'user.phone'] }`), or a hook called for the store
4
+ * value and every nested value with its dotted path; what it returns is
5
+ * printed instead.
6
+ *
7
+ * Paths are store paths. A `store.call` result that is a piece of the store's
8
+ * state (same object, e.g. `() => get().user`) is matched by where it lives; a
9
+ * copy or derived value (`() => ({ ...get().user })`) is not, so its paths are
10
+ * matched relative to the result and store paths like `user.phone` won't apply.
11
+ * Use the hook, or don't return secrets from actions.
12
+ */
13
+ type Redact = Record<string, string[]> | ((store: string, path: string, value: unknown) => unknown);
14
+ /**
15
+ * Keeps secrets out of what an adapter prints, by the name of the slice or
16
+ * atom: dotted paths inside it (`{ auth: ['token', 'user.phone'] }`), or `true`
17
+ * for all of it (`{ authToken: true }`). Or a hook called for the whole value
18
+ * (path `''`) and every nested value with its dotted path; what it returns is
19
+ * printed instead.
20
+ */
21
+ type NamedRedact = Record<string, string[] | true> | ((name: string, path: string, value: unknown) => unknown);
22
+
23
+ export type { NamedRedact as N, Redact as R };
@@ -6,6 +6,10 @@ type CallMessage = {
6
6
  args?: unknown[];
7
7
  /** Device to run on. Expo's socket broadcasts, so every app sees every call. */
8
8
  to?: string;
9
+ /** A session's own: the journal tag it holds, so the reply carries the journal only when it is stale. */
10
+ journalAck?: string;
11
+ /** A session's own secret; the runtime keeps the first one it gets and ignores the journal calls of anyone else. */
12
+ journalKey?: string;
9
13
  };
10
14
  /** An error or warning the app logged, threw or left unhandled. */
11
15
  type LogEntry = {
@@ -39,6 +43,10 @@ type ResultMessage = ({
39
43
  loadId?: string;
40
44
  /** Areas (`store`, `query`, ...) whose `*.restore` has something to undo. */
41
45
  pending?: string[];
46
+ /** Goes up when something persistent changed that a restore could undo. */
47
+ journal?: number;
48
+ /** What each area could undo from storage; sent only to a session that proved it holds the key and a stale tag. */
49
+ journalData?: Record<string, unknown>;
42
50
  };
43
51
  type ToolInfo = {
44
52
  name: string;
@@ -82,6 +90,20 @@ type ToolDefinition = ToolFn | {
82
90
  pending?: (options?: {
83
91
  detail?: boolean;
84
92
  }) => unknown;
93
+ /**
94
+ * On a `*.restore` tool: what it could undo that lives in persistent
95
+ * storage (so it outlives a reload), as JSON, or undefined. A session
96
+ * keeps it, and hands it to `adopt` after the app reloads.
97
+ */
98
+ journal?: () => unknown;
99
+ /**
100
+ * Takes what `journal` returned before a reload and re-arms the undo for
101
+ * it in the new load, without changing the app. Returns an
102
+ * `AdoptResult`.
103
+ */
104
+ adopt?: (data: unknown) => unknown;
105
+ /** Goes up when `journal` may have changed. Not shared state: keep it in the adapter's own module. */
106
+ journalRev?: () => number;
85
107
  };
86
108
  /** Tools by name. Namespace them with a dot, e.g. `query.pin`. */
87
109
  type Tools = Record<string, ToolDefinition>;
@@ -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/async-storage.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/async-storage.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/device.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/device.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/jotai.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/jotai.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/react-navigation.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/react-navigation.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/redux.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/redux.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/secure-store.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/secure-store.cjs')
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avasapp/agent-bridge",
3
- "version": "0.4.2",
3
+ "version": "0.6.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",
@@ -38,6 +38,14 @@
38
38
  "types": "./dist/adapters/zustand.d.cts",
39
39
  "default": "./entries/zustand.cjs"
40
40
  },
41
+ "./redux": {
42
+ "types": "./dist/adapters/redux.d.cts",
43
+ "default": "./entries/redux.cjs"
44
+ },
45
+ "./jotai": {
46
+ "types": "./dist/adapters/jotai.d.cts",
47
+ "default": "./entries/jotai.cjs"
48
+ },
41
49
  "./react-native-mmkv": {
42
50
  "types": "./dist/adapters/react-native-mmkv.d.cts",
43
51
  "default": "./entries/react-native-mmkv.cjs"
@@ -46,6 +54,10 @@
46
54
  "types": "./dist/adapters/expo-router.d.cts",
47
55
  "default": "./entries/expo-router.cjs"
48
56
  },
57
+ "./react-navigation": {
58
+ "types": "./dist/adapters/react-navigation.d.cts",
59
+ "default": "./entries/react-navigation.cjs"
60
+ },
49
61
  "./react-native-webview": {
50
62
  "types": "./dist/adapters/react-native-webview.d.cts",
51
63
  "default": "./entries/react-native-webview.cjs"
@@ -66,6 +78,18 @@
66
78
  "types": "./dist/adapters/socket-io.d.cts",
67
79
  "default": "./entries/socket.io.cjs"
68
80
  },
81
+ "./async-storage": {
82
+ "types": "./dist/adapters/async-storage.d.cts",
83
+ "default": "./entries/async-storage.cjs"
84
+ },
85
+ "./secure-store": {
86
+ "types": "./dist/adapters/secure-store.d.cts",
87
+ "default": "./entries/secure-store.cjs"
88
+ },
89
+ "./device": {
90
+ "types": "./dist/adapters/device.d.cts",
91
+ "default": "./entries/device.cjs"
92
+ },
69
93
  "./client": {
70
94
  "types": "./dist/client/index.d.ts",
71
95
  "import": "./dist/client/index.js",
@@ -92,7 +116,11 @@
92
116
  "cdp",
93
117
  "tanstack-query",
94
118
  "zustand",
119
+ "redux",
120
+ "jotai",
95
121
  "mmkv",
122
+ "async-storage",
123
+ "secure-store",
96
124
  "realtime",
97
125
  "ably",
98
126
  "socket.io",
@@ -103,8 +131,9 @@
103
131
  "build": "bun run generate && rm -rf dist && tsup",
104
132
  "test": "bun run generate && bun test src/",
105
133
  "typecheck": "bun run generate && tsc -p tsconfig.json --noEmit",
106
- "check": "bun run typecheck && bun run test && bun run build && bun run check:gates",
134
+ "check": "bun run typecheck && bun run test && bun run build && bun run check:gates && bun run check:consumer-types",
107
135
  "check:gates": "node scripts/check-gates.mjs",
136
+ "check:consumer-types": "node scripts/check-consumer-types.mjs",
108
137
  "changeset": "changeset",
109
138
  "release": "bun run check && changeset publish",
110
139
  "prepublishOnly": "bun run check"
@@ -134,12 +163,15 @@
134
163
  "devDependencies": {
135
164
  "@changesets/cli": "^3.0.3",
136
165
  "@expo/devtools": "^57.0.1",
166
+ "@reduxjs/toolkit": "^2.14.0",
137
167
  "@tanstack/query-core": "^5.90.0",
138
168
  "@types/bun": "^1.4.2",
139
169
  "@types/react": "^19.3.0",
140
170
  "@types/ws": "^8.18.1",
141
171
  "ably": "^2.29.0",
142
172
  "happy-dom": "^20.14.5",
173
+ "jotai": "^3.0.2",
174
+ "redux-persist": "^6.0.0",
143
175
  "socket.io-client": "^4.8.4",
144
176
  "tsup": "^8.5.1",
145
177
  "typescript": "~6.0.3",
@@ -26,28 +26,49 @@ bridge call can set.
26
26
 
27
27
  ## Run the CLI directly
28
28
 
29
- The examples say `npx agent-bridge` for brevity. Prefer the installed bin,
30
- `./node_modules/.bin/agent-bridge` (or `bunx agent-bridge`): `npx` loads npm's
29
+ The examples say `npx @avasapp/agent-bridge`; never drop the scope, because
30
+ `agent-bridge` on npm is an unrelated package. Prefer the installed bin,
31
+ `./node_modules/.bin/agent-bridge` (with bun too): `npx` loads npm's
31
32
  config on every call, so a project `.npmrc` with keys npm doesn't know prints
32
33
  `npm warn Unknown project config` into your output each time, and it adds
33
34
  roughly 200 ms more startup per call (machine-dependent).
34
35
 
36
+ If the app's tools are already in your tool list as an MCP server
37
+ (`agent-bridge mcp`; names like `screen_snapshot` for `screen.snapshot`), call
38
+ those: each takes `args`, an array of the tool's arguments, and `full: true` for
39
+ a result over 32 KB. If the list is empty or a call fails, call `agent_bridge_status`. A failed call keeps the session's own error (`may or may not have run`): check state before repeating a call that changes it. The rest of this skill applies the same way.
40
+
35
41
  ## Start a session
36
42
 
37
43
  ```sh
38
- npx agent-bridge session start # add --metro host:port if Metro isn't on localhost:8081
39
- npx agent-bridge tools # what this app exposes
44
+ npx @avasapp/agent-bridge session start # add --metro host:port if Metro isn't on localhost:8081
45
+ npx @avasapp/agent-bridge tools # what this app exposes
40
46
  ```
41
47
 
48
+ If Metro runs on another port, pass the same `--metro` (or set
49
+ `AGENT_BRIDGE_METRO`) on every command that doesn't go through the session. A
50
+ `Nothing is listening on …` error names the Metro it found instead and its apps;
51
+ check it's yours before switching to it.
52
+
53
+ Calls take `--timeout <ms>` (default 10000) for a slow call. To drive two apps
54
+ at once, start named sessions (`session start --name ios`, `--name android`) and
55
+ pass `--session <name>` on each call. A session is found from the nearest
56
+ `package.json` above where you run: a command at a monorepo root also finds the
57
+ one session started in an app folder under it (downward only; a nested checkout
58
+ with its own `.git` doesn't count). With several app sessions under the root it
59
+ connects directly, so pass `--session`.
60
+
42
61
  A session holds one connection for all your calls. It stops itself after 15
43
62
  minutes without calls and runs `bridge.restore` when it does.
44
63
 
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
49
- again in `session stop`. When you see it, redo your setup: nothing you changed
50
- before the reload will be undone.
64
+ A JS reload (an error screen's Retry, or `app.reload`) throws away the app's
65
+ undo state, except what lives in storage: string `mmkv.set`/`mmkv.delete` and
66
+ persisted zustand stores are kept by the session and still undone by
67
+ `bridge.restore`. The session reports what it kept, and
68
+ `app reloaded; N pending restores lost: query, ...` (the areas that had
69
+ something to undo) for the rest, as a warning on the next call (also on a
70
+ failed one) and again in `session stop`. For the lost ones, redo your setup:
71
+ they will not be undone.
51
72
 
52
73
  If a call fails with `app not connected; reconnecting` or `The app is gone`, the
53
74
  session lost the app (Metro or the app is down, or mid-reload). It retries on
@@ -55,17 +76,32 @@ its 5 s health check and on your next call: wait a few seconds and call again.
55
76
  `session stop` still works. A session never moves to another app: `<app> is
56
77
  not connected; connected: …` means its app is gone and something else is there.
57
78
 
79
+ If a call fails with `the app reloaded during <tool>; it may or may not have run`
80
+ (or `disconnected`, `the connection dropped`), the connection dropped mid-call
81
+ and the session did not repeat it (only read-only tools are repeated). Check the screen or state before you call again.
82
+
83
+ A `timeoutMs` in the options of `screen.waitFor` or `webview.waitFor` longer
84
+ than the call timeout stretches it, so you don't need `--timeout` for that.
85
+
58
86
  If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
59
87
  this build. Say so; don't fall back to tapping silently.
60
88
 
89
+ If the app doesn't have the package or a bridge file yet, run
90
+ `npx @avasapp/agent-bridge init --json` in the app (`--dir apps/x` from a
91
+ monorepo root). It writes `src/dev/agent-bridge.tsx` but never edits the root
92
+ layout: paste `mount.snippet` into `mount.file` as `mount.note` says. Ask the
93
+ user about each entry in `choices` it reports as `open` (such as network
94
+ tools), then rerun with the `flag` it names, or add its `lines` by hand when
95
+ there's no flag. Then follow `next`, from `runFrom`.
96
+
61
97
  ## Start from a scenario
62
98
 
63
99
  Apps can define named setups, such as a locally signed-in user. Use one
64
100
  instead of signing in by hand or seeding auth state yourself:
65
101
 
66
102
  ```sh
67
- npx agent-bridge scenarios # what this app defines
68
- npx agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
103
+ npx @avasapp/agent-bridge scenarios # what this app defines
104
+ npx @avasapp/agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
69
105
  ```
70
106
 
71
107
  `scenarios` shows each one's options as a JSON Schema. Bad options fail
@@ -85,29 +121,37 @@ real account instead. If you're asked to add one, read
85
121
  ## See and use the screen
86
122
 
87
123
  ```sh
88
- npx agent-bridge call screen.snapshot # buttons, inputs, text on the focused screen
89
- npx agent-bridge call screen.press '"add-plant"' # by testID, label or text
90
- npx agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
91
- npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
92
- npx agent-bridge call router.navigate /inbox
124
+ npx @avasapp/agent-bridge call screen.snapshot # one line per button, input, text on the focused screen (--json for rects)
125
+ npx @avasapp/agent-bridge call screen.press '"add-plant"' # by testID, label or text
126
+ npx @avasapp/agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
127
+ npx @avasapp/agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
128
+ npx @avasapp/agent-bridge call router.navigate /inbox
93
129
  ```
94
130
 
131
+ Check `tools` for `router.*` (expo-router) or `nav.*` (React Navigation). `nav.*`
132
+ takes route names (`nav.navigate Inbox`, `nav.popTo Home`), and paths only if the
133
+ app passed `linking`; read the returned `warning`.
134
+
95
135
  `press` and `fill` return once the app has rendered the result, so the next
96
- check sees it. Arguments are a JSON array (or a single JSON value): the array is
136
+ check sees it. `press` on plain text presses the nearest pressable ancestor.
137
+ Unfocused tabs (native tabs included) and screens under others count as hidden.
138
+ Screen tools need React Native's New Architecture (Fabric); where nothing can
139
+ be measured they fail with a message saying so (`snapshot --all` and
140
+ `findText` still return text, without rects). Arguments are a JSON array (or a single JSON value): the array is
97
141
  spread into the tool's arguments.
98
142
 
99
143
  ## Watch for errors
100
144
 
101
145
  Every reply carries errors the app logged or threw since the previous reply,
102
146
  printed as `! error during <tool>` or `! error after <tool>`. Treat them as
103
- failures of the step they name. `npx agent-bridge call bridge.logs` shows the
104
- last 200 errors and warnings.
147
+ failures of the step they name. `npx @avasapp/agent-bridge call bridge.logs`
148
+ shows the last 200 errors and warnings.
105
149
 
106
150
  ## Clean up
107
151
 
108
152
  ```sh
109
- npx agent-bridge call bridge.pending # what restore will put back, per area
110
- npx agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
153
+ npx @avasapp/agent-bridge call bridge.pending # what restore will put back, per area
154
+ npx @avasapp/agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
111
155
  ```
112
156
 
113
157
  `bridge.restore` unpins queries, puts back store values, removes network mocks,
@@ -123,6 +167,7 @@ from the real state.
123
167
  | Read or set query data, store values, routes, modals; get argument shapes right | [references/state.md](references/state.md) |
124
168
  | Mock or inspect requests, or deal with strict network mode | [references/network.md](references/network.md) |
125
169
  | See or fake realtime messages (`realtime.*` in `tools`) | [references/realtime.md](references/realtime.md) |
170
+ | Fake a deep link, background, offline, a push or the clock (`device.*` in `tools`) | [references/device.md](references/device.md) |
126
171
  | Handle a result over 32 KB or an argument over ~128 KB | [references/large-data.md](references/large-data.md) |
127
172
  | Run many calls at once, write a flow file, or measure timing | [references/flows.md](references/flows.md) |
128
173
  | Understand what restore undoes, or fix a value restore got wrong | [references/restore.md](references/restore.md) |
@@ -138,4 +183,7 @@ The app may expose its own tools for hooks the bridge has no tool for
138
183
  overlap. Finish a flow with one real UI check or screenshot from agent-device.
139
184
  - `fill` skips the keyboard: autocorrect and native-only input behaviour need
140
185
  one real typing step from agent-device.
186
+ - Swipe and drag gestures need agent-device, unless the control is screen-reader
187
+ operable: `press` sends its `activate` accessibility action (see
188
+ [references/screen.md](references/screen.md)).
141
189
  - With several apps on one Metro, pass `--device`.
@@ -0,0 +1,21 @@
1
+ # Device events (`device.*`)
2
+
3
+ Only there if the app added `deviceTools`; check `bridge.tools`.
4
+
5
+ ```sh
6
+ npx @avasapp/agent-bridge call device.openUrl '"myapp://orders/5"' # deep link into the running app; returns the route when the app passes navigation
7
+ npx @avasapp/agent-bridge call device.appState '"background"' # then '"active"'
8
+ npx @avasapp/agent-bridge call device.offline true # fetch, XHR and NetInfo; false goes back
9
+ npx @avasapp/agent-bridge call device.notification '[{"title":"Hi","data":{"id":5}},{"tapped":true}]'
10
+ npx @avasapp/agent-bridge call device.setTime '"2030-01-02T03:04:05Z"' # only with time: true
11
+ npx @avasapp/agent-bridge call device.restore
12
+ ```
13
+
14
+ - `device.openUrl` is for a running app. A cold start reads its URL before the bridge can fake it: open that link with agent-device.
15
+ - `device.offline` fails mocked requests too. Metro stays reachable.
16
+ - Without `{tapped:true}` a notification calls the received listeners (app open); with it, the response listeners.
17
+ - The clock moves `Date.now()` and `new Date()` only; timers do not move. After `device.setTime`, call `device.appState '"background"'` then `'"active"'` so query caches recheck staleness (`active` alone does nothing when the app is already active). JS-driven `Animated` animations and Reanimated worklets are not shifted safely.
18
+ - `device.offline` shows in `net.log` as `(device offline)`. NetInfo is faked only for listeners, `useNetInfo` and `NetInfo.refresh`; `fetch('wifi')` and a real native network event are not.
19
+ - `device.notification` returns `listeners`; 0 means nothing heard it.
20
+ - `device.openUrl` also reaches `useLinkingURL()`, but `getLinkingURL()` still reads the native value.
21
+ - `bridge.pending` lists what is set under `device`, and `bridge.restore` undoes it.
@@ -8,7 +8,7 @@ one JSON line back per call.
8
8
 
9
9
  ```sh
10
10
  printf '%s\n' 'router.navigate /inbox' 'screen.waitFor "Inbox"' 'screen.snapshot' \
11
- | npx agent-bridge call --batch --stop-on-error
11
+ | npx @avasapp/agent-bridge call --batch --stop-on-error
12
12
  # {"tool":"router.navigate","ok":true,"ms":1.8,"appMs":1,"value":...}
13
13
  # {"tool":"screen.waitFor","ok":false,"ms":2003.1,"error":"..."} (plus "logs"/"notice" when present)
14
14
  ```
@@ -29,23 +29,23 @@ reading the output.
29
29
  ## A flow file
30
30
 
31
31
  Write the steps into a flow file and run it when a step depends on an earlier
32
- result, and for timing checks.
32
+ result, and for timing checks. Name it `flows/<name>.flow.mjs` in the app: plain
33
+ `.mjs`, never under `app/`, never `*.test.*`.
33
34
 
34
35
  ```js
35
36
  export const scenario = 'signedIn' // applied first, undone after, even on failure
36
37
 
37
- export default async ({ step }) => {
38
+ export default async ({ step, expect }) => {
38
39
  await step('open form', 'screen.press', 'add-plant')
39
40
  await step('save empty', 'screen.press', 'save-plant')
40
- await step('error shown', 'screen.waitFor', 'Name is required')
41
- await step('undo', 'bridge.restore')
41
+ await expect.text('Name is required') // also expect.gone, expect.route, expect.result
42
42
  }
43
43
  ```
44
44
 
45
45
  ```sh
46
- npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an error
47
- npx agent-bridge run flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
48
- npx agent-bridge run flow.mjs --scenario 'signedIn={"user":{"name":"Ada"}}' # with options
46
+ npx @avasapp/agent-bridge run flows/check.flow.mjs --strict # --strict fails if the app logged an error
47
+ npx @avasapp/agent-bridge run flows/check.flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
48
+ npx @avasapp/agent-bridge run flows/check.flow.mjs --scenario 'signedIn={"user":{"name":"Ada"}}' # with options
49
49
  ```
50
50
 
51
51
  A flow can declare several scenarios, with options:
@@ -55,6 +55,17 @@ when the flow fails. The flow gets what each scenario's `apply` returned as
55
55
  `scenarios.signedIn`. A failed restorer makes the run exit non-zero.
56
56
  `--scenario` replaces a declared one of the same name.
57
57
 
58
+ Run several at once: `run flows/` (a directory's `*.flow.mjs`), several files, or a
59
+ glob, in order on one connection, with a pass/fail line each and a summary; exit 1 if
60
+ any failed. `--bail` stops at the first failure, `--grep <text>` filters by name or path
61
+ (relative to here), `--reporter json|junit` is for CI: report on stdout with all
62
+ other output on stderr, or `--out report.xml`. Globs only match `*.flow.mjs`. Every flow ends with
63
+ `bridge.restore`, pass or fail (it is global: through a session it also undoes what
64
+ you set up by hand first); `--keep` skips it to debug. A failed flow prints the
65
+ screen elements and route. `expect.text(t)`, `expect.gone(t)`, `expect.route(path)`
66
+ and `expect.result(tool, args, match)` (a RegExp matches a string) throw readable
67
+ messages.
68
+
58
69
  `step` takes the same argument shapes as `call`; see [state.md](state.md).
59
70
 
60
71
  ## Timing
@@ -11,9 +11,9 @@ and `connect()` always get the full value.
11
11
  Narrow the call before reaching for `--out`:
12
12
 
13
13
  ```sh
14
- npx agent-bridge call query.get '["feed", {"pages": [0, 2]}]' # first two pages of an infinite query, plus totalPages
15
- npx agent-bridge call query.get '["feed", {"path": "pages.0.items"}]' # just that value
16
- npx agent-bridge call net.mocks '[{"full": true}]' # net.mocks cuts bodies over ~2 KB unless full
14
+ npx @avasapp/agent-bridge call query.get '["feed", {"pages": [0, 2]}]' # first two pages of an infinite query, plus totalPages
15
+ npx @avasapp/agent-bridge call query.get '["feed", {"path": "pages.0.items"}]' # just that value
16
+ npx @avasapp/agent-bridge call net.mocks '[{"full": true}]' # net.mocks cuts bodies over ~2 KB unless full
17
17
  ```
18
18
 
19
19
  `pages` is `[from, to]` with `to` exclusive, like `Array.slice`; `path` applies
@@ -25,9 +25,9 @@ The shell caps one argument at about 128 KB on Linux. For a bigger value (a long
25
25
  feed, a large `net.mock` body), write JSON to a file and pass `@file`:
26
26
 
27
27
  ```sh
28
- npx agent-bridge call query.pin @args.json # the file is the argument list, like the inline form
29
- npx agent-bridge call net.mock '"/feed"' @feed.json # several words: each is one argument; @file is its JSON as-is
30
- some-generator | npx agent-bridge call store.set @- # @- reads stdin
28
+ npx @avasapp/agent-bridge call query.pin @args.json # the file is the argument list, like the inline form
29
+ npx @avasapp/agent-bridge call net.mock '"/feed"' @feed.json # several words: each is one argument; @file is its JSON as-is
30
+ some-generator | npx @avasapp/agent-bridge call store.set @- # @- reads stdin
31
31
  ```
32
32
 
33
33
  A file holding an array is the argument list; any other JSON value is one
@@ -1,11 +1,12 @@
1
1
  # Network mocks and logs
2
2
 
3
3
  ```sh
4
- npx agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
5
- npx agent-bridge call net.log '[{"since": 1700000000000}]' # entries carry startedAt (epoch ms)
6
- npx agent-bridge call net.entry 3 # one request, bodies whole
7
- npx agent-bridge call net.mockFromLog '[3, {"user": {"name": "Moss"}}]' # a real response, patched
8
- npx agent-bridge call net.mocks '[{"full": true}]' # active mocks; bodies over ~2 KB are cut unless full
4
+ npx @avasapp/agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
5
+ npx @avasapp/agent-bridge call net.log '[{"since": 1700000000000}]' # entries carry startedAt (epoch ms)
6
+ npx @avasapp/agent-bridge call net.entry 3 # one request: bodies whole, headers (credential headers such as Authorization, Cookie, *-token, *-api-key cut unless {"full": true} second)
7
+ npx @avasapp/agent-bridge call net.waitFor '[{"path": "/orders", "method": "POST"}, {"timeoutMs": 5000}]' # wait for it to finish
8
+ npx @avasapp/agent-bridge call net.mockFromLog '[3, {"user": {"name": "Moss"}}]' # a real response, patched
9
+ npx @avasapp/agent-bridge call net.mocks '[{"full": true}]' # active mocks; bodies over ~2 KB are cut unless full
9
10
  ```
10
11
 
11
12
  `net.log` cuts bodies at about 2 KB; use `net.entry <id>` (or `{"full": true}`)
@@ -15,6 +16,23 @@ The newest mock is tried first, so a later broad mock shadows an earlier
15
16
  specific one: give the specific one `{"priority": 1}` as `net.mock`'s third
16
17
  argument, or read `shadows` in its result.
17
18
 
19
+ ## What a mock matches
20
+
21
+ A string is a URL substring: `"/orders"` also answers `/orders/5/items`. For
22
+ exact matches use the object form (every key given must match):
23
+
24
+ ```sh
25
+ call net.mock '[{"path": "/orders/:id"}, {"json": {"id": 5}}]' # whole path, any host, query ignored; * is the rest
26
+ call net.mock '[{"graphql": "GetUser"}, {"json": {"data": {}}}]' # operationName in the POST body or ?operationName=
27
+ call net.mock '[{"path": "/search", "body": {"filter": {"status": "open"}}}, {"json": []}]' # JSON body contains this
28
+ ```
29
+
30
+ `path` ignores case and the query, takes `:name` only as a whole segment (`/items:batchGet` is literal), and a trailing slash is optional; spaces are refused (write `%20`). A mock with only `body` or `graphql` looks at every request. `body` matches objects by key (extra keys are fine) and arrays by length and
31
+ position; form and binary bodies never match. A batched GraphQL array body
32
+ isn't matched by `graphql`; use `body`. `net.mockFromLog` returns a `warning` when a POST body wasn't kept and the mock may answer other operations. The reply's `matches` says it in words.
33
+ `net.waitFor` takes the same match; pass `{"sinceId": <id from net.log>}` so an
34
+ older request doesn't satisfy it.
35
+
18
36
  For a mock body too big for the command line, pass it as `@file`; see
19
37
  [large-data.md](large-data.md).
20
38
 
@@ -25,8 +43,8 @@ answers fails with a 501, and the reply carries an error that names it
25
43
  (`! error during …: agent-bridge strict network: no mock for GET https://…`).
26
44
  Add a mock for it (`net.mock`), don't turn strict mode off.
27
45
 
28
- `npx agent-bridge call net.strict` lists what it blocked. Strict mode sees JS
29
- `fetch`/XHR only, not images, WebSockets or native SDKs.
46
+ `npx @avasapp/agent-bridge call net.strict` lists what it blocked. Strict mode
47
+ sees JS `fetch`/XHR only, not images, WebSockets or native SDKs.
30
48
 
31
49
  ## Mocks and cached queries
32
50
 
@@ -4,11 +4,11 @@ If the app has `realtime.*` tools, you can see its realtime messages and fake
4
4
  them instead of waiting for a backend to send one:
5
5
 
6
6
  ```sh
7
- npx agent-bridge call realtime.channels # what the app listens on
8
- npx agent-bridge call realtime.log '{"channel": "chat"}' # recent messages, real and faked
9
- npx agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
10
- npx agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
11
- npx agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
7
+ npx @avasapp/agent-bridge call realtime.channels # what the app listens on
8
+ npx @avasapp/agent-bridge call realtime.log '{"channel": "chat"}' # recent messages: real, faked, and `sent: true` ones the app sent (credential keys are [redacted] unless `{"full":true}`)
9
+ npx @avasapp/agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
10
+ npx @avasapp/agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
11
+ npx @avasapp/agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
12
12
  ```
13
13
 
14
14
  Copy the shape of a real message from `realtime.log` before you emit one. With
@@ -1,20 +1,27 @@
1
1
  # What restore undoes
2
2
 
3
3
  ```sh
4
- npx agent-bridge call bridge.pending # what restore will put back, per area
5
- npx agent-bridge session stop --dry-run # the same, and keeps the session
6
- npx agent-bridge session stop # runs bridge.restore
7
- npx agent-bridge session stop --keep # leaves the app as is; warns about anything still pending
4
+ npx @avasapp/agent-bridge call bridge.pending # what restore will put back, per area
5
+ npx @avasapp/agent-bridge session stop --dry-run # the same, and keeps the session
6
+ npx @avasapp/agent-bridge session stop # runs bridge.restore
7
+ npx @avasapp/agent-bridge session stop --keep # leaves the app as is; warns about anything still pending
8
8
  ```
9
9
 
10
- `bridge.restore` unpins queries, puts back store and MMKV values, removes
10
+ `bridge.restore` unpins queries, puts back store, MMKV, AsyncStorage and SecureStore values, removes
11
11
  network mocks, unmutes realtime channels and ends a faked connection state,
12
12
  then undoes active scenarios (newest first), so the next run starts from the
13
13
  real state. With `session stop --keep`, a later restore will still apply what
14
14
  is pending.
15
15
 
16
- A JS reload throws away every pending restore; the session warns
17
- `app reloaded; N pending restores lost`. Redo your setup when you see it.
16
+ A JS reload throws away the app's undo state. In a session, what lives in
17
+ storage is kept: `mmkv.set`/`mmkv.delete` of string values and `store.set`/
18
+ `store.call` on a persisted zustand store (one made with `persist`) are still
19
+ undone by `bridge.restore` after the reload, and the next call says
20
+ `app reloaded; kept N undos across the reload (...)`. A value the app changed
21
+ after your write is left alone and named. Everything else (stores that are not
22
+ persisted, redux, jotai, AsyncStorage and SecureStore changes, queries, mocks, `onRestore`) is lost: the session warns
23
+ `app reloaded; N pending restores lost: ...`. Redo that setup when you see it. After a reload that also lost non-persisted changes, check `bridge.pending`: `kept N undos` does not list what was lost in the same area. `not kept across reload: store:key` means that key (a Set, Date, class instance, one `partialize` renames, or one `redact` hides) is not journaled, so the reload keeps the agent's value for it; `could not re-arm after the reload: ...` means a store or MMKV instance never became ready and its undo was dropped; `<tool> was cut off; ... that undo may be lost` means a write's reply was lost and may not be journaled.
24
+ AsyncStorage and SecureStore values stay on disk across a reload with their old values gone, so run `bridge.restore` before `app.reload`. A key the app wrote after the agent did is left alone and returned under `skipped`.
18
25
 
19
26
  ## Mocked queries
20
27
 
@@ -38,6 +45,17 @@ with `store.set` snapshots the broken one.
38
45
  keeps the current value and drops the snapshot.
39
46
  - `store.set <store> {}` arms the snapshot before you drive the UI.
40
47
 
48
+ `redux.*` and `jotai.*` work the same way, under their own names. `redux.set`
49
+ snapshots each slice on its first change and restore puts back only those
50
+ slices; `redux.dispatch` snapshots the whole state on its first call, so restore
51
+ puts all of it back, including what the app changed in other slices after that.
52
+ A slice or atom the app changed after the agent is left alone and returned in
53
+ `{restored, skipped}`. `redux.commit` drops the snapshots. Jotai snapshots each
54
+ atom on its first `jotai.set`; `jotai.commit <atom>` drops one. These snapshots
55
+ live in memory, so **restore before `app.reload`**: with redux-persist or
56
+ `atomWithStorage` the agent's values stay saved until then. With an async
57
+ `atomWithStorage`, `jotai.get` the atom before the first set.
58
+
41
59
  ## WebViews
42
60
 
43
61
  `webview.press`, `fill` and `send` change the page and app, and restore does