surfagent 1.0.6 → 1.0.8

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/API.md CHANGED
@@ -226,6 +226,26 @@ Get clean, structured readable content from a page. Use this instead of screensh
226
226
 
227
227
  ---
228
228
 
229
+ ### POST /dismiss
230
+
231
+ Dismiss cookie banners, consent dialogs, and modal overlays. Supports 15+ language patterns (English, Norwegian, German, French, Spanish, Italian, Portuguese).
232
+
233
+ ```json
234
+ { "tab": "0" }
235
+ ```
236
+
237
+ Response:
238
+ ```json
239
+ { "dismissed": [{ "type": "cookie", "text": "reject all" }], "count": 1 }
240
+ ```
241
+
242
+ If nothing was found to dismiss:
243
+ ```json
244
+ { "dismissed": [], "count": 0 }
245
+ ```
246
+
247
+ ---
248
+
229
249
  ### POST /focus
230
250
 
231
251
  Bring a tab to the front in Chrome. Use this when a tab is behind other tabs or windows.
@@ -250,11 +270,18 @@ Click an element on a page.
250
270
  { "tab": "0", "selector": "#submit-btn" }
251
271
  ```
252
272
 
253
- **By text** (fuzzy match against visible text):
273
+ **By text** (fuzzy match against visible text, including dropdown options):
254
274
  ```json
255
275
  { "tab": "0", "text": "Submit" }
256
276
  ```
257
277
 
278
+ **With wait** (wait for page to settle after click — useful for SPA navigation):
279
+ ```json
280
+ { "tab": "0", "text": "Search", "waitAfter": 2000 }
281
+ ```
282
+
283
+ Text search matches: buttons, links, `role="option"`, `role="menuitem"`, `role="listitem"`, `li[aria-label]`, and elements with `onclick`. This means autocomplete dropdown items are clickable by text without needing `/eval`.
284
+
258
285
  **Response:**
259
286
  ```json
260
287
  { "success": true, "clicked": "BUTTON: Submit" }
package/README.md CHANGED
@@ -68,7 +68,8 @@ curl -X POST localhost:3456/read -H 'Content-Type: application/json' \
68
68
  | `/recon` | POST | Full page map — every element, form, selector, heading, nav link, metadata, captcha detection |
69
69
  | `/read` | POST | Structured page content — headings, tables, code blocks, notifications, result areas |
70
70
  | `/fill` | POST | Fill form fields with real CDP keystrokes (works with React, Vue, SPAs) |
71
- | `/click` | POST | Click by CSS selector or text match (handles `target="_blank"` automatically) |
71
+ | `/click` | POST | Click by selector or text, including dropdown options. Optional `waitAfter` for SPAs |
72
+ | `/dismiss` | POST | Auto-dismiss cookie banners, consent dialogs, modals (multi-language) |
72
73
  | `/scroll` | POST | Scroll page, returns visible content preview and scroll position |
73
74
  | `/navigate` | POST | Go to URL, back, or forward in the same tab |
74
75
  | `/eval` | POST | Run JavaScript in any tab or cross-origin iframe |
package/dist/api/act.d.ts CHANGED
@@ -23,6 +23,7 @@ export interface ClickRequest {
23
23
  tab: string;
24
24
  selector?: string;
25
25
  text?: string;
26
+ waitAfter?: number;
26
27
  }
27
28
  export declare function clickElement(request: ClickRequest, options: {
28
29
  port?: number;
@@ -70,6 +71,10 @@ export declare function readPage(tabPattern: string, options: {
70
71
  host?: string;
71
72
  selector?: string;
72
73
  }): Promise<any>;
74
+ export declare function dismissOverlays(tabPattern: string, options: {
75
+ port?: number;
76
+ host?: string;
77
+ }): Promise<any>;
73
78
  export interface CaptchaRequest {
74
79
  tab: string;
75
80
  action: 'detect' | 'read' | 'next' | 'prev' | 'submit' | 'audio' | 'restart';
package/dist/api/act.js CHANGED
@@ -78,8 +78,11 @@ export async function fillFields(request, options) {
78
78
  if (actual === field.value) {
79
79
  results.push({ selector: field.selector, success: true });
80
80
  }
81
+ else if (actual === undefined || actual === null) {
82
+ results.push({ selector: field.selector, success: false, error: `Element not found or has no value: ${field.selector}` });
83
+ }
81
84
  else {
82
- results.push({ selector: field.selector, success: true, error: `Value mismatch: got "${actual}"` });
85
+ results.push({ selector: field.selector, success: false, error: `Value mismatch: expected "${field.value}", got "${actual}"` });
83
86
  }
84
87
  }
85
88
  catch (error) {
@@ -135,9 +138,9 @@ export async function clickElement(request, options) {
135
138
  }
136
139
  if (!el && text) {
137
140
  const lower = text.toLowerCase();
138
- const all = document.querySelectorAll('a, button, input[type="submit"], [role="button"], [onclick]');
141
+ const all = document.querySelectorAll('a, button, input[type="submit"], [role="button"], [role="option"], [role="menuitem"], [role="listitem"], [role="tab"], [role="link"], li[aria-label], [onclick]');
139
142
  for (const candidate of all) {
140
- const t = (candidate.innerText || candidate.textContent || candidate.value || '').trim();
143
+ const t = (candidate.innerText || candidate.textContent || candidate.value || candidate.getAttribute('aria-label') || '').trim();
141
144
  if (t.toLowerCase().includes(lower)) { el = candidate; break; }
142
145
  }
143
146
  }
@@ -159,6 +162,10 @@ export async function clickElement(request, options) {
159
162
  `,
160
163
  returnByValue: true
161
164
  });
165
+ // Wait after click if requested (for page to settle after navigation/SPA route change)
166
+ if (request.waitAfter && request.waitAfter > 0) {
167
+ await new Promise(resolve => setTimeout(resolve, Math.min(request.waitAfter, 10000)));
168
+ }
162
169
  await client.close();
163
170
  return result.result.value;
164
171
  }
@@ -252,12 +259,14 @@ export async function evalInTab(tab, expression, options) {
252
259
  const resolved = await resolveTab(tab, port, host);
253
260
  const client = await connectToTab(resolved.id, port, host);
254
261
  try {
255
- const result = await client.Runtime.evaluate({
262
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Eval timed out after 30s')), 30000));
263
+ const evalPromise = client.Runtime.evaluate({
256
264
  expression,
257
265
  returnByValue: true
258
266
  });
267
+ const result = await Promise.race([evalPromise, timeout]);
259
268
  await client.close();
260
- return result.result.value;
269
+ return result.result.value ?? null;
261
270
  }
262
271
  catch (error) {
263
272
  await client.close();
@@ -357,6 +366,67 @@ export async function readPage(tabPattern, options) {
357
366
  throw error;
358
367
  }
359
368
  }
369
+ const DISMISS_OVERLAYS_SCRIPT = `
370
+ (function() {
371
+ const dismissed = [];
372
+
373
+ // Common cookie consent button patterns (multi-language)
374
+ const consentPatterns = [
375
+ 'reject all', 'reject', 'decline', 'deny',
376
+ 'accept all', 'accept', 'godta alle', 'godta',
377
+ 'alle ablehnen', 'ablehnen', 'tout refuser', 'refuser',
378
+ 'rechazar todo', 'rechazar', 'rifiuta tutto', 'rifiuta',
379
+ 'bare nødvendige', 'only necessary', 'nur notwendige',
380
+ 'manage preferences', 'cookie settings',
381
+ ];
382
+
383
+ // Try cookie consent buttons
384
+ for (const btn of document.querySelectorAll('button, a[role="button"]')) {
385
+ const text = (btn.innerText || btn.textContent || '').trim().toLowerCase();
386
+ if (text.length > 50 || text.length < 2) continue;
387
+ for (const pattern of consentPatterns) {
388
+ if (text === pattern || text.startsWith(pattern)) {
389
+ btn.click();
390
+ dismissed.push({ type: 'cookie', text: text.substring(0, 40) });
391
+ break;
392
+ }
393
+ }
394
+ if (dismissed.length) break;
395
+ }
396
+
397
+ // Try closing modal dialogs (X button, close button, dismiss)
398
+ if (!dismissed.length) {
399
+ for (const btn of document.querySelectorAll('[aria-label*="Close" i], [aria-label*="Dismiss" i], [aria-label*="Lukk" i], [aria-label*="Schließen" i], [aria-label*="Fermer" i]')) {
400
+ const dialog = btn.closest('[role="dialog"], [role="alertdialog"], .modal, [data-overlay]');
401
+ if (dialog) {
402
+ btn.click();
403
+ dismissed.push({ type: 'dialog', text: btn.getAttribute('aria-label') || 'close' });
404
+ break;
405
+ }
406
+ }
407
+ }
408
+
409
+ return { dismissed, count: dismissed.length };
410
+ })()
411
+ `;
412
+ export async function dismissOverlays(tabPattern, options) {
413
+ const port = options.port || 9222;
414
+ const host = options.host || 'localhost';
415
+ const tab = await resolveTab(tabPattern, port, host);
416
+ const client = await connectToTab(tab.id, port, host);
417
+ try {
418
+ const r = await client.Runtime.evaluate({
419
+ expression: DISMISS_OVERLAYS_SCRIPT,
420
+ returnByValue: true
421
+ });
422
+ await client.close();
423
+ return r.result.value;
424
+ }
425
+ catch (error) {
426
+ await client.close();
427
+ throw error;
428
+ }
429
+ }
360
430
  const CAPTCHA_DETECT_SCRIPT = `
361
431
  (function() {
362
432
  // Find captcha iframes on the page
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import http from 'node:http';
3
3
  import { reconUrl, reconTab } from './recon.js';
4
- import { fillFields, clickElement, scrollPage, navigatePage, evalInTab, focusTab, readPage, captchaInteract } from './act.js';
4
+ import { fillFields, clickElement, scrollPage, navigatePage, evalInTab, focusTab, readPage, captchaInteract, dismissOverlays } from './act.js';
5
5
  import { getAllTabs } from '../chrome/tabs.js';
6
6
  const PORT = parseInt(process.env.API_PORT || '3456', 10);
7
7
  const CDP_PORT = parseInt(process.env.CDP_PORT || '9222', 10);
@@ -90,6 +90,15 @@ const server = http.createServer(async (req, res) => {
90
90
  const result = await scrollPage(body, { port: CDP_PORT, host: CDP_HOST });
91
91
  return json(res, 200, result);
92
92
  }
93
+ // POST /dismiss — dismiss cookie banners, modals, overlays
94
+ if (path === '/dismiss' && req.method === 'POST') {
95
+ const body = parseBody(await readBody(req));
96
+ if (!body.tab) {
97
+ return json(res, 400, { error: 'Provide "tab"' });
98
+ }
99
+ const result = await dismissOverlays(body.tab, { port: CDP_PORT, host: CDP_HOST });
100
+ return json(res, 200, result);
101
+ }
93
102
  // POST /captcha — detect and interact with captchas
94
103
  if (path === '/captcha' && req.method === 'POST') {
95
104
  const body = parseBody(await readBody(req));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surfagent",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Browser automation API for AI agents — structured page recon, form filling, clicking, and navigation via Chrome CDP",
5
5
  "keywords": [
6
6
  "ai-agent",
package/src/api/act.ts CHANGED
@@ -104,8 +104,10 @@ export async function fillFields(
104
104
  const actual = verify.result.value as string;
105
105
  if (actual === field.value) {
106
106
  results.push({ selector: field.selector, success: true });
107
+ } else if (actual === undefined || actual === null) {
108
+ results.push({ selector: field.selector, success: false, error: `Element not found or has no value: ${field.selector}` });
107
109
  } else {
108
- results.push({ selector: field.selector, success: true, error: `Value mismatch: got "${actual}"` });
110
+ results.push({ selector: field.selector, success: false, error: `Value mismatch: expected "${field.value}", got "${actual}"` });
109
111
  }
110
112
  } catch (error) {
111
113
  results.push({ selector: field.selector, success: false, error: (error as Error).message });
@@ -148,6 +150,7 @@ export interface ClickRequest {
148
150
  tab: string;
149
151
  selector?: string;
150
152
  text?: string;
153
+ waitAfter?: number; // ms to wait after click for page to settle
151
154
  }
152
155
 
153
156
  export async function clickElement(
@@ -173,9 +176,9 @@ export async function clickElement(
173
176
  }
174
177
  if (!el && text) {
175
178
  const lower = text.toLowerCase();
176
- const all = document.querySelectorAll('a, button, input[type="submit"], [role="button"], [onclick]');
179
+ const all = document.querySelectorAll('a, button, input[type="submit"], [role="button"], [role="option"], [role="menuitem"], [role="listitem"], [role="tab"], [role="link"], li[aria-label], [onclick]');
177
180
  for (const candidate of all) {
178
- const t = (candidate.innerText || candidate.textContent || candidate.value || '').trim();
181
+ const t = (candidate.innerText || candidate.textContent || candidate.value || candidate.getAttribute('aria-label') || '').trim();
179
182
  if (t.toLowerCase().includes(lower)) { el = candidate; break; }
180
183
  }
181
184
  }
@@ -198,6 +201,11 @@ export async function clickElement(
198
201
  returnByValue: true
199
202
  });
200
203
 
204
+ // Wait after click if requested (for page to settle after navigation/SPA route change)
205
+ if (request.waitAfter && request.waitAfter > 0) {
206
+ await new Promise<void>(resolve => setTimeout(resolve, Math.min(request.waitAfter!, 10000)));
207
+ }
208
+
201
209
  await client.close();
202
210
  return result.result.value as any;
203
211
  } catch (error) {
@@ -323,12 +331,16 @@ export async function evalInTab(
323
331
  const client = await connectToTab(resolved.id, port, host);
324
332
 
325
333
  try {
326
- const result = await client.Runtime.evaluate({
334
+ const timeout = new Promise<never>((_, reject) =>
335
+ setTimeout(() => reject(new Error('Eval timed out after 30s')), 30000)
336
+ );
337
+ const evalPromise = client.Runtime.evaluate({
327
338
  expression,
328
339
  returnByValue: true
329
340
  });
341
+ const result = await Promise.race([evalPromise, timeout]);
330
342
  await client.close();
331
- return result.result.value;
343
+ return result.result.value ?? null;
332
344
  } catch (error) {
333
345
  await client.close();
334
346
  throw error;
@@ -434,6 +446,73 @@ export async function readPage(
434
446
  }
435
447
  }
436
448
 
449
+ const DISMISS_OVERLAYS_SCRIPT = `
450
+ (function() {
451
+ const dismissed = [];
452
+
453
+ // Common cookie consent button patterns (multi-language)
454
+ const consentPatterns = [
455
+ 'reject all', 'reject', 'decline', 'deny',
456
+ 'accept all', 'accept', 'godta alle', 'godta',
457
+ 'alle ablehnen', 'ablehnen', 'tout refuser', 'refuser',
458
+ 'rechazar todo', 'rechazar', 'rifiuta tutto', 'rifiuta',
459
+ 'bare nødvendige', 'only necessary', 'nur notwendige',
460
+ 'manage preferences', 'cookie settings',
461
+ ];
462
+
463
+ // Try cookie consent buttons
464
+ for (const btn of document.querySelectorAll('button, a[role="button"]')) {
465
+ const text = (btn.innerText || btn.textContent || '').trim().toLowerCase();
466
+ if (text.length > 50 || text.length < 2) continue;
467
+ for (const pattern of consentPatterns) {
468
+ if (text === pattern || text.startsWith(pattern)) {
469
+ btn.click();
470
+ dismissed.push({ type: 'cookie', text: text.substring(0, 40) });
471
+ break;
472
+ }
473
+ }
474
+ if (dismissed.length) break;
475
+ }
476
+
477
+ // Try closing modal dialogs (X button, close button, dismiss)
478
+ if (!dismissed.length) {
479
+ for (const btn of document.querySelectorAll('[aria-label*="Close" i], [aria-label*="Dismiss" i], [aria-label*="Lukk" i], [aria-label*="Schließen" i], [aria-label*="Fermer" i]')) {
480
+ const dialog = btn.closest('[role="dialog"], [role="alertdialog"], .modal, [data-overlay]');
481
+ if (dialog) {
482
+ btn.click();
483
+ dismissed.push({ type: 'dialog', text: btn.getAttribute('aria-label') || 'close' });
484
+ break;
485
+ }
486
+ }
487
+ }
488
+
489
+ return { dismissed, count: dismissed.length };
490
+ })()
491
+ `;
492
+
493
+ export async function dismissOverlays(
494
+ tabPattern: string,
495
+ options: { port?: number; host?: string }
496
+ ): Promise<any> {
497
+ const port = options.port || 9222;
498
+ const host = options.host || 'localhost';
499
+
500
+ const tab = await resolveTab(tabPattern, port, host);
501
+ const client = await connectToTab(tab.id, port, host);
502
+
503
+ try {
504
+ const r = await client.Runtime.evaluate({
505
+ expression: DISMISS_OVERLAYS_SCRIPT,
506
+ returnByValue: true
507
+ });
508
+ await client.close();
509
+ return r.result.value;
510
+ } catch (error) {
511
+ await client.close();
512
+ throw error;
513
+ }
514
+ }
515
+
437
516
  const CAPTCHA_DETECT_SCRIPT = `
438
517
  (function() {
439
518
  // Find captcha iframes on the page
package/src/api/server.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import http from 'node:http';
4
4
  import { reconUrl, reconTab } from './recon.js';
5
- import { fillFields, clickElement, scrollPage, navigatePage, evalInTab, focusTab, readPage, captchaInteract } from './act.js';
5
+ import { fillFields, clickElement, scrollPage, navigatePage, evalInTab, focusTab, readPage, captchaInteract, dismissOverlays } from './act.js';
6
6
  import { getAllTabs } from '../chrome/tabs.js';
7
7
 
8
8
  const PORT = parseInt(process.env.API_PORT || '3456', 10);
@@ -110,6 +110,16 @@ const server = http.createServer(async (req, res) => {
110
110
  return json(res, 200, result);
111
111
  }
112
112
 
113
+ // POST /dismiss — dismiss cookie banners, modals, overlays
114
+ if (path === '/dismiss' && req.method === 'POST') {
115
+ const body = parseBody(await readBody(req));
116
+ if (!body.tab) {
117
+ return json(res, 400, { error: 'Provide "tab"' });
118
+ }
119
+ const result = await dismissOverlays(body.tab, { port: CDP_PORT, host: CDP_HOST });
120
+ return json(res, 200, result);
121
+ }
122
+
113
123
  // POST /captcha — detect and interact with captchas
114
124
  if (path === '/captcha' && req.method === 'POST') {
115
125
  const body = parseBody(await readBody(req));