Repository navigation
Releases
@avasapp/agent-bridge@0.6.0
Minor Changes
-
1e989d3:
screen.snapshottakes{format: "text"}for one line per element (button #save "Save plant" disabled, withindex=Non lines that repeat and@x,yon lines with no testID, text, label or placeholder) instead of JSON with a rect per element: about a fifth of the size. It also takes{kinds: ["button", "input"]}to keep only those kinds and{within: target}to keep only elements under a target in the tree. An empty filtered result says what matched nothing, a long label is never cut, and--outwith{"format":"text"}writes the text (--jsonwith it is an error).Behaviour change:
agent-bridge call screen.snapshotand the MCPscreen_snapshottool now print the text form by default. For the old JSON pass--json(or--out) tocall, or{"format": "json"}as the argument (args: [{"format": "json"}]over MCP).connect(),bridge.calland flows still get JSON unless they ask for text. -
6534b6b: New opt-in
@avasapp/agent-bridge/deviceentry withdeviceTools({ NetInfo, Notifications, navigation, time }).device.openUrldelivers an incoming deep link to the app's Linking listeners (and makesgetInitialURLreturn it), returning the route the app ended on when you pass the navigation container.device.appStatefires AppState change listeners and setsAppState.currentState.device.offlinemakes everyfetchand XHR fail like no network, mocks included, and tells NetInfo; requests it fails show innet.logas(device offline).device.notificationcalls expo-notifications' received or (with{tapped: true}) response listeners.device.setTime(withtime: true) shiftsDate.now()andnew Date().device.restoreundoes all of it and reportsrestored: trueif anything was undone, andbridge.pendinglists what is set. The Ably and socket.io adapters now also log what the app sends (channel.publish,socket.emit) inrealtime.logwithsent: true;socket.emitis logged once per call as its argument list, payloads are copied and cut, and credential-looking keys show as[redacted]unlessrealtime.loggets{full: true};createRealtimeTapgetssent()and@avasapp/agent-bridge/realtimeexportstapOutgoingfor custom taps. -
08aff63:
agent-bridge runnow runs several flows: pass files, a directory (its*.flow.mjs) or a glob (globs only match*.flow.mjs), and it runs them in order on one connection, prints a pass/fail line for each and a summary, and exits 1 if any failed.--bailstops at the first failure,--grep <text>picks flows by name or by path relative to where you ran,--keepleaves the app as a flow left it, and--reporter json|junitwrites a report for CI: on stdout (with step lines and a flow'sconsole.logon stderr) or to a file with--out <file>, which needs--reporter. A flow file that won't load, a failed connect and a--grepwith no match show up as failed testcases in the report instead of crashing before it. JUnit uses the folder as the classname and relative file paths, so same-named flows in different folders stay apart.Behaviour change: every flow now ends with
bridge.restore, not only flows that declare a scenario, so a failed flow can't leave the app dirty for the next one. Through a session this also undoes changes you made by hand beforerun; pass--keepto skip it.runFlowdoes the same ({ keep: true }opts out). A flow's ownstep('undo', 'bridge.restore')is no longer needed. Errors the closing restore reports are printed but don't count for--strict.Running one flow now also shows that closing
restorestep. If the app stops answering, the error is printed at once and the remaining flows are skipped with that reason.Flows get an
expecthelper (expect.text,expect.gone,expect.route,expect.result) that fails with a readable message (expect.resulttakes aRegExpto match a string and rejectsDate,Mapand other class instances), and a failed flow prints what was on screen (screen.snapshotandrouter.current). -
97ff9e0:
agent-bridge mcpruns an MCP server on stdio, for agents that prefer MCP tools to a shell:claude mcp add agent-bridge -- ./node_modules/.bin/agent-bridge mcp. Each tool the app exposes becomes an MCP tool (screen.snapshotisscreen_snapshot) that takesargs, an array of its arguments, andfull. It uses the project's session (and stays on that one, never another server'smcp-*session), or starts one and runsbridge.restorewhen the server exits (--keepskips that). If its session is stopped, the next call says so once and the call after that uses a new session. It sendsnotifications/tools/list_changedwhenever the list a client received is out of date, including when the app starts after the agent, and always lists anagent_bridge_statustool that says why it is not connected. Results over 32 KB come back as a summary unlessfullis true,screen.captureimages are sent as image content by their real type, and session errors such asmay or may not have runpass through unchanged. See the CLI docs. -
2f8a9eb: Network mocks can match more exactly.
net.mocktakes{path: "/orders/:id"}(the whole path on any host, query ignored;:nameis one segment,*the rest),{body: {...}}(the JSON request body contains these keys) and{graphql: "GetUser"}(the request'soperationName, from the JSON body or?operationName=), alone or withurlandmethod. A bare string still matches any URL containing it, andnet.mockandnet.mocksnow returnmatches, the mock in words. An unknown key in a match is now an error instead of being ignored.net.mockFromLogon a GraphQL request now only answers that operation, where it used to answer every request to the endpoint; when the logged body wasn't kept it says so in awarning. Strict mode lists blocked GraphQL requests by operation and names mocks that matched the URL but not the body.New
net.waitFor(match, {timeoutMs, status, sinceId})waits for a matching request to finish and returns its entry.net.entrynow shows request and response headers, with credential values cut unless you pass{full: true}: any header whose name contains auth, cookie, token, secret, session, password, signature or api-key (soapikey,x-auth-tokenandx-amz-security-tokentoo). Authorization keeps only its scheme word, the rest at most 4 characters and a quarter of the value.net.waitForwaits for the response body to be read and matches a GraphQL?operationName=on a GET.networkTools({logSize})sets how many requests the log keeps. An XHR aborted just aftersend()is no longer sent when no mock answers it, and one aborted beforesend()is sent as normal.pathalso treats:nameas a parameter only as a whole segment, takes a trailing slash either way, matches/on a bare host, and tries the percent-decoded URL. Abodythat is empty once serialised ({a: undefined}) is refused. A body or GraphQL mock no longer makesfetch(Request)calls with a binary body wait for a body read. A handler mock withtimes: 1now answers one of two requests in flight, andshadowsskips mocks that havetimes. -
d785078:
screen.pressno longer treats a pressable that wraps an input or two or more other controls (a tap-to-dismiss-keyboard screen, a modal backdrop around Cancel and OK) as one big button holding all the text under it. A button built from wrapper components, a card with one icon button, and a row with inline text links keep their text. When the handler it called belongs to an ancestor, the reply saysvia: "ancestor".screen.fillnow works on uncontrolled inputs (including Fabric andvalue: undefined), firesonEndEditing, clears with"", and takes{type:true}to type one character at a time, waiting for a render after each, with"\n"as the Enter key. For an uncontrolled input the reply'selement.valueis what was written, and it carries awarningif the text could not be written natively.router.navigate,pushandreplacenow return the route the app ended on (an object, not the href string) once the screen has rendered, throw on+not-found, and warn when the path or query is not the href's, or when the navigation container has not mounted. -
a30731f: New adapter for apps on
@react-navigation/nativewithout expo-router:navTools(navigationRef)from@avasapp/agent-bridge/react-navigation. It addsnav.navigate,nav.push,nav.replace,nav.popTo,nav.back,nav.popToTopandnav.current. Each navigation tool waits for the new screen and returns the route the app ended on (name,params,path,stack,canGoBack), adds awarningwhen the screens asked for (including a nested{screen}chain) are not at the end of the path or a push/replace changed nothing, and throws on an unknown route name.nav.pushandnav.replacethrow when the focused navigator is tabs or a drawer, andnav.popToTopdoes nothing at a stack's first screen instead of popping a parent. In React Navigation 7,nav.navigatepushes unless the screen is already focused; usenav.popToto return to an earlier one. PassgetStateFromPathand alinkingobject with aconfigto letnav.navigatetake a path like/post/7or a URL with one oflinking.prefixes; apps with a static config orlinking: { enabled: 'auto' }should use route names. The tools are namednav.*so they do not clash withrouter.*.initnow writes a commentednavTools(navigationRef)line for these apps, replacing the old sample tools. -
840cd82: Two new adapters.
@avasapp/agent-bridge/redux(reduxTools(store, {redact})) givesredux.list,get(the state or a path, withpickandkeys),dispatch(a plain{type, payload}action),set(merge into a slice),commitandrestore. Wrap your root reducer in `bridg...
@avasapp/agent-bridge@0.4.2
Patch Changes
-
577fd6a: A session no longer switches to another app when it reconnects. It used to re-run discovery and attach to whatever single app was connected, so after the app went away its calls could land in a different app on the same Metro. Reconnects now only accept the app the session started on (same name and platform, and the same debugger target over CDP; its deviceId changes on every reload), skip an app another session owns, and otherwise fail with
<app> is not connected; connected: …until it comes back.--devicematching several apps is now an error that lists them instead of picking the first (an exact match still wins).Using session "<name>"on stderr now names the app and Metro too. Closes #69.A call over CDP now ends at its timeout even when the debugger never answers the evaluate, as the old runtime's socket does after a JS reload; a session on CDP used to hang there for good instead of reconnecting.
-
6c7afa4:
screen.waitFortakes{scroll:true}, likepressandfill: a match that is rendered but off screen (below the fold of aScrollView) is scrolled into view and returned, instead of timing out as an off-screen near miss. Without it, waitFor still matches on-screen elements only, and a timeout with an off-screen near miss now points at{scroll:true}.{gone:true}with{scroll:true}is refused. Closes #68.
@avasapp/agent-bridge@0.4.1
Patch Changes
- 71c82e2: Fixes release builds of apps using
useWebViewToolsfrom@avasapp/agent-bridge/react-native-webviewlosing their owninjectedJavaScriptBeforeContentLoaded,onLoadStart,onLoadEndandonNavigationStateChange. The README says to pass these as hook options rather than WebView props, but the release stub ignored its options and returned emptyprops, so an app that did so shipped with no injected script and no load or navigation handlers, silently. The release hook now returns those four from the options inprops(leaving out any not set, and withoutinjectedJavaScriptBeforeContentLoadedForMainFrameOnly), so the WebView behaves as if the adapter weren't there;wrapstill returns the handler unchanged. Closes #63. - The README now covers setup and the core CLI; the full reference moved to
docs/, linked from it. CONTRIBUTING.md has a section on using AI tools.
v0.4.0
Upgrading from 0.3
These change what existing calls return or accept; details are in the entries below.
query.geterrors on a key that isn't cached instead of returningnull;query.refetchreturns{ matched, refetched, data }instead of the data.- In flows,
step(label, tool, [a, b])now spreads a single array as the argument list, likeagent-bridge call; wrap it ([[a, b]]) to pass one array. screen.*only sees the focused screen: elements on screens underneath, inactive tabs and content under a modal no longer match (screen.snapshot {all:true}still lists them).screen.findTextmatches the snapshot's joined text.agent-bridge callprints results over 32 KB as a summary unless--fullor--outis given; a bare argument starting with@is a file path; several argument words are each an argument.net.mockscuts response bodies over ~2 KB ({ full: true }for whole ones).store.setandstore.callreturn only what changed, not the whole store.connect()and one-off CLI calls fail after 15 s when Metro doesn't answer, instead of hanging.
Minor Changes
-
eebfd42: CLI:
agent-bridge call <tool> @args.jsonreads the arguments from a file (@-for stdin), so a value over the shell's ~128 KB argument limit no longer fails withArgument list too long. A file follows the inline rules: an array is the argument list, any other value is one argument. With several words, each is one argument and@feed.jsonis that file's JSON (call net.mock '"/feed"' @feed.json).call --batchlines accept@fileas their argument list. A missing file or invalid JSON fails with an error naming the path.Behaviour changes: a bare argument starting with
@is now a file path, not a string (pass such a string as JSON:'"@user"'), andcallwith several argument words now sends each as an argument instead of ignoring all but the first. -
9ba7799: CLI:
agent-bridge call --batchreadstool argslines from stdin and prints one JSON line per call over a single connection (--stop-on-error,--full,--out <dir>; exits 1 if a call failed), andagent-bridge replis an interactive prompt on the same loop with history, tab completion and.time/.tools/.pending/.restoredot commands. With stdin not a terminal,replbehaves like--batch.In batch and REPL input, arguments that start like JSON (
[,{,") but don't parse are reported as an error instead of being sent as a string;callitself is unchanged. -
b722069:
query.gettakes a last{ pages: [from, to] }to return only those pages of an infinite query (withtotalPages), or{ path: "pages.0.items" }to return only the value there.net.mocksnow cuts responsejsonandbodyover ~2 KB likenet.logdoes and marks the mocktruncated: true;{ full: true }returns them whole (behaviour change for anything reading big mock bodies fromnet.mocks). A mock matched by a regex URL now lists the regex instead of{}. Fixes a CDP session dying when the app's JS thread was busy for over 1.5 s (for example while another client sent a multi-MB value): the health ping's timeout became an unhandled rejection in the session daemon. -
6518dae:
agent-bridge callhas--out <file>, which writes the result to a file and prints its size and top-level shape. A result over 32 KB now prints as a JSON summary (resultTooLarge,bytes,shape,hint) unless--fullor--outis given; behaviour change for scripts that parse bigcalloutput (add--full). Flows and the client API still return full values. Fixes results being cut off through a session: a value containing U+2028 or U+2029 (common in scraped text) was split by the session socket's line reader and arrived as invalid JSON. -
30ef813: Network log and mocks: log entries carry
startedAt(epoch ms) andnet.logtakes{ since }.net.entry <id>andnet.log { full: true }return whole bodies instead of ~2 KB, andnet.mockFromLog <id> [patch]turns a logged response into a mock, deep-merging a patch.net.mockdocuments that the newest mock wins, takesoptions.priority(higher first, negative for a fallback) and reportsshadowswhen it will be tried before an agent mock it also matches. -
1f67933: A session now notices when the app reloaded and says which pending restores were lost:
app reloaded; N pending restores lost: store, querycomes back with the next call, failed or not (agent-bridge callprints it as a warning,timed()has it asnotice,AgentBridgeCallErrorasnotice), inagent-bridge runoutput, and again insession stop. Every reply carries the id of the JS bundle load and the areas with pending undo state (pendingon a*.restoretool), so a reload is caught on the next reply and Fast Refresh is not mistaken for one. On Expo, calls to a reloaded app fail at once instead of timing out. Newapp.reloadtool reloads the app's JS (through Expo'sreloadAppAsyncwhen available, see the patch note below); a session never runs it twice and waits for the new bridge. -
bed11f0: Show what
bridge.restorewill undo and let apps add their own undos. Newbridge.pendinglists each area's pending restore (for stores, the snapshot against the current value, redacted and truncated).session stop --dry-runprints the same and keeps the session;session stop --keepwarns about snapshots still pending. Newstore.commitdrops a store's snapshot so the current value becomes the baseline. NewonRestore(fn, label?)export registers an undo from an app tool;bridge.restoreruns it once via the newapp.restore, andbridge.pendinglists it underapp. The store tool descriptions now say when the snapshot is taken. -
68142d5: New
router.dismissandrouter.dismissAlltools close route-based modals.router.back's description now says it only pops the navigator and leaves modals and sheets the app renders itself open; the README recommends registering amodal.closetool for those.RouterLikegains optionaldismiss,dismissAllandcanDismiss; a router without them makes the two tools throw a clear error. -
de337cf: New
screen.capturetool takes a PNG screenshot and returns{path}(or{base64}with{base64:true}when small) usingreact-native-view-shot, only if the app already has it; the bridge does not depend on it. Without it the tool errors and points to agent-device,xcrun simctl iooradb exec-out screencap -p. Closes #11. -
1c99220:
screen.snapshot,screen.findText,screen.press,screen.fillandscreen.waitFornow see only the focused screen: unfocused tabs and native-stack screens, content under an open modal,display: noneand accessibility-hidden subtrees are skipped, and the result says how many elements (hidden) were left out.screen.snapshot {all:true}still lists everything and marks hidden ones. Newscreen.scroll(to a target,{toEnd},{toStart},{by}, optionally{within}) andscreen.refresh(calls the RefreshControl'sonRefresh), and{scroll:true}onscreen.press/screen.fill. Behaviour change: elements on screens beneath the top one no longer match targets, so a press that only worked by picking a hidden duplicate now needs{all:true}to see it. -
d904592:
screen.pressandscreen.fillreject unknown target keys instead of matching everything. New{at:[x,y]}target presses icon-only buttons by point.{index}now also works as a trailing options argument, and the ambiguity error shows a concrete example.screen.press(target, {force:true})presses a disabled element on purpose. -
1d30aa7:
screen.findTextandscreen.waitFormatch the same joined textscreen.snapshotshows (nestedTextis one string), andscreen.findTextalso matches accessibility labels ({labels:false}for text only). A miss lists near misses, including elements that are off screen.screen.snapshotshowschecked,selectedandexpanded(fromaccessibilityState/aria-*, and aSwitch's value) and lists views named only by an accessibility label.Behaviour changes for existing callers:
screen.findTextnow matches the snapshot's text, which collapses whitespace, drops icon glyphs and joins a button's severalTextparts with spaces, soexact:truecompares against that one string. Text on an inactive tab or screen is now not found at all (before: found, withonScreen: 0). Matches also carry afield, andfindTextresults for a miss may carrynearMisses. -
1d5bdae: Tool arguments no longer go wrong silently when a JSON array is spread into them.
query.get,query.refetch,query.list,query.invalidateandquery.unpintake a key flat (["todos", 1]) or wrapped ([["todos", 1]]).query.listaddsisStale,isInvalidated,dataUpdatedAt,errorUpdatedAtandfetchStatus, and takes a key prefix.store.gettakes its path as a dotted string, an array of segments or separate arguments, and a last{ pick: [...] }or{ keys: true }returns only some fields.query.setandquery.pinreject a key that isn't an array. A tool definition can setmaxArgsso a call with extra arguments fails;query.set,query.pin,query.unpinAll,query.restore,store.listandstore.restoredo, other tools are unchanged.Breaking changes:
query.geterrors on a query that isn't cached (with similar keys) instead of returning null.query.refetchreturns{ matched, refetched, data }instead of the data, and errors when nothing matches or nothing can be refetched.- In flows,
steptreats a single array argument as the whole argument list, asagent-bridge calldoes. A step that passed one array to a tool,step('s', 'cart.setItems', [a, b]), now spreads it; wrap it to keep the old behaviour:[[a, b]].callis unchanged.
-
49b8a01: New
@avasapp/agent-bridge/react-native-webviewadapter (optional peer dependencyreact-native-webview, empty stub in release builds)...
v0.3.0
Minor Changes
- f118441: Scenarios: named setups the app defines, such as a locally signed-in user, passed as
useAgentBridge({ scenarios }). A scenario's options are a JSON Schema, andscenario.applyrejects options that don't fit, unknown keys included, before anything changes. An agent lists them withagent-bridge scenariosand applies one withscenario.apply. A flow declares what it needs (export const scenario = 'signedIn'), andagent-bridge runapplies it first and runsbridge.restoreafterwards, even when the flow fails.run --scenario <name>adds one to any flow. A scenario'sonUndocallbacks run after every other restorer.createGateholds off app side effects, such as a realtime connection, while a scenario runs. The network entry adds strict mode (strictNetwork(),net.strict), where a fetch or XHR that no mock answers fails with a 501 (or like offline) and an error naming the request, andmockApi(baseUrl, routes)for fixtures with:paramroutes.runFlowfrom@avasapp/agent-bridge/clientruns a flow module from a script or CI.
v0.2.0
Minor Changes
- fc7cbff: Realtime tools: see, fake and drop realtime messages, and fake connection states (
realtime.channels,log,emit,mute,unmute,connection,restore). Adapters patch an Ably client (@avasapp/agent-bridge/ably) or a socket.io socket (@avasapp/agent-bridge/socket.io); any other library can wrap its listeners withcreateRealtimeTapfrom@avasapp/agent-bridge/realtime.tapListeners,fakeableConnectionandrealtimeAdapterfrom the same import turn a new adapter into a few lines.
Patch Changes
- b6856e4: CDP: when the app's debugger can't run code (Expo Go on Android has no
Runtime.evaluate), say so and suggest--transport expo, instead of reporting that agent-bridge isn't running. - 1d6b463: The agent skill says how to pair agent-bridge with agent-device: agent-device opens the app and covers native UI, real taps and screenshots, and agent-bridge sets up and checks the state inside the app.
v0.1.0
Minor Changes
- a99f57e: Errors the app logs, throws or leaves unhandled come back with the next tool reply (
logson the result,Timed.logsin the client), tagged with the tool they happened during or after.bridge.logsreads the last 200 errors and warnings. The CLI prints them under each call and step;run --strictfails if any came back. - a4a8d4a: New
@avasapp/agent-bridge/networksubpath:networkTools()logs fetch and XMLHttpRequest traffic (net.log,net.clear) and lets agents mock responses, failures and offline (net.mock,net.unmock,net.mocks,net.restore). Apps can register their own mocks, including stateful handlers, withmock()andmockRequests();net.restoreremoves only the agent's. - aade64e: -
bridge.restoreundoes what the agent changed by running every tool named*.restore, in name order, and reports each result or error.- New
query.restore,store.restoreandmmkv.restoreunpin and refetch queries, put back store state from beforestore.set/store.call, and put back MMKV keys from beforemmkv.set/mmkv.delete.
- New
- 05479a5: -
router.currentreturns the current route (pathname, href, params, segments, name, canGoBack). PassrouterTools(router, { navigation: useNavigationContainerRef() }); the navigation tools then return once the route has changed.routerToolsaccepts expo-router'srouterwith typed routes on.
- 1f83e71: - New built-in tools:
screen.snapshotlists the buttons, inputs, text and testID views on screen;screen.pressandscreen.fillcall a control's handlers by testID, label, placeholder or text;screen.waitForwaits for a target to appear or go.pressandfillreturn once React has committed what they caused. Custom tools can do the same with the newsettle()export.
- a244811: - Sessions:
agent-bridge session startconnects to the app once in the background, andcall,toolsandrungo through it with no discovery (about 50 ms per CLI call instead of 650). It reconnects after a reload, andsession stopor 15 minutes idle runsbridge.restoreand ends it.--no-sessionconnects directly;connectSession()does the same from code.
Patch Changes
- f1d5a06: -
screen.findTextworks on React Native 0.8x: it measures through Fabric's UIManager instead of host instances React now creates only for refs. It also measures DOM elements (react-dom, react-native-web) and no longer imports React Native internals, which broke web bundles.query.set,query.pinandquery.refetchreturn after the screen has the data, so a check right after them sees it.- Pins survive tools being rebuilt on every render, so
query.unpinAllstill finds them.