Skip to content

Releases

@avasapp/agent-bridge@0.6.0

Choose a tag to compare

@crockalet crockalet released this 12 Nov 09:12
5a3348d

Minor Changes

  • 1e989d3: screen.snapshot takes {format: "text"} for one line per element (button #save "Save plant" disabled, with index=N on lines that repeat and @x,y on 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 --out with {"format":"text"} writes the text (--json with it is an error).

    Behaviour change: agent-bridge call screen.snapshot and the MCP screen_snapshot tool now print the text form by default. For the old JSON pass --json (or --out) to call, or {"format": "json"} as the argument (args: [{"format": "json"}] over MCP). connect(), bridge.call and flows still get JSON unless they ask for text.

  • 6534b6b: New opt-in @avasapp/agent-bridge/device entry with deviceTools({ NetInfo, Notifications, navigation, time }). device.openUrl delivers an incoming deep link to the app's Linking listeners (and makes getInitialURL return it), returning the route the app ended on when you pass the navigation container. device.appState fires AppState change listeners and sets AppState.currentState. device.offline makes every fetch and XHR fail like no network, mocks included, and tells NetInfo; requests it fails show in net.log as (device offline). device.notification calls expo-notifications' received or (with {tapped: true}) response listeners. device.setTime (with time: true) shifts Date.now() and new Date(). device.restore undoes all of it and reports restored: true if anything was undone, and bridge.pending lists what is set. The Ably and socket.io adapters now also log what the app sends (channel.publish, socket.emit) in realtime.log with sent: true; socket.emit is logged once per call as its argument list, payloads are copied and cut, and credential-looking keys show as [redacted] unless realtime.log gets {full: true}; createRealtimeTap gets sent() and @avasapp/agent-bridge/realtime exports tapOutgoing for custom taps.

  • 08aff63: agent-bridge run now 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. --bail stops at the first failure, --grep <text> picks flows by name or by path relative to where you ran, --keep leaves the app as a flow left it, and --reporter json|junit writes a report for CI: on stdout (with step lines and a flow's console.log on stderr) or to a file with --out <file>, which needs --reporter. A flow file that won't load, a failed connect and a --grep with 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 before run; pass --keep to skip it. runFlow does the same ({ keep: true } opts out). A flow's own step('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 restore step. If the app stops answering, the error is printed at once and the remaining flows are skipped with that reason.

    Flows get an expect helper (expect.text, expect.gone, expect.route, expect.result) that fails with a readable message (expect.result takes a RegExp to match a string and rejects Date, Map and other class instances), and a failed flow prints what was on screen (screen.snapshot and router.current).

  • 97ff9e0: agent-bridge mcp runs 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.snapshot is screen_snapshot) that takes args, an array of its arguments, and full. It uses the project's session (and stays on that one, never another server's mcp-* session), or starts one and runs bridge.restore when the server exits (--keep skips that). If its session is stopped, the next call says so once and the call after that uses a new session. It sends notifications/tools/list_changed whenever the list a client received is out of date, including when the app starts after the agent, and always lists an agent_bridge_status tool that says why it is not connected. Results over 32 KB come back as a summary unless full is true, screen.capture images are sent as image content by their real type, and session errors such as may or may not have run pass through unchanged. See the CLI docs.

  • 2f8a9eb: Network mocks can match more exactly. net.mock takes {path: "/orders/:id"} (the whole path on any host, query ignored; :name is one segment, * the rest), {body: {...}} (the JSON request body contains these keys) and {graphql: "GetUser"} (the request's operationName, from the JSON body or ?operationName=), alone or with url and method. A bare string still matches any URL containing it, and net.mock and net.mocks now return matches, the mock in words. An unknown key in a match is now an error instead of being ignored. net.mockFromLog on 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 a warning. 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.entry now 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 (so apikey, x-auth-token and x-amz-security-token too). Authorization keeps only its scheme word, the rest at most 4 characters and a quarter of the value. net.waitFor waits 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 after send() is no longer sent when no mock answers it, and one aborted before send() is sent as normal. path also treats :name as a parameter only as a whole segment, takes a trailing slash either way, matches / on a bare host, and tries the percent-decoded URL. A body that is empty once serialised ({a: undefined}) is refused. A body or GraphQL mock no longer makes fetch(Request) calls with a binary body wait for a body read. A handler mock with times: 1 now answers one of two requests in flight, and shadows skips mocks that have times.

  • d785078: screen.press no 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 says via: "ancestor". screen.fill now works on uncontrolled inputs (including Fabric and value: undefined), fires onEndEditing, 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's element.value is what was written, and it carries a warning if the text could not be written natively. router.navigate, push and replace now 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/native without expo-router: navTools(navigationRef) from @avasapp/agent-bridge/react-navigation. It adds nav.navigate, nav.push, nav.replace, nav.popTo, nav.back, nav.popToTop and nav.current. Each navigation tool waits for the new screen and returns the route the app ended on (name, params, path, stack, canGoBack), adds a warning when 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.push and nav.replace throw when the focused navigator is tabs or a drawer, and nav.popToTop does nothing at a stack's first screen instead of popping a parent. In React Navigation 7, nav.navigate pushes unless the screen is already focused; use nav.popTo to return to an earlier one. Pass getStateFromPath and a linking object with a config to let nav.navigate take a path like /post/7 or a URL with one of linking.prefixes; apps with a static config or linking: { enabled: 'auto' } should use route names. The tools are named nav.* so they do not clash with router.*. init now writes a commented navTools(navigationRef) line for these apps, replacing the old sample tools.

  • 840cd82: Two new adapters. @avasapp/agent-bridge/redux (reduxTools(store, {redact})) gives redux.list, get (the state or a path, with pick and keys), dispatch (a plain {type, payload} action), set (merge into a slice), commit and restore. Wrap your root reducer in `bridg...

Read more

@avasapp/agent-bridge@0.4.2

Choose a tag to compare

@crockalet crockalet released this 06 Oct 06:14
fa6c040

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. --device matching 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.waitFor takes {scroll:true}, like press and fill: a match that is rendered but off screen (below the fold of a ScrollView) 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

Choose a tag to compare

Patch Changes

  • 71c82e2: Fixes release builds of apps using useWebViewTools from @avasapp/agent-bridge/react-native-webview losing their own injectedJavaScriptBeforeContentLoaded, onLoadStart, onLoadEnd and onNavigationStateChange. The README says to pass these as hook options rather than WebView props, but the release stub ignored its options and returned empty props, 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 in props (leaving out any not set, and without injectedJavaScriptBeforeContentLoadedForMainFrameOnly), so the WebView behaves as if the adapter weren't there; wrap still 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

Choose a tag to compare

Upgrading from 0.3

These change what existing calls return or accept; details are in the entries below.

  • query.get errors on a key that isn't cached instead of returning null; query.refetch returns { matched, refetched, data } instead of the data.
  • In flows, step(label, tool, [a, b]) now spreads a single array as the argument list, like agent-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.findText matches the snapshot's joined text.
  • agent-bridge call prints results over 32 KB as a summary unless --full or --out is given; a bare argument starting with @ is a file path; several argument words are each an argument.
  • net.mocks cuts response bodies over ~2 KB ({ full: true } for whole ones).
  • store.set and store.call return 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.json reads the arguments from a file (@- for stdin), so a value over the shell's ~128 KB argument limit no longer fails with Argument 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.json is that file's JSON (call net.mock '"/feed"' @feed.json). call --batch lines accept @file as 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"'), and call with several argument words now sends each as an argument instead of ignoring all but the first.

  • 9ba7799: CLI: agent-bridge call --batch reads tool args lines 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), and agent-bridge repl is an interactive prompt on the same loop with history, tab completion and .time/.tools/.pending/.restore dot commands. With stdin not a terminal, repl behaves 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; call itself is unchanged.

  • b722069: query.get takes a last { pages: [from, to] } to return only those pages of an infinite query (with totalPages), or { path: "pages.0.items" } to return only the value there. net.mocks now cuts response json and body over ~2 KB like net.log does and marks the mock truncated: true; { full: true } returns them whole (behaviour change for anything reading big mock bodies from net.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 call has --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 --full or --out is given; behaviour change for scripts that parse big call output (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) and net.log takes { since }. net.entry <id> and net.log { full: true } return whole bodies instead of ~2 KB, and net.mockFromLog <id> [patch] turns a logged response into a mock, deep-merging a patch. net.mock documents that the newest mock wins, takes options.priority (higher first, negative for a fallback) and reports shadows when 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, query comes back with the next call, failed or not (agent-bridge call prints it as a warning, timed() has it as notice, AgentBridgeCallError as notice), in agent-bridge run output, and again in session stop. Every reply carries the id of the JS bundle load and the areas with pending undo state (pending on a *.restore tool), 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. New app.reload tool reloads the app's JS (through Expo's reloadAppAsync when available, see the patch note below); a session never runs it twice and waits for the new bridge.

  • bed11f0: Show what bridge.restore will undo and let apps add their own undos. New bridge.pending lists each area's pending restore (for stores, the snapshot against the current value, redacted and truncated). session stop --dry-run prints the same and keeps the session; session stop --keep warns about snapshots still pending. New store.commit drops a store's snapshot so the current value becomes the baseline. New onRestore(fn, label?) export registers an undo from an app tool; bridge.restore runs it once via the new app.restore, and bridge.pending lists it under app. The store tool descriptions now say when the snapshot is taken.

  • 68142d5: New router.dismiss and router.dismissAll tools 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 a modal.close tool for those. RouterLike gains optional dismiss, dismissAll and canDismiss; a router without them makes the two tools throw a clear error.

  • de337cf: New screen.capture tool takes a PNG screenshot and returns {path} (or {base64} with {base64:true} when small) using react-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 io or adb exec-out screencap -p. Closes #11.

  • 1c99220: screen.snapshot, screen.findText, screen.press, screen.fill and screen.waitFor now see only the focused screen: unfocused tabs and native-stack screens, content under an open modal, display: none and 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. New screen.scroll (to a target, {toEnd}, {toStart}, {by}, optionally {within}) and screen.refresh (calls the RefreshControl's onRefresh), and {scroll:true} on screen.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.press and screen.fill reject 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.findText and screen.waitFor match the same joined text screen.snapshot shows (nested Text is one string), and screen.findText also matches accessibility labels ({labels:false} for text only). A miss lists near misses, including elements that are off screen. screen.snapshot shows checked, selected and expanded (from accessibilityState / aria-*, and a Switch's value) and lists views named only by an accessibility label.

    Behaviour changes for existing callers: screen.findText now matches the snapshot's text, which collapses whitespace, drops icon glyphs and joins a button's several Text parts with spaces, so exact:true compares against that one string. Text on an inactive tab or screen is now not found at all (before: found, with onScreen: 0). Matches also carry a field, and findText results for a miss may carry nearMisses.

  • 1d5bdae: Tool arguments no longer go wrong silently when a JSON array is spread into them. query.get, query.refetch, query.list, query.invalidate and query.unpin take a key flat (["todos", 1]) or wrapped ([["todos", 1]]). query.list adds isStale, isInvalidated, dataUpdatedAt, errorUpdatedAt and fetchStatus, and takes a key prefix. store.get takes 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.set and query.pin reject a key that isn't an array. A tool definition can set maxArgs so a call with extra arguments fails; query.set, query.pin, query.unpinAll, query.restore, store.list and store.restore do, other tools are unchanged.

    Breaking changes:

    • query.get errors on a query that isn't cached (with similar keys) instead of returning null.
    • query.refetch returns { matched, refetched, data } instead of the data, and errors when nothing matches or nothing can be refetched.
    • In flows, step treats a single array argument as the whole argument list, as agent-bridge call does. 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]]. call is unchanged.
  • 49b8a01: New @avasapp/agent-bridge/react-native-webview adapter (optional peer dependency react-native-webview, empty stub in release builds)...

Read more

v0.3.0

Choose a tag to compare

@crockalet crockalet released this 28 Sep 04:59
5f450ef

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, and scenario.apply rejects options that don't fit, unknown keys included, before anything changes. An agent lists them with agent-bridge scenarios and applies one with scenario.apply. A flow declares what it needs (export const scenario = 'signedIn'), and agent-bridge run applies it first and runs bridge.restore afterwards, even when the flow fails. run --scenario <name> adds one to any flow. A scenario's onUndo callbacks run after every other restorer. createGate holds 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, and mockApi(baseUrl, routes) for fixtures with :param routes. runFlow from @avasapp/agent-bridge/client runs a flow module from a script or CI.

v0.2.0

Choose a tag to compare

@crockalet crockalet released this 28 Sep 05:16
b5b6146

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 with createRealtimeTap from @avasapp/agent-bridge/realtime. tapListeners, fakeableConnection and realtimeAdapter from 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

Choose a tag to compare

@crockalet crockalet released this 24 Sep 12:19

Minor Changes

  • a99f57e: Errors the app logs, throws or leaves unhandled come back with the next tool reply (logs on the result, Timed.logs in the client), tagged with the tool they happened during or after. bridge.logs reads the last 200 errors and warnings. The CLI prints them under each call and step; run --strict fails if any came back.
  • a4a8d4a: New @avasapp/agent-bridge/network subpath: 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, with mock() and mockRequests(); net.restore removes only the agent's.
  • aade64e: - bridge.restore undoes what the agent changed by running every tool named *.restore, in name order, and reports each result or error.
    • New query.restore, store.restore and mmkv.restore unpin and refetch queries, put back store state from before store.set/store.call, and put back MMKV keys from before mmkv.set/mmkv.delete.
  • 05479a5: - router.current returns the current route (pathname, href, params, segments, name, canGoBack). Pass routerTools(router, { navigation: useNavigationContainerRef() }); the navigation tools then return once the route has changed.
    • routerTools accepts expo-router's router with typed routes on.
  • 1f83e71: - New built-in tools: screen.snapshot lists the buttons, inputs, text and testID views on screen; screen.press and screen.fill call a control's handlers by testID, label, placeholder or text; screen.waitFor waits for a target to appear or go.
    • press and fill return once React has committed what they caused. Custom tools can do the same with the new settle() export.
  • a244811: - Sessions: agent-bridge session start connects to the app once in the background, and call, tools and run go through it with no discovery (about 50 ms per CLI call instead of 650). It reconnects after a reload, and session stop or 15 minutes idle runs bridge.restore and ends it. --no-session connects directly; connectSession() does the same from code.

Patch Changes

  • f1d5a06: - screen.findText works 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.pin and query.refetch return after the screen has the data, so a check right after them sees it.
    • Pins survive tools being rebuilt on every render, so query.unpinAll still finds them.