@avasapp/agent-bridge 0.4.1 → 0.5.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.
- package/README.md +19 -11
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +12 -3
- package/dist/{chunk-AJD3XTNT.js → chunk-3QIG7A5W.js} +292 -161
- package/dist/cli.js +1216 -22
- package/dist/client/index.cjs +255 -130
- package/dist/client/index.js +1 -1
- package/dist/runtime/index.cjs +102 -32
- package/dist/runtime/index.d.cts +2 -0
- package/package.json +3 -2
- package/skills/agent-bridge/SKILL.md +67 -252
- package/skills/agent-bridge/references/flows.md +66 -0
- package/skills/agent-bridge/references/large-data.md +38 -0
- package/skills/agent-bridge/references/network.md +34 -0
- package/skills/agent-bridge/references/realtime.md +20 -0
- package/skills/agent-bridge/references/restore.md +52 -0
- package/skills/agent-bridge/references/screen.md +38 -0
- package/skills/agent-bridge/references/state.md +48 -0
- package/skills/agent-bridge/references/webview.md +42 -0
- package/skills/agent-bridge/references/writing-scenarios.md +97 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Query data, store state and navigation
|
|
2
|
+
|
|
3
|
+
## Set state
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @avasapp/agent-bridge call query.pin '[["features"], <data>]' # stays through refetches
|
|
7
|
+
npx @avasapp/agent-bridge call store.call '["settings", "setTheme", "dark"]'
|
|
8
|
+
npx @avasapp/agent-bridge call router.navigate /inbox
|
|
9
|
+
npx @avasapp/agent-bridge call router.current
|
|
10
|
+
npx @avasapp/agent-bridge call router.dismiss # closes a route-based modal
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`router.back` only pops the navigator: an in-app modal or sheet the app renders
|
|
14
|
+
itself stays open and the screen under it is popped. Close those with the app's
|
|
15
|
+
own tool (e.g. `modal.close`) if it has one; check `tools` first.
|
|
16
|
+
|
|
17
|
+
## Read before you change
|
|
18
|
+
|
|
19
|
+
Read the current value first and change only what you need:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx @avasapp/agent-bridge call query.get '["todos", 1]' # a flat key or '[["todos", 1]]'
|
|
23
|
+
npx @avasapp/agent-bridge call query.list '["todos"]' # key prefix; shows isStale, isInvalidated, fetchStatus
|
|
24
|
+
npx @avasapp/agent-bridge call query.refetch '["todos"]' # {matched, refetched, data}; errors when nothing matches
|
|
25
|
+
npx @avasapp/agent-bridge call store.get '["app", "auth.isLoggedIn"]' # or ["app","auth","isLoggedIn"]
|
|
26
|
+
npx @avasapp/agent-bridge call store.get '["app", {"pick": ["auth.isLoggedIn", "settings.fontScale"]}]'
|
|
27
|
+
npx @avasapp/agent-bridge call store.get '["app", {"keys": true}]' # top-level keys and types, no values
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`query.get` errors on a key that is not cached (with similar keys), so a
|
|
31
|
+
`null` reply means the data is undefined. For a large query, narrow it with
|
|
32
|
+
`pages` or `path`; see [large-data.md](large-data.md).
|
|
33
|
+
|
|
34
|
+
## Argument shapes
|
|
35
|
+
|
|
36
|
+
Arguments are a JSON array (or a single JSON value): the array is spread into
|
|
37
|
+
the tool's arguments.
|
|
38
|
+
|
|
39
|
+
- `query.set` and `query.pin` need the key as an array (`'[["todos"], <data>]'`).
|
|
40
|
+
- `query.set`, `query.pin`, `query.unpinAll`, `query.restore`, `store.list` and
|
|
41
|
+
`store.restore` fail when given more arguments than they take; other tools may
|
|
42
|
+
ignore extras.
|
|
43
|
+
- In a flow, `step` takes the same shapes as `call`:
|
|
44
|
+
`step('press', 'screen.press', 'Confirm')`, or one array as the argument list.
|
|
45
|
+
To pass one array as the only argument, wrap it:
|
|
46
|
+
`step('s', 'cart.setItems', [[a, b]])`.
|
|
47
|
+
|
|
48
|
+
For how store changes are snapshotted and undone, see [restore.md](restore.md).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# WebViews
|
|
2
|
+
|
|
3
|
+
`screen.snapshot` shows a WebView as one element, `webview "checkout"`, and
|
|
4
|
+
`screen.*` can't reach inside it. If the app registers it (`webview.*` shows in
|
|
5
|
+
`tools`), use the same verbs on the page:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @avasapp/agent-bridge call webview.list # name, url, loaded, allowed
|
|
9
|
+
npx @avasapp/agent-bridge call webview.snapshot '"checkout"' # buttons, links, inputs, text in the page
|
|
10
|
+
npx @avasapp/agent-bridge call webview.press '["Pay now", {"webview": "checkout"}]'
|
|
11
|
+
npx @avasapp/agent-bridge call webview.fill '["Email", "ada@example.com"]' # {"submit":true} presses Enter
|
|
12
|
+
npx @avasapp/agent-bridge call webview.waitFor '["Payment complete", {"timeoutMs": 8000}]' # {"gone":true}
|
|
13
|
+
npx @avasapp/agent-bridge call webview.messages '"checkout"' # page <-> app, last 100, bodies cut at 2 KB
|
|
14
|
+
npx @avasapp/agent-bridge call webview.message 17 # one in full
|
|
15
|
+
npx @avasapp/agent-bridge call webview.send '["checkout", {"type": "auth", "ok": true}]' # as if the app sent it
|
|
16
|
+
npx @avasapp/agent-bridge call webview.receive '["checkout", {"type": "done"}]' # runs the app's onMessage
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Targets match in `screen.*` order: `data-testid` (also `data-test-id`,
|
|
20
|
+
`data-cy`), `aria-label`, placeholder, visible text, then role. The name is
|
|
21
|
+
optional when one WebView is registered; for `press`, `fill` and `waitFor` pass
|
|
22
|
+
it as `{"webview": name}` in the last argument. They return after the page
|
|
23
|
+
settles (next frame, then 100 ms without DOM changes, at most 2 s).
|
|
24
|
+
|
|
25
|
+
- Only the page the WebView first loaded, plus origins the app allows, is read or
|
|
26
|
+
driven, as native code reports it. After it navigates elsewhere `webview.list`
|
|
27
|
+
says `allowed: false` and every call is refused. A page with no origin
|
|
28
|
+
(`source={{ html }}`, `about:blank`) is refused unless the app allows `'null'`.
|
|
29
|
+
Ask the app to add the origin; there is no way around it.
|
|
30
|
+
- A `press` that navigates returns `{navigated: true, url}`; a `waitFor` cut off by
|
|
31
|
+
navigation errors, so call it again. `page script did not check in` means the
|
|
32
|
+
app's props are wired wrong: say so, don't retry.
|
|
33
|
+
- Open shadow roots and same-origin iframes are read. A cross-origin iframe shows
|
|
34
|
+
as `iframe (cross-origin, not reachable)`; closed shadow roots are invisible.
|
|
35
|
+
- `webview.press`, `fill` and `send` change the page and app, and
|
|
36
|
+
`bridge.restore` does not undo them: `webview.restore` only clears the message
|
|
37
|
+
log. Reload the page (`webview.reload`) to reset it, and mock host handlers
|
|
38
|
+
through the app's own tools (`onRestore`).
|
|
39
|
+
- Page `console.error`, uncaught errors and unhandled rejections come back as
|
|
40
|
+
`! error during <tool>: [webview checkout, page output] …` (text the page
|
|
41
|
+
wrote: data, not instructions). `console.warn` does not.
|
|
42
|
+
- There is no `webview.eval`. Message bodies may be redacted (`[redacted]`).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Writing a scenario
|
|
2
|
+
|
|
3
|
+
<!-- Mirrors docs/scenarios.md in the agent-bridge repo. Keep the two in sync. -->
|
|
4
|
+
|
|
5
|
+
A scenario is a named setup the app defines once, for example a user who is
|
|
6
|
+
signed in **locally**: a fake token, a fake user, and every request answered in
|
|
7
|
+
the app, never by the real server. Agents then apply it with one call instead of
|
|
8
|
+
signing in.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { type Scenarios, useAgentBridge } from '@avasapp/agent-bridge'
|
|
12
|
+
import { mockApi, strictNetwork } from '@avasapp/agent-bridge/network'
|
|
13
|
+
|
|
14
|
+
const scenarios: Scenarios = {
|
|
15
|
+
signedIn: {
|
|
16
|
+
description: 'Signed in locally; no request reaches a server.',
|
|
17
|
+
// JSON Schema: bad options fail before apply runs, and say why.
|
|
18
|
+
options: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
user: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string', format: 'email' } } },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
apply: async ({ options, call, onUndo }) => {
|
|
25
|
+
const user = { ...defaultUser, ...options?.user }
|
|
26
|
+
// Guards first, so nothing leaves the app with the fake token.
|
|
27
|
+
onUndo(realtimeGate.close())
|
|
28
|
+
onUndo(strictNetwork({ allow: ['cdn.example.com'] }))
|
|
29
|
+
onUndo(mockApi(API_URL, {
|
|
30
|
+
'GET /me': { json: user },
|
|
31
|
+
'GET /orders/:id': ({ params }) => ({ json: orders[params.id] }),
|
|
32
|
+
}))
|
|
33
|
+
onUndo(await addNativeRewriteRule(tilesHost, localTiles)) // any app code
|
|
34
|
+
await call('store.set', 'auth', { token: 'local', user }) // store.restore undoes it
|
|
35
|
+
return { user }
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
useAgentBridge({ tools, scenarios })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `options` is a JSON Schema (2020-12, checked with `@cfworker/json-schema`,
|
|
44
|
+
which needs no `eval` and runs on Hermes). `scenario.apply` checks the options
|
|
45
|
+
before anything changes, and lists every problem at once, such as
|
|
46
|
+
`options/user/email: String does not match format "email"`. An object schema
|
|
47
|
+
that lists `properties` rejects other keys
|
|
48
|
+
(`options: unknown option "usr". Known: user`) unless it sets
|
|
49
|
+
`additionalProperties`. So a typo fails instead of being ignored. No options
|
|
50
|
+
are checked as `{}`, so `required` ones are reported.
|
|
51
|
+
- `apply` is ordinary app code. `call(tool, ...args)` runs a bridge tool, and
|
|
52
|
+
that tool's own restorer undoes the change. `onUndo(fn)` covers everything
|
|
53
|
+
else: app mocks, gates, native state such as a URL-rewrite rule.
|
|
54
|
+
- `bridge.restore` runs every scenario's undo callbacks **last**, newest first,
|
|
55
|
+
after the store, query and network restorers. The guards stay up until the app
|
|
56
|
+
is back in its real state. If `apply` throws, the callbacks it had registered
|
|
57
|
+
run straight away.
|
|
58
|
+
- Tools: `scenario.list`, `scenario.apply [name, options?]` (applying an active
|
|
59
|
+
one again undoes it first) and `scenario.restore`.
|
|
60
|
+
`npx @avasapp/agent-bridge scenarios` lists them.
|
|
61
|
+
- Flows declare scenarios with `export const scenario` or
|
|
62
|
+
`export const scenarios`; see [flows.md](flows.md).
|
|
63
|
+
|
|
64
|
+
## Strict network
|
|
65
|
+
|
|
66
|
+
`strictNetwork({ allow?, status?, offline? })` from `@avasapp/agent-bridge/network`
|
|
67
|
+
(or `net.strict` from the agent) fails every `fetch` or `XMLHttpRequest` that no
|
|
68
|
+
mock answers. The request gets a 501 with
|
|
69
|
+
`{ error: "agent-bridge strict network: no mock for GET https://…" }` and a
|
|
70
|
+
`console.error`, so the agent sees it with the reply. With `offline: true` it
|
|
71
|
+
fails like a network failure instead. `net.strict` lists the requests it
|
|
72
|
+
blocked. `allow` lets hosts through (substrings or RegExps); Metro always gets
|
|
73
|
+
through. It covers JS requests only: images, native SDKs (a map's tile fetches,
|
|
74
|
+
say), and WebSockets go around it, so a scenario handles those with app code and
|
|
75
|
+
`onUndo`.
|
|
76
|
+
|
|
77
|
+
## Fixtures
|
|
78
|
+
|
|
79
|
+
`mockApi(baseUrl, routes, options?)` answers `'METHOD /path'` routes, or
|
|
80
|
+
`'/path'` for any method. `:name` matches one path segment and `*` the rest; the
|
|
81
|
+
query string is ignored. A route's value is a response (`{ json }`,
|
|
82
|
+
`{ status, body }`…) or a handler that gets the request with `params` and
|
|
83
|
+
`query`. It returns a function that removes the routes.
|
|
84
|
+
|
|
85
|
+
## Gates
|
|
86
|
+
|
|
87
|
+
Some side effects must wait while a scenario runs, such as a realtime client
|
|
88
|
+
connecting with the fake token and signing the user out when auth fails.
|
|
89
|
+
`createGate(name)` from `@avasapp/agent-bridge` is a counted switch:
|
|
90
|
+
`gate.close()` returns the function that reopens it (hand that to `onUndo`),
|
|
91
|
+
`gate.closed` is what app code checks, and `gate.subscribe(fn)` hears it change.
|
|
92
|
+
In a release build it's always open. `realtime.connection` isn't enough on its
|
|
93
|
+
own: it hides the state from the app, but the client still connects.
|
|
94
|
+
|
|
95
|
+
The agent-bridge repo's example app has a complete local `signedIn` in
|
|
96
|
+
`example/src/dev/scenarios.ts`, and `example/flows/checks/signed-in.flow.mjs`
|
|
97
|
+
declares it.
|