surf-cli 2.7.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -9
- package/native/browser-lock.cjs +169 -0
- package/native/chatgpt-client.cjs +18 -1
- package/native/cli.cjs +670 -279
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +2 -9
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +583 -0
- package/native/gemini-client.cjs +91 -20
- package/native/grok-client.cjs +270 -170
- package/native/host-helpers.cjs +51 -4
- package/native/host.cjs +22 -7
- package/native/mcp-server.cjs +10 -7
- package/native/socket-path.cjs +46 -0
- package/package.json +5 -5
- package/scripts/install-native-host.cjs +155 -53
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/surf/SKILL.md +46 -18
package/native/grok-client.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Grok Web Client for surf-cli
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* CDP-based client for X.com's Grok AI using browser automation.
|
|
5
5
|
* Provides access to Grok's unique real-time X/Twitter data capabilities.
|
|
6
6
|
*/
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
const { loadConfig, getConfigPath, clearCache } = require("./config.cjs");
|
|
9
9
|
|
|
10
10
|
const GROK_URL = "https://x.com/i/grok";
|
|
11
|
-
const DEFAULT_MODEL = "
|
|
11
|
+
const DEFAULT_MODEL = "fast";
|
|
12
12
|
|
|
13
|
-
// Default models (as of
|
|
13
|
+
// Default models (as of Jun 2026)
|
|
14
14
|
const DEFAULT_GROK_MODELS = {
|
|
15
15
|
"auto": { id: "auto", name: "Auto", desc: "Chooses Fast or Expert" },
|
|
16
16
|
"fast": { id: "fast", name: "Fast", desc: "Quick responses" },
|
|
17
17
|
"expert": { id: "expert", name: "Expert", desc: "Thinks hard" },
|
|
18
|
-
"
|
|
18
|
+
"grok-4.20-beta": { id: "grok-4.20-beta", name: "Grok 4.20 Beta", desc: "4 Agents" },
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
// Load models from surf.json config or use defaults
|
|
@@ -60,6 +60,70 @@ function buildClickDispatcher() {
|
|
|
60
60
|
}`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function normalizeGrokModelLabel(text) {
|
|
64
|
+
return String(text || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getGrokModelMatchLabels(desiredModel) {
|
|
68
|
+
const labels = new Set([desiredModel]);
|
|
69
|
+
const models = getGrokModels();
|
|
70
|
+
const configured = models[desiredModel];
|
|
71
|
+
if (configured) {
|
|
72
|
+
labels.add(configured.id);
|
|
73
|
+
labels.add(configured.name);
|
|
74
|
+
} else {
|
|
75
|
+
for (const model of Object.values(models)) {
|
|
76
|
+
if (model?.id === desiredModel) {
|
|
77
|
+
labels.add(model.id);
|
|
78
|
+
labels.add(model.name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return Array.from(labels).filter(Boolean).map(normalizeGrokModelLabel);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function grokModelLabelsMatch(foundLabel, requestedLabels) {
|
|
86
|
+
const found = normalizeGrokModelLabel(foundLabel);
|
|
87
|
+
return requestedLabels.some(label => label && (found.includes(label) || label.includes(found)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function grokSendButtonFinderScript() {
|
|
91
|
+
return `
|
|
92
|
+
const isVisible = (el) => {
|
|
93
|
+
const style = window.getComputedStyle?.(el);
|
|
94
|
+
const rect = el.getBoundingClientRect?.();
|
|
95
|
+
return el.offsetParent !== null &&
|
|
96
|
+
(!style || (style.visibility !== 'hidden' && style.display !== 'none')) &&
|
|
97
|
+
(!rect || (rect.width > 0 && rect.height > 0));
|
|
98
|
+
};
|
|
99
|
+
const isEnabled = (el) => !el.disabled && el.getAttribute('aria-disabled') !== 'true';
|
|
100
|
+
const matchesSendButton = (button) => {
|
|
101
|
+
const label = (button.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
102
|
+
const testId = (button.getAttribute('data-testid') || '').trim().toLowerCase();
|
|
103
|
+
return label === 'send' || label === 'submit' ||
|
|
104
|
+
label.startsWith('send ') || label.startsWith('submit ') ||
|
|
105
|
+
testId === 'groksend' || testId === 'send-button' ||
|
|
106
|
+
testId.includes('composer-send') || testId.includes('grok-send');
|
|
107
|
+
};
|
|
108
|
+
const input = document.querySelector('textarea, [contenteditable="true"][role="textbox"], [data-testid="grokComposerInput"]');
|
|
109
|
+
const scopes = [];
|
|
110
|
+
if (input) {
|
|
111
|
+
const form = input.closest('form');
|
|
112
|
+
const composer = input.closest('[data-testid*="composer" i], [aria-label*="composer" i], [role="form"]');
|
|
113
|
+
if (form) scopes.push(form);
|
|
114
|
+
if (composer && composer !== form) scopes.push(composer);
|
|
115
|
+
}
|
|
116
|
+
scopes.push(document);
|
|
117
|
+
let sendBtn = null;
|
|
118
|
+
for (const scope of scopes) {
|
|
119
|
+
sendBtn = Array.from(scope.querySelectorAll('button')).find(b =>
|
|
120
|
+
matchesSendButton(b) && isVisible(b) && isEnabled(b)
|
|
121
|
+
);
|
|
122
|
+
if (sendBtn) break;
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
63
127
|
function hasRequiredCookies(cookies) {
|
|
64
128
|
if (!cookies || !Array.isArray(cookies)) return false;
|
|
65
129
|
// auth_token is the primary session cookie for X.com
|
|
@@ -71,8 +135,8 @@ function hasRequiredCookies(cookies) {
|
|
|
71
135
|
async function evaluate(cdp, expression) {
|
|
72
136
|
const result = await cdp(expression);
|
|
73
137
|
if (result.exceptionDetails) {
|
|
74
|
-
const desc = result.exceptionDetails.exception?.description ||
|
|
75
|
-
result.exceptionDetails.text ||
|
|
138
|
+
const desc = result.exceptionDetails.exception?.description ||
|
|
139
|
+
result.exceptionDetails.text ||
|
|
76
140
|
"Evaluation failed";
|
|
77
141
|
throw new Error(desc);
|
|
78
142
|
}
|
|
@@ -106,30 +170,30 @@ async function checkLoginStatus(cdp) {
|
|
|
106
170
|
const hasLoginButton = !!document.querySelector('a[href*="/login"], [data-testid="loginButton"]');
|
|
107
171
|
const hasGrokUI = body.includes('ask anything') || body.includes('grok');
|
|
108
172
|
const hasPremiumPrompt = body.includes('subscribe') || body.includes('premium required');
|
|
109
|
-
|
|
173
|
+
|
|
110
174
|
return {
|
|
111
175
|
loggedIn: !hasLoginButton && hasGrokUI,
|
|
112
176
|
hasPremium: hasGrokUI && !hasPremiumPrompt,
|
|
113
177
|
url: location.href
|
|
114
178
|
};
|
|
115
179
|
})()`);
|
|
116
|
-
|
|
180
|
+
|
|
117
181
|
return result || { loggedIn: false, hasPremium: false };
|
|
118
182
|
}
|
|
119
183
|
|
|
120
184
|
async function waitForGrokReady(cdp, timeoutMs = 20000) {
|
|
121
185
|
const deadline = Date.now() + timeoutMs;
|
|
122
186
|
let lastState = null;
|
|
123
|
-
|
|
187
|
+
|
|
124
188
|
while (Date.now() < deadline) {
|
|
125
189
|
const state = await evaluate(cdp, `(() => {
|
|
126
190
|
// Check for Grok-specific elements
|
|
127
191
|
const hasInput = !!document.querySelector('textarea, [contenteditable="true"][role="textbox"], [data-testid="grokComposerInput"]');
|
|
128
|
-
const hasGrokBranding = document.body.innerText.includes('Grok') ||
|
|
192
|
+
const hasGrokBranding = document.body.innerText.includes('Grok') ||
|
|
129
193
|
!!document.querySelector('[data-testid*="grok"]');
|
|
130
194
|
const isGrokPage = location.pathname.includes('/grok');
|
|
131
195
|
const isLoginPage = location.pathname.includes('/login') || location.pathname.includes('/i/flow');
|
|
132
|
-
|
|
196
|
+
|
|
133
197
|
return {
|
|
134
198
|
ready: isGrokPage && (hasInput || hasGrokBranding),
|
|
135
199
|
hasInput,
|
|
@@ -138,26 +202,26 @@ async function waitForGrokReady(cdp, timeoutMs = 20000) {
|
|
|
138
202
|
url: location.href
|
|
139
203
|
};
|
|
140
204
|
})()`);
|
|
141
|
-
|
|
205
|
+
|
|
142
206
|
lastState = state;
|
|
143
|
-
|
|
207
|
+
|
|
144
208
|
if (state && state.ready) {
|
|
145
209
|
return state;
|
|
146
210
|
}
|
|
147
|
-
|
|
211
|
+
|
|
148
212
|
// If redirected to login, fail fast
|
|
149
213
|
if (state && state.isLoginPage) {
|
|
150
214
|
throw new Error("Redirected to login page - X.com login required");
|
|
151
215
|
}
|
|
152
|
-
|
|
216
|
+
|
|
153
217
|
await delay(200);
|
|
154
218
|
}
|
|
155
|
-
|
|
219
|
+
|
|
156
220
|
// Timeout - provide helpful error based on last state
|
|
157
221
|
if (lastState && !lastState.isGrokPage) {
|
|
158
222
|
throw new Error(`Not on Grok page (current: ${lastState.url}) - may need to log in`);
|
|
159
223
|
}
|
|
160
|
-
|
|
224
|
+
|
|
161
225
|
// Return fallback for edge cases where we're on Grok page but UI isn't detected
|
|
162
226
|
return { ready: true, fallback: true };
|
|
163
227
|
}
|
|
@@ -167,13 +231,13 @@ async function waitForGrokReady(cdp, timeoutMs = 20000) {
|
|
|
167
231
|
// ============================================================================
|
|
168
232
|
|
|
169
233
|
async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
170
|
-
const
|
|
171
|
-
|
|
234
|
+
const requestedLabels = getGrokModelMatchLabels(desiredModel);
|
|
235
|
+
|
|
172
236
|
// First, find and click the model selector button
|
|
173
237
|
const buttonClicked = await evaluate(cdp, `(() => {
|
|
174
238
|
${buildClickDispatcher()}
|
|
175
|
-
|
|
176
|
-
// Look for model selector button (shows current model: Auto, Fast, Expert, or Grok 4.
|
|
239
|
+
|
|
240
|
+
// Look for model selector button (shows current model: Auto, Fast, Expert, or Grok 4.x)
|
|
177
241
|
const buttons = Array.from(document.querySelectorAll('button'));
|
|
178
242
|
const modelBtn = buttons.find(b => {
|
|
179
243
|
const text = (b.textContent || '').toLowerCase();
|
|
@@ -184,78 +248,81 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
|
184
248
|
const hasModelLabel = label.includes('model') || testId.includes('model');
|
|
185
249
|
return hasModelName || hasModelLabel;
|
|
186
250
|
});
|
|
187
|
-
|
|
251
|
+
|
|
188
252
|
if (!modelBtn) return { success: false, error: 'Model selector not found' };
|
|
189
|
-
|
|
253
|
+
|
|
190
254
|
dispatchClickSequence(modelBtn);
|
|
191
255
|
return { success: true };
|
|
192
256
|
})()`);
|
|
193
|
-
|
|
257
|
+
|
|
194
258
|
if (!buttonClicked || !buttonClicked.success) {
|
|
195
259
|
// Model selector might not exist (single model), continue anyway
|
|
196
260
|
return desiredModel;
|
|
197
261
|
}
|
|
198
|
-
|
|
262
|
+
|
|
199
263
|
await delay(400);
|
|
200
|
-
|
|
264
|
+
|
|
201
265
|
// Select from menu - loop in Node.js to avoid CDP timeout issues
|
|
202
266
|
const deadline = Date.now() + timeoutMs;
|
|
203
|
-
|
|
267
|
+
|
|
204
268
|
while (Date.now() < deadline) {
|
|
205
269
|
const result = await evaluate(cdp, `(() => {
|
|
206
270
|
${buildClickDispatcher()}
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9
|
|
210
|
-
|
|
271
|
+
|
|
272
|
+
const requestedLabels = ${JSON.stringify(requestedLabels)};
|
|
273
|
+
const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
274
|
+
|
|
211
275
|
// Look for menu items
|
|
212
276
|
const items = document.querySelectorAll('[role="menuitem"], [role="menuitemradio"], [role="option"]');
|
|
213
|
-
|
|
277
|
+
|
|
214
278
|
if (items.length === 0) {
|
|
215
279
|
return { found: false, waiting: true };
|
|
216
280
|
}
|
|
217
|
-
|
|
281
|
+
|
|
218
282
|
let bestMatch = null;
|
|
219
283
|
let bestScore = 0;
|
|
220
|
-
|
|
284
|
+
|
|
221
285
|
for (const item of items) {
|
|
222
286
|
const text = normalize(item.textContent || '');
|
|
223
287
|
let score = 0;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
288
|
+
|
|
289
|
+
for (const label of requestedLabels) {
|
|
290
|
+
if (!label) continue;
|
|
291
|
+
if (text === label) score = Math.max(score, 100);
|
|
292
|
+
else if (text.includes(label)) score = Math.max(score, 90);
|
|
293
|
+
else if (label.includes(text) && text.length > 3) score = Math.max(score, 50);
|
|
294
|
+
}
|
|
295
|
+
|
|
229
296
|
if (score > bestScore) {
|
|
230
297
|
bestScore = score;
|
|
231
298
|
bestMatch = item;
|
|
232
299
|
}
|
|
233
300
|
}
|
|
234
|
-
|
|
301
|
+
|
|
235
302
|
if (bestMatch) {
|
|
236
303
|
dispatchClickSequence(bestMatch);
|
|
237
304
|
return { found: true, success: true, model: bestMatch.textContent?.trim() };
|
|
238
305
|
}
|
|
239
|
-
|
|
306
|
+
|
|
240
307
|
return { found: true, success: false, error: 'No matching model in menu' };
|
|
241
308
|
})()`);
|
|
242
|
-
|
|
309
|
+
|
|
243
310
|
if (result && result.found) {
|
|
244
311
|
if (result.success) {
|
|
245
312
|
await delay(200);
|
|
246
313
|
return result.model;
|
|
247
314
|
}
|
|
248
|
-
// Items found but no match - close menu and
|
|
315
|
+
// Items found but no match - close menu and surface the failure to callers.
|
|
249
316
|
await evaluate(cdp, `document.body.click()`);
|
|
250
|
-
|
|
317
|
+
throw new Error(result.error ? `${result.error} for "${desiredModel}"` : `No matching model in menu for "${desiredModel}"`);
|
|
251
318
|
}
|
|
252
|
-
|
|
319
|
+
|
|
253
320
|
await delay(100);
|
|
254
321
|
}
|
|
255
|
-
|
|
322
|
+
|
|
256
323
|
// Timeout - close menu
|
|
257
324
|
await evaluate(cdp, `document.body.click()`);
|
|
258
|
-
|
|
325
|
+
throw new Error(`Timed out waiting for model menu to show "${desiredModel}"`);
|
|
259
326
|
}
|
|
260
327
|
|
|
261
328
|
// ============================================================================
|
|
@@ -265,7 +332,7 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
|
265
332
|
async function enableDeepSearch(cdp) {
|
|
266
333
|
const result = await evaluate(cdp, `(() => {
|
|
267
334
|
${buildClickDispatcher()}
|
|
268
|
-
|
|
335
|
+
|
|
269
336
|
// Look for DeepSearch toggle or button
|
|
270
337
|
const buttons = Array.from(document.querySelectorAll('button, [role="switch"]'));
|
|
271
338
|
const deepSearchBtn = buttons.find(b => {
|
|
@@ -275,27 +342,27 @@ async function enableDeepSearch(cdp) {
|
|
|
275
342
|
return text.includes('deepsearch') || text.includes('deep search') ||
|
|
276
343
|
label.includes('deepsearch') || label.includes('deep search');
|
|
277
344
|
});
|
|
278
|
-
|
|
345
|
+
|
|
279
346
|
if (!deepSearchBtn) {
|
|
280
347
|
return { success: false, error: 'DeepSearch toggle not found' };
|
|
281
348
|
}
|
|
282
|
-
|
|
349
|
+
|
|
283
350
|
// Check if already enabled
|
|
284
351
|
const isEnabled = deepSearchBtn.getAttribute('aria-checked') === 'true' ||
|
|
285
352
|
deepSearchBtn.classList.contains('active');
|
|
286
|
-
|
|
353
|
+
|
|
287
354
|
if (isEnabled) {
|
|
288
355
|
return { success: true, alreadyEnabled: true };
|
|
289
356
|
}
|
|
290
|
-
|
|
357
|
+
|
|
291
358
|
dispatchClickSequence(deepSearchBtn);
|
|
292
359
|
return { success: true };
|
|
293
360
|
})()`);
|
|
294
|
-
|
|
361
|
+
|
|
295
362
|
if (result && result.success) {
|
|
296
363
|
await delay(300);
|
|
297
364
|
}
|
|
298
|
-
|
|
365
|
+
|
|
299
366
|
return result || { success: false };
|
|
300
367
|
}
|
|
301
368
|
|
|
@@ -307,7 +374,7 @@ async function typePrompt(cdp, inputCdp, prompt) {
|
|
|
307
374
|
// Focus the input area
|
|
308
375
|
const focused = await evaluate(cdp, `(() => {
|
|
309
376
|
${buildClickDispatcher()}
|
|
310
|
-
|
|
377
|
+
|
|
311
378
|
// Strategy 1: Find textarea or contenteditable
|
|
312
379
|
const inputs = document.querySelectorAll('textarea, [contenteditable="true"][role="textbox"], [data-testid="grokComposerInput"]');
|
|
313
380
|
for (const el of inputs) {
|
|
@@ -317,7 +384,7 @@ async function typePrompt(cdp, inputCdp, prompt) {
|
|
|
317
384
|
return { success: true, method: 'input' };
|
|
318
385
|
}
|
|
319
386
|
}
|
|
320
|
-
|
|
387
|
+
|
|
321
388
|
// Strategy 2: Look for elements with "Ask" placeholder (more targeted selector)
|
|
322
389
|
const placeholderEls = document.querySelectorAll('[placeholder*="Ask"], [placeholder*="ask"], [aria-placeholder*="Ask"]');
|
|
323
390
|
for (const el of placeholderEls) {
|
|
@@ -327,16 +394,16 @@ async function typePrompt(cdp, inputCdp, prompt) {
|
|
|
327
394
|
return { success: true, method: 'placeholder' };
|
|
328
395
|
}
|
|
329
396
|
}
|
|
330
|
-
|
|
397
|
+
|
|
331
398
|
return { success: false, error: 'Input not found' };
|
|
332
399
|
})()`);
|
|
333
|
-
|
|
400
|
+
|
|
334
401
|
if (!focused || !focused.success) {
|
|
335
402
|
throw new Error(`Could not focus input: ${focused?.error || 'unknown'}`);
|
|
336
403
|
}
|
|
337
|
-
|
|
404
|
+
|
|
338
405
|
await delay(300);
|
|
339
|
-
|
|
406
|
+
|
|
340
407
|
// Type using CDP Input API
|
|
341
408
|
await inputCdp("Input.insertText", { text: prompt });
|
|
342
409
|
await delay(200);
|
|
@@ -346,24 +413,17 @@ async function submitPrompt(cdp, inputCdp) {
|
|
|
346
413
|
// Try to click send button
|
|
347
414
|
const clicked = await evaluate(cdp, `(() => {
|
|
348
415
|
${buildClickDispatcher()}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const label = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
354
|
-
const testId = b.getAttribute('data-testid') || '';
|
|
355
|
-
return label.includes('send') || testId.includes('send') ||
|
|
356
|
-
testId.includes('submit') || testId.includes('grokSend');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
if (sendBtn && !sendBtn.disabled) {
|
|
416
|
+
|
|
417
|
+
${grokSendButtonFinderScript()}
|
|
418
|
+
|
|
419
|
+
if (sendBtn) {
|
|
360
420
|
dispatchClickSequence(sendBtn);
|
|
361
421
|
return { success: true, method: 'button' };
|
|
362
422
|
}
|
|
363
|
-
|
|
423
|
+
|
|
364
424
|
return { success: false };
|
|
365
425
|
})()`);
|
|
366
|
-
|
|
426
|
+
|
|
367
427
|
if (!clicked || !clicked.success) {
|
|
368
428
|
// Fallback: press Enter
|
|
369
429
|
await inputCdp("Input.dispatchKeyEvent", {
|
|
@@ -382,7 +442,7 @@ async function submitPrompt(cdp, inputCdp) {
|
|
|
382
442
|
nativeVirtualKeyCode: 13,
|
|
383
443
|
});
|
|
384
444
|
}
|
|
385
|
-
|
|
445
|
+
|
|
386
446
|
await delay(500);
|
|
387
447
|
}
|
|
388
448
|
|
|
@@ -390,13 +450,56 @@ async function submitPrompt(cdp, inputCdp) {
|
|
|
390
450
|
// Response Handling
|
|
391
451
|
// ============================================================================
|
|
392
452
|
|
|
453
|
+
function looksLikeTrailingSuggestion(line) {
|
|
454
|
+
if (!line || line.length < 4 || line.length > 90) return false;
|
|
455
|
+
if (/[.!:;)]$/.test(line)) return false;
|
|
456
|
+
const words = line.split(/\s+/).filter(Boolean);
|
|
457
|
+
if (words.length < 2 || words.length > 9) return false;
|
|
458
|
+
if (/^[-*•\d]/.test(line)) return false;
|
|
459
|
+
if (/\b(https?:\/\/|www\.)\b/i.test(line)) return false;
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function trimTrailingSuggestionLines(lines) {
|
|
464
|
+
let end = lines.length;
|
|
465
|
+
while (end > 0 && looksLikeTrailingSuggestion(lines[end - 1])) {
|
|
466
|
+
end--;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const trimmedCount = lines.length - end;
|
|
470
|
+
if (trimmedCount >= 2 && end > 0) {
|
|
471
|
+
return lines.slice(0, end);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const last = lines[lines.length - 1];
|
|
475
|
+
if (
|
|
476
|
+
trimmedCount === 1 &&
|
|
477
|
+
lines.length > 1 &&
|
|
478
|
+
/^(explain|tell|share|compare|derive|make|show|summari[sz]e|expand|rewrite)\b/i.test(last)
|
|
479
|
+
) {
|
|
480
|
+
return lines.slice(0, -1);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const previous = lines[lines.length - 2];
|
|
484
|
+
if (
|
|
485
|
+
last &&
|
|
486
|
+
previous &&
|
|
487
|
+
/^[A-Z][a-z]{3,}$/.test(last) &&
|
|
488
|
+
/(?:[.!?)]|^\d+(?:\.\d+)?$)/.test(previous)
|
|
489
|
+
) {
|
|
490
|
+
return lines.slice(0, -1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return lines;
|
|
494
|
+
}
|
|
495
|
+
|
|
393
496
|
// Extract Grok's response from the full page body text
|
|
394
497
|
function extractGrokResponse(bodyText, userPrompt = '') {
|
|
395
498
|
if (!bodyText) return null;
|
|
396
|
-
|
|
499
|
+
|
|
397
500
|
// Split into lines and filter out navigation/UI elements
|
|
398
501
|
const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l);
|
|
399
|
-
|
|
502
|
+
|
|
400
503
|
// Known UI elements to skip
|
|
401
504
|
const uiPatterns = [
|
|
402
505
|
/^(Home|Explore|Notifications|Messages|Chat|Grok|Premium|Bookmarks|Communities|Profile|More|Post)$/i,
|
|
@@ -405,15 +508,15 @@ function extractGrokResponse(bodyText, userPrompt = '') {
|
|
|
405
508
|
/^(Create recurring tasks|Get access to|Explore)$/i,
|
|
406
509
|
/^(Think Harder)$/i,
|
|
407
510
|
/^(Auto|Fast|Expert)$/i, // Model names
|
|
408
|
-
/^Grok\s*\d/i, //
|
|
511
|
+
/^Grok\s*\d/i, // Grok 4.x model names
|
|
409
512
|
/^@\w+$/, // Username mentions alone
|
|
410
513
|
/^[A-Z][a-z]+ \d+$/, // Dates like "Jan 20"
|
|
411
514
|
/^(See new posts|Talk to Grok|Get access to)/, // Sidebar promos
|
|
412
515
|
];
|
|
413
|
-
|
|
516
|
+
|
|
414
517
|
// Normalize prompt for comparison (first 30 chars to handle truncation)
|
|
415
518
|
const promptNorm = userPrompt.toLowerCase().replace(/[^a-z0-9]/g, '').substring(0, 30);
|
|
416
|
-
|
|
519
|
+
|
|
417
520
|
// Find the LAST occurrence of the user's question to get the most recent conversation
|
|
418
521
|
let lastQuestionIndex = -1;
|
|
419
522
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -423,31 +526,30 @@ function extractGrokResponse(bodyText, userPrompt = '') {
|
|
|
423
526
|
break;
|
|
424
527
|
}
|
|
425
528
|
}
|
|
426
|
-
|
|
529
|
+
|
|
427
530
|
// Extract content after the last question
|
|
428
531
|
const contentLines = [];
|
|
429
532
|
const startIndex = lastQuestionIndex >= 0 ? lastQuestionIndex + 1 : 0;
|
|
430
|
-
|
|
533
|
+
|
|
431
534
|
for (let i = startIndex; i < lines.length; i++) {
|
|
432
535
|
const line = lines[i];
|
|
433
|
-
|
|
536
|
+
|
|
434
537
|
// Skip empty and UI lines
|
|
435
538
|
if (!line || uiPatterns.some(p => p.test(line))) continue;
|
|
436
|
-
|
|
539
|
+
|
|
437
540
|
// Skip very short lines that are likely icons/buttons (but keep numbers)
|
|
438
541
|
if (line.length <= 2 && !/^\d+$/.test(line)) continue;
|
|
439
|
-
|
|
440
|
-
// Stop at follow-up suggestions (they mark the end of the response)
|
|
441
|
-
if (/^(Explain|Tell me|Learn more|Show me|Multiplication)/i.test(line)) break;
|
|
442
|
-
|
|
542
|
+
|
|
443
543
|
contentLines.push(line);
|
|
444
544
|
}
|
|
445
|
-
|
|
545
|
+
|
|
546
|
+
const responseLines = trimTrailingSuggestionLines(contentLines);
|
|
547
|
+
|
|
446
548
|
// If we found content after the question, return the response
|
|
447
|
-
if (
|
|
448
|
-
return
|
|
549
|
+
if (responseLines.length > 0) {
|
|
550
|
+
return responseLines.join('\n').trim();
|
|
449
551
|
}
|
|
450
|
-
|
|
552
|
+
|
|
451
553
|
// Fallback: look for the LAST standalone numeric answer
|
|
452
554
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
453
555
|
const line = lines[i];
|
|
@@ -455,15 +557,15 @@ function extractGrokResponse(bodyText, userPrompt = '') {
|
|
|
455
557
|
return line;
|
|
456
558
|
}
|
|
457
559
|
}
|
|
458
|
-
|
|
560
|
+
|
|
459
561
|
return null;
|
|
460
562
|
}
|
|
461
563
|
|
|
462
564
|
async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
463
565
|
// Grok can take a long time:
|
|
464
|
-
// - Thinking models
|
|
566
|
+
// - Thinking models: 40-60+ seconds to think, then streams
|
|
465
567
|
// - Fast/Auto models: No thinking phase, just streams directly
|
|
466
|
-
|
|
568
|
+
|
|
467
569
|
const deadline = Date.now() + timeoutMs;
|
|
468
570
|
let previousText = '';
|
|
469
571
|
let previousLength = 0;
|
|
@@ -472,30 +574,30 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
472
574
|
let thinkingComplete = false;
|
|
473
575
|
let lastResponseText = '';
|
|
474
576
|
let responseStableCycles = 0;
|
|
475
|
-
|
|
577
|
+
|
|
476
578
|
while (Date.now() < deadline) {
|
|
477
579
|
// Get page state with multiple completion indicators
|
|
478
580
|
const snapshot = await evaluate(cdp, `(function() {
|
|
479
581
|
const bodyText = document.body.innerText || '';
|
|
480
|
-
|
|
582
|
+
|
|
481
583
|
// Check for stop/cancel button (indicates still generating)
|
|
482
584
|
const hasStopBtn = !!document.querySelector('button[aria-label*="Stop"], button[aria-label*="stop"], button[aria-label*="Cancel"]');
|
|
483
|
-
|
|
585
|
+
|
|
484
586
|
// Check for "Thought for Xs" which indicates thinking model completed thinking
|
|
485
587
|
const thinkMatch = bodyText.match(/Thought for (\\d+)s/i);
|
|
486
588
|
const thinkingDone = !!thinkMatch;
|
|
487
589
|
const thinkingSecs = thinkMatch ? parseInt(thinkMatch[1], 10) : null;
|
|
488
|
-
|
|
590
|
+
|
|
489
591
|
// Check if actively showing "thinking..." or similar loading state
|
|
490
|
-
const isThinking = /\\bthinking\\.\\.\\./i.test(bodyText) ||
|
|
592
|
+
const isThinking = /\\bthinking\\.\\.\\./i.test(bodyText) ||
|
|
491
593
|
/\\bSearching\\.\\.\\./i.test(bodyText) ||
|
|
492
594
|
bodyText.includes('Grok is thinking') ||
|
|
493
595
|
bodyText.includes('is thinking...');
|
|
494
|
-
|
|
596
|
+
|
|
495
597
|
// Try to find the actual Grok response in the DOM
|
|
496
598
|
// Look for the main content area - Grok responses appear in the conversation area
|
|
497
599
|
let responseText = '';
|
|
498
|
-
|
|
600
|
+
|
|
499
601
|
// Strategy 1: Look for article elements or main content containers
|
|
500
602
|
const articles = document.querySelectorAll('article');
|
|
501
603
|
if (articles.length > 0) {
|
|
@@ -503,7 +605,7 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
503
605
|
const lastArticle = articles[articles.length - 1];
|
|
504
606
|
responseText = lastArticle.innerText || '';
|
|
505
607
|
}
|
|
506
|
-
|
|
608
|
+
|
|
507
609
|
// Strategy 2: If no articles, look for the conversation container
|
|
508
610
|
if (!responseText) {
|
|
509
611
|
const convArea = document.querySelector('[data-testid="conversation"], [role="main"] > div > div');
|
|
@@ -511,14 +613,14 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
511
613
|
responseText = convArea.innerText || '';
|
|
512
614
|
}
|
|
513
615
|
}
|
|
514
|
-
|
|
616
|
+
|
|
515
617
|
// Strategy 3: Fallback to looking for text after common Grok UI patterns
|
|
516
618
|
if (!responseText || responseText.length < 10) {
|
|
517
619
|
// Find content between user question and follow-up suggestions
|
|
518
620
|
const mainArea = document.querySelector('main') || document.body;
|
|
519
621
|
responseText = mainArea.innerText || bodyText;
|
|
520
622
|
}
|
|
521
|
-
|
|
623
|
+
|
|
522
624
|
return {
|
|
523
625
|
bodyText: bodyText,
|
|
524
626
|
responseText: responseText,
|
|
@@ -530,22 +632,22 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
530
632
|
url: location.href
|
|
531
633
|
};
|
|
532
634
|
})()`);
|
|
533
|
-
|
|
635
|
+
|
|
534
636
|
if (!snapshot || !snapshot.bodyText) {
|
|
535
637
|
await delay(300);
|
|
536
638
|
continue;
|
|
537
639
|
}
|
|
538
|
-
|
|
640
|
+
|
|
539
641
|
const bodyText = snapshot.bodyText;
|
|
540
642
|
const bodyLength = snapshot.bodyLength;
|
|
541
|
-
|
|
643
|
+
|
|
542
644
|
// Track thinking time (for thinking models)
|
|
543
645
|
if (snapshot.thinkingSecs) {
|
|
544
646
|
if (!thinkingTime || snapshot.thinkingSecs > thinkingTime) {
|
|
545
647
|
thinkingTime = snapshot.thinkingSecs;
|
|
546
648
|
}
|
|
547
649
|
}
|
|
548
|
-
|
|
650
|
+
|
|
549
651
|
// Detect when thinking completes (thinking models only)
|
|
550
652
|
// "Thought for Xs" is a DEFINITIVE signal that thinking AND response generation is done
|
|
551
653
|
if (snapshot.thinkingDone && !thinkingComplete) {
|
|
@@ -553,7 +655,7 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
553
655
|
// Give a brief moment for final render, then we're done
|
|
554
656
|
await delay(500);
|
|
555
657
|
}
|
|
556
|
-
|
|
658
|
+
|
|
557
659
|
// Extract the actual response text - try DOM-extracted first, fall back to body parsing
|
|
558
660
|
let currentResponseText = '';
|
|
559
661
|
if (snapshot.responseText && snapshot.responseText.length > 10) {
|
|
@@ -562,7 +664,7 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
562
664
|
if (!currentResponseText || currentResponseText.length < 5) {
|
|
563
665
|
currentResponseText = extractGrokResponse(bodyText, userPrompt) || '';
|
|
564
666
|
}
|
|
565
|
-
|
|
667
|
+
|
|
566
668
|
// Track RESPONSE text stability (more reliable than body text)
|
|
567
669
|
if (currentResponseText !== lastResponseText) {
|
|
568
670
|
lastResponseText = currentResponseText;
|
|
@@ -571,34 +673,34 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
571
673
|
} else if (currentResponseText.length > 0) {
|
|
572
674
|
responseStableCycles++;
|
|
573
675
|
}
|
|
574
|
-
|
|
676
|
+
|
|
575
677
|
// Track body text for timeout fallback
|
|
576
678
|
if (bodyLength !== previousLength) {
|
|
577
679
|
previousText = bodyText;
|
|
578
680
|
previousLength = bodyLength;
|
|
579
681
|
}
|
|
580
|
-
|
|
682
|
+
|
|
581
683
|
const stableMs = Date.now() - lastChangeAt;
|
|
582
684
|
const noStopButton = !snapshot.hasStopBtn;
|
|
583
|
-
|
|
685
|
+
|
|
584
686
|
// Response is stable if the extracted response text hasn't changed
|
|
585
687
|
// Use shorter thresholds since we're checking actual content, not noisy body text
|
|
586
688
|
// 4 cycles (1.2s) + 1.5s minimum is enough for response stability
|
|
587
689
|
const responseIsStable = responseStableCycles >= 4 && stableMs >= 1500 && currentResponseText.length > 10;
|
|
588
|
-
|
|
690
|
+
|
|
589
691
|
// "Thought for Xs" is the strongest completion signal - response is definitely done
|
|
590
692
|
const thinkingModelDone = snapshot.thinkingDone && noStopButton;
|
|
591
|
-
|
|
693
|
+
|
|
592
694
|
// SIMPLE CHECK: If we have response content, no stop button, and stable for 3+ cycles
|
|
593
695
|
const hasResponseNoStop = currentResponseText.length > 5 && noStopButton && responseStableCycles >= 3;
|
|
594
|
-
|
|
696
|
+
|
|
595
697
|
// Response is complete when:
|
|
596
698
|
// 1. Has meaningful response content (> 5 chars)
|
|
597
699
|
// 2. No stop button
|
|
598
700
|
// 3. Either: thinking done, response stable for 3+ cycles, OR stable for 4+ cycles with 1.5s
|
|
599
701
|
const isDone = currentResponseText.length > 5 && noStopButton &&
|
|
600
702
|
(thinkingModelDone || hasResponseNoStop || responseIsStable);
|
|
601
|
-
|
|
703
|
+
|
|
602
704
|
if (isDone) {
|
|
603
705
|
return {
|
|
604
706
|
text: currentResponseText,
|
|
@@ -606,10 +708,10 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
606
708
|
url: snapshot.url,
|
|
607
709
|
};
|
|
608
710
|
}
|
|
609
|
-
|
|
711
|
+
|
|
610
712
|
await delay(300);
|
|
611
713
|
}
|
|
612
|
-
|
|
714
|
+
|
|
613
715
|
// Timeout - return whatever we have (partial response is better than nothing)
|
|
614
716
|
const finalText = extractGrokResponse(previousText, userPrompt);
|
|
615
717
|
if (finalText && finalText.length > 10) {
|
|
@@ -619,7 +721,7 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
619
721
|
partial: true,
|
|
620
722
|
};
|
|
621
723
|
}
|
|
622
|
-
|
|
724
|
+
|
|
623
725
|
throw new Error("Response timeout - Grok did not complete in time");
|
|
624
726
|
}
|
|
625
727
|
|
|
@@ -640,34 +742,34 @@ async function query(options) {
|
|
|
640
742
|
cdpCommand,
|
|
641
743
|
log = () => {},
|
|
642
744
|
} = options;
|
|
643
|
-
|
|
745
|
+
|
|
644
746
|
const startTime = Date.now();
|
|
645
747
|
log("Starting Grok query");
|
|
646
|
-
|
|
748
|
+
|
|
647
749
|
// Check cookies for X.com authentication
|
|
648
750
|
const { cookies } = await getCookies();
|
|
649
751
|
if (!hasRequiredCookies(cookies)) {
|
|
650
752
|
throw new Error("X.com login required - log in to x.com in Chrome first");
|
|
651
753
|
}
|
|
652
754
|
log(`Got ${cookies.length} cookies`);
|
|
653
|
-
|
|
755
|
+
|
|
654
756
|
// Create tab
|
|
655
757
|
const tabInfo = await createTab();
|
|
656
758
|
const { tabId } = tabInfo || {};
|
|
657
|
-
|
|
759
|
+
|
|
658
760
|
if (!tabId) {
|
|
659
761
|
throw new Error(`Failed to create Grok tab: ${JSON.stringify(tabInfo)}`);
|
|
660
762
|
}
|
|
661
763
|
log(`Created tab ${tabId}`);
|
|
662
|
-
|
|
764
|
+
|
|
663
765
|
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
664
766
|
const inputCdp = (method, params) => cdpCommand(tabId, method, params);
|
|
665
|
-
|
|
767
|
+
|
|
666
768
|
try {
|
|
667
769
|
// Wait for page load
|
|
668
770
|
await waitForPageLoad(cdp);
|
|
669
771
|
log("Page loaded");
|
|
670
|
-
|
|
772
|
+
|
|
671
773
|
// Check login status
|
|
672
774
|
const loginStatus = await checkLoginStatus(cdp);
|
|
673
775
|
if (!loginStatus.loggedIn) {
|
|
@@ -677,14 +779,14 @@ async function query(options) {
|
|
|
677
779
|
log("Warning: X Premium may be required for some Grok features");
|
|
678
780
|
}
|
|
679
781
|
log(`Login: yes${loginStatus.hasPremium ? ' (Premium)' : ''}`);
|
|
680
|
-
|
|
782
|
+
|
|
681
783
|
// Track warnings for agent feedback
|
|
682
784
|
const warnings = [];
|
|
683
|
-
|
|
785
|
+
|
|
684
786
|
// Wait for Grok UI
|
|
685
787
|
await waitForGrokReady(cdp);
|
|
686
788
|
log("Grok ready");
|
|
687
|
-
|
|
789
|
+
|
|
688
790
|
// Select model (use default if not specified)
|
|
689
791
|
const targetModel = model || DEFAULT_MODEL;
|
|
690
792
|
let selectedModel = targetModel;
|
|
@@ -703,7 +805,7 @@ async function query(options) {
|
|
|
703
805
|
warnings.push(`Model selection failed: ${e.message}. Run 'surf grok --validate' to check available models.`);
|
|
704
806
|
log(`Model selection failed: ${e.message}`);
|
|
705
807
|
}
|
|
706
|
-
|
|
808
|
+
|
|
707
809
|
// Enable DeepSearch if requested
|
|
708
810
|
let deepSearchEnabled = false;
|
|
709
811
|
if (deepSearch) {
|
|
@@ -720,20 +822,20 @@ async function query(options) {
|
|
|
720
822
|
log(`DeepSearch toggle failed: ${e.message}`);
|
|
721
823
|
}
|
|
722
824
|
}
|
|
723
|
-
|
|
825
|
+
|
|
724
826
|
// Type prompt
|
|
725
827
|
await typePrompt(cdp, inputCdp, prompt);
|
|
726
828
|
log("Prompt typed");
|
|
727
|
-
|
|
829
|
+
|
|
728
830
|
// Submit
|
|
729
831
|
await submitPrompt(cdp, inputCdp);
|
|
730
832
|
log("Submitted, waiting for response...");
|
|
731
|
-
|
|
833
|
+
|
|
732
834
|
// Wait for response
|
|
733
835
|
const response = await waitForResponse(cdp, timeout, prompt);
|
|
734
836
|
const thinkingInfo = response.thinkingTime ? ` (thought for ${response.thinkingTime}s)` : '';
|
|
735
837
|
log(`Response: ${response.text.length} chars${thinkingInfo}${response.partial ? ' (partial)' : ''}`);
|
|
736
|
-
|
|
838
|
+
|
|
737
839
|
return {
|
|
738
840
|
response: response.text,
|
|
739
841
|
model: selectedModel,
|
|
@@ -764,10 +866,10 @@ async function validate(options) {
|
|
|
764
866
|
cdpEvaluate,
|
|
765
867
|
log = () => {},
|
|
766
868
|
} = options;
|
|
767
|
-
|
|
869
|
+
|
|
768
870
|
const startTime = Date.now();
|
|
769
871
|
log("Starting Grok validation");
|
|
770
|
-
|
|
872
|
+
|
|
771
873
|
const result = {
|
|
772
874
|
authenticated: false,
|
|
773
875
|
premium: false,
|
|
@@ -779,7 +881,7 @@ async function validate(options) {
|
|
|
779
881
|
errors: [],
|
|
780
882
|
configPath: getConfigPath() || "~/surf.json",
|
|
781
883
|
};
|
|
782
|
-
|
|
884
|
+
|
|
783
885
|
// Check cookies
|
|
784
886
|
try {
|
|
785
887
|
const { cookies } = await getCookies();
|
|
@@ -793,7 +895,7 @@ async function validate(options) {
|
|
|
793
895
|
result.errors.push(`Cookie check failed: ${e.message}`);
|
|
794
896
|
return { ...result, tookMs: Date.now() - startTime };
|
|
795
897
|
}
|
|
796
|
-
|
|
898
|
+
|
|
797
899
|
// Create tab
|
|
798
900
|
let tabId;
|
|
799
901
|
try {
|
|
@@ -808,29 +910,29 @@ async function validate(options) {
|
|
|
808
910
|
result.errors.push(`Tab creation failed: ${e.message}`);
|
|
809
911
|
return { ...result, tookMs: Date.now() - startTime };
|
|
810
912
|
}
|
|
811
|
-
|
|
913
|
+
|
|
812
914
|
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
813
|
-
|
|
915
|
+
|
|
814
916
|
try {
|
|
815
917
|
// Wait for page load
|
|
816
918
|
await waitForPageLoad(cdp);
|
|
817
919
|
log("Page loaded");
|
|
818
|
-
|
|
920
|
+
|
|
819
921
|
// Check login status
|
|
820
922
|
const loginStatus = await checkLoginStatus(cdp);
|
|
821
923
|
result.authenticated = loginStatus.loggedIn;
|
|
822
924
|
result.premium = loginStatus.hasPremium;
|
|
823
|
-
|
|
925
|
+
|
|
824
926
|
if (!loginStatus.loggedIn) {
|
|
825
927
|
result.errors.push("Page shows logged out state");
|
|
826
928
|
return { ...result, tookMs: Date.now() - startTime };
|
|
827
929
|
}
|
|
828
930
|
log(`Login: yes${result.premium ? ' (Premium)' : ''}`);
|
|
829
|
-
|
|
931
|
+
|
|
830
932
|
// Wait for Grok UI
|
|
831
933
|
await waitForGrokReady(cdp);
|
|
832
934
|
log("Grok ready");
|
|
833
|
-
|
|
935
|
+
|
|
834
936
|
// Check for input field
|
|
835
937
|
const inputCheck = await evaluate(cdp, `(() => {
|
|
836
938
|
const input = document.querySelector('textarea, [contenteditable="true"][role="textbox"], [data-testid="grokComposerInput"]');
|
|
@@ -838,20 +940,15 @@ async function validate(options) {
|
|
|
838
940
|
})()`);
|
|
839
941
|
result.inputFound = inputCheck?.found || false;
|
|
840
942
|
log(`Input field: ${result.inputFound ? 'found' : 'NOT FOUND'}`);
|
|
841
|
-
|
|
943
|
+
|
|
842
944
|
// Check for send button
|
|
843
945
|
const sendCheck = await evaluate(cdp, `(() => {
|
|
844
|
-
|
|
845
|
-
const sendBtn = buttons.find(b => {
|
|
846
|
-
const label = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
847
|
-
const testId = b.getAttribute('data-testid') || '';
|
|
848
|
-
return label.includes('send') || testId.includes('send') || testId.includes('submit');
|
|
849
|
-
});
|
|
946
|
+
${grokSendButtonFinderScript()}
|
|
850
947
|
return { found: !!sendBtn };
|
|
851
948
|
})()`);
|
|
852
949
|
result.sendButtonFound = sendCheck?.found || false;
|
|
853
950
|
log(`Send button: ${result.sendButtonFound ? 'found' : 'NOT FOUND'}`);
|
|
854
|
-
|
|
951
|
+
|
|
855
952
|
// Click model selector and scrape models
|
|
856
953
|
const modelButtonClicked = await evaluate(cdp, `(() => {
|
|
857
954
|
${buildClickDispatcher()}
|
|
@@ -868,10 +965,10 @@ async function validate(options) {
|
|
|
868
965
|
dispatchClickSequence(modelBtn);
|
|
869
966
|
return { success: true };
|
|
870
967
|
})()`);
|
|
871
|
-
|
|
968
|
+
|
|
872
969
|
if (modelButtonClicked?.success) {
|
|
873
970
|
await delay(500);
|
|
874
|
-
|
|
971
|
+
|
|
875
972
|
// Scrape model options
|
|
876
973
|
const modelScrape = await evaluate(cdp, `(() => {
|
|
877
974
|
const items = document.querySelectorAll('[role="menuitem"], [role="menuitemradio"], [role="option"]');
|
|
@@ -887,24 +984,24 @@ async function validate(options) {
|
|
|
887
984
|
}
|
|
888
985
|
return { models };
|
|
889
986
|
})()`);
|
|
890
|
-
|
|
987
|
+
|
|
891
988
|
result.models = modelScrape?.models || [];
|
|
892
989
|
log(`Found models: ${result.models.join(', ')}`);
|
|
893
|
-
|
|
990
|
+
|
|
894
991
|
// Close the menu
|
|
895
992
|
await evaluate(cdp, `document.body.click()`);
|
|
896
993
|
} else {
|
|
897
994
|
log("Could not open model selector");
|
|
898
995
|
result.errors.push("Model selector button not found");
|
|
899
996
|
}
|
|
900
|
-
|
|
997
|
+
|
|
901
998
|
// Check for model mismatch
|
|
902
|
-
const expectedNames = Object.values(getGrokModels()).map(m => m.name
|
|
903
|
-
const foundNames = result.models.map(m => m
|
|
904
|
-
|
|
999
|
+
const expectedNames = Object.values(getGrokModels()).map(m => normalizeGrokModelLabel(m.name));
|
|
1000
|
+
const foundNames = result.models.map(m => normalizeGrokModelLabel(m));
|
|
1001
|
+
|
|
905
1002
|
const missing = expectedNames.filter(e => !foundNames.some(f => f.includes(e) || e.includes(f)));
|
|
906
1003
|
const extra = foundNames.filter(f => !expectedNames.some(e => f.includes(e) || e.includes(f)));
|
|
907
|
-
|
|
1004
|
+
|
|
908
1005
|
if (missing.length > 0 || extra.length > 0) {
|
|
909
1006
|
result.modelMismatch = true;
|
|
910
1007
|
if (missing.length > 0) {
|
|
@@ -914,13 +1011,13 @@ async function validate(options) {
|
|
|
914
1011
|
result.errors.push(`Unexpected models found: ${extra.join(', ')}`);
|
|
915
1012
|
}
|
|
916
1013
|
}
|
|
917
|
-
|
|
1014
|
+
|
|
918
1015
|
} catch (e) {
|
|
919
1016
|
result.errors.push(`Validation error: ${e.message}`);
|
|
920
1017
|
} finally {
|
|
921
1018
|
await closeTab(tabId).catch(() => {});
|
|
922
1019
|
}
|
|
923
|
-
|
|
1020
|
+
|
|
924
1021
|
result.tookMs = Date.now() - startTime;
|
|
925
1022
|
return result;
|
|
926
1023
|
}
|
|
@@ -930,14 +1027,14 @@ function saveModels(models) {
|
|
|
930
1027
|
const fs = require("fs");
|
|
931
1028
|
const path = require("path");
|
|
932
1029
|
const os = require("os");
|
|
933
|
-
|
|
1030
|
+
|
|
934
1031
|
try {
|
|
935
1032
|
// Use existing config path or default to ~/surf.json
|
|
936
1033
|
let configPath = getConfigPath();
|
|
937
1034
|
if (!configPath) {
|
|
938
1035
|
configPath = path.join(os.homedir(), "surf.json");
|
|
939
1036
|
}
|
|
940
|
-
|
|
1037
|
+
|
|
941
1038
|
// Load existing config or start fresh
|
|
942
1039
|
let config = {};
|
|
943
1040
|
if (fs.existsSync(configPath)) {
|
|
@@ -947,11 +1044,11 @@ function saveModels(models) {
|
|
|
947
1044
|
// Start fresh if parse fails
|
|
948
1045
|
}
|
|
949
1046
|
}
|
|
950
|
-
|
|
1047
|
+
|
|
951
1048
|
// Update grok.models
|
|
952
1049
|
config.grok = config.grok || {};
|
|
953
1050
|
config.grok.models = models;
|
|
954
|
-
|
|
1051
|
+
|
|
955
1052
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
956
1053
|
clearCache(); // Clear config cache so subsequent reads see new values
|
|
957
1054
|
return { success: true, path: configPath };
|
|
@@ -960,13 +1057,16 @@ function saveModels(models) {
|
|
|
960
1057
|
}
|
|
961
1058
|
}
|
|
962
1059
|
|
|
963
|
-
module.exports = {
|
|
1060
|
+
module.exports = {
|
|
964
1061
|
query,
|
|
965
1062
|
validate,
|
|
966
|
-
hasRequiredCookies,
|
|
1063
|
+
hasRequiredCookies,
|
|
967
1064
|
getGrokModels,
|
|
968
1065
|
saveModels,
|
|
969
1066
|
extractGrokResponse,
|
|
1067
|
+
normalizeGrokModelLabel,
|
|
1068
|
+
getGrokModelMatchLabels,
|
|
1069
|
+
grokModelLabelsMatch,
|
|
970
1070
|
GROK_URL,
|
|
971
1071
|
GROK_MODELS,
|
|
972
1072
|
DEFAULT_GROK_MODELS,
|