# Technical Documentation: citrolabs/ego-lite

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/citrolabs/ego-lite)

### Overview (`README.md`)

<div align="center">

<img src="docs/assets/banner.png" alt="ego lite" width="100%" />

**The fastest browser for AI agents to run web automation**

<a href="https://trendshift.io/repositories/42334?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-42334" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/42334" alt="citrolabs%2Fego-lite | Trendshift" width="250" height="55"/></a>

<p>
  <a href="https://cdn.ego.app/setup/macos/arm64/egolite-Y7MbxKIuhzFB.dmg"><img src="https://img.shields.io/badge/Download-Apple%20Silicon-000000?style=for-the-badge&logo=apple&logoColor=white" alt="Download for Apple Silicon" /></a>
  <a href="https://cdn.ego.app/setup/macos/x64/egolite-Y7MbxKIuhzFB.dmg"><img src="https://img.shields.io/badge/Download-Intel-000000?style=for-the-badge&logo=apple&logoColor=white" alt="Download for Intel" /></a>
  <a href="https://discord.gg/5eGZVvHbTq"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord" /></a>
  <a href="https://x.com/ego_agent"><img src="https://img.shields.io/badge/Follow-%40ego__agent-000000?style=for-the-badge&logo=x&logoColor=white" alt="Follow @ego_agent on X" /></a>
  <a href="https://lite.ego.app/document/"><img src="https://img.shields.io/badge/Docs-lite.ego.app-1E90FF?style=for-the-badge&logo=gitbook&logoColor=white" alt="Docs" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-3DA639?style=for-the-badge" alt="License MIT" /></a>
</p>

<p>
  <b>English</b> ·
  <a href="README.zh-CN.md">简体中文</a> ·
  <a href="README.ja.md">日本語</a> ·
  <a href="README.ko.md">한국어</a> ·
  <a href="README.pt.md">Português</a> ·
  <a href="README.es.md">Español</a> ·
  <a href="README.fr.md">Français</a> ·
  <a href="README.it.md">Italiano</a> ·
  <a href="README.ru.md">Русский</a>
</p>

</div>

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](https://lite.ego.app/roadmap).

### 1. Install

Pick whichever fits your flow.

**1.1 Download the macOS app**

<a href="https://cdn.ego.app/setup/macos/arm64/egolite-Y7MbxKIuhzFB.dmg"><img src="https://img.shields.io/badge/⬇%20Apple%20Silicon-.dmg-000000?style=for-the-badge&logo=apple&logoColor=white" alt="Download ego lite for Apple Silicon" /></a>
<a href="https://cdn.ego.app/setup/macos/x64/egolite-Y7MbxKIuhzFB.dmg"><img src="https://img.shields.io/badge/⬇%20Intel-.dmg-000000?style=for-the-badge&logo=apple&logoColor=white" alt="Download ego lite for Intel" /></a>

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:

```
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:

```
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.

<div align="center">

<img src="docs/assets/ego-vs-agent-benchmark.png" alt="ego lite vs agent-browser, speed and cost across four tasks" width="100%" />

</div>

## Docs

Tutorials, the full tool reference, and integration guides live at [lite.ego.app/document/](https://lite.ego.app/document/).

## Community

- [Discord](https://discord.gg/5eGZVvHbTq), questions, setup help, and skill sharing
- [GitHub Discussions](https://github.com/citrolabs/ego-lite/discussions), ideas and longer threads
- [X/Twitter](https://x.com/ego_agent), updates and releases

## Star History

<a href="https://github.com/citrolabs/ego-lite/stargazers">


<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/citrolabs/ego-lite/star-history/star-history-dark.svg">
  <img alt="Star history" src="https://raw.githubusercontent.com/citrolabs/ego-lite/star-history/star-history-light.svg">
</picture>

</a>

## License

The contents of this repository are released under the [MIT License](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

```
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](https://code.claude.com/docs/en/plugins-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](https://developers.openai.com/plugins/deploy/submission) and
the [submission error reference](https://developers.openai.com/plugins/deploy/submission-errors).

See the [OpenAI plugin guide](https://developers.openai.com/plugins/build/plugins).

### 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](https://docs.github.com/en/copilot/concepts/agents/about-plugins).

### 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](https://www.codebuddy.cn/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/Plug-In)
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](https://qwenwork.cn/docs/desktop/expert-kits).

### 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](https://opencode.ai/docs/plugins/).

### 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](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md).

## 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](native-javascript-dialog-requirement.md).

## 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
`"<id> 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
`"<id> 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
`"<id> 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
`"<id> 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 `"<id> 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 `"<id> 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 `"<id> task space completed."`, `"<id> state updated."`, and
  `"Task space not found: <id>"`.

### 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 ---
- Files Extracted: 9
- Estimated Token Budget: ~16997 tokens
- Canonical Reference: https://codewiki.google/github.com/citrolabs/ego-lite
