surf-cli 2.5.0 → 2.5.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/native/cli.cjs CHANGED
@@ -290,7 +290,7 @@ function resizeImage(filePath, maxSize) {
290
290
  }
291
291
  }
292
292
  const args = process.argv.slice(2);
293
- const VERSION = "2.0.0";
293
+ const VERSION = "2.5.2";
294
294
 
295
295
  const ALIASES = {
296
296
  snap: "screenshot",
@@ -91,15 +91,12 @@ function hasRequiredCookies(cookieMap) {
91
91
  return REQUIRED_COOKIES.every(name => Boolean(cookieMap[name]));
92
92
  }
93
93
 
94
- function delay(ms) {
95
- return new Promise(resolve => setTimeout(resolve, ms));
96
- }
97
-
98
94
  // ============================================================================
99
95
  // HTTP Helpers
100
96
  // ============================================================================
101
97
 
102
- function httpsGet(url, headers, binary = false) {
98
+ function httpsGet(url, headers, opts = {}) {
99
+ const { binary = false, timeoutMs = 30000, log = null, label = "httpsGet" } = opts;
103
100
  return new Promise((resolve, reject) => {
104
101
  const urlObj = new URL(url);
105
102
  const options = {
@@ -111,9 +108,12 @@ function httpsGet(url, headers, binary = false) {
111
108
  "user-agent": USER_AGENT,
112
109
  ...headers,
113
110
  },
111
+ rejectUnauthorized: false, // Required for Gemini API
112
+ timeout: timeoutMs,
114
113
  };
115
114
 
116
115
  const req = https.request(options, (res) => {
116
+ if (log) log(`${label}: response ${res.statusCode} ${urlObj.hostname}${urlObj.pathname}`);
117
117
  const chunks = [];
118
118
  res.on("data", chunk => chunks.push(chunk));
119
119
  res.on("end", () => {
@@ -125,16 +125,33 @@ function httpsGet(url, headers, binary = false) {
125
125
  buffer: binary ? buffer : null,
126
126
  });
127
127
  });
128
+ res.on("error", (err) => {
129
+ if (log) log(`${label}: response error ${err.message}`);
130
+ reject(err);
131
+ });
128
132
  });
129
133
 
130
- req.on("error", reject);
134
+ req.on("timeout", () => {
135
+ req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
136
+ });
137
+ req.on("error", (err) => {
138
+ if (log) log(`${label}: request error ${err.message}`);
139
+ reject(err);
140
+ });
131
141
  req.end();
132
142
  });
133
143
  }
134
144
 
135
- function httpsPost(url, headers, body) {
145
+ function httpsPost(url, headers, body, opts = {}) {
146
+ const { timeoutMs = 30000, log = null, label = "httpsPost" } = opts;
136
147
  return new Promise((resolve, reject) => {
137
148
  const urlObj = new URL(url);
149
+ const bodyBuffer = body == null
150
+ ? null
151
+ : Buffer.isBuffer(body)
152
+ ? body
153
+ : Buffer.from(String(body), "utf-8");
154
+ const hasLength = Object.keys(headers || {}).some((key) => key.toLowerCase() === "content-length");
138
155
  const options = {
139
156
  hostname: urlObj.hostname,
140
157
  port: 443,
@@ -143,25 +160,39 @@ function httpsPost(url, headers, body) {
143
160
  headers: {
144
161
  "user-agent": USER_AGENT,
145
162
  ...headers,
163
+ ...(bodyBuffer && !hasLength ? { "content-length": String(bodyBuffer.length) } : {}),
146
164
  },
165
+ rejectUnauthorized: false, // Required for Gemini API
166
+ timeout: timeoutMs,
147
167
  };
148
168
 
149
169
  const req = https.request(options, (res) => {
170
+ if (log) log(`${label}: response ${res.statusCode} ${urlObj.hostname}${urlObj.pathname}`);
150
171
  let data = "";
151
172
  res.on("data", chunk => data += chunk);
152
173
  res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, text: data }));
174
+ res.on("error", (err) => {
175
+ if (log) log(`${label}: response error ${err.message}`);
176
+ reject(err);
177
+ });
153
178
  });
154
179
 
155
- req.on("error", reject);
156
- if (body) req.write(body);
180
+ req.on("timeout", () => {
181
+ req.destroy(new Error(`${label}: request timeout after ${timeoutMs}ms`));
182
+ });
183
+ req.on("error", (err) => {
184
+ if (log) log(`${label}: request error ${err.message}`);
185
+ reject(err);
186
+ });
187
+ if (bodyBuffer) req.write(bodyBuffer);
157
188
  req.end();
158
189
  });
159
190
  }
160
191
 
161
- async function fetchWithRedirects(url, headers, maxRedirects = 10, binary = false) {
192
+ async function fetchWithRedirects(url, headers, maxRedirects = 10, binary = false, opts = {}) {
162
193
  let current = url;
163
194
  for (let i = 0; i <= maxRedirects; i++) {
164
- const res = await httpsGet(current, headers, binary);
195
+ const res = await httpsGet(current, headers, { ...opts, binary, label: opts.label || "httpsGet" });
165
196
  if (res.status >= 300 && res.status < 400 && res.headers.location) {
166
197
  current = new URL(res.headers.location, current).toString();
167
198
  continue;
@@ -175,9 +206,12 @@ async function fetchWithRedirects(url, headers, maxRedirects = 10, binary = fals
175
206
  // Gemini API Functions
176
207
  // ============================================================================
177
208
 
178
- async function fetchGeminiAccessToken(cookieMap) {
209
+ async function fetchGeminiAccessToken(cookieMap, opts = {}) {
179
210
  const cookieHeader = buildCookieHeader(cookieMap);
180
- const res = await fetchWithRedirects(GEMINI_APP_URL, { cookie: cookieHeader });
211
+ const res = await fetchWithRedirects(GEMINI_APP_URL, { cookie: cookieHeader }, 10, false, {
212
+ ...opts,
213
+ label: opts.label || "geminiAccessToken",
214
+ });
181
215
  const html = res.text;
182
216
 
183
217
  const tokens = ["SNlM0e", "thykhd"];
@@ -220,8 +254,7 @@ function extractGgdlUrls(rawText) {
220
254
  }
221
255
 
222
256
  function ensureFullSizeImageUrl(url) {
223
- if (url.includes("=s2048")) return url;
224
- if (url.includes("=s")) return url;
257
+ if (url.includes("=s")) return url; // Already has size parameter
225
258
  return `${url}=s2048`;
226
259
  }
227
260
 
@@ -314,7 +347,7 @@ function parseGeminiStreamGenerateResponse(rawText) {
314
347
  // File Upload
315
348
  // ============================================================================
316
349
 
317
- async function uploadGeminiFile(filePath) {
350
+ async function uploadGeminiFile(filePath, opts = {}) {
318
351
  const absPath = path.resolve(process.cwd(), filePath);
319
352
  const data = fs.readFileSync(absPath);
320
353
  const fileName = path.basename(absPath);
@@ -333,7 +366,7 @@ async function uploadGeminiFile(filePath) {
333
366
  const res = await httpsPost(GEMINI_UPLOAD_URL, {
334
367
  "content-type": `multipart/form-data; boundary=${boundary}`,
335
368
  "push-id": GEMINI_UPLOAD_PUSH_ID,
336
- }, body);
369
+ }, body, { ...opts, label: opts.label || "geminiUpload" });
337
370
 
338
371
  if (res.status < 200 || res.status >= 300) {
339
372
  throw new Error(`File upload failed: ${res.status} (${res.text.slice(0, 200)})`);
@@ -346,12 +379,15 @@ async function uploadGeminiFile(filePath) {
346
379
  // Image Download
347
380
  // ============================================================================
348
381
 
349
- async function downloadGeminiImage(url, cookieMap, outputPath) {
382
+ async function downloadGeminiImage(url, cookieMap, outputPath, opts = {}) {
350
383
  const cookieHeader = buildCookieHeader(cookieMap);
351
384
  const fullUrl = ensureFullSizeImageUrl(url);
352
385
 
353
386
  // Use binary mode for image download
354
- const res = await fetchWithRedirects(fullUrl, { cookie: cookieHeader }, 10, true);
387
+ const res = await fetchWithRedirects(fullUrl, { cookie: cookieHeader }, 10, true, {
388
+ ...opts,
389
+ label: opts.label || "geminiImageDownload",
390
+ });
355
391
 
356
392
  if (res.status < 200 || res.status >= 300) {
357
393
  throw new Error(`Failed to download image: ${res.status}`);
@@ -366,18 +402,18 @@ async function downloadGeminiImage(url, cookieMap, outputPath) {
366
402
  fs.writeFileSync(outputPath, res.buffer);
367
403
  }
368
404
 
369
- async function saveFirstGeminiImage(output, cookieMap, outputPath) {
405
+ async function saveFirstGeminiImage(output, cookieMap, outputPath, opts = {}) {
370
406
  // Try generated or web images first
371
407
  const genOrWeb = output.images.find(img => img.kind === "generated") ?? output.images[0];
372
408
  if (genOrWeb?.url) {
373
- await downloadGeminiImage(genOrWeb.url, cookieMap, outputPath);
409
+ await downloadGeminiImage(genOrWeb.url, cookieMap, outputPath, opts);
374
410
  return { saved: true, imageCount: output.images.length };
375
411
  }
376
412
 
377
413
  // Fall back to gg-dl URLs in raw response
378
414
  const ggdl = extractGgdlUrls(output.rawResponseText);
379
415
  if (ggdl[0]) {
380
- await downloadGeminiImage(ggdl[0], cookieMap, outputPath);
416
+ await downloadGeminiImage(ggdl[0], cookieMap, outputPath, opts);
381
417
  return { saved: true, imageCount: ggdl.length };
382
418
  }
383
419
 
@@ -398,16 +434,16 @@ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
398
434
  }
399
435
 
400
436
  async function runGeminiWebOnce(input) {
401
- const { prompt, files, model, cookieMap, chatMetadata } = input;
437
+ const { prompt, files, model, cookieMap, chatMetadata, timeoutMs = 30000, log = null } = input;
402
438
  const cookieHeader = buildCookieHeader(cookieMap);
403
439
 
404
440
  // 1. Get access token
405
- const at = await fetchGeminiAccessToken(cookieMap);
441
+ const at = await fetchGeminiAccessToken(cookieMap, { timeoutMs, log, label: "geminiAccessToken" });
406
442
 
407
443
  // 2. Upload files
408
444
  const uploaded = [];
409
445
  for (const file of files ?? []) {
410
- uploaded.push(await uploadGeminiFile(file));
446
+ uploaded.push(await uploadGeminiFile(file, { timeoutMs, log, label: "geminiUpload" }));
411
447
  }
412
448
 
413
449
  // 3. Build request
@@ -419,12 +455,13 @@ async function runGeminiWebOnce(input) {
419
455
  // 4. Send request
420
456
  const res = await httpsPost(GEMINI_STREAM_GENERATE_URL, {
421
457
  "content-type": "application/x-www-form-urlencoded;charset=utf-8",
458
+ "host": "gemini.google.com",
422
459
  "origin": "https://gemini.google.com",
423
460
  "referer": "https://gemini.google.com/",
424
461
  "x-same-domain": "1",
425
462
  "cookie": cookieHeader,
426
463
  [MODEL_HEADER_NAME]: MODEL_HEADERS[model] || MODEL_HEADERS["gemini-3-pro"],
427
- }, params.toString());
464
+ }, params.toString(), { timeoutMs, log, label: "geminiStreamGenerate" });
428
465
 
429
466
  const rawResponseText = res.text;
430
467
 
@@ -496,8 +533,8 @@ async function query(options) {
496
533
  output,
497
534
  youtube,
498
535
  aspectRatio,
499
- timeout = 300000,
500
536
  getCookies,
537
+ timeout = 300000,
501
538
  log = () => {},
502
539
  } = options;
503
540
 
@@ -550,6 +587,8 @@ async function query(options) {
550
587
  model: resolvedModel,
551
588
  cookieMap,
552
589
  chatMetadata: null,
590
+ timeoutMs: timeout,
591
+ log,
553
592
  });
554
593
 
555
594
  log("Sending edit request...");
@@ -560,13 +599,15 @@ async function query(options) {
560
599
  model: resolvedModel,
561
600
  cookieMap,
562
601
  chatMetadata: intro.metadata,
602
+ timeoutMs: timeout,
603
+ log,
563
604
  });
564
605
 
565
606
  response = out;
566
607
 
567
608
  // Save output image
568
609
  const outputPath = output || generateImage || "edited.png";
569
- const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath);
610
+ const imageSave = await saveFirstGeminiImage(out, cookieMap, outputPath, { timeoutMs: timeout, log });
570
611
  if (!imageSave.saved) {
571
612
  throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
572
613
  }
@@ -581,12 +622,14 @@ async function query(options) {
581
622
  model: resolvedModel,
582
623
  cookieMap,
583
624
  chatMetadata: null,
625
+ timeoutMs: timeout,
626
+ log,
584
627
  });
585
628
 
586
629
  response = out;
587
630
 
588
631
  // Save output image
589
- const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage);
632
+ const imageSave = await saveFirstGeminiImage(out, cookieMap, generateImage, { timeoutMs: timeout, log });
590
633
  if (!imageSave.saved) {
591
634
  throw new Error(`No images generated. Response: ${out.text?.slice(0, 200) || "(empty)"}`);
592
635
  }
@@ -601,6 +644,8 @@ async function query(options) {
601
644
  model: resolvedModel,
602
645
  cookieMap,
603
646
  chatMetadata: null,
647
+ timeoutMs: timeout,
648
+ log,
604
649
  });
605
650
 
606
651
  response = out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",