Claude-code-sdk chatgpt-search

install
source · Clone the upstream repo
git clone https://github.com/SeifBenayed/cloclo
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/SeifBenayed/cloclo "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/chatgpt-search" ~/.claude/skills/seifbenayed-claude-code-sdk-chatgpt-search && rm -rf "$T"
manifest: .claude/skills/chatgpt-search/SKILL.md
source content

ChatGPT Search — Extract structured response + hydration JSON

Prerequisites

A Chrome instance must be running with

--remote-debugging-port=9222
. If not running, launch it:

# Kill existing Chrome first
pkill -9 -f "Google Chrome" 2>/dev/null; sleep 2

# Launch with debug port + user's real profile copy
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.claude/browser-profiles/chrome-debug-full" \
  --no-first-run &>/dev/null &

If the profile doesn't exist yet, create it by copying the user's real Chrome profile:

REAL="$HOME/Library/Application Support/Google/Chrome"
COPY="$HOME/.claude/browser-profiles/chrome-debug-full"
mkdir -p "$COPY"
cp "$REAL/Local State" "$COPY/" 2>/dev/null
cp -R "$REAL/Default" "$COPY/Default" 2>/dev/null
rm -rf "$COPY/Default/Cache" "$COPY/Default/Code Cache" 2>/dev/null

Workflow

The user provides a search query. Execute these steps:

Step 1: Verify Chrome is running with debug port

curl -s http://127.0.0.1:9222/json/version | head -1

If it fails, launch Chrome as described in Prerequisites.

Step 2: Attach + enable network log + open ChatGPT

Browser { action: "new_session", session_id: "chatgpt", cdp_url: "http://127.0.0.1:9222" }
Browser { action: "enable_network_log", session_id: "chatgpt" }
Browser { action: "new_tab", url: "https://chatgpt.com", session_id: "chatgpt" }

Wait 5 seconds for page load.

Step 3: Inject fetch/SSE interceptor

Browser { action: "evaluate", session_id: "chatgpt", value: "window.__sse=[];window.__api=[];const _f=window.fetch;window.fetch=async function(...a){const url=typeof a[0]==='string'?a[0]:a[0]?.url||'?';const m=a[1]?.method||'GET';const rb=a[1]?.body;const resp=await _f.apply(this,a);const ct=resp.headers.get('content-type')||'';const cl=resp.clone();if(ct.includes('event-stream')){const rd=cl.body.getReader();const dc=new TextDecoder();let full='';(async()=>{try{while(true){const{done,value:v}=await rd.read();if(done)break;full+=dc.decode(v,{stream:true});}}catch{}window.__sse.push({url,m,s:resp.status,sz:full.length,body:full,ts:Date.now()});})();}else{cl.text().then(t=>{if(t.length>30)window.__api.push({url,m,s:resp.status,sz:t.length,body:t,ts:Date.now()});}).catch(()=>{});}return resp;};'ok'" }

Step 4: Type query + submit

Use real CDP key events for React compatibility:

Browser { action: "click", selector: "#prompt-textarea, textarea", session_id: "chatgpt" }

Then type each character of the query using

send_keys
— but for long queries, use
evaluate
with the native setter:

Browser { action: "evaluate", session_id: "chatgpt", value: "Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(document.querySelector('#prompt-textarea,textarea'),'THE QUERY HERE');document.querySelector('#prompt-textarea,textarea').dispatchEvent(new Event('input',{bubbles:true}));'typed'" }

Wait 800ms, then submit:

Browser { action: "evaluate", session_id: "chatgpt", value: "(function(){var btn=document.querySelector('[data-testid=\"send-button\"],button[aria-label*=\"Envoyer\"],button[aria-label*=\"Send\"]');if(btn&&!btn.disabled){btn.click();return 'sent';}var btns=[...document.querySelectorAll('button')];var s=btns.find(function(b){return b.querySelector('svg')&&!b.disabled&&b.closest('[class*=\"composer\"]');});if(s){s.click();return 'sent-svg';}return 'no-btn';})()" }

Step 5: Wait for response

Poll every 5 seconds for up to 60 seconds, checking message count:

Browser { action: "evaluate", session_id: "chatgpt", value: "document.querySelectorAll('[data-message-author-role]').length" }

Stop when message count >= 2 (user + assistant).

Step 6: Extract results

Messages (user + assistant full text):

Browser { action: "evaluate", session_id: "chatgpt", value: "(function(){var msgs=document.querySelectorAll('[data-message-author-role]');var r=[];msgs.forEach(function(m){r.push({role:m.getAttribute('data-message-author-role'),text:m.textContent.trim()})});return JSON.stringify(r);})()" }

Hydration JSON (session, config, feature flags):

Browser { action: "evaluate", session_id: "chatgpt", value: "var e=document.querySelector('script[type=\"application/json\"]');e?e.textContent:''" }

Intercepted SSE streams (conversation stream + search product updates):

Browser { action: "evaluate", session_id: "chatgpt", value: "JSON.stringify((window.__sse||[]).map(function(s){return{url:s.url,method:s.m,status:s.s,size:s.sz}}))" }

To get a specific SSE body (e.g., index 0):

Browser { action: "evaluate", session_id: "chatgpt", value: "(window.__sse||[])[0]?.body||''" }

Intercepted API calls:

Browser { action: "evaluate", session_id: "chatgpt", value: "JSON.stringify((window.__api||[]).filter(function(a){return a.url.includes('backend')}).map(function(a){return{url:a.url,method:a.m,status:a.s,size:a.sz}}))" }

CDP network log (all HTTP requests/responses):

Browser { action: "get_network_log", session_id: "chatgpt", filter: "backend" }

Step 7: Save to /tmp

Save all extracted data to

/tmp/chatgpt-search-*.json
:

  • /tmp/chatgpt-search-messages.json
    — user + assistant messages
  • /tmp/chatgpt-search-hydration.json
    — UI hydration JSON
  • /tmp/chatgpt-search-sse.json
    — SSE stream data (if captured)
  • /tmp/chatgpt-search-api.json
    — API call responses

Step 8: Report

Present the assistant's response to the user, and list saved files with sizes.

Output format

## ChatGPT Search: [query]

### Response
[Full assistant message text]

### Files saved
- /tmp/chatgpt-search-messages.json (X KB) — conversation messages
- /tmp/chatgpt-search-hydration.json (X KB) — UI hydration data
- /tmp/chatgpt-search-sse.json (X KB) — SSE streams
- /tmp/chatgpt-search-api.json (X KB) — API responses

### Metadata
- Model: [from hydration or conversation init]
- Session ID: [from hydration]
- Country: [from hydration]

Important notes

  • ChatGPT works without login for basic queries (anonymous mode)
  • For authenticated queries, the user must log in manually in the debug Chrome
  • The fetch interceptor captures SSE streams (conversation + search/product_update)
  • CDP network log captures JSON API calls but NOT SSE stream bodies
  • Always close the session when done:
    Browser { action: "close_session", session_id: "chatgpt" }