/ clis / grok / ask.js
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  };