CDP — Compétence browser-harness-js
SDK CDP généré sur mesure (chaque méthode des fichiers browser_protocol.json + js_protocol.json dispose d'un wrapper typé) plus un petit serveur HTTP qui maintient une session CDP persistante. La CLI browser-harness-js démarre automatiquement le serveur lors de la première utilisation et transmet des extraits JS vers celui-ci.
Le SDK se trouve dans le répertoire sdk/ de la compétence. Dans le reste de ce document, <rép-comp> fait référence à l'endroit où npx skills add a installé la compétence (Claude Code : ~/.claude/skills/cdp ; Cursor : ~/.cursor/skills/cdp ; autres agents variables). La CLI devrait être accessible via le PATH sous le nom browser-harness-js.
Configuration (une fois, première utilisation)
npx skills add place la compétence dans le répertoire de compétences de votre agent mais ne met PAS la CLI sur le PATH. Avant le premier appel, vérifiez qu'elle est accessible et créez un lien symbolique vers un répertoire sur votre PATH si ce n'est pas le cas :
# macOS (Apple Silicon + Homebrew)
command -v browser-harness-js >/dev/null || ln -sf <rép-comp>/sdk/browser-harness-js /opt/homebrew/bin/browser-harness-js
# macOS (Intel) / plupart des Linux — peut nécessiter sudo
command -v browser-harness-js >/dev/null || ln -sf <rép-comp>/sdk/browser-harness-js /usr/local/bin/browser-harness-js
# Linux sans sudo (assurez-vous que ~/.local/bin est dans le PATH)
command -v browser-harness-js >/dev/null || { mkdir -p ~/.local/bin && ln -sf <rép-comp>/sdk/browser-harness-js ~/.local/bin/browser-harness-js; }
La CLI installe automatiquement bun au premier lancement s'il manque (le serveur est natif Bun). Définissez BROWSER_HARNESS_SKIP_BUN_INSTALL=1 pour refuser.
Comment utiliser
Exécutez simplement browser-harness-js '<JS>'. Le premier appel lance le serveur en arrière-plan ; les appels suivants atteignent le même processus et réutilisent donc la même session, la même WebSocket vers Chrome et toutes les variables globales que vous avez définies.
browser-harness-js 'await session.connect()'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'
browser-harness-js '(await session.Runtime.evaluate({expression:"document.title",returnByValue:true})).result.value'
La sortie est le contenu brut du résultat — pas d'enveloppe {ok,result}.
| Type de résultat | stdout |
|---|---|
| chaîne | texte brut, sans guillemets JSON (ex. Exemple de domaine) |
| nombre / booléen | 42, true |
| objet / tableau (non vide) | JSON compact (ex. {"frameId":"..."}, [1,2,3]) |
undefined / null / "" / {} / [] | vide (pas de sortie) |
Les erreurs vont sur stderr, code de sortie 1. Le message d'erreur CDP et la pile JS sont imprimés textuellement, ex. :
Error: CDP -32602: invalid params
at _call (.../session.ts:117:33)
...
Détectez l'échec avec if browser-harness-js '...'; then ...; else handle_error; fi ou en vérifiant $?.
Extraits multi-lignes via stdin (heredoc). Important : un extrait multi-instructions ne renvoie PAS automatiquement la dernière expression — écrivez return X explicitement. Les extraits à expression unique passés comme premier argument renvoient automatiquement.
browser-harness-js <<'EOF'
const tabs = await listPageTargets();
globalThis.tid = tabs[0].targetId;
await session.use(globalThis.tid);
return globalThis.tid;
EOF
Commandes CLI
| Commande | Comportement |
|---|---|
browser-harness-js '<js>' | Démarrage automatique du serveur si nécessaire, évalue le JS, affiche le résultat. |
browser-harness-js <<EOF…EOF | Idem, code depuis stdin. |
browser-harness-js --status | Affiche le JSON de santé (uptime, connecté, sessionId) ou sort avec le code 1 si arrêté. |
browser-harness-js --start | Démarrage explicite (sans effet si déjà en cours). |
browser-harness-js --stop | Arrêt gracieux. Supprime l'état de la session. |
browser-harness-js --restart | Arrêt + nouveau démarrage. |
browser-harness-js --logs | tail -f du journal du serveur (/tmp/browser-harness-js.log). |
Variables d'environnement : CDP_REPL_PORT (défaut 9876), CDP_REPL_LOG (défaut /tmp/browser-harness-js.log).
Surface API dans les extraits
Ces variables globales sont pré-chargées — pas besoin d'imports :
session— laSessionpersistante. Possède tous les domaines CDP montés :session.Page,session.DOM,session.Runtime,session.Network, … 56 domaines, 652 méthodes au total.listPageTargets()— liste les cibles de page réelles viaTarget.getTargetsdu CDP (fonctionne aussi sur Chrome 144+), avec les URLchrome://etdevtools://filtrées. Pas d'arguments — utilise la session connectée.detectBrowsers()— analyse les répertoires de profil spécifiques au système d'exploitation pour trouver les navigateurs basés sur Chromium en cours d'exécution avec débogage à distance activé. Renvoie[{name, profileDir, port, wsPath, wsUrl, mtimeMs}], trié par lancement le plus récent.resolveWsUrl(opts)— résout une URL WS à partir de{wsUrl}|{port, host?}|{profileDir}. Pour le flux de détection automatique sans argument, appelezsession.connect()directement à la place.CDP— les espaces de noms générés (CDP.Page,CDP.Runtime, …) pour référence de nom de type.
Appeler une méthode CDP
Chaque méthode prend un seul argument objet correspondant aux paramètres filaires CDP ; elle se résout en la valeur de retour typée (pas d'enveloppe result, pas de corrélation id — géré pour vous).
// pas de paramètres
await session.DOM.enable()
// paramètres obligatoires
await session.Page.navigate({ url: 'https://example.com' })
// paramètres tous optionnels (l'objet aussi optionnel)
await session.Page.captureScreenshot()
await session.Page.captureScreenshot({ format: 'png', quality: 80 })
// les retours sont réduits à la forme typée
const { root } = await session.DOM.getDocument()
const { nodeId } = await session.DOM.querySelector({ nodeId: root.nodeId, selector: 'h1' })
Connexion
Par défaut : appelez simplement session.connect() sans arguments. Il détecte automatiquement les navigateurs basés sur Chromium en cours d'exécution (Chrome, Chromium, Edge, Brave, Arc, Vivaldi, Opera, Comet, Canary) en analysant les répertoires de profil spécifiques au système d'exploitation pour un fichier DevToolsActivePort, trié par lancement le plus récent, et choisit le premier dont la WebSocket accepte. Indépendant du système d'exploitation — fonctionne sur macOS, Linux, Windows.
await session.connect() // détection automatique
Utilisez detectBrowsers() d'abord si vous voulez voir ce qui est disponible (ou laisser l'utilisateur choisir) avant de vous connecter :
const found = await detectBrowsers()
// [{ name: 'Google Chrome', profileDir, port, wsPath, wsUrl, mtimeMs }, ...]
Formes explicites — utilisez-les uniquement lorsque la détection automatique choisit le mauvais navigateur, ou lorsque vous savez déjà où vous connecter :
| Forme | Quand l'utiliser |
|---|---|
{ profileDir } | Cibler un navigateur spécifique lorsque plusieurs sont en cours d'exécution. Lit directement <profileDir>/DevToolsActivePort. |
{ wsUrl } | Vous avez déjà ws://…/devtools/browser/<uuid> (ex. transmis depuis ailleurs). |
await session.connect({ profileDir: '/Users/<vous>/Library/Application Support/Google/Chrome' })
await session.connect({ wsUrl: 'ws://127.0.0.1:9222/devtools/browser/<uuid>' })
Chemins de profil par système d'exploitation — utilisez-les avec { profileDir } :
- macOS :
~/Library/Application Support/<Navigateur>(ex.Google/Chrome,Comet,BraveSoftware/Brave-Browser,Arc/User Data) - Linux :
~/.config/<navigateur>(ex.google-chrome,chromium,BraveSoftware/Brave-Browser) - Windows :
%LOCALAPPDATA%\<Navigateur>\User Data(ex.Google\Chrome,Microsoft\Edge,BraveSoftware\Brave-Browser)
Le timeout d'ouverture WS par candidature est par défaut de 5 s — les navigateurs actifs répondent par une ouverture/fermeture en ~100 ms, donc 5 s est déjà généreux. Le seul cas où 5 s est trop court est lorsque Chrome affiche la fenêtre contextuelle Autoriser et attend que l'utilisateur clique. Si vous vous attendez à cela, passez timeoutMs: 30000 :
await session.connect({ profileDir: '/Users/<vous>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })
Si vous voyez Aucun navigateur détecté n'a accepté la connexion — les navigateurs ont des fichiers DevToolsActivePort mais aucun ne dessert actuellement de WS. La cause la plus courante : le débogage à distance est activé mais l'utilisateur n'a pas encore cliqué sur Autoriser dans l'invite. Dites-lui de cliquer sur Autoriser, puis réessayez (ou augmentez timeoutMs).
Choisir une cible (onglet)
Après connect(), appelez session.use(targetId) une fois ; les appels ultérieurs au niveau de la page (Page/DOM/Runtime/Network/etc.) sont automatiquement routés vers le sessionId de cette cible. Les appels Browser.* et Target.* atteignent toujours le point de terminaison du navigateur.
const tabs = await listPageTargets() // pas d'arguments ; utilise la session connectée
const sid = await session.use(tabs[0].targetId)
await session.Page.enable()
await session.Page.navigate({ url: 'https://example.com' })
listPageTargets() utilise Target.getTargets du CDP (pas /json), donc cela fonctionne aussi sur Chrome 144+. Il filtre déjà les URL chrome:// et devtools://. Appel brut équivalent :
const { targetInfos } = await session.Target.getTargets({})
const tabs = targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'))
Pour changer d'onglet : session.use(otherTargetId). Pour se détacher : session.setActiveSession(undefined).
Événements
// S'abonner (renvoie une fonction de désabonnement)
const off = session.onEvent((method, params, sessionId) => { ... })
// Ou attendre un seul événement correspondant avec prédicat et timeout optionnels
await session.Network.enable()
const ev = await session.waitFor(
'Page.frameNavigated',
(p) => p.frame.url.includes('example.com'),
10_000
)
Persistance de l'état entre les appels
Chaque extrait s'exécute dans son propre wrapper asynchrone, donc ses déclarations let/const disparaissent lorsqu'il se termine. Pour transporter des données, attachez-les à globalThis :
browser-harness-js '(await listPageTargets()).forEach((t,i)=>globalThis["tab"+i]=t.targetId)'
browser-harness-js 'await session.use(globalThis.tab0)'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'
session elle-même, le sessionId actif et les abonnés aux événements sont déjà préservés par le serveur — les variables globales ne sont nécessaires que pour les données ad hoc.
Connexion à un Chrome en cours d'exécution (flux chrome://inspect)
Lors de la connexion au navigateur déjà en cours d'exécution de l'utilisateur :
- Essayez
await session.connect()d'abord (sans arguments) — la détection automatique gère tous les navigateurs basés sur Chromium viaDevToolsActivePort. Si elle réussit, vous avez terminé. - Si la détection automatique échoue avec
Aucun navigateur en cours d'exécution avec débogage à distance détecté, l'utilisateur doit l'activer. Ouvrez la page d'inspection :Seule la voie AppleScript de macOS évite le sélecteur de profil ; Linux/Windows peuvent inviter l'utilisateur à choisir un profil d'abord.# macOS — préférez AppleScript à `open -a` (réutilise le profil actuel, évite le sélecteur de profil) osascript -e 'open location "chrome://inspect/#remote-debugging"' # Linux google-chrome 'chrome://inspect/#remote-debugging' # ou : chromium, google-chrome-stable # Windows (PowerShell) Start-Process chrome 'chrome://inspect/#remote-debugging' - Cochez « Découvrir les cibles réseau » dans chrome://inspect, puis cliquez sur Autoriser lorsque Chrome le demande.
- Si la détection automatique choisit le mauvais navigateur (plusieurs en cours d'exécution, vous en voulez un spécifique) : listez-les avec
await detectBrowsers(), puisawait session.connect({ profileDir: <celui que vous voulez> }). - Si
session.connect()renvoieAucun navigateur détecté n'a accepté la connexion, l'utilisateur a le débogage à distance activé mais n'a pas encore cliqué sur Autoriser. Dites-lui de cliquer dessus et réessayez, ou passeztimeoutMs: 30000pour attendre le clic.
Travailler avec les cibles (onglets)
- Filtrez les internes de Chrome.
listPageTargets()supprime déjà les URLchrome://etdevtools://. Si vous appelezTarget.getTargets()directement, filtrez manuellement. - L'ordre des cibles CDP ≠ l'ordre visible de la barre d'onglets. Lorsque l'utilisateur dit « le premier onglet que je peux voir », utilisez une capture d'écran ou le titre de la page pour l'identifier —
Target.activateTargetne fait que passer à un targetId connu.
Rechercher une méthode
La surface typée complète se trouve dans <rép-comp>/sdk/generated.ts (~655 Ko, chargé uniquement si vous le lisez). Chaque méthode a sa description CDP comme commentaire JSDoc plus des interfaces *Params / *Return typées dans des espaces de noms par domaine.
grep -n "navigate" <rép-comp>/sdk/generated.ts | head
Régénérer le SDK
Lorsque les JSON de protocole amont changent, remplacez sdk/browser_protocol.json et/ou sdk/js_protocol.json et réexécutez :
cd <rép-comp>/sdk && bun gen.ts
browser-harness-js --restart # prendre en compte les nouvelles liaisons
Fichiers
Tous les chemins sont relatifs à <rép-comp> (le chemin d'installation — voir le début de ce document).
/usr/local/bin/browser-harness-js→<rép-comp>/sdk/browser-harness-js(la CLI)sdk/repl.ts— serveur HTTP (Bun.servesur127.0.0.1:9876)sdk/session.ts— classeSession(transport, connexion, routage des cibles, événements)sdk/generated.ts— sortie de génération de code : chaque méthode CDP sous forme de wrapper typésdk/gen.ts— script de génération de codesdk/{browser,js}_protocol.json— protocole amont (vendorisé)