surfagent 1.0.0 → 1.0.1
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 +11 -12
- package/dist/api/act.d.ts +0 -11
- package/dist/api/act.js +0 -27
- package/dist/api/server.js +1 -16
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +202 -0
- package/package.json +6 -2
- package/src/cli.ts +220 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# surfagent
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
@@ -6,24 +6,23 @@ A local API that gives AI agents structured page data from your Chrome browser.
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
**1. Install**
|
|
10
9
|
```bash
|
|
11
|
-
npm install
|
|
12
|
-
|
|
10
|
+
npm install -g surfagent
|
|
11
|
+
surfagent start
|
|
13
12
|
```
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```
|
|
14
|
+
That's it. Chrome opens with debug mode, API starts on `http://localhost:3456`. Your agent can start calling it immediately.
|
|
15
|
+
|
|
16
|
+
### Other commands
|
|
19
17
|
|
|
20
|
-
**3. Start the API**
|
|
21
18
|
```bash
|
|
22
|
-
|
|
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
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
That's it. The API is running on `http://localhost:3456`.
|
|
26
|
-
|
|
27
26
|
## Your First Recon
|
|
28
27
|
|
|
29
28
|
Open any website in Chrome, then:
|
package/dist/api/act.d.ts
CHANGED
|
@@ -70,17 +70,6 @@ export declare function readPage(tabPattern: string, options: {
|
|
|
70
70
|
host?: string;
|
|
71
71
|
selector?: string;
|
|
72
72
|
}): Promise<any>;
|
|
73
|
-
export declare function screenshotTab(tabPattern: string, options: {
|
|
74
|
-
port?: number;
|
|
75
|
-
host?: string;
|
|
76
|
-
selector?: string;
|
|
77
|
-
clip?: {
|
|
78
|
-
x: number;
|
|
79
|
-
y: number;
|
|
80
|
-
width: number;
|
|
81
|
-
height: number;
|
|
82
|
-
};
|
|
83
|
-
}): Promise<string>;
|
|
84
73
|
export interface CaptchaRequest {
|
|
85
74
|
tab: string;
|
|
86
75
|
action: 'detect' | 'read' | 'next' | 'prev' | 'submit' | 'audio' | 'restart';
|
package/dist/api/act.js
CHANGED
|
@@ -357,33 +357,6 @@ export async function readPage(tabPattern, options) {
|
|
|
357
357
|
throw error;
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
export async function screenshotTab(tabPattern, options) {
|
|
361
|
-
const port = options.port || 9222;
|
|
362
|
-
const host = options.host || 'localhost';
|
|
363
|
-
const tab = await resolveTab(tabPattern, port, host);
|
|
364
|
-
const client = await connectToTab(tab.id, port, host);
|
|
365
|
-
try {
|
|
366
|
-
// If selector provided, get its bounding rect and use as clip
|
|
367
|
-
let clip = options.clip;
|
|
368
|
-
if (options.selector && !clip) {
|
|
369
|
-
const r = await client.Runtime.evaluate({
|
|
370
|
-
expression: `(function(){ const el = document.querySelector(${JSON.stringify(options.selector)}); if(!el) return null; const r = el.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, scale: 1 } })()`,
|
|
371
|
-
returnByValue: true
|
|
372
|
-
});
|
|
373
|
-
clip = r.result.value;
|
|
374
|
-
}
|
|
375
|
-
const params = { format: 'png', fromSurface: true };
|
|
376
|
-
if (clip)
|
|
377
|
-
params.clip = { ...clip, scale: 2 }; // 2x for sharper zoom
|
|
378
|
-
const result = await client.Page.captureScreenshot(params);
|
|
379
|
-
await client.close();
|
|
380
|
-
return result.data; // base64 PNG
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
await client.close();
|
|
384
|
-
throw error;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
360
|
const CAPTCHA_DETECT_SCRIPT = `
|
|
388
361
|
(function() {
|
|
389
362
|
// 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
|
|
4
|
+
import { fillFields, clickElement, scrollPage, navigatePage, evalInTab, focusTab, readPage, captchaInteract } 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);
|
|
@@ -82,21 +82,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
82
82
|
const result = await scrollPage(body, { port: CDP_PORT, host: CDP_HOST });
|
|
83
83
|
return json(res, 200, result);
|
|
84
84
|
}
|
|
85
|
-
// POST /screenshot — capture full page, element, or region at 2x resolution
|
|
86
|
-
if (path === '/screenshot' && req.method === 'POST') {
|
|
87
|
-
const body = JSON.parse(await readBody(req));
|
|
88
|
-
if (!body.tab) {
|
|
89
|
-
return json(res, 400, { error: 'Provide "tab", optional "selector" or "clip" {x,y,width,height}' });
|
|
90
|
-
}
|
|
91
|
-
const data = await screenshotTab(body.tab, { port: CDP_PORT, host: CDP_HOST, selector: body.selector, clip: body.clip });
|
|
92
|
-
// Return as base64 or save to file
|
|
93
|
-
if (body.output) {
|
|
94
|
-
const fs = await import('node:fs');
|
|
95
|
-
fs.writeFileSync(body.output, Buffer.from(data, 'base64'));
|
|
96
|
-
return json(res, 200, { success: true, output: body.output });
|
|
97
|
-
}
|
|
98
|
-
return json(res, 200, { data });
|
|
99
|
-
}
|
|
100
85
|
// POST /captcha — detect and interact with captchas
|
|
101
86
|
if (path === '/captcha' && req.method === 'POST') {
|
|
102
87
|
const body = JSON.parse(await readBody(req));
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawn } from 'node:child_process';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const CDP_PORT = parseInt(process.env.CDP_PORT || '9222', 10);
|
|
8
|
+
const API_PORT = parseInt(process.env.API_PORT || '3456', 10);
|
|
9
|
+
function log(msg) {
|
|
10
|
+
console.log(`[surfagent] ${msg}`);
|
|
11
|
+
}
|
|
12
|
+
function checkCDP() {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const req = http.get(`http://localhost:${CDP_PORT}/json/version`, (res) => {
|
|
15
|
+
resolve(res.statusCode === 200);
|
|
16
|
+
});
|
|
17
|
+
req.on('error', () => resolve(false));
|
|
18
|
+
req.setTimeout(1000, () => { req.destroy(); resolve(false); });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function detectOS() {
|
|
22
|
+
const platform = process.platform;
|
|
23
|
+
if (platform === 'darwin')
|
|
24
|
+
return 'mac';
|
|
25
|
+
if (platform === 'win32')
|
|
26
|
+
return 'windows';
|
|
27
|
+
return 'linux';
|
|
28
|
+
}
|
|
29
|
+
function getChromePath() {
|
|
30
|
+
const os = detectOS();
|
|
31
|
+
const paths = {
|
|
32
|
+
mac: [
|
|
33
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
34
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
35
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
36
|
+
],
|
|
37
|
+
linux: [
|
|
38
|
+
'/usr/bin/google-chrome',
|
|
39
|
+
'/usr/bin/google-chrome-stable',
|
|
40
|
+
'/usr/bin/chromium-browser',
|
|
41
|
+
'/usr/bin/chromium',
|
|
42
|
+
'/snap/bin/chromium',
|
|
43
|
+
],
|
|
44
|
+
windows: [
|
|
45
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
46
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
47
|
+
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
for (const p of paths[os] || []) {
|
|
51
|
+
try {
|
|
52
|
+
execSync(`test -f "${p}"`, { stdio: 'ignore' });
|
|
53
|
+
return p;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function startChrome(chromePath) {
|
|
62
|
+
const userDataDir = process.env.CHROME_USER_DATA_DIR || '/tmp/surfagent-chrome';
|
|
63
|
+
// Copy cookies from default Chrome profile if available and dir is fresh
|
|
64
|
+
const os = detectOS();
|
|
65
|
+
try {
|
|
66
|
+
execSync(`mkdir -p "${userDataDir}/Default"`, { stdio: 'ignore' });
|
|
67
|
+
if (os === 'mac') {
|
|
68
|
+
const defaultProfile = `${process.env.HOME}/Library/Application Support/Google/Chrome/Default`;
|
|
69
|
+
execSync(`cp "${defaultProfile}/Cookies" "${userDataDir}/Default/" 2>/dev/null || true`, { stdio: 'ignore' });
|
|
70
|
+
}
|
|
71
|
+
else if (os === 'linux') {
|
|
72
|
+
const defaultProfile = `${process.env.HOME}/.config/google-chrome/Default`;
|
|
73
|
+
execSync(`cp "${defaultProfile}/Cookies" "${userDataDir}/Default/" 2>/dev/null || true`, { stdio: 'ignore' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
const args = [
|
|
78
|
+
`--user-data-dir=${userDataDir}`,
|
|
79
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
80
|
+
'--disable-save-password-bubble',
|
|
81
|
+
'--disable-popup-blocking',
|
|
82
|
+
'--disable-notifications',
|
|
83
|
+
'--disable-infobars',
|
|
84
|
+
'--disable-translate',
|
|
85
|
+
'--disable-features=PasswordManager,AutofillSaveCardBubble,TranslateUI',
|
|
86
|
+
'--password-store=basic',
|
|
87
|
+
];
|
|
88
|
+
const chrome = spawn(chromePath, args, {
|
|
89
|
+
detached: true,
|
|
90
|
+
stdio: 'ignore',
|
|
91
|
+
});
|
|
92
|
+
chrome.unref();
|
|
93
|
+
log(`Chrome started (pid ${chrome.pid}) on port ${CDP_PORT}`);
|
|
94
|
+
}
|
|
95
|
+
async function waitForCDP(maxWait = 10000) {
|
|
96
|
+
const start = Date.now();
|
|
97
|
+
while (Date.now() - start < maxWait) {
|
|
98
|
+
if (await checkCDP())
|
|
99
|
+
return true;
|
|
100
|
+
await new Promise(r => setTimeout(r, 500));
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
async function main() {
|
|
105
|
+
const command = process.argv[2];
|
|
106
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
107
|
+
console.log(`
|
|
108
|
+
surfagent — Browser Recon API for AI agents
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
surfagent start Start Chrome + API server
|
|
112
|
+
surfagent api Start API only (Chrome must be running)
|
|
113
|
+
surfagent chrome Start Chrome debug session only
|
|
114
|
+
surfagent health Check if everything is running
|
|
115
|
+
surfagent help Show this message
|
|
116
|
+
|
|
117
|
+
Environment variables:
|
|
118
|
+
CDP_PORT Chrome debug port (default: 9222)
|
|
119
|
+
API_PORT API server port (default: 3456)
|
|
120
|
+
CHROME_USER_DATA_DIR Chrome profile directory (default: /tmp/surfagent-chrome)
|
|
121
|
+
|
|
122
|
+
After starting, your AI agent can call http://localhost:3456
|
|
123
|
+
Full API docs: https://github.com/AllAboutAI-YT/webpilot#readme
|
|
124
|
+
`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (command === 'health') {
|
|
128
|
+
const cdp = await checkCDP();
|
|
129
|
+
console.log(`Chrome CDP (port ${CDP_PORT}): ${cdp ? 'connected' : 'not running'}`);
|
|
130
|
+
if (cdp) {
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(`http://localhost:${API_PORT}/health`);
|
|
133
|
+
const data = await res.json();
|
|
134
|
+
console.log(`API (port ${API_PORT}): ${data.status} — ${data.tabCount} tabs`);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
console.log(`API (port ${API_PORT}): not running`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (command === 'chrome') {
|
|
143
|
+
const cdpRunning = await checkCDP();
|
|
144
|
+
if (cdpRunning) {
|
|
145
|
+
log(`Chrome already running on port ${CDP_PORT}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const chromePath = getChromePath();
|
|
149
|
+
if (!chromePath) {
|
|
150
|
+
console.error('[surfagent] Chrome not found. Install Google Chrome and try again.');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
startChrome(chromePath);
|
|
154
|
+
const connected = await waitForCDP();
|
|
155
|
+
if (!connected) {
|
|
156
|
+
console.error('[surfagent] Chrome started but CDP not responding. Check port ' + CDP_PORT);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
log('Chrome ready');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (command === 'api') {
|
|
163
|
+
const cdpRunning = await checkCDP();
|
|
164
|
+
if (!cdpRunning) {
|
|
165
|
+
console.error(`[surfagent] Chrome not running on port ${CDP_PORT}. Run: surfagent chrome`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
await import('./api/server.js');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (command === 'start' || !command) {
|
|
172
|
+
log('Starting...');
|
|
173
|
+
// 1. Check/start Chrome
|
|
174
|
+
let cdpRunning = await checkCDP();
|
|
175
|
+
if (cdpRunning) {
|
|
176
|
+
log(`Chrome already running on port ${CDP_PORT}`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const chromePath = getChromePath();
|
|
180
|
+
if (!chromePath) {
|
|
181
|
+
console.error('[surfagent] Chrome not found. Install Google Chrome and try again.');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
startChrome(chromePath);
|
|
185
|
+
cdpRunning = await waitForCDP();
|
|
186
|
+
if (!cdpRunning) {
|
|
187
|
+
console.error('[surfagent] Chrome failed to start. Try running it manually with --remote-debugging-port=9222');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
log('Chrome ready');
|
|
191
|
+
}
|
|
192
|
+
// 2. Start API
|
|
193
|
+
await import('./api/server.js');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
console.error(`Unknown command: ${command}. Run: surfagent help`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
main().catch((err) => {
|
|
200
|
+
console.error('[surfagent]', err.message);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surfagent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Local API that gives AI agents structured page data from Chrome for fast, precise web navigation",
|
|
5
5
|
"main": "dist/api/server.js",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"surfagent": "dist/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"scripts": {
|
|
8
11
|
"build": "tsc",
|
|
9
12
|
"dev": "tsc --watch",
|
|
10
|
-
"api": "node dist/api/server.js"
|
|
13
|
+
"api": "node dist/api/server.js",
|
|
14
|
+
"start": "node dist/cli.js start"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
17
|
"chrome-remote-interface": "^0.33.0",
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync, spawn } from 'node:child_process';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const CDP_PORT = parseInt(process.env.CDP_PORT || '9222', 10);
|
|
10
|
+
const API_PORT = parseInt(process.env.API_PORT || '3456', 10);
|
|
11
|
+
|
|
12
|
+
function log(msg: string) {
|
|
13
|
+
console.log(`[surfagent] ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function checkCDP(): Promise<boolean> {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const req = http.get(`http://localhost:${CDP_PORT}/json/version`, (res) => {
|
|
19
|
+
resolve(res.statusCode === 200);
|
|
20
|
+
});
|
|
21
|
+
req.on('error', () => resolve(false));
|
|
22
|
+
req.setTimeout(1000, () => { req.destroy(); resolve(false); });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function detectOS(): 'mac' | 'linux' | 'windows' {
|
|
27
|
+
const platform = process.platform;
|
|
28
|
+
if (platform === 'darwin') return 'mac';
|
|
29
|
+
if (platform === 'win32') return 'windows';
|
|
30
|
+
return 'linux';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getChromePath(): string | null {
|
|
34
|
+
const os = detectOS();
|
|
35
|
+
const paths: Record<string, string[]> = {
|
|
36
|
+
mac: [
|
|
37
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
38
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
39
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
40
|
+
],
|
|
41
|
+
linux: [
|
|
42
|
+
'/usr/bin/google-chrome',
|
|
43
|
+
'/usr/bin/google-chrome-stable',
|
|
44
|
+
'/usr/bin/chromium-browser',
|
|
45
|
+
'/usr/bin/chromium',
|
|
46
|
+
'/snap/bin/chromium',
|
|
47
|
+
],
|
|
48
|
+
windows: [
|
|
49
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
50
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
51
|
+
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const p of paths[os] || []) {
|
|
56
|
+
try {
|
|
57
|
+
execSync(`test -f "${p}"`, { stdio: 'ignore' });
|
|
58
|
+
return p;
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function startChrome(chromePath: string) {
|
|
67
|
+
const userDataDir = process.env.CHROME_USER_DATA_DIR || '/tmp/surfagent-chrome';
|
|
68
|
+
|
|
69
|
+
// Copy cookies from default Chrome profile if available and dir is fresh
|
|
70
|
+
const os = detectOS();
|
|
71
|
+
try {
|
|
72
|
+
execSync(`mkdir -p "${userDataDir}/Default"`, { stdio: 'ignore' });
|
|
73
|
+
|
|
74
|
+
if (os === 'mac') {
|
|
75
|
+
const defaultProfile = `${process.env.HOME}/Library/Application Support/Google/Chrome/Default`;
|
|
76
|
+
execSync(`cp "${defaultProfile}/Cookies" "${userDataDir}/Default/" 2>/dev/null || true`, { stdio: 'ignore' });
|
|
77
|
+
} else if (os === 'linux') {
|
|
78
|
+
const defaultProfile = `${process.env.HOME}/.config/google-chrome/Default`;
|
|
79
|
+
execSync(`cp "${defaultProfile}/Cookies" "${userDataDir}/Default/" 2>/dev/null || true`, { stdio: 'ignore' });
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
|
|
83
|
+
const args = [
|
|
84
|
+
`--user-data-dir=${userDataDir}`,
|
|
85
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
86
|
+
'--disable-save-password-bubble',
|
|
87
|
+
'--disable-popup-blocking',
|
|
88
|
+
'--disable-notifications',
|
|
89
|
+
'--disable-infobars',
|
|
90
|
+
'--disable-translate',
|
|
91
|
+
'--disable-features=PasswordManager,AutofillSaveCardBubble,TranslateUI',
|
|
92
|
+
'--password-store=basic',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const chrome = spawn(chromePath, args, {
|
|
96
|
+
detached: true,
|
|
97
|
+
stdio: 'ignore',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
chrome.unref();
|
|
101
|
+
log(`Chrome started (pid ${chrome.pid}) on port ${CDP_PORT}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function waitForCDP(maxWait = 10000): Promise<boolean> {
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
while (Date.now() - start < maxWait) {
|
|
107
|
+
if (await checkCDP()) return true;
|
|
108
|
+
await new Promise(r => setTimeout(r, 500));
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main() {
|
|
114
|
+
const command = process.argv[2];
|
|
115
|
+
|
|
116
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
117
|
+
console.log(`
|
|
118
|
+
surfagent — Browser Recon API for AI agents
|
|
119
|
+
|
|
120
|
+
Usage:
|
|
121
|
+
surfagent start Start Chrome + API server
|
|
122
|
+
surfagent api Start API only (Chrome must be running)
|
|
123
|
+
surfagent chrome Start Chrome debug session only
|
|
124
|
+
surfagent health Check if everything is running
|
|
125
|
+
surfagent help Show this message
|
|
126
|
+
|
|
127
|
+
Environment variables:
|
|
128
|
+
CDP_PORT Chrome debug port (default: 9222)
|
|
129
|
+
API_PORT API server port (default: 3456)
|
|
130
|
+
CHROME_USER_DATA_DIR Chrome profile directory (default: /tmp/surfagent-chrome)
|
|
131
|
+
|
|
132
|
+
After starting, your AI agent can call http://localhost:3456
|
|
133
|
+
Full API docs: https://github.com/AllAboutAI-YT/webpilot#readme
|
|
134
|
+
`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (command === 'health') {
|
|
139
|
+
const cdp = await checkCDP();
|
|
140
|
+
console.log(`Chrome CDP (port ${CDP_PORT}): ${cdp ? 'connected' : 'not running'}`);
|
|
141
|
+
if (cdp) {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(`http://localhost:${API_PORT}/health`);
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
console.log(`API (port ${API_PORT}): ${data.status} — ${data.tabCount} tabs`);
|
|
146
|
+
} catch {
|
|
147
|
+
console.log(`API (port ${API_PORT}): not running`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (command === 'chrome') {
|
|
154
|
+
const cdpRunning = await checkCDP();
|
|
155
|
+
if (cdpRunning) {
|
|
156
|
+
log(`Chrome already running on port ${CDP_PORT}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const chromePath = getChromePath();
|
|
161
|
+
if (!chromePath) {
|
|
162
|
+
console.error('[surfagent] Chrome not found. Install Google Chrome and try again.');
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
startChrome(chromePath);
|
|
167
|
+
const connected = await waitForCDP();
|
|
168
|
+
if (!connected) {
|
|
169
|
+
console.error('[surfagent] Chrome started but CDP not responding. Check port ' + CDP_PORT);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
log('Chrome ready');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (command === 'api') {
|
|
177
|
+
const cdpRunning = await checkCDP();
|
|
178
|
+
if (!cdpRunning) {
|
|
179
|
+
console.error(`[surfagent] Chrome not running on port ${CDP_PORT}. Run: surfagent chrome`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
await import('./api/server.js');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (command === 'start' || !command) {
|
|
187
|
+
log('Starting...');
|
|
188
|
+
|
|
189
|
+
// 1. Check/start Chrome
|
|
190
|
+
let cdpRunning = await checkCDP();
|
|
191
|
+
if (cdpRunning) {
|
|
192
|
+
log(`Chrome already running on port ${CDP_PORT}`);
|
|
193
|
+
} else {
|
|
194
|
+
const chromePath = getChromePath();
|
|
195
|
+
if (!chromePath) {
|
|
196
|
+
console.error('[surfagent] Chrome not found. Install Google Chrome and try again.');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
startChrome(chromePath);
|
|
200
|
+
cdpRunning = await waitForCDP();
|
|
201
|
+
if (!cdpRunning) {
|
|
202
|
+
console.error('[surfagent] Chrome failed to start. Try running it manually with --remote-debugging-port=9222');
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
log('Chrome ready');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 2. Start API
|
|
209
|
+
await import('./api/server.js');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.error(`Unknown command: ${command}. Run: surfagent help`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch((err) => {
|
|
218
|
+
console.error('[surfagent]', err.message);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
});
|