#!/usr/bin/env node // chatgpt-send.mjs — Send a prompt to ChatGPT via the user's running Brave browser // Connects via Chrome DevTools Protocol to an already-authenticated session. // // Usage: node chatgpt-send.mjs [--timeout ] // // Requires: Brave running with --remote-debugging-port=9222 import puppeteer from 'puppeteer-core'; import { readFileSync, writeFileSync } from 'fs'; const CDP_URL = `http://127.0.0.1:${process.env.CHATGPT_CDP_PORT || '9222'}`; const DEFAULT_TIMEOUT_SEC = 600; // 10 minutes for long responses function parseArgs() { const args = process.argv.slice(2); let promptFile = null; let outputFile = null; let timeoutSec = DEFAULT_TIMEOUT_SEC; for (let i = 0; i < args.length; i++) { if (args[i] === '--timeout' && args[i + 1]) { timeoutSec = parseInt(args[i + 1], 10); i++; } else if (!promptFile) { promptFile = args[i]; } else if (!outputFile) { outputFile = args[i]; } } if (!promptFile || !outputFile) { console.error('Usage: node chatgpt-send.mjs [--timeout ]'); process.exit(2); } return { promptFile, outputFile, timeoutSec }; } async function waitForSelector(page, selector, timeout) { return page.waitForSelector(selector, { timeout }); } async function main() { const { promptFile, outputFile, timeoutSec } = parseArgs(); const prompt = readFileSync(promptFile, 'utf-8').trim(); const timeoutMs = timeoutSec * 1000; // Connect to running Brave let browser; try { browser = await puppeteer.connect({ browserURL: CDP_URL }); } catch (err) { console.error(`Cannot connect to Brave at ${CDP_URL}`); console.error('Make sure Brave is running with: --remote-debugging-port=9222'); console.error(''); console.error('Relaunch Brave:'); console.error(' 1. Quit Brave (Cmd+Q)'); console.error(' 2. Run: open -a "Brave Browser" --args --remote-debugging-port=9222'); process.exit(1); } // Open new tab for ChatGPT const page = await browser.newPage(); try { console.error('Navigating to ChatGPT...'); await page.goto('https://chatgpt.com/', { waitUntil: 'networkidle2', timeout: 30000 }); // Verify we're logged in by checking for the composer const composerSelector = '#prompt-textarea, [id="prompt-textarea"], div[contenteditable="true"][data-placeholder]'; try { await waitForSelector(page, composerSelector, 15000); } catch { // Check if login button is present const loginBtn = await page.$('button[data-testid="login-button"], a[href*="auth"]'); if (loginBtn) { console.error('ERROR: Not logged into ChatGPT. Log in via Brave first.'); process.exit(1); } console.error('ERROR: Could not find ChatGPT composer. The UI may have changed.'); process.exit(1); } console.error('Logged in. Sending prompt...'); // Find and focus the composer const composer = await page.$(composerSelector); await composer.click(); // Type the prompt — use clipboard for large prompts await page.evaluate(async (text) => { const composer = document.querySelector('#prompt-textarea, [id="prompt-textarea"], div[contenteditable="true"][data-placeholder]'); if (composer) { // Use execCommand for contenteditable divs composer.focus(); // Clear existing content document.execCommand('selectAll', false, null); // Insert via clipboard API for reliability with large text const clipItem = new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }) }); await navigator.clipboard.write([clipItem]); document.execCommand('paste'); } }, prompt); // Small delay to ensure content is rendered await new Promise(r => setTimeout(r, 500)); // Verify content was entered const composerText = await page.evaluate(() => { const el = document.querySelector('#prompt-textarea, [id="prompt-textarea"], div[contenteditable="true"][data-placeholder]'); return el ? el.textContent.length : 0; }); if (composerText < 10) { // Fallback: type directly (slower but more reliable) console.error('Clipboard paste failed, typing directly...'); await composer.click({ clickCount: 3 }); // select all await page.keyboard.type(prompt, { delay: 1 }); } // Find and click the send button const sendSelector = 'button[data-testid="send-button"], button[aria-label="Send prompt"], button[aria-label*="Send"]'; const sendBtn = await page.$(sendSelector); if (sendBtn) { await sendBtn.click(); } else { // Fallback: press Enter await page.keyboard.press('Enter'); } console.error('Prompt sent. Waiting for response...'); // Wait for the response to complete // Strategy: watch for the stop button to appear then disappear const stopSelector = 'button[data-testid="stop-button"], button[aria-label="Stop generating"], button[aria-label*="Stop"]'; // Wait for generation to start (stop button appears) try { await waitForSelector(page, stopSelector, 30000); console.error('Generating...'); } catch { // Stop button might not appear for very fast responses console.error('Response may have completed quickly.'); } // Wait for generation to finish (stop button disappears) await page.waitForFunction( (sel) => !document.querySelector(sel), { timeout: timeoutMs, polling: 1000 }, stopSelector ); // Small delay for final rendering await new Promise(r => setTimeout(r, 2000)); console.error('Response complete. Extracting...'); // Extract the last assistant message const response = await page.evaluate(() => { // ChatGPT renders assistant messages in article elements or divs with specific data attributes const messages = document.querySelectorAll( '[data-message-author-role="assistant"], article[data-testid*="conversation-turn"]' ); if (messages.length === 0) return null; const lastMessage = messages[messages.length - 1]; // Try to get markdown content via the copy button's data // Fallback to innerText const markdownEl = lastMessage.querySelector('.markdown, .prose'); if (markdownEl) return markdownEl.innerText; return lastMessage.innerText; }); if (!response) { console.error('ERROR: Could not extract response from page.'); process.exit(1); } writeFileSync(outputFile, response, 'utf-8'); console.error(`Response saved to: ${outputFile} (${response.length} chars)`); } finally { // Close the tab we opened, don't close the browser await page.close(); browser.disconnect(); } } main().catch(err => { console.error(`Fatal: ${err.message}`); process.exit(1); });