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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # browser-cdp
2
2
 
3
- Browser automation via Chrome DevTools Protocol. Control Chrome, Brave, Comet, or Edge using your real browser - same fingerprint, real cookies, no automation detection.
3
+ [![npm version](https://img.shields.io/npm/v/browser-cdp.svg)](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, comet, edge) | `chrome` |
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(" nav <url> Navigate to URL");
29
- console.log(" eval '<code>' Evaluate JavaScript in page");
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, comet, edge)");
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 screenshot");
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 code = process.argv.slice(2).join(" ");
8
- if (!code) {
9
- console.log("Usage: eval.js 'code'");
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
- process.exit(1);
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
- const page = pages[pages.length - 1];
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
- let url = process.argv[2];
8
- const newTab = process.argv[3] === "--new";
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
- // Add protocol if missing
11
- if (url && !url.match(/^https?:\/\//i)) {
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> [--new]");
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 # Navigate current tab");
19
- console.log(" nav.js example.com --new # Open in new tab");
20
- process.exit(1);
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
- const page = await context.newPage();
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 page = pages[pages.length - 1] || await context.newPage();
34
- await page.goto(url, { waitUntil: "domcontentloaded" });
35
- console.log("Navigated to:", url);
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.2.1",
4
- "description": "Browser automation via Chrome DevTools Protocol - control Chrome, Brave, Comet, Edge with real browser profiles",
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
- const page = pages[pages.length - 1];
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
- const page = pages[pages.length - 1];
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(" --isolated Use isolated profile (default: real profile)");
62
- console.log(" --port=N Use custom debugging port (default: 9222)");
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, comet, edge)");
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 # Start Chrome with real profile");
69
- console.log(" start.js brave # Start Brave with real profile");
70
- console.log(" start.js comet --isolated # Start Comet with isolated profile");
71
- console.log(" start.js --port=9333 # Start Chrome on port 9333");
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 = [`--remote-debugging-port=${port}`];
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
- console.log(`Starting ${browserConfig.name} on port ${port}${isolated ? " (isolated)" : ""}...`);
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}${isolated ? " (isolated)" : ""}`);
236
+ console.log(`${browserConfig.name} started on :${port}${profileInfo}`);