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
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const https = require("https");
9
+ const { abortError, abortableDelay, raceAbort, throwIfAborted } = require("./abort.cjs");
9
10
  const fs = require("fs");
10
11
  const path = require("path");
11
12
 
@@ -22,9 +23,9 @@ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
22
23
 
23
24
  const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
24
25
  const MODEL_HEADERS = {
25
- "gemini-3-pro": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]',
26
- "gemini-2.5-pro": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]',
27
- "gemini-2.5-flash": '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]',
26
+ "gemini-3.1-pro": '[1,null,null,null,"e6fa609c3fa255c0",null,null,0,[4]]',
27
+ "gemini-3.5-flash": '[1,null,null,null,"56fdd199312815e2",null,null,0,[4]]',
28
+ "gemini-3.1-flash-lite": '[1,null,null,null,"8c46e95b1a07cecc",null,null,0,[4]]',
28
29
  };
29
30
 
30
31
  const REQUIRED_COOKIES = ["__Secure-1PSID", "__Secure-1PSIDTS"];
@@ -96,7 +97,8 @@ function hasRequiredCookies(cookieMap) {
96
97
  // ============================================================================
97
98
 
98
99
  function httpsGet(url, headers, opts = {}) {
99
- const { binary = false, timeoutMs = 30000, log = null, label = "httpsGet" } = opts;
100
+ const { binary = false, timeoutMs = 30000, log = null, label = "httpsGet", signal } = opts;
101
+ throwIfAborted(signal);
100
102
  return new Promise((resolve, reject) => {
101
103
  const urlObj = new URL(url);
102
104
  const options = {
@@ -131,13 +133,17 @@ function httpsGet(url, headers, opts = {}) {
131
133
  });
132
134
  });
133
135
 
136
+ const onAbort = () => req.destroy(abortError(signal, "Request cancelled"));
137
+ signal?.addEventListener("abort", onAbort, { once: true });
134
138
  req.on("timeout", () => {
135
139
  req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
136
140
  });
137
141
  req.on("error", (err) => {
142
+ signal?.removeEventListener("abort", onAbort);
138
143
  if (log) log(`${label}: request error ${err.message}`);
139
144
  reject(err);
140
145
  });
146
+ req.on("close", () => signal?.removeEventListener("abort", onAbort));
141
147
  req.end();
142
148
  });
143
149
  }
@@ -151,7 +157,8 @@ function httpsPut(url, headers, body, opts = {}) {
151
157
  }
152
158
 
153
159
  function httpsSend(method, url, headers, body, opts = {}) {
154
- const { timeoutMs = 30000, log = null, label = "httpsSend" } = opts;
160
+ const { timeoutMs = 30000, log = null, label = "httpsSend", signal } = opts;
161
+ throwIfAborted(signal);
155
162
  return new Promise((resolve, reject) => {
156
163
  const urlObj = new URL(url);
157
164
  const bodyBuffer = body == null
@@ -185,13 +192,17 @@ function httpsSend(method, url, headers, body, opts = {}) {
185
192
  });
186
193
  });
187
194
 
195
+ const onAbort = () => req.destroy(abortError(signal, "Request cancelled"));
196
+ signal?.addEventListener("abort", onAbort, { once: true });
188
197
  req.on("timeout", () => {
189
198
  req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
190
199
  });
191
200
  req.on("error", (err) => {
201
+ signal?.removeEventListener("abort", onAbort);
192
202
  if (log) log(`${label}: request error ${err.message}`);
193
203
  reject(err);
194
204
  });
205
+ req.on("close", () => signal?.removeEventListener("abort", onAbort));
195
206
  if (bodyBuffer) req.write(bodyBuffer);
196
207
  req.end();
197
208
  });
@@ -292,14 +303,49 @@ function ensureFullSizeImageUrl(url) {
292
303
  return `${url}=s2048`;
293
304
  }
294
305
 
306
+ function getFirstStringAtPaths(value, paths) {
307
+ for (const pathParts of paths) {
308
+ const found = getNestedValue(value, pathParts, "");
309
+ if (typeof found === "string" && found.trim()) return found;
310
+ }
311
+ return "";
312
+ }
313
+
314
+ const GEMINI_TEXT_PATHS = [
315
+ [1, 0],
316
+ [1, 0, 0],
317
+ [1, 0, 1],
318
+ [1, 1, 0],
319
+ [2, 0],
320
+ [22, 0],
321
+ ];
322
+
323
+ const GEMINI_CARD_CONTENT_RE = /^http:\/\/googleusercontent\.com\/card_content\/\d+/;
324
+ const GEMINI_CARD_ALT_PATHS = [[22, 0], [1, 1, 0], [2, 0]];
325
+
326
+ function resolveCandidateText(candidate) {
327
+ const textRaw = getFirstStringAtPaths(candidate, GEMINI_TEXT_PATHS);
328
+ if (GEMINI_CARD_CONTENT_RE.test(textRaw)) {
329
+ return getFirstStringAtPaths(candidate, GEMINI_CARD_ALT_PATHS) || textRaw;
330
+ }
331
+ return textRaw;
332
+ }
333
+
334
+ // An unresolved card_content placeholder URL is not answer text; score it zero.
335
+ function candidateTextScore(candidate) {
336
+ const resolved = resolveCandidateText(candidate);
337
+ return GEMINI_CARD_CONTENT_RE.test(resolved) ? 0 : resolved.length;
338
+ }
339
+
295
340
  function parseGeminiStreamGenerateResponse(rawText) {
296
341
  const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
297
342
  const errorCode = extractErrorCode(responseJson);
298
343
 
299
344
  const parts = Array.isArray(responseJson) ? responseJson : [];
300
- let bodyIndex = 0;
301
345
  let body = null;
302
-
346
+ let bestTextLength = -1;
347
+
348
+ // Stream chunks are cumulative; the longest text is the most complete answer.
303
349
  for (let i = 0; i < parts.length; i++) {
304
350
  const partBody = getNestedValue(parts[i], [2], null);
305
351
  if (!partBody) continue;
@@ -307,9 +353,14 @@ function parseGeminiStreamGenerateResponse(rawText) {
307
353
  const parsed = JSON.parse(partBody);
308
354
  const candidateList = getNestedValue(parsed, [4], []);
309
355
  if (Array.isArray(candidateList) && candidateList.length > 0) {
310
- bodyIndex = i;
311
- body = parsed;
312
- break;
356
+ if (!body) {
357
+ body = parsed;
358
+ }
359
+ const score = candidateTextScore(candidateList[0]);
360
+ if (score > bestTextLength) {
361
+ bestTextLength = score;
362
+ body = parsed;
363
+ }
313
364
  }
314
365
  } catch {
315
366
  // ignore
@@ -318,11 +369,7 @@ function parseGeminiStreamGenerateResponse(rawText) {
318
369
 
319
370
  const candidateList = getNestedValue(body, [4], []);
320
371
  const firstCandidate = candidateList[0];
321
- const textRaw = getNestedValue(firstCandidate, [1, 0], "");
322
- const cardContent = /^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(textRaw);
323
- const text = cardContent
324
- ? (getNestedValue(firstCandidate, [22, 0], null) ?? textRaw)
325
- : textRaw;
372
+ const text = resolveCandidateText(firstCandidate);
326
373
  const thoughts = getNestedValue(firstCandidate, [37, 0, 0], null);
327
374
  const metadata = getNestedValue(body, [1], []);
328
375
 
@@ -341,27 +388,23 @@ function parseGeminiStreamGenerateResponse(rawText) {
341
388
  });
342
389
  }
343
390
 
344
- // Generated images
345
- const hasGenerated = Boolean(getNestedValue(firstCandidate, [12, 7, 0], null));
346
- if (hasGenerated) {
347
- let imgBody = null;
348
- for (let i = bodyIndex; i < parts.length; i++) {
349
- const partBody = getNestedValue(parts[i], [2], null);
350
- if (!partBody) continue;
351
- try {
352
- const parsed = JSON.parse(partBody);
353
- const candidateImages = getNestedValue(parsed, [4, 0, 12, 7, 0], null);
354
- if (candidateImages != null) {
355
- imgBody = parsed;
356
- break;
357
- }
358
- } catch {
359
- // ignore
391
+ // Keep the last chunk with a non-empty image list; a trailing empty must not erase it.
392
+ let imgBody = null;
393
+ for (let i = 0; i < parts.length; i++) {
394
+ const partBody = getNestedValue(parts[i], [2], null);
395
+ if (!partBody) continue;
396
+ try {
397
+ const parsed = JSON.parse(partBody);
398
+ const imgs = getNestedValue(parsed, [4, 0, 12, 7, 0], null);
399
+ if (Array.isArray(imgs) && imgs.length > 0) {
400
+ imgBody = parsed;
360
401
  }
402
+ } catch {
403
+ // ignore
361
404
  }
362
-
363
- const imgCandidate = getNestedValue(imgBody ?? body, [4, 0], null);
364
- const generated = getNestedValue(imgCandidate, [12, 7, 0], []);
405
+ }
406
+ if (imgBody) {
407
+ const generated = getNestedValue(imgBody, [4, 0, 12, 7, 0], []);
365
408
  for (const genImage of generated) {
366
409
  const url = getNestedValue(genImage, [0, 3, 3], null);
367
410
  if (!url) continue;
@@ -517,16 +560,17 @@ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
517
560
  }
518
561
 
519
562
  async function runGeminiWebOnce(input) {
520
- const { prompt, files, model, cookieMap, chatMetadata, timeoutMs = 30000, log = null } = input;
563
+ const { prompt, files, model, cookieMap, chatMetadata, timeoutMs = 30000, log = null, signal } = input;
564
+ throwIfAborted(signal);
521
565
  const cookieHeader = buildCookieHeader(cookieMap);
522
566
 
523
567
  // 1. Get access token
524
- const at = await fetchGeminiAccessToken(cookieMap, { timeoutMs, log, label: "geminiAccessToken" });
568
+ const at = await fetchGeminiAccessToken(cookieMap, { timeoutMs, log, label: "geminiAccessToken", signal });
525
569
 
526
570
  // 2. Upload files
527
571
  const uploaded = [];
528
572
  for (const file of files ?? []) {
529
- uploaded.push(await uploadGeminiFile(file, cookieMap, { timeoutMs, log, label: "geminiUpload" }));
573
+ uploaded.push(await uploadGeminiFile(file, cookieMap, { timeoutMs, log, label: "geminiUpload", signal }));
530
574
  }
531
575
 
532
576
  // 3. Build request
@@ -543,8 +587,8 @@ async function runGeminiWebOnce(input) {
543
587
  "referer": "https://gemini.google.com/",
544
588
  "x-same-domain": "1",
545
589
  "cookie": cookieHeader,
546
- [MODEL_HEADER_NAME]: MODEL_HEADERS[model] || MODEL_HEADERS["gemini-3-pro"],
547
- }, params.toString(), { timeoutMs, log, label: "geminiStreamGenerate" });
590
+ [MODEL_HEADER_NAME]: MODEL_HEADERS[model] || MODEL_HEADERS["gemini-3.1-pro"],
591
+ }, params.toString(), { timeoutMs, log, label: "geminiStreamGenerate", signal });
548
592
 
549
593
  const rawResponseText = res.text;
550
594
 
@@ -591,12 +635,13 @@ async function runGeminiWebOnce(input) {
591
635
  }
592
636
 
593
637
  async function runGeminiWebWithFallback(input) {
638
+ throwIfAborted(input.signal);
594
639
  const attempt = await runGeminiWebOnce(input);
595
640
 
596
641
  // Auto-fallback to flash if model unavailable
597
- if (isModelUnavailable(attempt.errorCode) && input.model !== "gemini-2.5-flash") {
598
- const fallback = await runGeminiWebOnce({ ...input, model: "gemini-2.5-flash" });
599
- return { ...fallback, effectiveModel: "gemini-2.5-flash" };
642
+ if (isModelUnavailable(attempt.errorCode) && input.model !== "gemini-3.5-flash") {
643
+ const fallback = await runGeminiWebOnce({ ...input, model: "gemini-3.5-flash" });
644
+ return { ...fallback, effectiveModel: "gemini-3.5-flash" };
600
645
  }
601
646
 
602
647
  return { ...attempt, effectiveModel: input.model };
@@ -607,7 +652,14 @@ async function runGeminiWebWithFallback(input) {
607
652
  // ============================================================================
608
653
 
609
654
  async function runGeminiWebViaPage(input) {
610
- const { prompt, files, model, timeoutMs = 120000, log = null, createTab, closeTab, jsEval, fetchUrl, uploadFile } = input;
655
+ const { prompt, files, model, timeoutMs = 120000, log = null, createTab, closeTab, jsEval, fetchUrl, uploadFile, signal } = input;
656
+ throwIfAborted(signal);
657
+ const guardedUploadFile = uploadFile
658
+ ? (...args) => raceAbort(() => uploadFile(...args), signal)
659
+ : uploadFile;
660
+ const guardedFetchUrl = fetchUrl
661
+ ? (...args) => raceAbort(() => fetchUrl(...args), signal)
662
+ : fetchUrl;
611
663
 
612
664
  if (!createTab || !closeTab || !jsEval) {
613
665
  throw new Error("In-page execution requires createTab, closeTab, and jsEval callbacks");
@@ -616,19 +668,19 @@ async function runGeminiWebViaPage(input) {
616
668
  let tabId = null;
617
669
  try {
618
670
  if (log) log("Creating Gemini tab...");
619
- const tabResult = await createTab();
671
+ const tabResult = await raceAbort(createTab, signal);
620
672
  tabId = tabResult?.tabId;
621
673
  if (!tabId) throw new Error("Failed to create Gemini tab");
622
674
  if (log) log(`Gemini tab created: ${tabId}`);
623
- await new Promise(r => setTimeout(r, 12000));
675
+ await abortableDelay(12000, signal);
624
676
 
625
677
  if (files?.length && uploadFile) {
626
678
  const absFiles = files.map(f => path.resolve(process.cwd(), f));
627
679
  if (log) log(`Uploading ${absFiles.length} file(s) via file chooser...`);
628
- const result = await uploadFile(tabId, absFiles);
680
+ const result = await guardedUploadFile(tabId, absFiles);
629
681
  if (result?.error) throw new Error(`File upload failed: ${result.error}`);
630
682
  if (log) log("File uploaded, waiting for processing...");
631
- await new Promise(r => setTimeout(r, 3000));
683
+ await abortableDelay(3000, signal);
632
684
  }
633
685
 
634
686
  const checkJsResult = (result, context) => {
@@ -638,20 +690,20 @@ async function runGeminiWebViaPage(input) {
638
690
  };
639
691
 
640
692
  // Type prompt
641
- const fullPrompt = prompt.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
693
+ const promptJson = JSON.stringify(prompt);
642
694
  if (log) log("Typing prompt...");
643
- const typeResult = await jsEval(tabId, `
695
+ const typeResult = await jsEval(tabId, `(() => {
644
696
  const editor = document.querySelector('.ql-editor[contenteditable=true]');
645
697
  if (!editor) return JSON.stringify({ error: "No editor found on page" });
646
698
  editor.focus();
647
699
  document.execCommand('selectAll', false, null);
648
- document.execCommand('insertText', false, '${fullPrompt}');
700
+ document.execCommand('insertText', false, ${promptJson});
649
701
  return JSON.stringify({ ok: true, len: editor.textContent.length });
650
- `);
702
+ })()`);
651
703
  const typed = JSON.parse(JSON.parse(checkJsResult(typeResult, "Type prompt")));
652
704
  if (typed.error) throw new Error(typed.error);
653
705
 
654
- const beforeResult = await jsEval(tabId, `
706
+ const beforeResult = await jsEval(tabId, `(() => {
655
707
  const imageKey = (img) => {
656
708
  const url = img.currentSrc || img.src || "";
657
709
  return url + "|" + img.naturalWidth + "x" + img.naturalHeight;
@@ -665,16 +717,16 @@ async function runGeminiWebViaPage(input) {
665
717
  })
666
718
  .map(imageKey);
667
719
  return JSON.stringify(baselineKeys);
668
- `);
720
+ })()`);
669
721
  const baselineImageKeys = JSON.parse(JSON.parse(checkJsResult(beforeResult, "Count images")) || "[]");
670
722
 
671
723
  if (log) log("Submitting...");
672
- const sendResult = await jsEval(tabId, `
724
+ const sendResult = await jsEval(tabId, `(() => {
673
725
  const btn = document.querySelector('button[aria-label="Send message"]');
674
726
  if (!btn) return 'no-btn';
675
727
  btn.click();
676
728
  return 'sent';
677
- `);
729
+ })()`);
678
730
  const sendVal = JSON.parse(checkJsResult(sendResult, "Click send"));
679
731
  if (sendVal === "no-btn") throw new Error("Send button not found on Gemini page");
680
732
 
@@ -685,8 +737,8 @@ async function runGeminiWebViaPage(input) {
685
737
  let responseText = "";
686
738
 
687
739
  while (Date.now() < deadline) {
688
- await new Promise(r => setTimeout(r, 2000));
689
- const pollResult = await jsEval(tabId, `
740
+ await abortableDelay(2000, signal);
741
+ const pollResult = await jsEval(tabId, `(async () => {
690
742
  const baselineKeys = new Set(${JSON.stringify(baselineImageKeys)});
691
743
  const imageKey = (img) => {
692
744
  const url = img.currentSrc || img.src || "";
@@ -729,7 +781,7 @@ async function runGeminiWebViaPage(input) {
729
781
  const lastTurn = turns.length ? turns[turns.length - 1] : null;
730
782
  const text = lastTurn ? lastTurn.textContent?.trim() : "";
731
783
  return JSON.stringify({ images, loading, text, turns: turns.length });
732
- `);
784
+ })()`);
733
785
  const poll = JSON.parse(JSON.parse(checkJsResult(pollResult, "Poll response")));
734
786
  const newImgs = poll.images || [];
735
787
 
@@ -758,7 +810,7 @@ async function runGeminiWebViaPage(input) {
758
810
  const chunkSize = 40000;
759
811
  let type = img.type || "image/png";
760
812
  while (true) {
761
- const chunkResult = await jsEval(tabId, `
813
+ const chunkResult = await jsEval(tabId, `(() => {
762
814
  const item = window.__surfGeminiBlobImages?.[${img.blobIndex}];
763
815
  if (!item) return JSON.stringify({ error: "Blob image not found" });
764
816
  return JSON.stringify({
@@ -767,7 +819,7 @@ async function runGeminiWebViaPage(input) {
767
819
  type: item.type || "image/png",
768
820
  url: item.url,
769
821
  });
770
- `);
822
+ })()`);
771
823
  const chunk = JSON.parse(JSON.parse(checkJsResult(chunkResult, "Read blob image chunk")));
772
824
  if (chunk.error) throw new Error(chunk.error);
773
825
  b64 += chunk.chunk || "";
@@ -780,7 +832,7 @@ async function runGeminiWebViaPage(input) {
780
832
  }
781
833
  if (img?.url && fetchUrl) {
782
834
  if (log) log(`Downloading image (${img.url.slice(0, 60)}...)...`);
783
- const dlResult = await fetchUrl(img.url);
835
+ const dlResult = await guardedFetchUrl(img.url);
784
836
  if (dlResult?.b64) {
785
837
  images.push({ url: img.url, b64: dlResult.b64, type: dlResult.type || "image/png" });
786
838
  }
@@ -798,7 +850,13 @@ async function runGeminiWebViaPage(input) {
798
850
  _pageTabId: tabId,
799
851
  };
800
852
  } catch (err) {
801
- if (tabId) { try { await closeTab(tabId); } catch {} }
853
+ if (tabId) {
854
+ try {
855
+ await closeTab(tabId);
856
+ } catch (closeError) {
857
+ log?.(`Failed to close Gemini tab ${tabId}: ${closeError?.message || closeError}`);
858
+ }
859
+ }
802
860
  throw err;
803
861
  }
804
862
  }
@@ -810,7 +868,7 @@ async function runGeminiWebViaPage(input) {
810
868
  async function query(options) {
811
869
  const {
812
870
  prompt,
813
- model = "gemini-3-pro",
871
+ model = "gemini-3.1-pro",
814
872
  file,
815
873
  generateImage,
816
874
  editImage,
@@ -825,14 +883,17 @@ async function query(options) {
825
883
  uploadFile,
826
884
  timeout = 300000,
827
885
  log = () => {},
886
+ signal,
828
887
  } = options;
888
+ throwIfAborted(signal);
829
889
  const hasPageCallbacks = !!(createTab && closeTab && jsEval);
890
+ const guardedJsEval = (...args) => raceAbort(() => jsEval(...args), signal);
830
891
 
831
892
  const startTime = Date.now();
832
893
  log("Starting Gemini query");
833
894
 
834
895
  // 1. Get cookies from Chrome
835
- const cookieResponse = await getCookies();
896
+ const cookieResponse = await raceAbort(getCookies, signal);
836
897
  const cookies = cookieResponse?.cookies;
837
898
  if (!Array.isArray(cookies)) {
838
899
  throw new Error("Failed to get cookies from Chrome. Make sure the extension is loaded and Chrome is running.");
@@ -846,7 +907,13 @@ async function query(options) {
846
907
  log(`Got ${Object.keys(cookieMap).length} Gemini cookies`);
847
908
 
848
909
  // 2. Resolve model
849
- const resolvedModel = MODEL_HEADERS[model] ? model : "gemini-3-pro";
910
+ let resolvedModel;
911
+ if (MODEL_HEADERS[model]) {
912
+ resolvedModel = model;
913
+ } else {
914
+ resolvedModel = "gemini-3.1-pro";
915
+ log(`Unknown Gemini model "${model}"; using "${resolvedModel}"`);
916
+ }
850
917
 
851
918
  // 3. Build prompt
852
919
  let fullPrompt = prompt || "";
@@ -883,16 +950,17 @@ async function query(options) {
883
950
  log,
884
951
  createTab,
885
952
  closeTab,
886
- jsEval,
953
+ jsEval: guardedJsEval,
887
954
  fetchUrl,
888
955
  uploadFile,
956
+ signal,
889
957
  });
890
958
 
891
959
  response = out;
892
960
 
893
961
  // Save output image
894
962
  const outputPath = output || generateImage || "edited.png";
895
- const saveOpts = { timeoutMs: timeout, log };
963
+ const saveOpts = { timeoutMs: timeout, log, signal };
896
964
  if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
897
965
  try {
898
966
  const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath, saveOpts);
@@ -900,7 +968,13 @@ async function query(options) {
900
968
  throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
901
969
  }
902
970
  } finally {
903
- if (out._pageTabId && closeTab) { try { await closeTab(out._pageTabId); } catch {} }
971
+ if (out._pageTabId && closeTab) {
972
+ try {
973
+ await closeTab(out._pageTabId);
974
+ } catch (closeError) {
975
+ log(`Failed to close Gemini tab ${out._pageTabId}: ${closeError?.message || closeError}`);
976
+ }
977
+ }
904
978
  }
905
979
  imagePath = outputPath;
906
980
 
@@ -916,8 +990,9 @@ async function query(options) {
916
990
  log,
917
991
  createTab,
918
992
  closeTab,
919
- jsEval,
993
+ jsEval: guardedJsEval,
920
994
  fetchUrl,
995
+ signal,
921
996
  });
922
997
  } else {
923
998
  out = await runGeminiWebWithFallback({
@@ -928,13 +1003,14 @@ async function query(options) {
928
1003
  chatMetadata: null,
929
1004
  timeoutMs: timeout,
930
1005
  log,
1006
+ signal,
931
1007
  });
932
1008
  }
933
1009
 
934
1010
  response = out;
935
1011
 
936
1012
  // Save output image
937
- const saveOpts = { timeoutMs: timeout, log };
1013
+ const saveOpts = { timeoutMs: timeout, log, signal };
938
1014
  if (fetchUrl) saveOpts.fetchUrl = fetchUrl;
939
1015
  try {
940
1016
  const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage, saveOpts);
@@ -942,7 +1018,13 @@ async function query(options) {
942
1018
  throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
943
1019
  }
944
1020
  } finally {
945
- if (out._pageTabId && closeTab) { try { await closeTab(out._pageTabId); } catch {} }
1021
+ if (out._pageTabId && closeTab) {
1022
+ try {
1023
+ await closeTab(out._pageTabId);
1024
+ } catch (closeError) {
1025
+ log(`Failed to close Gemini tab ${out._pageTabId}: ${closeError?.message || closeError}`);
1026
+ }
1027
+ }
946
1028
  }
947
1029
  imagePath = generateImage;
948
1030
 
@@ -957,11 +1039,13 @@ async function query(options) {
957
1039
  chatMetadata: null,
958
1040
  timeoutMs: timeout,
959
1041
  log,
1042
+ signal,
960
1043
  });
961
1044
 
962
1045
  response = out;
963
1046
  }
964
1047
  } catch (error) {
1048
+ if (error?.code === "SURF_REQUEST_ABORTED") throw error;
965
1049
  throw new Error(`Gemini request failed: ${error.message}`);
966
1050
  }
967
1051
 
@@ -983,6 +1067,7 @@ async function query(options) {
983
1067
  // ============================================================================
984
1068
 
985
1069
  module.exports = {
1070
+ httpsGet,
986
1071
  query,
987
1072
  hasRequiredCookies,
988
1073
  buildCookieMap,