ego-lite Documentation

The fastest browser for AI agents to run browser automation, built for sharing your logged-in browser state with your AI agents, like Codex or Claude Code,...

RAW Doc

Technical Documentation: citrolabs/ego-lite

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

ego (lite) is a browser where you and your AI agents work in parallel. Your agents run their browser tasks in their own Spaces, isolated workspaces inside the same browser, while you keep browsing in yours, so no agent ever takes the browser away from you. And the automation itself finishes faster, on fewer tokens.

Existing tools like browser-use and agent-browser are a bridge to the browser, not a browser of their own: they need a separate one to drive, your browser data rarely carries over intact, the connection is unstable, and you and the agent end up fighting over control of the browser. ego lite is one browser designed from the start for the two of you to share. No extra setup, and the agent can always reach your real logins and tabs through ego-browser.

Demo

https://github.com/user-attachments/assets/ffe7954b-58ee-411e-b35d-ec30c58a08bc

Quick Start

ego lite runs on macOS today, a Windows closed beta is coming soon, and Linux is on the roadmap.

1. Install

Pick whichever fits your flow.

1.1 Download the macOS app


Click to download, then open it to install. Either way, ego lite adds the ego-browser skill to every agent's skills directory on your machine.

1.2 Add the skill with npx

Install just the ego-browser skill:

bash
npx skills add citrolabs/ego-lite

The first time your agent runs a browser task, it walks you through installing the ego lite app.

1.3 Let your agent set it up

Paste this into your agent:

text
Set up ego lite for me: https://github.com/citrolabs/ego-lite

Read `skills/ego-browser/references/install.md` and follow the steps to install ego lite.

On first launch, ego lite asks one question, whether to migrate your Chrome data. Say yes and your agent inherits your existing logins, cookies, extensions, and bookmarks.

2. Try your first task

In your agent CLI, type /ego-browser followed by a space, then describe what you want in plain language:

text
ego-browser follow @ego_agent on x.com for me

The agent picks up the ego-browser skill, opens the page in its own Space, reads a Snapshot (the page turned into structured text), acts on the page, and reports back, all while your own tabs stay untouched.

Your browsing data, cookies, and everything else the browser holds stay on your device. ego lite keeps its data collection deliberately narrow: simple product signals, like whether you've set ego lite as your default browser.

Highlights of ego lite

Feature What it does
Code-based, not CLI-based: faster runs on fewer tokens for complex tasks The capabilities ego lite exposes to the agent are wrapped as JavaScript functions the agent calls directly. The agent gets to do what it does best: write code, composing a multi-step task into a single output instead of getting stuck in a "call two commands, look at the result, call two more commands" loop. Compared to the conventional CLI approach, complex workflows finish far faster, with higher task success rates, far fewer tool calls per task, and a much lower cost per task.
A dedicated Space for every agent ego lite gives each agent its own fully isolated Space. You browse up front, your agent works in the background, and they don't get in each other's way. You can see which Space has an agent running at any moment, and take it over or stop it whenever you want.
Your agents multitask in Spaces, parallel workspaces inside the same browser Each Space gets its own AI agent or its own task, all running at the same time. Claude Code enriching 10 leads in 10 parallel Spaces. Codex scraping 5 competitor sites in 5 more. They don't collide or steal your tabs. Your mouse stays where you left it.
The strongest page Snapshot on the market Thanks to customization inside the browser engine, ego lite produces the highest-quality page snapshots, the view text-only models rely on to "see" and act on a webpage. It reliably handles tough cases like deeply nested iframes, exactly where other approaches consistently break down.
Any agent can drive it through ego-browser ego-browser is the connection layer between any agent CLI (Claude Code, Codex, Cursor, or a custom one) and ego lite. It exposes the browser as a set of in-page JavaScript tools: snapshot, fill, click, wait, navigate, capture. The agent writes a JavaScript snippet calling those tools, and ego-browser runs it on the page in one pass.
Experience accumulation that makes your agent faster the more you use it (coming soon) Most of an agent's time on browser tasks goes to trial and error. ego lite's official Skill distills every successful action into reusable tools and workflows, so similar tasks down the line run up to 5x faster.

ego lite vs existing products

Most tools can automate a browser. The real questions are what browser the agent gets, whether you can keep working at the same time, and whether the tool is built for the agent you already use or a built-in one.

Capability ego lite Browser-Use agent-browser (Vercel) ChatGPT Atlas Perplexity Comet
Multitask in parallel โœ“ โ€” โ€” โ€” โ€”
Reusable skills โœ“ โ€” โ€” โ€” โ€”
Inherits Chrome's data โœ“ โ€” โ€” โœ“ โœ“
Same browser, separate workspace โœ“ โ€” โ€” โ€” โ€”
Compressed semantic input โœ“ โ€” โœ“ โ€” โ€”
Controllable by external agents โœ“ โœ“ โœ“ โ€” โ€”
Data stored locally โœ“ โœ“ โœ“ โ€” โ€”
No login friction โœ“ โ€” โ€” โœ“ โœ“
Daily-use browser โœ“ โ€” โ€” โœ“ โœ“
Free โœ“ โœ“ โœ“ โ€” โ€”

Two other categories try to solve the same problem. Browser automation frameworks like Browser-Use and Vercel's agent-browser are libraries the agent calls; they ship no browser of their own, so they need a separate one to drive and your logins rarely carry cleanly. AI browsers like ChatGPT Atlas and Perplexity Comet ship a built-in agent, and only that agent can drive the browser. ego lite is one browser, designed from the start for you and any agent you bring to share.

Benchmarks

We benchmarked ego lite against Vercel's agent-browser on four complex browser automation tasks. ego lite finished each task up to 2.5ร— faster, with substantially fewer tokens. The harder the task, the bigger the gap. See the comparison below.

Docs

Tutorials, the full tool reference, and integration guides live at lite.ego.app/document/.

Community

Star History

License

The contents of this repository are released under the MIT License. The ego lite browser is a separate, free download.


Overview (`package/ego-browser/README.md`)

ego-browser (Node helper runtime)

The Node.js helper layer that runs inside the ego-browser Chromium browser. The browser exposes an ego runtime (tabs, CDP, snapshots, task spaces); this package bundles the agent-facing helpers that script that runtime.

text
ego-browser (Chromium) โ†’ globalThis.ego โ†’ helper functions โ†’ agent heredoc

Build and run

bash
npm ci
npm run build     # bundle to dist/out/index.js
npm test          # build + tsc --noEmit + node --test

The build emits a single ESM file dist/out/index.js. The ego-browser browser dispatches ego-browser nodejs <<'EOF' ... EOF heredocs to that bundle. The v2 TaskSpace and Page APIs are preloaded; the v1 global helpers remain available only for existing scripts.

bash
ego-browser nodejs <<'EOF'
const task = await taskSpace('demo')
const page = task.page('p1')
await page.goto('https://example.com')
console.log(await page.snapshot())
EOF

Local invocation without the browser (for debugging the helper bundle itself) reads stdin:

bash
node dist/out/index.js <<'JS'
help()
JS

Use -h or --help to print the local CLI usage.

Skill workspace

The usage guide lives in skill-body/SKILL.md; the build renders it to dist/out/ego-browser/SKILL.md, where ego-browser skill prints it. Site learnings live in the entry Skill at ../../skills/ego-browser/learnings/; the build copies the entry Skill to dist/out/agent-skills/ego-browser. By default the runtime loads agent helpers and site learnings from that bundled entry Skill:

text
dist/out/agent-skills/ego-browser

Override with EGO_BROWSER_AGENT_WORKSPACE:

bash
EGO_BROWSER_AGENT_WORKSPACE=/path/to/skill ego-browser nodejs <<'EOF'
cliLog(await siteSkills())
EOF

Site learnings under agentWorkspace()/learnings/<site>/ are always active and read on every helper call. Validate them with:

bash
npm run validate:site-skills

Source layout

text
src/
  run.ts                 CLI entry; reads stdin, injects helpers, executes
  helpers.ts             public helper surface (re-exports + glue)
  page-model.ts          TaskSpace/Page lifecycle and operations
  public-api-schema.ts   v2 validation, help, and reference source
  browser-runtime.ts     bridge to globalThis.ego (CDP, sessions, events)
  element-resolver.ts    resolves @N / CSS / XPath / ARIA targets
  driver/
    pointer.ts           click, hover, drag, scroll, scrollBy
    observe.ts           snapshot, captureScreenshot, elementCenter
    keyboard.ts          typeText, pressKey, fillInput, dispatchKey
    nav.ts               tabs, gotoUrl, openOrReuseTab, closeTab
    load.ts              waitForLoad and load orchestration
    waits.ts             waitForElement, waitForNetworkIdle, wait
    files.ts             uploadFile
  http.ts                serverFetch, browserFetch
  cdp-eval.ts            cdp() and js() raw eval
  learning/              site-learnings discovery and manifest validation
scripts/
  build.mjs              esbuild bundling

See skill-body/SKILL.md for the agent-facing workflow and help() for the v2
API reference.
The old global helpers remain available as a v1 compatibility surface for
existing scripts.

Design constraints

  • The browser runtime owns tabs, task spaces, CDP transport, snapshots, and event delivery. This package keeps only agent-facing ergonomics.
  • Snapshot helpers use the browser runtime contract: ego.snapshot({ scope, root, includeActionMarks, includeStableLocator }); the Page API resolves a subtree root from a Page-scoped snapshot ref before invoking it.
  • V2 Page refs are SDK-assigned ids bound to a frame, document, and backend node. Input actions preserve refs to unchanged nodes, allowing consecutive ref actions. Partial snapshots retain omitted refs; full-page snapshots replace the active set. The Page ledger persists mappings and invalidation across Agent rounds. Missing, stale, or unresolvable refs require a fresh snapshot rather than native renumbering or role/name fallback.
  • Page selectors search the top document first. Input actions search frames
    when the top document has no match usable for that action, then require one
    usable frame match.
  • The selector parser supports the documented Ego forms plus a narrow
    Playwright-compatible subset: css=, terminal :has-text(), :text-is(),
    >> nth=N after CSS/text/href selectors (N >= 0 or -1), and role
    name*= matching.
  • Clicks require an enabled element and a real hit target. Hover and drag keep
    the hit-target requirement but may address disabled elements. DOM-backed
    actions such as selectOption() require a rendered, enabled control but may
    operate on an opacity-zero native control used by a custom widget. Like
    Playwright, selectOption() can programmatically choose a disabled option.
  • Network-idle waits use continuous per-Page and OOPIF request state; they do
    not consume the CDP events returned by page.events() or include other Pages.
  • fill() verifies that editing took effect, not that application formatting
    preserved the requested string byte-for-byte. Business postconditions remain
    explicit Page reads or waits.
  • New public APIs must be added to public-api-schema.ts; runtime validation,
    default help(), and the usage guide must remain aligned.
  • Embedded hosts should await the exported disposeEgoSdk() hook before
    discarding a Node context; see ../../docs/native-sdk-lifecycle-requirement.md.
  • Site-specific reusable experience belongs under ../../skills/ego-browser/learnings/, not in this package.

License

MIT


Overview (`plugins/ego/README.md`)

ego agent plugin

Package the same ego-browser Skill for nine agent hosts. Each distribution
contains only its host entry points, a complete copy of the canonical Skill,
and installation instructions for that host.

Requirements

  • macOS or Windows with ego lite installed and open
  • ego-browser available on the agent's PATH
  • An agent host that can read Skills and execute local shell commands

Packaging

From the repository root, build all nine artifacts with one command:

bash
npm --prefix plugins/ego run package

The output directory is dist/plugins/ego-v2.1.0/:

Host Artifact Contents
Claude Code ego-claude-code-v2.1.0.zip Claude plugin
Codex ego-codex-v2.1.0.zip Codex plugin at the archive root, plus marketplace
Cursor ego-cursor-v2.1.0.zip Cursor plugin
GitHub Copilot ego-github-copilot-v2.1.0.zip Claude-compatible plugin
Grok Build ego-grok-build-v2.1.0.zip Agent Plugins manifest and Skill
WorkBuddy ego-workbuddy-v2.1.0.zip Local marketplace and WorkBuddy/CodeBuddy manifests
QwenWork Desktop ego-qwenwork-v2.1.0.zip Claude-compatible expert kit
OpenCode ego-opencode-v2.1.0.tgz @citrolabs/ego-opencode npm package
DeepSeek Harness ego-deepseek-harness-v2.1.0.tgz @citrolabs/ego-deepseek-harness npm bundle

Every ZIP includes a host-specific README.md at its root; npm archives include
it under package/. The WorkBuddy ZIP contains a marketplace root with the
plugin under plugins/ego/. Other ZIPs start directly at the plugin root. The
Codex ZIP starts there too and carries its marketplace file alongside, because
the public plugin directory rejects an archive whose manifest is not at the
root.

The two npm packages have separate names and exports so their adapters cannot
be confused by package caches. All nine packages contain real Skill files;
none depends on the source checkout or its symlink.

Override the output parent directory, or package all plugin directories:

bash
npm --prefix plugins/ego run package -- --output /absolute/path/to/artifacts
node plugins/scripts/package-plugins.mjs --all

Each plugin gets its own <name>-v<version>/ directory. Rebuilding replaces the
nine named artifacts only after all nine have been built. A build failure leaves
previous artifacts intact. Older universal ZIP/npm outputs outside the version
directory are left untouched and are no longer generated.

Packaging requires Node.js, npm, zip, and unzip. Run this command instead of
npm pack in the source directory: npm omits the linked Skill when packing the
source directly.

Installation

Choose the artifact for the host. Extract ZIPs into a persistent local directory
unless the host accepts ZIP upload. The paths below are placeholders for that
directory or the downloaded npm archive. Each packaged README repeats only the
matching section and the requirements above.

Claude Code

Extract the Claude Code ZIP, then load its plugin directory for a session:

bash
claude --plugin-dir /absolute/path/to/ego-claude-code

Use /ego:ego-browser. This flag loads the plugin for that session. Persistent
installation uses a plugin marketplace; the repository marketplace exposes
ego@ego-agent-skills when that source version is available.

See the Claude Code plugin reference.

Codex

The Codex ZIP serves both install routes. .codex-plugin/plugin.json and
skills/ sit at the archive root, and .agents/plugins/marketplace.json
points at that same root with "path": "./".

To install locally, extract the ZIP and add the extracted directory:

bash
codex plugin marketplace add /absolute/path/to/ego-codex
codex plugin add ego@ego-codex-local

Restart Codex if the plugin does not appear, then ask it to use $ego-browser.
Remove the installed plugin with codex plugin remove ego@ego-codex-local.

To publish, upload the same ZIP as a skills-only plugin in the submission
portal and select the verified developer identity that matches author.name.
The listing copy comes from the manifest's interface object, whose category
must be one of the values the portal accepts. See
Submit plugins and
the submission error reference.

See the OpenAI plugin guide.

Cursor

Extract the Cursor ZIP and load it with Cursor CLI:

bash
cursor-agent --plugin-dir /absolute/path/to/ego-cursor

The package declares its Skill through .cursor-plugin/plugin.json. Use
/ego-browser in the session. This command does not install a Cursor Desktop
marketplace entry.

GitHub Copilot

Extract the GitHub Copilot ZIP and load it with Copilot CLI:

bash
copilot --plugin-dir /absolute/path/to/ego-github-copilot

Use /ego-browser. This local session uses the packaged Claude-compatible
manifest. VS Code marketplace installation is a separate host workflow.

Check the Skill's source when a personal ego-browser Skill is already
installed. In the tested Copilot CLI, the personal copy takes precedence over
the plugin copy, and a disabled personal copy prevents the plugin Skill from
being invoked. Resolve that duplicate and enable the plugin Skill before use.

See the GitHub Copilot plugin guide.

Grok Build

Extract the Grok Build ZIP, then install the local directory:

bash
grok plugin install /absolute/path/to/ego-grok-build

Use /ego-browser. The package contains the Agent Plugins plugin.json and
Skill directory. Remove it with grok plugin uninstall ego.

WorkBuddy

Extract the WorkBuddy ZIP as a marketplace root. It contains
.claude-plugin/marketplace.json and plugins/ego/, including
.codebuddy-plugin/plugin.json, .workbuddy-plugin/plugin.json, and the
Claude-compatible manifest.

For WorkBuddy versions that expose ๆทปๅŠ ๅธ‚ๅœบ with local sources, add the
absolute path of the extracted root, select ego-workbuddy-local, and install
ego. Start a new task and ask it to use the ego-browser Skill.

Local marketplace import still needs verification in WorkBuddy Desktop. The
official WorkBuddy plugin guide
documents marketplace installation but does not establish a direct ZIP import
workflow. If the installed version only accepts a published marketplace, this
local archive must be distributed through that supported marketplace first.

QwenWork Desktop

Upload the QwenWork ZIP directly through ๆ‰ฉๅฑ• โ†’ ไธ“ๅฎถๅฅ—ไปถ โ†’ + ๆทปๅŠ  โ†’ ไธŠไผ ๅฅ—ไปถ,
then click ๅฎ‰่ฃ…. Start a new task and ask it to use ego-browser.

This package uses .claude-plugin/plugin.json, an explicitly supported expert
kit format. The older .qoder-plugin/plugin.json source manifest is not used
for this artifact. See the QwenWork expert-kit guide.

OpenCode

Extract the local npm archive into a persistent directory, then register its
JavaScript entry point:

bash
mkdir -p /absolute/path/to/ego-opencode
tar -xzf /absolute/path/to/ego-opencode-v2.1.0.tgz -C /absolute/path/to/ego-opencode
opencode plugin "file:///absolute/path/to/ego-opencode/package/index.js" -g

Keep the extracted directory in place. OpenCode 1.14.30 accepts a local .tgz
in its installer but fails to resolve that archive's server entry at startup;
the extracted file URL loads successfully. Remove any earlier .tgz entry
from the config's plugin array when switching to the file URL.

The package is named @citrolabs/ego-opencode. Its . and ./server exports
load index.js, which adds the packaged Skill instructions and /ego-browser
command. To remove it, delete its entry from the applicable opencode.json
plugin array.

See the OpenCode plugin guide.

DeepSeek Harness

Extract the npm archive into a persistent directory, then add its package/
directory to the desired profile:

bash
mkdir -p /absolute/path/to/ego-deepseek-harness
tar -xzf /absolute/path/to/ego-deepseek-harness-v2.1.0.tgz -C /absolute/path/to/ego-deepseek-harness
dsh plugin --profile web add /absolute/path/to/ego-deepseek-harness/package
dsh --profile web --dump-config
dsh --profile web

The package is named @citrolabs/ego-deepseek-harness. Its bundle patch loads
@citrolabs/ego-deepseek-harness/dsh and registers the packaged Skill. Use
/ego-browser. Remove it with
dsh plugin --profile web remove @citrolabs/ego-deepseek-harness.

An existing personal ego-browser Skill takes precedence over this bundled
provider. Check the base directory returned by the Skill tool: it must point
inside this extracted package when validating the plugin. Resolve the duplicate
personal Skill if you want named invocation to use the plugin copy.

See the DeepSeek Harness local package guide.

Verification

Packaging tests check all nine payloads, marketplace paths, canonical Skill
contents, source symlink preservation, repeat builds, and failed-batch handling.
They load both npm adapters after extraction outside the repository. These
checks do not establish end-to-end compatibility in all nine host applications.

After installation, confirm that the host discovers ego-browser and can run:

bash
ego-browser nodejs <<'EOF'
console.log('ego-browser ready')
EOF

Development

skills/ego-browser is a relative symlink to ../../../skills/ego-browser.
It is the thin entry Skill; the usage guide it points to lives in
package/ego-browser/skill-body/SKILL.md and ships with ego lite. Edit the
canonical entry Skill; the packager materializes it in temporary directories
and leaves the source link untouched. Unexpected or broken links fail packaging.

Plugin tests belong in plugins/ego/tests/ and run independently of browser
runtime tests:

bash
npm --prefix plugins/ego test

CI and the commit hook run this suite for plugin changes.


Ego Browser Skill Command (`docs/ego-browser-skill-command.md`)

`ego-browser skill` browser integration

The ego-browser Skill has two layers:

  • Entry Skill โ€” skills/ego-browser/ in this repository. It is what skill
    markets, plugins, and ~/.agents/skills/ego-browser contain. It only tells the
    agent to run ego-browser skill, plus the install guide and the
    cookie/cache/storage guide that must be readable before ego lite exists.
  • Usage guide โ€” package/ego-browser/skill-body/SKILL.md. It is versioned
    with the SDK and printed by ego-browser skill, so the agent always reads the
    guide that matches the installed browser.

This document lists what the ego lite app and its native ego-browser launcher
must implement. Everything else lives in this repository.

Release payload

npm run build writes the payload to package/ego-browser/dist/out/
(CI archives the same directory as ego-browser-release-payload.tar.gz):

text
dist/out/
โ”œโ”€โ”€ index.js                     SDK bundle
โ”œโ”€โ”€ ego-browser/
โ”‚   โ””โ”€โ”€ SKILL.md                 usage guide printed by `ego-browser skill`
โ””โ”€โ”€ agent-skills/
    โ””โ”€โ”€ ego-browser/             entry Skill to install into ~/.agents/skills/ego-browser
        โ”œโ”€โ”€ SKILL.md
        โ”œโ”€โ”€ references/
        โ”œโ”€โ”€ scripts/
        โ”œโ”€โ”€ agents/
        โ”œโ”€โ”€ assets/
        โ””โ”€โ”€ learnings/           site learnings; the SDK reads this bundled copy

Ship dist/out/ as one unit inside the app, keeping ego-browser/ next to
index.js.

1. Add the `ego-browser skill` subcommand

  1. Resolve the SDK with exactly the same rules as ego-browser nodejs:
    --sdk-path, then the debug override
    (~/Library/Application Support/Citro Labs/debug/index.js), then the bundled
    SDK. Do not implement a separate lookup, or the guide and SDK can come from
    different builds.

  2. Read ego-browser/SKILL.md from the directory that contains the resolved
    index.js.

  3. Write it to stdout unchanged and exit 0. The build already adds the first
    line ([ego-browser:skill] ego-browser usage guide v<version>) and the last
    line ([ego-browser:skill] end of guide v<version>); do not add or parse
    anything else.

  4. When the SDK is not the bundled one, print one extra line before the guide so
    developers notice the override:

    text
    [ego-browser:skill] source=debug-sdk dir=/path/to/dist/out/ego-browser (override, not the bundled guide)

    source is sdk-path or debug-sdk.

Errors:

Case Behavior
Any extra argument, e.g. ego-browser skill foo Print usage to stderr, exit non-zero
ego-browser/SKILL.md missing next to the resolved SDK Print the path that was checked to stderr, exit non-zero; do not fall back to another source

Also list skill in ego-browser help, e.g. "Print the usage guide for this
ego lite version".

Older launchers print Unknown command: skill and exit 2. The entry Skill
relies on that to send the agent to references/install.md, which asks the user
to run ego-browser upgrade.

2. Install the entry Skill on launch

On every launch, check ~/.agents/skills/ego-browser:

Current state Action
Missing Copy agent-skills/ego-browser/ there as real files
A symlink the browser created earlier (to ~/.local/share/ego/ego-skills) Replace it with a copy of agent-skills/ego-browser/, then delete ~/.local/share/ego/ego-skills
Anything else (installed by the user or a package manager) Leave it untouched

Stop extracting the full Skill to ~/.local/share/ego/ego-skills. After the
entry Skill is in place, the browser never writes ~/.agents/skills again;
upgrades only replace the bundled payload.

Acceptance checks

  • ego-browser skill output is byte-identical to the bundled
    ego-browser/SKILL.md, starts with the version line, and ends with
    [ego-browser:skill] end of guide v<version>.
  • With --sdk-path <checkout>/package/ego-browser/dist/out/index.js or the
    debug override, the output is the checkout's guide and starts with a
    source= line; removing the override restores the bundled guide.
  • ego-browser skill foo and a missing guide both exit non-zero with a
    readable error.
  • ego-browser help lists skill.
  • Fresh install: ~/.agents/skills/ego-browser contains the entry Skill.
  • Upgrade from a version that used the ego-skills symlink: the symlink is
    replaced by the entry Skill. A user-installed Skill stays unchanged.

Local Runtime Development (`docs/local-runtime-development.md`)

Use a Local Runtime with Ego Lite

Ego Lite installs the ego-browser CLI and provides the native browser bridge.
This repository builds the JavaScript runtime that the CLI can load for a
single command:

text
Installed Ego Lite CLI
  -> --sdk-path
  -> local dist/out/index.js
  -> Ego Lite and Chromium

The override is temporary. It does not modify the installed application.

Build

Ego Lite must be installed and onboarding must be complete. The examples below
assume that ego-browser on PATH belongs to Ego Lite.

From the repository root:

bash
cd package/ego-browser
npm ci
npm run build

The runtime is written to package/ego-browser/dist/out/index.js. Run
npm run build again after changing runtime source code.

Run with the local runtime

Use the CLI installed by Ego Lite and pass the absolute bundle path:

bash
cd package/ego-browser

ego-browser nodejs --sdk-path "$PWD/dist/out/index.js" <<'EOF'
if (!globalThis.ego) throw new Error('Ego Lite native bindings are unavailable')
process.stdout.write('Local runtime loaded successfully\n')
EOF

For browser automation, keep the same command and write the heredoc using the
API documented by the current checkout in package/ego-browser/skill-body/SKILL.md.

Every manual invocation needs --sdk-path; it is not a persistent setting.
Do not use npm link or replace files inside the Ego Lite application bundle.
The installed CLI is still required to provide the native browser environment.

Verify against the real browser

Run the real-browser E2E suite:

bash
cd package/ego-browser
npm run e2e

The suite builds the current worktree and automatically runs it through the
Ego Lite CLI with --sdk-path. On macOS it prefers the app's stable
Versions/Current helper path, so another installed Ego product cannot silently
take over the test. Set EGO_BROWSER_REAL_E2E_CLI to use another installation.
This is the preferred end-to-end compatibility check. npm test remains useful
for repository tests but does not replace the real-browser check.

The command is self-contained: it starts its own local fixture server, creates a
uniquely named temporary task space, and cleans both up. No manual SDK linking,
task-space selection, takeover, or browser interaction is part of a successful
run. A fresh agent can use this command without any context from an earlier
debugging session.

Keep the Skill aligned

The usage guide describes the APIs implemented by the runtime. ego-browser skill
prints the guide next to the SDK it loads, so an agent that runs it with the same
--sdk-path (or the debug override) reads the guide from the same checkout as
the bundle. A guide from another release may generate helper calls that the
local runtime does not support.

Troubleshooting

  • ego-browser not found: finish Ego Lite onboarding and add its CLI
    directory, commonly ~/.local/bin, to PATH.
  • Multiple Ego products are installed: set EGO_BROWSER_REAL_E2E_CLI to
    the intended Ego Lite helper. The E2E output prints the selected CLI path.
  • --sdk-path unsupported: update Ego Lite to a build that supports loading
    an external runtime.
  • Changes are not visible: rebuild and confirm that the absolute SDK path
    points to the intended checkout.
  • Browser APIs are missing: align the Skill with the checkout, then run
    npm run e2e to check app/runtime compatibility.

Native Bindings Api (`docs/native-bindings-api.md`)

Ego Lite Native Bindings API Reference

This document describes the low-level globalThis.ego API injected into the
ego-browser nodejs environment by the installed Ego Lite application.

These bindings are provided by Ego Lite, not implemented by this repository.
Their availability is determined by the installed Ego Lite build and is
independent of the Git tag or branch used to build the local ego-browser
runtime.

The runtime in this repository consumes these bindings and may expose only a
subset of them through its higher-level helpers. Agent automation should
normally follow the usage guide printed by ego-browser skill; runtime maintainers may access
globalThis.ego directly when working with the native bridge.

Runtime Model

Most methods return a Promise and route work to the browser-side
NodeClient. Promise results are converted from browser-side base::Value
objects, so result shapes can follow the command implementation that handled the
request.

Task-scoped APIs operate on the current task context. Select that context with
ego.useTaskSpace(id) inside the script. id is the numeric task-space id
reported by ego.createTaskSpace(), ego.claimTaskSpace(), or
ego.listTaskSpaces(). Selecting a task only updates the current Node
invocation; it does not create, claim, or validate the browser-side task space.

Profiles are selected when a task space is created, not through
ego.useTaskSpace(). Call ego.listProfiles() to discover the available
profiles, then pass the selected profile's id to
ego.createTaskSpace(name, profileId). Existing task-scoped APIs continue to
operate on the profile that owns the selected task space.

Task-space result objects currently expose both a numeric locator and a
user-visible name:

js
{
  id: 7,                    // numeric unique task-space locator
  taskId: 'checkout-flow',  // same value as name
  name: 'checkout-flow',    // user-visible display name
}

Use id when calling APIs that operate on an existing task space. taskId is a
display-name compatibility field and should not be used as a locator.

If no task context is available, task-scoped APIs resolve to an error object or
reject depending on the underlying operation. Regular task commands usually
resolve an object like
{ error: "Task space not selected", error_code: "EGO_TASK_SPACE_NOT_SELECTED" };
ego.snapshot() rejects with an Error whose error_code property is set.

When a task has been handed off to the user, browser-mutating automation calls
are blocked until the agent takes the task back. The affected APIs are
ego.createTab(), ego.listTabs(), ego.snapshot(), ego.closeTaskSpace(),
ego.completeTaskSpace(), ego.animationHighlightMouseToPosition(),
ego.setAgentTaskState(), and raw CDP sends through ego.sendCDPMessage().
Most of these APIs resolve an error object with error and error_code fields;
ego.snapshot() rejects with an Error containing error_code; raw CDP sends
report the failure through ego.onSendCDPMessageError.

Error Codes

Browser-side failures expose a stable error_code in addition to a human-readable
message. Promise-returning APIs use one of these shapes:

js
{ error: 'Task space not selected', error_code: 'EGO_TASK_SPACE_NOT_SELECTED' }

or, for rejected operations:

js
try {
  await ego.snapshot();
} catch (error) {
  console.log(error.message);
  console.log(error.error_code);
}

Argument validation failures throw TypeError with
error_code: 'EGO_INVALID_ARGUMENT'.

Known error codes:

text
EGO_BROWSER_UNAVAILABLE
EGO_CDP_CHANNEL_UNAVAILABLE
EGO_CDP_SEND_FAILED
EGO_INVALID_ARGUMENT
EGO_INVALID_RESULT_PAYLOAD
EGO_OPERATION_FAILED
EGO_PROFILE_NOT_FOUND
EGO_RESULT_CONVERSION_FAILED
EGO_SNAPSHOT_FAILED
EGO_TASK_HOST_DISCONNECTED
EGO_TASK_SPACE_INACTIVE
EGO_TASK_SPACE_NOT_FOUND
EGO_TASK_SPACE_NOT_SELECTED
EGO_TASK_SPACE_UNAVAILABLE
EGO_TASK_SPACE_USER_IN_CONTROL
EGO_WEB_CONTENTS_UNAVAILABLE

All async callbacks are bounced through the Node event loop before JS is
invoked. Scripts can safely await these APIs and allow the process to exit
after pending requests settle.

Browser Version

`await ego.getBrowserVersion()`

Returns the installed Ego Lite version and whether its updater has an available
release. This method is not task-scoped and does not start an update.

js
{
  currentVersion: '0.5.0.5',
  updateAvailable: false,
}

Older Ego Lite builds may not expose this method. Callers should treat a missing
method, a failed request, or an invalid result as unavailable update status.

Task Context

`ego.useTaskSpace(id)`

Selects the task context for subsequent task-scoped helper calls in the current
Node invocation. This is a synchronous local selection only.

js
const id = ego.useTaskSpace(task.id);

Arguments:

text
id: number

Returns the selected id synchronously.

Throws TypeError when id is missing, not a number, or extra arguments are
provided. The thrown error has error_code: 'EGO_INVALID_ARGUMENT'.

Use ego.createTaskSpace(name) to create a new agent-owned task space. Regular
Spaces created by the user are returned by ego.listTaskSpaces() with
ownership: 'user'. Pass their numeric id to
ego.claimTaskSpace(id, name?) before selecting them.

Profiles

Profiles determine which browser identity, cookies, storage, and login state a
new task space inherits. Profile selection only applies when creating the task
space. After creation, select the returned task-space id with
ego.useTaskSpace(task.id) as usual.

Profile APIs are available when the installed Ego Lite build exposes them,
even if the checked-out repository runtime does not yet provide equivalent
higher-level helper methods.

`await ego.listProfiles()`

Lists the profiles that may be used by ego.createTaskSpace().

js
const result = await ego.listProfiles();
for (const profile of result.profiles) {
  console.log(profile.id, profile.name, profile.isDefault);
}

Arguments: none.

Returns:

js
{
  profiles: [
    {
      id: 'Profile 2',
      name: 'Work',
      isDefault: false,
    },
    {
      id: 'Default',
      name: 'Personal',
      isDefault: true,
    },
  ],
}

Field meanings:

text
id:
  Locator accepted by ego.createTaskSpace(name, profileId), for example
  "Default" or "Profile 2".

name:
  User-visible profile name. It may be duplicated and is not a locator.

isDefault:
  True for the profile selected when profileId is omitted. This does not
  necessarily mean that id is "Default".

The result contains available regular profiles. The internal Ego authentication
profile is not exposed.

Throws TypeError when any argument is passed. The thrown error has
error_code: 'EGO_INVALID_ARGUMENT'.

To create a task space with a profile, pass profile.id to
ego.createTaskSpace(name, profileId). See the Task Spaces section for the
complete call and error handling.

Tabs

`await ego.createTab(url)`

Creates a tab in the current task space.

js
const tab = await ego.createTab('https://example.com/');
console.log(tab.targetId);

Arguments:

text
url: string

Returns a browser-side result object. Current task implementations return a
tab descriptor containing at least targetId when creation succeeds.

Throws TypeError when url is missing or not a string.
The thrown error has error_code: 'EGO_INVALID_ARGUMENT'.

`await ego.listTabs()`

Lists tabs in the current task space.

js
const result = await ego.listTabs();
for (const tab of result.tabs) {
  console.log(tab.index, tab.title, tab.url, tab.active);
}

Arguments: none.

Typical result:

js
{
  tabs: [
    {
      index: 0,
      targetId: '...',
      url: 'https://example.com/',
      title: 'Example',
      active: true,
    },
  ],
}

If the current task has been handed off to the user, the promise resolves to an
error object:

js
{
  error: 'The task is under user control...',
  error_code: 'EGO_TASK_SPACE_USER_IN_CONTROL',
}

error_code is 'EGO_TASK_SPACE_USER_IN_CONTROL'. For this code, error is a
stable reason key rather than display text. Current keys are:

text
notifications
location
camera
microphone
pan_tilt_zoom_microphone
midi
bluetooth
usb
serial
hid
protocol_handler
manual_takeover

For example, a location prompt returns:

js
{
  error: 'location',
  error_code: 'EGO_TASK_SPACE_USER_IN_CONTROL',
}

Callers should use error_code to identify the control boundary, then use the
reason key to choose guidance. Unknown reason keys must fall back to generic
user-control guidance.

Page-authored JavaScript dialogs must not use this user-control path; see
Native JavaScript dialog requirement.

Snapshot

`await ego.snapshot(options?)`

Captures a structured snapshot for the current task space.

js
const snapshot = await ego.snapshot({
  scope: 'only_within_viewport',
  includeActionMarks: true,
  interactiveOnly: true,
  includeStableLocator: true,
  maxResultLength: 4000,
});

console.log(snapshot.content);
console.log(snapshot.refs);

const subtree = await ego.snapshot({
  scope: 'subtree',
  root: snapshot.refs[0].backendNodeId,
});

Arguments:

text
options?: object

Supported options:

text
scope:
  'full_page'            Capture the full page. This is the default.
  'only_within_viewport' Capture only the viewport.
  'subtree'              Capture the node identified by root and its descendants.

root: number
  backendNodeId from a previous snapshot ref. Required only for subtree scope.

includeActionMarks: boolean
  Include action markers in the textual snapshot.

interactiveOnly: boolean
  Limit output to interactive elements.

includeStableLocator: boolean
  Include reusable locator hints on refs when available.

maxResultLength: number
  Maximum result content length. Zero or omitted means unlimited.

In Ego Lite 0.5.0.19, subtree scope requires launching the browser with
--enable-features=SnapshotSubtree. Do not use PageSnapshotSubtree.
This version matches root by backendNodeId across all frames, although that
identifier is only frame-local. If the same value occurs in multiple frames,
the call fails with an ambiguity error instead of selecting an arbitrary node.

Result:

js
{
  content: '...',
  refs: [
    {
      backendNodeId: 17,
      role: 'button',
      name: 'Continue',
      loc: 'role:button[name=Continue]', // only when available
    },
  ],
}

When no stable, unique locator is available, omit loc from both refs and
the textual snapshot. Do not return unstable or ambiguous as locator values.

If the browser side reports a snapshot error, the promise rejects with an
Error. The message contains the human-readable error, and error_code
contains a stable code such as EGO_SNAPSHOT_FAILED,
EGO_TASK_SPACE_NOT_SELECTED, EGO_TASK_SPACE_NOT_FOUND,
EGO_TASK_SPACE_USER_IN_CONTROL, or EGO_WEB_CONTENTS_UNAVAILABLE.

Task Spaces

`await ego.listTaskSpaces()`

Lists Agent task spaces and regular user-created Spaces that are available to
the native task-space bridge. Listing is read-only: it does not select or claim
a Space.

js
const spaces = await ego.listTaskSpaces();
for (const space of spaces.taskSpaces) {
  console.log(space.id, space.name, space.ownership);
}

Arguments: none.

Returns:

js
{
  taskSpaces: [
    {
      taskId: 'research',
      id: 3,
      name: 'research',
      createdBy: 'user',
      ownership: 'user',
      profileId: 'Default',
      profileName: 'Work',
      recentTabTitles: ['Project notes', 'Reference'],
    },
    {
      taskId: 'checkout-flow',
      id: 7,
      name: 'checkout-flow',
      createdBy: 'agent',
      ownership: 'agent',
      recentTabTitles: ['Checkout', 'Cart'],
    },
  ],
}

id is the locator for ego.claimTaskSpace() and other task-space APIs.
name and taskId are display values and may be duplicated. A regular Space
created in the Ego Lite UI has createdBy: 'user' and ownership: 'user'.
recentTabTitles may help the caller identify the intended Space without
claiming it.

Throws TypeError when any argument is passed.
The thrown error has error_code: 'EGO_INVALID_ARGUMENT'.

`await ego.createTaskSpace(name, profileId?)`

Creates an agent-owned task space by name, optionally using a specific profile.

js
const { profiles } = await ego.listProfiles();
const profile = profiles.find(item => !item.isDefault) ?? profiles[0];
if (!profile) {
  throw new Error('No available profile');
}

const taskName = `profile-demo-${Date.now()}`;
const task = await ego.createTaskSpace(taskName, profile.id);
if (task.error) {
  throw new Error(`${task.error} (${task.error_code})`);
}
ego.useTaskSpace(task.id);
console.log(JSON.stringify({ profile, task }, null, 2));

Arguments:

text
name: string, non-empty
profileId?: string
  Unique profile id returned by ego.listProfiles(). If omitted, empty, or
  "default", the currently active/last-used regular profile is selected.

Returns:

js
{
  taskId: 'checkout-flow',
  id: 7,
  name: 'checkout-flow',
}

Throws TypeError when name is missing, not a string, or empty; when
profileId is present but not a string; or when extra arguments are provided.
The thrown error has error_code: 'EGO_INVALID_ARGUMENT'.

If profileId does not identify an available profile, the promise resolves to:

js
{
  error: 'Profile not found',
  error_code: 'EGO_PROFILE_NOT_FOUND',
}

If the browser cannot create a task space, the promise resolves to an error
object:

js
{
  error: 'No active browser',
  error_code: 'EGO_BROWSER_UNAVAILABLE',
}

`await ego.claimTaskSpace(id, name?)`

Claims a Space returned by ego.listTaskSpaces() and turns it into an
agent-owned task space. This includes regular Spaces created by the user. Pass
name to rename the Space while claiming it.

js
const spaces = await ego.listTaskSpaces();
const checkout = spaces.taskSpaces.find(space => space.name === 'checkout-flow');
const task = await ego.claimTaskSpace(checkout.id, 'checkout-flow');
ego.useTaskSpace(task.id);

Arguments:

text
id: number
name?: string

Returns:

js
{
  taskId: 'checkout-flow',
  id: 7,
  name: 'checkout-flow',
}

Claiming a regular user Space keeps its browser profile, tabs, active tab, and
numeric id; it does not create a replacement Space. The caller must have the
user's approval before taking ownership.

Throws TypeError when id is missing or not a number, when name is present
but not a string, or when extra arguments are provided. The thrown error has
error_code: 'EGO_INVALID_ARGUMENT'.

If the space does not exist or cannot be claimed, the promise resolves to an
error object. Error messages identify the numeric id that was used for lookup:

js
{
  error: 'Task space not found: 7',
  error_code: 'EGO_TASK_SPACE_NOT_FOUND',
}

claimTaskSpace() does not select the task context by itself. Call
ego.useTaskSpace(task.id) after a successful claim before using task-scoped
helpers.

`await ego.closeTaskSpace()`

Closes the current task space and removes the task-owned browser state.

js
await ego.closeTaskSpace();

Arguments: none.

Returns a browser-side status value, currently a string such as
" task space closed." on success, or an error object if the task does
not exist. Error objects include error and error_code.

Throws TypeError when any argument is passed. The thrown error has
error_code: 'EGO_INVALID_ARGUMENT'.

`await ego.completeTaskSpace()`

Completes the current task space by removing agent task state while leaving the
Space open for the user. It remains discoverable through
ego.listTaskSpaces() under the same numeric id with user ownership.

js
await ego.completeTaskSpace();

Arguments: none.

Returns a browser-side status value, currently a string such as
" task space completed." on success, or an error object if the task
does not exist. Error objects include error and error_code.

Throws TypeError when any argument is passed. The thrown error has
error_code: 'EGO_INVALID_ARGUMENT'.

Task Control

`await ego.handOffTaskSpace()`

Transfers the current task space to user control.

js
await ego.handOffTaskSpace();

Arguments: none.

Returns the browser-side status value from the handoff operation.
Current implementations return a string such as
" has been handed off to the user.".

Throws TypeError when any argument is passed. The thrown error has
error_code: 'EGO_INVALID_ARGUMENT'.

`await ego.takeOverTaskSpace()`

Returns the current task space to agent control.

js
await ego.takeOverTaskSpace();

Arguments: none.

Returns the browser-side status value from the takeover operation.
Current implementations return a string such as
" has been taken over by the agent.".

Throws TypeError when any argument is passed. The thrown error has
error_code: 'EGO_INVALID_ARGUMENT'.

`await ego.setAgentTaskState(state)`

Updates the user-visible agent progress text for the current task space.

js
await ego.setAgentTaskState('Waiting for checkout page');

Arguments:

text
state: string

Returns the browser-side status value from the task state update. Current
implementations return a string such as " state updated.".

Throws TypeError when state is missing, not a string, or extra arguments are
provided. The thrown error has error_code: 'EGO_INVALID_ARGUMENT'.

UI Helpers

`await ego.animationHighlightMouseToPosition(x, y)`

Shows an animated highlight at the given screen position for the current task
space.

js
await ego.animationHighlightMouseToPosition(120, 240);

Arguments:

text
x: number
y: number

Returns the browser-side status value from the highlight operation. Current
implementations return a string such as " mouse highlight shown.".

Throws TypeError when either coordinate is missing, not a number, or extra
arguments are provided. The thrown error has
error_code: 'EGO_INVALID_ARGUMENT'.

Chrome DevTools Protocol

`ego.sendCDPMessage(message)`

Sends a raw CDP message through the Node CDP channel. The message must already
be JSON-encoded. The current task must be selected with ego.useTaskSpace(id),
and its ownership must be agent. If the task is missing, inactive, delegated
to the user, or unavailable, the message is not sent and
ego.onSendCDPMessageError receives the reconstructed error message and code.

js
const task = await ego.createTaskSpace('cdp-demo');
ego.useTaskSpace(task.id);
ego.sendCDPMessage(JSON.stringify({
  id: 1,
  method: 'Runtime.enable',
}));

Arguments:

text
message: string

Returns undefined.

Throws TypeError when message is missing or not a string, or when the CDP
channel is not connected. The thrown error has error_code set to
EGO_INVALID_ARGUMENT for invalid arguments or EGO_CDP_CHANNEL_UNAVAILABLE
when the channel is missing.

Notes:

text
Raw CDP user-control checks use the task selected by ego.useTaskSpace(id).
If no task has been selected, the send fails with EGO_TASK_SPACE_NOT_SELECTED.

`ego.onCDPMessage`

Property used to receive raw CDP messages from the browser side.

js
ego.onCDPMessage = message => {
  const event = JSON.parse(message);
  console.log(event.method);
};

Assign a function to receive messages. Assigning any non-function value clears
the callback. Incoming messages are delivered asynchronously through the Node
event loop.

`ego.onSendCDPMessageError`

Property used to receive local send failures for ego.sendCDPMessage().

js
ego.onSendCDPMessageError = (message, error_code) => {
  console.error(message, error_code);
};

const task = await ego.createTaskSpace('cdp-version');
ego.useTaskSpace(task.id);
ego.sendCDPMessage(JSON.stringify({
  id: 2,
  method: 'Browser.getVersion',
}));

Arguments delivered to the callback:

text
message: string
error_code: string

The callback does not include the reason key. A user-control send failure
therefore identifies the control boundary, not the permission that caused it.

Assign a function to receive errors. Assigning any non-function value clears the
callback. Errors are delivered asynchronously through the Node event loop.

This callback is used for failures that happen before the message reaches CDP,
including:

text
- the task is currently under user control
- no task space has been selected
- the selected task space does not exist
- the selected task space has not been claimed by the agent
- the browser-side CDP agent host is no longer available

User-control example:

js
const task = await ego.createTaskSpace('user-control-demo');
ego.useTaskSpace(task.id);
await ego.handOffTaskSpace();

const result = await ego.createTab('https://example.com/');
console.log(result.error);
console.log(result.error_code); // "EGO_TASK_SPACE_USER_IN_CONTROL"

try {
  await ego.snapshot();
} catch (error) {
  console.log(error.message);
  console.log(error.error_code); // "EGO_TASK_SPACE_USER_IN_CONTROL"
}

ego.onSendCDPMessageError = (message, error_code) => {
  console.log(message);
  console.log(error_code); // "EGO_TASK_SPACE_USER_IN_CONTROL"
};
ego.sendCDPMessage(JSON.stringify({
  id: 1,
  method: 'Browser.getVersion',
}));

await ego.takeOverTaskSpace();

Complete Example

js
const { profiles } = await ego.listProfiles();
const profile = profiles.find(item => !item.isDefault) ?? profiles[0];
if (!profile) {
  throw new Error('No available profile');
}

const task = await ego.createTaskSpace('checkout-flow', profile.id);
if (task.error) {
  throw new Error(`${task.error} (${task.error_code})`);
}
ego.useTaskSpace(task.id);

await ego.createTab('https://example.com/');
await ego.setAgentTaskState('Reading page');

const snapshot = await ego.snapshot({
  scope: 'only_within_viewport',
  includeActionMarks: true,
  interactiveOnly: true,
});

console.log(snapshot.content);

await ego.completeTaskSpace();

Native API Compatibility Notes

Profile-aware task-space creation

  • Added ego.listProfiles(), which returns available profiles as
    {id, name, isDefault}.
  • ego.createTaskSpace(name) now accepts an optional profile locator:
    ego.createTaskSpace(name, profileId?).
  • Profile display names are not accepted as locators because they are not
    unique. Callers must pass the id returned by ego.listProfiles().
  • Omitting profileId, passing an empty string, or passing "default" selects
    the current default regular profile.
  • Unknown profile ids resolve an error object with
    error_code: 'EGO_PROFILE_NOT_FOUND'.

Task-space ids are numeric locators

  • ego.useTaskSpace(taskId) changed to ego.useTaskSpace(id).
    id is a numeric task-space locator returned by task-space APIs.

  • ego.claimTaskSpace(name) changed to ego.claimTaskSpace(id, name?).
    The first argument identifies an existing task space by numeric id. name is
    optional and renames the task space when provided.

  • Task-space result objects now distinguish locator and display fields:

    js
    {
      id: 7,
      taskId: 'checkout-flow',
      name: 'checkout-flow',
    }

    Use id for API calls. taskId is currently the same value as name and is
    retained as a display-name compatibility field.

  • Task-scoped examples now select task spaces with task.id, including
    createTab, listTabs, snapshot, closeTaskSpace,
    completeTaskSpace, handOffTaskSpace, takeOverTaskSpace,
    setAgentTaskState, animationHighlightMouseToPosition, and raw CDP sends.

  • Browser-side operation messages now identify task spaces by numeric id, for
    example " task space completed.", " state updated.", and
    "Task space not found: ".

Error codes

  • Browser-side error results now include a stable error_code string.
  • ego.snapshot() rejects with an Error whose error_code property is set.
  • ego.onSendCDPMessageError receives (message, error_code).

Native Javascript Dialog Requirement (`docs/native-javascript-dialog-requirement.md`)

Native JavaScript dialog requirement

Ego Lite 0.5.0.12 keeps page-authored alert, confirm, and prompt dialogs
under Agent control, including a confirm opened by a file input change handler.
The real-browser regression covers these paths without a takeover or recovery
step.

beforeunload remains outstanding. In 0.5.0.12, navigation proceeds without a
dialog event even after a trusted click and sticky user activation. The desired
behavior is to keep the dialog under Agent control and report it through the
same Page API.

No page-authored dialog may change TaskSpace ownership or report
EGO_TASK_SPACE_USER_IN_CONTROL with
fallback_site_dialog_required_notice.

No new binding is required. The existing CDP bridge must:

  • forward Page.javascriptDialogOpening and
    Page.javascriptDialogClosed on the Page session;
  • accept Page.handleJavaScriptDialog on that session while the dialog is
    open, even when the triggering command is still pending;
  • preserve the dialog type, message, URL, prompt default, and supplied
    promptText.

Permission prompts and device choosers must continue to transfer control to
the user.

A dialog can outlive the ego-browser nodejs invocation that opened it. The
SDK keeps dialog state per process, and while a dialog is open Chromium does
not answer renderer commands, including Page.enable. The next invocation
therefore cannot discover the dialog and every command it sends times out.
Ego Lite 0.5.2.2 does not meet this requirement yet: in local runs the dialog
was dismissed when the invocation exited, and sometimes the TaskSpace also
moved to user control. In the field, the dialog stayed open and the next
invocation timed out. When an invocation exits with a dialog open, the bridge
must:

  • keep the dialog open and the TaskSpace Agent-owned;
  • report the open dialog to the next session that attaches to the Page, for
    example by emitting Page.javascriptDialogOpening after Page.enable.

Acceptance: trigger each JavaScript dialog from a real Page click, verify that
the TaskSpace remains Agent-owned, accept a prompt with promptText: "agent",
dismiss a confirm, accept an alert, and verify the returned values and opening
and closing events. Also set files on an intercepted file input whose change
handler opens a confirm, then accept it while DOM.setFileInputFiles is still
pending. For beforeunload, verify that the opening event and action receipt
arrive before choosing whether navigation may continue. Separately verify that
location, camera, and device prompts still transfer control to the user.


Native Sdk Lifecycle Requirement (`docs/native-sdk-lifecycle-requirement.md`)

Embedded SDK host requirements

Disposal

When Ego Lite unloads or replaces an embedded ego-browser Node context, it
should call and await the module's exported disposeEgoSdk() function before
destroying that context.

The function rejects unfinished CDP requests, clears Page/session state, and
removes onCDPMessage and onSendCDPMessageError only when they are still
owned by that SDK instance. This prevents native code from retaining callbacks
into a discarded JavaScript context.

No new binding method is required. The host only needs to honor the module
lifecycle hook. Older SDK builds without disposeEgoSdk may be unloaded as
before.

Acceptance: load the SDK, complete one CDP command, leave another command
pending, call disposeEgoSdk(), then verify that the pending command rejects
and both native callback slots are empty before the Node context is destroyed.

Syntax diagnostics

The ego-browser nodejs host compiles the heredoc before the embedded SDK can
inspect it. When compilation fails, report the message, one-based line and
column, and the corresponding source line. The direct JavaScript entry point
already provides this diagnostic, but it cannot recover source location after
the host has reduced the failure to a generic node:internal/vm stack.

No new Agent API is needed. The host may either pass the uncompiled source to
the SDK executor or format the V8 compile error itself.

Acceptance: run a heredoc containing an invalid token on line 4 and verify that
stderr identifies line 4, points to the token, and prints the source line.


Native Snapshot Ref Requirement (`docs/native-snapshot-ref-requirement.md`)

Native snapshot requirements

Compact snapshot text

Please simplify the snapshot tree before serializing content:

  • Omit text nodes that are empty, whitespace-only (including NBSP), or contain
    only zero-width formatting characters.
  • Omit an unnamed container with no remaining children.
  • Replace an unnamed container with its only remaining child.
  • Keep unnamed containers with multiple children because they preserve
    grouping.
  • Keep named or ref-bearing containers and semantic nodes such as image,
    svg_root, canvas, iframe, root, list items, and table cells.

The transformation must preserve the order of remaining nodes, refs, stable
locators, URLs, and iframe boundaries. It applies to both viewport and full-page
snapshots. Never remove or rewrite non-empty page text because it is
non-interactive, because its parent container is simplified, or while cleaning
locator metadata.

Acceptance: a fixture containing nested containers, blank text, two sibling
groups, an iframe, an image, an SVG, a canvas, and an empty table cell is
captured twice. The compact output is identical on the second pass, the sibling
groups remain distinct, and every ref still resolves to its original node.

Deferred: targeted snapshot scope

A future snapshot API should be able to limit capture to one subtree selected
by a current ref or selector, including content inside an iframe or OOPIF. The
browser should apply the scope before serializing the snapshot; filtering a
full snapshot in JS does not reduce native work or transferred content.

The selected root must resolve uniquely, retain Page and frame provenance, and
produce refs that remain valid for normal Page actions. A missing or ambiguous
root should fail clearly. The exact public API is intentionally undecided, and
this requirement is not scheduled for the current implementation.

Snapshot refs for iframe content

ego.snapshot() already includes text from ordinary iframes and OOPIFs, but
actionable nodes inside those frames are currently omitted from refs. The
result is visible content that has no @N target.

Please return a ref for every actionable frame node printed by the snapshot:

js
{
  refId: 901,
  backendNodeId: 21,
  frameId: "FRAME_OR_OOPIF_TARGET_ID",
  role: "button",
  name: "Run iframe action",
}
  • The content must print [ref=901] for this node.
  • refId must be unique within one snapshot, including across renderer
    processes. It is the value shown to the Agent.
  • backendNodeId remains the node id local to its renderer.
  • frameId identifies the frame that owns the node. It may be omitted for the
    top-level document.
  • Capturing the snapshot must not activate, focus, or reload a frame.

Acceptance: one page contains a same-origin iframe and a cross-origin OOPIF,
with repeated backend node ids in different renderers. Every printed button and
textbox has a distinct ref, and each ref resolves to the node in its own frame.

ego-browser now backfills a missing frameId from the frame AX tree when one
backend node id has only one possible owner. Native metadata is still required
when renderer-local backend ids repeat, and for actionable frame nodes omitted
from refs entirely.

Stable locator correctness

A stable locator must resolve uniquely to the same node that produced the
snapshot ref. It is invalid when it matches no node, matches more than one node,
or uniquely matches a different backendNodeId.

Please cover at least these cases in native tests:

  • repeated controls in one document, such as buttons with the same name;
  • a hidden copy and a visible editor sharing the same accessible name;
  • the same role and name in the top-level document and an iframe;
  • a locator whose element type does not match the source node.

Omit the locator when uniqueness and node identity cannot both be established.
Do not serialize loc=unstable or loc=ambiguous in content, and do not use
those status strings in refs. The JS runtime validates advertised locators
and omits invalid ones as a compatibility safeguard.


--- METRICS ---