Tweet Posting via CDP
Post tweets to X/Twitter by driving the already-logged-in Chrome through the DevTools Protocol. No extension needed — relies on Chrome's remote debugging endpoint.
Prerequisites
- Chrome running with remote debugging. If
~/Library/Application Support/Google/Chrome/DevToolsActivePortexists, the endpoint is live (the file contains port + browser WebSocket path). If missing, relaunch Chrome:
open -a "Google Chrome" --args --remote-debugging-port=9222
- Chrome logged into X (https://x.com) — the script reuses the logged-in session.
Post a tweet
echo "推文内容" | ~/.agent-reach/scripts/post-tweet-cdp.mjs
# 或
~/.agent-reach/scripts/post-tweet-cdp.mjs "推文内容"
Script output: composer text length, button state, success confirmation. On success the composer is emptied (that's the verification signal).
How it works
- Read
DevToolsActivePort→ws://127.0.0.1:<port>/devtools/browser/<token>- NOTE: HTTP
http://localhost:9222/json/*returns 404/502 on modern Chrome (v136+); the WebSocket from this file is the only working entry point.
- NOTE: HTTP
Target.getTargets→ find thepagetarget whose URL containsx.com(open one if absent).Target.attachToTargetwithflatten: true→ session-scoped commands.- Focus the composer via
Runtime.evaluate:document.querySelector('[data-testid="tweetTextarea_0"]').focus(), then clear any draft withexecCommand('selectAll')+execCommand('delete'). - Insert text in ~50-char chunks via
Input.insertText(trusted input events — React/Draft.js accepts these; a single big insertText or plainexecCommand('insertText')does NOT enable the Post button). - Click
[data-testid="tweetButtonInline"](fallback[data-testid="tweetButton"]) viaRuntime.evaluate→.click(). - Verify: composer textContent length drops to 0/gone.
Gotchas (learned the hard way)
execCommand('insertText')alone leaves the Post buttondisabled— React never registers the text.Input.insertText(CDP) is required.Input.insertTextwith the whole tweet in one call silently drops trailing chars. Chunk at ~50 chars with ~150ms sleep between chunks.- The composer element itself is the contenteditable (
DIV.tweetTextarea_0);.focus()on it works, thenInput.insertTextlands correctly. - OpenCLI's browser-bridge approach (
opencli twitter post) breaks withattach failed: Cannot access a chrome-extension:// URL of different extensionwhen the extension ID drifts after updates — CDP avoids extensions entirely. - cua-driver (
cua.ai/cua-driver) native path (AX click + CGEvent typing) works for navigation but web-content writes areunverifiable; itspagetool needs the same CDP port. Direct CDP is the most reliable layer.
Posting rules (project convention)
- One post per sweep round; never retry or repost with different wording (opencli-era rule, still honored).
- No post when a sweep round adds zero entries.