Files
plan-tools/lib/chatgpt-send.mjs
Taylor Eernisse e7882b917b Add Brave CDP automation, replace Oracle browser mode
Connects to user's running Brave via Chrome DevTools Protocol
to automate ChatGPT interaction. Uses puppeteer-core to open a
tab, send the prompt, wait for response, and extract the result.

No cookies, no separate profiles, no copy/paste. Just connects
to the browser where the user is already logged in.

One-time setup: relaunch Brave with --remote-debugging-port=9222

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:16:41 -05:00

197 lines
6.8 KiB
JavaScript

#!/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 <prompt-file> <output-file> [--timeout <seconds>]
//
// 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 <prompt-file> <output-file> [--timeout <seconds>]');
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);
});