CDP — browser-harness-js スキル
カスタムコード生成された CDP SDK(browser_protocol.json + js_protocol.json のすべてのメソッドに型付きラッパーを提供)と、1つの永続的な CDP Session を保持する小さな HTTP サーバーです。browser-harness-js CLI は初回使用時に自動的にサーバーを起動し、JavaScript スニペットを転送します。
SDK はスキルの sdk/ ディレクトリにあります。以降のドキュメントでは、<skill-dir> は npx skills add がスキルをインストールしたパスを指します(Claude Code の場合: ~/.claude/skills/cdp、Cursor の場合: ~/.cursor/skills/cdp、その他のエージェントは異なる場合があります)。CLI は browser-harness-js として PATH に追加されている必要があります。
セットアップ(初回のみ)
npx skills add はスキルをエージェントのスキルディレクトリに配置しますが、CLI を PATH に追加しません。初回呼び出しの前に、CLI が実行可能であることを確認し、必要に応じて 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
# sudo なしの Linux(~/.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 または $? の確認で行います。
標準入力(ヒアドキュメント)による複数行スニペット。 重要: 複数ステートメントのスニペットは最後の式を自動返却しません — 明示的に 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 | 同上、コードは標準入力から取得します。 |
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 サーフェス
以下のグローバル変数は事前にロードされています — インポートは不要です:
session— 永続的なSession。すべての CDP ドメインがマウントされています:session.Page、session.DOM、session.Runtime、session.Network、… 56 ドメイン、合計 652 メソッド。listPageTargets()— CDP のTarget.getTargetsを使用して実際のページターゲットをリストします(Chrome 144+ でも動作)。chrome://とdevtools://の URL は除外されます。引数なし — 接続済みのセッションを使用します。detectBrowsers()— OS 固有のプロファイルディレクトリをスキャンし、リモートデバッグが有効な実行中の Chromium 系ブラウザを検出します。[{name, profileDir, port, wsPath, wsUrl, mtimeMs}]を返し、最後に起動された順にソートされます。resolveWsUrl(opts)—{wsUrl}|{port, host?}|{profileDir}から WS URL を解決します。引数なしの自動検出フローでは、代わりにsession.connect()を直接呼び出してください。CDP— 生成された名前空間(CDP.Page、CDP.Runtime、…)型名参照用。
CDP メソッドの呼び出し
すべてのメソッドは CDP ワイヤーパラメータに一致する単一のオブジェクト引数を受け取り、型付けされた戻り値に解決されます(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() を呼び出すだけです。 実行中の Chromium 系ブラウザ(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)
各候補に対する WS 接続タイムアウトのデフォルトは 5 秒 です — 実行中のブラウザは約 100ms で open/close に応答するため、5 秒で十分です。5 秒が短すぎる唯一のケースは、Chrome が 許可 ポップアップを表示し、ユーザーのクリックを待っている場合です。その場合は、timeoutMs: 30000 を渡します:
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })
No detected browser accepted a connection が表示された場合 — ブラウザには DevToolsActivePort ファイルがありますが、現在 WS を提供していません。最も一般的な原因は、リモートデバッグが有効になっているものの、ユーザーがプロンプトで 許可 をクリックしていないことです。ユーザーに許可をクリックするよう伝え、再試行するか(または timeoutMs を増やして)ください。
ターゲット(タブ)の選択
connect() の後、session.use(targetId) を 1 回呼び出します。以降のページレベルの呼び出し(Page/DOM/Runtime/Network など)は、そのターゲットの 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を介してすべての Chromium 系ブラウザを処理します。接続に成功したら完了です。 - 自動検出が
No running browser with remote debugging detectedで失敗した場合、ユーザーがリモートデバッグを有効にする必要があります。インスペクトページを開きます:macOS の AppleScript パスのみがプロファイル選択画面を回避します。Linux/Windows では、ユーザーにプロファイルの選択を促す場合があります。# macOS — `open -a` よりも AppleScript が推奨(現在のプロファイルを再利用し、プロファイル選択画面を回避) 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 で「ネットワークターゲットを検出」にチェックを入れ、Chrome がプロンプトを表示したら 許可 をクリックします。
- 自動検出が誤ったブラウザを選択した場合(複数実行中で、特定のブラウザが必要な場合):
await detectBrowsers()で一覧表示し、その後await session.connect({ profileDir: <目的のもの> })を実行します。 session.connect()がNo detected browser accepted a connectionを返す場合、ユーザーはリモートデバッグをオンにしているが、まだ 許可 をクリックしていません。ユーザーにクリックして再試行するよう伝えるか、timeoutMs: 30000を渡してクリックを待ちます。
ターゲット(タブ)の操作
- Chrome 内部ページをフィルタリングします。
listPageTargets()は既にchrome://とdevtools://の URL を除外しています。Target.getTargets()を直接呼び出す場合は、手動でフィルタリングしてください。 - CDP ターゲットの順序は、表示されているタブストリップの順序とは異なります。 ユーザーが「最初に表示されているタブ」と言った場合、スクリーンショットやページタイトルを使用して特定してください —
Target.activateTargetは既知の targetId に切り替えるだけです。
メソッドの検索
完全な型付きサーフェスは <skill-dir>/sdk/generated.ts にあります(約 655 KB、読み込んだ場合のみロードされます)。各メソッドには JSDoc コメントとして CDP の説明と、ドメイン別の名前空間に型付きの *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 サーバー(127.0.0.1:9876でBun.serve)sdk/session.ts—Sessionクラス(トランスポート、接続、ターゲットルーティング、イベント)sdk/generated.ts— コード生成出力: すべての CDP メソッドの型付きラッパーsdk/gen.ts— コード生成スクリプトsdk/{browser,js}_protocol.json— 上流のプロトコル(ベンダリング済み)