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 CHANGED
@@ -1,8 +1,17 @@
1
1
  # surfagent
2
2
 
3
- A local API that gives AI agents structured page data from your Chrome browser. Instead of guessing selectors or taking screenshots, your agent gets a complete map of every element, form, and link on the page then acts on it precisely.
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
- **One recon call replaces dozens of trial-and-error clicks.**
5
+ `npm install -g surfagent` two commands to give your agent a browser.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/surfagent.svg)](https://www.npmjs.com/package/surfagent)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
- That's it. A **new Chrome window** opens with debug mode — your personal Chrome is not affected. The API starts on `http://localhost:3456` and your agent can start calling it immediately.
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
- ### Other commands
25
+ ## Why surfagent?
17
26
 
18
- ```bash
19
- surfagent start # Start Chrome + API (one command)
20
- surfagent chrome # Start Chrome debug session only
21
- surfagent api # Start API only (Chrome must be running)
22
- surfagent health # Check if everything is running
23
- surfagent help # Show all options
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
- ## Your First Recon
35
+ ## How Agents Use It
27
36
 
28
- Open any website in Chrome, then:
37
+ The workflow is: **recon act → read**.
29
38
 
30
- ```bash
31
- curl -s -X POST localhost:3456/recon -d '{"tab":"0"}' -H 'Content-Type: application/json'
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
- You get back:
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
- # Get full page structure
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
- # Open a URL and map it
51
- curl -X POST localhost:3456/recon -H 'Content-Type: application/json' \
52
- -d '{"url":"https://example.com", "keepTab": true}'
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
- ### Read page content
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
- ### Fill forms
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
- ### Click elements
76
- ```bash
77
- # By text
78
- curl -X POST localhost:3456/click -H 'Content-Type: application/json' \
79
- -d '{"tab":"0", "text":"Sign In"}'
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
- # By selector (from recon)
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
- ### Navigate
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
- # Go back
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
- ### Scroll
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
- ### Run JavaScript
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
- ## All Endpoints
90
+ **SPA navigation** — handles single-page apps (YouTube, Gmail, Google Flights). Enter key submission, client-side routing, dynamic content — all work.
110
91
 
111
- | Endpoint | Method | What it does |
112
- |---|---|---|
113
- | `/recon` | POST | Full page map elements, forms, selectors, metadata |
114
- | `/read` | POST | Structured page content — headings, tables, notifications |
115
- | `/fill` | POST | Fill form fields with real keystrokes |
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 takes a `tab` field. You can target tabs three ways:
100
+ Every endpoint accepts a `tab` field:
130
101
 
131
102
  ```json
132
103
  {"tab": "0"} // by index
133
- {"tab": "github"} // by URL or title (partial match)
134
- {"tab": "cdpn.io"} // cross-origin iframes work too
104
+ {"tab": "github"} // partial match on URL or title
105
+ {"tab": "stripe.com"} // matches cross-origin iframes too
135
106
  ```
136
107
 
137
- ## How Agents Should Use This
138
-
139
- The workflow is: **recon → act → read**.
108
+ ## Commands
140
109
 
141
- ```
142
- 1. /recon → get the page map (selectors, forms, elements)
143
- 2. /click → click something using a selector from step 1
144
- /fill → fill a form using selectors from step 1
145
- 3. /read check what happened (success message? error? new content?)
146
- 4. /recon → if the page changed, map it again
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
- - Google Flights (autocomplete dropdowns, date pickers, complex forms)
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
@@ -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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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",
4
- "description": "Local API that gives AI agents structured page data from Chrome for fast, precise web navigation",
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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 = JSON.parse(await readBody(req));
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
  });