surf-cli 2.8.0 → 2.9.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 +98 -4
- package/dist/content/index.js +116 -0
- package/dist/content/index.js.map +1 -0
- package/dist/manifest.json +2 -11
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +261 -61
- package/dist/service-worker/index.js.map +1 -1
- package/native/abort.cjs +65 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +2 -2
- package/native/chatgpt-client.cjs +47 -31
- package/native/cli.cjs +300 -204
- package/native/client-transport.cjs +168 -0
- package/native/do-executor.cjs +25 -44
- package/native/doctor.cjs +55 -5
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +156 -71
- package/native/grok-client.cjs +98 -89
- package/native/host-helpers.cjs +37 -12
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +800 -620
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -65
- package/native/network-export.cjs +113 -0
- package/native/perplexity-client.cjs +46 -17
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +1 -1
- package/package.json +8 -6
- package/scripts/install-native-host.cjs +36 -5
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +31 -4
- package/dist/content/accessibility-tree.js +0 -11
- package/dist/content/accessibility-tree.js.map +0 -1
- package/dist/content/visual-indicator.js +0 -111
- package/dist/content/visual-indicator.js.map +0 -1
package/native/grok-client.cjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { loadConfig, getConfigPath, clearCache } = require("./config.cjs");
|
|
9
|
+
const { abortableDelay, raceAbort, throwIfAborted } = require("./abort.cjs");
|
|
9
10
|
|
|
10
11
|
const GROK_URL = "https://x.com/i/grok";
|
|
11
12
|
const DEFAULT_MODEL = "fast";
|
|
@@ -38,8 +39,8 @@ const GROK_MODELS = DEFAULT_GROK_MODELS;
|
|
|
38
39
|
// Helpers
|
|
39
40
|
// ============================================================================
|
|
40
41
|
|
|
41
|
-
function delay(ms) {
|
|
42
|
-
return
|
|
42
|
+
function delay(ms, signal) {
|
|
43
|
+
return abortableDelay(ms, signal);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function buildClickDispatcher() {
|
|
@@ -132,8 +133,10 @@ function hasRequiredCookies(cookies) {
|
|
|
132
133
|
return Boolean(authToken);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
async function evaluate(cdp, expression) {
|
|
136
|
+
async function evaluate(cdp, expression, signal) {
|
|
137
|
+
throwIfAborted(signal);
|
|
136
138
|
const result = await cdp(expression);
|
|
139
|
+
throwIfAborted(signal);
|
|
137
140
|
if (result.exceptionDetails) {
|
|
138
141
|
const desc = result.exceptionDetails.exception?.description ||
|
|
139
142
|
result.exceptionDetails.text ||
|
|
@@ -450,53 +453,17 @@ async function submitPrompt(cdp, inputCdp) {
|
|
|
450
453
|
// Response Handling
|
|
451
454
|
// ============================================================================
|
|
452
455
|
|
|
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
|
-
|
|
496
456
|
// Extract Grok's response from the full page body text
|
|
497
|
-
function extractGrokResponse(bodyText, userPrompt = '') {
|
|
457
|
+
function extractGrokResponse(bodyText, userPrompt = '', chipTexts = []) {
|
|
498
458
|
if (!bodyText) return null;
|
|
499
459
|
|
|
460
|
+
// Suggestion chips are captured from the DOM as button texts; count occurrences.
|
|
461
|
+
const chipCounts = new Map();
|
|
462
|
+
for (const t of chipTexts || []) {
|
|
463
|
+
const key = String(t).trim();
|
|
464
|
+
if (key) chipCounts.set(key, (chipCounts.get(key) || 0) + 1);
|
|
465
|
+
}
|
|
466
|
+
|
|
500
467
|
// Split into lines and filter out navigation/UI elements
|
|
501
468
|
const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l);
|
|
502
469
|
|
|
@@ -543,7 +510,15 @@ function extractGrokResponse(bodyText, userPrompt = '') {
|
|
|
543
510
|
contentLines.push(line);
|
|
544
511
|
}
|
|
545
512
|
|
|
546
|
-
|
|
513
|
+
// Chips follow the answer; strip trailing chip lines but never the first line.
|
|
514
|
+
let contentEnd = contentLines.length;
|
|
515
|
+
while (contentEnd > 1) {
|
|
516
|
+
const remaining = chipCounts.get(contentLines[contentEnd - 1]) || 0;
|
|
517
|
+
if (remaining <= 0) break;
|
|
518
|
+
chipCounts.set(contentLines[contentEnd - 1], remaining - 1);
|
|
519
|
+
contentEnd--;
|
|
520
|
+
}
|
|
521
|
+
const responseLines = contentLines.slice(0, contentEnd);
|
|
547
522
|
|
|
548
523
|
// If we found content after the question, return the response
|
|
549
524
|
if (responseLines.length > 0) {
|
|
@@ -561,7 +536,8 @@ function extractGrokResponse(bodyText, userPrompt = '') {
|
|
|
561
536
|
return null;
|
|
562
537
|
}
|
|
563
538
|
|
|
564
|
-
async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
539
|
+
async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '', signal) {
|
|
540
|
+
throwIfAborted(signal);
|
|
565
541
|
// Grok can take a long time:
|
|
566
542
|
// - Thinking models: 40-60+ seconds to think, then streams
|
|
567
543
|
// - Fast/Auto models: No thinking phase, just streams directly
|
|
@@ -569,6 +545,7 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
569
545
|
const deadline = Date.now() + timeoutMs;
|
|
570
546
|
let previousText = '';
|
|
571
547
|
let previousLength = 0;
|
|
548
|
+
let lastChipTexts = [];
|
|
572
549
|
let lastChangeAt = Date.now();
|
|
573
550
|
let thinkingTime = null;
|
|
574
551
|
let thinkingComplete = false;
|
|
@@ -583,44 +560,55 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
583
560
|
// Check for stop/cancel button (indicates still generating)
|
|
584
561
|
const hasStopBtn = !!document.querySelector('button[aria-label*="Stop"], button[aria-label*="stop"], button[aria-label*="Cancel"]');
|
|
585
562
|
|
|
586
|
-
// Check for "Thought for Xs" which indicates thinking model completed thinking
|
|
587
|
-
const thinkMatch = bodyText.match(/Thought for (\\d+)s/i);
|
|
588
|
-
const thinkingDone = !!thinkMatch;
|
|
589
|
-
const thinkingSecs = thinkMatch ? parseInt(thinkMatch[1], 10) : null;
|
|
590
|
-
|
|
591
|
-
// Check if actively showing "thinking..." or similar loading state
|
|
592
|
-
const isThinking = /\\bthinking\\.\\.\\./i.test(bodyText) ||
|
|
593
|
-
/\\bSearching\\.\\.\\./i.test(bodyText) ||
|
|
594
|
-
bodyText.includes('Grok is thinking') ||
|
|
595
|
-
bodyText.includes('is thinking...');
|
|
596
|
-
|
|
597
563
|
// Try to find the actual Grok response in the DOM
|
|
598
564
|
// Look for the main content area - Grok responses appear in the conversation area
|
|
599
565
|
let responseText = '';
|
|
566
|
+
let responseRoot = null;
|
|
567
|
+
let preciseRoot = false;
|
|
600
568
|
|
|
601
569
|
// Strategy 1: Look for article elements or main content containers
|
|
602
570
|
const articles = document.querySelectorAll('article');
|
|
603
571
|
if (articles.length > 0) {
|
|
604
572
|
// Get the last article which should be the response
|
|
605
|
-
|
|
606
|
-
responseText =
|
|
573
|
+
responseRoot = articles[articles.length - 1];
|
|
574
|
+
responseText = responseRoot.innerText || '';
|
|
575
|
+
preciseRoot = true;
|
|
607
576
|
}
|
|
608
577
|
|
|
609
578
|
// Strategy 2: If no articles, look for the conversation container
|
|
610
579
|
if (!responseText) {
|
|
611
580
|
const convArea = document.querySelector('[data-testid="conversation"], [role="main"] > div > div');
|
|
612
581
|
if (convArea) {
|
|
582
|
+
responseRoot = convArea;
|
|
613
583
|
responseText = convArea.innerText || '';
|
|
584
|
+
preciseRoot = true;
|
|
614
585
|
}
|
|
615
586
|
}
|
|
616
587
|
|
|
617
588
|
// Strategy 3: Fallback to looking for text after common Grok UI patterns
|
|
618
589
|
if (!responseText || responseText.length < 10) {
|
|
619
590
|
// Find content between user question and follow-up suggestions
|
|
620
|
-
|
|
621
|
-
responseText =
|
|
591
|
+
responseRoot = document.querySelector('main') || document.body;
|
|
592
|
+
responseText = responseRoot.innerText || bodyText;
|
|
593
|
+
preciseRoot = false;
|
|
622
594
|
}
|
|
623
595
|
|
|
596
|
+
// Completion state must come from the current response container. Page-wide markers can belong to an earlier turn and would make a new short response finish prematurely.
|
|
597
|
+
const currentStateText = preciseRoot && responseRoot ? (responseRoot.innerText || '') : '';
|
|
598
|
+
const thinkMatch = currentStateText.match(/Thought for (\\d+)s/i);
|
|
599
|
+
const thinkingDone = !!thinkMatch;
|
|
600
|
+
const thinkingSecs = thinkMatch ? parseInt(thinkMatch[1], 10) : null;
|
|
601
|
+
const isThinking = /\\bthinking\\.\\.\\./i.test(currentStateText) ||
|
|
602
|
+
/\\bSearching\\.\\.\\./i.test(currentStateText) ||
|
|
603
|
+
currentStateText.includes('Grok is thinking') ||
|
|
604
|
+
currentStateText.includes('is thinking...');
|
|
605
|
+
|
|
606
|
+
// Suggestion chips are the no-testid/no-aria-label buttons inside the response container. Only collect them from a precise container: a broad main/body fallback holds unrelated buttons that could erase a matching answer line.
|
|
607
|
+
const chipTexts = (preciseRoot && responseRoot ? Array.from(responseRoot.querySelectorAll('button')) : [])
|
|
608
|
+
.filter(function(b){ return !b.getAttribute('data-testid') && !b.getAttribute('aria-label'); })
|
|
609
|
+
.map(function(b){ return (b.innerText || '').trim(); })
|
|
610
|
+
.filter(function(t){ return t && !t.includes('\\n') && t.length <= 120; });
|
|
611
|
+
|
|
624
612
|
return {
|
|
625
613
|
bodyText: bodyText,
|
|
626
614
|
responseText: responseText,
|
|
@@ -629,12 +617,13 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
629
617
|
thinkingDone: thinkingDone,
|
|
630
618
|
thinkingSecs: thinkingSecs,
|
|
631
619
|
isThinking: isThinking,
|
|
620
|
+
chipTexts: chipTexts,
|
|
632
621
|
url: location.href
|
|
633
622
|
};
|
|
634
623
|
})()`);
|
|
635
624
|
|
|
636
625
|
if (!snapshot || !snapshot.bodyText) {
|
|
637
|
-
await delay(300);
|
|
626
|
+
await delay(300, signal);
|
|
638
627
|
continue;
|
|
639
628
|
}
|
|
640
629
|
|
|
@@ -653,16 +642,18 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
653
642
|
if (snapshot.thinkingDone && !thinkingComplete) {
|
|
654
643
|
thinkingComplete = true;
|
|
655
644
|
// Give a brief moment for final render, then we're done
|
|
656
|
-
await delay(500);
|
|
645
|
+
await delay(500, signal);
|
|
657
646
|
}
|
|
658
647
|
|
|
659
648
|
// Extract the actual response text - try DOM-extracted first, fall back to body parsing
|
|
649
|
+
const chipTexts = snapshot.chipTexts || [];
|
|
650
|
+
lastChipTexts = chipTexts;
|
|
660
651
|
let currentResponseText = '';
|
|
661
652
|
if (snapshot.responseText && snapshot.responseText.length > 10) {
|
|
662
|
-
currentResponseText = extractGrokResponse(snapshot.responseText, userPrompt) || '';
|
|
653
|
+
currentResponseText = extractGrokResponse(snapshot.responseText, userPrompt, chipTexts) || '';
|
|
663
654
|
}
|
|
664
655
|
if (!currentResponseText || currentResponseText.length < 5) {
|
|
665
|
-
currentResponseText = extractGrokResponse(bodyText, userPrompt) || '';
|
|
656
|
+
currentResponseText = extractGrokResponse(bodyText, userPrompt, chipTexts) || '';
|
|
666
657
|
}
|
|
667
658
|
|
|
668
659
|
// Track RESPONSE text stability (more reliable than body text)
|
|
@@ -681,24 +672,24 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
681
672
|
}
|
|
682
673
|
|
|
683
674
|
const stableMs = Date.now() - lastChangeAt;
|
|
684
|
-
const noStopButton = !snapshot.hasStopBtn;
|
|
675
|
+
const noStopButton = !snapshot.hasStopBtn && !snapshot.isThinking;
|
|
685
676
|
|
|
686
677
|
// Response is stable if the extracted response text hasn't changed
|
|
687
678
|
// Use shorter thresholds since we're checking actual content, not noisy body text
|
|
688
679
|
// 4 cycles (1.2s) + 1.5s minimum is enough for response stability
|
|
689
|
-
const responseIsStable = responseStableCycles >= 4 && stableMs >= 1500 && currentResponseText.length >
|
|
680
|
+
const responseIsStable = responseStableCycles >= 4 && stableMs >= 1500 && currentResponseText.trim().length > 0;
|
|
690
681
|
|
|
691
682
|
// "Thought for Xs" is the strongest completion signal - response is definitely done
|
|
692
683
|
const thinkingModelDone = snapshot.thinkingDone && noStopButton;
|
|
693
684
|
|
|
694
685
|
// SIMPLE CHECK: If we have response content, no stop button, and stable for 3+ cycles
|
|
695
|
-
const hasResponseNoStop = currentResponseText.length >
|
|
686
|
+
const hasResponseNoStop = currentResponseText.trim().length > 0 && noStopButton && responseStableCycles >= 3;
|
|
696
687
|
|
|
697
688
|
// Response is complete when:
|
|
698
|
-
// 1. Has
|
|
689
|
+
// 1. Has any non-empty extracted text
|
|
699
690
|
// 2. No stop button
|
|
700
691
|
// 3. Either: thinking done, response stable for 3+ cycles, OR stable for 4+ cycles with 1.5s
|
|
701
|
-
const isDone = currentResponseText.length >
|
|
692
|
+
const isDone = currentResponseText.trim().length > 0 && noStopButton &&
|
|
702
693
|
(thinkingModelDone || hasResponseNoStop || responseIsStable);
|
|
703
694
|
|
|
704
695
|
if (isDone) {
|
|
@@ -709,12 +700,12 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
709
700
|
};
|
|
710
701
|
}
|
|
711
702
|
|
|
712
|
-
await delay(300);
|
|
703
|
+
await delay(300, signal);
|
|
713
704
|
}
|
|
714
705
|
|
|
715
706
|
// Timeout - return whatever we have (partial response is better than nothing)
|
|
716
|
-
const finalText = extractGrokResponse(previousText, userPrompt);
|
|
717
|
-
if (finalText && finalText.length >
|
|
707
|
+
const finalText = extractGrokResponse(previousText, userPrompt, lastChipTexts);
|
|
708
|
+
if (finalText && finalText.trim().length > 0) {
|
|
718
709
|
return {
|
|
719
710
|
text: finalText,
|
|
720
711
|
thinkingTime: thinkingTime,
|
|
@@ -741,20 +732,22 @@ async function query(options) {
|
|
|
741
732
|
cdpEvaluate,
|
|
742
733
|
cdpCommand,
|
|
743
734
|
log = () => {},
|
|
735
|
+
signal,
|
|
744
736
|
} = options;
|
|
737
|
+
throwIfAborted(signal);
|
|
745
738
|
|
|
746
739
|
const startTime = Date.now();
|
|
747
740
|
log("Starting Grok query");
|
|
748
741
|
|
|
749
742
|
// Check cookies for X.com authentication
|
|
750
|
-
const { cookies } = await getCookies
|
|
743
|
+
const { cookies } = await raceAbort(getCookies, signal);
|
|
751
744
|
if (!hasRequiredCookies(cookies)) {
|
|
752
745
|
throw new Error("X.com login required - log in to x.com in Chrome first");
|
|
753
746
|
}
|
|
754
747
|
log(`Got ${cookies.length} cookies`);
|
|
755
748
|
|
|
756
749
|
// Create tab
|
|
757
|
-
const tabInfo = await createTab
|
|
750
|
+
const tabInfo = await raceAbort(createTab, signal);
|
|
758
751
|
const { tabId } = tabInfo || {};
|
|
759
752
|
|
|
760
753
|
if (!tabId) {
|
|
@@ -762,8 +755,8 @@ async function query(options) {
|
|
|
762
755
|
}
|
|
763
756
|
log(`Created tab ${tabId}`);
|
|
764
757
|
|
|
765
|
-
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
766
|
-
const inputCdp = (method, params) => cdpCommand(tabId, method, params);
|
|
758
|
+
const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
|
|
759
|
+
const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
767
760
|
|
|
768
761
|
try {
|
|
769
762
|
// Wait for page load
|
|
@@ -801,6 +794,7 @@ async function query(options) {
|
|
|
801
794
|
warnings.push(`Requested model "${targetModel}" but got "${selectedModel}" - model may not be available`);
|
|
802
795
|
}
|
|
803
796
|
} catch (e) {
|
|
797
|
+
if (signal?.aborted) throw e;
|
|
804
798
|
modelSelectionFailed = true;
|
|
805
799
|
warnings.push(`Model selection failed: ${e.message}. Run 'surf grok --validate' to check available models.`);
|
|
806
800
|
log(`Model selection failed: ${e.message}`);
|
|
@@ -818,6 +812,7 @@ async function query(options) {
|
|
|
818
812
|
warnings.push(`DeepSearch toggle not found - feature may require X Premium or UI changed`);
|
|
819
813
|
}
|
|
820
814
|
} catch (e) {
|
|
815
|
+
if (signal?.aborted) throw e;
|
|
821
816
|
warnings.push(`DeepSearch toggle failed: ${e.message}`);
|
|
822
817
|
log(`DeepSearch toggle failed: ${e.message}`);
|
|
823
818
|
}
|
|
@@ -832,7 +827,7 @@ async function query(options) {
|
|
|
832
827
|
log("Submitted, waiting for response...");
|
|
833
828
|
|
|
834
829
|
// Wait for response
|
|
835
|
-
const response = await waitForResponse(cdp, timeout, prompt);
|
|
830
|
+
const response = await waitForResponse(cdp, timeout, prompt, signal);
|
|
836
831
|
const thinkingInfo = response.thinkingTime ? ` (thought for ${response.thinkingTime}s)` : '';
|
|
837
832
|
log(`Response: ${response.text.length} chars${thinkingInfo}${response.partial ? ' (partial)' : ''}`);
|
|
838
833
|
|
|
@@ -850,7 +845,11 @@ async function query(options) {
|
|
|
850
845
|
tookMs: Date.now() - startTime,
|
|
851
846
|
};
|
|
852
847
|
} finally {
|
|
853
|
-
|
|
848
|
+
try {
|
|
849
|
+
await closeTab(tabId);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
log(`Failed to close Grok tab ${tabId}: ${error?.message || error}`);
|
|
852
|
+
}
|
|
854
853
|
}
|
|
855
854
|
}
|
|
856
855
|
|
|
@@ -865,7 +864,9 @@ async function validate(options) {
|
|
|
865
864
|
closeTab,
|
|
866
865
|
cdpEvaluate,
|
|
867
866
|
log = () => {},
|
|
867
|
+
signal,
|
|
868
868
|
} = options;
|
|
869
|
+
throwIfAborted(signal);
|
|
869
870
|
|
|
870
871
|
const startTime = Date.now();
|
|
871
872
|
log("Starting Grok validation");
|
|
@@ -884,7 +885,7 @@ async function validate(options) {
|
|
|
884
885
|
|
|
885
886
|
// Check cookies
|
|
886
887
|
try {
|
|
887
|
-
const { cookies } = await getCookies
|
|
888
|
+
const { cookies } = await raceAbort(getCookies, signal);
|
|
888
889
|
result.authenticated = hasRequiredCookies(cookies);
|
|
889
890
|
if (!result.authenticated) {
|
|
890
891
|
result.errors.push("Not authenticated - log in to x.com in Chrome first");
|
|
@@ -892,6 +893,7 @@ async function validate(options) {
|
|
|
892
893
|
}
|
|
893
894
|
log("Cookies OK");
|
|
894
895
|
} catch (e) {
|
|
896
|
+
if (signal?.aborted) throw e;
|
|
895
897
|
result.errors.push(`Cookie check failed: ${e.message}`);
|
|
896
898
|
return { ...result, tookMs: Date.now() - startTime };
|
|
897
899
|
}
|
|
@@ -899,7 +901,7 @@ async function validate(options) {
|
|
|
899
901
|
// Create tab
|
|
900
902
|
let tabId;
|
|
901
903
|
try {
|
|
902
|
-
const tabInfo = await createTab
|
|
904
|
+
const tabInfo = await raceAbort(createTab, signal);
|
|
903
905
|
tabId = tabInfo?.tabId;
|
|
904
906
|
if (!tabId) {
|
|
905
907
|
result.errors.push("Failed to create tab");
|
|
@@ -907,15 +909,16 @@ async function validate(options) {
|
|
|
907
909
|
}
|
|
908
910
|
log(`Created tab ${tabId}`);
|
|
909
911
|
} catch (e) {
|
|
912
|
+
if (signal?.aborted) throw e;
|
|
910
913
|
result.errors.push(`Tab creation failed: ${e.message}`);
|
|
911
914
|
return { ...result, tookMs: Date.now() - startTime };
|
|
912
915
|
}
|
|
913
916
|
|
|
914
|
-
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
917
|
+
const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
|
|
915
918
|
|
|
916
919
|
try {
|
|
917
920
|
// Wait for page load
|
|
918
|
-
await waitForPageLoad(cdp);
|
|
921
|
+
await raceAbort(waitForPageLoad(cdp), signal);
|
|
919
922
|
log("Page loaded");
|
|
920
923
|
|
|
921
924
|
// Check login status
|
|
@@ -930,7 +933,7 @@ async function validate(options) {
|
|
|
930
933
|
log(`Login: yes${result.premium ? ' (Premium)' : ''}`);
|
|
931
934
|
|
|
932
935
|
// Wait for Grok UI
|
|
933
|
-
await waitForGrokReady(cdp);
|
|
936
|
+
await raceAbort(waitForGrokReady(cdp), signal);
|
|
934
937
|
log("Grok ready");
|
|
935
938
|
|
|
936
939
|
// Check for input field
|
|
@@ -967,7 +970,7 @@ async function validate(options) {
|
|
|
967
970
|
})()`);
|
|
968
971
|
|
|
969
972
|
if (modelButtonClicked?.success) {
|
|
970
|
-
await
|
|
973
|
+
await abortableDelay(500, signal);
|
|
971
974
|
|
|
972
975
|
// Scrape model options
|
|
973
976
|
const modelScrape = await evaluate(cdp, `(() => {
|
|
@@ -1013,9 +1016,14 @@ async function validate(options) {
|
|
|
1013
1016
|
}
|
|
1014
1017
|
|
|
1015
1018
|
} catch (e) {
|
|
1019
|
+
if (signal?.aborted) throw e;
|
|
1016
1020
|
result.errors.push(`Validation error: ${e.message}`);
|
|
1017
1021
|
} finally {
|
|
1018
|
-
|
|
1022
|
+
try {
|
|
1023
|
+
await closeTab(tabId);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
log(`Failed to close Grok validation tab ${tabId}: ${error?.message || error}`);
|
|
1026
|
+
}
|
|
1019
1027
|
}
|
|
1020
1028
|
|
|
1021
1029
|
result.tookMs = Date.now() - startTime;
|
|
@@ -1067,6 +1075,7 @@ module.exports = {
|
|
|
1067
1075
|
normalizeGrokModelLabel,
|
|
1068
1076
|
getGrokModelMatchLabels,
|
|
1069
1077
|
grokModelLabelsMatch,
|
|
1078
|
+
waitForResponse,
|
|
1070
1079
|
GROK_URL,
|
|
1071
1080
|
GROK_MODELS,
|
|
1072
1081
|
DEFAULT_GROK_MODELS,
|
package/native/host-helpers.cjs
CHANGED
|
@@ -20,7 +20,7 @@ function normalizeModelString(model) {
|
|
|
20
20
|
* @param {Function} log - Logging function (defaults to no-op for testing)
|
|
21
21
|
* @returns {Array} Array of content objects with type and text/data
|
|
22
22
|
*/
|
|
23
|
-
function formatToolContent(result, log = () => {}) {
|
|
23
|
+
function formatToolContent(result, log = () => {}, options = {}) {
|
|
24
24
|
const text = (s) => [{ type: "text", text: s }];
|
|
25
25
|
|
|
26
26
|
if (!result) return text("OK");
|
|
@@ -390,6 +390,7 @@ function formatToolContent(result, log = () => {}) {
|
|
|
390
390
|
|
|
391
391
|
if (result.autoScreenshot) {
|
|
392
392
|
const { path: ssPath, width, height } = result.autoScreenshot;
|
|
393
|
+
if (options.suppressImages) return text(`OK\nScreenshot saved: ${ssPath}`);
|
|
393
394
|
try {
|
|
394
395
|
const imgData = fs.readFileSync(ssPath);
|
|
395
396
|
const base64 = imgData.toString("base64");
|
|
@@ -471,11 +472,16 @@ function mapComputerAction(args, tabId) {
|
|
|
471
472
|
if (ref) return { type: "CLICK_REF", ref, button: "triple", ...baseMsg };
|
|
472
473
|
return { type: "EXECUTE_TRIPLE_CLICK", x: coordinate?.[0], y: coordinate?.[1], modifiers, ...baseMsg };
|
|
473
474
|
|
|
474
|
-
case "type":
|
|
475
|
+
case "type": {
|
|
475
476
|
if (ref) {
|
|
476
477
|
return { type: "FORM_FILL", data: [{ ref, value: text }], ...baseMsg };
|
|
477
478
|
}
|
|
479
|
+
const typeSelector = a.selector || a.into;
|
|
480
|
+
if (typeSelector) {
|
|
481
|
+
return { type: "SMART_TYPE", selector: typeSelector, text, clear: a.clear ?? true, submit: a.submit ?? false, ...baseMsg };
|
|
482
|
+
}
|
|
478
483
|
return { type: "EXECUTE_TYPE", text, ...baseMsg };
|
|
484
|
+
}
|
|
479
485
|
|
|
480
486
|
case "key": {
|
|
481
487
|
const keyValue = a.key || text;
|
|
@@ -726,7 +732,6 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
726
732
|
type: "EXPORT_NETWORK_REQUESTS",
|
|
727
733
|
har: a.har,
|
|
728
734
|
jsonl: a.jsonl,
|
|
729
|
-
output: a.output,
|
|
730
735
|
...baseMsg
|
|
731
736
|
};
|
|
732
737
|
|
|
@@ -798,6 +803,12 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
798
803
|
}
|
|
799
804
|
return { type: "CLOSE_TAB", tabId: id, tabIds: ids };
|
|
800
805
|
}
|
|
806
|
+
case "tab.move": {
|
|
807
|
+
const id = a.id || a.tab_id || a.tabId;
|
|
808
|
+
const ids = a.ids || a.tab_ids || a.tabIds;
|
|
809
|
+
const windowId = a["to-window"] || a.toWindow || a.window_id || a.windowId;
|
|
810
|
+
return { type: "TAB_MOVE", tabId: id, tabIds: ids, windowId, index: a.index };
|
|
811
|
+
}
|
|
801
812
|
case "tab.name":
|
|
802
813
|
return { type: "TABS_REGISTER", name: a.name, ...baseMsg };
|
|
803
814
|
case "tab.unname":
|
|
@@ -890,18 +901,32 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
890
901
|
case "upload":
|
|
891
902
|
const files = a.files ? (typeof a.files === "string" ? a.files.split(",").map(f => f.trim()) : a.files) : [];
|
|
892
903
|
return { type: "UPLOAD_FILE", ref: a.ref, files, ...baseMsg };
|
|
893
|
-
case "page.read":
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
904
|
+
case "page.read": {
|
|
905
|
+
let maxBytes;
|
|
906
|
+
if (a["max-bytes"] !== undefined) {
|
|
907
|
+
const raw = String(a["max-bytes"]).trim();
|
|
908
|
+
if (!/^\d+$/.test(raw) || raw === "0") {
|
|
909
|
+
throw new Error("max-bytes must be a positive integer");
|
|
910
|
+
}
|
|
911
|
+
maxBytes = parseInt(raw, 10);
|
|
912
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
|
|
913
|
+
throw new Error("max-bytes must be a positive integer");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
type: "READ_PAGE",
|
|
918
|
+
options: {
|
|
919
|
+
filter: a.filter || "interactive",
|
|
920
|
+
refId: a.ref,
|
|
899
921
|
includeText: a["no-text"] !== true,
|
|
900
922
|
depth: a.depth !== undefined ? parseInt(a.depth, 10) : undefined,
|
|
901
923
|
compact: a.compact || false,
|
|
902
|
-
|
|
903
|
-
|
|
924
|
+
maxBytes,
|
|
925
|
+
forceFullSnapshot: a.compact === true || maxBytes !== undefined,
|
|
926
|
+
},
|
|
927
|
+
...baseMsg
|
|
904
928
|
};
|
|
929
|
+
}
|
|
905
930
|
case "page.text":
|
|
906
931
|
return { type: "GET_PAGE_TEXT", ...baseMsg };
|
|
907
932
|
case "page.state":
|
|
@@ -1081,7 +1106,7 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
1081
1106
|
return {
|
|
1082
1107
|
type: "GEMINI_QUERY",
|
|
1083
1108
|
query: a.query,
|
|
1084
|
-
model: a.model || "gemini-3-pro",
|
|
1109
|
+
model: a.model || "gemini-3.1-pro",
|
|
1085
1110
|
withPage: a["with-page"],
|
|
1086
1111
|
file: a.file,
|
|
1087
1112
|
generateImage: a["generate-image"],
|