@avasapp/agent-bridge 0.1.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/adapters/expo-router.cjs +144 -0
  4. package/dist/adapters/expo-router.d.cts +38 -0
  5. package/dist/adapters/react-native-mmkv.cjs +102 -0
  6. package/dist/adapters/react-native-mmkv.d.cts +15 -0
  7. package/dist/adapters/tanstack-query.cjs +161 -0
  8. package/dist/adapters/tanstack-query.d.cts +10 -0
  9. package/dist/adapters/zustand.cjs +97 -0
  10. package/dist/adapters/zustand.d.cts +15 -0
  11. package/dist/chunk-UEWFQWCY.js +575 -0
  12. package/dist/cli.js +657 -0
  13. package/dist/client/index.cjs +568 -0
  14. package/dist/client/index.d.ts +112 -0
  15. package/dist/client/index.js +14 -0
  16. package/dist/expo/index.cjs +62 -0
  17. package/dist/expo/index.d.cts +9 -0
  18. package/dist/network/index.cjs +568 -0
  19. package/dist/network/index.d.cts +85 -0
  20. package/dist/noop/expo-router.cjs +26 -0
  21. package/dist/noop/expo.cjs +27 -0
  22. package/dist/noop/index.cjs +36 -0
  23. package/dist/noop/network.cjs +34 -0
  24. package/dist/noop/react-native-mmkv.cjs +26 -0
  25. package/dist/noop/tanstack-query.cjs +26 -0
  26. package/dist/noop/zustand.cjs +26 -0
  27. package/dist/runtime/index.cjs +933 -0
  28. package/dist/runtime/index.d.cts +69 -0
  29. package/dist/types-C6DUUHnB.d.cts +76 -0
  30. package/entries/expo-router.cjs +9 -0
  31. package/entries/expo.cjs +9 -0
  32. package/entries/index.cjs +9 -0
  33. package/entries/network.cjs +9 -0
  34. package/entries/react-native-mmkv.cjs +9 -0
  35. package/entries/tanstack-query.cjs +9 -0
  36. package/entries/zustand.cjs +9 -0
  37. package/package.json +120 -0
  38. package/skills/agent-bridge/SKILL.md +89 -0
@@ -0,0 +1,69 @@
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';
3
+
4
+ /**
5
+ * Works in any React Native app served by Metro. The client evaluates one fixed
6
+ * entry point with an ASCII-only JSON payload, and the app pushes the result
7
+ * back through a CDP binding (Hermes can't `awaitPromise` RN's promises).
8
+ */
9
+ declare function cdpTransport(): Transport;
10
+
11
+ type SettleResult = {
12
+ commits: number;
13
+ ms: number;
14
+ };
15
+ /**
16
+ * Resolves once React has committed the renders queued so far: after a tick
17
+ * (TanStack Query notifies on a timeout), then until a frame passes with no
18
+ * commit and nothing queued. Returns right away when nothing was queued, and
19
+ * gives up after `maxMs` (default 500).
20
+ */
21
+ declare function settle(options?: {
22
+ maxMs?: number;
23
+ }): Promise<SettleResult>;
24
+
25
+ type Rect = {
26
+ x: number;
27
+ y: number;
28
+ width: number;
29
+ height: number;
30
+ };
31
+
32
+ type ElementKind = 'button' | 'input' | 'text' | 'view';
33
+ type ScreenElement = {
34
+ kind: ElementKind;
35
+ text?: string;
36
+ testID?: string;
37
+ label?: string;
38
+ placeholder?: string;
39
+ value?: string;
40
+ editable?: boolean;
41
+ role?: string;
42
+ disabled?: boolean;
43
+ rect: Rect | null;
44
+ /** Only in `screen.snapshot({ all: true })`. */
45
+ onScreen?: boolean;
46
+ };
47
+
48
+ /**
49
+ * What to act on. A string tries testID, then label, then placeholder, then
50
+ * visible text (exact, then substring). An object must match every field it
51
+ * sets; `index` picks one of several matches.
52
+ */
53
+ type Target = string | {
54
+ testID?: string;
55
+ label?: string;
56
+ placeholder?: string;
57
+ text?: string;
58
+ index?: number;
59
+ };
60
+
61
+ /** Starts the bridge outside React. Returns a function that stops it. */
62
+ declare function startAgentBridge(options?: AgentBridgeOptions): () => void;
63
+ /**
64
+ * Exposes `tools` to coding agents while mounted. Tools are read on every call,
65
+ * so passing a fresh object each render is fine. Transports start once.
66
+ */
67
+ declare function useAgentBridge(options?: AgentBridgeOptions): void;
68
+
69
+ export { AgentBridgeOptions, type ScreenElement, type SettleResult, type Target, Transport, cdpTransport, settle, startAgentBridge, useAgentBridge };
@@ -0,0 +1,76 @@
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 it. */
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
+ };
39
+ type ToolInfo = {
40
+ name: string;
41
+ description?: string;
42
+ };
43
+ type DeviceInfo = {
44
+ deviceId: string;
45
+ name: string;
46
+ platform: string;
47
+ protocol: number;
48
+ tools: ToolInfo[];
49
+ };
50
+
51
+ type ToolFn = (...args: any[]) => unknown;
52
+ type ToolDefinition = ToolFn | {
53
+ description?: string;
54
+ run: ToolFn;
55
+ };
56
+ /** Tools by name. Namespace them with a dot, e.g. `query.pin`. */
57
+ type Tools = Record<string, ToolDefinition>;
58
+ type TransportContext = {
59
+ info: () => DeviceInfo;
60
+ dispatch: (call: CallMessage) => Promise<ResultMessage>;
61
+ };
62
+ type Transport = {
63
+ name: string;
64
+ /** Starts listening; returns a function that stops. */
65
+ start: (context: TransportContext) => () => void;
66
+ };
67
+ type AgentBridgeOptions = {
68
+ /** Your tools, or a function returning them (read on every call). */
69
+ tools?: Tools | (() => Tools);
70
+ /** How this app shows up in `agent-bridge devices`. */
71
+ name?: string;
72
+ /** Defaults to CDP only. Add `expoTransport()` on Expo. */
73
+ transports?: Transport[];
74
+ };
75
+
76
+ export { type AgentBridgeOptions as A, type CallMessage as C, type DeviceInfo as D, type LogEntry as L, RUNTIME_MARKER as R, type Tools as T, type Transport as a, type ResultMessage as b, type ToolDefinition as c, type ToolFn as d, type ToolInfo as e };
@@ -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/expo-router.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/expo-router.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/expo.cjs')
7
+ } else {
8
+ module.exports = require('../dist/expo/index.cjs')
9
+ }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ // Metro inlines NODE_ENV and drops the dead branch, so a release bundle
4
+ // never includes the bridge, the same way react/index.js works.
5
+ if (process.env.NODE_ENV === 'production') {
6
+ module.exports = require('../dist/noop/index.cjs')
7
+ } else {
8
+ module.exports = require('../dist/runtime/index.cjs')
9
+ }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ // Metro inlines NODE_ENV and drops the dead branch, so a release bundle
4
+ // never includes the bridge, the same way react/index.js works.
5
+ if (process.env.NODE_ENV === 'production') {
6
+ module.exports = require('../dist/noop/network.cjs')
7
+ } else {
8
+ module.exports = require('../dist/network/index.cjs')
9
+ }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ // Metro inlines NODE_ENV and drops the dead branch, so a release bundle
4
+ // never includes the bridge, the same way react/index.js works.
5
+ if (process.env.NODE_ENV === 'production') {
6
+ module.exports = require('../dist/noop/react-native-mmkv.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/react-native-mmkv.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/tanstack-query.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/tanstack-query.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/zustand.cjs')
7
+ } else {
8
+ module.exports = require('../dist/adapters/zustand.cjs')
9
+ }
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@avasapp/agent-bridge",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/avas-app/agent-bridge#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/avas-app/agent-bridge.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/avas-app/agent-bridge/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "provenance": true
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "sideEffects": false,
23
+ "main": "./entries/index.cjs",
24
+ "types": "./dist/runtime/index.d.cts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/runtime/index.d.cts",
28
+ "default": "./entries/index.cjs"
29
+ },
30
+ "./expo": {
31
+ "types": "./dist/expo/index.d.cts",
32
+ "default": "./entries/expo.cjs"
33
+ },
34
+ "./tanstack-query": {
35
+ "types": "./dist/adapters/tanstack-query.d.cts",
36
+ "default": "./entries/tanstack-query.cjs"
37
+ },
38
+ "./zustand": {
39
+ "types": "./dist/adapters/zustand.d.cts",
40
+ "default": "./entries/zustand.cjs"
41
+ },
42
+ "./react-native-mmkv": {
43
+ "types": "./dist/adapters/react-native-mmkv.d.cts",
44
+ "default": "./entries/react-native-mmkv.cjs"
45
+ },
46
+ "./expo-router": {
47
+ "types": "./dist/adapters/expo-router.d.cts",
48
+ "default": "./entries/expo-router.cjs"
49
+ },
50
+ "./network": {
51
+ "types": "./dist/network/index.d.cts",
52
+ "default": "./entries/network.cjs"
53
+ },
54
+ "./client": {
55
+ "types": "./dist/client/index.d.ts",
56
+ "import": "./dist/client/index.js",
57
+ "require": "./dist/client/index.cjs"
58
+ },
59
+ "./package.json": "./package.json"
60
+ },
61
+ "bin": {
62
+ "agent-bridge": "./dist/cli.js"
63
+ },
64
+ "files": [
65
+ "dist",
66
+ "entries",
67
+ "skills"
68
+ ],
69
+ "keywords": [
70
+ "react-native",
71
+ "expo",
72
+ "ai",
73
+ "agents",
74
+ "testing",
75
+ "e2e",
76
+ "devtools",
77
+ "cdp",
78
+ "tanstack-query",
79
+ "zustand",
80
+ "mmkv"
81
+ ],
82
+ "scripts": {
83
+ "build": "rm -rf dist && tsup",
84
+ "test": "bun test src/",
85
+ "typecheck": "tsc -p tsconfig.json --noEmit",
86
+ "check": "bun run typecheck && bun run test && bun run build && bun run check:gates",
87
+ "check:gates": "node scripts/check-gates.mjs",
88
+ "changeset": "changeset",
89
+ "release": "bun run check && changeset publish",
90
+ "prepublishOnly": "bun run check"
91
+ },
92
+ "peerDependencies": {
93
+ "@tanstack/query-core": ">=5",
94
+ "expo": ">=52",
95
+ "react": ">=18",
96
+ "react-native": ">=0.76"
97
+ },
98
+ "peerDependenciesMeta": {
99
+ "@tanstack/query-core": {
100
+ "optional": true
101
+ },
102
+ "expo": {
103
+ "optional": true
104
+ }
105
+ },
106
+ "dependencies": {
107
+ "ws": "^8.21.3"
108
+ },
109
+ "devDependencies": {
110
+ "@changesets/cli": "^3.0.3",
111
+ "@expo/devtools": "^57.0.1",
112
+ "@tanstack/query-core": "^5.90.0",
113
+ "@types/bun": "^1.4.2",
114
+ "@types/react": "^19.3.0",
115
+ "@types/ws": "^8.18.1",
116
+ "tsup": "^8.5.1",
117
+ "typescript": "~6.0.3",
118
+ "zustand": "^5.0.15"
119
+ }
120
+ }
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: agent-bridge
3
+ description: "Drive a running React Native or Expo dev build directly through @avasapp/agent-bridge: see what's on screen, fill in forms, press buttons, seed query data, flip feature flags, set store state, navigate, mock the network and undo it all, in milliseconds per call instead of tapping through the app. Use whenever you need the app in a particular state to test a change, or need to verify what a screen shows."
4
+ ---
5
+
6
+ # Driving an app with agent-bridge
7
+
8
+ The app mounts `useAgentBridge({ tools })` in a dev build. You call those tools
9
+ by name. Nothing is tapped, so there is no setup to click through.
10
+
11
+ ## Start a session
12
+
13
+ ```sh
14
+ npx agent-bridge session start # add --metro host:port if Metro isn't on localhost:8081
15
+ npx agent-bridge tools # what this app exposes
16
+ ```
17
+
18
+ A session holds one connection for all your calls. It stops itself after 15
19
+ minutes without calls and runs `bridge.restore` when it does.
20
+
21
+ If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
22
+ this build. Say so; don't fall back to tapping silently.
23
+
24
+ ## See and use the screen
25
+
26
+ ```sh
27
+ npx agent-bridge call screen.snapshot # buttons, inputs, text on screen
28
+ npx agent-bridge call screen.press '"add-plant"' # by testID, label or text
29
+ npx agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
30
+ npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
31
+ ```
32
+
33
+ `press` and `fill` return once the app has rendered the result, so the next
34
+ check sees it. `fill` skips the keyboard: autocorrect and native-only input
35
+ behaviour need one real typing step from your device tool.
36
+
37
+ ## Get the app into a state
38
+
39
+ ```sh
40
+ npx agent-bridge call query.pin '[["features"], <data>]' # stays through refetches
41
+ npx agent-bridge call store.call '["settings", "setTheme", "dark"]'
42
+ npx agent-bridge call router.navigate /inbox
43
+ npx agent-bridge call router.current
44
+ npx agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
45
+ npx agent-bridge call net.log
46
+ ```
47
+
48
+ Arguments are a JSON array (or a single JSON value). Read the current value
49
+ first (`query.get`, `store.get`) and change only what you need.
50
+
51
+ ## Watch for errors
52
+
53
+ Every reply carries errors the app logged or threw since the previous reply,
54
+ printed as `! error during <tool>` or `! error after <tool>`. Treat them as
55
+ failures of the step they name. `npx agent-bridge call bridge.logs` shows the
56
+ last 200 errors and warnings.
57
+
58
+ ## Repeat without the model
59
+
60
+ Write the steps into a flow file and run it:
61
+
62
+ ```js
63
+ export default async ({ step }) => {
64
+ await step('open form', 'screen.press', 'add-plant')
65
+ await step('save empty', 'screen.press', 'save-plant')
66
+ await step('error shown', 'screen.waitFor', 'Name is required')
67
+ await step('undo', 'bridge.restore')
68
+ }
69
+ ```
70
+
71
+ ```sh
72
+ npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an error
73
+ ```
74
+
75
+ ## Clean up
76
+
77
+ ```sh
78
+ npx agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
79
+ ```
80
+
81
+ `bridge.restore` unpins queries, puts back store and MMKV values and removes
82
+ network mocks, so the next run starts from the real state.
83
+
84
+ ## Traps
85
+
86
+ - Run next to the simulator. From another machine every call pays the network.
87
+ - It can't see native UI (system alerts, permission prompts, native sheets) or
88
+ overlap. Finish a flow with one real UI check or screenshot from your device tool.
89
+ - With several apps on one Metro, pass `--device`.