surfagent 1.0.7 → 1.0.9
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.js +8 -3
- package/dist/api/server.js +8 -0
- package/package.json +1 -1
- package/src/api/act.ts +9 -3
- package/src/api/server.ts +9 -0
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.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) {
|
|
@@ -256,12 +259,14 @@ export async function evalInTab(tab, expression, options) {
|
|
|
256
259
|
const resolved = await resolveTab(tab, port, host);
|
|
257
260
|
const client = await connectToTab(resolved.id, port, host);
|
|
258
261
|
try {
|
|
259
|
-
const
|
|
262
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Eval timed out after 30s')), 30000));
|
|
263
|
+
const evalPromise = client.Runtime.evaluate({
|
|
260
264
|
expression,
|
|
261
265
|
returnByValue: true
|
|
262
266
|
});
|
|
267
|
+
const result = await Promise.race([evalPromise, timeout]);
|
|
263
268
|
await client.close();
|
|
264
|
-
return result.result.value;
|
|
269
|
+
return result.result.value ?? null;
|
|
265
270
|
}
|
|
266
271
|
catch (error) {
|
|
267
272
|
await client.close();
|
package/dist/api/server.js
CHANGED
|
@@ -179,6 +179,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
179
179
|
json(res, 500, { error: message });
|
|
180
180
|
}
|
|
181
181
|
});
|
|
182
|
+
server.on('error', (err) => {
|
|
183
|
+
if (err.code === 'EADDRINUSE') {
|
|
184
|
+
console.error(`[surfagent] Port ${PORT} is already in use. API may already be running.`);
|
|
185
|
+
console.error(`[surfagent] Check with: curl localhost:${PORT}/health`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
});
|
|
182
190
|
server.listen(PORT, () => {
|
|
183
191
|
console.log(`Browser Recon API running on http://localhost:${PORT}`);
|
|
184
192
|
console.log(`CDP target: ${CDP_HOST}:${CDP_PORT}`);
|
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 });
|
|
@@ -329,12 +331,16 @@ export async function evalInTab(
|
|
|
329
331
|
const client = await connectToTab(resolved.id, port, host);
|
|
330
332
|
|
|
331
333
|
try {
|
|
332
|
-
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({
|
|
333
338
|
expression,
|
|
334
339
|
returnByValue: true
|
|
335
340
|
});
|
|
341
|
+
const result = await Promise.race([evalPromise, timeout]);
|
|
336
342
|
await client.close();
|
|
337
|
-
return result.result.value;
|
|
343
|
+
return result.result.value ?? null;
|
|
338
344
|
} catch (error) {
|
|
339
345
|
await client.close();
|
|
340
346
|
throw error;
|
package/src/api/server.ts
CHANGED
|
@@ -207,6 +207,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
211
|
+
if (err.code === 'EADDRINUSE') {
|
|
212
|
+
console.error(`[surfagent] Port ${PORT} is already in use. API may already be running.`);
|
|
213
|
+
console.error(`[surfagent] Check with: curl localhost:${PORT}/health`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
throw err;
|
|
217
|
+
});
|
|
218
|
+
|
|
210
219
|
server.listen(PORT, () => {
|
|
211
220
|
console.log(`Browser Recon API running on http://localhost:${PORT}`);
|
|
212
221
|
console.log(`CDP target: ${CDP_HOST}:${CDP_PORT}`);
|