ego-browser
O runtime de automação de navegador que os agentes de IA usam para dirigir a sessão Chromium real do ego lite.
O ego-browser é o runtime de automação de navegador que o ego lite entrega para os agentes de IA. Ele conversa pelo Chrome DevTools Protocol com a sessão Chromium real dentro do ego lite e recebe como ponto de entrada um script Node.js em heredoc: o agente escreve o fluxo JS inteiro em uma única entrega no stdin, todos os helpers já vêm injetados no escopo, e o estado do navegador segue vivo dentro de um Space.
ego-browser não foi pensado para uma pessoa dirigir o navegador na mão, e também não substitui Playwright ou Puppeteer. O leitor pretendido é um agente LLM.
Para quem é
- Agentes de coding com IA que precisam dirigir um navegador: Claude Code, Codex, Cursor, agentes SDK próprios.
- Times construindo agentes verticais: automatizar Lark, Google Docs, Salesforce e back-offices parecidos.
- Fluxos web repetitivos: login, preencher formulário, exportar, buscar, ler tabela.
- Quem já tentou enfiar um DOM inteiro ou uma página de HTML num LLM e bateu no limite de token.
Instalar
Vem junto com o ego lite — veja Início rápido. Após instalar, basta chamar ego-browser de qualquer diretório.
A skill também pode ser instalada sozinha:
npx skills add github:CitroLabs/ego-lite/skills/ego-browser
Ciclo principal
O ritmo típico do agente operando uma página, tudo num único 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
- Reaproveitar ou criar um Task Space (declare em cada heredoc — veja Space).
- Abrir a página alvo.
- Ler o snapshot (
snapshotText()) para receber a árvore semântica com[ref=N, loc=..., url=...]. - Agir na página por
@Nou seletor CSS. - Imprimir o resultado com
cliLog(...).
Dentro do heredoc, você está num processo Node.js. Dentro de
js(...), você está no contexto da página. Não misture.
Referência de helpers
Todos os helpers estão disponíveis no escopo do script pelo nome camelCase. Não precisa de import.
Task Space
await listTaskSpaces()
const task = await useOrCreateTaskSpace('describe task') // reusa ou cria
await completeTaskSpace(task.name) // concluído, mantém a aba
await closeTaskSpace(task.name) // não precisa mais, fecha o space
name deve descrever a tarefa em 3 a 6 palavras, em linguagem natural. Sem placeholder.
Navegação e estado
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() // um task space recém-criado pode não ter aba ainda
Observação
await snapshotText() // snapshot semântico da página toda (padrão)
await snapshotText({ scope: 'only_within_viewport' })
await captureScreenshot('result.png')
await drainEvents() // consumir a fila de eventos de navegação / rede
Mouse e scroll
click, doubleClick, hover e dragMouse aceitam o mesmo formato de target (pixels CSS):
'string': seletor CSS ou@ref. Clica no centro do elemento.[x, y]ou{x, y}: coordenadas no viewport.{selector, x, y}: deslocamento a partir do canto superior esquerdo do elemento.options.label: descrição em 3 a 6 palavras; se passada, a ação dispara animação de destaque.
await click('@21', { label: 'conferir o login' })
await click('button.primary', { label: 'clicar no botão Enviar' })
await click([420, 260])
await hover('@5', { label: 'passar o mouse no menu' })
await dragMouse([from, to], { label: 'arrastar o card' })
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 },
)
Teclado e entrada
await typeText('hello world')
await fillInput('@2', 'user@test.com')
await pressKey('Enter')
await dispatchKey({ ... })
Arquivos e rede
await uploadFile('input[type="file"]', '/absolute/path/to/file.pdf')
await httpGet('https://api.example.com/data') // GET disparado no contexto da página
Esperas
await wait(1) // segundos
await waitForLoad()
await waitForElement('@1')
await waitForNetworkIdle()
wait()etimeoutsão em segundos. Só parâmetros terminados emMssão em milissegundos.
Execução no navegador
js(source) é, na prática, um Runtime.evaluate e aceita uma string. Não passe função + argumentos no estilo Puppeteer — ele dá warning, envolve tudo em .toString(), e as variáveis capturadas e o canal de argumentos somem.
Para lógica multi-step, embrulhe em uma IIFE que retorna uma única vez:
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' })
Saída e auto-descoberta
cliLog(value) // o único canal de saída dentro de um heredoc
cliLog(help('click')) // consultar o uso de um helper
Fluxo recomendado
Comece por snapshotText + ref / loc — preserva a semântica e evita a fragilidade de coordenadas:
- Reaproveitar ou criar o Task Space.
- Abrir ou trocar de página (
openOrReuseTab/gotoAndWait). snapshotText()para receber a árvore[ref=N, loc=..., url=...]. Refs são registradas automaticamente no refMap.- Agir sobre
@Ncomclick/fillInput/elementEval, ou fazer extração de DOM em um únicojs(...). cliLog(...)com o resultado final.
Outros caminhos para combinar:
captureScreenshot+click([x, y]): layouts visuais, UIs em canvas, listas virtualizadas, páginas com acessibilidade incompleta.js/elementEval/cdp: extrair DOM diretamente, inspecionar estado do navegador, ou qualquer coisa que não encaixa direito num helper padrão.
Mantenha navegação, observação, scroll, extração, filtragem, agregação e saída dentro de um único heredoc
ego-browser nodejs. Não passe os mesmos dados para um segundo scriptnodelocal.
Validade das refs
@N só vale para o refMap do último snapshotText. Cada snapshotText() reconstrói o refMap. Os números das refs vêm do backendNodeId CDP do elemento, então o mesmo elemento costuma manter o mesmo número entre snapshots — mas para operar @N, N precisa aparecer no snapshot mais recente.
Causas comuns de Unknown ref:
- O elemento saiu do viewport.
- O DOM re-renderizou.
- A rodada anterior usou
scope: 'only_within_viewport'e a atual não cobriu o elemento.
Quando precisar de uma referência estável ao mesmo elemento por várias rodadas, use o loc=... do snapshot ou escreva CSS selector direto. É também a base da acumulação de Experience — veja Skills.
Skill workspace
O ego-browser por si só não carrega experiência mutável de agente. Por padrão ele carrega extensões de helper e a experiência aprendida sobre sites a partir do bundle de skills do repo:
../../skills/ego-browser
Pode ser sobrescrito por variável de ambiente:
EGO_BROWSER_AGENT_WORKSPACE=/path/to/ego-browser ego-browser nodejs <<'EOF'
cliLog(await siteSkills())
EOF
A experiência por site em learnings/ fica sempre ativa, e é lida em toda chamada de helper. O modelo de escrita e descoberta de Experience está em Skills.
Validar a experiência aprendida:
npm run validate:learnings
Estrutura de pastas
package/ego-browser/
├── src/ # browser-runtime / helpers / run.js
│ ├── browser-runtime.js # ponte ego runtime do lado do navegador
│ ├── helpers.js # helpers expostos ao script do agente
│ ├── run.js # entrada CLI (executa stdin)
│ └── learning/ # índice de experiência, validação de domínio, validação de formato
├── artifacts/ego-browser/ # resultado de build; npm bin aponta para cá
└── test/ # testes unitários
skills/ego-browser/
├── SKILL.md / SKILL.zh.md # ponto de entrada para o agente
└── learnings/ # diretório de experiência por site
Observações
snapshotText()usascope: 'full_page'por padrão e cobre a página inteira. Só passe'only_within_viewport'quando realmente precisar só da área visível.js()devolve direto o resultado da expressão; não façaJSON.parse(...)por cima.- Quando escrever regex em template string dentro de
js(), duplique as contrabarras (\\d,\\s) ou useString.raw. returnno topo é embrulhado em IIFE automaticamente.returnem callback aninhado também pode disparar isso, então escreva expressões complexas como(() => { ... })()direto.- Quando o usuário pediu explicitamente o ego-browser, o runtime já está pronto. Não faça pré-checagem (
which ego-browser/node -v/ dump do help) a menos que uma execução de fato dê erro.