@avasapp/agent-bridge 0.6.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Let coding agents drive any running Expo or React Native app directly. Seed data
4
4
 
5
5
  ![The same checks with agent-device alone (22.4 s) and with agent-bridge (1.4 s): hide a tab, turn on dark mode, fill in a form, seed data, fake a live message and undo it all](example/media/demo.gif)
6
6
 
7
- The same 19 checks on [the example app](example), at real speed: 22.4 s with agent-device alone, 1.4 s with agent-bridge. Most calls take a few milliseconds ([benchmarks](docs/benchmarks.md)).
7
+ The same 19 checks on [the example app](example), at real speed: 22.4 s with agent-device alone, 1.4 s with agent-bridge. With [Argent](https://github.com/software-mansion/argent) instead of agent-device it is [17.1 s against 1.4 s](example/media/demo-argent.gif) while screen recording (Argent alone takes 11.6 s without the recorder). Most calls take a few milliseconds ([benchmarks](docs/benchmarks.md)).
8
8
 
9
9
  Works on Expo (dev-tools socket) and on bare React Native (CDP over Metro). Nothing ships to users: every entry point compiles to an empty stub in release builds, and `agent-bridge assert-absent` checks a bundle in CI ([security](docs/security.md)).
10
10
 
package/dist/cli.js CHANGED
@@ -1685,7 +1685,7 @@ import { dirname as dirname6, relative as relative6, resolve as resolve2, sep as
1685
1685
  import { createInterface as createInterface4 } from "readline/promises";
1686
1686
 
1687
1687
  // package.json
1688
- var version = "0.6.0";
1688
+ var version = "0.6.1";
1689
1689
 
1690
1690
  // src/init/detect.ts
1691
1691
  import { dirname as dirname4, join as join6, relative as relative3 } from "path";
@@ -2903,9 +2903,39 @@ async function runInit(args, io) {
2903
2903
 
2904
2904
  // src/mcp/server.ts
2905
2905
  import { createInterface as createInterface5 } from "readline";
2906
+
2907
+ // src/runtime/tools/descriptions.ts
2908
+ var INTERNAL_TOOLS = ["bridge.adopt"];
2909
+ var UNLISTED_WITHOUT_APP = [...INTERNAL_TOOLS, "bridge.ping"];
2910
+ var TARGET_HELP = "target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument.";
2911
+ var TOOL_DESCRIPTIONS = {
2912
+ "app.reload": `Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins, storage) are lost, and the reply names them: restore first. Returns before it reloads; a session waits for the new bridge and reports what was lost.`,
2913
+ "app.restore": `Undo what the app's own tools changed: runs the undos they registered with onRestore, newest first, once. Returns how many ran.`,
2914
+ "bridge.adopt": `Internal: a session hands back its journal after a reload.`,
2915
+ "bridge.logs": `Errors and warnings the app logged, newest last. Options: {level: "error"|"warn"|"all", limit, clear}.`,
2916
+ "bridge.pending": `What bridge.restore would undo right now, per area (store, query, net, app, ...): the value is true, or detail such as a store's snapshot against its current value. Empty when nothing is pending. Changes nothing.`,
2917
+ "bridge.ping": `Round-trip check. Returns the app clock.`,
2918
+ "bridge.restore": `Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.`,
2919
+ "bridge.tools": `Every tool this app exposes.`,
2920
+ "scenario.apply": `Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.`,
2921
+ "scenario.list": `Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.`,
2922
+ "scenario.restore": `Undo every active scenario, newest first. bridge.restore runs this after every other restorer.`,
2923
+ "screen.capture": `Screenshot the app as a PNG. Returns {path} of a file on the device. \`{base64:true}\` returns {base64} instead when small enough (else {path}). Only works if the app already has react-native-view-shot; otherwise errors and you use agent-device, simctl or adb.`,
2924
+ "screen.fill": `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits. "" clears it. {type:true} types one character at a time, waiting for a render after each (for debounced or per-character inputs; "\\n" is the Enter key). An input with no value prop gets its native text set; if that cannot be done the reply has a warning.`,
2925
+ "screen.findText": `Find text on screen (substring, or exact with {exact:true}) and whether each match is on screen. Searches the same joined text screen.snapshot shows (nested Text concatenated) and accessibility labels; {labels:false} searches text only. Each match says which field matched; a miss lists near misses. Ignores screens the user can't reach (unfocused tabs and stack screens, under a modal); \`hidden\` counts matches there.`,
2926
+ "screen.press": `Press a button, then wait for the render. ${TARGET_HELP} Text with no handler of its own presses the nearest ancestor that has one, and the reply says via:"ancestor". Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead. A Switch (native switch host or role switch, with onValueChange) is toggled with onValueChange(!value), not the row around it; reply via:"onValueChange". {long:true} long presses: onPressIn, onLongPress, onPressOut (never onPress); an error when there is no onLongPress, on the element or an ancestor. {scroll:true} also pages the largest vertical FlatList/SectionList/FlashList for a row it has not rendered yet (and scrolls back if it finds none; onEndReached may fire). A tap inside a react-native-gesture-handler GestureDetector cannot be pressed from here (its callbacks run on the UI thread): the error says so.`,
2927
+ "screen.refresh": `Pull to refresh: call the onRefresh of the RefreshControl of the scrollable holding a target (or the largest one with a RefreshControl on screen), then wait for the render. It does not wait for the data: follow with screen.waitFor.`,
2928
+ "screen.scroll": `Scroll the nearest ScrollView/FlatList/FlashList, then wait for the render. Takes a target (or {to: target}) to bring into the middle of its scrollable, {toEnd:true}, {toStart:true}, or {by: points} (negative scrolls back); the last argument may be {within: target, index} to name the scrollable (a target inside it, or its testID). Without a target it scrolls the largest scrollable on screen. Only rendered elements can be targeted: rows a FlatList has not rendered yet need {toEnd:true} or {by} first. Returns the offset and, for a target, whether it is now on screen. ${TARGET_HELP}`,
2929
+ "screen.setValue": `Set a Switch, Slider, Picker or SegmentedControl: calls its onValueChange (and a Slider's onSlidingComplete) with the value, waits for the render and reads the value back. A Switch takes true or false, a Slider a number within minimumValue and maximumValue (0 and 1 by default) and on its step (onSlidingStart, onValueChange, onSlidingComplete), a Picker one of its Picker.Item values, a SegmentedControl one of its values (a wrong type or value is an error that lists the options). The reply has \`value\` as the app now shows it, and a warning when that is not what was set (a controlled value the app refused). Disabled controls are refused; {force:true} sets anyway. {scroll:true} scrolls or pages the target into view first (pages the largest vertical list; onEndReached may fire). ${TARGET_HELP}`,
2930
+ "screen.snapshot": `Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch's value is checked). \`actions\` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and \`hidden\` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one \`webview\` element with its name: use webview.* inside it. Options: {format:"json"|"text"} (the runtime default is JSON with rects; the CLI and the MCP server return the text form unless you pass {format:"json"}). Text is one line per element: kind (or role: switch, checkbox, tab), #testID, text, label, value, state, then \`readonly\`, \`actions=\u2026\`, \`offscreen\`, \`hidden\`. Lines that repeat get \`index=N\` (the {index} of the target that names them); a line with no testID, text, label or placeholder gets \`@x,y\`, the point for {at:[x,y]}. JSON has rects, onScreen:false and hidden:true. {kinds:["button","input"]} keeps only those kinds (kinds are button, input, text, view, webview; a switch or tab is a button); {within: target} keeps only elements under that target in the tree (a target as for screen.press; with several matches pass {testID, index} inside it).`,
2931
+ "screen.waitFor": `Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. {scroll:true} also accepts a match rendered off screen and scrolls it into view, (it does not page lists for rows they have not rendered: wait for the list, then press/fill/setValue with {scroll:true}; not with gone). Default timeout 5000 ms; a timeout lists near misses.`
2932
+ };
2933
+
2934
+ // src/mcp/server.ts
2906
2935
  var SUPPORTED = ["2025-06-18", "2024-11-05"];
2907
2936
  var STATUS_TOOL = "agent_bridge_status";
2908
2937
  var MAX_NAME = 64;
2938
+ var BUILTIN_LIST = Object.entries(TOOL_DESCRIPTIONS).filter(([name]) => !UNLISTED_WITHOUT_APP.includes(name)).map(([name, description]) => ({ name, description }));
2909
2939
  function mcpNames(tools, reserved = []) {
2910
2940
  const out = /* @__PURE__ */ new Map();
2911
2941
  const taken = new Set(reserved);
@@ -3001,25 +3031,34 @@ function startMcpServer(options) {
3001
3031
  options.log?.(`agent-bridge mcp: not connected: ${now}`);
3002
3032
  };
3003
3033
  const announce = () => {
3004
- if (!initialized || listed === null || signature === listed || signature === announced)
3034
+ if (!initialized || listed === null) return;
3035
+ if (signature === listed) {
3036
+ announced = "";
3005
3037
  return;
3038
+ }
3039
+ if (signature === announced) return;
3006
3040
  announced = signature;
3007
3041
  send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
3008
3042
  };
3043
+ const setTools = (list2, sig) => {
3044
+ signature = sig;
3045
+ names2 = mcpNames(list2, [STATUS_TOOL]);
3046
+ tools = [...names2].map(([mcp, name]) => ({
3047
+ name,
3048
+ mcp,
3049
+ description: list2.find((t) => t.name === name)?.description
3050
+ }));
3051
+ announce();
3052
+ };
3053
+ setTools(BUILTIN_LIST, "builtin");
3054
+ const BUILTIN_MCP = mcpNames(BUILTIN_LIST, [STATUS_TOOL]);
3009
3055
  const adopt = (next) => {
3010
3056
  const list2 = next.tools();
3011
3057
  const sig = JSON.stringify([next.device.deviceId, list2]);
3012
3058
  bridge = next;
3013
3059
  lastError = void 0;
3014
3060
  if (sig !== signature) {
3015
- signature = sig;
3016
- names2 = mcpNames(list2, [STATUS_TOOL]);
3017
- tools = [...names2].map(([mcp, name]) => ({
3018
- name,
3019
- mcp,
3020
- description: list2.find((t) => t.name === name)?.description
3021
- }));
3022
- announce();
3061
+ setTools(list2, sig);
3023
3062
  }
3024
3063
  };
3025
3064
  const ensure = () => {
@@ -3121,7 +3160,15 @@ Start the app and call again; the server reconnects on the next call. The ${STAT
3121
3160
  );
3122
3161
  }
3123
3162
  const tool = typeof params.name === "string" ? names2.get(params.name) : void 0;
3124
- if (!tool) return null;
3163
+ if (!tool) {
3164
+ const builtin = typeof params.name === "string" ? BUILTIN_MCP.get(params.name) : void 0;
3165
+ if (!builtin) return null;
3166
+ return failure(
3167
+ text(
3168
+ `The connected app has no ${builtin} tool (for scenario tools: it defines no scenarios). Call tools/list again for its tools.`
3169
+ )
3170
+ );
3171
+ }
3125
3172
  const input = params.arguments ?? {};
3126
3173
  let args = input.args ?? [];
3127
3174
  if (typeof args === "string") {
@@ -3170,7 +3217,7 @@ The call may or may not have run; check before repeating one that changes state.
3170
3217
  protocolVersion: SUPPORTED.includes(asked) ? asked : SUPPORTED[0],
3171
3218
  capabilities: { tools: { listChanged: true } },
3172
3219
  serverInfo: { name: "agent-bridge", version: options.version ?? "0.0.0" },
3173
- instructions: `Tools of the connected React Native app. Each takes \`args\`, an array of the tool's arguments. Results over 32 KB come back as a summary unless \`full\` is true. If the list is empty or calls fail, call ${STATUS_TOOL}.`
3220
+ instructions: `Tools of the connected React Native app. Each takes \`args\`, an array of the tool's arguments. Results over 32 KB come back as a summary unless \`full\` is true. Until an app connects the list shows only the built-in tools (screen, app, bridge, and scenario, which only exists when the app defines scenarios); calls fail until then, and the app's own tools (query, store, net, ...) appear once it connects. If calls fail, call ${STATUS_TOOL}.`
3174
3221
  }
3175
3222
  };
3176
3223
  }
@@ -70,6 +70,33 @@ async function runUndos() {
70
70
  return { ran: batch.length, errors };
71
71
  }
72
72
 
73
+ // src/runtime/tools/descriptions.ts
74
+ var INTERNAL_TOOLS = ["bridge.adopt"];
75
+ var UNLISTED_WITHOUT_APP = [...INTERNAL_TOOLS, "bridge.ping"];
76
+ var TARGET_HELP = "target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument.";
77
+ var TOOL_DESCRIPTIONS = {
78
+ "app.reload": `Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins, storage) are lost, and the reply names them: restore first. Returns before it reloads; a session waits for the new bridge and reports what was lost.`,
79
+ "app.restore": `Undo what the app's own tools changed: runs the undos they registered with onRestore, newest first, once. Returns how many ran.`,
80
+ "bridge.adopt": `Internal: a session hands back its journal after a reload.`,
81
+ "bridge.logs": `Errors and warnings the app logged, newest last. Options: {level: "error"|"warn"|"all", limit, clear}.`,
82
+ "bridge.pending": `What bridge.restore would undo right now, per area (store, query, net, app, ...): the value is true, or detail such as a store's snapshot against its current value. Empty when nothing is pending. Changes nothing.`,
83
+ "bridge.ping": `Round-trip check. Returns the app clock.`,
84
+ "bridge.restore": `Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.`,
85
+ "bridge.tools": `Every tool this app exposes.`,
86
+ "scenario.apply": `Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.`,
87
+ "scenario.list": `Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.`,
88
+ "scenario.restore": `Undo every active scenario, newest first. bridge.restore runs this after every other restorer.`,
89
+ "screen.capture": `Screenshot the app as a PNG. Returns {path} of a file on the device. \`{base64:true}\` returns {base64} instead when small enough (else {path}). Only works if the app already has react-native-view-shot; otherwise errors and you use agent-device, simctl or adb.`,
90
+ "screen.fill": `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits. "" clears it. {type:true} types one character at a time, waiting for a render after each (for debounced or per-character inputs; "\\n" is the Enter key). An input with no value prop gets its native text set; if that cannot be done the reply has a warning.`,
91
+ "screen.findText": `Find text on screen (substring, or exact with {exact:true}) and whether each match is on screen. Searches the same joined text screen.snapshot shows (nested Text concatenated) and accessibility labels; {labels:false} searches text only. Each match says which field matched; a miss lists near misses. Ignores screens the user can't reach (unfocused tabs and stack screens, under a modal); \`hidden\` counts matches there.`,
92
+ "screen.press": `Press a button, then wait for the render. ${TARGET_HELP} Text with no handler of its own presses the nearest ancestor that has one, and the reply says via:"ancestor". Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead. A Switch (native switch host or role switch, with onValueChange) is toggled with onValueChange(!value), not the row around it; reply via:"onValueChange". {long:true} long presses: onPressIn, onLongPress, onPressOut (never onPress); an error when there is no onLongPress, on the element or an ancestor. {scroll:true} also pages the largest vertical FlatList/SectionList/FlashList for a row it has not rendered yet (and scrolls back if it finds none; onEndReached may fire). A tap inside a react-native-gesture-handler GestureDetector cannot be pressed from here (its callbacks run on the UI thread): the error says so.`,
93
+ "screen.refresh": `Pull to refresh: call the onRefresh of the RefreshControl of the scrollable holding a target (or the largest one with a RefreshControl on screen), then wait for the render. It does not wait for the data: follow with screen.waitFor.`,
94
+ "screen.scroll": `Scroll the nearest ScrollView/FlatList/FlashList, then wait for the render. Takes a target (or {to: target}) to bring into the middle of its scrollable, {toEnd:true}, {toStart:true}, or {by: points} (negative scrolls back); the last argument may be {within: target, index} to name the scrollable (a target inside it, or its testID). Without a target it scrolls the largest scrollable on screen. Only rendered elements can be targeted: rows a FlatList has not rendered yet need {toEnd:true} or {by} first. Returns the offset and, for a target, whether it is now on screen. ${TARGET_HELP}`,
95
+ "screen.setValue": `Set a Switch, Slider, Picker or SegmentedControl: calls its onValueChange (and a Slider's onSlidingComplete) with the value, waits for the render and reads the value back. A Switch takes true or false, a Slider a number within minimumValue and maximumValue (0 and 1 by default) and on its step (onSlidingStart, onValueChange, onSlidingComplete), a Picker one of its Picker.Item values, a SegmentedControl one of its values (a wrong type or value is an error that lists the options). The reply has \`value\` as the app now shows it, and a warning when that is not what was set (a controlled value the app refused). Disabled controls are refused; {force:true} sets anyway. {scroll:true} scrolls or pages the target into view first (pages the largest vertical list; onEndReached may fire). ${TARGET_HELP}`,
96
+ "screen.snapshot": `Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch's value is checked). \`actions\` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and \`hidden\` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one \`webview\` element with its name: use webview.* inside it. Options: {format:"json"|"text"} (the runtime default is JSON with rects; the CLI and the MCP server return the text form unless you pass {format:"json"}). Text is one line per element: kind (or role: switch, checkbox, tab), #testID, text, label, value, state, then \`readonly\`, \`actions=\u2026\`, \`offscreen\`, \`hidden\`. Lines that repeat get \`index=N\` (the {index} of the target that names them); a line with no testID, text, label or placeholder gets \`@x,y\`, the point for {at:[x,y]}. JSON has rects, onScreen:false and hidden:true. {kinds:["button","input"]} keeps only those kinds (kinds are button, input, text, view, webview; a switch or tab is a button); {within: target} keeps only elements under that target in the tree (a target as for screen.press; with several matches pass {testID, index} inside it).`,
97
+ "screen.waitFor": `Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. {scroll:true} also accepts a match rendered off screen and scrolls it into view, (it does not page lists for rows they have not rendered: wait for the list, then press/fill/setValue with {scroll:true}; not with gone). Default timeout 5000 ms; a timeout lists near misses.`
98
+ };
99
+
73
100
  // src/runtime/tools/restore.ts
74
101
  var SUFFIX = ".restore";
75
102
  var SELF = "bridge.restore";
@@ -108,17 +135,16 @@ function journalRev(tools) {
108
135
  function restorers(tools) {
109
136
  return Object.entries(tools).filter(([name, d]) => name.endsWith(SUFFIX) && name !== SELF && typeof d !== "function").map(([name, d]) => [name.slice(0, -SUFFIX.length), d]).filter((e) => typeof e[1] !== "function");
110
137
  }
111
- var INTERNAL_TOOLS = ["bridge.adopt"];
112
138
  function restoreTools(getTools) {
113
139
  return {
114
140
  "bridge.pending": {
115
141
  maxArgs: 0,
116
- description: "What bridge.restore would undo right now, per area (store, query, net, app, ...): the value is true, or detail such as a store's snapshot against its current value. Empty when nothing is pending. Changes nothing.",
142
+ description: TOOL_DESCRIPTIONS["bridge.pending"],
117
143
  run: () => pendingByArea(getTools(), true)
118
144
  },
119
145
  "bridge.adopt": {
120
146
  maxArgs: 1,
121
- description: "Internal: a session hands back its journal after a reload.",
147
+ description: TOOL_DESCRIPTIONS["bridge.adopt"],
122
148
  run: async (data) => {
123
149
  const tools = getTools();
124
150
  const out = {};
@@ -137,7 +163,7 @@ function restoreTools(getTools) {
137
163
  }
138
164
  },
139
165
  [SELF]: {
140
- description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
166
+ description: TOOL_DESCRIPTIONS["bridge.restore"],
141
167
  run: async () => {
142
168
  const tools = getTools();
143
169
  const results = {};
@@ -176,7 +202,7 @@ function selectReload(host, devSettingsReload) {
176
202
  function appTools(reload, getTools) {
177
203
  return {
178
204
  "app.reload": {
179
- description: "Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins, storage) are lost, and the reply names them: restore first. Returns before it reloads; a session waits for the new bridge and reports what was lost.",
205
+ description: TOOL_DESCRIPTIONS["app.reload"],
180
206
  run: () => {
181
207
  setTimeout(reload, 50);
182
208
  const lost = getTools ? Object.keys(pendingByArea(getTools())) : [];
@@ -190,7 +216,7 @@ function appTools(reload, getTools) {
190
216
  const labels = pendingUndos();
191
217
  return labels.length ? labels : false;
192
218
  },
193
- description: "Undo what the app's own tools changed: runs the undos they registered with onRestore, newest first, once. Returns how many ran.",
219
+ description: TOOL_DESCRIPTIONS["app.restore"],
194
220
  run: async () => {
195
221
  const { ran, errors } = await runUndos();
196
222
  if (errors.length)
@@ -205,11 +231,11 @@ function appTools(reload, getTools) {
205
231
  function bridgeTools(listTools) {
206
232
  return {
207
233
  "bridge.ping": {
208
- description: "Round-trip check. Returns the app clock.",
234
+ description: TOOL_DESCRIPTIONS["bridge.ping"],
209
235
  run: () => ({ pong: true, at: Date.now() })
210
236
  },
211
237
  "bridge.tools": {
212
- description: "Every tool this app exposes.",
238
+ description: TOOL_DESCRIPTIONS["bridge.tools"],
213
239
  run: listTools
214
240
  }
215
241
  };
@@ -229,7 +255,7 @@ function loadViewShot() {
229
255
  function captureTools(load) {
230
256
  return {
231
257
  "screen.capture": {
232
- description: "Screenshot the app as a PNG. Returns {path} of a file on the device. `{base64:true}` returns {base64} instead when small enough (else {path}). Only works if the app already has react-native-view-shot; otherwise errors and you use agent-device, simctl or adb.",
258
+ description: TOOL_DESCRIPTIONS["screen.capture"],
233
259
  maxArgs: 1,
234
260
  run: async (options) => {
235
261
  const viewShot = load();
@@ -255,7 +281,7 @@ function captureTools(load) {
255
281
  function logTools(logs) {
256
282
  return {
257
283
  "bridge.logs": {
258
- description: 'Errors and warnings the app logged, newest last. Options: {level: "error"|"warn"|"all", limit, clear}.',
284
+ description: TOOL_DESCRIPTIONS["bridge.logs"],
259
285
  run: (options = {}) => {
260
286
  const entries = logs.read(options);
261
287
  if (options.clear) logs.clear();
@@ -361,7 +387,7 @@ function scenarioTools(getScenarios, getTools) {
361
387
  };
362
388
  return {
363
389
  "scenario.list": {
364
- description: "Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.",
390
+ description: TOOL_DESCRIPTIONS["scenario.list"],
365
391
  run: () => Object.entries(getScenarios()).map(([name, s]) => ({
366
392
  name,
367
393
  description: s.description,
@@ -370,7 +396,7 @@ function scenarioTools(getScenarios, getTools) {
370
396
  })).sort((a, b) => a.name.localeCompare(b.name))
371
397
  },
372
398
  "scenario.apply": {
373
- description: "Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.",
399
+ description: TOOL_DESCRIPTIONS["scenario.apply"],
374
400
  run: async (name, options) => {
375
401
  const scenario = find(name);
376
402
  if (scenario.options) {
@@ -407,7 +433,7 @@ scenario.list shows its options schema.`
407
433
  },
408
434
  "scenario.restore": {
409
435
  pending: () => active.size > 0,
410
- description: "Undo every active scenario, newest first. bridge.restore runs this after every other restorer.",
436
+ description: TOOL_DESCRIPTIONS["scenario.restore"],
411
437
  run: async () => {
412
438
  const names = [...active.keys()].reverse();
413
439
  const errors = [];
@@ -2196,7 +2222,6 @@ function createScreen(env) {
2196
2222
 
2197
2223
  // src/runtime/tools/screen.ts
2198
2224
  var devToolsHook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2199
- var TARGET_HELP = "target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument.";
2200
2225
  function screenTools() {
2201
2226
  const screen = createScreen({
2202
2227
  roots: () => fiberRoots(devToolsHook()),
@@ -2204,35 +2229,35 @@ function screenTools() {
2204
2229
  });
2205
2230
  return {
2206
2231
  "screen.findText": {
2207
- description: "Find text on screen (substring, or exact with {exact:true}) and whether each match is on screen. Searches the same joined text screen.snapshot shows (nested Text concatenated) and accessibility labels; {labels:false} searches text only. Each match says which field matched; a miss lists near misses. Ignores screens the user can't reach (unfocused tabs and stack screens, under a modal); `hidden` counts matches there.",
2232
+ description: TOOL_DESCRIPTIONS["screen.findText"],
2208
2233
  run: (text, options) => screen.findText(text, options)
2209
2234
  },
2210
2235
  "screen.snapshot": {
2211
- description: 'Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch\'s value is checked). `actions` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and `hidden` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one `webview` element with its name: use webview.* inside it. Options: {format:"json"|"text"} (the runtime default is JSON with rects; the CLI and the MCP server return the text form unless you pass {format:"json"}). Text is one line per element: kind (or role: switch, checkbox, tab), #testID, text, label, value, state, then `readonly`, `actions=\u2026`, `offscreen`, `hidden`. Lines that repeat get `index=N` (the {index} of the target that names them); a line with no testID, text, label or placeholder gets `@x,y`, the point for {at:[x,y]}. JSON has rects, onScreen:false and hidden:true. {kinds:["button","input"]} keeps only those kinds (kinds are button, input, text, view, webview; a switch or tab is a button); {within: target} keeps only elements under that target in the tree (a target as for screen.press; with several matches pass {testID, index} inside it).',
2236
+ description: TOOL_DESCRIPTIONS["screen.snapshot"],
2212
2237
  run: (options) => options?.format === "text" ? screen.snapshotText(options) : screen.snapshot(options)
2213
2238
  },
2214
2239
  "screen.fill": {
2215
- description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits. "" clears it. {type:true} types one character at a time, waiting for a render after each (for debounced or per-character inputs; "\\n" is the Enter key). An input with no value prop gets its native text set; if that cannot be done the reply has a warning.`,
2240
+ description: TOOL_DESCRIPTIONS["screen.fill"],
2216
2241
  run: (target, text, options) => screen.fill(target, text, options)
2217
2242
  },
2218
2243
  "screen.press": {
2219
- description: `Press a button, then wait for the render. ${TARGET_HELP} Text with no handler of its own presses the nearest ancestor that has one, and the reply says via:"ancestor". Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead. A Switch (native switch host or role switch, with onValueChange) is toggled with onValueChange(!value), not the row around it; reply via:"onValueChange". {long:true} long presses: onPressIn, onLongPress, onPressOut (never onPress); an error when there is no onLongPress, on the element or an ancestor. {scroll:true} also pages the largest vertical FlatList/SectionList/FlashList for a row it has not rendered yet (and scrolls back if it finds none; onEndReached may fire). A tap inside a react-native-gesture-handler GestureDetector cannot be pressed from here (its callbacks run on the UI thread): the error says so.`,
2244
+ description: TOOL_DESCRIPTIONS["screen.press"],
2220
2245
  run: (target, options) => screen.press(target, options)
2221
2246
  },
2222
2247
  "screen.setValue": {
2223
- description: `Set a Switch, Slider, Picker or SegmentedControl: calls its onValueChange (and a Slider's onSlidingComplete) with the value, waits for the render and reads the value back. A Switch takes true or false, a Slider a number within minimumValue and maximumValue (0 and 1 by default) and on its step (onSlidingStart, onValueChange, onSlidingComplete), a Picker one of its Picker.Item values, a SegmentedControl one of its values (a wrong type or value is an error that lists the options). The reply has \`value\` as the app now shows it, and a warning when that is not what was set (a controlled value the app refused). Disabled controls are refused; {force:true} sets anyway. {scroll:true} scrolls or pages the target into view first (pages the largest vertical list; onEndReached may fire). ${TARGET_HELP}`,
2248
+ description: TOOL_DESCRIPTIONS["screen.setValue"],
2224
2249
  run: (target, value, options) => screen.setValue(target, value, options)
2225
2250
  },
2226
2251
  "screen.scroll": {
2227
- description: `Scroll the nearest ScrollView/FlatList/FlashList, then wait for the render. Takes a target (or {to: target}) to bring into the middle of its scrollable, {toEnd:true}, {toStart:true}, or {by: points} (negative scrolls back); the last argument may be {within: target, index} to name the scrollable (a target inside it, or its testID). Without a target it scrolls the largest scrollable on screen. Only rendered elements can be targeted: rows a FlatList has not rendered yet need {toEnd:true} or {by} first. Returns the offset and, for a target, whether it is now on screen. ${TARGET_HELP}`,
2252
+ description: TOOL_DESCRIPTIONS["screen.scroll"],
2228
2253
  run: (arg, options) => screen.scroll(arg, options)
2229
2254
  },
2230
2255
  "screen.refresh": {
2231
- description: "Pull to refresh: call the onRefresh of the RefreshControl of the scrollable holding a target (or the largest one with a RefreshControl on screen), then wait for the render. It does not wait for the data: follow with screen.waitFor.",
2256
+ description: TOOL_DESCRIPTIONS["screen.refresh"],
2232
2257
  run: (target, options) => screen.refresh(target, options)
2233
2258
  },
2234
2259
  "screen.waitFor": {
2235
- description: "Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. {scroll:true} also accepts a match rendered off screen and scrolls it into view, (it does not page lists for rows they have not rendered: wait for the list, then press/fill/setValue with {scroll:true}; not with gone). Default timeout 5000 ms; a timeout lists near misses.",
2260
+ description: TOOL_DESCRIPTIONS["screen.waitFor"],
2236
2261
  run: (target, options) => screen.waitFor(target, options)
2237
2262
  }
2238
2263
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avasapp/agent-bridge",
3
- "version": "0.6.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",
@@ -36,7 +36,7 @@ roughly 200 ms more startup per call (machine-dependent).
36
36
  If the app's tools are already in your tool list as an MCP server
37
37
  (`agent-bridge mcp`; names like `screen_snapshot` for `screen.snapshot`), call
38
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.
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
40
 
41
41
  ## Start a session
42
42