CDP — browser-harness-js 스킬
커스텀 코드 생성 CDP SDK(browser_protocol.json + js_protocol.json의 모든 메서드에 타이핑된 래퍼 제공)와 하나의 영구 CDP Session을 유지하는 작은 HTTP 서버를 포함합니다. browser-harness-js CLI는 최초 사용 시 서버를 자동으로 시작하고 JS 스니펫을 전달합니다.
SDK는 스킬의 sdk/ 디렉토리에 있습니다. 이 문서의 나머지 부분에서 <skill-dir>은 npx skills add가 스킬을 설치한 경로를 의미합니다(Claude Code: ~/.claude/skills/cdp; Cursor: ~/.cursor/skills/cdp; 기타 에이전트는 다를 수 있음). CLI는 browser-harness-js로 PATH에 있어야 합니다.
설정 (최초 1회)
npx skills add는 스킬을 에이전트 스킬 디렉토리에 추가하지만 CLI를 PATH에 자동으로 추가하지는 않습니다. 첫 호출 전에 실행 가능한지 확인하고, 그렇지 않으면 PATH의 디렉토리에 심볼릭 링크를 생성하십시오:
# 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) / 대부분의 Linux — sudo 필요할 수 있음
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /usr/local/bin/browser-harness-js
# Linux, sudo 없이 (~/.local/bin이 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; }
CLI는 최초 실행 시 bun이 없으면 자동으로 설치합니다(서버는 Bun 네이티브). BROWSER_HARNESS_SKIP_BUN_INSTALL=1을 설정하면 설치를 건너뜁니다.
사용 방법
browser-harness-js '<JS>'를 실행하기만 하면 됩니다. 첫 번째 호출은 서버를 백그라운드에서 시작하고, 이후 호출은 동일한 프로세스에 접속하므로 동일한 session, 동일한 Chrome WebSocket, 그리고 설정한 모든 전역 변수를 재사용합니다.
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'
출력은 원시 결과 내용입니다 — {ok,result} 래핑이 없습니다.
| 결과 유형 | stdout |
|---|---|
| 문자열 | 따옴표 없는 일반 텍스트 (예: Example Domain) |
| 숫자 / 불리언 | 42, true |
| 객체 / 배열 (비어 있지 않음) | 축소 JSON (예: {"frameId":"..."}, [1,2,3]) |
undefined / null / "" / {} / [] | 비어 있음 (출력 없음) |
오류는 stderr로 출력되며, 종료 코드는 1입니다. CDP 오류 메시지와 JS 스택이 그대로 출력됩니다. 예:
Error: CDP -32602: invalid params
at _call (.../session.ts:117:33)
...
실패 감지는 if browser-harness-js '...'; then ...; else handle_error; fi 또는 $? 확인을 통해 수행합니다.
여러 줄 스니펫 (heredoc 사용). 중요: 여러 문장으로 구성된 스니펫은 마지막 표현식을 자동으로 반환하지 않습니다 — 명시적으로 return X를 작성하세요. 첫 번째 인자로 전달된 단일 표현식 스니펫은 자동으로 반환됩니다.
browser-harness-js <<'EOF'
const tabs = await listPageTargets();
globalThis.tid = tabs[0].targetId;
await session.use(globalThis.tid);
return globalThis.tid;
EOF
CLI 명령어
| 명령어 | 동작 |
|---|---|
browser-harness-js '<js>' | 필요시 서버 자동 시작, JS 평가, 결과 출력. |
browser-harness-js <<EOF…EOF | 동일, stdin에서 코드 읽음. |
browser-harness-js --status | 상태 JSON 출력 (가동 시간, 연결 상태, sessionId) 또는 다운 시 exit 1. |
browser-harness-js --start | 명시적 시작 (이미 실행 중이면 아무 동작 안함). |
browser-harness-js --stop | 정상 종료. 세션 상태 해제. |
browser-harness-js --restart | 중지 후 새로 시작. |
browser-harness-js --logs | 서버 로그 tail -f (/tmp/browser-harness-js.log). |
환경 변수: CDP_REPL_PORT (기본값 9876), CDP_REPL_LOG (기본값 /tmp/browser-harness-js.log).
스니펫 내 API 표면
다음 전역 변수는 사전 로드되어 있습니다 — import가 필요 없습니다:
session— 영구Session. 모든 CDP 도메인이 마운트되어 있습니다:session.Page,session.DOM,session.Runtime,session.Network, … 총 56개 도메인, 652개 메서드.listPageTargets()— CDP의Target.getTargets를 통해 실제 페이지 대상을 나열합니다(Chrome 144+에서도 작동).chrome://및devtools://URL은 필터링됩니다. 인자 없음 — 연결된 세션을 사용합니다.detectBrowsers()— OS별 프로필 디렉토리에서 원격 디버깅이 활성화된 실행 중인 크로미움 기반 브라우저를 스캔합니다.[{name, profileDir, port, wsPath, wsUrl, mtimeMs}]를 반환하며, 가장 최근에 실행된 순서로 정렬됩니다.resolveWsUrl(opts)—{wsUrl}|{port, host?}|{profileDir}에서 WebSocket URL을 확인합니다. 인자 없는 자동 감지 흐름의 경우 대신session.connect()를 직접 호출하세요.CDP— 생성된 네임스페이스(CDP.Page,CDP.Runtime, …)로, 타입 이름 참조용입니다.
CDP 메서드 호출
모든 메서드는 CDP 와이어 파라미터와 일치하는 단일 객체 인자를 받으며, 타이핑된 반환 값으로 resolve됩니다(result 래핑, id 상관 관계 없음 — 자동 처리).
// 인자 없음
await session.DOM.enable()
// 필수 인자
await session.Page.navigate({ url: 'https://example.com' })
// 모든 인자가 선택적 (객체 자체도 선택적)
await session.Page.captureScreenshot()
await session.Page.captureScreenshot({ format: 'png', quality: 80 })
// 반환 값은 타이핑된 형태로 제공됨
const { root } = await session.DOM.getDocument()
const { nodeId } = await session.DOM.querySelector({ nodeId: root.nodeId, selector: 'h1' })
연결
기본: 인자 없이 session.connect()를 호출하기만 하면 됩니다. 실행 중인 크로미움 기반 브라우저(Chrome, Chromium, Edge, Brave, Arc, Vivaldi, Opera, Comet, Canary)를 OS별 프로필 디렉토리에서 DevToolsActivePort 파일을 스캔하여 자동 감지하고, 가장 최근에 실행된 순서대로 WebSocket을 수락하는 첫 번째 브라우저를 선택합니다. OS에 구애받지 않음 — macOS, Linux, Windows에서 작동합니다.
await session.connect() // 자동 감지
연결하기 전에 사용 가능한 브라우저를 보려면(또는 사용자가 직접 선택하도록 하려면) 먼저 detectBrowsers()를 사용하세요:
const found = await detectBrowsers()
// [{ name: 'Google Chrome', profileDir, port, wsPath, wsUrl, mtimeMs }, ...]
명시적 형식 — 자동 감지가 잘못된 브라우저를 선택했거나 연결할 위치를 이미 알고 있는 경우에만 사용하세요:
| 형식 | 사용 시점 |
|---|---|
{ profileDir } | 여러 브라우저가 실행 중일 때 특정 브라우저를 대상으로 지정. <profileDir>/DevToolsActivePort를 직접 읽음. |
{ wsUrl } | 이미 ws://…/devtools/browser/<uuid>를 가지고 있는 경우 (예: 다른 곳에서 전달됨). |
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome' })
await session.connect({ wsUrl: 'ws://127.0.0.1:9222/devtools/browser/<uuid>' })
OS별 프로필 경로 — { profileDir }과 함께 사용:
- macOS:
~/Library/Application Support/<Browser>(예:Google/Chrome,Comet,BraveSoftware/Brave-Browser,Arc/User Data) - Linux:
~/.config/<browser>(예:google-chrome,chromium,BraveSoftware/Brave-Browser) - Windows:
%LOCALAPPDATA%\<Browser>\User Data(예:Google\Chrome,Microsoft\Edge,BraveSoftware\Brave-Browser)
후보별 WebSocket 열기 타임아웃 기본값은 5초입니다 — 실행 중인 브라우저는 약 100ms 내에 열기/닫기로 응답하므로 5초면 충분합니다. 5초가 너무 짧은 유일한 경우는 Chrome이 Allow 팝업을 표시하고 사용자 클릭을 기다리는 경우입니다. 이를 예상한다면 timeoutMs: 30000을 전달하세요:
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })
No detected browser accepted a connection 메시지가 표시되는 경우 — 브라우저에 DevToolsActivePort 파일은 있지만 현재 WebSocket을 서비스하고 있지 않습니다. 가장 일반적인 원인: 원격 디버깅이 활성화되어 있지만 사용자가 프롬프트에서 Allow를 아직 클릭하지 않은 경우입니다. 사용자에게 Allow를 클릭한 후 재시도하라고 안내하거나 timeoutMs를 늘리십시오.
대상(탭) 선택
connect() 후, session.use(targetId)를 한 번 호출하면 이후 페이지 레벨 호출(Page/DOM/Runtime/Network 등)이 해당 target의 sessionId로 자동 라우팅됩니다. Browser.* 및 Target.* 호출은 항상 브라우저 엔드포인트에 도달합니다.
const tabs = await listPageTargets() // 인자 없음; 연결된 세션 사용
const sid = await session.use(tabs[0].targetId)
await session.Page.enable()
await session.Page.navigate({ url: 'https://example.com' })
listPageTargets()는 CDP의 Target.getTargets(/json 아님)를 사용하므로 Chrome 144+에서도 작동합니다. chrome:// 및 devtools:// URL을 이미 필터링합니다. 동등한 원시 호출:
const { targetInfos } = await session.Target.getTargets({})
const tabs = targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'))
탭 전환: session.use(otherTargetId). 분리: session.setActiveSession(undefined).
이벤트
// 구독 (구독 해제 함수 반환)
const off = session.onEvent((method, params, sessionId) => { ... })
// 또는 선택적 조건자와 타임아웃을 포함한 단일 일치 이벤트 대기
await session.Network.enable()
const ev = await session.waitFor(
'Page.frameNavigated',
(p) => p.frame.url.includes('example.com'),
10_000
)
호출 간 상태 유지
각 스니펫은 자체 비동기 래퍼 내에서 실행되므로, let/const 선언은 반환 시 사라집니다. 데이터를 전달하려면 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 자체, 활성 sessionId, 이벤트 구독자는 이미 서버에 의해 유지됩니다 — 전역 변수는 임시 데이터에만 필요합니다.
실행 중인 Chrome에 연결 (chrome://inspect 흐름)
사용자의 이미 실행 중인 브라우저에 연결할 때:
- 먼저
await session.connect()를 시도 (인자 없음) — 자동 감지가 모든 크로미움 기반 브라우저를DevToolsActivePort를 통해 처리합니다. 성공하면 완료입니다. - 자동 감지가 실패하여
No running browser with remote debugging detected가 표시되면 사용자가 원격 디버깅을 켜야 합니다. 검사 페이지를 엽니다:macOS의 AppleScript 경로만 프로필 선택기를 피합니다. Linux/Windows는 사용자에게 먼저 프로필을 선택하라는 메시지를 표시할 수 있습니다.# macOS — AppleScript가 `open -a`보다 선호됨 (현재 프로필 재사용, 프로필 선택기 방지) osascript -e 'open location "chrome://inspect/#remote-debugging"' # Linux google-chrome 'chrome://inspect/#remote-debugging' # 또는: chromium, google-chrome-stable # Windows (PowerShell) Start-Process chrome 'chrome://inspect/#remote-debugging' - chrome://inspect에서 "Discover network targets"를 체크한 다음, Chrome이 프롬프트를 표시하면 Allow를 클릭합니다.
- 자동 감지가 잘못된 브라우저를 선택한 경우 (여러 개 실행 중, 특정 브라우저를 원하는 경우):
await detectBrowsers()로 목록을 확인한 다음await session.connect({ profileDir: <원하는 프로필> })를 사용합니다. session.connect()가No detected browser accepted a connection을 반환하는 경우, 사용자가 원격 디버깅은 켰지만 Allow를 아직 클릭하지 않은 것입니다. 클릭한 후 재시도하라고 안내하거나timeoutMs: 30000을 전달하여 클릭을 기다리십시오.
대상(탭) 작업
- Chrome 내부 필터링.
listPageTargets()는 이미chrome://및devtools://URL을 제거합니다.Target.getTargets()를 직접 호출하는 경우 수동으로 필터링하세요. - CDP 대상 순서 ≠ 표시된 탭 순서. 사용자가 "내가 볼 수 있는 첫 번째 탭"이라고 말하면 스크린샷이나 페이지 제목을 사용하여 식별하세요 —
Target.activateTarget은 알려진 targetId로만 전환됩니다.
메서드 조회
전체 타이핑된 표면은 <skill-dir>/sdk/generated.ts에 있습니다 (~655 KB, 읽을 때만 로드됨). 각 메서드는 CDP 설명이 JSDoc 주석으로 포함되어 있으며, 도메인별 네임스페이스에 타이핑된 *Params / *Return 인터페이스가 있습니다.
grep -n "navigate" <skill-dir>/sdk/generated.ts | head
SDK 재생성
업스트림 프로토콜 JSON이 변경되면 sdk/browser_protocol.json 및/또는 sdk/js_protocol.json을 교체하고 다시 실행하세요:
cd <skill-dir>/sdk && bun gen.ts
browser-harness-js --restart # 새 바인딩 적용
파일
모든 경로는 <skill-dir>을 기준으로 합니다 (설치 경로 — 이 문서 상단 참조).
/usr/local/bin/browser-harness-js→<skill-dir>/sdk/browser-harness-js(CLI)sdk/repl.ts— HTTP 서버 (Bun.serveon127.0.0.1:9876)sdk/session.ts—Session클래스 (전송, 연결, 대상 라우팅, 이벤트)sdk/generated.ts— 코드 생성 출력: 모든 CDP 메서드의 타이핑된 래퍼sdk/gen.ts— 코드 생성 스크립트sdk/{browser,js}_protocol.json— 업스트림 프로토콜 (벤더됨)