ask.js
1 import { cli, Strategy } from '@jackwener/opencli/registry'; 2 const GROK_URL = 'https://grok.com/'; 3 const RESPONSE_SELECTOR = 'div.message-bubble, [data-testid="message-bubble"]'; 4 const BLOCKED_PREFIX = '[BLOCKED]'; 5 const NO_RESPONSE_PREFIX = '[NO RESPONSE]'; 6 const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing grok.com browser session.'; 7 function blocked(message) { 8 return [{ response: `${BLOCKED_PREFIX} ${message} ${SESSION_HINT}` }]; 9 } 10 function normalizeBubbleText(value) { 11 return typeof value === 'string' ? value.trim() : ''; 12 } 13 function normalizeBooleanFlag(value) { 14 if (typeof value === 'boolean') 15 return value; 16 const normalized = String(value ?? '').trim().toLowerCase(); 17 return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on'; 18 } 19 function pickLatestAssistantCandidate(bubbles, baselineCount, prompt) { 20 const normalizedPrompt = prompt.trim(); 21 const freshBubbles = bubbles 22 .slice(Math.max(0, baselineCount)) 23 .map(normalizeBubbleText) 24 .filter(Boolean); 25 for (let i = freshBubbles.length - 1; i >= 0; i -= 1) { 26 if (freshBubbles[i] !== normalizedPrompt) 27 return freshBubbles[i]; 28 } 29 return ''; 30 } 31 function updateStableState(previousText, stableCount, nextText) { 32 if (!nextText) 33 return { previousText: '', stableCount: 0 }; 34 if (nextText === previousText) 35 return { previousText, stableCount: stableCount + 1 }; 36 return { previousText: nextText, stableCount: 0 }; 37 } 38 /** Check whether the tab is already on grok.com (any path). */ 39 async function isOnGrok(page) { 40 // catch handles blank tabs (about:blank) or detached pages 41 const url = await page.evaluate('window.location.href').catch(() => ''); 42 if (typeof url !== 'string' || !url) 43 return false; 44 try { 45 const hostname = new URL(url).hostname; 46 return hostname === 'grok.com' || hostname.endsWith('.grok.com'); 47 } 48 catch { 49 return false; 50 } 51 } 52 async function runDefaultAsk(page, prompt, timeoutMs, newChat) { 53 if (newChat) { 54 // Explicitly start a fresh conversation via the homepage 55 await page.goto(GROK_URL); 56 await page.wait(2); 57 await tryStartFreshChat(page); 58 await page.wait(2); 59 } 60 else if (!(await isOnGrok(page))) { 61 // First invocation or tab was recycled — navigate to Grok 62 await page.goto(GROK_URL); 63 await page.wait(3); 64 } 65 const promptJson = JSON.stringify(prompt); 66 const sendResult = await page.evaluate(`(async () => { 67 try { 68 const box = document.querySelector('textarea'); 69 if (!box) return { ok: false, msg: 'no textarea' }; 70 box.focus(); box.value = ''; 71 document.execCommand('selectAll'); 72 document.execCommand('insertText', false, ${promptJson}); 73 await new Promise(r => setTimeout(r, 1500)); 74 const btn = document.querySelector('button[aria-label="\\u63d0\\u4ea4"]'); 75 if (btn && !btn.disabled) { btn.click(); return { ok: true, msg: 'clicked' }; } 76 const sub = [...document.querySelectorAll('button[type="submit"]')].find(b => !b.disabled); 77 if (sub) { sub.click(); return { ok: true, msg: 'clicked-submit' }; } 78 box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); 79 return { ok: true, msg: 'enter' }; 80 } catch (e) { return { ok: false, msg: e.toString() }; } 81 })()`); 82 if (!sendResult || !sendResult.ok) { 83 return [{ response: '[SEND FAILED] ' + JSON.stringify(sendResult) }]; 84 } 85 const startTime = Date.now(); 86 let lastText = ''; 87 let stableCount = 0; 88 while (Date.now() - startTime < timeoutMs) { 89 await page.wait(3); 90 const response = await page.evaluate(`(() => { 91 const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"]'); 92 if (bubbles.length < 2) return ''; 93 const last = bubbles[bubbles.length - 1]; 94 const text = (last.innerText || '').trim(); 95 if (!text || text.length < 2) return ''; 96 return text; 97 })()`); 98 if (response && response.length > 2) { 99 if (response === lastText) { 100 stableCount++; 101 if (stableCount >= 2) 102 return [{ response }]; 103 } 104 else { 105 stableCount = 0; 106 } 107 } 108 lastText = response || ''; 109 } 110 if (lastText) 111 return [{ response: lastText }]; 112 return [{ response: NO_RESPONSE_PREFIX }]; 113 } 114 async function getBubbleTexts(page) { 115 const result = await page.evaluate(`(() => { 116 return Array.from(document.querySelectorAll(${JSON.stringify(RESPONSE_SELECTOR)})) 117 .map(node => (node instanceof HTMLElement ? node.innerText : node?.textContent || '')) 118 .map(text => (typeof text === 'string' ? text.trim() : '')) 119 .filter(Boolean); 120 })()`); 121 return Array.isArray(result) ? result.map(normalizeBubbleText).filter(Boolean) : []; 122 } 123 async function tryStartFreshChat(page) { 124 await page.evaluate(`(() => { 125 const isVisible = (node) => { 126 if (!(node instanceof HTMLElement)) return false; 127 const rect = node.getBoundingClientRect(); 128 const style = window.getComputedStyle(node); 129 return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'; 130 }; 131 132 const candidates = Array.from(document.querySelectorAll('a, button')).filter(node => { 133 if (!isVisible(node)) return false; 134 const text = (node.textContent || '').trim().toLowerCase(); 135 const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase(); 136 const href = node.getAttribute('href') || ''; 137 return text.includes('new chat') 138 || text.includes('new conversation') 139 || aria.includes('new chat') 140 || aria.includes('new conversation') 141 || href === '/'; 142 }); 143 144 const target = candidates[0]; 145 if (target instanceof HTMLElement) target.click(); 146 })()`); 147 } 148 async function sendPromptViaExplicitWeb(page, prompt) { 149 return page.evaluate(`(async () => { 150 const waitFor = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 151 const composerSelector = '.ProseMirror[contenteditable="true"]'; 152 let composer = null; 153 154 for (let attempt = 0; attempt < 12; attempt += 1) { 155 const candidate = document.querySelector(composerSelector); 156 if (candidate instanceof HTMLElement) { 157 composer = candidate; 158 break; 159 } 160 161 await waitFor(1000); 162 } 163 164 if (!(composer instanceof HTMLElement)) { 165 return { 166 ok: false, 167 reason: 'Grok composer was not found on grok.com.', 168 }; 169 } 170 171 const editor = composer.editor; 172 if (!editor?.commands?.focus || !editor?.commands?.insertContent) { 173 return { 174 ok: false, 175 reason: 'Grok composer editor API was unavailable.', 176 }; 177 } 178 179 const isVisibleEnabledSubmit = (node) => { 180 if (!(node instanceof HTMLButtonElement)) return false; 181 const rect = node.getBoundingClientRect(); 182 const style = window.getComputedStyle(node); 183 return !node.disabled 184 && rect.width > 0 185 && rect.height > 0 186 && style.visibility !== 'hidden' 187 && style.display !== 'none'; 188 }; 189 190 try { 191 if (editor.commands.clearContent) editor.commands.clearContent(); 192 editor.commands.focus(); 193 editor.commands.insertContent(${JSON.stringify(prompt)}); 194 } catch (error) { 195 return { 196 ok: false, 197 reason: 'Failed to insert the prompt into the Grok composer.', 198 detail: error instanceof Error ? error.message : String(error), 199 }; 200 } 201 202 let submit = null; 203 for (let attempt = 0; attempt < 6; attempt += 1) { 204 const candidate = Array.from(document.querySelectorAll('button[aria-label="Submit"]')) 205 .find(isVisibleEnabledSubmit); 206 207 if (candidate instanceof HTMLButtonElement) { 208 submit = candidate; 209 break; 210 } 211 212 await waitFor(500); 213 } 214 215 if (!(submit instanceof HTMLButtonElement)) { 216 return { 217 ok: false, 218 reason: 'Grok submit button did not reach a clickable ready state after prompt insertion.', 219 }; 220 } 221 222 submit.click(); 223 return { ok: true }; 224 })()`); 225 } 226 async function runExplicitWebAsk(page, prompt, timeoutMs, newChat) { 227 if (newChat) { 228 // Navigate to homepage and start a fresh conversation 229 await page.goto(GROK_URL, { settleMs: 2000 }); 230 await tryStartFreshChat(page); 231 await page.wait(2); 232 } 233 else if (!(await isOnGrok(page))) { 234 // First invocation or tab was recycled — navigate to Grok 235 await page.goto(GROK_URL, { settleMs: 2000 }); 236 } 237 const baselineBubbles = await getBubbleTexts(page); 238 const sendResult = await sendPromptViaExplicitWeb(page, prompt); 239 if (!sendResult?.ok) { 240 const details = sendResult?.detail ? ` ${sendResult.detail}` : ''; 241 return blocked(`${sendResult?.reason || 'Unable to send the prompt to Grok.'}${details}`); 242 } 243 const startTime = Date.now(); 244 let lastText = ''; 245 let stableCount = 0; 246 while (Date.now() - startTime < timeoutMs) { 247 await page.wait(2); 248 const bubbleTexts = await getBubbleTexts(page); 249 const candidate = pickLatestAssistantCandidate(bubbleTexts, baselineBubbles.length, prompt); 250 const nextState = updateStableState(lastText, stableCount, candidate); 251 lastText = nextState.previousText; 252 stableCount = nextState.stableCount; 253 if (candidate && stableCount >= 2) { 254 return [{ response: candidate }]; 255 } 256 } 257 if (lastText) 258 return [{ response: lastText }]; 259 return [{ response: `${NO_RESPONSE_PREFIX} No new assistant message bubble appeared within ${Math.round(timeoutMs / 1000)}s.` }]; 260 } 261 export const askCommand = cli({ 262 site: 'grok', 263 name: 'ask', 264 description: 'Send a message to Grok and get response', 265 domain: 'grok.com', 266 strategy: Strategy.COOKIE, 267 browser: true, 268 args: [ 269 { name: 'prompt', positional: true, type: 'string', required: true, help: 'Prompt to send to Grok' }, 270 { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response (default: 120)' }, 271 { name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending (default: false)' }, 272 { name: 'web', type: 'boolean', default: false, help: 'Use the explicit grok.com consumer web flow (default: false)' }, 273 ], 274 columns: ['response'], 275 func: async (page, kwargs) => { 276 const prompt = kwargs.prompt; 277 const timeoutMs = (kwargs.timeout || 120) * 1000; 278 const newChat = normalizeBooleanFlag(kwargs.new); 279 const useExplicitWeb = normalizeBooleanFlag(kwargs.web); 280 if (useExplicitWeb) { 281 return runExplicitWebAsk(page, prompt, timeoutMs, newChat); 282 } 283 return runDefaultAsk(page, prompt, timeoutMs, newChat); 284 }, 285 }); 286 export const __test__ = { 287 pickLatestAssistantCandidate, 288 updateStableState, 289 normalizeBooleanFlag, 290 normalizeBubbleText, 291 isOnGrok, 292 };