---
name: chatgpt
description: Automate ChatGPT — log in with camoufox (anti-detection browser) via residential proxy, send prompts, grab responses, manage conversations, and maintain authenticated sessions. Use this skill whenever you need to access, interact with, or automate ChatGPT on behalf of a user — whether that's logging in, sending prompts, extracting results, refreshing sessions, or any other ChatGPT task.
---

# ChatGPT Automation Skill

Automates ChatGPT using **camoufox** (an anti-fingerprinting browser) and
the tenant's **residential proxy** to bypass Cloudflare detection. Covers
the full automation surface: login, session management, prompting, and
result extraction.

---

## Quick reference

| Task | Command |
|------|---------|
| First-time login | `python3 /home/node/.claude/skills/chatgpt/scripts/login.py --workspace "Workspace Name"` |
| Login (personal account) | `python3 /home/node/.claude/skills/chatgpt/scripts/login.py` |
| Verify saved session | `python3 /home/node/.claude/skills/chatgpt/scripts/load_session.py` |
| **Start v2 daemon** | `PYTHONUNBUFFERED=1 python3 /home/node/.claude/skills/chatgpt/scripts/daemon.py &` |
| **Send prompt (v2)** | `python3 /home/node/.claude/skills/chatgpt/scripts/send.py "your prompt"` |
| Stop daemon | `curl -s -X POST http://localhost:9223/stop` |
| Use session in a script | See [Loading a saved session](#loading-a-saved-session) |

---

## v2 Daemon (preferred for repeated use)

The daemon launches camoufox + loads the session **once**, then stays alive. Every
subsequent `send.py` call reuses the live browser — no startup overhead (~20s saved per call).

```bash
# 1. Start daemon in background
PYTHONUNBUFFERED=1 python3 /home/node/.claude/skills/chatgpt/scripts/daemon.py > /tmp/chatgpt-daemon.log 2>&1 &

# 2. Send prompts (instant — no browser launch)
python3 /home/node/.claude/skills/chatgpt/scripts/send.py "what is the capital of France?"

# 3. Stop when done
curl -s -X POST http://localhost:9223/stop
```

**API endpoints** (localhost:9223):
- `GET /health` — `{"status": "ok", "url": "<current page URL>"}`
- `POST /send` body `{"prompt": "..."}` — `{"kind": "text", "text": "...", "conv_url": "..."}`
  - For image prompts: `{"kind": "image", "images": ["/path/to/file.webp"], "conv_url": "..."}`
- `POST /stop` — shuts down daemon

**Completion detection:** polls last assistant message every 0.8s; returns when text stabilises. For images, detects `button[aria-label*="Download"]`.

---

## Prerequisites

### 1. Store credentials in tc

```bash
tc creds set chatgpt_username <email>
tc creds set chatgpt_password <password>
```

The scripts read these automatically. You can also override with environment
variables `CHATGPT_USERNAME` / `CHATGPT_PASSWORD` for one-off runs.

### 2. Ensure the residential proxy is running

```bash
tc proxy   # Should print a socks5:// URL
```

If empty, file a bug report — the proxy is required to avoid Cloudflare's bot detection.

### 3. camoufox is pre-installed

camoufox is baked into the container image at `/opt/camoufox`. No installation or download needed.

---

## Logging in

Run the login script. Pass `--workspace` if the account belongs to multiple
OpenAI organisations (e.g. a Team/Enterprise workspace):

```bash
# With workspace selection
python3 /home/node/.claude/skills/chatgpt/scripts/login.py \
    --workspace "Devit Workspace"

# Personal account only (no workspace selector)
python3 /home/node/.claude/skills/chatgpt/scripts/login.py
```

The script will:
1. Open `chatgpt.com` via camoufox + residential proxy
2. Fill in email → Continue → password → Continue
3. Click the requested workspace (if provided) or the first available one
4. Wait for the ChatGPT interface to fully load
5. Save the session to `/workspace/group/chatgpt-session.json`
6. Take screenshots into `/workspace/group/screenshots/chatgpt-login/` for debugging

### Optional flags

| Flag | Default | Purpose |
|------|---------|---------|
| `--username <email>` | *(none)* | ChatGPT email — overrides env var and tc creds |
| `--password <pass>` | *(none)* | ChatGPT password — overrides env var and tc creds |
| `--workspace "Name"` | *(none)* | Which org workspace to select |
| `--session /path/to/file.json` | `/workspace/group/chatgpt-session.json` | Where to save the session |

---

## Loading a saved session

Once logged in, future scripts can skip the login entirely by loading the
saved session cookies:

```python
import sys
sys.path.insert(0, "/home/node/.claude/skills/chatgpt/scripts")
from load_session import open_chatgpt_session

with open_chatgpt_session() as (ctx, page):
    page.goto("https://chatgpt.com/")
    # page is ready — fully logged in, correct workspace active
    # ... do your automation here
```

`open_chatgpt_session()` raises `RuntimeError` if the session has expired.
Catch it and re-run `login.py` to refresh:

```python
try:
    with open_chatgpt_session() as (ctx, page):
        # automation ...
except RuntimeError:
    import subprocess
    subprocess.run([
        "python3",
        "/home/node/.claude/skills/chatgpt/scripts/login.py",
        "--workspace", "Devit Workspace"
    ], check=True)
    with open_chatgpt_session() as (ctx, page):
        # automation ...
```

---

## Session management

- **Session file**: `/workspace/group/chatgpt-session.json` (cookies + localStorage)
- **Session lifespan**: Typically several weeks; OpenAI will expire it server-side
- **Verify validity**: `python3 load_session.py` — exits 0 if valid, 1 if expired
- **Refresh**: Simply re-run `login.py`; it overwrites the session file

---

## How the login flow works

```
chatgpt.com/auth/login
  → click "Log in"
  → auth.openai.com  (email entry)
  → auth.openai.com/log-in/password  (password entry)
  → auth.openai.com/workspace  (workspace selector — only if multiple orgs)
  → chatgpt.com  (success!)
```

### Why camoufox + residential proxy?

ChatGPT's login page is protected by **Cloudflare Turnstile**, which detects:
- Headless browser fingerprints (`navigator.webdriver`, canvas/WebGL hashes)
- Datacenter IP ranges

**camoufox** patches Firefox to remove all automation signals.
The **residential proxy** provides an ISP-routed IP address.
Together they bypass the Cloudflare challenge without any manual CAPTCHA solving.

---

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| `tc creds get chatgpt_username` returns empty | Run `tc creds set chatgpt_username <email>` |
| Proxy URL empty / connection refused | Run `tc proxy`; if empty, file a bug report |
| Stuck on Cloudflare challenge | Ensure residential proxy is active; try updating camoufox |
| Password step asks for passkey | Look for "Try another way" or "Continue with password" — the script handles this |
| Workspace selector not found | Account may only have one workspace — omit `--workspace` flag |
| Session expired | Re-run `login.py` |
| Screenshots show blank page | Page took too long to render; increase `time.sleep()` values in the script |

---

## Adapting for any tenant

This skill is fully generic. To use it for any tenant:

1. `tc creds set chatgpt_username <their-email>`
2. `tc creds set chatgpt_password <their-password>`
3. `tc proxy` — confirm the proxy is up
4. Run `login.py --workspace "Their Workspace Name"` (or omit for personal account)
5. Session file saved to `/workspace/group/chatgpt-session.json`

---

## Sending Prompts & Reading Responses

Once logged in, use `open_chatgpt_session()` to load the session, then interact
with the chat interface programmatically:

```python
import sys, time
sys.path.insert(0, "/home/node/.claude/skills/chatgpt/scripts")
from load_session import open_chatgpt_session

with open_chatgpt_session() as (ctx, page):
    # 1. Use a wide viewport so the send button is always visible
    page.set_viewport_size({"width": 1280, "height": 900})
    page.goto("https://chatgpt.com/")

    # 2. Click the textarea FIRST — it is inactive until focused
    composer = page.locator(
        '#prompt-textarea, '
        'div[contenteditable="true"][data-id="root"], '
        'textarea[placeholder]'
    ).first
    composer.click()
    time.sleep(0.5)

    # 3. Type the prompt
    composer.type("Generate an image of a sunset over the ocean", delay=40)
    time.sleep(0.3)

    # 4. Click the send button — Enter is a newline in the multi-line composer
    send_btn = page.locator(
        'button[data-testid="send-button"], '
        'button[aria-label="Send message"], '
        'button[aria-label*="Send"]'
    ).first
    send_btn.click()

    # 5. Wait for response (text: ~15s, image generation: 60-120s)
    #    Poll until the send button re-enables (means generation is complete)
    for _ in range(120):
        time.sleep(1)
        try:
            btn = page.locator('button[data-testid="send-button"]').first
            if btn.is_enabled():
                break
        except Exception:
            pass

    # 6. Read the last assistant message text
    messages = page.locator('[data-message-author-role="assistant"]').all()
    if messages:
        print(messages[-1].inner_text())
```

**Key points:**
- Always click the textarea before typing — it is visually inactive until focused
- Use `button[data-testid="send-button"]` to submit (not Enter — it inserts a newline)
- Poll for send-button re-enabling to detect completion, especially for image generation
- Use viewport 1280×900 so the send button is never clipped off-screen

---

## Downloading Generated Images

ChatGPT-generated images are served from `oaiusercontent.com` and require
authenticated session cookies. Extract them from the browser context and
pass them to a download request:

```python
import requests, re, time
from pathlib import Path

with open_chatgpt_session() as (ctx, page):
    # ... send image prompt and wait for completion (see above) ...

    # Extract all generated image URLs from the page
    img_elements = page.query_selector_all(
        'img[src*="oaiusercontent"], img[src*="files.oai"]'
    )
    img_urls = [el.get_attribute("src") for el in img_elements if el.get_attribute("src")]

    if not img_urls:
        print("No generated images found")
    else:
        # Extract cookies from the browser context for authenticated download
        cookies = {c["name"]: c["value"] for c in ctx.cookies()}
        session = requests.Session()
        session.cookies.update(cookies)

        for i, url in enumerate(img_urls):
            resp = session.get(url, timeout=30)
            resp.raise_for_status()
            # Determine extension from Content-Type
            ext = "webp" if "webp" in resp.headers.get("content-type", "") else "png"
            out_path = f"/workspace/group/chatgpt-image-{i+1}.{ext}"
            Path(out_path).write_bytes(resp.content)
            print(f"Saved: {out_path}")
```

**Notes:**
- DALL-E sometimes returns 2 images — iterate all `img_urls`
- Images are typically WebP; check `Content-Type` for the correct extension
- The cookies from the browser context are required — a plain `requests.get(url)` without cookies will get a 403

---

## Session Validity Check

`load_session.py` opens the browser with saved cookies, but the session may
have expired server-side. Check that you are on `chatgpt.com` after loading,
not redirected back to `auth.openai.com`:

```python
from load_session import open_chatgpt_session

try:
    with open_chatgpt_session() as (ctx, page):
        if "auth.openai.com" in page.url or "login" in page.url.lower():
            raise RuntimeError("Session expired — redirected to login page")
        # session is valid, proceed
except RuntimeError as e:
    print(f"Session invalid: {e}")
    print("Re-run: python3 /home/node/.claude/skills/chatgpt/scripts/login.py")
```
