cdp-skill 1.0.2 → 1.0.4
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 +3 -0
- package/SKILL.md +34 -5
- package/package.json +2 -1
- package/src/capture/console-capture.js +241 -0
- package/src/capture/debug-capture.js +144 -0
- package/src/capture/error-aggregator.js +151 -0
- package/src/capture/eval-serializer.js +320 -0
- package/src/capture/index.js +40 -0
- package/src/capture/network-capture.js +211 -0
- package/src/capture/pdf-capture.js +256 -0
- package/src/capture/screenshot-capture.js +325 -0
- package/src/cdp/browser.js +569 -0
- package/src/cdp/connection.js +369 -0
- package/src/cdp/discovery.js +138 -0
- package/src/cdp/index.js +29 -0
- package/src/cdp/target-and-session.js +439 -0
- package/src/cdp-skill.js +25 -11
- package/src/constants.js +79 -0
- package/src/dom/actionability.js +638 -0
- package/src/dom/click-executor.js +923 -0
- package/src/dom/element-handle.js +496 -0
- package/src/dom/element-locator.js +475 -0
- package/src/dom/element-validator.js +120 -0
- package/src/dom/fill-executor.js +489 -0
- package/src/dom/index.js +248 -0
- package/src/dom/input-emulator.js +406 -0
- package/src/dom/keyboard-executor.js +202 -0
- package/src/dom/quad-helpers.js +89 -0
- package/src/dom/react-filler.js +94 -0
- package/src/dom/wait-executor.js +423 -0
- package/src/index.js +6 -6
- package/src/page/cookie-manager.js +202 -0
- package/src/page/dom-stability.js +181 -0
- package/src/page/index.js +36 -0
- package/src/{page.js → page/page-controller.js} +109 -839
- package/src/page/wait-utilities.js +302 -0
- package/src/page/web-storage-manager.js +108 -0
- package/src/runner/context-helpers.js +224 -0
- package/src/runner/execute-browser.js +518 -0
- package/src/runner/execute-form.js +315 -0
- package/src/runner/execute-input.js +308 -0
- package/src/runner/execute-interaction.js +672 -0
- package/src/runner/execute-navigation.js +180 -0
- package/src/runner/execute-query.js +771 -0
- package/src/runner/index.js +51 -0
- package/src/runner/step-executors.js +421 -0
- package/src/runner/step-validator.js +641 -0
- package/src/tests/Actionability.test.js +613 -0
- package/src/tests/BrowserClient.test.js +1 -1
- package/src/tests/ChromeDiscovery.test.js +1 -1
- package/src/tests/ClickExecutor.test.js +554 -0
- package/src/tests/ConsoleCapture.test.js +1 -1
- package/src/tests/ContextHelpers.test.js +453 -0
- package/src/tests/CookieManager.test.js +450 -0
- package/src/tests/DebugCapture.test.js +307 -0
- package/src/tests/ElementHandle.test.js +1 -1
- package/src/tests/ElementLocator.test.js +1 -1
- package/src/tests/ErrorAggregator.test.js +1 -1
- package/src/tests/EvalSerializer.test.js +391 -0
- package/src/tests/FillExecutor.test.js +611 -0
- package/src/tests/InputEmulator.test.js +1 -1
- package/src/tests/KeyboardExecutor.test.js +430 -0
- package/src/tests/NetworkErrorCapture.test.js +1 -1
- package/src/tests/PageController.test.js +1 -1
- package/src/tests/PdfCapture.test.js +333 -0
- package/src/tests/ScreenshotCapture.test.js +1 -1
- package/src/tests/SessionRegistry.test.js +1 -1
- package/src/tests/StepValidator.test.js +527 -0
- package/src/tests/TargetManager.test.js +1 -1
- package/src/tests/TestRunner.test.js +1 -1
- package/src/tests/WaitStrategy.test.js +1 -1
- package/src/tests/WaitUtilities.test.js +508 -0
- package/src/tests/WebStorageManager.test.js +333 -0
- package/src/types.js +309 -0
- package/src/capture.js +0 -1400
- package/src/cdp.js +0 -1286
- package/src/dom.js +0 -4379
- package/src/runner.js +0 -3676
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Module
|
|
3
|
+
* High-level browser client and Chrome launcher
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createBrowser(options?) - Factory for browser client
|
|
7
|
+
* - findChromePath() - Find Chrome executable
|
|
8
|
+
* - launchChrome(options?) - Launch Chrome with CDP
|
|
9
|
+
* - getChromeStatus(options?) - Check/launch Chrome
|
|
10
|
+
* - isChromeProcessRunning() - Check if Chrome process exists
|
|
11
|
+
* - createNewTab(host?, port?, url?) - Create new tab via HTTP
|
|
12
|
+
*
|
|
13
|
+
* @module cdp-skill/cdp/browser
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn, execSync } from 'child_process';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import { timeoutError, sleep } from '../utils.js';
|
|
21
|
+
import { createConnection } from './connection.js';
|
|
22
|
+
import { createDiscovery } from './discovery.js';
|
|
23
|
+
import { createTargetManager, createSessionRegistry, createPageSession } from './target-and-session.js';
|
|
24
|
+
|
|
25
|
+
// Chrome executable paths by platform
|
|
26
|
+
const CHROME_PATHS = {
|
|
27
|
+
darwin: [
|
|
28
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
29
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
30
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
|
|
31
|
+
],
|
|
32
|
+
linux: [
|
|
33
|
+
'google-chrome',
|
|
34
|
+
'google-chrome-stable',
|
|
35
|
+
'chromium-browser',
|
|
36
|
+
'chromium',
|
|
37
|
+
'/usr/bin/google-chrome',
|
|
38
|
+
'/usr/bin/chromium-browser',
|
|
39
|
+
'/usr/bin/chromium',
|
|
40
|
+
'/snap/bin/chromium'
|
|
41
|
+
],
|
|
42
|
+
win32: [
|
|
43
|
+
process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
44
|
+
process.env.PROGRAMFILES + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
45
|
+
process.env['PROGRAMFILES(X86)'] + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
46
|
+
process.env.LOCALAPPDATA + '\\Chromium\\Application\\chrome.exe'
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find Chrome executable on the system
|
|
52
|
+
* @returns {string|null} Path to Chrome executable or null if not found
|
|
53
|
+
*/
|
|
54
|
+
export function findChromePath() {
|
|
55
|
+
// Check environment variable first
|
|
56
|
+
if (process.env.CHROME_PATH) {
|
|
57
|
+
if (fs.existsSync(process.env.CHROME_PATH)) {
|
|
58
|
+
return process.env.CHROME_PATH;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const platform = os.platform();
|
|
63
|
+
const paths = CHROME_PATHS[platform] || [];
|
|
64
|
+
|
|
65
|
+
for (const chromePath of paths) {
|
|
66
|
+
try {
|
|
67
|
+
// For Linux, check if command exists in PATH
|
|
68
|
+
if (platform === 'linux' && !chromePath.startsWith('/')) {
|
|
69
|
+
try {
|
|
70
|
+
const result = execSync(`which ${chromePath}`, { encoding: 'utf8' }).trim();
|
|
71
|
+
if (result) return result;
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
} else if (fs.existsSync(chromePath)) {
|
|
76
|
+
return chromePath;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if Chrome process is running (without necessarily having CDP enabled)
|
|
88
|
+
* On macOS, Chrome can run without windows and without CDP port
|
|
89
|
+
* @returns {{running: boolean, hasCdpPort: boolean, pid: number|null}}
|
|
90
|
+
*/
|
|
91
|
+
export function isChromeProcessRunning() {
|
|
92
|
+
const platform = os.platform();
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (platform === 'darwin') {
|
|
96
|
+
// macOS: Check for Chrome process
|
|
97
|
+
const result = execSync('pgrep -x "Google Chrome" 2>/dev/null || pgrep -f "Google Chrome.app" 2>/dev/null', {
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
timeout: 5000
|
|
100
|
+
}).trim();
|
|
101
|
+
if (result) {
|
|
102
|
+
const pids = result.split('\n').filter(p => p);
|
|
103
|
+
// Check if any Chrome process has --remote-debugging-port
|
|
104
|
+
try {
|
|
105
|
+
const psResult = execSync(`ps aux | grep -E "Google Chrome.*--remote-debugging-port" | grep -v grep`, {
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
timeout: 5000
|
|
108
|
+
}).trim();
|
|
109
|
+
return { running: true, hasCdpPort: psResult.length > 0, pid: parseInt(pids[0]) };
|
|
110
|
+
} catch {
|
|
111
|
+
return { running: true, hasCdpPort: false, pid: parseInt(pids[0]) };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else if (platform === 'linux') {
|
|
115
|
+
const result = execSync('pgrep -f "(chrome|chromium)" 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
116
|
+
if (result) {
|
|
117
|
+
const pids = result.split('\n').filter(p => p);
|
|
118
|
+
try {
|
|
119
|
+
const psResult = execSync(`ps aux | grep -E "(chrome|chromium).*--remote-debugging-port" | grep -v grep`, {
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
timeout: 5000
|
|
122
|
+
}).trim();
|
|
123
|
+
return { running: true, hasCdpPort: psResult.length > 0, pid: parseInt(pids[0]) };
|
|
124
|
+
} catch {
|
|
125
|
+
return { running: true, hasCdpPort: false, pid: parseInt(pids[0]) };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (platform === 'win32') {
|
|
129
|
+
const result = execSync('tasklist /FI "IMAGENAME eq chrome.exe" /NH', { encoding: 'utf8', timeout: 5000 });
|
|
130
|
+
if (result.includes('chrome.exe')) {
|
|
131
|
+
// Windows: harder to check command line args, assume CDP might be available
|
|
132
|
+
return { running: true, hasCdpPort: true, pid: null };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Process check failed, assume not running
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { running: false, hasCdpPort: false, pid: null };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a new tab in Chrome via CDP HTTP endpoint
|
|
144
|
+
* @param {string} [host='localhost'] - Chrome debugging host
|
|
145
|
+
* @param {number} [port=9222] - Chrome debugging port
|
|
146
|
+
* @param {string} [url='about:blank'] - URL to open
|
|
147
|
+
* @returns {Promise<{targetId: string, url: string}>}
|
|
148
|
+
*/
|
|
149
|
+
export async function createNewTab(host = 'localhost', port = 9222, url = 'about:blank') {
|
|
150
|
+
const response = await fetch(`http://${host}:${port}/json/new?${encodeURIComponent(url)}`);
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Failed to create new tab: ${response.statusText}`);
|
|
153
|
+
}
|
|
154
|
+
const target = await response.json();
|
|
155
|
+
return {
|
|
156
|
+
targetId: target.id,
|
|
157
|
+
url: target.url
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Launch Chrome with remote debugging enabled
|
|
163
|
+
*
|
|
164
|
+
* IMPORTANT: On macOS/Linux, if Chrome is already running without CDP,
|
|
165
|
+
* starting Chrome with --remote-debugging-port will just open a new window
|
|
166
|
+
* in the existing Chrome (which ignores the flag). To solve this, we
|
|
167
|
+
* automatically use a separate user data directory when Chrome is already running.
|
|
168
|
+
*
|
|
169
|
+
* @param {Object} [options] - Launch options
|
|
170
|
+
* @param {number} [options.port=9222] - Debugging port
|
|
171
|
+
* @param {string} [options.chromePath] - Custom Chrome path
|
|
172
|
+
* @param {boolean} [options.headless=false] - Run in headless mode
|
|
173
|
+
* @param {string} [options.userDataDir] - Custom user data directory
|
|
174
|
+
* @returns {Promise<{process: ChildProcess, port: number, usedSeparateProfile: boolean}>}
|
|
175
|
+
*/
|
|
176
|
+
export async function launchChrome(options = {}) {
|
|
177
|
+
const {
|
|
178
|
+
port = 9222,
|
|
179
|
+
chromePath = findChromePath(),
|
|
180
|
+
headless = false,
|
|
181
|
+
userDataDir = null
|
|
182
|
+
} = options;
|
|
183
|
+
|
|
184
|
+
if (!chromePath) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
'Chrome not found. Install Google Chrome or set CHROME_PATH environment variable.\n' +
|
|
187
|
+
'Download: https://www.google.com/chrome/'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const args = [
|
|
192
|
+
`--remote-debugging-port=${port}`,
|
|
193
|
+
'--no-first-run',
|
|
194
|
+
'--no-default-browser-check'
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
if (headless) {
|
|
198
|
+
args.push('--headless=new');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Chrome requires --user-data-dir for remote debugging (as of Chrome 129+)
|
|
202
|
+
// Always use a dedicated profile for CDP to avoid conflicts with user's normal browsing
|
|
203
|
+
let usedSeparateProfile = false;
|
|
204
|
+
if (userDataDir) {
|
|
205
|
+
args.push(`--user-data-dir=${userDataDir}`);
|
|
206
|
+
} else {
|
|
207
|
+
// Use a separate profile per port to allow multiple instances
|
|
208
|
+
// Include headless flag to separate headless from headful profiles
|
|
209
|
+
const profileSuffix = headless ? `-headless-${port}` : `-${port}`;
|
|
210
|
+
const tempDir = path.join(os.tmpdir(), `cdp-skill-chrome-profile${profileSuffix}`);
|
|
211
|
+
args.push(`--user-data-dir=${tempDir}`);
|
|
212
|
+
usedSeparateProfile = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Capture stderr to report Chrome errors to the agent
|
|
216
|
+
const chromeProcess = spawn(chromePath, args, {
|
|
217
|
+
detached: true,
|
|
218
|
+
stdio: ['ignore', 'ignore', 'pipe'] // capture stderr
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Collect stderr output for error reporting
|
|
222
|
+
let stderrOutput = '';
|
|
223
|
+
chromeProcess.stderr.on('data', (data) => {
|
|
224
|
+
stderrOutput += data.toString();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Don't let this process keep Node alive
|
|
228
|
+
chromeProcess.unref();
|
|
229
|
+
|
|
230
|
+
// Wait for Chrome to be ready
|
|
231
|
+
const discovery = createDiscovery('localhost', port, 1000);
|
|
232
|
+
const maxWait = 10000;
|
|
233
|
+
const startTime = Date.now();
|
|
234
|
+
|
|
235
|
+
while (Date.now() - startTime < maxWait) {
|
|
236
|
+
if (await discovery.isAvailable()) {
|
|
237
|
+
return { process: chromeProcess, port, usedSeparateProfile };
|
|
238
|
+
}
|
|
239
|
+
await sleep(100);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Kill process if it didn't start properly
|
|
243
|
+
try {
|
|
244
|
+
chromeProcess.kill();
|
|
245
|
+
} catch { /* ignore */ }
|
|
246
|
+
|
|
247
|
+
// Include Chrome's error output in the error message
|
|
248
|
+
const errorDetails = stderrOutput.trim();
|
|
249
|
+
const errorMsg = errorDetails
|
|
250
|
+
? `Chrome failed to start within ${maxWait}ms. Chrome error: ${errorDetails}`
|
|
251
|
+
: `Chrome failed to start within ${maxWait}ms`;
|
|
252
|
+
throw new Error(errorMsg);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get Chrome status - check if running, optionally launch if not
|
|
257
|
+
*
|
|
258
|
+
* Handles several scenarios:
|
|
259
|
+
* 1. Chrome not running -> launch new Chrome with CDP port
|
|
260
|
+
* 2. Chrome running but without CDP port (common on macOS when all windows closed)
|
|
261
|
+
* -> launch NEW Chrome instance with CDP (never kills existing Chrome)
|
|
262
|
+
* 3. Chrome running with CDP but no tabs -> create new tab
|
|
263
|
+
* 4. Chrome running with CDP and tabs -> return tabs
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} [options] - Options
|
|
266
|
+
* @param {string} [options.host='localhost'] - Chrome host
|
|
267
|
+
* @param {number} [options.port=9222] - Chrome debugging port
|
|
268
|
+
* @param {boolean} [options.autoLaunch=true] - Auto-launch if not running
|
|
269
|
+
* @param {boolean} [options.headless=false] - Launch in headless mode
|
|
270
|
+
* @returns {Promise<{running: boolean, launched?: boolean, version?: string, tabs?: Array, error?: string, note?: string}>}
|
|
271
|
+
*/
|
|
272
|
+
export async function getChromeStatus(options = {}) {
|
|
273
|
+
const {
|
|
274
|
+
host = 'localhost',
|
|
275
|
+
port = 9222,
|
|
276
|
+
autoLaunch = true,
|
|
277
|
+
headless = false
|
|
278
|
+
} = options;
|
|
279
|
+
|
|
280
|
+
const discovery = createDiscovery(host, port, 2000);
|
|
281
|
+
|
|
282
|
+
// Check if CDP is available on the port
|
|
283
|
+
let cdpAvailable = await discovery.isAvailable();
|
|
284
|
+
let launched = false;
|
|
285
|
+
let createdTab = false;
|
|
286
|
+
let note = null;
|
|
287
|
+
|
|
288
|
+
// If CDP not available, check if Chrome process is running
|
|
289
|
+
if (!cdpAvailable && autoLaunch && host === 'localhost') {
|
|
290
|
+
const processCheck = isChromeProcessRunning();
|
|
291
|
+
|
|
292
|
+
if (processCheck.running) {
|
|
293
|
+
// Chrome is running but CDP isn't available
|
|
294
|
+
const reason = processCheck.hasCdpPort
|
|
295
|
+
? 'Chrome has --remote-debugging-port flag but CDP is not responding (stale instance)'
|
|
296
|
+
: 'Chrome is running without CDP port';
|
|
297
|
+
note = `${reason}. Launched new instance with debugging enabled.`;
|
|
298
|
+
try {
|
|
299
|
+
await launchChrome({ port, headless });
|
|
300
|
+
launched = true;
|
|
301
|
+
cdpAvailable = true;
|
|
302
|
+
} catch (err) {
|
|
303
|
+
return {
|
|
304
|
+
running: false,
|
|
305
|
+
launched: false,
|
|
306
|
+
error: `${reason}. Failed to launch new instance: ${err.message}`,
|
|
307
|
+
note: 'On macOS, Chrome keeps running after closing all windows. A new Chrome instance with CDP was attempted but failed.'
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// Chrome not running at all - launch it
|
|
312
|
+
try {
|
|
313
|
+
await launchChrome({ port, headless });
|
|
314
|
+
launched = true;
|
|
315
|
+
cdpAvailable = true;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
return {
|
|
318
|
+
running: false,
|
|
319
|
+
launched: false,
|
|
320
|
+
error: err.message
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!cdpAvailable) {
|
|
327
|
+
const processCheck = isChromeProcessRunning();
|
|
328
|
+
if (processCheck.running) {
|
|
329
|
+
const reason = processCheck.hasCdpPort
|
|
330
|
+
? `Chrome has --remote-debugging-port flag but CDP is not responding on port ${port} (stale instance).`
|
|
331
|
+
: `Chrome is running but not with CDP debugging enabled on port ${port}.`;
|
|
332
|
+
return {
|
|
333
|
+
running: false,
|
|
334
|
+
launched: false,
|
|
335
|
+
error: `${reason} On macOS, Chrome stays running after closing all windows. ` +
|
|
336
|
+
`Set autoLaunch:true to start a new Chrome instance with CDP.`
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
running: false,
|
|
341
|
+
launched: false,
|
|
342
|
+
error: `Chrome not running on ${host}:${port}`
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Get version and tabs
|
|
347
|
+
try {
|
|
348
|
+
const version = await discovery.getVersion();
|
|
349
|
+
let pages = await discovery.getPages();
|
|
350
|
+
|
|
351
|
+
// If no tabs and autoLaunch is enabled, create a new tab
|
|
352
|
+
if (pages.length === 0 && autoLaunch && host === 'localhost') {
|
|
353
|
+
try {
|
|
354
|
+
const newTab = await createNewTab(host, port, 'about:blank');
|
|
355
|
+
pages = [{ id: newTab.targetId, url: newTab.url, title: '' }];
|
|
356
|
+
createdTab = true;
|
|
357
|
+
note = note ? note + ' Created new tab.' : 'Chrome had no tabs open. Created new tab.';
|
|
358
|
+
} catch (err) {
|
|
359
|
+
return {
|
|
360
|
+
running: true,
|
|
361
|
+
launched,
|
|
362
|
+
version: version.browser,
|
|
363
|
+
port,
|
|
364
|
+
tabs: [],
|
|
365
|
+
error: `Chrome running but has no tabs and failed to create one: ${err.message}`,
|
|
366
|
+
note: 'On macOS, Chrome can run without any windows. Try opening a new Chrome window manually.'
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = {
|
|
372
|
+
running: true,
|
|
373
|
+
launched,
|
|
374
|
+
version: version.browser,
|
|
375
|
+
port,
|
|
376
|
+
tabs: pages.map(p => ({
|
|
377
|
+
targetId: p.id,
|
|
378
|
+
url: p.url,
|
|
379
|
+
title: p.title
|
|
380
|
+
}))
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (createdTab) result.createdTab = true;
|
|
384
|
+
if (note) result.note = note;
|
|
385
|
+
|
|
386
|
+
return result;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return {
|
|
389
|
+
running: false,
|
|
390
|
+
launched,
|
|
391
|
+
error: err.message
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Create a high-level browser client
|
|
398
|
+
* @param {Object} [options] - Configuration options
|
|
399
|
+
* @param {string} [options.host='localhost'] - Chrome host
|
|
400
|
+
* @param {number} [options.port=9222] - Chrome debugging port
|
|
401
|
+
* @param {number} [options.connectTimeout=30000] - Connection timeout in ms
|
|
402
|
+
* @returns {Object} Browser client interface
|
|
403
|
+
*/
|
|
404
|
+
export function createBrowser(options = {}) {
|
|
405
|
+
const host = options.host ?? 'localhost';
|
|
406
|
+
const port = options.port ?? 9222;
|
|
407
|
+
const connectTimeout = options.connectTimeout ?? 30000;
|
|
408
|
+
|
|
409
|
+
let discovery = createDiscovery(host, port, connectTimeout);
|
|
410
|
+
let connection = null;
|
|
411
|
+
let targetManager = null;
|
|
412
|
+
let sessionRegistry = null;
|
|
413
|
+
let connected = false;
|
|
414
|
+
const targetLocks = new Map();
|
|
415
|
+
|
|
416
|
+
async function acquireLock(targetId) {
|
|
417
|
+
// Wait for any existing lock to be released
|
|
418
|
+
while (targetLocks.has(targetId)) {
|
|
419
|
+
await targetLocks.get(targetId);
|
|
420
|
+
}
|
|
421
|
+
// Create a new lock
|
|
422
|
+
let releaseFn;
|
|
423
|
+
const lockPromise = new Promise(resolve => {
|
|
424
|
+
releaseFn = resolve;
|
|
425
|
+
});
|
|
426
|
+
targetLocks.set(targetId, lockPromise);
|
|
427
|
+
return { promise: lockPromise, release: releaseFn };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function releaseLock(targetId, lock) {
|
|
431
|
+
if (targetLocks.get(targetId) === lock.promise) {
|
|
432
|
+
targetLocks.delete(targetId);
|
|
433
|
+
}
|
|
434
|
+
lock.release();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function ensureConnected() {
|
|
438
|
+
if (!connected) {
|
|
439
|
+
throw new Error('BrowserClient not connected. Call connect() first.');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function doConnect() {
|
|
444
|
+
const version = await discovery.getVersion();
|
|
445
|
+
connection = createConnection(version.webSocketDebuggerUrl);
|
|
446
|
+
await connection.connect();
|
|
447
|
+
|
|
448
|
+
targetManager = createTargetManager(connection);
|
|
449
|
+
sessionRegistry = createSessionRegistry(connection);
|
|
450
|
+
|
|
451
|
+
await targetManager.enableDiscovery();
|
|
452
|
+
connected = true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Connect to Chrome
|
|
457
|
+
* @returns {Promise<void>}
|
|
458
|
+
*/
|
|
459
|
+
async function connect() {
|
|
460
|
+
if (connected) return;
|
|
461
|
+
|
|
462
|
+
const connectPromise = doConnect();
|
|
463
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
464
|
+
setTimeout(() => {
|
|
465
|
+
reject(timeoutError(`Connection to Chrome timed out after ${connectTimeout}ms`));
|
|
466
|
+
}, connectTimeout);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Disconnect from Chrome
|
|
474
|
+
* @returns {Promise<void>}
|
|
475
|
+
*/
|
|
476
|
+
async function disconnect() {
|
|
477
|
+
if (!connected) return;
|
|
478
|
+
|
|
479
|
+
await sessionRegistry.cleanup();
|
|
480
|
+
await targetManager.cleanup();
|
|
481
|
+
await connection.close();
|
|
482
|
+
connected = false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get all page targets
|
|
487
|
+
* @returns {Promise<Array>} Array of page info objects
|
|
488
|
+
*/
|
|
489
|
+
async function getPages() {
|
|
490
|
+
ensureConnected();
|
|
491
|
+
return targetManager.getPages();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Create a new page (tab)
|
|
496
|
+
* @param {string} [url='about:blank'] - Initial URL
|
|
497
|
+
* @returns {Promise<import('../types.js').CDPSession>} Page session
|
|
498
|
+
*/
|
|
499
|
+
async function newPage(url = 'about:blank') {
|
|
500
|
+
ensureConnected();
|
|
501
|
+
|
|
502
|
+
const targetId = await targetManager.createTarget(url);
|
|
503
|
+
const sessionId = await sessionRegistry.attach(targetId);
|
|
504
|
+
|
|
505
|
+
return createPageSession(connection, sessionId, targetId);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Attach to existing page
|
|
510
|
+
* @param {string} targetId - Target ID to attach to
|
|
511
|
+
* @returns {Promise<import('../types.js').CDPSession>} Page session
|
|
512
|
+
*/
|
|
513
|
+
async function attachToPage(targetId) {
|
|
514
|
+
ensureConnected();
|
|
515
|
+
const lock = await acquireLock(targetId);
|
|
516
|
+
try {
|
|
517
|
+
const sessionId = await sessionRegistry.attach(targetId);
|
|
518
|
+
return createPageSession(connection, sessionId, targetId);
|
|
519
|
+
} finally {
|
|
520
|
+
releaseLock(targetId, lock);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Find and attach to page by URL pattern
|
|
526
|
+
* @param {string|RegExp} urlPattern - URL pattern to match
|
|
527
|
+
* @returns {Promise<import('../types.js').CDPSession|null>} Page session or null
|
|
528
|
+
*/
|
|
529
|
+
async function findPage(urlPattern) {
|
|
530
|
+
ensureConnected();
|
|
531
|
+
|
|
532
|
+
const pages = await getPages();
|
|
533
|
+
const regex = urlPattern instanceof RegExp ? urlPattern : new RegExp(urlPattern);
|
|
534
|
+
const target = pages.find(p => regex.test(p.url));
|
|
535
|
+
|
|
536
|
+
if (!target) return null;
|
|
537
|
+
return attachToPage(target.targetId);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Close a page
|
|
542
|
+
* @param {string} targetId - Target ID to close
|
|
543
|
+
* @returns {Promise<void>}
|
|
544
|
+
*/
|
|
545
|
+
async function closePage(targetId) {
|
|
546
|
+
ensureConnected();
|
|
547
|
+
const lock = await acquireLock(targetId);
|
|
548
|
+
try {
|
|
549
|
+
await sessionRegistry.detachByTarget(targetId);
|
|
550
|
+
await targetManager.closeTarget(targetId);
|
|
551
|
+
} finally {
|
|
552
|
+
releaseLock(targetId, lock);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
connect,
|
|
558
|
+
disconnect,
|
|
559
|
+
getPages,
|
|
560
|
+
newPage,
|
|
561
|
+
attachToPage,
|
|
562
|
+
findPage,
|
|
563
|
+
closePage,
|
|
564
|
+
isConnected: () => connected,
|
|
565
|
+
get connection() { return connection; },
|
|
566
|
+
get targets() { return targetManager; },
|
|
567
|
+
get sessions() { return sessionRegistry; }
|
|
568
|
+
};
|
|
569
|
+
}
|