surf-cli 2.4.0 → 2.4.2
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 +8 -9
- package/native/cli.cjs +2 -2
- package/native/do-parser.cjs +12 -6
- package/native/grok-client.cjs +120 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -378,18 +378,17 @@ Auto-cleanup: 24 hours TTL, 200MB max.
|
|
|
378
378
|
Execute multi-step browser automation as a single command:
|
|
379
379
|
|
|
380
380
|
```bash
|
|
381
|
-
# Inline workflow (
|
|
382
|
-
surf do 'go "https://example.com
|
|
383
|
-
type "user@example.com" --selector "input[name=email]"
|
|
384
|
-
type "password123" --selector "input[name=password]"
|
|
385
|
-
click --selector "button[type=submit]"
|
|
386
|
-
screenshot --output /tmp/after-login.png'
|
|
381
|
+
# Inline workflow (pipe-separated)
|
|
382
|
+
surf do 'go "https://example.com" | click e5 | screenshot'
|
|
387
383
|
|
|
388
|
-
#
|
|
389
|
-
surf do
|
|
384
|
+
# Multi-step login flow
|
|
385
|
+
surf do 'go "https://example.com/login" | type "user@example.com" --selector "#email" | type "pass" --selector "#password" | click --selector "button[type=submit]"'
|
|
386
|
+
|
|
387
|
+
# From JSON file
|
|
388
|
+
surf do --file workflow.json
|
|
390
389
|
|
|
391
390
|
# Validate without executing
|
|
392
|
-
surf do 'go "url"
|
|
391
|
+
surf do 'go "url" | click e5 | screenshot' --dry-run
|
|
393
392
|
```
|
|
394
393
|
|
|
395
394
|
**Why workflows?** Instead of 6-8 separate CLI calls with LLM orchestration between each step, a workflow executes deterministically with smart auto-waits. Faster, cheaper, and more reliable.
|
package/native/cli.cjs
CHANGED
|
@@ -817,9 +817,9 @@ const TOOLS = {
|
|
|
817
817
|
"dry-run": "Parse and validate without executing"
|
|
818
818
|
},
|
|
819
819
|
examples: [
|
|
820
|
-
{ cmd: 'do \'go "https://example.com"
|
|
820
|
+
{ cmd: 'do \'go "https://example.com" | click e5 | screenshot\'', desc: "Inline workflow" },
|
|
821
821
|
{ cmd: 'do -f login.json', desc: "From JSON file" },
|
|
822
|
-
{ cmd: 'do \'go "url"
|
|
822
|
+
{ cmd: 'do \'go "url" | click e5\' --dry-run', desc: "Validate without running" },
|
|
823
823
|
]
|
|
824
824
|
},
|
|
825
825
|
}
|
package/native/do-parser.cjs
CHANGED
|
@@ -206,17 +206,23 @@ function parseCommandLine(line) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
|
-
* Parse a
|
|
210
|
-
*
|
|
209
|
+
* Parse a workflow string into step array
|
|
210
|
+
* Supports pipe-separated (inline) or newline-separated (file) commands
|
|
211
|
+
* @param {string} input - Workflow string
|
|
211
212
|
* @returns {Array<{ cmd: string, args: object }>}
|
|
212
213
|
*/
|
|
213
214
|
function parseDoCommands(input) {
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
215
|
+
// Determine separator: use pipe if present, otherwise newlines
|
|
216
|
+
// Pipe is preferred for inline: 'go "url" | click e5 | screenshot'
|
|
217
|
+
// Newlines for files or heredocs
|
|
218
|
+
const hasPipe = input.includes('|');
|
|
219
|
+
const separator = hasPipe ? '|' : '\n';
|
|
220
|
+
|
|
221
|
+
// Also handle literal \n for backwards compatibility
|
|
222
|
+
const normalized = hasPipe ? input : input.replace(/\\n/g, '\n');
|
|
217
223
|
|
|
218
224
|
return normalized
|
|
219
|
-
.split(
|
|
225
|
+
.split(separator)
|
|
220
226
|
.map(line => line.trim())
|
|
221
227
|
.filter(line => line && !line.startsWith('#'))
|
|
222
228
|
.map(line => parseCommandLine(line))
|
package/native/grok-client.cjs
CHANGED
|
@@ -460,28 +460,76 @@ function extractGrokResponse(bodyText, userPrompt = '') {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
463
|
-
// Grok
|
|
464
|
-
//
|
|
463
|
+
// Grok can take a long time:
|
|
464
|
+
// - Thinking models (Grok 4.1 Thinking): 40-60+ seconds to think, then streams
|
|
465
|
+
// - Fast/Auto models: No thinking phase, just streams directly
|
|
465
466
|
|
|
466
467
|
const deadline = Date.now() + timeoutMs;
|
|
467
468
|
let previousText = '';
|
|
468
|
-
let
|
|
469
|
-
const requiredStableCycles = 4; // 4 cycles at 300ms = 1.2s stable
|
|
469
|
+
let previousLength = 0;
|
|
470
470
|
let lastChangeAt = Date.now();
|
|
471
|
-
const minStableMs = 1500; // 1.5 seconds stable
|
|
472
471
|
let thinkingTime = null;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const initialLength = initialSnapshot || 0;
|
|
472
|
+
let thinkingComplete = false;
|
|
473
|
+
let lastResponseText = '';
|
|
474
|
+
let responseStableCycles = 0;
|
|
477
475
|
|
|
478
476
|
while (Date.now() < deadline) {
|
|
479
|
-
//
|
|
480
|
-
const snapshot = await evaluate(cdp, `({
|
|
481
|
-
bodyText
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
477
|
+
// Get page state with multiple completion indicators
|
|
478
|
+
const snapshot = await evaluate(cdp, `(function() {
|
|
479
|
+
const bodyText = document.body.innerText || '';
|
|
480
|
+
|
|
481
|
+
// Check for stop/cancel button (indicates still generating)
|
|
482
|
+
const hasStopBtn = !!document.querySelector('button[aria-label*="Stop"], button[aria-label*="stop"], button[aria-label*="Cancel"]');
|
|
483
|
+
|
|
484
|
+
// Check for "Thought for Xs" which indicates thinking model completed thinking
|
|
485
|
+
const thinkMatch = bodyText.match(/Thought for (\\d+)s/i);
|
|
486
|
+
const thinkingDone = !!thinkMatch;
|
|
487
|
+
const thinkingSecs = thinkMatch ? parseInt(thinkMatch[1], 10) : null;
|
|
488
|
+
|
|
489
|
+
// Check if actively showing "thinking..." or similar loading state
|
|
490
|
+
const isThinking = /\\bthinking\\.\\.\\./i.test(bodyText) ||
|
|
491
|
+
/\\bSearching\\.\\.\\./i.test(bodyText) ||
|
|
492
|
+
bodyText.includes('Grok is thinking') ||
|
|
493
|
+
bodyText.includes('is thinking...');
|
|
494
|
+
|
|
495
|
+
// Try to find the actual Grok response in the DOM
|
|
496
|
+
// Look for the main content area - Grok responses appear in the conversation area
|
|
497
|
+
let responseText = '';
|
|
498
|
+
|
|
499
|
+
// Strategy 1: Look for article elements or main content containers
|
|
500
|
+
const articles = document.querySelectorAll('article');
|
|
501
|
+
if (articles.length > 0) {
|
|
502
|
+
// Get the last article which should be the response
|
|
503
|
+
const lastArticle = articles[articles.length - 1];
|
|
504
|
+
responseText = lastArticle.innerText || '';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Strategy 2: If no articles, look for the conversation container
|
|
508
|
+
if (!responseText) {
|
|
509
|
+
const convArea = document.querySelector('[data-testid="conversation"], [role="main"] > div > div');
|
|
510
|
+
if (convArea) {
|
|
511
|
+
responseText = convArea.innerText || '';
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Strategy 3: Fallback to looking for text after common Grok UI patterns
|
|
516
|
+
if (!responseText || responseText.length < 10) {
|
|
517
|
+
// Find content between user question and follow-up suggestions
|
|
518
|
+
const mainArea = document.querySelector('main') || document.body;
|
|
519
|
+
responseText = mainArea.innerText || bodyText;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
bodyText: bodyText,
|
|
524
|
+
responseText: responseText,
|
|
525
|
+
bodyLength: bodyText.length,
|
|
526
|
+
hasStopBtn: hasStopBtn,
|
|
527
|
+
thinkingDone: thinkingDone,
|
|
528
|
+
thinkingSecs: thinkingSecs,
|
|
529
|
+
isThinking: isThinking,
|
|
530
|
+
url: location.href
|
|
531
|
+
};
|
|
532
|
+
})()`);
|
|
485
533
|
|
|
486
534
|
if (!snapshot || !snapshot.bodyText) {
|
|
487
535
|
await delay(300);
|
|
@@ -489,62 +537,82 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
489
537
|
}
|
|
490
538
|
|
|
491
539
|
const bodyText = snapshot.bodyText;
|
|
540
|
+
const bodyLength = snapshot.bodyLength;
|
|
492
541
|
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
542
|
+
// Track thinking time (for thinking models)
|
|
543
|
+
if (snapshot.thinkingSecs) {
|
|
544
|
+
if (!thinkingTime || snapshot.thinkingSecs > thinkingTime) {
|
|
545
|
+
thinkingTime = snapshot.thinkingSecs;
|
|
546
|
+
}
|
|
498
547
|
}
|
|
499
548
|
|
|
500
|
-
//
|
|
501
|
-
|
|
549
|
+
// Detect when thinking completes (thinking models only)
|
|
550
|
+
// "Thought for Xs" is a DEFINITIVE signal that thinking AND response generation is done
|
|
551
|
+
if (snapshot.thinkingDone && !thinkingComplete) {
|
|
552
|
+
thinkingComplete = true;
|
|
553
|
+
// Give a brief moment for final render, then we're done
|
|
554
|
+
await delay(500);
|
|
555
|
+
}
|
|
502
556
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
557
|
+
// Extract the actual response text - try DOM-extracted first, fall back to body parsing
|
|
558
|
+
let currentResponseText = '';
|
|
559
|
+
if (snapshot.responseText && snapshot.responseText.length > 10) {
|
|
560
|
+
currentResponseText = extractGrokResponse(snapshot.responseText, userPrompt) || '';
|
|
561
|
+
}
|
|
562
|
+
if (!currentResponseText || currentResponseText.length < 5) {
|
|
563
|
+
currentResponseText = extractGrokResponse(bodyText, userPrompt) || '';
|
|
564
|
+
}
|
|
506
565
|
|
|
507
|
-
// Track
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
566
|
+
// Track RESPONSE text stability (more reliable than body text)
|
|
567
|
+
if (currentResponseText !== lastResponseText) {
|
|
568
|
+
lastResponseText = currentResponseText;
|
|
569
|
+
responseStableCycles = 0;
|
|
511
570
|
lastChangeAt = Date.now();
|
|
512
|
-
} else {
|
|
513
|
-
|
|
571
|
+
} else if (currentResponseText.length > 0) {
|
|
572
|
+
responseStableCycles++;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Track body text for timeout fallback
|
|
576
|
+
if (bodyLength !== previousLength) {
|
|
577
|
+
previousText = bodyText;
|
|
578
|
+
previousLength = bodyLength;
|
|
514
579
|
}
|
|
515
580
|
|
|
516
581
|
const stableMs = Date.now() - lastChangeAt;
|
|
517
|
-
const
|
|
582
|
+
const noStopButton = !snapshot.hasStopBtn;
|
|
583
|
+
|
|
584
|
+
// Response is stable if the extracted response text hasn't changed
|
|
585
|
+
// Use shorter thresholds since we're checking actual content, not noisy body text
|
|
586
|
+
// 4 cycles (1.2s) + 1.5s minimum is enough for response stability
|
|
587
|
+
const responseIsStable = responseStableCycles >= 4 && stableMs >= 1500 && currentResponseText.length > 10;
|
|
588
|
+
|
|
589
|
+
// "Thought for Xs" is the strongest completion signal - response is definitely done
|
|
590
|
+
const thinkingModelDone = snapshot.thinkingDone && noStopButton;
|
|
591
|
+
|
|
592
|
+
// SIMPLE CHECK: If we have response content, no stop button, and stable for 3+ cycles
|
|
593
|
+
const hasResponseNoStop = currentResponseText.length > 5 && noStopButton && responseStableCycles >= 3;
|
|
518
594
|
|
|
519
595
|
// Response is complete when:
|
|
520
|
-
// 1.
|
|
521
|
-
// 2.
|
|
522
|
-
// 3.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
(hasFollowUps || isStable) &&
|
|
526
|
-
bodyText.length > initialLength;
|
|
596
|
+
// 1. Has meaningful response content (> 5 chars)
|
|
597
|
+
// 2. No stop button
|
|
598
|
+
// 3. Either: thinking done, response stable for 3+ cycles, OR stable for 4+ cycles with 1.5s
|
|
599
|
+
const isDone = currentResponseText.length > 5 && noStopButton &&
|
|
600
|
+
(thinkingModelDone || hasResponseNoStop || responseIsStable);
|
|
527
601
|
|
|
528
602
|
if (isDone) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
text: responseText,
|
|
536
|
-
thinkingTime: thinkingTime,
|
|
537
|
-
url: snapshot.url,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
603
|
+
return {
|
|
604
|
+
text: currentResponseText,
|
|
605
|
+
thinkingTime: thinkingTime,
|
|
606
|
+
url: snapshot.url,
|
|
607
|
+
};
|
|
540
608
|
}
|
|
541
609
|
|
|
542
610
|
await delay(300);
|
|
543
611
|
}
|
|
544
612
|
|
|
545
|
-
// Timeout - return whatever we have
|
|
613
|
+
// Timeout - return whatever we have (partial response is better than nothing)
|
|
546
614
|
const finalText = extractGrokResponse(previousText, userPrompt);
|
|
547
|
-
if (finalText) {
|
|
615
|
+
if (finalText && finalText.length > 10) {
|
|
548
616
|
return {
|
|
549
617
|
text: finalText,
|
|
550
618
|
thinkingTime: thinkingTime,
|