@avasapp/agent-bridge 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +20 -6
  2. package/dist/adapters/ably.cjs +79 -3
  3. package/dist/adapters/ably.d.cts +2 -1
  4. package/dist/adapters/async-storage.cjs +158 -0
  5. package/dist/adapters/async-storage.d.cts +14 -0
  6. package/dist/adapters/device.cjs +1110 -0
  7. package/dist/adapters/device.d.cts +49 -0
  8. package/dist/adapters/expo-router.cjs +112 -6
  9. package/dist/adapters/expo-router.d.cts +1 -1
  10. package/dist/adapters/jotai.cjs +266 -0
  11. package/dist/adapters/jotai.d.cts +19 -0
  12. package/dist/adapters/react-native-mmkv.cjs +80 -0
  13. package/dist/adapters/react-native-mmkv.d.cts +3 -1
  14. package/dist/adapters/react-native-webview.d.cts +1 -1
  15. package/dist/adapters/react-navigation.cjs +293 -0
  16. package/dist/adapters/react-navigation.d.cts +55 -0
  17. package/dist/adapters/redux.cjs +322 -0
  18. package/dist/adapters/redux.d.cts +28 -0
  19. package/dist/adapters/secure-store.cjs +169 -0
  20. package/dist/adapters/secure-store.d.cts +28 -0
  21. package/dist/adapters/socket-io.cjs +77 -3
  22. package/dist/adapters/socket-io.d.cts +2 -1
  23. package/dist/adapters/tanstack-query.d.cts +1 -1
  24. package/dist/adapters/zustand.cjs +193 -22
  25. package/dist/adapters/zustand.d.cts +11 -15
  26. package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
  27. package/dist/cli.js +1383 -255
  28. package/dist/client/index.cjs +233 -27
  29. package/dist/client/index.d.ts +67 -4
  30. package/dist/client/index.js +5 -3
  31. package/dist/expo/index.d.cts +1 -1
  32. package/dist/network/index.cjs +320 -73
  33. package/dist/network/index.d.cts +18 -3
  34. package/dist/noop/async-storage.cjs +26 -0
  35. package/dist/noop/device.cjs +26 -0
  36. package/dist/noop/jotai.cjs +26 -0
  37. package/dist/noop/react-navigation.cjs +26 -0
  38. package/dist/noop/realtime.cjs +6 -1
  39. package/dist/noop/redux.cjs +28 -0
  40. package/dist/noop/secure-store.cjs +26 -0
  41. package/dist/realtime/index.cjs +71 -4
  42. package/dist/realtime/index.d.cts +21 -2
  43. package/dist/runtime/index.cjs +822 -140
  44. package/dist/runtime/index.d.cts +2 -2
  45. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  46. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  47. package/entries/async-storage.cjs +9 -0
  48. package/entries/device.cjs +9 -0
  49. package/entries/jotai.cjs +9 -0
  50. package/entries/react-navigation.cjs +9 -0
  51. package/entries/redux.cjs +9 -0
  52. package/entries/secure-store.cjs +9 -0
  53. package/package.json +32 -1
  54. package/skills/agent-bridge/SKILL.md +39 -8
  55. package/skills/agent-bridge/references/device.md +21 -0
  56. package/skills/agent-bridge/references/flows.md +13 -3
  57. package/skills/agent-bridge/references/network.md +19 -1
  58. package/skills/agent-bridge/references/realtime.md +1 -1
  59. package/skills/agent-bridge/references/restore.md +21 -3
  60. package/skills/agent-bridge/references/screen.md +8 -2
  61. package/skills/agent-bridge/references/state.md +21 -1
@@ -1,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
@@ -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.5.0",
3
+ "version": "0.6.1",
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",
@@ -135,12 +163,15 @@
135
163
  "devDependencies": {
136
164
  "@changesets/cli": "^3.0.3",
137
165
  "@expo/devtools": "^57.0.1",
166
+ "@reduxjs/toolkit": "^2.14.0",
138
167
  "@tanstack/query-core": "^5.90.0",
139
168
  "@types/bun": "^1.4.2",
140
169
  "@types/react": "^19.3.0",
141
170
  "@types/ws": "^8.18.1",
142
171
  "ably": "^2.29.0",
143
172
  "happy-dom": "^20.14.5",
173
+ "jotai": "^3.0.2",
174
+ "redux-persist": "^6.0.0",
144
175
  "socket.io-client": "^4.8.4",
145
176
  "tsup": "^8.5.1",
146
177
  "typescript": "~6.0.3",
@@ -33,6 +33,11 @@ config on every call, so a project `.npmrc` with keys npm doesn't know prints
33
33
  `npm warn Unknown project config` into your output each time, and it adds
34
34
  roughly 200 ms more startup per call (machine-dependent).
35
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. With no app connected the MCP list shows only the built-in tools and calls fail until the app connects; if 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
+
36
41
  ## Start a session
37
42
 
38
43
  ```sh
@@ -45,15 +50,25 @@ If Metro runs on another port, pass the same `--metro` (or set
45
50
  `Nothing is listening on …` error names the Metro it found instead and its apps;
46
51
  check it's yours before switching to it.
47
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
+
48
61
  A session holds one connection for all your calls. It stops itself after 15
49
62
  minutes without calls and runs `bridge.restore` when it does.
50
63
 
51
- A JS reload (an error screen's Retry, or `app.reload`) throws away every pending
52
- restore. The session notices the app is a new runtime and reports
53
- `app reloaded; N pending restores lost: store, query` (the areas that
54
- had something to undo) as a warning on the next call (also on a failed one) and
55
- again in `session stop`. When you see it, redo your setup: nothing you changed
56
- 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.
57
72
 
58
73
  If a call fails with `app not connected; reconnecting` or `The app is gone`, the
59
74
  session lost the app (Metro or the app is down, or mid-reload). It retries on
@@ -61,6 +76,13 @@ its 5 s health check and on your next call: wait a few seconds and call again.
61
76
  `session stop` still works. A session never moves to another app: `<app> is
62
77
  not connected; connected: …` means its app is gone and something else is there.
63
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
+
64
86
  If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
65
87
  this build. Say so; don't fall back to tapping silently.
66
88
 
@@ -99,15 +121,23 @@ real account instead. If you're asked to add one, read
99
121
  ## See and use the screen
100
122
 
101
123
  ```sh
102
- npx @avasapp/agent-bridge call screen.snapshot # buttons, inputs, text on the focused screen
124
+ npx @avasapp/agent-bridge call screen.snapshot # one line per button, input, text on the focused screen (--json for rects)
103
125
  npx @avasapp/agent-bridge call screen.press '"add-plant"' # by testID, label or text
104
126
  npx @avasapp/agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
105
127
  npx @avasapp/agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
106
128
  npx @avasapp/agent-bridge call router.navigate /inbox
107
129
  ```
108
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
+
109
135
  `press` and `fill` return once the app has rendered the result, so the next
110
- 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
111
141
  spread into the tool's arguments.
112
142
 
113
143
  ## Watch for errors
@@ -137,6 +167,7 @@ from the real state.
137
167
  | Read or set query data, store values, routes, modals; get argument shapes right | [references/state.md](references/state.md) |
138
168
  | Mock or inspect requests, or deal with strict network mode | [references/network.md](references/network.md) |
139
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) |
140
171
  | Handle a result over 32 KB or an argument over ~128 KB | [references/large-data.md](references/large-data.md) |
141
172
  | Run many calls at once, write a flow file, or measure timing | [references/flows.md](references/flows.md) |
142
173
  | Understand what restore undoes, or fix a value restore got wrong | [references/restore.md](references/restore.md) |
@@ -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.
@@ -35,11 +35,10 @@ result, and for timing checks. Name it `flows/<name>.flow.mjs` in the app: plain
35
35
  ```js
36
36
  export const scenario = 'signedIn' // applied first, undone after, even on failure
37
37
 
38
- export default async ({ step }) => {
38
+ export default async ({ step, expect }) => {
39
39
  await step('open form', 'screen.press', 'add-plant')
40
40
  await step('save empty', 'screen.press', 'save-plant')
41
- await step('error shown', 'screen.waitFor', 'Name is required')
42
- await step('undo', 'bridge.restore')
41
+ await expect.text('Name is required') // also expect.gone, expect.route, expect.result
43
42
  }
44
43
  ```
45
44
 
@@ -56,6 +55,17 @@ when the flow fails. The flow gets what each scenario's `apply` returned as
56
55
  `scenarios.signedIn`. A failed restorer makes the run exit non-zero.
57
56
  `--scenario` replaces a declared one of the same name.
58
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
+
59
69
  `step` takes the same argument shapes as `call`; see [state.md](state.md).
60
70
 
61
71
  ## Timing
@@ -3,7 +3,8 @@
3
3
  ```sh
4
4
  npx @avasapp/agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
5
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
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
7
8
  npx @avasapp/agent-bridge call net.mockFromLog '[3, {"user": {"name": "Moss"}}]' # a real response, patched
8
9
  npx @avasapp/agent-bridge call net.mocks '[{"full": true}]' # active mocks; bodies over ~2 KB are cut unless full
9
10
  ```
@@ -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
 
@@ -5,7 +5,7 @@ them instead of waiting for a backend to send one:
5
5
 
6
6
  ```sh
7
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 and faked
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
9
  npx @avasapp/agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
10
10
  npx @avasapp/agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
11
11
  npx @avasapp/agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
@@ -7,14 +7,21 @@ npx @avasapp/agent-bridge session stop # runs bridge.restore
7
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
@@ -1,13 +1,19 @@
1
1
  # See and use the screen
2
2
 
3
3
  ```sh
4
- npx @avasapp/agent-bridge call screen.snapshot # buttons, inputs, text on the focused screen (`hidden` counts what was skipped; {"all":true} lists everything)
4
+ npx @avasapp/agent-bridge call screen.snapshot # one line per element: `button #save "Save" disabled` (`hidden` counts what was skipped; {"all":true} lists everything)
5
+ npx @avasapp/agent-bridge call screen.snapshot '{"kinds":["input","button"],"within":"checkout-form"}' # only those kinds, only inside that target
6
+ npx @avasapp/agent-bridge call screen.snapshot --json # JSON with rects and `role` (the text form already shows actions=, readonly, offscreen); `@x,y` on a line is `{"at":[x,y]}`, `index=N` is `{"index":N}`
5
7
  npx @avasapp/agent-bridge call screen.findText "Payment" # text or accessibility label; {"labels":false} for text only
6
8
  npx @avasapp/agent-bridge call screen.press '"add-plant"' # by testID, label or text
7
9
  npx @avasapp/agent-bridge call screen.press '[{"at":[350,60]}]' # icon-only button: by point; {"index":1} picks among matches; {"force":true} presses disabled
8
10
  npx @avasapp/agent-bridge call screen.press '["save-plant", {"scroll":true}]' # scrolls an off-screen target into view first
9
11
  npx @avasapp/agent-bridge call screen.press '"Swipe to confirm"' # no onPress: sends the "activate" accessibility action
10
12
  npx @avasapp/agent-bridge call screen.press '["Volume", {"action":"increment"}]' # any action in the element's snapshot `actions`
13
+ npx @avasapp/agent-bridge call screen.press '["Item 80", {"scroll":true}]' # a row a FlatList has not rendered yet: pages the main list until it shows up (waitFor does not page)
14
+ npx @avasapp/agent-bridge call screen.press '["Message", {"long":true}]' # onPressIn, onLongPress, onPressOut; errors when there is no onLongPress
15
+ npx @avasapp/agent-bridge call screen.press '"dark-mode"' # a Switch is toggled with onValueChange(!value)
16
+ npx @avasapp/agent-bridge call screen.setValue '["volume", 7]' # Switch true/false, Slider number, Picker or SegmentedControl item; reads the value back
11
17
  npx @avasapp/agent-bridge call screen.scroll '"Notes"' # bring a target to the middle of its ScrollView/FlatList; {"toEnd":true}, {"by":400}, [{"toEnd":true},{"within":"list"}]
12
18
  npx @avasapp/agent-bridge call screen.refresh # pull to refresh (the RefreshControl's onRefresh); then screen.waitFor the data
13
19
  npx @avasapp/agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
@@ -16,7 +22,7 @@ npx @avasapp/agent-bridge call screen.waitFor '["Section title", {"scroll":true}
16
22
  ```
17
23
 
18
24
  `press` and `fill` return once the app has rendered the result, so the next
19
- check sees it. `fill` skips the keyboard: autocorrect and native-only input
25
+ check sees it. `fill` also takes `{"submit":true}` and `{"type":true}` (per-character, for debounced search); `""` clears. A press that hit an ancestor's handler says `via: "ancestor"`: check it did what you meant. `fill` skips the keyboard: autocorrect and native-only input
20
26
  behaviour need one real typing step from agent-device.
21
27
 
22
28
  A swipe-to-confirm or slider with no `onPress` shows as a button (or a view)
@@ -6,7 +6,7 @@
6
6
  npx @avasapp/agent-bridge call query.pin '[["features"], <data>]' # stays through refetches
7
7
  npx @avasapp/agent-bridge call store.call '["settings", "setTheme", "dark"]'
8
8
  npx @avasapp/agent-bridge call router.navigate /inbox
9
- npx @avasapp/agent-bridge call router.current
9
+ npx @avasapp/agent-bridge call router.current # React Navigation apps: nav.navigate Inbox, nav.current
10
10
  npx @avasapp/agent-bridge call router.dismiss # closes a route-based modal
11
11
  ```
12
12
 
@@ -31,6 +31,26 @@ npx @avasapp/agent-bridge call store.get '["app", {"keys": true}]' # top-level
31
31
  `null` reply means the data is undefined. For a large query, narrow it with
32
32
  `pages` or `path`; see [large-data.md](large-data.md).
33
33
 
34
+ ## Redux and Jotai
35
+
36
+ Same ideas under their own prefixes, so a Zustand `store.*` and these can coexist.
37
+
38
+ ```sh
39
+ npx @avasapp/agent-bridge call redux.list
40
+ npx @avasapp/agent-bridge call redux.get '["auth", "user.name"]' # or [] for the whole state
41
+ npx @avasapp/agent-bridge call redux.dispatch '[{"type":"cart/add","payload":"apple"}]'
42
+ npx @avasapp/agent-bridge call redux.set '["auth", {"token": "abc"}]' # merge into a slice
43
+ npx @avasapp/agent-bridge call jotai.list # read-write or read-only
44
+ npx @avasapp/agent-bridge call jotai.get '["user", "name"]'
45
+ npx @avasapp/agent-bridge call jotai.set '["count", 5]' # writable atoms only
46
+ ```
47
+
48
+ `redux.set` and restore need the app to wrap its root reducer in
49
+ `bridgeReducer()` (inside `persistReducer` with redux-persist); without it they
50
+ fail and say so. `redux.set` merges into object slices only. `redux.dispatch`
51
+ sends plain actions only. `jotai.set` refuses atoms whose value isn't the one
52
+ sent (action atoms, `atomWithReducer`). Redacted slices, atoms and paths print as `"[redacted]"`.
53
+
34
54
  ## Argument shapes
35
55
 
36
56
  Arguments are a JSON array (or a single JSON value): the array is spread into