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 +28 -1
- package/README.md +2 -1
- package/dist/api/act.d.ts +5 -0
- package/dist/api/act.js +75 -5
- package/dist/api/server.js +10 -1
- package/package.json +1 -1
- package/src/api/act.ts +84 -5
- package/src/api/server.ts +11 -1
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
|
|
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:
|
|
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
|
|
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
|
package/dist/api/server.js
CHANGED
|
@@ -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
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:
|
|
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
|
|
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));
|