KMPwright

Coming from agent-browser

How agent-browser CLI commands and flags map to page.runCommand (same command grammar) and to the typed Kotlin API, including what is partial, unsupported or intentionally out of scope.

KMPwright adapts agent-browser to a Kotlin library. There are two ways to use what you know:

  1. page.runCommand("…") accepts agent-browser command lines for the core agent loop (open, snapshot, act on @e refs, read, wait). Hand it to an LLM as a single tool.
  2. The typed Kotlin API covers almost every agent-browser command, including the ones runCommand does not parse (network, tabs, state, recording, diffs and more).
// agent-browser                       // KMPwright
// agent-browser open example.com      page.runCommand("open example.com")
// agent-browser snapshot -i           page.runCommand("snapshot -i")
// agent-browser click @e2             page.runCommand("click @e2")
//                                     // or, typed:
//                                     page.locator("@e2").click()

The comparison was made against agent-browser 0.39.0. The machine-readable inventory is docs/compatibility/agent-browser-compatibility.json.

Out of scope

KMPwright is a library you embed. These agent-browser parts are intentionally not included:

  • the CLI executable and its daemon (sessions, --session, --namespace, --idle-timeout, --json, batch, get cdp-url),
  • the MCP stdio server (mcp),
  • executable plugins (plugin, --plugins),
  • the chat gateway (chat, --model),
  • installation management (install, upgrade, doctor, skills).

Your Kotlin code owns browser and page lifetimes, output formatting and tool wiring instead.

runCommand: the supported grammar

page.runCommand(command) parses one command line. Arguments are split on whitespace; single or double quotes group words; a backslash escapes the next character. It returns AgentCommandResult(command, output, snapshot, screenshot): output is plain text for a person or a model; snapshot and screenshot carry structured data when the command produces it. Unknown or malformed commands throw KmpwrightException with a usage hint.

A target is a snapshot ref (@e3, e3, ref=e3) or any selector that page.locator accepts: CSS, text=Save, role=button[name="Save"], label=Email, xpath=….

CommandDoesTyped equivalent
open <url> (aliases goto, navigate)Navigates; adds https:// to bare hosts, http:// to localhost, 127.0.0.1 and 10.0.2.2. Throws when the navigation fails. Output: Opened <url> — <title>.page.goto(url)
backHistory back.page.goBack()
forwardHistory forward.page.goForward()
reloadReloads.page.reload()

Observe

CommandFlagsTyped equivalent
snapshot-i / --interactive, -u / --urls, -c / --compact, -d / --depth <n>, -s / --selector <sel>, -C / --cursorpage.snapshot(interactive, urls, compact, maxDepth, selector, cursor) (named arguments)
screenshot--full / -f, --annotate / -apage.screenshot(fullPage, annotate) (named arguments)

snapshot output is the snapshot text and result.snapshot holds the Snapshot. The text uses agent-browser's format, one element per line; elements that can be targeted start with their ref, children are indented two spaces, and page text is printed as JSON string literals:

@e1 [heading] "Example Domain" [level=1]
@e2 [button] "Submit"
@e3 [input type="email"] placeholder="Email"
@e4 [checkbox] "Remember me" [checked]
@e5 [link] "Learn more" url="https://example.com/more"
  • -i prints only elements with refs (buttons, links, inputs, iframes, and named headings and landmarks) as a flat list; refs nested inside a ref, such as iframe content, are indented.
  • -u adds each link's resolved href as url="…".
  • -c drops structural nodes that contain no refs or values.
  • -C also includes elements that are interactive only through cursor: pointer or onclick.
  • With no output, the text is (no interactive elements) for -i and (empty page) otherwise.

screenshot output is Screenshot <width>×<height>, followed with --annotate by one @eN role: name line per drawn ref; the image is in result.screenshot. No path argument: write result.screenshot!!.bytes yourself.

Act

CommandTyped equivalent
click <target>locator(target).click()
dblclick <target>locator(target).dblclick()
tap <target>locator(target).tap()
hover <target>locator(target).hover()
focus <target>locator(target).focus()
check <target> / uncheck <target>locator(target).check() / uncheck()
highlight <target>locator(target).highlight()
scrollintoview <target> (alias scrollinto)locator(target).scrollIntoViewIfNeeded()
fill <target> <text>locator(target).fill(text). Remaining words are joined with spaces; an empty text clears.
type <target> <text>locator(target).pressSequentially(text)
press <key> [target] (alias key)locator(target).press(key), or page.keyboard.press(key) without a target
select <target> <value>locator(target).selectOption(value) (single value)
drag <source> <target>locator(source).dragTo(locator(target))
scroll <up|down|left|right> [px]page.scroll(ScrollDirection.Down, px); default 300 px, exactly that amount
scroll <top|bottom>page.scrollToTop() / page.scrollToBottom()

Query

CommandOutputTyped equivalent
get title / get urlCurrent title / URLpage.title.value / page.url.value
get text <target>Rendered textlocator.innerText()
get html <target>Inner HTMLlocator.innerHTML()
get value <target>Input valuelocator.inputValue()
get attr <target> <name>Attribute or nulllocator.getAttribute(name)
get count <selector>Match countlocator.count()
get box <target>x=… y=… width=… height=… or nulllocator.boundingBox()
is visible|enabled|checked|focused|editable <target>true / falseisVisible(), isEnabled(), isChecked(), isFocused(), isEditable()

Wait

CommandTyped equivalent
wait <ms>page.waitForTimeout(ms)
wait <target>locator(target).waitFor(SelectorState.Visible)
wait --text <text>page.waitForText(text)
wait --load <load|domcontentloaded|networkidle>page.waitForLoadState(state); dom and idle are accepted as short forms
wait --fn <js>page.waitForFunction(js)

Every wait form accepts --timeout <ms>. The default is 10 000 ms for runCommand waits (not the configured TimeoutConfig). Put the target or milliseconds first: wait @e3 --timeout 5000.

Page

CommandOutputTyped equivalent
eval <js>The value as textpage.evaluate(js).value
console[type] text per messagepage.consoleMessages()
errorsOne error per linepage.pageErrors()
cookies (or cookies list, cookies get)name=value (domain/path) per cookiepage.cookies()
cookies clearpage.clearCookies()
storage local|sessionkey=value per entrypage.localStorage.entries() / page.sessionStorage.entries()
storage local|session clearpage.localStorage.clear() / page.sessionStorage.clear()
helpThe command referenceAgentCommands.HELP

Helpers on AgentCommands: HELP (the reference text, suitable for a tool description), tokenize(command) and normalizeUrl(input).

Not parsed by runCommand

These agent-browser commands are not part of the runCommand grammar. Use the typed API (next section) and, if your agent needs them, expose them as additional tools: find, upload, pdf, read, keyboard, mouse, clipboard, set …, network …, tab, window, frame, dialog, diff …, trace, profiler, pushstate, react, vitals, addinitscript, removeinitscript, auth, state, stream, a11y, webmcp, record, cookies set, storage … set. runCommand also has no --json output mode.

Every command, mapped to the Kotlin API

Status comes from the compatibility inventory: Supported means the documented core behavior has a public entry point; Partial means one exists but backend, lifecycle, output or option semantics differ; Unsupported means there is none.

agent-browserStatusKMPwrightNotes
open / goto / navigateSupportedbrowser.newPage(), page.goto(url)
back / forward / reloadSupportedpage.goBack(), goForward(), reload()
pushstateSupportedpage.pushState(url)Next.js router when present, else history.pushState; origin policy enforced.
clickPartiallocator.click()JVM dispatches trusted CDP input with shadow/label hit testing and cross-process frames. Mobile uses DOM input semantics.
dblclick, focus, hover, check, uncheck, dragSupporteddblclick(), focus(), hover(), check(), uncheck(), dragTo(target)
typeSupportedlocator.pressSequentially(text)
fillSupportedlocator.fill(value)
press / keySupportedlocator.press(key)
keyboardSupportedpage.keyboard.type, insertText, press, down, up
mouseSupportedpage.mouse.move, down, up, wheel, drag
selectSupportedlocator.selectOption(value)Matches value first, then normalized visible label; rejects unmatched or disabled choices before changing anything.
scrollSupportedpage.scroll(direction, amount), locator.scrollIntoViewIfNeeded()
scrollintoview / scrollintoSupportedlocator.scrollIntoViewIfNeeded()
uploadPartiallocator.setInputFiles(paths)JVM uses DOM.setFileInputFiles. Unsupported on mobile WebViews.
highlightSupportedlocator.highlight()
clipboardSupportedpage.clipboard.readText(), writeText(text), page.paste()
findPartialpage.locator(...), getByRole, getByText, …, first(), last(), nth()Shared role/name matching approximates ARIA; not identical to browser-computed accessibility roles and names.
waitPartiallocator.waitFor, page.waitForText, waitForFunction, waitForLoadState, waitForTimeout, waitForURLJVM awaits Promise-based conditions; mobile Promise semantics may differ.
dialogSupportedpage.onDialog { … }
framePartialpage.frameLocator(sel)CDP handles nested cross-origin/process frames; mobile WebViews are limited to same-origin frames.

Observation and content

agent-browserStatusKMPwrightNotes
snapshotSupportedpage.snapshot(...), then page.locator("@e3")CDP follows Chromium's accessibility tree; mobile and includeHidden use bounded DOM traversal.
screenshotSupportedpage.screenshot(...), locator.screenshot()
pdfSupportedpage.pdf(...)Capability is Partial on every backend.
getSupportedpage.title.value, page.url.value, textContent(), innerHTML(), getAttribute(), inputValue(), count(), boundingBox(), evaluate()
isSupportedisVisible(), isEnabled(), isChecked()
evalPartialpage.evaluate(js)Blocked when KmpwrightConfig.allowEval = false or by an action policy.
readSupportedpage.read(url, ...), HttpReader(config).read(url)Independent HTTP client without browser cookies; redirects and links pass origin/domain checks; bounded budgets.
console / errorsSupportedpage.consoleMessages() / page.pageErrors()
diff snapshotSupportedpage.diffSnapshot(name), page.diffSnapshot(before, after)
diff urlSupportedpage.diffUrls(url1, url2)
diff screenshotPartialpage.visualDiff(expected, actual), VisualDiff.compareRgba(...)An API over image bytes, not a baseline-file workflow.
a11yPartialpage.accessibilityAudit(tags, selector)Bundled axe-core 4.14.0. CDP merges isolated frame results; mobile reports skipped child frames.
vitalsSupportedpage.webVitals()Bundled web-vitals 6.2.3.
reactSupportedpage.react (enable, tree, inspect, suspense, startRenderProfile, stopRenderProfile)
webmcpPartialpage.webMcp (discover, context, invoke, awaitInvocation, cancelInvocation)Experimental; CDP only, depends on the Chromium build.

Browser settings

agent-browserStatusKMPwright
set viewportSupportedpage.setViewportSize(w, h), page.setDeviceScaleFactor(scale)
set deviceSupportedpage.emulateDevice(Devices.iPhone15); Devices.all
set geoSupportedpage.setGeolocation(Geolocation(lat, lon))
set offlineSupportedpage.setOffline(true)
set headersSupportedpage.setExtraHTTPHeaders(map)
set credentialsSupportedpage.setHTTPCredentials(HttpCredentials(user, pass))
set mediaSupportedpage.emulateMedia(ColorScheme.Dark)

Network, storage and state

agent-browserStatusKMPwrightNotes
network routePartialpage.route(url) { route -> … }, page.unroute(url)Backend dependent; WebView interception is not full browser-network parity.
network requests / network requestPartialpage.network.requests(filter, limit), detail(id), responseBody(id)At most 500 hops within 4 MiB of metadata, kept across navigations. Mobile response bodies throw unsupported.
network harPartialpage.network.startHar() / stopHar()HAR 1.2. CDP includes available bodies; mobile has partial metadata.
cookiesSupportedpage.cookies(), addCookies(), clearCookies()
storage local / storage sessionSupportedpage.localStorage / page.sessionStorage (entries, get, set, remove, clear)
statePartialpage.storageState().save(path, password), StorageState.load(path, password), page.setStorageState(state), page.autosaveStorageState(...)Authenticated encryption, atomic file replacement; no daemon session protocol.
authPartialAuthManager.save/load/list/delete, page.login(profile)Encrypted profiles; synthetic input; no vault or plugin protocols.
session / profilePartialKmpwrightConfig(cdp = …), launchBrowser(path, userDataDirectory = …) or profileSnapshot = …No daemon session ids.

Tabs, windows and recording

agent-browserStatusKMPwrightNotes
tabPartialbrowser.tabList(), switchToTab(i), closeTab(i), page.openNewTab(url), browser.targets(), attachTarget(id), activateTarget(id), closeTarget(id)Tabs opened from a page inherit its headers, credentials, routes, emulation and init scripts. Attaching to an existing CDP tab does not take ownership.
window newPartialbrowser.newWindow()CDP only.
addinitscript / removeinitscriptPartialpage.addInitScript(script) → id, page.removeInitScript(id)Android depends on the installed WebView supporting document-start scripts.
tracePartialpage.tracing.start() / stop()CDP only.
profilerPartialpage.profiler.start() / stop()CDP only.
recordPartialpage.video.start(path, ...) / stop()CDP + FFmpeg, 1–60 FPS, cursor overlay, contact sheets.
streamPartialpage.screencast(maxFps, quality, ...)A Flow of frames; you supply the display or transport. CDP only.

Unsupported commands

agent-browserWhy
batchNo CLI process; call APIs in sequence.
get cdp-urlNo daemon endpoint to expose.
install, upgrade, doctor, skillsInstallation management of the CLI.
mcp, chat, pluginOut of scope (see above).

Global flags

agent-browser flagStatusKMPwright
--headedPartiallaunchBrowser(path, headless = false) (JVM launch only)
--executable-pathPartiallaunchBrowser(executablePath)
--argsPartiallaunchBrowser(path, arguments = …)
--extensionPartiallaunchBrowser(path, extensionDirectories = …)
--profilePartiallaunchBrowser(path, profileSnapshot = ChromeProfileSource(...)), ChromeProfiles
--ignore-https-errorsPartiallaunchBrowser(path, ignoreHttpsErrors = true)
--webgpu / --no-webmcp / --enablePartiallaunchBrowser(path, webGpu = true, webMcp = false)
--ca-cert / --clear-ca-certPartiallaunchBrowser(path, caCertificatePath = …), read(url, caCertificatePath = …); closing the launched browser removes its trust store
--use-system-caPartialread(url, useSystemCa = …)
--cdpSupportedCdpConfig(websocketUrl)
--auto-connectSupportedCdpConfig(provider = CloudProvider.AutoConnect)
--providerSupportedCdpConfig(provider = CloudProvider.Browserbase | Browserless | BrowserUse | Kernel)
--proxy / --proxy-bypassPartialJVM launches: launchBrowser(path, proxy = ProxyConfig(server, bypass)). No proxy authentication; WebViews have no per-page proxy.
--hide-scrollbarsSupportedCdpConfig(hideScrollbars)
--headersSupportedpage.setExtraHTTPHeaders(map)
--user-agentSupportedKmpwrightConfig(userAgent), page.setUserAgent(ua)
--deviceSupportedpage.emulateDevice(device)
--color-schemeSupportedpage.emulateMedia(scheme)
--download-pathSupportedpage.setDownloadPath(dir)
--init-scriptSupportedpage.addInitScript(script)
--allowed-domainsSupportedpage.setAllowedDomains(list), KmpwrightConfig(allowedOriginRules)
--action-policySupportedpage.setActionPolicy(ActionPolicy(...))
--confirm-actionsSupportedpage.setPolicyConfirmCallback { action, url -> … }
--content-boundariesSupportedpage.boundedContent(), page.boundedSnapshot()
--no-auto-dialogSupportedpage.onDialog { … }
--debugSupportedKmpwrightConfig(debug = DebugConfig(enabled = true))
--annotateSupportedpage.screenshot(annotate = true)
--screenshot-quality / --screenshot-formatPartialpage.screenshot(quality = …, format = …); encoding depends on the backend
--screenshot-dirPartialYou write ScreenshotResult.bytes where you want
--state / --restorePartialStorageState.load(path, password) + page.setStorageState(state)
--restore-savePartialpage.autosaveStorageState(...)
--restore-check-url / -text / -fnPartialCompose your own check with waitForFunction / waitForText
--sessionPartialNo daemon session id; configure each Browser
--enginePartialThe backend is chosen by platform; read page.capabilities().backend
--allow-file-accessPartialOnly platform WebView settings expose related controls
--max-output, --json, --namespace, --session-name, --pin-tab, --confirm-interactive, --idle-timeout, --model, --pluginsUnsupportedCLI, daemon, chat and plugin options; typed APIs return values instead of formatted output

Behavior differences

  • Refs are per snapshot. A ref resolves against the latest snapshot of that page. page.locator("@e3") does not take a new snapshot for you; take one after the page changes.
  • Errors are exceptions. runCommand throws KmpwrightException (or KmpwrightTimeoutException) instead of printing an error and exiting. Catch it and return the message to the model.
  • No implicit session. There is no daemon keeping a browser alive between calls. Keep your Browser and Page for as long as the agent runs, and close them when done.
  • Capabilities vary by backend. The same command can be Supported on JVM and Partial or Unsupported on a mobile WebView. Check page.capabilities() and its notes; see Capabilities.
  • Security flags are APIs. See Security for setAllowedDomains, action policies, confirmation callbacks and content boundaries.

On this page