Build an integration
This page is for developers who build a bridge, remote daemon, device-cloud backend, or in-process runner on top of agent-device. These entry points let your server own device access, command dispatch, or Metro while reusing agent-device's behavior.
To drive devices from your own Node.js code, use the Node.js API instead.
Runnable examples
The repository includes runnable, typechecked examples for these entry points:
Build the package with pnpm build, then run an example with Node:
API reference
Entry points for integrations:
agent-device/android-adbcreateAndroidPortReverseManager(provider)/createAndroidPortReverseManager(executor, { noRebind })captureAndroidLogcatWithAdb(executor, options?)readAndroidClipboardWithAdb(executor)/writeAndroidClipboardWithAdb(executor, text)getAndroidKeyboardStatusWithAdb(executor)/dismissAndroidKeyboardWithAdb(executor)openAndroidAppWithAdb(executor, packageName)forceStopAndroidAppWithAdb(executor, packageName)listAndroidAppsWithAdb(executor)getAndroidAppStateWithAdb(executor)runAdbShell(executor, words, options?)/runAdbExecOut(executor, words, options?)runAndroidShell(device, words, options?)/runAndroidExecOut(device, words, options?)- types:
AndroidAdbExecutor,AndroidAdbExecutorOptions,AndroidAdbProvider,AndroidKeyboardState,AndroidKeyboardDismissResult,AndroidPortReverseEndpoint
agent-device/batchrunBatch(req, sessionName, invoke)
agent-device/remote-configresolveRemoteConfigProfile(options)- types:
RemoteConfigProfile
agent-device/metrobuildBundleUrl(baseUrl, platform)normalizeBaseUrl(baseUrl)resolveRuntimeTransport(runtime)prepareMetroRuntime(options?),reloadMetro(options?),stopMetroTunnel(options)- types:
MetroBridgeDescriptor,MetroTunnelRequestMessage,MetroTunnelResponseMessage
agent-device/contractscenterOfRect(rect)defaultHintForCode(code),normalizeError(error)- types:
DaemonError,DaemonInstallSource,DaemonRequest,DaemonResponse,DaemonResponseData,JsonRpcId,JsonRpcRequestEnvelope,LeaseBackend,SessionRuntimeHints
agent-device/selectorsparseSelectorChain(expression)tryParseSelectorChain(expression)resolveSelectorChain(nodes, chain, options)findSelectorChainMatch(nodes, chain, options)listSelectorChainMatches(nodes, chain, options)formatSelectorFailure(chain, diagnostics, options)isNodeVisible(node)isSelectorToken(token)isNodeEditable(node, platform)- types:
SelectorChain,SelectorDiagnostics
agent-device/findersfindBestMatchesByLocator(nodes, locator, query, requireRectOrOptions)parseFindArgs(args)- types:
FindMatchOptions
agent-device/install-sourceARCHIVE_EXTENSIONSisTrustedInstallSourceUrl(sourceUrl)(deprecated; install sources are not gated on it)validateDownloadSourceUrl(url)- types:
MaterializeInstallSource
agent-device/artifactsresolveAndroidArchivePackageName(archivePath)
agent-device/limrunnew LimrunRuntime(options)runtime.getDeviceSession(device)- types:
LimrunRuntimeOptions,LimrunDeviceSession,LimrunAndroidDeviceSession,LimrunIosDeviceSession,LimrunIosCommandExecution
agent-device/plugins/webdriver- experimental
WebDriverPluginOptionsfor providers using the shared engine.
- experimental
agent-device/plugins- experimental plugin contract:
AgentDevicePluginFactoryandProviderPluginHost; see provider plugins.
- experimental plugin contract:
Android ADB providers
Use agent-device/android-adb when your bridge owns Android device access and you want
agent-device's behavior for ADB operations. Executors receive the arguments that follow adb, so a
remote bridge can route the same argument arrays through an ADB tunnel, websocket API, or another
remote transport.
The helpers take an executor directly. Use captureAndroidLogcatWithAdb(executor, options?) for a
bounded logcat capture.
A provider can also expose reverse to own port reversal. Plain executors do not advertise reverse
support; call createAndroidPortReverseManager(providerOrExecutor)
only when the provider supports adb reverse argument semantics. The manager makes duplicate setup
idempotent for the same owner and rejects conflicting owners for the same local endpoint. For a
device that other adb clients also drive, pass an executor with { noRebind: true }: the manager
runs adb reverse --no-rebind and never replaces an existing device mapping, including one it
created. When adb reverse --list shows the mapping, the refusal fails with COMMAND_FAILED and
details.reason: 'android_port_reverse_rebind_refused'. Otherwise it fails as an ordinary adb error.
The device shell re-parses whatever follows shell or exec-out, so those commands are built for you:
every dynamic word is rendered for the quoting its transport applies before it reaches the device. adb
forwards words verbatim, so a word is single-quoted; hdc wraps each element it sends in double quotes,
where $, a backquote, and " stay live, so a word is escaped for that context instead. An array that
begins with shell or exec-out and did not come from those builders is refused with INVALID_ARGS and
details.reason: 'unguarded-device-shell-argv' instead of being dispatched. A bridge that composes its
own device commands calls runAdbShell(executor, words, options?) or
runAdbExecOut(executor, words, options?) from agent-device/android-adb, passing each value as its
own word; runAndroidShell(device, words, options?) and runAndroidExecOut(device, words, options?)
resolve the executor from a device instead.
Batch orchestration for custom transports
Use agent-device/batch when a bridge or in-process runner receives daemon-shaped requests but owns command dispatch itself. The helper applies the same validation, inherited flags, serial execution, partial results, and error envelopes as the daemon batch command.
Full example from examples/sdk/batch-orchestration.ts:
Remote Metro helpers
Use agent-device/remote-config for profile loading and path resolution, agent-device/metro for Metro preparation, reload, and tunnel lifecycle, and agent-device/contracts when a server consumer needs daemon request or runtime contract types. For bridged remote Metro, proxyBaseUrl is the bridge origin and publicBaseUrl is optional; the bridge descriptor supplies cloud iOS wildcard HTTPS hints and Android runtime-route hints. reloadMetro() calls Metro's /reload endpoint, matching the terminal r reload path for connected React Native apps.
stopMetroTunnel() releases a companion once per consumer, so it only returns once the companion is gone when it releases the last one. If that companion belongs to another user — you started it with sudo, for example — the call fails with its pid, and the profile keeps tracking it so a later stop retries the same process instead of starting a second companion beside it. Stop the surviving process as its owner (sudo kill <pid>) and run stopMetroTunnel() again.
Selector helpers
Use agent-device/selectors to parse and match selector expressions in a remote daemon or bridge. The role= term matches the platform-neutral kind vocabulary that snapshot --json publishes (see Snapshots): pass the nodes as captured, and a node whose kind is text always matches role=text. Separately, the deprecated raw leaf spellings (statictext, edittext, textview, …) also match, each on the nodes that actually carried that class. A leaf spelling matches beside such a node's kind, not in place of it — role=webarea resolves a macOS-helper AXWebArea node whose kind is axwebarea, for example. Matching stays platform-aware because editability checks differ by backend.
listSelectorChainMatches(nodes, chain, options) returns every node the winning selector alternative matches, in snapshot order, plus that alternative and its index — the same first-match domain findSelectorChainMatch uses, without uniqueness refusal, so a runner applies its own strictness to the same nodes the CLI matched. resolveSelectorChain can name a later alternative: by default it refuses an ambiguous one and keeps walking, so the indices agree when it passes requireUnique: false, when the first matching alternative is unique, or when disambiguateAmbiguous: true resolves that alternative in place. It returns null when no alternative matches. findSelectorChainMatch and listSelectorChainMatches take { platform, requireRect? }; resolveSelectorChain also takes requireUnique? (default true) and disambiguateAmbiguous?. The matched SnapshotNode objects are the ones passed in.
