browser-cdp 0.3.0 → 0.5.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 +30 -5
- package/cli.js +9 -2
- package/console.js +52 -10
- package/eval.js +75 -5
- package/insights.js +6 -1
- package/nav.js +88 -17
- package/package.json +2 -1
- package/pick.js +6 -1
- package/screenshot.js +6 -1
- package/start.js +52 -11
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# browser-cdp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/browser-cdp)
|
|
4
|
+
|
|
3
5
|
Browser automation via Chrome DevTools Protocol. Control Chrome, Brave, or Edge using your real browser - same fingerprint, real cookies, no automation detection.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
@@ -12,7 +14,10 @@ npm install -g browser-cdp
|
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
16
|
# Start browser with CDP enabled
|
|
15
|
-
browser-cdp start [browser] [--isolated] [--port=PORT]
|
|
17
|
+
browser-cdp start [browser] [--profile=NAME] [--isolated] [--port=PORT]
|
|
18
|
+
|
|
19
|
+
# Close the browser
|
|
20
|
+
browser-cdp close
|
|
16
21
|
|
|
17
22
|
# Navigate to URL
|
|
18
23
|
browser-cdp nav <url> [--new]
|
|
@@ -26,11 +31,14 @@ browser-cdp screenshot
|
|
|
26
31
|
# Interactive element picker
|
|
27
32
|
browser-cdp pick '<message>'
|
|
28
33
|
|
|
29
|
-
# Stream browser console output
|
|
34
|
+
# Stream browser console output (network errors, exceptions, logs)
|
|
30
35
|
browser-cdp console [--duration=SECONDS]
|
|
31
36
|
|
|
32
37
|
# Show page performance metrics
|
|
33
38
|
browser-cdp insights [--json]
|
|
39
|
+
|
|
40
|
+
# Run Lighthouse audit (Chrome only)
|
|
41
|
+
browser-cdp lighthouse [--json] [--category=NAME]
|
|
34
42
|
```
|
|
35
43
|
|
|
36
44
|
## Environment Variables
|
|
@@ -47,6 +55,9 @@ browser-cdp insights [--json]
|
|
|
47
55
|
# Start Brave with real profile
|
|
48
56
|
browser-cdp start brave
|
|
49
57
|
|
|
58
|
+
# Start Brave with specific profile (by name)
|
|
59
|
+
browser-cdp start brave --profile=Work
|
|
60
|
+
|
|
50
61
|
# Start Chrome on custom port
|
|
51
62
|
DEBUG_PORT=9333 browser-cdp start
|
|
52
63
|
|
|
@@ -61,15 +72,25 @@ browser-cdp screenshot
|
|
|
61
72
|
# Pick elements interactively
|
|
62
73
|
browser-cdp pick "Select the login button"
|
|
63
74
|
|
|
64
|
-
# Stream console output
|
|
75
|
+
# Stream console output (captures network errors, exceptions, console.log)
|
|
76
|
+
browser-cdp console
|
|
77
|
+
# Then refresh the page to see errors
|
|
78
|
+
|
|
79
|
+
# Stream console for 10 seconds
|
|
65
80
|
browser-cdp console --duration=10
|
|
66
81
|
|
|
67
82
|
# Get page performance insights
|
|
68
83
|
browser-cdp insights
|
|
69
84
|
# Returns: TTFB, First Paint, FCP, DOM loaded, resources, memory
|
|
70
85
|
|
|
71
|
-
#
|
|
72
|
-
browser-cdp
|
|
86
|
+
# Run Lighthouse audit (Chrome only - Brave blocks CDP debugger)
|
|
87
|
+
browser-cdp start chrome --isolated
|
|
88
|
+
browser-cdp nav https://example.com
|
|
89
|
+
browser-cdp lighthouse
|
|
90
|
+
# Returns: Performance, Accessibility, Best Practices, SEO scores
|
|
91
|
+
|
|
92
|
+
# Close browser when done
|
|
93
|
+
browser-cdp close
|
|
73
94
|
```
|
|
74
95
|
|
|
75
96
|
## Pre-started Browser
|
|
@@ -116,6 +137,10 @@ Use `BROWSER_PATH` env var to override if your browser is installed elsewhere.
|
|
|
116
137
|
| Detection | Not detectable as automation | Automation flags present |
|
|
117
138
|
| Use case | Real-world testing, scraping | Isolated E2E tests |
|
|
118
139
|
|
|
140
|
+
## See Also
|
|
141
|
+
|
|
142
|
+
- [dev-browser](https://github.com/SawyerHood/dev-browser) - Browser automation plugin for Claude Code with LLM-optimized DOM snapshots
|
|
143
|
+
|
|
119
144
|
## License
|
|
120
145
|
|
|
121
146
|
MIT
|
package/cli.js
CHANGED
|
@@ -12,12 +12,15 @@ const args = process.argv.slice(3);
|
|
|
12
12
|
|
|
13
13
|
const commands = {
|
|
14
14
|
start: "./start.js",
|
|
15
|
+
close: "./close.js",
|
|
15
16
|
nav: "./nav.js",
|
|
16
17
|
eval: "./eval.js",
|
|
18
|
+
dom: "./dom.js",
|
|
17
19
|
screenshot: "./screenshot.js",
|
|
18
20
|
pick: "./pick.js",
|
|
19
21
|
console: "./console.js",
|
|
20
22
|
insights: "./insights.js",
|
|
23
|
+
lighthouse: "./lighthouse.js",
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
function printUsage() {
|
|
@@ -27,12 +30,15 @@ function printUsage() {
|
|
|
27
30
|
console.log("");
|
|
28
31
|
console.log("Commands:");
|
|
29
32
|
console.log(" start [browser] Start browser with CDP (uses real profile)");
|
|
30
|
-
console.log("
|
|
31
|
-
console.log("
|
|
33
|
+
console.log(" close Close the browser");
|
|
34
|
+
console.log(" nav <url> Navigate to URL (--console to capture logs)");
|
|
35
|
+
console.log(" eval '<code>' Evaluate JS in page (--console to capture logs)");
|
|
36
|
+
console.log(" dom Capture full page DOM/HTML");
|
|
32
37
|
console.log(" screenshot Take screenshot of current page");
|
|
33
38
|
console.log(" pick '<message>' Interactive element picker");
|
|
34
39
|
console.log(" console Stream browser console output");
|
|
35
40
|
console.log(" insights Show page performance metrics");
|
|
41
|
+
console.log(" lighthouse Run Lighthouse audit");
|
|
36
42
|
console.log("");
|
|
37
43
|
console.log("Environment:");
|
|
38
44
|
console.log(" DEBUG_PORT CDP port (default: 9222)");
|
|
@@ -43,6 +49,7 @@ function printUsage() {
|
|
|
43
49
|
console.log(" browser-cdp start brave");
|
|
44
50
|
console.log(" browser-cdp nav https://google.com");
|
|
45
51
|
console.log(" browser-cdp eval 'document.title'");
|
|
52
|
+
console.log(" browser-cdp dom > page.html");
|
|
46
53
|
console.log(" browser-cdp console --duration=10");
|
|
47
54
|
console.log(" browser-cdp insights --json");
|
|
48
55
|
process.exit(0);
|
package/console.js
CHANGED
|
@@ -7,16 +7,19 @@ const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
|
7
7
|
const args = process.argv.slice(2);
|
|
8
8
|
const duration = args.find((a) => a.startsWith("--duration="));
|
|
9
9
|
const durationMs = duration ? parseInt(duration.split("=")[1]) * 1000 : null;
|
|
10
|
+
const shouldReload = args.includes("--reload") || args.includes("-r");
|
|
10
11
|
const showHelp = args.includes("--help") || args.includes("-h");
|
|
11
12
|
|
|
12
13
|
if (showHelp) {
|
|
13
|
-
console.log("Usage: console.js [
|
|
14
|
+
console.log("Usage: console.js [options]");
|
|
14
15
|
console.log("\nCapture browser console output in real-time.");
|
|
15
16
|
console.log("\nOptions:");
|
|
16
17
|
console.log(" --duration=N Stop after N seconds (default: run until Ctrl+C)");
|
|
18
|
+
console.log(" --reload, -r Reload the page before capturing");
|
|
17
19
|
console.log("\nExamples:");
|
|
18
20
|
console.log(" console.js # Stream console logs until Ctrl+C");
|
|
19
21
|
console.log(" console.js --duration=5 # Capture for 5 seconds");
|
|
22
|
+
console.log(" console.js --reload # Reload page and capture logs");
|
|
20
23
|
process.exit(0);
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -30,35 +33,74 @@ if (!context) {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const pages = context.pages();
|
|
33
|
-
|
|
36
|
+
// Filter out devtools pages and pick a real page
|
|
37
|
+
const realPages = pages.filter(p => {
|
|
38
|
+
const url = p.url();
|
|
39
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
40
|
+
});
|
|
41
|
+
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
34
42
|
|
|
35
43
|
if (!page) {
|
|
36
44
|
console.error("No active tab found");
|
|
37
45
|
process.exit(1);
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
console.error(`Connected to: ${page.url()}`);
|
|
49
|
+
|
|
40
50
|
const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
|
|
41
51
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
info: "\x1b[36m",
|
|
45
|
-
|
|
46
|
-
error: "\x1b[31m",
|
|
47
|
-
debug: "\x1b[90m", // gray
|
|
52
|
+
const levelColors = {
|
|
53
|
+
verbose: "\x1b[90m", // gray
|
|
54
|
+
info: "\x1b[36m", // cyan
|
|
55
|
+
warning: "\x1b[33m", // yellow
|
|
56
|
+
error: "\x1b[31m", // red
|
|
48
57
|
};
|
|
49
58
|
const reset = "\x1b[0m";
|
|
50
59
|
|
|
60
|
+
// Use CDP directly for Log domain (captures network errors, etc.)
|
|
61
|
+
const cdp = await page.context().newCDPSession(page);
|
|
62
|
+
await cdp.send("Log.enable");
|
|
63
|
+
|
|
64
|
+
cdp.on("Log.entryAdded", ({ entry }) => {
|
|
65
|
+
const color = levelColors[entry.level] || levelColors.info;
|
|
66
|
+
const source = entry.source ? `[${entry.source}]` : "";
|
|
67
|
+
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${reset}`);
|
|
68
|
+
if (entry.url) {
|
|
69
|
+
console.log(`${color} URL: ${entry.url}${reset}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Also capture runtime exceptions
|
|
74
|
+
await cdp.send("Runtime.enable");
|
|
75
|
+
cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
76
|
+
const text = exceptionDetails.exception?.description || exceptionDetails.text;
|
|
77
|
+
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${reset}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Capture network failures (ERR_BLOCKED_BY_CLIENT, etc.)
|
|
81
|
+
await cdp.send("Network.enable");
|
|
82
|
+
cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
|
|
83
|
+
const reason = blockedReason ? ` (${blockedReason})` : "";
|
|
84
|
+
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${reset}`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Keep Playwright listeners for console.log() calls
|
|
51
88
|
page.on("console", (msg) => {
|
|
52
89
|
const type = msg.type();
|
|
53
|
-
const color =
|
|
90
|
+
const color = levelColors[type] || levelColors.info;
|
|
54
91
|
const text = msg.text();
|
|
55
|
-
console.log(`${color}[${formatTime()}] [
|
|
92
|
+
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${text}${reset}`);
|
|
56
93
|
});
|
|
57
94
|
|
|
58
95
|
page.on("pageerror", (error) => {
|
|
59
96
|
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${reset}`);
|
|
60
97
|
});
|
|
61
98
|
|
|
99
|
+
if (shouldReload) {
|
|
100
|
+
console.error("Reloading page...");
|
|
101
|
+
await page.reload();
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
console.error(`Listening for console output... (Ctrl+C to stop)`);
|
|
63
105
|
|
|
64
106
|
if (durationMs) {
|
package/eval.js
CHANGED
|
@@ -4,13 +4,25 @@ import { chromium } from "playwright";
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
9
|
+
const captureConsole = args.includes("--console");
|
|
10
|
+
const durationArg = args.find((a) => a.startsWith("--duration="));
|
|
11
|
+
const durationMs = durationArg ? parseInt(durationArg.split("=")[1]) * 1000 : 3000;
|
|
12
|
+
|
|
13
|
+
// Get code (everything that's not a flag)
|
|
14
|
+
const code = args.filter((a) => !a.startsWith("--")).join(" ");
|
|
15
|
+
|
|
16
|
+
if (showHelp || !code) {
|
|
17
|
+
console.log("Usage: eval.js '<code>' [options]");
|
|
18
|
+
console.log("\nOptions:");
|
|
19
|
+
console.log(" --console Capture console output during evaluation");
|
|
20
|
+
console.log(" --duration=N With --console, capture for N seconds (default: 3)");
|
|
10
21
|
console.log("\nExamples:");
|
|
11
22
|
console.log(' eval.js "document.title"');
|
|
12
23
|
console.log(" eval.js \"document.querySelectorAll('a').length\"");
|
|
13
|
-
|
|
24
|
+
console.log(" eval.js \"fetch('/api/data')\" --console");
|
|
25
|
+
process.exit(showHelp ? 0 : 1);
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
@@ -23,13 +35,64 @@ if (!context) {
|
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
const pages = context.pages();
|
|
26
|
-
|
|
38
|
+
// Filter out devtools pages and pick a real page
|
|
39
|
+
const realPages = pages.filter(p => {
|
|
40
|
+
const url = p.url();
|
|
41
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
42
|
+
});
|
|
43
|
+
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
27
44
|
|
|
28
45
|
if (!page) {
|
|
29
46
|
console.error("No active tab found");
|
|
30
47
|
process.exit(1);
|
|
31
48
|
}
|
|
32
49
|
|
|
50
|
+
// Set up console capture BEFORE evaluation
|
|
51
|
+
if (captureConsole) {
|
|
52
|
+
const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
|
|
53
|
+
const levelColors = {
|
|
54
|
+
verbose: "\x1b[90m",
|
|
55
|
+
info: "\x1b[36m",
|
|
56
|
+
warning: "\x1b[33m",
|
|
57
|
+
error: "\x1b[31m",
|
|
58
|
+
};
|
|
59
|
+
const reset = "\x1b[0m";
|
|
60
|
+
|
|
61
|
+
const cdp = await page.context().newCDPSession(page);
|
|
62
|
+
await cdp.send("Log.enable");
|
|
63
|
+
await cdp.send("Runtime.enable");
|
|
64
|
+
await cdp.send("Network.enable");
|
|
65
|
+
|
|
66
|
+
cdp.on("Log.entryAdded", ({ entry }) => {
|
|
67
|
+
const color = levelColors[entry.level] || levelColors.info;
|
|
68
|
+
const source = entry.source ? `[${entry.source}]` : "";
|
|
69
|
+
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${reset}`);
|
|
70
|
+
if (entry.url) {
|
|
71
|
+
console.log(`${color} URL: ${entry.url}${reset}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
76
|
+
const text = exceptionDetails.exception?.description || exceptionDetails.text;
|
|
77
|
+
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${reset}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
|
|
81
|
+
const reason = blockedReason ? ` (${blockedReason})` : "";
|
|
82
|
+
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${reset}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
page.on("console", (msg) => {
|
|
86
|
+
const type = msg.type();
|
|
87
|
+
const color = levelColors[type] || levelColors.info;
|
|
88
|
+
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${reset}`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
page.on("pageerror", (error) => {
|
|
92
|
+
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${reset}`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
33
96
|
let result;
|
|
34
97
|
|
|
35
98
|
try {
|
|
@@ -44,6 +107,7 @@ try {
|
|
|
44
107
|
process.exit(1);
|
|
45
108
|
}
|
|
46
109
|
|
|
110
|
+
// Print result
|
|
47
111
|
if (Array.isArray(result)) {
|
|
48
112
|
for (let i = 0; i < result.length; i++) {
|
|
49
113
|
if (i > 0) console.log("");
|
|
@@ -59,4 +123,10 @@ if (Array.isArray(result)) {
|
|
|
59
123
|
console.log(result);
|
|
60
124
|
}
|
|
61
125
|
|
|
126
|
+
// Wait for async console output
|
|
127
|
+
if (captureConsole) {
|
|
128
|
+
console.error(`\nListening for ${durationMs / 1000}s...`);
|
|
129
|
+
await new Promise((r) => setTimeout(r, durationMs));
|
|
130
|
+
}
|
|
131
|
+
|
|
62
132
|
await browser.close();
|
package/insights.js
CHANGED
|
@@ -31,7 +31,12 @@ if (!context) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const pages = context.pages();
|
|
34
|
-
|
|
34
|
+
// Filter out devtools pages and pick a real page
|
|
35
|
+
const realPages = pages.filter(p => {
|
|
36
|
+
const url = p.url();
|
|
37
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
38
|
+
});
|
|
39
|
+
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
35
40
|
|
|
36
41
|
if (!page) {
|
|
37
42
|
console.error("No active tab found");
|
package/nav.js
CHANGED
|
@@ -4,35 +4,106 @@ import { chromium } from "playwright";
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
9
|
+
const newTab = args.includes("--new");
|
|
10
|
+
const captureConsole = args.includes("--console");
|
|
11
|
+
const durationArg = args.find((a) => a.startsWith("--duration="));
|
|
12
|
+
const durationMs = durationArg ? parseInt(durationArg.split("=")[1]) * 1000 : 5000;
|
|
9
13
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
url = "https://" + url;
|
|
13
|
-
}
|
|
14
|
+
// Get URL (first arg that doesn't start with --)
|
|
15
|
+
let url = args.find((a) => !a.startsWith("--"));
|
|
14
16
|
|
|
15
|
-
if (!url) {
|
|
16
|
-
console.log("Usage: nav.js <url> [
|
|
17
|
+
if (showHelp || !url) {
|
|
18
|
+
console.log("Usage: nav.js <url> [options]");
|
|
19
|
+
console.log("\nOptions:");
|
|
20
|
+
console.log(" --new Open in new tab");
|
|
21
|
+
console.log(" --console Capture console output during navigation");
|
|
22
|
+
console.log(" --duration=N With --console, capture for N seconds (default: 5)");
|
|
17
23
|
console.log("\nExamples:");
|
|
18
|
-
console.log(" nav.js example.com
|
|
19
|
-
console.log(" nav.js example.com --new
|
|
20
|
-
|
|
24
|
+
console.log(" nav.js example.com # Navigate current tab");
|
|
25
|
+
console.log(" nav.js example.com --new # Open in new tab");
|
|
26
|
+
console.log(" nav.js example.com --console # Navigate and capture console");
|
|
27
|
+
process.exit(showHelp ? 0 : 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Add protocol if missing
|
|
31
|
+
if (!url.match(/^https?:\/\//i)) {
|
|
32
|
+
url = "https://" + url;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
24
36
|
const contexts = browser.contexts();
|
|
25
37
|
const context = contexts[0] || await browser.newContext();
|
|
26
38
|
|
|
39
|
+
let page;
|
|
27
40
|
if (newTab) {
|
|
28
|
-
|
|
29
|
-
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
30
|
-
console.log("Opened:", url);
|
|
41
|
+
page = await context.newPage();
|
|
31
42
|
} else {
|
|
32
43
|
const pages = context.pages();
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
const realPages = pages.filter(p => {
|
|
45
|
+
const u = p.url();
|
|
46
|
+
return u.startsWith("http://") || u.startsWith("https://") || u === "about:blank";
|
|
47
|
+
});
|
|
48
|
+
page = realPages[realPages.length - 1] || pages[pages.length - 1] || await context.newPage();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Set up console capture BEFORE navigation
|
|
52
|
+
if (captureConsole) {
|
|
53
|
+
const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
|
|
54
|
+
const levelColors = {
|
|
55
|
+
verbose: "\x1b[90m",
|
|
56
|
+
info: "\x1b[36m",
|
|
57
|
+
warning: "\x1b[33m",
|
|
58
|
+
error: "\x1b[31m",
|
|
59
|
+
};
|
|
60
|
+
const reset = "\x1b[0m";
|
|
61
|
+
|
|
62
|
+
const cdp = await page.context().newCDPSession(page);
|
|
63
|
+
await cdp.send("Log.enable");
|
|
64
|
+
await cdp.send("Runtime.enable");
|
|
65
|
+
await cdp.send("Network.enable");
|
|
66
|
+
|
|
67
|
+
cdp.on("Log.entryAdded", ({ entry }) => {
|
|
68
|
+
const color = levelColors[entry.level] || levelColors.info;
|
|
69
|
+
const source = entry.source ? `[${entry.source}]` : "";
|
|
70
|
+
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${reset}`);
|
|
71
|
+
if (entry.url) {
|
|
72
|
+
console.log(`${color} URL: ${entry.url}${reset}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
77
|
+
const text = exceptionDetails.exception?.description || exceptionDetails.text;
|
|
78
|
+
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${reset}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
|
|
82
|
+
const reason = blockedReason ? ` (${blockedReason})` : "";
|
|
83
|
+
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${reset}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
page.on("console", (msg) => {
|
|
87
|
+
const type = msg.type();
|
|
88
|
+
const color = levelColors[type] || levelColors.info;
|
|
89
|
+
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${reset}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
page.on("pageerror", (error) => {
|
|
93
|
+
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${reset}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.error(`Navigating to ${url} (capturing console for ${durationMs / 1000}s)...`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
100
|
+
|
|
101
|
+
if (captureConsole) {
|
|
102
|
+
console.error(`Loaded: ${url}`);
|
|
103
|
+
await new Promise((r) => setTimeout(r, durationMs));
|
|
104
|
+
console.error("Done.");
|
|
105
|
+
} else {
|
|
106
|
+
console.log(newTab ? "Opened:" : "Navigated to:", url);
|
|
36
107
|
}
|
|
37
108
|
|
|
38
109
|
await browser.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-cdp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Browser automation via Chrome DevTools Protocol - control Chrome, Brave, Edge with real browser profiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"url": "https://github.com/dpaluy/browser-cdp/issues"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"lighthouse": "^13.0.1",
|
|
36
37
|
"playwright": "^1.49.0"
|
|
37
38
|
},
|
|
38
39
|
"keywords": [
|
package/pick.js
CHANGED
|
@@ -22,7 +22,12 @@ if (!context) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const pages = context.pages();
|
|
25
|
-
|
|
25
|
+
// Filter out devtools pages and pick a real page
|
|
26
|
+
const realPages = pages.filter(p => {
|
|
27
|
+
const url = p.url();
|
|
28
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
29
|
+
});
|
|
30
|
+
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
26
31
|
|
|
27
32
|
if (!page) {
|
|
28
33
|
console.error("No active tab found");
|
package/screenshot.js
CHANGED
|
@@ -16,7 +16,12 @@ if (!context) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const pages = context.pages();
|
|
19
|
-
|
|
19
|
+
// Filter out devtools pages and pick a real page
|
|
20
|
+
const realPages = pages.filter(p => {
|
|
21
|
+
const url = p.url();
|
|
22
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
23
|
+
});
|
|
24
|
+
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
20
25
|
|
|
21
26
|
if (!page) {
|
|
22
27
|
console.error("No active tab found");
|
package/start.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn, execFileSync } from "node:child_process";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { platform } from "node:os";
|
|
6
6
|
import { chromium } from "playwright";
|
|
7
7
|
|
|
@@ -44,24 +44,53 @@ const BROWSERS = {
|
|
|
44
44
|
|
|
45
45
|
const DEFAULT_PORT = 9222;
|
|
46
46
|
|
|
47
|
+
// Resolve profile name to directory (supports both "Profile 1" and "Suppli" style names)
|
|
48
|
+
function resolveProfileDir(profileSource, profileName) {
|
|
49
|
+
// If it looks like a directory name already, use it
|
|
50
|
+
if (profileName === "Default" || profileName.startsWith("Profile ")) {
|
|
51
|
+
return profileName;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Try to find profile by name in Local State
|
|
55
|
+
if (profileSource) {
|
|
56
|
+
try {
|
|
57
|
+
const localStatePath = `${profileSource}Local State`;
|
|
58
|
+
const localState = JSON.parse(readFileSync(localStatePath, "utf8"));
|
|
59
|
+
const profiles = localState.profile?.info_cache || {};
|
|
60
|
+
|
|
61
|
+
for (const [dir, info] of Object.entries(profiles)) {
|
|
62
|
+
if (info.name?.toLowerCase() === profileName.toLowerCase()) {
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Fall through to return original name
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return profileName;
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
function printUsage() {
|
|
48
|
-
console.log("Usage: start.js [browser] [--isolated] [--port=PORT]");
|
|
75
|
+
console.log("Usage: start.js [browser] [--profile=NAME] [--isolated] [--port=PORT]");
|
|
49
76
|
console.log("\nBrowsers:");
|
|
50
77
|
console.log(" chrome - Google Chrome (default)");
|
|
51
78
|
console.log(" brave - Brave Browser");
|
|
52
79
|
console.log(" edge - Microsoft Edge");
|
|
53
80
|
console.log("\nOptions:");
|
|
54
|
-
console.log(" --
|
|
55
|
-
console.log(" --
|
|
81
|
+
console.log(" --profile=NAME Use specific profile by name or directory");
|
|
82
|
+
console.log(" --isolated Use isolated profile (default: real profile)");
|
|
83
|
+
console.log(" --port=N Use custom debugging port (default: 9222)");
|
|
56
84
|
console.log("\nEnvironment variables:");
|
|
57
85
|
console.log(" BROWSER Default browser (chrome, brave, edge)");
|
|
58
86
|
console.log(" BROWSER_PATH Custom browser executable path");
|
|
59
87
|
console.log(" DEBUG_PORT Custom debugging port");
|
|
60
88
|
console.log("\nExamples:");
|
|
61
|
-
console.log(" start.js
|
|
62
|
-
console.log(" start.js brave
|
|
63
|
-
console.log(" start.js
|
|
64
|
-
console.log(" start.js --
|
|
89
|
+
console.log(" start.js # Start Chrome with default profile");
|
|
90
|
+
console.log(" start.js brave # Start Brave with default profile");
|
|
91
|
+
console.log(" start.js brave --profile=Work # Start Brave with 'Work' profile");
|
|
92
|
+
console.log(" start.js edge --isolated # Start Edge with isolated profile");
|
|
93
|
+
console.log(" start.js --port=9333 # Start Chrome on port 9333");
|
|
65
94
|
process.exit(1);
|
|
66
95
|
}
|
|
67
96
|
|
|
@@ -69,6 +98,7 @@ function printUsage() {
|
|
|
69
98
|
const args = process.argv.slice(2);
|
|
70
99
|
let browserName = process.env.BROWSER || "chrome";
|
|
71
100
|
let isolated = false;
|
|
101
|
+
let profile = null;
|
|
72
102
|
let port = parseInt(process.env.DEBUG_PORT) || DEFAULT_PORT;
|
|
73
103
|
|
|
74
104
|
for (const arg of args) {
|
|
@@ -76,6 +106,8 @@ for (const arg of args) {
|
|
|
76
106
|
printUsage();
|
|
77
107
|
} else if (arg === "--isolated") {
|
|
78
108
|
isolated = true;
|
|
109
|
+
} else if (arg.startsWith("--profile=")) {
|
|
110
|
+
profile = arg.split("=")[1];
|
|
79
111
|
} else if (arg.startsWith("--port=")) {
|
|
80
112
|
port = parseInt(arg.split("=")[1]);
|
|
81
113
|
} else if (BROWSERS[arg]) {
|
|
@@ -155,7 +187,11 @@ if (!isolated && browserConfig.process) {
|
|
|
155
187
|
}
|
|
156
188
|
|
|
157
189
|
// Build browser arguments
|
|
158
|
-
const browserArgs = [
|
|
190
|
+
const browserArgs = [
|
|
191
|
+
`--remote-debugging-port=${port}`,
|
|
192
|
+
// Required for Lighthouse/CDP debugger access (prevents bfcache blocking)
|
|
193
|
+
"--disable-features=ProcessPerSiteUpToMainFrameThreshold",
|
|
194
|
+
];
|
|
159
195
|
|
|
160
196
|
if (isolated) {
|
|
161
197
|
const cacheBase = isMac
|
|
@@ -164,10 +200,15 @@ if (isolated) {
|
|
|
164
200
|
const profileDir = `${cacheBase}/browser-cdp/${browserName}`;
|
|
165
201
|
execFileSync("mkdir", ["-p", profileDir], { stdio: "ignore" });
|
|
166
202
|
browserArgs.push(`--user-data-dir=${profileDir}`);
|
|
203
|
+
} else if (profile) {
|
|
204
|
+
// Resolve profile name to directory if needed
|
|
205
|
+
const profileDir = resolveProfileDir(browserConfig.profileSource, profile);
|
|
206
|
+
browserArgs.push(`--profile-directory=${profileDir}`);
|
|
167
207
|
}
|
|
168
208
|
|
|
169
209
|
// Start browser
|
|
170
|
-
|
|
210
|
+
const profileInfo = isolated ? " (isolated)" : profile ? ` (${profile})` : "";
|
|
211
|
+
console.log(`Starting ${browserConfig.name} on port ${port}${profileInfo}...`);
|
|
171
212
|
|
|
172
213
|
spawn(browserPath, browserArgs, {
|
|
173
214
|
detached: true,
|
|
@@ -192,4 +233,4 @@ if (!connected) {
|
|
|
192
233
|
process.exit(1);
|
|
193
234
|
}
|
|
194
235
|
|
|
195
|
-
console.log(`${browserConfig.name} started on :${port}${
|
|
236
|
+
console.log(`${browserConfig.name} started on :${port}${profileInfo}`);
|