CDP — skill browser-harness-js
SDK CDP customizado com geração de código (cada método de browser_protocol.json + js_protocol.json recebe um wrapper tipado) mais um pequeno servidor HTTP que mantém uma Session CDP persistente. A CLI browser-harness-js inicia automaticamente o servidor no primeiro uso e encaminha trechos JS para ele.
O SDK está no diretório sdk/ do skill. No restante deste documento, <skill-dir> refere-se ao local onde npx skills add instalou o skill (Claude Code: ~/.claude/skills/cdp; Cursor: ~/.cursor/skills/cdp; outros agentes variam). A CLI deve estar no PATH como browser-harness-js.
Configuração (uma vez, primeiro uso)
npx skills add coloca o skill no diretório de skills do seu agente, mas NÃO coloca a CLI no PATH. Antes da primeira chamada, verifique se ela está acessível e crie um link simbólico para qualquer diretório no seu PATH, se necessário:
# macOS (Apple Silicon + Homebrew)
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /opt/homebrew/bin/browser-harness-js
# macOS (Intel) / maioria Linux — pode precisar de sudo
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /usr/local/bin/browser-harness-js
# Linux sem sudo (certifique-se de que ~/.local/bin está no PATH)
command -v browser-harness-js >/dev/null || { mkdir -p ~/.local/bin && ln -sf <skill-dir>/sdk/browser-harness-js ~/.local/bin/browser-harness-js; }
A CLI instala automaticamente o bun na primeira execução se estiver faltando (o servidor é nativo do Bun). Defina BROWSER_HARNESS_SKIP_BUN_INSTALL=1 para desativar.
Como usar
Basta executar browser-harness-js '<JS>'. A primeira chamada inicia o servidor em segundo plano; chamadas subsequentes atingem o mesmo processo e, portanto, reutilizam a mesma session, o mesmo WebSocket para o Chrome e quaisquer globais que você definiu.
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'
A saída é o conteúdo bruto do resultado — sem envelope {ok,result}.
| Tipo de resultado | stdout |
|---|---|
| string | texto simples, sem aspas JSON (ex.: Example Domain) |
| número / booleano | 42, true |
| objeto / array (não vazio) | JSON compacto (ex.: {"frameId":"..."}, [1,2,3]) |
undefined / null / "" / {} / [] | vazio (nenhuma saída) |
Erros vão para stderr, código de saída 1. A mensagem de erro CDP e o stack JS são impressos literalmente, ex.:
Error: CDP -32602: invalid params
at _call (.../session.ts:117:33)
...
Detecte falhas com if browser-harness-js '...'; then ...; else handle_error; fi ou verificando $?.
Trechos multilinha via stdin (heredoc). Importante: um trecho com várias instruções NÃO retorna automaticamente a última expressão — escreva return X explicitamente. Trechos de expressão única passados como primeiro argumento retornam automaticamente.
browser-harness-js <<'EOF'
const tabs = await listPageTargets();
globalThis.tid = tabs[0].targetId;
await session.use(globalThis.tid);
return globalThis.tid;
EOF
Comandos da CLI
| Comando | Comportamento |
|---|---|
browser-harness-js '<js>' | Inicia servidor automaticamente se necessário, avalia o JS, imprime resultado. |
browser-harness-js <<EOF…EOF | Mesmo, código do stdin. |
browser-harness-js --status | Imprime JSON de saúde (uptime, connected, sessionId) ou sai com 1 se estiver parado. |
browser-harness-js --start | Inicialização explícita (no-op se já estiver rodando). |
browser-harness-js --stop | Desligamento gracioso. Descarta estado da sessão. |
browser-harness-js --restart | Para + inicia do zero. |
browser-harness-js --logs | tail -f o log do servidor (/tmp/browser-harness-js.log). |
Variáveis de ambiente: CDP_REPL_PORT (padrão 9876), CDP_REPL_LOG (padrão /tmp/browser-harness-js.log).
Superfície da API dentro de trechos
Estas globais são pré-carregadas — sem necessidade de imports:
session— aSessionpersistente. Possui todos os domínios CDP montados:session.Page,session.DOM,session.Runtime,session.Network, … 56 domínios, 652 métodos no total.listPageTargets()— lista alvos de página reais viaTarget.getTargetsdo CDP (funciona também no Chrome 144+), com URLschrome://edevtools://filtradas. Sem argumentos — usa a sessão conectada.detectBrowsers()— varre diretórios de perfil específicos do SO em busca de navegadores baseados em Chromium em execução com depuração remota ativada. Retorna[{name, profileDir, port, wsPath, wsUrl, mtimeMs}], ordenados pelo mais recentemente iniciado.resolveWsUrl(opts)— resolve uma URL WS a partir de{wsUrl}|{port, host?}|{profileDir}. Para o fluxo de detecção automática sem argumentos, chamesession.connect()diretamente.CDP— os namespaces gerados (CDP.Page,CDP.Runtime, …) para referência de nomes de tipos.
Chamando um método CDP
Cada método recebe um único argumento objeto correspondente aos parâmetros CDP; ele resolve para o valor de retorno tipado (sem envelope result, sem correlação id — tratado para você).
// sem parâmetros
await session.DOM.enable()
// parâmetros obrigatórios
await session.Page.navigate({ url: 'https://example.com' })
// parâmetros opcionais (objeto também opcional)
await session.Page.captureScreenshot()
await session.Page.captureScreenshot({ format: 'png', quality: 80 })
// retornos são simplificados para a forma tipada
const { root } = await session.DOM.getDocument()
const { nodeId } = await session.DOM.querySelector({ nodeId: root.nodeId, selector: 'h1' })
Conectando
Padrão: apenas chame session.connect() sem argumentos. Ele detecta automaticamente navegadores baseados em Chromium em execução (Chrome, Chromium, Edge, Brave, Arc, Vivaldi, Opera, Comet, Canary) varrendo diretórios de perfil específicos do SO em busca de um arquivo DevToolsActivePort, ordenados pelo mais recentemente iniciado, e escolhe o primeiro cujo WebSocket aceite. Independente de SO — funciona no macOS, Linux, Windows.
await session.connect() // detecção automática
Use detectBrowsers() primeiro se quiser ver o que está disponível (ou deixar o usuário escolher) antes de conectar:
const found = await detectBrowsers()
// [{ name: 'Google Chrome', profileDir, port, wsPath, wsUrl, mtimeMs }, ...]
Formas explícitas — use-as apenas quando a detecção automática escolher o navegador errado, ou quando você já souber onde se conectar:
| Forma | Quando usar |
|---|---|
{ profileDir } | Segmentar um navegador específico quando vários estiverem em execução. Lê <profileDir>/DevToolsActivePort diretamente. |
{ wsUrl } | Você já tem ws://…/devtools/browser/<uuid> (ex.: redirecionado de outro lugar). |
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome' })
await session.connect({ wsUrl: 'ws://127.0.0.1:9222/devtools/browser/<uuid>' })
Caminhos de perfil por SO — use estes com { profileDir }:
- macOS:
~/Library/Application Support/<Navegador>(ex.:Google/Chrome,Comet,BraveSoftware/Brave-Browser,Arc/User Data) - Linux:
~/.config/<navegador>(ex.:google-chrome,chromium,BraveSoftware/Brave-Browser) - Windows:
%LOCALAPPDATA%\<Navegador>\User Data(ex.:Google\Chrome,Microsoft\Edge,BraveSoftware\Brave-Browser)
O tempo limite padrão de abertura WS por candidato é 5s — navegadores ativos respondem com abertura/fechamento em ~100ms, portanto 5s já é generoso. O único caso em que 5s é curto demais é quando o Chrome está exibindo o popup Permitir e aguardando o clique do usuário. Se você espera isso, passe timeoutMs: 30000:
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })
Se você receber Nenhum navegador detectado aceitou uma conexão — os navegadores têm arquivos DevToolsActivePort, mas nenhum está servindo WS no momento. A causa mais comum: a depuração remota está ativada, mas o usuário ainda não clicou em Permitir no prompt. Diga a ele para clicar em Permitir e tentar novamente (ou aumente timeoutMs).
Escolhendo um alvo (aba)
Após connect(), chame session.use(targetId) uma vez; chamadas subsequentes no nível da página (Page/DOM/Runtime/Network/etc.) roteiam automaticamente para o sessionId desse alvo. Chamadas Browser.* e Target.* sempre atingem o endpoint do navegador.
const tabs = await listPageTargets() // sem argumentos; usa a sessão conectada
const sid = await session.use(tabs[0].targetId)
await session.Page.enable()
await session.Page.navigate({ url: 'https://example.com' })
listPageTargets() usa Target.getTargets do CDP (não /json), portanto funciona também no Chrome 144+. Ele já filtra URLs chrome:// e devtools://. Chamada bruta equivalente:
const { targetInfos } = await session.Target.getTargets({})
const tabs = targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'))
Para alternar abas: session.use(outroTargetId). Para desanexar: session.setActiveSession(undefined).
Eventos
// Inscrever-se (retorna uma função canceladora)
const off = session.onEvent((method, params, sessionId) => { ... })
// Ou aguardar um único evento correspondente com predicado opcional + tempo limite
await session.Network.enable()
const ev = await session.waitFor(
'Page.frameNavigated',
(p) => p.frame.url.includes('example.com'),
10_000
)
Persistindo estado entre chamadas
Cada trecho é executado dentro de seu próprio wrapper assíncrono, portanto suas declarações let/const desaparecem quando ele retorna. Para transportar dados adiante, anexe a 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 em si, o sessionId ativo e os inscritos em eventos já são preservados pelo servidor — globais são necessárias apenas para dados ad-hoc.
Conectando a um Chrome em execução (fluxo chrome://inspect)
Ao anexar ao navegador já em execução do usuário:
- Tente
await session.connect()primeiro (sem argumentos) — a detecção automática lida com todos os navegadores baseados em Chromium viaDevToolsActivePort. Se retornar, você terminou. - Se a detecção automática falhar com
Nenhum navegador em execução com depuração remota detectado, o usuário precisa ativá-la. Abra a página de inspeção:Apenas o caminho AppleScript do macOS evita o seletor de perfil; Linux/Windows podem solicitar que o usuário escolha um perfil primeiro.# macOS — prefira AppleScript a `open -a` (reutiliza o perfil atual, evita o seletor de perfil) 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' - Marque "Descobrir destinos de rede" em chrome://inspect, depois clique em Permitir quando o Chrome solicitar.
- Se a detecção automática escolher o navegador errado (vários em execução, você quer um específico): liste-os com
await detectBrowsers(), depoisawait session.connect({ profileDir: <o que você quer> }). - Se
session.connect()retornarNenhum navegador detectado aceitou uma conexão, o usuário tem depuração remota ativada, mas ainda não clicou em Permitir. Diga a ele para clicar e tentar novamente, ou passetimeoutMs: 30000para aguardar o clique.
Trabalhando com alvos (abas)
- Filtrar internos do Chrome.
listPageTargets()já descarta URLschrome://edevtools://. Se você chamarTarget.getTargets()diretamente, filtre manualmente. - Ordem dos alvos CDP ≠ ordem da barra de abas visível. Quando o usuário disser "a primeira aba que vejo", use uma captura de tela ou título da página para identificá-la —
Target.activateTargetapenas alterna para um targetId conhecido.
Procurando um método
A superfície tipada completa está em <skill-dir>/sdk/generated.ts (~655 KB, carregado apenas se você o ler). Cada método tem sua descrição CDP como comentário JSDoc mais interfaces *Params / *Return tipadas em namespaces por domínio.
grep -n "navigate" <skill-dir>/sdk/generated.ts | head
Regenerando o SDK
Quando os JSONs de protocolo upstream mudarem, substitua sdk/browser_protocol.json e/ou sdk/js_protocol.json e execute novamente:
cd <skill-dir>/sdk && bun gen.ts
browser-harness-js --restart # usar as novas ligações
Arquivos
Todos os caminhos são relativos a <skill-dir> (o caminho de instalação — veja o topo deste documento).
/usr/local/bin/browser-harness-js→<skill-dir>/sdk/browser-harness-js(a CLI)sdk/repl.ts— servidor HTTP (Bun.serveem127.0.0.1:9876)sdk/session.ts— classeSession(transporte, conexão, roteamento de alvos, eventos)sdk/generated.ts— saída da geração de código: cada método CDP como um wrapper tipadosdk/gen.ts— script de geração de códigosdk/{browser,js}_protocol.json— protocolo upstream (incorporado)