surf-cli 2.8.0 → 2.10.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.
Files changed (47) hide show
  1. package/README.md +146 -8
  2. package/native/abort.cjs +65 -0
  3. package/native/activity-journal.cjs +55 -0
  4. package/native/ai-queue.cjs +64 -0
  5. package/native/aistudio-build.cjs +21 -13
  6. package/native/aistudio-client.cjs +40 -20
  7. package/native/browser-lock.cjs +2 -2
  8. package/native/chatgpt-client.cjs +49 -31
  9. package/native/cli.cjs +352 -482
  10. package/native/client-transport.cjs +168 -0
  11. package/native/do-executor.cjs +68 -510
  12. package/native/do-parser.cjs +8 -249
  13. package/native/doctor.cjs +55 -5
  14. package/native/endpoint.cjs +174 -0
  15. package/native/file-transfer.cjs +734 -0
  16. package/native/gemini-client.cjs +156 -71
  17. package/native/grok-client.cjs +98 -89
  18. package/native/host-helpers.cjs +43 -26
  19. package/native/host-sessions.cjs +287 -0
  20. package/native/host.cjs +998 -620
  21. package/native/listener.cjs +20 -0
  22. package/native/mcp-server.cjs +60 -65
  23. package/native/network-export.cjs +116 -0
  24. package/native/network-store.cjs +38 -58
  25. package/native/perplexity-client.cjs +46 -17
  26. package/native/playbook-authoring.cjs +44 -0
  27. package/native/playbook-cli.cjs +157 -0
  28. package/native/playbook-client.cjs +259 -0
  29. package/native/playbook-receipts.cjs +109 -0
  30. package/native/playbook-records.cjs +208 -0
  31. package/native/playbook-runtime.cjs +177 -0
  32. package/native/playbooks.cjs +235 -0
  33. package/native/private-state.cjs +156 -0
  34. package/native/redaction.cjs +104 -0
  35. package/native/remote-auth.cjs +279 -0
  36. package/native/remote-transport.cjs +337 -0
  37. package/native/request-pending.cjs +148 -0
  38. package/native/socket-path.cjs +1 -1
  39. package/native/workflow-definition.cjs +368 -0
  40. package/native/workflow-runtime.cjs +225 -0
  41. package/package.json +9 -6
  42. package/playbooks/page/ops/read.json +22 -0
  43. package/playbooks/page/playbook.json +7 -0
  44. package/scripts/install-native-host.cjs +36 -5
  45. package/skills/README.md +11 -5
  46. package/skills/deep-x-research/SKILL.md +106 -0
  47. package/skills/surf/SKILL.md +72 -5
@@ -33,6 +33,7 @@ const {
33
33
  waitForGenerateResponseFromNetwork,
34
34
  waitForResponse,
35
35
  } = require("./aistudio-response.cjs");
36
+ const { raceAbort, throwIfAborted } = require("./abort.cjs");
36
37
 
37
38
  const DEFAULT_MODEL = "gemini-3.1-pro-preview";
38
39
 
@@ -310,22 +311,27 @@ async function query(options) {
310
311
  cdpCommand,
311
312
  readNetworkEntries,
312
313
  log = () => {},
314
+ signal,
313
315
  } = options;
316
+ throwIfAborted(signal);
314
317
 
318
+ const guardedReadNetworkEntries = readNetworkEntries
319
+ ? (...args) => raceAbort(() => readNetworkEntries(...args), signal)
320
+ : readNetworkEntries;
315
321
  const startTime = Date.now();
316
322
  log("Starting AI Studio query");
317
323
 
318
324
  const resolvedModel = normalizeModelString(model) || DEFAULT_MODEL;
319
325
  log(`Requested model: ${resolvedModel}`);
320
326
 
321
- const { cookies } = await getCookies();
327
+ const { cookies } = await raceAbort(getCookies, signal);
322
328
  if (!hasRequiredCookies(cookies)) {
323
329
  throw new Error("Google login required - sign into Google in Chrome first");
324
330
  }
325
331
  log(`Got ${cookies.length} cookies`);
326
332
 
327
333
  const createdUrl = buildAiStudioUrl(resolvedModel);
328
- const tabInfo = await createTab(createdUrl);
334
+ const tabInfo = await raceAbort(() => createTab(createdUrl), signal);
329
335
  const { tabId } = tabInfo || {};
330
336
 
331
337
  if (!tabId) {
@@ -333,21 +339,21 @@ async function query(options) {
333
339
  }
334
340
  log(`Created tab ${tabId}`);
335
341
 
336
- const cdp = (expr) => cdpEvaluate(tabId, expr);
337
- const inputCdp = (method, params) => cdpCommand(tabId, method, params);
342
+ const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
343
+ const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
338
344
 
339
345
  let baselineGenerateEntryIds = new Set();
340
346
 
341
347
  try {
342
- await waitForPageLoad(cdp);
348
+ await raceAbort(waitForPageLoad(cdp), signal);
343
349
  log("Page loaded");
344
350
 
345
- await waitForStudioReady(cdp);
351
+ await raceAbort(waitForStudioReady(cdp), signal);
346
352
  log("AI Studio ready");
347
353
 
348
354
  if (typeof readNetworkEntries === 'function') {
349
355
  try {
350
- const baselineNetwork = await readNetworkEntries(tabId);
356
+ const baselineNetwork = await guardedReadNetworkEntries(tabId);
351
357
  const baselineEntries = Array.isArray(baselineNetwork?.entries)
352
358
  ? baselineNetwork.entries
353
359
  : Array.isArray(baselineNetwork?.requests)
@@ -362,6 +368,7 @@ async function query(options) {
362
368
 
363
369
  log(`Network baseline ready (${baselineGenerateEntryIds.size} GenerateContent entries)`);
364
370
  } catch (e) {
371
+ if (signal?.aborted) throw e;
365
372
  log(`Network baseline failed: ${e.message || e}`);
366
373
  }
367
374
  }
@@ -369,6 +376,7 @@ async function query(options) {
369
376
  try {
370
377
  await enableUnformattedMarkdownView(cdp, log);
371
378
  } catch (e) {
379
+ if (signal?.aborted) throw e;
372
380
  log(`Markdown toggle failed: ${e.message}`);
373
381
  }
374
382
 
@@ -387,8 +395,8 @@ async function query(options) {
387
395
  try {
388
396
  log(`Runtime model param missing; retrying direct navigation: ${createdUrl}`);
389
397
  await inputCdp('Page.navigate', { url: createdUrl });
390
- await waitForPageLoad(cdp);
391
- await waitForStudioReady(cdp);
398
+ await raceAbort(waitForPageLoad(cdp), signal);
399
+ await raceAbort(waitForStudioReady(cdp), signal);
392
400
  runtimeUrl = await evaluate(cdp, 'location.href');
393
401
  runtimeModelParam = await evaluate(cdp, `(() => {
394
402
  try {
@@ -398,6 +406,7 @@ async function query(options) {
398
406
  }
399
407
  })()`);
400
408
  } catch (e) {
409
+ if (signal?.aborted) throw e;
401
410
  log(`Direct model URL navigation retry failed: ${e.message || e}`);
402
411
  }
403
412
  }
@@ -408,7 +417,10 @@ async function query(options) {
408
417
  log(`Model via UI (no URL param): requested="${resolvedModel}"`);
409
418
  }
410
419
 
411
- const currentModelInfo = await readCurrentModelInfo(cdp).catch(() => ({ found: false, label: '', modelId: '' }));
420
+ const currentModelInfo = await readCurrentModelInfo(cdp).catch((error) => {
421
+ if (signal?.aborted) throw error;
422
+ return { found: false, label: '', modelId: '' };
423
+ });
412
424
 
413
425
  log(`AI Studio URL: ${runtimeUrl}`);
414
426
  if (currentModelInfo?.label) {
@@ -427,8 +439,9 @@ async function query(options) {
427
439
 
428
440
  if (!modelApplied && usedUrlParam) {
429
441
  try {
430
- modelApplied = await waitForModelToApply(cdp, resolvedModel, log);
442
+ modelApplied = await raceAbort(waitForModelToApply(cdp, resolvedModel, log), signal);
431
443
  } catch (e) {
444
+ if (signal?.aborted) throw e;
432
445
  log(`Model apply wait failed: ${e.message}`);
433
446
  }
434
447
  }
@@ -436,19 +449,20 @@ async function query(options) {
436
449
  if (!modelApplied) {
437
450
  try {
438
451
  log(`Attempting UI model selection fallback: ${resolvedModel}`);
439
- await selectModel(cdp, resolvedModel, log);
440
- modelApplied = await waitForModelToApply(cdp, resolvedModel, log, 10000);
452
+ await raceAbort(selectModel(cdp, resolvedModel, log), signal);
453
+ modelApplied = await raceAbort(waitForModelToApply(cdp, resolvedModel, log, 10000), signal);
441
454
  } catch (e) {
455
+ if (signal?.aborted) throw e;
442
456
  log(`UI model selection fallback failed: ${e.message}`);
443
457
  }
444
458
  }
445
459
 
446
- await closeModelSelectorIfOpen(cdp, log);
460
+ await raceAbort(closeModelSelectorIfOpen(cdp, log), signal);
447
461
 
448
- await typePrompt(cdp, inputCdp, prompt);
462
+ await raceAbort(typePrompt(cdp, inputCdp, prompt), signal);
449
463
  log("Prompt typed");
450
464
 
451
- await submitPrompt(cdp, inputCdp);
465
+ await raceAbort(submitPrompt(cdp, inputCdp), signal);
452
466
  log("Submitted, waiting for response...");
453
467
 
454
468
  let response;
@@ -457,10 +471,11 @@ async function query(options) {
457
471
  try {
458
472
  const networkResult = await waitForGenerateResponseFromNetwork({
459
473
  tabId,
460
- readNetworkEntries,
474
+ readNetworkEntries: guardedReadNetworkEntries,
461
475
  timeoutMs: timeout,
462
476
  baselineEntryIds: baselineGenerateEntryIds,
463
477
  prompt,
478
+ signal,
464
479
  log,
465
480
  });
466
481
 
@@ -474,13 +489,14 @@ async function query(options) {
474
489
  `${networkResult.requestId ? ` (request ${networkResult.requestId})` : ''}`
475
490
  );
476
491
  } catch (networkErr) {
492
+ if (signal?.aborted) throw networkErr;
477
493
  log(`Network extraction failed, falling back to DOM: ${networkErr.message || networkErr}`);
478
494
 
479
495
  const remainingTimeoutMs = Math.max(timeout - (Date.now() - startTime), 10000);
480
- response = await waitForResponse(cdp, remainingTimeoutMs, prompt, log);
496
+ response = await raceAbort(waitForResponse(cdp, remainingTimeoutMs, prompt, log), signal);
481
497
  }
482
498
  } else {
483
- response = await waitForResponse(cdp, timeout, prompt, log);
499
+ response = await raceAbort(waitForResponse(cdp, timeout, prompt, log), signal);
484
500
  }
485
501
 
486
502
  const thinkingInfo = response.thinkingTime ? ` (thought for ${response.thinkingTime}s)` : '';
@@ -493,7 +509,11 @@ async function query(options) {
493
509
  tookMs: Date.now() - startTime,
494
510
  };
495
511
  } finally {
496
- await closeTab(tabId).catch(() => {});
512
+ try {
513
+ await closeTab(tabId);
514
+ } catch (error) {
515
+ log(`Failed to close AI Studio tab ${tabId}: ${error?.message || error}`);
516
+ }
497
517
  }
498
518
  }
499
519
 
@@ -11,8 +11,8 @@ function sleepSync(ms) {
11
11
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
12
12
  }
13
13
 
14
- function getBrowserLockDir(socketPath, tempDir) {
15
- const hash = crypto.createHash("sha256").update(socketPath).digest("hex").slice(0, 16);
14
+ function getBrowserLockDir(endpointKey, tempDir) {
15
+ const hash = crypto.createHash("sha256").update(endpointKey).digest("hex").slice(0, 16);
16
16
  return path.join(tempDir, `surf-lock-${hash}`);
17
17
  }
18
18
 
@@ -1,4 +1,5 @@
1
1
  const path = require("path");
2
+ const { abortableDelay, raceAbort, throwIfAborted } = require("./abort.cjs");
2
3
 
3
4
  const CHATGPT_URL = "https://chatgpt.com/";
4
5
 
@@ -14,8 +15,8 @@ const SELECTORS = {
14
15
  cloudflareScript: 'script[src*="/challenge-platform/"]',
15
16
  };
16
17
 
17
- function delay(ms) {
18
- return new Promise((resolve) => setTimeout(resolve, ms));
18
+ function delay(ms, signal) {
19
+ return abortableDelay(ms, signal);
19
20
  }
20
21
 
21
22
  function buildClickDispatcher() {
@@ -189,8 +190,10 @@ function isChatGPTResponseComplete(snapshot, stableCycles, stableMs) {
189
190
  return stableCycles >= 6 && stableMs >= 1200;
190
191
  }
191
192
 
192
- async function evaluate(cdp, expression) {
193
+ async function evaluate(cdp, expression, signal) {
194
+ throwIfAborted(signal);
193
195
  const result = await cdp(expression);
196
+ throwIfAborted(signal);
194
197
  if (result.exceptionDetails) {
195
198
  const desc = result.exceptionDetails.exception?.description ||
196
199
  result.exceptionDetails.text ||
@@ -203,14 +206,15 @@ async function evaluate(cdp, expression) {
203
206
  return result.result?.value;
204
207
  }
205
208
 
206
- async function waitForPageLoad(cdp, timeoutMs = 45000) {
209
+ async function waitForPageLoad(cdp, timeoutMs = 45000, signal) {
210
+ throwIfAborted(signal);
207
211
  const deadline = Date.now() + timeoutMs;
208
212
  while (Date.now() < deadline) {
209
213
  const ready = await evaluate(cdp, "document.readyState");
210
214
  if (ready === "complete" || ready === "interactive") {
211
215
  return;
212
216
  }
213
- await delay(100);
217
+ await delay(100, signal);
214
218
  }
215
219
  throw new Error("Page did not load in time");
216
220
  }
@@ -252,7 +256,8 @@ async function checkLoginStatus(cdp) {
252
256
  return result || { status: 0 };
253
257
  }
254
258
 
255
- async function waitForPromptReady(cdp, timeoutMs = 30000) {
259
+ async function waitForPromptReady(cdp, timeoutMs = 30000, signal) {
260
+ throwIfAborted(signal);
256
261
  const deadline = Date.now() + timeoutMs;
257
262
  const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
258
263
  while (Date.now() < deadline) {
@@ -270,7 +275,7 @@ async function waitForPromptReady(cdp, timeoutMs = 30000) {
270
275
  })()`
271
276
  );
272
277
  if (found) return true;
273
- await delay(200);
278
+ await delay(200, signal);
274
279
  }
275
280
  return false;
276
281
  }
@@ -302,7 +307,8 @@ function resolveChatGPTModelMenuOption(items, desiredModel) {
302
307
  }) || null;
303
308
  }
304
309
 
305
- async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
310
+ async function selectModel(cdp, desiredModel, timeoutMs = 8000, signal) {
311
+ throwIfAborted(signal);
306
312
  const modelButton = await evaluate(
307
313
  cdp,
308
314
  `(() => {
@@ -321,7 +327,7 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
321
327
  if (btn) dispatchClickSequence(btn);
322
328
  })()`
323
329
  );
324
- await delay(300);
330
+ await delay(300, signal);
325
331
 
326
332
  const normalizedModel = normalizeChatGPTModelChoice(desiredModel);
327
333
  const deadline = Date.now() + timeoutMs;
@@ -361,7 +367,7 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
361
367
  if (item) dispatchClickSequence(item);
362
368
  })()`
363
369
  );
364
- await delay(200);
370
+ await delay(200, signal);
365
371
  return match.label;
366
372
  }
367
373
 
@@ -379,13 +385,14 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
379
385
  );
380
386
  }
381
387
 
382
- await delay(100);
388
+ await delay(100, signal);
383
389
  }
384
390
 
385
391
  throw new Error(`Model not found: ${desiredModel} (timeout)`);
386
392
  }
387
393
 
388
- async function typePrompt(cdp, inputCdp, prompt) {
394
+ async function typePrompt(cdp, inputCdp, prompt, signal) {
395
+ throwIfAborted(signal);
389
396
  const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
390
397
  const encodedPrompt = JSON.stringify(prompt);
391
398
  const focused = await evaluate(
@@ -416,7 +423,7 @@ async function typePrompt(cdp, inputCdp, prompt) {
416
423
  throw new Error("Failed to focus prompt textarea");
417
424
  }
418
425
  await inputCdp("Input.insertText", { text: prompt });
419
- await delay(300);
426
+ await delay(300, signal);
420
427
  const verified = await evaluate(
421
428
  cdp,
422
429
  `(() => {
@@ -449,7 +456,8 @@ async function typePrompt(cdp, inputCdp, prompt) {
449
456
  }
450
457
  }
451
458
 
452
- async function clickSend(cdp, inputCdp) {
459
+ async function clickSend(cdp, inputCdp, signal) {
460
+ throwIfAborted(signal);
453
461
  const selectors = SELECTORS.sendButton.split(", ");
454
462
  const selectorsJson = JSON.stringify(selectors);
455
463
  const deadline = Date.now() + 8000;
@@ -475,7 +483,7 @@ async function clickSend(cdp, inputCdp) {
475
483
  );
476
484
  if (result === "clicked") return true;
477
485
  if (result === "missing") break;
478
- await delay(100);
486
+ await delay(100, signal);
479
487
  }
480
488
  await inputCdp("Input.dispatchKeyEvent", {
481
489
  type: "keyDown",
@@ -574,8 +582,10 @@ async function waitForResponse(
574
582
  cdp,
575
583
  timeoutMs = 2700000,
576
584
  baselineAssistant,
577
- baselineAssistantCount
585
+ baselineAssistantCount,
586
+ signal
578
587
  ) {
588
+ throwIfAborted(signal);
579
589
  const deadline = Date.now() + timeoutMs;
580
590
  let previousText = "";
581
591
  let stableCycles = 0;
@@ -588,7 +598,7 @@ async function waitForResponse(
588
598
  const snapshot = await readChatGPTResponseSnapshot(cdp);
589
599
 
590
600
  if (!snapshot) {
591
- await delay(400);
601
+ await delay(400, signal);
592
602
  continue;
593
603
  }
594
604
 
@@ -602,7 +612,7 @@ async function waitForResponse(
602
612
  );
603
613
 
604
614
  if (!hasNewAssistantContent) {
605
- await delay(400);
615
+ await delay(400, signal);
606
616
  continue;
607
617
  }
608
618
 
@@ -630,7 +640,7 @@ async function waitForResponse(
630
640
  };
631
641
  }
632
642
 
633
- await delay(400);
643
+ await delay(400, signal);
634
644
  }
635
645
 
636
646
  throw new Error("Response timeout");
@@ -648,27 +658,33 @@ async function query(options) {
648
658
  cdpEvaluate,
649
659
  cdpCommand,
650
660
  uploadFile,
661
+ beforeSubmit,
651
662
  log = () => {},
663
+ signal,
652
664
  } = options;
665
+ throwIfAborted(signal);
666
+ const guardedUploadFile = uploadFile
667
+ ? (...args) => raceAbort(() => uploadFile(...args), signal)
668
+ : uploadFile;
653
669
  const startTime = Date.now();
654
670
  log("Starting ChatGPT query");
655
- const { cookies } = await getCookies();
671
+ const { cookies } = await raceAbort(getCookies, signal);
656
672
  if (!hasRequiredCookies(cookies)) {
657
673
  throw new Error("ChatGPT login required");
658
674
  }
659
675
  log(`Got ${cookies.length} cookies`);
660
- const tabInfo = await createTab();
676
+ const tabInfo = await raceAbort(createTab, signal);
661
677
  const { tabId } = tabInfo;
662
678
  if (!tabId) {
663
679
  throw new Error("Failed to create ChatGPT tab");
664
680
  }
665
681
  log(`Created tab ${tabId}`);
666
682
 
667
- const cdp = (expr) => cdpEvaluate(tabId, expr);
668
- const inputCdp = (method, params) => cdpCommand(tabId, method, params);
683
+ const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
684
+ const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
669
685
 
670
686
  try {
671
- await waitForPageLoad(cdp);
687
+ await waitForPageLoad(cdp, 45000, signal);
672
688
  log("Page loaded");
673
689
  if (await isCloudflareBlocked(cdp)) {
674
690
  throw new Error("Cloudflare challenge detected - complete in browser");
@@ -685,13 +701,13 @@ async function query(options) {
685
701
  throw new Error("ChatGPT login required");
686
702
  }
687
703
  log("Login verified");
688
- const promptReady = await waitForPromptReady(cdp);
704
+ const promptReady = await waitForPromptReady(cdp, 30000, signal);
689
705
  if (!promptReady) {
690
706
  throw new Error("Prompt textarea not ready");
691
707
  }
692
708
  log("Prompt ready");
693
709
  if (model) {
694
- const selectedLabel = await selectModel(cdp, model);
710
+ const selectedLabel = await selectModel(cdp, model, 8000, signal);
695
711
  log(`Selected model: ${selectedLabel}`);
696
712
  }
697
713
  if (file) {
@@ -701,7 +717,7 @@ async function query(options) {
701
717
  const files = Array.isArray(file) ? file : [file];
702
718
  const absFiles = files.map((filePath) => path.resolve(process.cwd(), filePath));
703
719
  log(`Uploading ${absFiles.length} file(s) to ChatGPT...`);
704
- const uploadResult = await uploadFile(tabId, absFiles);
720
+ const uploadResult = await guardedUploadFile(tabId, absFiles);
705
721
  if (uploadResult?.error) {
706
722
  throw new Error(`ChatGPT file upload failed: ${uploadResult.error}`);
707
723
  }
@@ -709,18 +725,20 @@ async function query(options) {
709
725
  throw new Error("ChatGPT file upload failed: upload did not report success");
710
726
  }
711
727
  log("File uploaded, waiting for ChatGPT attachment processing...");
712
- await delay(1500);
728
+ await delay(1500, signal);
713
729
  }
714
- await typePrompt(cdp, inputCdp, prompt);
730
+ await typePrompt(cdp, inputCdp, prompt, signal);
715
731
  log("Prompt typed");
716
732
  const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
717
- await clickSend(cdp, inputCdp);
733
+ if (beforeSubmit) await raceAbort(beforeSubmit, signal);
734
+ await clickSend(cdp, inputCdp, signal);
718
735
  log("Prompt sent, waiting for response...");
719
736
  const response = await waitForResponse(
720
737
  cdp,
721
738
  timeout,
722
739
  baseline.latestAssistant,
723
- baseline.assistantCount
740
+ baseline.assistantCount,
741
+ signal
724
742
  );
725
743
  log(`Response received (${response.text.length} chars)`);
726
744
  return {