surfagent 1.0.4 → 1.0.6
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/README.md +75 -111
- package/dist/api/server.js +26 -9
- package/package.json +24 -2
- package/src/api/server.ts +27 -9
package/README.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# surfagent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Browser automation API for AI agents.** Give any AI agent the ability to see, navigate, and interact with real web pages through Chrome.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`npm install -g surfagent` — two commands to give your agent a browser.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/surfagent)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
**surfagent** connects to a local Chrome browser via CDP and exposes a simple HTTP API that returns structured page data — every interactive element, form field, link, and CSS selector — so AI agents can navigate websites fast and precisely without screenshots or trial-and-error.
|
|
13
|
+
|
|
14
|
+
**Works with any AI agent framework:** LangChain, CrewAI, AutoGPT, Claude Code, OpenAI Agents, custom agents — anything that can make HTTP calls.
|
|
6
15
|
|
|
7
16
|
## Quick Start
|
|
8
17
|
|
|
@@ -11,153 +20,104 @@ npm install -g surfagent
|
|
|
11
20
|
surfagent start
|
|
12
21
|
```
|
|
13
22
|
|
|
14
|
-
|
|
23
|
+
A **new Chrome window** opens with debug mode — your personal Chrome is not affected. The API starts on `http://localhost:3456`.
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
## Why surfagent?
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
| Without surfagent | With surfagent |
|
|
28
|
+
|---|---|
|
|
29
|
+
| Agent takes screenshots, sends to vision model | Agent calls `/recon`, gets structured JSON in 30ms |
|
|
30
|
+
| Guesses CSS selectors, fails, retries | Gets exact selectors from recon response |
|
|
31
|
+
| Can't read forms, dropdowns, or modals | Gets form schemas with labels, types, required flags |
|
|
32
|
+
| Breaks on SPAs, iframes, shadow DOM | Handles all of them out of the box |
|
|
33
|
+
| Slow (2-5s per screenshot round-trip) | Fast (20-60ms per API call on existing tabs) |
|
|
25
34
|
|
|
26
|
-
##
|
|
35
|
+
## How Agents Use It
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
The workflow is: **recon → act → read**.
|
|
29
38
|
|
|
30
|
-
```
|
|
31
|
-
|
|
39
|
+
```
|
|
40
|
+
1. POST /recon → get the page map (selectors, forms, elements)
|
|
41
|
+
2. POST /click → click something using a selector from step 1
|
|
42
|
+
POST /fill → fill a form using selectors from step 1
|
|
43
|
+
3. POST /read → check what happened (success? error? new content?)
|
|
44
|
+
4. POST /recon → if the page changed, map it again
|
|
32
45
|
```
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
- Every clickable element with a CSS selector
|
|
36
|
-
- Every form with field labels, types, and required flags
|
|
37
|
-
- Page headings, navigation links, metadata
|
|
38
|
-
- Overlay/modal detection
|
|
39
|
-
|
|
40
|
-
Your agent uses those selectors to interact — no guessing.
|
|
41
|
-
|
|
42
|
-
## What Can It Do?
|
|
47
|
+
### Example: search on any website
|
|
43
48
|
|
|
44
|
-
### Map a page
|
|
45
49
|
```bash
|
|
46
|
-
#
|
|
50
|
+
# 1. Recon the page — find the search input
|
|
47
51
|
curl -X POST localhost:3456/recon -H 'Content-Type: application/json' \
|
|
48
52
|
-d '{"tab":"0"}'
|
|
53
|
+
# Response includes: { "selector": "input[name='search']", "text": "Search..." }
|
|
49
54
|
|
|
50
|
-
#
|
|
51
|
-
curl -X POST localhost:3456/
|
|
52
|
-
-d '{"
|
|
53
|
-
```
|
|
55
|
+
# 2. Type and submit
|
|
56
|
+
curl -X POST localhost:3456/fill -H 'Content-Type: application/json' \
|
|
57
|
+
-d '{"tab":"0", "fields":[{"selector":"input[name=\"search\"]","value":"AI agents"}], "submit":"enter"}'
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
# Structured text — headings, tables, notifications
|
|
59
|
+
# 3. Read the results
|
|
58
60
|
curl -X POST localhost:3456/read -H 'Content-Type: application/json' \
|
|
59
61
|
-d '{"tab":"0"}'
|
|
60
|
-
|
|
61
|
-
# Read a specific element
|
|
62
|
-
curl -X POST localhost:3456/read -H 'Content-Type: application/json' \
|
|
63
|
-
-d '{"tab":"0", "selector":".results"}'
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
curl -X POST localhost:3456/fill -H 'Content-Type: application/json' \
|
|
69
|
-
-d '{"tab":"0", "fields":[
|
|
70
|
-
{"selector":"#email", "value":"me@example.com"},
|
|
71
|
-
{"selector":"#password", "value":"secret"}
|
|
72
|
-
], "submit":"enter"}'
|
|
73
|
-
```
|
|
64
|
+
## All Endpoints
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
66
|
+
| Endpoint | Method | Description |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `/recon` | POST | Full page map — every element, form, selector, heading, nav link, metadata, captcha detection |
|
|
69
|
+
| `/read` | POST | Structured page content — headings, tables, code blocks, notifications, result areas |
|
|
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) |
|
|
72
|
+
| `/scroll` | POST | Scroll page, returns visible content preview and scroll position |
|
|
73
|
+
| `/navigate` | POST | Go to URL, back, or forward in the same tab |
|
|
74
|
+
| `/eval` | POST | Run JavaScript in any tab or cross-origin iframe |
|
|
75
|
+
| `/captcha` | POST | Detect and interact with captchas — Arkose, reCAPTCHA, hCaptcha (experimental) |
|
|
76
|
+
| `/focus` | POST | Bring a tab to the front in Chrome |
|
|
77
|
+
| `/tabs` | GET | List all open Chrome tabs |
|
|
78
|
+
| `/health` | GET | Check if Chrome and API are connected |
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
curl -X POST localhost:3456/click -H 'Content-Type: application/json' \
|
|
83
|
-
-d '{"tab":"0", "selector":"#submit-btn"}'
|
|
84
|
-
```
|
|
80
|
+
Full API reference with request/response schemas: **[API.md](./API.md)**
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
# Go to URL (same tab)
|
|
89
|
-
curl -X POST localhost:3456/navigate -H 'Content-Type: application/json' \
|
|
90
|
-
-d '{"tab":"0", "url":"https://example.com"}'
|
|
82
|
+
## Key Features
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
curl -X POST localhost:3456/navigate -H 'Content-Type: application/json' \
|
|
94
|
-
-d '{"tab":"0", "back":true}'
|
|
95
|
-
```
|
|
84
|
+
**Page reconnaissance** — one call returns every interactive element with stable CSS selectors, form schemas with field labels and validation, navigation structure, metadata, and content summary.
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
curl -X POST localhost:3456/scroll -H 'Content-Type: application/json' \
|
|
100
|
-
-d '{"tab":"0", "direction":"down", "amount":1000}'
|
|
101
|
-
```
|
|
86
|
+
**Real keyboard input** — fills forms using CDP `Input.dispatchKeyEvent`, not JavaScript value injection. Works with React, Vue, Angular, and any framework-controlled inputs.
|
|
102
87
|
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
curl -X POST localhost:3456/eval -H 'Content-Type: application/json' \
|
|
106
|
-
-d '{"tab":"0", "expression":"document.title"}'
|
|
107
|
-
```
|
|
88
|
+
**Cross-origin iframe support** — target iframes by domain (`"tab": "stripe.com"`). CDP connects to them as separate targets, bypassing same-origin restrictions.
|
|
108
89
|
|
|
109
|
-
|
|
90
|
+
**SPA navigation** — handles single-page apps (YouTube, Gmail, Google Flights). Enter key submission, client-side routing, dynamic content — all work.
|
|
110
91
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
| `/click` | POST | Click by selector or text |
|
|
117
|
-
| `/scroll` | POST | Scroll with content preview |
|
|
118
|
-
| `/navigate` | POST | Go to URL, back, or forward (same tab) |
|
|
119
|
-
| `/eval` | POST | Run JavaScript in any tab or iframe |
|
|
120
|
-
| `/captcha` | POST | Detect captchas, basic interaction (experimental) |
|
|
121
|
-
| `/focus` | POST | Bring a tab to the front |
|
|
122
|
-
| `/tabs` | GET | List open tabs |
|
|
123
|
-
| `/health` | GET | Check Chrome connection |
|
|
124
|
-
|
|
125
|
-
Full API reference with response schemas: [API.md](./API.md)
|
|
92
|
+
**Captcha detection** — `/recon` automatically detects captcha iframes (Arkose, reCAPTCHA, hCaptcha) and flags them. `/captcha` endpoint provides basic interaction.
|
|
93
|
+
|
|
94
|
+
**Overlay detection** — modals, cookie banners, and blocking overlays are detected and reported so agents can dismiss them before interacting.
|
|
95
|
+
|
|
96
|
+
**Same-tab navigation** — links with `target="_blank"` are automatically opened in the same tab instead of spawning new ones.
|
|
126
97
|
|
|
127
98
|
## Tab Targeting
|
|
128
99
|
|
|
129
|
-
Every endpoint
|
|
100
|
+
Every endpoint accepts a `tab` field:
|
|
130
101
|
|
|
131
102
|
```json
|
|
132
103
|
{"tab": "0"} // by index
|
|
133
|
-
{"tab": "github"} //
|
|
134
|
-
{"tab": "
|
|
104
|
+
{"tab": "github"} // partial match on URL or title
|
|
105
|
+
{"tab": "stripe.com"} // matches cross-origin iframes too
|
|
135
106
|
```
|
|
136
107
|
|
|
137
|
-
##
|
|
138
|
-
|
|
139
|
-
The workflow is: **recon → act → read**.
|
|
108
|
+
## Commands
|
|
140
109
|
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
110
|
+
```bash
|
|
111
|
+
surfagent start # Start Chrome + API (one command)
|
|
112
|
+
surfagent chrome # Start Chrome debug session only
|
|
113
|
+
surfagent api # Start API only (Chrome must be running)
|
|
114
|
+
surfagent health # Check if everything is running
|
|
115
|
+
surfagent help # Show all options
|
|
147
116
|
```
|
|
148
117
|
|
|
149
|
-
Agents never need to guess selectors or parse screenshots. The recon response has everything.
|
|
150
|
-
|
|
151
118
|
## Tested On
|
|
152
119
|
|
|
153
|
-
|
|
154
|
-
- YouTube (SPA navigation, search, video selection)
|
|
155
|
-
- GitHub (login forms, repository pages)
|
|
156
|
-
- Supabase (dashboard navigation, SQL editor)
|
|
157
|
-
- Hacker News (link following, content reading)
|
|
158
|
-
- Reddit (thread navigation, comments)
|
|
159
|
-
- CodePen (cross-origin iframe interaction)
|
|
160
|
-
- Polymarket (market data extraction)
|
|
120
|
+
Google Flights, YouTube, GitHub, Supabase, Hacker News, Reddit, CodePen, Polymarket, npm — including autocomplete dropdowns, date pickers, complex forms, SPA navigation, cross-origin iframes, and captchas.
|
|
161
121
|
|
|
162
122
|
## Platform Support
|
|
163
123
|
|
|
@@ -173,6 +133,10 @@ Agents never need to guess selectors or parse screenshots. The recon response ha
|
|
|
173
133
|
- Chrome (any recent version)
|
|
174
134
|
- Node.js 18+
|
|
175
135
|
|
|
136
|
+
## Contributing
|
|
137
|
+
|
|
138
|
+
Issues and PRs welcome at [github.com/AllAboutAI-YT/surfagent](https://github.com/AllAboutAI-YT/surfagent).
|
|
139
|
+
|
|
176
140
|
## License
|
|
177
141
|
|
|
178
142
|
MIT
|
package/dist/api/server.js
CHANGED
|
@@ -16,6 +16,11 @@ function json(res, status, data) {
|
|
|
16
16
|
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
17
17
|
res.end(JSON.stringify(data));
|
|
18
18
|
}
|
|
19
|
+
function parseBody(raw) {
|
|
20
|
+
if (!raw || !raw.trim())
|
|
21
|
+
throw new SyntaxError('Empty request body');
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
19
24
|
function cors(res) {
|
|
20
25
|
res.writeHead(204, {
|
|
21
26
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -32,7 +37,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
32
37
|
try {
|
|
33
38
|
// POST /recon — full page reconnaissance
|
|
34
39
|
if (path === '/recon' && req.method === 'POST') {
|
|
35
|
-
const body =
|
|
40
|
+
const body = parseBody(await readBody(req));
|
|
36
41
|
if (!body.url && !body.tab) {
|
|
37
42
|
return json(res, 400, { error: 'Provide "url" (to open new page) or "tab" (to recon existing tab)' });
|
|
38
43
|
}
|
|
@@ -56,17 +61,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
56
61
|
}
|
|
57
62
|
// POST /fill — fill form fields via CDP keystrokes
|
|
58
63
|
if (path === '/fill' && req.method === 'POST') {
|
|
59
|
-
const body =
|
|
64
|
+
const body = parseBody(await readBody(req));
|
|
60
65
|
if (!body.tab || !body.fields) {
|
|
61
66
|
return json(res, 400, { error: 'Provide "tab" and "fields" [{ selector, value }]' });
|
|
62
67
|
}
|
|
68
|
+
if (!Array.isArray(body.fields)) {
|
|
69
|
+
return json(res, 400, { error: '"fields" must be an array of { selector, value }' });
|
|
70
|
+
}
|
|
63
71
|
const start = Date.now();
|
|
64
72
|
const result = await fillFields(body, { port: CDP_PORT, host: CDP_HOST });
|
|
65
73
|
return json(res, 200, { ...result, _fillMs: Date.now() - start });
|
|
66
74
|
}
|
|
67
75
|
// POST /click — click an element
|
|
68
76
|
if (path === '/click' && req.method === 'POST') {
|
|
69
|
-
const body =
|
|
77
|
+
const body = parseBody(await readBody(req));
|
|
70
78
|
if (!body.tab || (!body.selector && !body.text)) {
|
|
71
79
|
return json(res, 400, { error: 'Provide "tab" and "selector" or "text"' });
|
|
72
80
|
}
|
|
@@ -75,7 +83,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
75
83
|
}
|
|
76
84
|
// POST /scroll — scroll a page
|
|
77
85
|
if (path === '/scroll' && req.method === 'POST') {
|
|
78
|
-
const body =
|
|
86
|
+
const body = parseBody(await readBody(req));
|
|
79
87
|
if (!body.tab) {
|
|
80
88
|
return json(res, 400, { error: 'Provide "tab", optional "direction" (down/up), "amount" (pixels)' });
|
|
81
89
|
}
|
|
@@ -84,7 +92,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
84
92
|
}
|
|
85
93
|
// POST /captcha — detect and interact with captchas
|
|
86
94
|
if (path === '/captcha' && req.method === 'POST') {
|
|
87
|
-
const body =
|
|
95
|
+
const body = parseBody(await readBody(req));
|
|
88
96
|
if (!body.action) {
|
|
89
97
|
return json(res, 400, { error: 'Provide "action": detect, read, next, prev, submit, audio, restart' });
|
|
90
98
|
}
|
|
@@ -96,7 +104,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
96
104
|
}
|
|
97
105
|
// POST /read — get structured readable content from a page
|
|
98
106
|
if (path === '/read' && req.method === 'POST') {
|
|
99
|
-
const body =
|
|
107
|
+
const body = parseBody(await readBody(req));
|
|
100
108
|
if (!body.tab) {
|
|
101
109
|
return json(res, 400, { error: 'Provide "tab", optional "selector"' });
|
|
102
110
|
}
|
|
@@ -105,7 +113,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
105
113
|
}
|
|
106
114
|
// POST /focus — bring a tab to front
|
|
107
115
|
if (path === '/focus' && req.method === 'POST') {
|
|
108
|
-
const body =
|
|
116
|
+
const body = parseBody(await readBody(req));
|
|
109
117
|
if (!body.tab) {
|
|
110
118
|
return json(res, 400, { error: 'Provide "tab"' });
|
|
111
119
|
}
|
|
@@ -114,7 +122,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
114
122
|
}
|
|
115
123
|
// POST /eval — run JavaScript in a tab or iframe
|
|
116
124
|
if (path === '/eval' && req.method === 'POST') {
|
|
117
|
-
const body =
|
|
125
|
+
const body = parseBody(await readBody(req));
|
|
118
126
|
if (!body.tab || !body.expression) {
|
|
119
127
|
return json(res, 400, { error: 'Provide "tab" and "expression"' });
|
|
120
128
|
}
|
|
@@ -123,7 +131,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
123
131
|
}
|
|
124
132
|
// POST /navigate — go to url, back, or forward in same tab
|
|
125
133
|
if (path === '/navigate' && req.method === 'POST') {
|
|
126
|
-
const body =
|
|
134
|
+
const body = parseBody(await readBody(req));
|
|
127
135
|
if (!body.tab) {
|
|
128
136
|
return json(res, 400, { error: 'Provide "tab" and one of: "url", "back":true, "forward":true' });
|
|
129
137
|
}
|
|
@@ -150,6 +158,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
150
158
|
catch (error) {
|
|
151
159
|
const message = error instanceof Error ? error.message : String(error);
|
|
152
160
|
console.error(`[${new Date().toISOString()}] Error:`, message);
|
|
161
|
+
if (error instanceof SyntaxError) {
|
|
162
|
+
return json(res, 400, { error: 'Invalid JSON: ' + message });
|
|
163
|
+
}
|
|
164
|
+
if (message.includes('Tab not found')) {
|
|
165
|
+
return json(res, 404, { error: message });
|
|
166
|
+
}
|
|
167
|
+
if (message.includes('Cannot connect to Chrome') || message.includes('ECONNREFUSED')) {
|
|
168
|
+
return json(res, 503, { error: 'Chrome not running. Start with: surfagent start' });
|
|
169
|
+
}
|
|
153
170
|
json(res, 500, { error: message });
|
|
154
171
|
}
|
|
155
172
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surfagent",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Browser automation API for AI agents — structured page recon, form filling, clicking, and navigation via Chrome CDP",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agent",
|
|
7
|
+
"browser-automation",
|
|
8
|
+
"chrome-devtools",
|
|
9
|
+
"cdp",
|
|
10
|
+
"web-scraping",
|
|
11
|
+
"automation",
|
|
12
|
+
"langchain",
|
|
13
|
+
"crewai",
|
|
14
|
+
"openai",
|
|
15
|
+
"claude",
|
|
16
|
+
"agent",
|
|
17
|
+
"browser",
|
|
18
|
+
"recon",
|
|
19
|
+
"selenium-alternative",
|
|
20
|
+
"puppeteer-alternative"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/AllAboutAI-YT/surfagent"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
5
27
|
"main": "dist/api/server.js",
|
|
6
28
|
"type": "module",
|
|
7
29
|
"bin": {
|
package/src/api/server.ts
CHANGED
|
@@ -27,6 +27,11 @@ function json(res: http.ServerResponse, status: number, data: any) {
|
|
|
27
27
|
res.end(JSON.stringify(data));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function parseBody(raw: string): any {
|
|
31
|
+
if (!raw || !raw.trim()) throw new SyntaxError('Empty request body');
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
function cors(res: http.ServerResponse) {
|
|
31
36
|
res.writeHead(204, {
|
|
32
37
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -45,7 +50,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
45
50
|
try {
|
|
46
51
|
// POST /recon — full page reconnaissance
|
|
47
52
|
if (path === '/recon' && req.method === 'POST') {
|
|
48
|
-
const body: RequestBody =
|
|
53
|
+
const body: RequestBody = parseBody(await readBody(req));
|
|
49
54
|
|
|
50
55
|
if (!body.url && !body.tab) {
|
|
51
56
|
return json(res, 400, { error: 'Provide "url" (to open new page) or "tab" (to recon existing tab)' });
|
|
@@ -73,10 +78,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
73
78
|
|
|
74
79
|
// POST /fill — fill form fields via CDP keystrokes
|
|
75
80
|
if (path === '/fill' && req.method === 'POST') {
|
|
76
|
-
const body =
|
|
81
|
+
const body = parseBody(await readBody(req));
|
|
77
82
|
if (!body.tab || !body.fields) {
|
|
78
83
|
return json(res, 400, { error: 'Provide "tab" and "fields" [{ selector, value }]' });
|
|
79
84
|
}
|
|
85
|
+
if (!Array.isArray(body.fields)) {
|
|
86
|
+
return json(res, 400, { error: '"fields" must be an array of { selector, value }' });
|
|
87
|
+
}
|
|
80
88
|
const start = Date.now();
|
|
81
89
|
const result = await fillFields(body, { port: CDP_PORT, host: CDP_HOST });
|
|
82
90
|
return json(res, 200, { ...result, _fillMs: Date.now() - start });
|
|
@@ -84,7 +92,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
84
92
|
|
|
85
93
|
// POST /click — click an element
|
|
86
94
|
if (path === '/click' && req.method === 'POST') {
|
|
87
|
-
const body =
|
|
95
|
+
const body = parseBody(await readBody(req));
|
|
88
96
|
if (!body.tab || (!body.selector && !body.text)) {
|
|
89
97
|
return json(res, 400, { error: 'Provide "tab" and "selector" or "text"' });
|
|
90
98
|
}
|
|
@@ -94,7 +102,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
94
102
|
|
|
95
103
|
// POST /scroll — scroll a page
|
|
96
104
|
if (path === '/scroll' && req.method === 'POST') {
|
|
97
|
-
const body =
|
|
105
|
+
const body = parseBody(await readBody(req));
|
|
98
106
|
if (!body.tab) {
|
|
99
107
|
return json(res, 400, { error: 'Provide "tab", optional "direction" (down/up), "amount" (pixels)' });
|
|
100
108
|
}
|
|
@@ -104,7 +112,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
104
112
|
|
|
105
113
|
// POST /captcha — detect and interact with captchas
|
|
106
114
|
if (path === '/captcha' && req.method === 'POST') {
|
|
107
|
-
const body =
|
|
115
|
+
const body = parseBody(await readBody(req));
|
|
108
116
|
if (!body.action) {
|
|
109
117
|
return json(res, 400, { error: 'Provide "action": detect, read, next, prev, submit, audio, restart' });
|
|
110
118
|
}
|
|
@@ -117,7 +125,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
117
125
|
|
|
118
126
|
// POST /read — get structured readable content from a page
|
|
119
127
|
if (path === '/read' && req.method === 'POST') {
|
|
120
|
-
const body =
|
|
128
|
+
const body = parseBody(await readBody(req));
|
|
121
129
|
if (!body.tab) {
|
|
122
130
|
return json(res, 400, { error: 'Provide "tab", optional "selector"' });
|
|
123
131
|
}
|
|
@@ -127,7 +135,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
127
135
|
|
|
128
136
|
// POST /focus — bring a tab to front
|
|
129
137
|
if (path === '/focus' && req.method === 'POST') {
|
|
130
|
-
const body =
|
|
138
|
+
const body = parseBody(await readBody(req));
|
|
131
139
|
if (!body.tab) {
|
|
132
140
|
return json(res, 400, { error: 'Provide "tab"' });
|
|
133
141
|
}
|
|
@@ -137,7 +145,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
137
145
|
|
|
138
146
|
// POST /eval — run JavaScript in a tab or iframe
|
|
139
147
|
if (path === '/eval' && req.method === 'POST') {
|
|
140
|
-
const body =
|
|
148
|
+
const body = parseBody(await readBody(req));
|
|
141
149
|
if (!body.tab || !body.expression) {
|
|
142
150
|
return json(res, 400, { error: 'Provide "tab" and "expression"' });
|
|
143
151
|
}
|
|
@@ -147,7 +155,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
147
155
|
|
|
148
156
|
// POST /navigate — go to url, back, or forward in same tab
|
|
149
157
|
if (path === '/navigate' && req.method === 'POST') {
|
|
150
|
-
const body =
|
|
158
|
+
const body = parseBody(await readBody(req));
|
|
151
159
|
if (!body.tab) {
|
|
152
160
|
return json(res, 400, { error: 'Provide "tab" and one of: "url", "back":true, "forward":true' });
|
|
153
161
|
}
|
|
@@ -175,6 +183,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
175
183
|
} catch (error) {
|
|
176
184
|
const message = error instanceof Error ? error.message : String(error);
|
|
177
185
|
console.error(`[${new Date().toISOString()}] Error:`, message);
|
|
186
|
+
|
|
187
|
+
if (error instanceof SyntaxError) {
|
|
188
|
+
return json(res, 400, { error: 'Invalid JSON: ' + message });
|
|
189
|
+
}
|
|
190
|
+
if (message.includes('Tab not found')) {
|
|
191
|
+
return json(res, 404, { error: message });
|
|
192
|
+
}
|
|
193
|
+
if (message.includes('Cannot connect to Chrome') || message.includes('ECONNREFUSED')) {
|
|
194
|
+
return json(res, 503, { error: 'Chrome not running. Start with: surfagent start' });
|
|
195
|
+
}
|
|
178
196
|
json(res, 500, { error: message });
|
|
179
197
|
}
|
|
180
198
|
});
|