ChatGPT Moli CDP Demo
This demo starts moli serve, connects through raw CDP, opens
https://chatgpt.com/, fills the email/password login form, then optionally
sends a prompt and prints the latest assistant response.
It does not bypass CAPTCHA, MFA, passkeys, or other human verification steps. If ChatGPT asks for one of those, the script exits with a clear error.
Run
From this directory:
uv run --with websockets python chatgpt_cdp_demo.py --email "$CHATGPT_EMAIL" --prompt "Say hello in one sentence."
The password is read with hidden terminal input. To use environment variables:
CHATGPT_EMAIL="you@example.com" \
CHATGPT_PASSWORD="..." \
uv run --with websockets python chatgpt_cdp_demo.py --prompt "Say hello in one sentence."
For a login-only smoke:
uv run --with websockets python chatgpt_cdp_demo.py --login-only
For an interactive prompt loop after login:
uv run --with websockets python chatgpt_cdp_demo.py
For the legacy raw-CDP TUI:
uv run --with websockets python chatgpt_cdp_tui.py
The preferred TUI uses Playwright as the CDP client framework and keeps a persistent Moli profile:
uv run --with websockets --with playwright python chatgpt_playwright_tui.py
On a fresh profile, the preferred TUI starts a headful Chromium under Xvfb for the OpenAI authentication pages. Chromium submits the credentials and waits for MFA; after approval, the TUI imports the resulting cookies and localStorage into Moli over CDP. Chromium and its temporary profile are then removed. ChatGPT navigation, prompt submission, and responses continue in Moli.
This bridge is necessary because direct automated login can be rejected by the
authentication challenge even when Moli can use an established session. Pass
--auth-backend moli only when diagnosing the direct login path.
For one-shot Playwright debugging:
uv run --with websockets --with playwright python chatgpt_playwright_demo.py \
--email "$CHATGPT_EMAIL" \
--password-stdin \
--prompt "今天几号?"
If --profile-dir already contains a logged-in session, avoid reading
credentials during repeated performance probes:
uv run --with websockets --with playwright python chatgpt_playwright_demo.py \
--use-existing-session \
--profile-dir .profile \
--moli-bin ../../target/release/moli \
--timestamps \
--prompt "只回复 OK"
If the account asks for device approval, the Playwright CLI waits until
--login-timeout expires. To switch that page to email-code verification:
uv run --with websockets --with playwright python chatgpt_playwright_demo.py \
--email "$CHATGPT_EMAIL" \
--password-stdin \
--try-email-verification \
--auth-code-prompt \
--prompt "今天几号?"
The Playwright TUI defaults to a longer login timeout and waits for mobile
device approval. Pass --try-email-verification to switch the device-approval
page to email-code verification. The TUI shows an Auth Code input when the
code page is reached.
TUI keys:
Tab: switch input fieldEnterorCtrl-S: login or send the current promptCtrl-L: clear the message logCtrl-Q: quit and stopmoli serve
Useful options:
--moli-bin ../../target/release/moli--profile-dir .profileto override the persistent cookies/localStorage directory--auth-chromium-bin /path/to/chromiumto select the Chromium used for login bootstrap--auth-backend molito bypass the Chromium bridge for direct-login diagnostics--http-proxy http://127.0.0.1:7890if the Moli runtime needs a proxy--http-no-proxy 127.0.0.1,localhostwhen testing against local URLs--http-timeout 120000is the demo default for Moli, because ChatGPT can serve large script bundles slowly through CDN/challenge paths--http-max-concurrent 16to test more active fetch transfers on script-heavy ChatGPT loads--debug-snapshotto print sanitized DOM snapshots around login steps--timestampsto print elapsed seconds for each status line in the one-shot Playwright demo--answer-timeout 300to wait longer for slow ChatGPT responses--no-reload-recoveryin the Playwright demo/TUI to expose live DOM render failures instead of recovering persisted answers by reloading the conversation--live-tracein the Playwright demo/TUI to record in-page fetch, response body stream, WebSocket, event-loop, MessageChannel, app-state, Router loaderData, and DOM selector trace events for live-render debugging--live-trace-output /tmp/chatgpt-live.jsonlto append sanitized trace summaries as JSON Lines for diffable debugging--login-timeout 180to allow time for device approval or email-code entry--auth-code-stdinto pipe password and email code as two stdin lines
The Playwright TUI profile defaults to
$XDG_DATA_HOME/moli/chatgpt-profile, or
~/.local/share/moli/chatgpt-profile when XDG_DATA_HOME is unset. It stores
login cookies/localStorage and should be treated like a credential store. Once
that profile is logged in, the password field may be left empty when reconnecting.
Prompt responses print answer source: live-dom when the current page renders
the assistant text directly. answer source: persisted-reload means the demo
had to reload the conversation and read the persisted response; that proves the
prompt/backend path worked, but not that live rendering is fixed.
Prompt submission in the Playwright demo/TUI first tries Playwright-native composer input and send-button click. The older page helper remains as a fallback so login/session debugging can continue when native input regresses.
For live-render debugging, combine --no-reload-recovery --live-trace. In that
mode a timeout after entering /c/... is useful: the output includes sanitized
conversation fetch/WebSocket/body-stream, event-loop, app-state, selector, and
patch-frame summaries so a persisted reload cannot hide a live DOM failure.
Current traces also report whether MessagePort.onmessage,
requestIdleCallback, the conversation route loaderData, React thread fiber
identity, shallow thread selector hook state, and selected thread fiber source
hints progressed. The live trace also includes a focused conversation
materialization probe: it checks whether the current React conversation wrapper
can already produce display turns even when those turns have not appeared in
the DOM. It also records focused source/lazy-payload hints for the thread-list
Suspense branch so a no-reload timeout can distinguish "data exists but render
subtree stayed suspended" from "conversation data never arrived".
Known limitation as of 2026-05-27: a no-reload run can reach /c/... with
conversation data materialized inside the React wrapper while the live DOM still
has no user/assistant turn. Treat answer source: persisted-reload as a
recovery path, not as proof that live conversation rendering is fixed.
For long traces, add --live-trace-output /tmp/chatgpt-live.jsonl. Each timeout
or answer result appends one sanitized JSON object with the compact trace
summary and answer path reason.
If prompt submission appears to hang, run the CLI with --debug-snapshot first.
The script reports whether it found the composer, whether the send button became
usable, and the last visible conversation state before timeout.