mcpField
X
xsearch

xsearch

github.com/monotykamary生産性471 回インストール今日更新
リポジトリ

xsearch について

xsearchは、browser-harness-jsを通じてXを検索し、投稿を構造化して取得するコマンドラインスキルです。ブラウザーでXにログインしたユーザー向けで、整形出力やJSON出力、並列検索に対応します。

主な機能

  1. 01Xの該当投稿を検索
  2. 02投稿者、ハンドル、本文、URL、時刻を抽出
  3. 03整形結果またはJSONを出力
  4. 04複数の検索を並列実行
  5. 05browser-harness-jsで投稿URLを開く

活用シーン

  1. 01検索語に合う最近の投稿を収集
  2. 02後続処理向けに投稿の詳細を取得
  3. 03複数トピックを同時に検索

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/monotykamary/browser-harness-js --skill xsearch

インストール後、Claude Code で/xsearchを入力して読み込まれたか確認します。

同梱ファイルscripts/setupscripts/xsearch

詳細

X 検索

⚠️ ブラウザーで X にログインしている必要があります。 X の検索ページは、ログアウト中の訪問者には検索結果を表示せず、ログイン画面にリダイレクトします。browser-harness-js が使用するブラウザーセッションで、X にログイン済みである必要があります。

X(Twitter)を検索し、CDP 経由で構造化された結果を抽出します。browser-harness-js(CDP セッションを提供)以外に外部依存関係はありません。各呼び出しで専用のタブと WebSocket セッションが開かれるため、並列で安全に使用できます。

クイック検索

xsearch "your query"             # pretty-printed, up to 10 results
xsearch "your query" 5           # 5 results, pretty-printed
xsearch --json "your query" 3   # raw JSON

並列での使用

各 xsearch 呼び出しは共有 WebSocket を再利用しますが、呼び出しごとの sessionId を使用して専用のタブに接続します。タブは Target.closeTarget を使って、呼び出し元がクリーンアップの完了を待たずに済むよう、完了を待たずに閉じられます。

xsearch "rust async" 3 &
xsearch "go channels" 3 &
wait

結果の形式

各結果は { author, handle, text, url, time } です。

[
  {
    "author": "Zane Chee",
    "handle": "@injaneity",
    "text": "people of pi, i'm excited to finally introduce browser use in pi-computer-use!",
    "url": "https://x.com/injaneity/status/2065110712511500620",
    "time": "2026-06-11T16:35:36.000Z"
  }
]

URL から X の投稿を表示する

結果の url(または任意の x.com/<handle>/status/<id> パーマリンク)を browser-harness-js 経由で直接開きます。再検索は不要です。アドホック検索と同じ接続/タブ作成/評価の流れを使い、 [data-testid="tweet"] セレクターを再利用しますが、フォーカス対象の投稿のみを取得します(パーマリンクでは DOM 順の先頭):

browser-harness-js <<'EOF'
if (!session.isConnected()) {
  try { await session.connect() } catch (e) { throw new Error("Cannot connect: " + e.message) }
}

const url = "https://x.com/injaneity/status/2065110712511500620"
const t = await session.Target.createTarget({ url: "about:blank", background: true })
const { sessionId } = await session.Target.attachToTarget({ targetId: t.targetId, flatten: true })

try {
  await cdp(sessionId, "Page.enable", {})
  // Required — without this Chrome emits zero Page.lifecycleEvent, so networkIdle
  // would never fire.
  await cdp(sessionId, "Page.setLifecycleEventsEnabled", { enabled: true })
  // Arm the wait BEFORE Page.navigate: lifecycle events fire once, and a fast
  // load can fire networkIdle between navigate returning and the listener subscribing.
  const ready = session.waitFor({ method: 'Page.lifecycleEvent', sessionId, predicate: (p) => p.name === 'networkIdle', timeoutMs: 30_000 })
  await cdp(sessionId, "Page.navigate", { url })
  await ready
  await new Promise(r => setTimeout(r, 4000))   // React hydration; see Traps

  const result = await cdp(sessionId, "Runtime.evaluate", {
    expression: `(() => {
      const el = document.querySelector('[data-testid="tweet"]')
      if (!el) return JSON.stringify(null)
      const textEl = el.querySelector('[data-testid="tweetText"]')
      const timeEl = el.querySelector('time')
      const userNamesEl = el.querySelector('[data-testid="User-Name"]')
      const nameLinks = userNamesEl ? [...userNamesEl.querySelectorAll('a[role="link"]')] : []
      return JSON.stringify({
        author: nameLinks[0]?.textContent?.trim() || "",
        handle: nameLinks[1]?.textContent?.trim() || "",
        text: textEl?.innerText?.trim() || "",
        time: timeEl?.getAttribute('datetime') || ""
      })
    })()`,
    returnByValue: true
  })
  return result.result.value
} finally {
  session.closeTab(t.targetId, sessionId).catch(() => {})
}
EOF
  • フォーカス対象の投稿(パーマリンクが指す投稿)は、DOM 順で最初の [data-testid="tweet"] です。返信と引用投稿はその下に表示されます。
  • 4 秒のハイドレーション待機時間は xsearch と同じです。networkIdle は React が投稿をレンダリングする前に発火します。ログアウト中の訪問者は、検索ページと同様に、パーマリンクでもログイン画面に移動します。
  • 待機方法。 この例では networkIdle(進行中のネットワークリクエストが 500ms 間ない状態)を待機します。X のページや多くのコンテンツサイトに適したデフォルトです。load の後に発火するため、少なくとも同等のコンテンツを取得でき、loadEventFired のように終わらない広告/分析ビーコンによって待機がブロックされることもありません。特定のページタイプでは、次の方法も利用できます。
    • networkAlmostIdle(250ms 間リクエストがない状態):継続的な XHR ポーリングがあり、500ms の無通信状態に到達しないページに使用します。
    • loadEventFired:すべてのサブリソースを確実に読み込む必要がある場合に使用します(テキスト抽出ではまれです)。
    • 評価の前に、準備完了後の短い await new Promise(r => setTimeout(r, 1000)) を追加します。networkIdle の後にコンテンツを遅延レンダリングするページで使用します。

仕組み

手順CDP 呼び出し内容
1session.connect() (once)共有 WebSocket をブラウザーに接続します
2Target.createTarget({ background: true })独立したバックグラウンドタブを作成します
3Target.attachToTargetタブ単位のルーティングに使う、呼び出しごとの sessionId を取得します
4cdp(sessionId, "Page.enable", …)ページイベントを購読します
5cdp(sessionId, "Page.setLifecycleEventsEnabled", …)ライフサイクルイベントを有効にします。これがないと networkIdle は発火しません
6session.waitFor('Page.lifecycleEvent' networkIdle) を cdp(sessionId, "Page.navigate", …) の前に設定します競合状態を修正します。ナビゲーションの前に networkIdle の待機を設定することで、読み込み完了後に待機が開始される競合を防ぎ、その後 x.com/search?q=…&src=typed_query&f=top に移動します
7setTimeout(4000)React のハイドレーションと投稿のレンダリングを待ちます
8スクロールループ(件数が 6 を超える場合)スクロールして、さらに投稿を読み込みます(スクロールごとに約 3 件)
9cdp(sessionId, "Runtime.evaluate", …)data-testid セレクターを使って DOM を一度だけ検索します
10closeTab(完了を待たずに実行)応答をブロックせずにタブを終了します

使用する DOM セレクター

X の検索ページでは、安定した data-testid 属性が使われています。

セレクター用途
[data-testid="tweet"]投稿コンテナー
[data-testid="tweetText"]投稿本文
[data-testid="User-Name"]投稿者名/ハンドルのブロック
User-Name 内の a[role="link"]リンク:[0]=名前、[1]=ハンドル、[2]=パーマリンク+タイムスタンプ
timedatetime 属性を使った ISO タイムスタンプ

注意点

  • ログインが必要です。 X はログアウト状態の訪問者には検索結果を表示せず、サインインを促す画面にリダイレクトします。ブラウザーセッションで X にログインしている必要があります。
  • React のハイドレーションに時間がかかります。 networkIdle は React がツイートを描画する前に発生します。初期バッチ(約 6 件のツイート)が表示されるまで、準備完了シグナルの後に 4 秒待つ必要があります。
  • 結果をさらに取得するにはスクロールします。 X は無限スクロールを使用します。初期表示には約 6 件のツイートが含まれます。count > 6 の場合、スクリプトは下へスクロールします(各スクロールで約 3 件のツイートが 1.5 秒の遅延後に読み込まれます)。
  • 新しいタブごとに Page.enable() と Page.setLifecycleEventsEnabled({ enabled: true }) の両方を呼び出す必要があります。 後者は、Chrome が Page.lifecycleEvent を発行するために必要です。これを呼び出さないと、networkIdle の待機は毎回タイムアウトします。
  • networkIdle の待機には 30s のタイムアウトがあります。 生の Promise の代わりに session.waitFor() を使用するため、ページが応答しなくなってもタブが残りません。X は継続的に読み込みを行いますが、実際には初期のネットワーク通信は約 2~3 秒で落ち着きます。ページが 500ms の静穏時間に一度も達しない場合は、代わりに networkAlmostIdle を使用してください(上記の待機戦略に関する注記を参照)。
  • 結果数が要求数を下回る場合があります。 X に一致するツイートが十分にない場合や、スクロールループで時間内に読み込めない場合があります。
  • ツイートのテキストが省略される場合があります。 長いツイートでは、X は「さらに表示」ボタンを表示します。抽出される text は、「さらに表示」をクリックせずに表示される部分です。
  • jq への依存はありません。 URI のエンコードには JS の encodeURIComponent() を使用し、出力の整形にはヒアドキュメント内の .map().join() を使用します。

コメント