ego-browser
Le runtime d'automatisation navigateur que les agents IA utilisent pour piloter la session Chromium réelle d'ego lite.
ego-browser est le runtime d'automatisation navigateur livré par ego lite pour les agents IA. Il parle le Chrome DevTools Protocol avec la session Chromium réelle d'ego lite, et prend pour point d'entrée un script Node.js en heredoc : l'agent écrit tout son flux JS dans un seul envoi sur stdin, tous les helpers sont injectés d'avance dans la portée du script, et l'état du navigateur survit dans un Space.
ego-browser n'est pas conçu pour qu'un humain pilote un navigateur à la main, et ce n'est pas un remplaçant de Playwright ou Puppeteer. Le lecteur visé est un agent LLM.
À qui c'est destiné
- Aux agents IA de coding qui doivent piloter un navigateur : Claude Code, Codex, Cursor, agents SDK maison.
- Aux équipes qui construisent des agents verticaux : automatiser Lark, Google Docs, Salesforce et autres back-offices.
- Aux flux web répétitifs : login, formulaire, export, recherche, lecture de tableaux.
- À ceux qui ont déjà essayé de pousser un DOM entier ou une page de HTML dans un LLM et se sont heurtés au mur des tokens.
Installation
Livré avec ego lite — voir Démarrage rapide. Une fois installé, ego-browser est appelable depuis n'importe quel dossier.
La skill peut aussi être installée seule :
npx skills add github:CitroLabs/ego-lite/skills/ego-browser
Boucle de base
Le rythme typique d'un agent qui pilote une page — tout dans un seul 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
- Réutiliser ou créer un Task Space (à déclarer dans chaque heredoc — voir Space).
- Ouvrir la page cible.
- Lire le snapshot (
snapshotText()) pour obtenir un arbre sémantique avec[ref=N, loc=..., url=...]. - Agir sur la page via
@Nou un sélecteur CSS. - Imprimer le résultat avec
cliLog(...).
Dans le heredoc, vous êtes dans un processus Node.js. Dans
js(...), vous êtes dans le contexte de la page. Ne mélangez pas les deux.
Référence des helpers
Tous les helpers sont disponibles dans la portée du script via leur nom camelCase. Pas besoin d'import.
Task Space
await listTaskSpaces()
const task = await useOrCreateTaskSpace('describe task') // réutiliser ou créer
await completeTaskSpace(task.name) // fini, on garde l'onglet
await closeTaskSpace(task.name) // on ferme l'espace
name doit décrire la tâche en 3 à 6 mots, en langage naturel. Pas de placeholder.
Navigation et état
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() // un task space tout neuf peut n'avoir aucun onglet
Observation
await snapshotText() // snapshot sémantique de toute la page (par défaut)
await snapshotText({ scope: 'only_within_viewport' })
await captureScreenshot('result.png')
await drainEvents() // consomme la file d'événements nav / réseau
Souris et scroll
click, doubleClick, hover et dragMouse acceptent le même format de cible (pixels CSS) :
'string': sélecteur CSS ou@ref. Clique au centre de l'élément.[x, y]ou{x, y}: coordonnées dans le viewport.{selector, x, y}: décalage relatif depuis le coin haut-gauche de l'élément.options.label: description de 3 à 6 mots. Si fournie, l'action déclenche une animation de surlignage.
await click('@21', { label: 'vérifier la session' })
await click('button.primary', { label: 'cliquer sur Envoyer' })
await click([420, 260])
await hover('@5', { label: 'survoler le menu' })
await dragMouse([from, to], { label: 'déplacer la carte' })
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 },
)
Clavier et saisie
await typeText('hello world')
await fillInput('@2', 'user@test.com')
await pressKey('Enter')
await dispatchKey({ ... })
Fichiers et réseau
await uploadFile('input[type="file"]', '/absolute/path/to/file.pdf')
await httpGet('https://api.example.com/data') // GET émis dans le contexte de la page
Attente
await wait(1) // secondes
await waitForLoad()
await waitForElement('@1')
await waitForNetworkIdle()
wait()ettimeoutsont en secondes. Seuls les paramètres terminant parMssont en millisecondes.
Exécution dans le navigateur
js(source) est en réalité un Runtime.evaluate et reçoit une chaîne. Ne lui passez pas une fonction et des arguments à la manière de Puppeteer — vous obtenez un warning, le tout est emballé dans un .toString(), et les variables capturées comme les arguments disparaissent.
Pour une logique multi-étapes, encapsulez tout dans une IIFE qui renvoie une seule valeur :
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' })
Sortie et auto-découverte
cliLog(value) // le seul canal de sortie dans un heredoc
cliLog(help('click')) // consulter l'usage d'un helper
Workflow recommandé
Commencez par snapshotText plus ref / loc — la sémantique est préservée, et on évite la fragilité des coordonnées :
- Réutiliser ou créer le Task Space.
- Ouvrir ou changer de page (
openOrReuseTab/gotoAndWait). snapshotText()pour obtenir l'arbre[ref=N, loc=..., url=...]. Les refs sont automatiquement enregistrées dans la refMap.- Agir sur
@Navecclick/fillInput/elementEval, ou faire une extraction DOM en une fois dansjs(...). cliLog(...)du résultat final.
D'autres trajectoires à combiner :
captureScreenshot+click([x, y]): mises en page visuelles, UI à base de canvas, listes virtualisées, pages dont l'accessibilité est incomplète.js/elementEval/cdp: extraire le DOM directement, inspecter l'état du navigateur, ou tout ce qui ne rentre pas proprement dans un helper standard.
Gardez navigation, observation, scroll, extraction, filtrage, agrégation et sortie dans un seul heredoc
ego-browser nodejs. Ne réintroduisez pas un second scriptnodelocal pour rejouer les mêmes données.
Portée des refs
@N n'est valide que pour la refMap du dernier snapshotText. Chaque snapshotText() reconstruit la refMap. Les numéros viennent du backendNodeId CDP de l'élément, donc le même élément garde généralement le même numéro d'un snapshot à l'autre — mais pour pouvoir utiliser @N, N doit figurer dans le dernier snapshot.
Causes classiques de Unknown ref :
- L'élément est sorti du viewport.
- Le DOM a été re-rendu.
- Le tour précédent utilisait
scope: 'only_within_viewport', et le suivant ne couvre pas cet élément.
Quand il faut référencer le même élément sur plusieurs tours, utilisez le sélecteur loc=... ou un sélecteur CSS. C'est aussi la base de l'accumulation d'expérience — voir Skills.
Skill workspace
ego-browser ne transporte pas d'expérience d'agent modifiable par défaut. Il charge les extensions de helper et l'expérience apprise depuis la bundle de skills du dépôt :
../../skills/ego-browser
À surcharger via une variable d'environnement :
EGO_BROWSER_AGENT_WORKSPACE=/path/to/ego-browser ego-browser nodejs <<'EOF'
cliLog(await siteSkills())
EOF
L'expérience par site sous learnings/ est toujours active ; chaque appel de helper la relit. Le modèle d'écriture et de découverte est décrit dans Skills.
Vérifier les expériences apprises :
npm run validate:learnings
Arborescence
package/ego-browser/
├── src/ # browser-runtime / helpers / run.js
│ ├── browser-runtime.js # pont ego runtime côté navigateur
│ ├── helpers.js # helpers exposés au script d'agent
│ ├── run.js # point d'entrée CLI (exécute stdin)
│ └── learning/ # index d'expérience, validation domaine, validation format
├── artifacts/ego-browser/ # build ; npm bin pointe ici
└── test/ # tests unitaires
skills/ego-browser/
├── SKILL.md / SKILL.zh.md # point d'entrée pour l'agent
└── learnings/ # dossier d'expérience par site
Notes
snapshotText()est enscope: 'full_page'par défaut. Passez'only_within_viewport'uniquement si vous voulez vraiment ne couvrir que la zone visible.js()renvoie directement le résultat d'évaluation. Ne leJSON.parse(...)pas en plus.- Pour écrire un regex dans un template
js(), doublez les antislashs (\\d,\\s) ou passez àString.raw. - Un
returnau top niveau est automatiquement enveloppé dans une IIFE. Unreturndans un callback imbriqué peut déclencher la même chose, alors écrivez les expressions complexes directement en(() => { ... })(). - Quand l'utilisateur demande explicitement ego-browser, le runtime est prêt. N'allez pas faire de pré-vérification (
which ego-browser/node -v/ dump de l'aide) — réservez ça à un cas où une première exécution a vraiment échoué.