browser-cdp 0.2.1 → 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 +44 -4
- package/cli.js +16 -4
- package/console.js +119 -0
- package/eval.js +75 -5
- package/insights.js +138 -0
- package/nav.js +88 -17
- package/package.json +5 -2
- package/pick.js +6 -1
- package/screenshot.js +6 -1
- package/start.js +53 -19
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# browser-cdp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/browser-cdp)
|
|
4
|
+
|
|
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
|
|
6
8
|
|
|
@@ -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]
|
|
@@ -25,6 +30,15 @@ browser-cdp screenshot
|
|
|
25
30
|
|
|
26
31
|
# Interactive element picker
|
|
27
32
|
browser-cdp pick '<message>'
|
|
33
|
+
|
|
34
|
+
# Stream browser console output (network errors, exceptions, logs)
|
|
35
|
+
browser-cdp console [--duration=SECONDS]
|
|
36
|
+
|
|
37
|
+
# Show page performance metrics
|
|
38
|
+
browser-cdp insights [--json]
|
|
39
|
+
|
|
40
|
+
# Run Lighthouse audit (Chrome only)
|
|
41
|
+
browser-cdp lighthouse [--json] [--category=NAME]
|
|
28
42
|
```
|
|
29
43
|
|
|
30
44
|
## Environment Variables
|
|
@@ -32,7 +46,7 @@ browser-cdp pick '<message>'
|
|
|
32
46
|
| Variable | Description | Default |
|
|
33
47
|
|----------|-------------|---------|
|
|
34
48
|
| `DEBUG_PORT` | CDP debugging port | `9222` |
|
|
35
|
-
| `BROWSER` | Browser to use (chrome, brave,
|
|
49
|
+
| `BROWSER` | Browser to use (chrome, brave, edge) | `chrome` |
|
|
36
50
|
| `BROWSER_PATH` | Custom browser executable path | (auto-detect) |
|
|
37
51
|
|
|
38
52
|
## Examples
|
|
@@ -41,6 +55,9 @@ browser-cdp pick '<message>'
|
|
|
41
55
|
# Start Brave with real profile
|
|
42
56
|
browser-cdp start brave
|
|
43
57
|
|
|
58
|
+
# Start Brave with specific profile (by name)
|
|
59
|
+
browser-cdp start brave --profile=Work
|
|
60
|
+
|
|
44
61
|
# Start Chrome on custom port
|
|
45
62
|
DEBUG_PORT=9333 browser-cdp start
|
|
46
63
|
|
|
@@ -54,6 +71,26 @@ browser-cdp screenshot
|
|
|
54
71
|
|
|
55
72
|
# Pick elements interactively
|
|
56
73
|
browser-cdp pick "Select the login button"
|
|
74
|
+
|
|
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
|
|
80
|
+
browser-cdp console --duration=10
|
|
81
|
+
|
|
82
|
+
# Get page performance insights
|
|
83
|
+
browser-cdp insights
|
|
84
|
+
# Returns: TTFB, First Paint, FCP, DOM loaded, resources, memory
|
|
85
|
+
|
|
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
|
|
57
94
|
```
|
|
58
95
|
|
|
59
96
|
## Pre-started Browser
|
|
@@ -78,7 +115,6 @@ browser-cdp nav https://example.com
|
|
|
78
115
|
|---------|---------|
|
|
79
116
|
| Chrome | `chrome` (default) |
|
|
80
117
|
| Brave | `brave` |
|
|
81
|
-
| Comet | `comet` |
|
|
82
118
|
| Edge | `edge` |
|
|
83
119
|
|
|
84
120
|
## Platform Support
|
|
@@ -101,6 +137,10 @@ Use `BROWSER_PATH` env var to override if your browser is installed elsewhere.
|
|
|
101
137
|
| Detection | Not detectable as automation | Automation flags present |
|
|
102
138
|
| Use case | Real-world testing, scraping | Isolated E2E tests |
|
|
103
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
|
+
|
|
104
144
|
## License
|
|
105
145
|
|
|
106
146
|
MIT
|
package/cli.js
CHANGED
|
@@ -12,10 +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",
|
|
21
|
+
console: "./console.js",
|
|
22
|
+
insights: "./insights.js",
|
|
23
|
+
lighthouse: "./lighthouse.js",
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
function printUsage() {
|
|
@@ -25,21 +30,28 @@ function printUsage() {
|
|
|
25
30
|
console.log("");
|
|
26
31
|
console.log("Commands:");
|
|
27
32
|
console.log(" start [browser] Start browser with CDP (uses real profile)");
|
|
28
|
-
console.log("
|
|
29
|
-
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");
|
|
30
37
|
console.log(" screenshot Take screenshot of current page");
|
|
31
38
|
console.log(" pick '<message>' Interactive element picker");
|
|
39
|
+
console.log(" console Stream browser console output");
|
|
40
|
+
console.log(" insights Show page performance metrics");
|
|
41
|
+
console.log(" lighthouse Run Lighthouse audit");
|
|
32
42
|
console.log("");
|
|
33
43
|
console.log("Environment:");
|
|
34
44
|
console.log(" DEBUG_PORT CDP port (default: 9222)");
|
|
35
|
-
console.log(" BROWSER Browser to use (chrome, brave,
|
|
45
|
+
console.log(" BROWSER Browser to use (chrome, brave, edge)");
|
|
36
46
|
console.log(" BROWSER_PATH Custom browser executable path");
|
|
37
47
|
console.log("");
|
|
38
48
|
console.log("Examples:");
|
|
39
49
|
console.log(" browser-cdp start brave");
|
|
40
50
|
console.log(" browser-cdp nav https://google.com");
|
|
41
51
|
console.log(" browser-cdp eval 'document.title'");
|
|
42
|
-
console.log(" browser-cdp
|
|
52
|
+
console.log(" browser-cdp dom > page.html");
|
|
53
|
+
console.log(" browser-cdp console --duration=10");
|
|
54
|
+
console.log(" browser-cdp insights --json");
|
|
43
55
|
process.exit(0);
|
|
44
56
|
}
|
|
45
57
|
|
package/console.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const duration = args.find((a) => a.startsWith("--duration="));
|
|
9
|
+
const durationMs = duration ? parseInt(duration.split("=")[1]) * 1000 : null;
|
|
10
|
+
const shouldReload = args.includes("--reload") || args.includes("-r");
|
|
11
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
12
|
+
|
|
13
|
+
if (showHelp) {
|
|
14
|
+
console.log("Usage: console.js [options]");
|
|
15
|
+
console.log("\nCapture browser console output in real-time.");
|
|
16
|
+
console.log("\nOptions:");
|
|
17
|
+
console.log(" --duration=N Stop after N seconds (default: run until Ctrl+C)");
|
|
18
|
+
console.log(" --reload, -r Reload the page before capturing");
|
|
19
|
+
console.log("\nExamples:");
|
|
20
|
+
console.log(" console.js # Stream console logs until Ctrl+C");
|
|
21
|
+
console.log(" console.js --duration=5 # Capture for 5 seconds");
|
|
22
|
+
console.log(" console.js --reload # Reload page and capture logs");
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
27
|
+
const contexts = browser.contexts();
|
|
28
|
+
const context = contexts[0];
|
|
29
|
+
|
|
30
|
+
if (!context) {
|
|
31
|
+
console.error("No browser context found");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pages = context.pages();
|
|
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];
|
|
42
|
+
|
|
43
|
+
if (!page) {
|
|
44
|
+
console.error("No active tab found");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.error(`Connected to: ${page.url()}`);
|
|
49
|
+
|
|
50
|
+
const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
|
|
51
|
+
|
|
52
|
+
const levelColors = {
|
|
53
|
+
verbose: "\x1b[90m", // gray
|
|
54
|
+
info: "\x1b[36m", // cyan
|
|
55
|
+
warning: "\x1b[33m", // yellow
|
|
56
|
+
error: "\x1b[31m", // red
|
|
57
|
+
};
|
|
58
|
+
const reset = "\x1b[0m";
|
|
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
|
|
88
|
+
page.on("console", (msg) => {
|
|
89
|
+
const type = msg.type();
|
|
90
|
+
const color = levelColors[type] || levelColors.info;
|
|
91
|
+
const text = msg.text();
|
|
92
|
+
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${text}${reset}`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
page.on("pageerror", (error) => {
|
|
96
|
+
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${reset}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (shouldReload) {
|
|
100
|
+
console.error("Reloading page...");
|
|
101
|
+
await page.reload();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.error(`Listening for console output... (Ctrl+C to stop)`);
|
|
105
|
+
|
|
106
|
+
if (durationMs) {
|
|
107
|
+
await new Promise((r) => setTimeout(r, durationMs));
|
|
108
|
+
await browser.close();
|
|
109
|
+
} else {
|
|
110
|
+
// Keep running until interrupted
|
|
111
|
+
process.on("SIGINT", async () => {
|
|
112
|
+
console.error("\nStopping...");
|
|
113
|
+
await browser.close();
|
|
114
|
+
process.exit(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Keep the process alive
|
|
118
|
+
await new Promise(() => {});
|
|
119
|
+
}
|
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
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
9
|
+
const jsonOutput = args.includes("--json");
|
|
10
|
+
|
|
11
|
+
if (showHelp) {
|
|
12
|
+
console.log("Usage: insights.js [--json]");
|
|
13
|
+
console.log("\nCollect page performance insights and Web Vitals.");
|
|
14
|
+
console.log("\nOptions:");
|
|
15
|
+
console.log(" --json Output as JSON");
|
|
16
|
+
console.log("\nMetrics collected:");
|
|
17
|
+
console.log(" - Page load timing (DOM, load, first paint)");
|
|
18
|
+
console.log(" - Web Vitals (LCP, FID, CLS, FCP, TTFB)");
|
|
19
|
+
console.log(" - Resource counts and sizes");
|
|
20
|
+
console.log(" - JavaScript heap usage");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
25
|
+
const contexts = browser.contexts();
|
|
26
|
+
const context = contexts[0];
|
|
27
|
+
|
|
28
|
+
if (!context) {
|
|
29
|
+
console.error("No browser context found");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pages = context.pages();
|
|
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];
|
|
40
|
+
|
|
41
|
+
if (!page) {
|
|
42
|
+
console.error("No active tab found");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Collect performance metrics
|
|
47
|
+
const metrics = await page.evaluate(() => {
|
|
48
|
+
const perf = performance;
|
|
49
|
+
const timing = perf.timing || {};
|
|
50
|
+
const navigation = perf.getEntriesByType("navigation")[0] || {};
|
|
51
|
+
const paint = perf.getEntriesByType("paint") || [];
|
|
52
|
+
const resources = perf.getEntriesByType("resource") || [];
|
|
53
|
+
|
|
54
|
+
// Calculate timing metrics
|
|
55
|
+
const navStart = timing.navigationStart || navigation.startTime || 0;
|
|
56
|
+
const domContentLoaded = (timing.domContentLoadedEventEnd || navigation.domContentLoadedEventEnd || 0) - navStart;
|
|
57
|
+
const loadComplete = (timing.loadEventEnd || navigation.loadEventEnd || 0) - navStart;
|
|
58
|
+
const firstPaint = paint.find((p) => p.name === "first-paint")?.startTime || 0;
|
|
59
|
+
const firstContentfulPaint = paint.find((p) => p.name === "first-contentful-paint")?.startTime || 0;
|
|
60
|
+
const ttfb = (timing.responseStart || navigation.responseStart || 0) - navStart;
|
|
61
|
+
|
|
62
|
+
// Resource breakdown
|
|
63
|
+
const resourceStats = resources.reduce(
|
|
64
|
+
(acc, r) => {
|
|
65
|
+
acc.count++;
|
|
66
|
+
acc.totalSize += r.transferSize || 0;
|
|
67
|
+
const type = r.initiatorType || "other";
|
|
68
|
+
acc.byType[type] = (acc.byType[type] || 0) + 1;
|
|
69
|
+
return acc;
|
|
70
|
+
},
|
|
71
|
+
{ count: 0, totalSize: 0, byType: {} }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Try to get LCP (requires PerformanceObserver to have recorded it)
|
|
75
|
+
let lcp = null;
|
|
76
|
+
try {
|
|
77
|
+
const lcpEntries = perf.getEntriesByType("largest-contentful-paint");
|
|
78
|
+
if (lcpEntries.length > 0) {
|
|
79
|
+
lcp = lcpEntries[lcpEntries.length - 1].startTime;
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {}
|
|
82
|
+
|
|
83
|
+
// Memory info (Chrome only)
|
|
84
|
+
let memory = null;
|
|
85
|
+
if (perf.memory) {
|
|
86
|
+
memory = {
|
|
87
|
+
usedJSHeapSize: Math.round(perf.memory.usedJSHeapSize / 1024 / 1024),
|
|
88
|
+
totalJSHeapSize: Math.round(perf.memory.totalJSHeapSize / 1024 / 1024),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
url: location.href,
|
|
94
|
+
timing: {
|
|
95
|
+
ttfb: Math.round(ttfb),
|
|
96
|
+
firstPaint: Math.round(firstPaint),
|
|
97
|
+
firstContentfulPaint: Math.round(firstContentfulPaint),
|
|
98
|
+
domContentLoaded: Math.round(domContentLoaded),
|
|
99
|
+
loadComplete: Math.round(loadComplete),
|
|
100
|
+
lcp: lcp ? Math.round(lcp) : null,
|
|
101
|
+
},
|
|
102
|
+
resources: {
|
|
103
|
+
count: resourceStats.count,
|
|
104
|
+
totalSizeKB: Math.round(resourceStats.totalSize / 1024),
|
|
105
|
+
byType: resourceStats.byType,
|
|
106
|
+
},
|
|
107
|
+
memory,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (jsonOutput) {
|
|
112
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
113
|
+
} else {
|
|
114
|
+
console.log(`Page Insights: ${metrics.url}\n`);
|
|
115
|
+
|
|
116
|
+
console.log("Timing:");
|
|
117
|
+
console.log(` TTFB: ${metrics.timing.ttfb}ms`);
|
|
118
|
+
console.log(` First Paint: ${metrics.timing.firstPaint}ms`);
|
|
119
|
+
console.log(` First Contentful Paint: ${metrics.timing.firstContentfulPaint}ms`);
|
|
120
|
+
console.log(` DOM Content Loaded: ${metrics.timing.domContentLoaded}ms`);
|
|
121
|
+
console.log(` Load Complete: ${metrics.timing.loadComplete}ms`);
|
|
122
|
+
if (metrics.timing.lcp) {
|
|
123
|
+
console.log(` Largest Contentful Paint: ${metrics.timing.lcp}ms`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log("\nResources:");
|
|
127
|
+
console.log(` Total: ${metrics.resources.count} requests`);
|
|
128
|
+
console.log(` Size: ${metrics.resources.totalSizeKB} KB`);
|
|
129
|
+
console.log(` Breakdown: ${Object.entries(metrics.resources.byType).map(([k, v]) => `${k}(${v})`).join(", ")}`);
|
|
130
|
+
|
|
131
|
+
if (metrics.memory) {
|
|
132
|
+
console.log("\nMemory:");
|
|
133
|
+
console.log(` JS Heap Used: ${metrics.memory.usedJSHeapSize} MB`);
|
|
134
|
+
console.log(` JS Heap Total: ${metrics.memory.totalJSHeapSize} MB`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await browser.close();
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-cdp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Browser automation via Chrome DevTools Protocol - control Chrome, Brave,
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Browser automation via Chrome DevTools Protocol - control Chrome, Brave, Edge with real browser profiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"browser-cdp": "./cli.js"
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"eval.js",
|
|
17
17
|
"screenshot.js",
|
|
18
18
|
"pick.js",
|
|
19
|
+
"console.js",
|
|
20
|
+
"insights.js",
|
|
19
21
|
"README.md",
|
|
20
22
|
"LICENSE"
|
|
21
23
|
],
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
"url": "https://github.com/dpaluy/browser-cdp/issues"
|
|
32
34
|
},
|
|
33
35
|
"dependencies": {
|
|
36
|
+
"lighthouse": "^13.0.1",
|
|
34
37
|
"playwright": "^1.49.0"
|
|
35
38
|
},
|
|
36
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
|
|
|
@@ -30,12 +30,6 @@ const BROWSERS = {
|
|
|
30
30
|
? `${process.env.HOME}/Library/Application Support/BraveSoftware/Brave-Browser/`
|
|
31
31
|
: `${process.env.HOME}/.config/BraveSoftware/Brave-Browser/`,
|
|
32
32
|
},
|
|
33
|
-
comet: {
|
|
34
|
-
name: "Comet",
|
|
35
|
-
path: isMac ? "/Applications/Comet.app/Contents/MacOS/Comet" : "/usr/bin/comet",
|
|
36
|
-
process: "Comet",
|
|
37
|
-
profileSource: null,
|
|
38
|
-
},
|
|
39
33
|
edge: {
|
|
40
34
|
name: "Microsoft Edge",
|
|
41
35
|
path: isMac
|
|
@@ -50,25 +44,53 @@ const BROWSERS = {
|
|
|
50
44
|
|
|
51
45
|
const DEFAULT_PORT = 9222;
|
|
52
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
|
+
|
|
53
74
|
function printUsage() {
|
|
54
|
-
console.log("Usage: start.js [browser] [--isolated] [--port=PORT]");
|
|
75
|
+
console.log("Usage: start.js [browser] [--profile=NAME] [--isolated] [--port=PORT]");
|
|
55
76
|
console.log("\nBrowsers:");
|
|
56
77
|
console.log(" chrome - Google Chrome (default)");
|
|
57
78
|
console.log(" brave - Brave Browser");
|
|
58
|
-
console.log(" comet - Comet Browser");
|
|
59
79
|
console.log(" edge - Microsoft Edge");
|
|
60
80
|
console.log("\nOptions:");
|
|
61
|
-
console.log(" --
|
|
62
|
-
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)");
|
|
63
84
|
console.log("\nEnvironment variables:");
|
|
64
|
-
console.log(" BROWSER Default browser (chrome, brave,
|
|
85
|
+
console.log(" BROWSER Default browser (chrome, brave, edge)");
|
|
65
86
|
console.log(" BROWSER_PATH Custom browser executable path");
|
|
66
87
|
console.log(" DEBUG_PORT Custom debugging port");
|
|
67
88
|
console.log("\nExamples:");
|
|
68
|
-
console.log(" start.js
|
|
69
|
-
console.log(" start.js brave
|
|
70
|
-
console.log(" start.js
|
|
71
|
-
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");
|
|
72
94
|
process.exit(1);
|
|
73
95
|
}
|
|
74
96
|
|
|
@@ -76,6 +98,7 @@ function printUsage() {
|
|
|
76
98
|
const args = process.argv.slice(2);
|
|
77
99
|
let browserName = process.env.BROWSER || "chrome";
|
|
78
100
|
let isolated = false;
|
|
101
|
+
let profile = null;
|
|
79
102
|
let port = parseInt(process.env.DEBUG_PORT) || DEFAULT_PORT;
|
|
80
103
|
|
|
81
104
|
for (const arg of args) {
|
|
@@ -83,6 +106,8 @@ for (const arg of args) {
|
|
|
83
106
|
printUsage();
|
|
84
107
|
} else if (arg === "--isolated") {
|
|
85
108
|
isolated = true;
|
|
109
|
+
} else if (arg.startsWith("--profile=")) {
|
|
110
|
+
profile = arg.split("=")[1];
|
|
86
111
|
} else if (arg.startsWith("--port=")) {
|
|
87
112
|
port = parseInt(arg.split("=")[1]);
|
|
88
113
|
} else if (BROWSERS[arg]) {
|
|
@@ -162,7 +187,11 @@ if (!isolated && browserConfig.process) {
|
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
// Build browser arguments
|
|
165
|
-
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
|
+
];
|
|
166
195
|
|
|
167
196
|
if (isolated) {
|
|
168
197
|
const cacheBase = isMac
|
|
@@ -171,10 +200,15 @@ if (isolated) {
|
|
|
171
200
|
const profileDir = `${cacheBase}/browser-cdp/${browserName}`;
|
|
172
201
|
execFileSync("mkdir", ["-p", profileDir], { stdio: "ignore" });
|
|
173
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}`);
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
// Start browser
|
|
177
|
-
|
|
210
|
+
const profileInfo = isolated ? " (isolated)" : profile ? ` (${profile})` : "";
|
|
211
|
+
console.log(`Starting ${browserConfig.name} on port ${port}${profileInfo}...`);
|
|
178
212
|
|
|
179
213
|
spawn(browserPath, browserArgs, {
|
|
180
214
|
detached: true,
|
|
@@ -199,4 +233,4 @@ if (!connected) {
|
|
|
199
233
|
process.exit(1);
|
|
200
234
|
}
|
|
201
235
|
|
|
202
|
-
console.log(`${browserConfig.name} started on :${port}${
|
|
236
|
+
console.log(`${browserConfig.name} started on :${port}${profileInfo}`);
|