@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 +1 -1
- package/dist/cli.js +59 -12
- package/dist/runtime/index.cjs +47 -22
- package/package.json +1 -1
- package/skills/agent-bridge/SKILL.md +1 -1
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
|

|
|
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.
|
|
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
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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
|
}
|
package/dist/runtime/index.cjs
CHANGED
|
@@ -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: "
|
|
142
|
+
description: TOOL_DESCRIPTIONS["bridge.pending"],
|
|
117
143
|
run: () => pendingByArea(getTools(), true)
|
|
118
144
|
},
|
|
119
145
|
"bridge.adopt": {
|
|
120
146
|
maxArgs: 1,
|
|
121
|
-
description: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
234
|
+
description: TOOL_DESCRIPTIONS["bridge.ping"],
|
|
209
235
|
run: () => ({ pong: true, at: Date.now() })
|
|
210
236
|
},
|
|
211
237
|
"bridge.tools": {
|
|
212
|
-
description: "
|
|
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: "
|
|
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:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
2232
|
+
description: TOOL_DESCRIPTIONS["screen.findText"],
|
|
2208
2233
|
run: (text, options) => screen.findText(text, options)
|
|
2209
2234
|
},
|
|
2210
2235
|
"screen.snapshot": {
|
|
2211
|
-
description:
|
|
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:
|
|
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:
|
|
2244
|
+
description: TOOL_DESCRIPTIONS["screen.press"],
|
|
2220
2245
|
run: (target, options) => screen.press(target, options)
|
|
2221
2246
|
},
|
|
2222
2247
|
"screen.setValue": {
|
|
2223
|
-
description:
|
|
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:
|
|
2252
|
+
description: TOOL_DESCRIPTIONS["screen.scroll"],
|
|
2228
2253
|
run: (arg, options) => screen.scroll(arg, options)
|
|
2229
2254
|
},
|
|
2230
2255
|
"screen.refresh": {
|
|
2231
|
-
description: "
|
|
2256
|
+
description: TOOL_DESCRIPTIONS["screen.refresh"],
|
|
2232
2257
|
run: (target, options) => screen.refresh(target, options)
|
|
2233
2258
|
},
|
|
2234
2259
|
"screen.waitFor": {
|
|
2235
|
-
description: "
|
|
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.
|
|
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.
|
|
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
|
|