ego (lite) is just a browser, ego is your personal agent across devices.
Join waitlist
Русский

ego-browser

Рантайм автоматизации браузера, которым AI-агенты управляют реальной сессией Chromium внутри ego lite.

llms.txt

ego-browser — это рантайм автоматизации браузера, который ego lite поставляет для AI-агентов. По Chrome DevTools Protocol он подключается к реальной сессии Chromium внутри ego lite, а в качестве точки входа принимает Node.js heredoc-скрипт: агент пишет весь JS-сценарий за одну подачу на stdin, все helper'ы заранее доступны в области видимости скрипта, а состояние браузера сохраняется в Space.

ego-browser не задуман для ручного управления браузером и не заменяет Playwright или Puppeteer. Целевой читатель — LLM-агент.

Кому подходит

  • AI-кодинг-агентам, которым нужно водить браузер: Claude Code, Codex, Cursor, кастомные SDK-агенты.
  • Командам, делающим вертикальных агентов: автоматизация Lark, Google Docs, Salesforce и других бэк-офисов.
  • Повторяющимся веб-флоу: логин, заполнение формы, экспорт, поиск, чтение таблиц.
  • Всем, кто пытался запихнуть полный DOM или страницу HTML в LLM и упирался в лимит токенов.

Установка

Идёт в комплекте с ego lite — см. Быстрый старт. После установки ego-browser доступен из любой директории.

Скилл можно поставить и отдельно:

npx skills add github:CitroLabs/ego-lite/skills/ego-browser

Базовый цикл

Типичный ритм агента, управляющего страницей, — всё внутри одного heredoc:

ego-browser nodejs <<'EOF'
const task = await useOrCreateTaskSpace('search github issues')

await openOrReuseTab('https://github.com/issues', { wait: true, timeout: 20 })

cliLog(await snapshotText())

EOF
  1. Переиспользовать или создать Task Space (в каждом heredoc — см. Space).
  2. Открыть нужную страницу.
  3. Прочитать снимок (snapshotText()) и получить семантическое дерево с [ref=N, loc=..., url=...].
  4. Действовать по странице через @N или CSS-селектор.
  5. Вывести итог через cliLog(...).

Внутри heredoc вы в Node.js-процессе; внутри js(...) — в контексте страницы. Не смешивайте.

Справочник helper'ов

Все helper'ы доступны в области видимости скрипта под camelCase именами, без import.

Task Space

await listTaskSpaces()
const task = await useOrCreateTaskSpace('describe task')   // переиспользовать или создать
await completeTaskSpace(task.name)                         // завершено, вкладку оставляем
await closeTaskSpace(task.name)                            // больше не нужно, закрываем space

name — описание задачи на естественном языке в 3–6 слов. Не используйте плейсхолдеры.

Навигация и состояние

await listTabs()
await openOrReuseTab(url, { wait: true, timeout: 20 })
await gotoAndWait(url, { timeout: 20, settle: 1 })
await newTab(url)
await switchTab(tabId)
await currentTab()
await pageInfo()
await ensureRealTab()        // в только что созданном task space вкладок может ещё не быть

Наблюдение

await snapshotText()                              // семантический снимок всей страницы (по умолчанию)
await snapshotText({ scope: 'only_within_viewport' })
await captureScreenshot('result.png')
await drainEvents()                               // вычитать очередь событий навигации / сети

Мышь и скролл

click, doubleClick, hover и dragMouse принимают единый формат target (CSS-пиксели):

  • 'string': CSS-селектор или @ref, клик по центру элемента.
  • [x, y] или {x, y}: координаты viewport.
  • {selector, x, y}: смещение от левого верхнего угла элемента.
  • options.label: описание в 3–6 слов; если передать, действие сопровождается визуальной подсветкой.
await click('@21', { label: 'проверить логин' })
await click('button.primary', { label: 'нажать кнопку «Отправить»' })
await click([420, 260])
await hover('@5', { label: 'навести курсор на меню' })
await dragMouse([from, to], { label: 'перетащить карточку' })

await scrollBy(900)
await scroll({ dy: 900 })
await scrollToBottomUntil(
  async () => await js(String.raw`document.querySelectorAll('article').length`) >= 20,
  { step: 900, wait: 1, maxSteps: 20 },
)

Клавиатура и ввод

await typeText('hello world')
await fillInput('@2', 'user@test.com')
await pressKey('Enter')
await dispatchKey({ ... })

Файлы и сеть

await uploadFile('input[type="file"]', '/absolute/path/to/file.pdf')
await httpGet('https://api.example.com/data')   // GET-запрос из контекста страницы

Ожидания

await wait(1)                                    // секунды
await waitForLoad()
await waitForElement('@1')
await waitForNetworkIdle()

wait() и timeout — в секундах. Только параметры, оканчивающиеся на Ms, — в миллисекундах.

Выполнение в браузере

js(source) по сути Runtime.evaluate и принимает строку. Не передавайте функцию и аргументы в стиле Puppeteer — получите варнинг, всё обернётся в .toString(), и захваченные переменные с каналом аргументов потеряются.

Многошаговую логику оборачивайте в IIFE и возвращайте одно значение:

const data = await js(String.raw`(() => {
  const items = [...document.querySelectorAll('article')]
  return items.map(el => ({
    text: el.innerText,
    links: [...el.querySelectorAll('a')].map(a => a.href),
  }))
})()`)

await elementEval('@1', el => el.getBoundingClientRect())
await cdp('Page.captureScreenshot', { format: 'png' })

Вывод и самоописание

cliLog(value)                  // единственный канал вывода внутри heredoc
cliLog(help('click'))          // посмотреть, как пользоваться helper'ом

Рекомендованный воркфлоу

Начинайте с snapshotText + ref / loc — семантика сохраняется, и нет хрупкости координат:

  1. Переиспользовать или создать Task Space.
  2. Открыть страницу или переключиться (openOrReuseTab / gotoAndWait).
  3. snapshotText() для дерева [ref=N, loc=..., url=...]. Ref'ы автоматически регистрируются в refMap.
  4. Действовать по @N через click / fillInput / elementEval или сделать одноразовое DOM-извлечение внутри js(...).
  5. cliLog(...) с итоговым результатом.

Полезные комбинации:

  • captureScreenshot + click([x, y]): визуальная разметка, canvas-UI, виртуализированные списки, страницы с дырявой доступностью.
  • js / elementEval / cdp: прямое извлечение DOM, просмотр состояния браузера, или когда стандартный helper не дотягивает.

Держите навигацию, наблюдение, скролл, извлечение, фильтрацию, агрегацию и вывод в одном heredoc ego-browser nodejs. Не перекачивайте те же данные через второй локальный node-скрипт.

Область действия ref

@N действителен только для refMap последнего snapshotText. Каждый вызов snapshotText() пересобирает refMap. Номера refs берутся из CDP backendNodeId элемента, поэтому один и тот же элемент часто получает один и тот же номер в нескольких снимках — но чтобы @N сработал, N должен присутствовать в последнем снимке.

Типичные причины Unknown ref:

  • Элемент уехал за пределы viewport.
  • DOM перерисовался.
  • Предыдущий проход был scope: 'only_within_viewport', а текущий не покрыл этот элемент.

Когда нужна стабильная ссылка на один и тот же элемент через много проходов, используйте loc=... из вывода snapshot или CSS-селектор. На этом же стоит накопление Experience — см. Skills.

Skill workspace

ego-browser не несёт изменяемого опыта агента сам по себе. По умолчанию он подгружает расширения helper'ов и накопленный опыт сайтов из skill-bundle репозитория:

../../skills/ego-browser

Переопределяется переменной окружения:

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

Опыт по сайтам в learnings/ активен всегда; при каждом вызове helper'а он перечитывается. Запись и обнаружение Experience описаны в Skills.

Проверить накопленный опыт:

npm run validate:learnings

Структура каталогов

package/ego-browser/
├── src/                      # browser-runtime / helpers / run.js
│   ├── browser-runtime.js    # мост ego runtime на стороне браузера
│   ├── helpers.js            # helper'ы, выставленные в скрипт агента
│   ├── run.js                # CLI-вход (исполняет stdin)
│   └── learning/             # индекс опыта, валидация домена, валидация формата
├── artifacts/ego-browser/    # результат сборки; npm bin указывает сюда
└── test/                     # unit-тесты

skills/ego-browser/
├── SKILL.md / SKILL.zh.md    # точка входа агента
└── learnings/                # каталог опыта по сайтам

Замечания

  • По умолчанию snapshotText() использует scope: 'full_page' — снимает всю страницу. Передавайте 'only_within_viewport', только если правда нужна только видимая область.
  • js() возвращает уже результат вычисления выражения, повторный JSON.parse(...) не нужен.
  • Когда пишете regex в шаблонной строке js(), удваивайте обратные слэши (\\d, \\s) или используйте String.raw.
  • return верхнего уровня автоматически оборачивается в IIFE. Вложенный return в коллбэке тоже может это вызвать, поэтому сложные выражения лучше сразу писать как (() => { ... })().
  • Если пользователь явно попросил ego-browser, рантайм уже готов. Не делайте предварительных проверок which ego-browser / node -v / дампа help — только если первый запуск реально упал с ошибкой.