Releases: Crawlio-app/crawlio-browser
Release list
v1.9.5 — background tabs now accept input
Clicking, typing and dragging on a tab the user is not looking at did nothing at all. This
release makes background operation actually work.
The defect
connect_tab({background: true}) correctly declined to steal focus — that part always worked.
What did not: a tab whose visibilityState is hidden does not process dispatched mouse or
key events. The command went out, nothing errored, and the page never saw it.
A comment in the extension asserted "CDP input still reaches a background tab", citing
browser_scroll and agent sessions as evidence. Both do work — and so do Runtime.evaluate,
hover and type. Click, double-click, press_key and drag do not. The claim generalised from the
mechanisms that survive backgrounding to the ones that don't, and nothing caught it because every
check looked at return values rather than effects.
The discriminator is visibilityState, not focus. Input to an unfocused but rendered tab always
worked, which is why this reproduced only on a tab created in the background that had never been
foregrounded.
The fix
Two independent causes, found by separating them:
- The target was outside the viewport.
DOM.scrollIntoViewIfNeededis a no-op on a tab
Chrome is not rendering, and does not report that it did nothing — so the in-page fallback,
which only ran in acatch, never fired. Scrolling is now verified rather than trusted. - The real blocker: even at correct, hit-testable coordinates the click still didn't
register. Background connections now enableEmulation.setFocusEmulationEnabledand
Page.setWebLifecycleState({state:"active"}), which is what makes a non-rendered page accept
dispatched input. Both are best-effort no-ops where unsupported.
Verified against a real browser
On a genuinely hidden tab: click, double-click, hover, type, press_key, select_option, drag,
scroll and a 183KB screenshot all work — and the user's active tab never moves. Two agent sessions
drive separate tabs with independent actions, snapshots and batches. 24/24 in the new session
suite; the existing stress suite is unchanged at 29/0 and 31/0, with cold start down from 24s to
3s.
Also added
get_capabilities now reports extensionVersion and buildId. An unpacked extension does not
hot-reload, and the manifest version does not change between rebuilds within a release — so there
was no way to tell whether Chrome had loaded newly built code. That ambiguity cost a full debugging
cycle chasing a fix that had never been loaded.
tests/e2e-sessions.mjs covers background operation, multi-session isolation, tab navigation
and the full input battery, asserting throughout that the active tab never changes.
Upgrading
npx crawlio-browser@latest init. The fix is in the extension — take the build from the release
assets and reload it, then confirm buildId in get_capabilities matches.
v1.9.4 — clicks that reported success without landing
A click on an element below the fold reported success and did nothing. This release fixes that,
and adds the harness that found it.
The defect
prepareElementForInteraction scrolled only when isElementVisible said no — and that check
asks whether an element is rendered: display, visibility, opacity, and a non-zero rect. A
button 1298px down an 800px viewport passes all four. So no scroll happened, getElementCenter
returned its box-model coordinate, and Input.dispatchMouseEvent interpreted that as a viewport
coordinate and dispatched at a point outside the viewport.
Nothing errored. The command returned {action:"click", selector, x, y, snapshot} — a success
shape, complete with coordinates, for a click that never landed.
Measured on the stress fixture before the fix: a raw bridge.send click reported y=1298 and the
page's own click counter stayed at 0, while smart.click reported y=468 and reached 1.
smart.click differed only because pollActionability happened to scroll first — so the same
command worked or silently missed depending on which path reached it.
The fix
A new isElementInViewport check tests the element's centre, because that is the exact point
a click is dispatched at — a half-visible element's centre is what decides whether the click
lands. prepareElementForInteraction now scrolls when a target is either not rendered or not
reachable.
That covers every handler routed through withFreshElement: click, double-click, hover, type and
the rest, in both modes and through raw bridge.send. After the fix all three paths report
y=468 and each call registers exactly one click — verified with a probe confirming a single call
does not double-fire and that clicking a neighbouring element leaves the counter untouched.
Also fixed
Six tools were missing from the code-mode invocation hints. detect_tables and extract_table
are server-composed and need their smart.* helpers; the four robot_training_* tools have
neither a bridge command nor a smart.* binding and are full-mode only. Code-mode callers who
found them through search got no hint and hit "Unknown command".
New: the stress harness
tests/e2e-stress.mjs drives one matrix through both code mode and --full against a real
browser and a deterministic fixture page. Every case asserts an effect rather than an
acknowledgement — a click is verified by the DOM changing, typing by the field holding the value.
That distinction is the whole point, and it is what caught this defect. It passes 29/0 in code
mode and 31/0 in full mode.
It is not part of npm test: it needs Chrome with the extension loaded, and belongs in the
release checklist as a manual pre-submission gate.
Known characteristic, not fixed
A second server spawned into an already-running session takes around 24 seconds before its first
command can succeed. The single-bridge election is deliberate — it refuses to cut over to a rival
while its incumbent is verified, so a rogue local server cannot hijack a live session — but it
does mean a client's first tool call inside that window times out.
Upgrading
npx crawlio-browser@latest init. If you use the Chrome extension, take the matching build from
the release assets; the fix is in the extension, not the server.
v1.9.3 — a skill that granted nothing, and four other silent wrongs
A second review pass found five defects the first one missed. One of them meant a shipped skill
had no permission to do its job.
Fixed
The robot-training skill granted nothing. Eight of its eleven allowed-tools entries used the
retired mcp__crawlio-agent__ prefix, but init writes the crawlio-browser config key — so
those entries named tools that do not exist. The skill loaded, read correctly, and silently had
no permission to run robot training or agent sessions. That is the worst shape a bug can take:
nothing errors, the capability is simply absent. npm run check:surface now fails on any skill
referencing the retired prefix.
The install wizard's closing line was wrong on both branches. It reported (114 tools) for
full mode and (3 tools, 147 commands) for code mode. The truth is 145, and 6 tools over a
178-command catalog. It is the last thing an installer reads, and it now comes from the same
builders as everything else.
smart.woocommerce was missing from the execute tool description, which enumerated 16 of
the 17 framework namespaces — so a model reading its own instructions had no way to learn the
namespace existed.
The README documented smart.screenshot() twice, in the core-methods table and in a worked
example, although it does not exist and the execute description says so outright. The seventh
core method is smart.rebuild(); the example now uses bridge.send({ type: 'take_screenshot' }).
Links to the archived crawlio-browser-agent repository in the shipped skill reference and
the plugin manifest now point at the live repository.
Why these survived the first pass
Every one of them is a claim that no test could contradict, because nothing executed them: a
permission grant naming a tool that does not exist, a summary string, a description listing
namespaces, a documented method. They are exactly the class the surface check was built for, so
the check grew to cover permission prefixes alongside counts — and both new guards were verified
by planting the failure and watching them catch it.
Upgrading
npx crawlio-browser@latest init. If you use the robot-training skill, this release is the one
that makes it work.
v1.9.2 — one name, measured numbers, a verifiable tool surface
One name, and numbers that are measured rather than typed.
The problem this release fixes
The README carried four different tool counts at the same time. It said "3 tools" where a client
sees 6, claimed "8 higher-order methods" in three places and "17" in a fourth when there are 18,
said the detector recognized 17 frameworks on one line and the correct 64 on another, and gave
bridge "133 commands" — a number matching nothing measurable. A section titled "All 145 tools"
listed 92. Worse, the server itself printed exposing all 114 tools while exposing 145.
None of that was dishonesty. It was arithmetic maintained by hand across 900 lines of prose,
which is a losing proposition.
What changed
Counts are derived. describeSurface() invokes the real tool factories with an inert bridge
and measures what they return. npm run check:surface verifies every marked number in the README
against what the server reports over JSON-RPC, and fails CI when they disagree.
crawlio-browser tools prints the exact surface each mode exposes — --full for all 145,
--json for a machine-readable form. It runs with no browser, no extension and no network.
Installing this server hands an agent the debugger permission on a browser holding live sessions;
being able to read what it exposes before configuring a client is a reasonable thing to ask for.
The token-reduction claim was wrong and is now measured. "~95% schema token reduction" was an
estimate. Serializing the tools/list payload each mode actually returns gives 83% — code
mode is not free, because execute and search carry long descriptions. 83% with a stated
method beats 95% with none.
One name: Crawlio Browser. The README's title said "Crawlio Agent" directly above an npm badge
and install command that both said crawlio-browser. The package, CLI, server name and repository
already agreed; only the prose had not caught up. The Chrome extension keeps its Web Store name,
Crawlio for Chrome, and is described as the browser-side component.
Three identifiers deliberately did not move, because each is load-bearing: the
<meta name="crawlio-agent"> opt-out is a protocol sites have already deployed, the dual
config-key check is the backward-compatibility path, and the macOS app bundle path is written
into client config files — renaming it would break every install pointing there.
Method Mode is the one coined term. The runtime it sits on is described rather than branded.
--help and --version no longer start a server. Both fell through to the transport setup,
so asking the CLI its version bound a WebSocket port and left a process running.
The MCP registry listing is now publishable. server.json had a description over the 100-char
limit and a deprecated schema, and the published package was missing the mcpName field the
registry uses to verify npm ownership — all three would have failed submission.
Upgrading
npx crawlio-browser@latest init — or just update the version your client pins. No configuration
changes are required, and no on-disk paths moved.
v1.8.0
Crawlio Browser is now open source under Apache-2.0 at
Crawlio-app/crawlio-browser —
the full MCP server and Chrome extension source, not just distribution artifacts.
The selector kernel in packages/selectors remains MIT (it ports MIT and
BSD-3-Clause code); see NOTICE and THIRD_PARTY_NOTICES.md.
Added
detect_sections— live-DOM region perception. Returns a depth-capped tree of page
regions (semantic tags, ARIA landmarks,data-component/data-testidattributes, and
PascalCase/BEM class hints) with a verified selector, bounding box, interactive-element
count, and text density per region. Regions scope perception; pair with
browser_snapshot({ selector })to mint interaction refs inside a chosen one- Table detection upgrades — a native
<table>fast path that promotes<thead>cells
to real column names (previously the header row was lost entirely), geometric consistency
filters on the repeated-block path, and normalizedconfidence/strategy/warnings
on every candidate so an agent can threshold results instead of reading a raw score - Semantic column naming in
extract_table— link-aware columns (title,title_url)
instead of DOM-path keys - Structured
problemerror codes — every failure now carries a machine-readable code
(disconnected,target_closed,permission_denied,timeout, …) alongside the message,
so callers branch on cause rather than parsing prose - Idle debugger release (opt-in) — after a configurable quiet period the debugger detaches
so Chrome's "being debugged" banner clears; the next command re-attaches and restores the
session, including the injected scripts only the capture path installs. Configure with the
newset_idle_release/get_idle_releasetools (minimum 60s;idleMs: 0
disables). It never fires during a recording, an active agent session, a pending dialog,
installed request-interception rules, active coverage, or a pinned frame
Fixed
pick_elementand robot-training were broken in every shipped build — both install
their in-page kernel via a singlebrowser_evaluate, but the extension hard-rejected any
expression over 10,000 characters and the kernel prelude is ~17KB, so both failed 100% of
the time. The cap is now a shared constant (32,768) with a regression test asserting the
prelude fits- CDP disconnect recovery re-enabled nothing — recovery rebuilt commands from stored
domain names that already included.enable, producingPage.enable.enable, and dropped
the parametersPerformance.enablerequires. Every replay failed silently, so after any
mid-session disconnect network and console capture stopped without an error - Stale bridge sockets could permanently unverify their replacement — a zombie socket's
lateonclosecleared the handshake state and identity timer belonging to a newer socket
on the same port, leaving it refused-but-never-dropped in token mode - Domain state was last-writer-wins —
ensureDebuggerandstartNetworkCaptureeach
overwrote the other's record, so recovery could re-enable a subset of the live domains - Per-port reconnects had no backoff — a flapping server produced a fixed 3s connect/close
loop; now exponential to a 30s cap, reset on a successful open - Errors thrown inside
execute()lost their structure — the sandbox boundary flattened
them to a bare message, discarding permission and problem details - Selectors were embedded into generated page JavaScript with
JSON.stringifyalone,
which leaves U+2028 and U+2029 raw. Both are valid JSON but are line terminators in
JavaScript source, so the programs were at the mercy of the parsing engine; they are now
escaped, and the injection tests execute a hostile selector rather than grepping for it - Offline-queue rejections carried no problem code — the failure a user hits whenever
the extension is closed was indistinguishable from any other error npm ci && npm run typecheckfailed on a fresh clone, because the two workspace
packages arefile:dependencies whosemain/typespoint at uncommitteddist/output
and neither had apreparescript. This broke CI and any new contributor's first build- The published npm package omitted
NOTICE, which Apache-2.0 §4(d) requires
redistributions to carry — so the tarball claimed Apache-2.0 while dropping the attribution
notice it depends on, including the Chromium BSD-3 and Selector Forge credits - Two declared problem codes were never emitted.
opt_outwas dead: all twelve
site-opt-out refusals return a response literal directly, bypassing the only place the
classifier runs. Four bridge rejections were untagged as well — a cleared queue, a
drain-side resend timeout, a disconnect inside the reconnect grace window, and a stopped
bridge
Changed
export_session_rawnow writes to~/.crawlio/<domain>-session.json(previously a
different home directory). PassoutputPathto choose your own location; it must still be
inside your home directory- Minimum
@modelcontextprotocol/sdkraised to 1.25.0. The server statically imports
@modelcontextprotocol/sdk/server/express.js, a subpath that does not exist before 1.25 —
the previous^1.8.0floor allowed an install that crashed at startup with
ERR_MODULE_NOT_FOUND, in stdio mode as well as portal mode - Relicensed from proprietary to Apache-2.0;
package.json,server.json, and README
repoint to the new public repository persistStatefailures are now logged instead of silently swallowed- CI verifies the extension bundle builds and that versions stay in sync across
package.json, both manifests, andserver.json CRAWLIO_RAW_LANEreplacesCRAWLIO_RE_LANEas the documented switch for the
raw-capture lane, andCRAWLIO_*_MODE=raw|trafficreplaces=re. The old spellings still
work and will be removed in the next major, so no configuration breaks today- Adds ESLint and oxlint, wired into CI as separate steps. One project-specific rule enforces
that every debugger attach goes throughattachDebugger(), so a tab can never end up
attached but untracked. Prettier is deliberately absent - Adds
.editorconfig,.gitattributes(pinning LF so a Windows checkout cannot rewrite
shell scripts),SUPPORT.md, issue and pull-request templates, Dependabot, and CodeQL - CI pins every third-party action to a commit rather than a mutable tag,
.npmrcholds new
dependency versions for a week before they may enter the lockfile, and a version gate fails
any pull request that changes shipped files without moving the version forward - The public repository is produced by a scripted, allowlist-based export whose sanitation
gate is itself covered by tests that plant each internal directory and require rejection