browser-cdp 0.1.0
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/LICENSE +21 -0
- package/README.md +106 -0
- package/cli.js +51 -0
- package/eval.js +62 -0
- package/nav.js +33 -0
- package/package.json +47 -0
- package/pick.js +162 -0
- package/screenshot.js +34 -0
- package/start.js +186 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Majestic Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# browser-cdp
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g browser-cdp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Start browser with CDP enabled
|
|
15
|
+
browser-cdp start [browser] [--profile] [--port=PORT]
|
|
16
|
+
|
|
17
|
+
# Navigate to URL
|
|
18
|
+
browser-cdp nav <url> [--new]
|
|
19
|
+
|
|
20
|
+
# Execute JavaScript in page
|
|
21
|
+
browser-cdp eval '<code>'
|
|
22
|
+
|
|
23
|
+
# Take screenshot
|
|
24
|
+
browser-cdp screenshot
|
|
25
|
+
|
|
26
|
+
# Interactive element picker
|
|
27
|
+
browser-cdp pick '<message>'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Environment Variables
|
|
31
|
+
|
|
32
|
+
| Variable | Description | Default |
|
|
33
|
+
|----------|-------------|---------|
|
|
34
|
+
| `DEBUG_PORT` | CDP debugging port | `9222` |
|
|
35
|
+
| `BROWSER` | Browser to use (chrome, brave, comet, edge) | `chrome` |
|
|
36
|
+
| `BROWSER_PATH` | Custom browser executable path | (auto-detect) |
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Start Brave with your profile
|
|
42
|
+
browser-cdp start brave --profile
|
|
43
|
+
|
|
44
|
+
# Start Chrome on custom port
|
|
45
|
+
DEBUG_PORT=9333 browser-cdp start
|
|
46
|
+
|
|
47
|
+
# Navigate and search
|
|
48
|
+
browser-cdp nav https://google.com
|
|
49
|
+
browser-cdp eval 'document.querySelector("textarea").value = "hello"'
|
|
50
|
+
|
|
51
|
+
# Take screenshot
|
|
52
|
+
browser-cdp screenshot
|
|
53
|
+
# Returns: /tmp/screenshot-2024-01-01T12-00-00.png
|
|
54
|
+
|
|
55
|
+
# Pick elements interactively
|
|
56
|
+
browser-cdp pick "Select the login button"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Pre-started Browser
|
|
60
|
+
|
|
61
|
+
If you already have a browser running with CDP enabled, the CLI will connect to it:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Add to ~/.zshrc
|
|
65
|
+
alias brave-debug='/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser --remote-debugging-port=9222'
|
|
66
|
+
|
|
67
|
+
# Start browser manually
|
|
68
|
+
brave-debug
|
|
69
|
+
|
|
70
|
+
# CLI detects and connects
|
|
71
|
+
browser-cdp start # "Browser already running on :9222"
|
|
72
|
+
browser-cdp nav https://example.com
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Supported Browsers
|
|
76
|
+
|
|
77
|
+
| Browser | Command | Profile Support |
|
|
78
|
+
|---------|---------|-----------------|
|
|
79
|
+
| Chrome | `chrome` (default) | Yes |
|
|
80
|
+
| Brave | `brave` | Yes |
|
|
81
|
+
| Comet | `comet` | No |
|
|
82
|
+
| Edge | `edge` | Yes |
|
|
83
|
+
|
|
84
|
+
## Platform Support
|
|
85
|
+
|
|
86
|
+
Works on **macOS** and **Linux**. Browser paths auto-detected per platform.
|
|
87
|
+
|
|
88
|
+
| Platform | Chrome Path | Config Path |
|
|
89
|
+
|----------|-------------|-------------|
|
|
90
|
+
| macOS | `/Applications/Google Chrome.app/...` | `~/Library/Application Support/Google/Chrome/` |
|
|
91
|
+
| Linux | `/usr/bin/google-chrome` | `~/.config/google-chrome/` |
|
|
92
|
+
|
|
93
|
+
Use `BROWSER_PATH` env var to override if your browser is installed elsewhere.
|
|
94
|
+
|
|
95
|
+
## Why Real Browser?
|
|
96
|
+
|
|
97
|
+
| Aspect | browser-cdp | Playwright Test Mode |
|
|
98
|
+
|--------|-------------|---------------------|
|
|
99
|
+
| Browser | Your installed Chrome/Brave/etc | Bundled Chromium |
|
|
100
|
+
| Profile | Can use real cookies/logins | Fresh test profile |
|
|
101
|
+
| Detection | Not detectable as automation | Automation flags present |
|
|
102
|
+
| Use case | Real-world testing, scraping | Isolated E2E tests |
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const command = process.argv[2];
|
|
4
|
+
const args = process.argv.slice(3);
|
|
5
|
+
|
|
6
|
+
const commands = {
|
|
7
|
+
start: "./start.js",
|
|
8
|
+
nav: "./nav.js",
|
|
9
|
+
eval: "./eval.js",
|
|
10
|
+
screenshot: "./screenshot.js",
|
|
11
|
+
pick: "./pick.js",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function printUsage() {
|
|
15
|
+
console.log("browser-cdp - Browser automation via Chrome DevTools Protocol");
|
|
16
|
+
console.log("");
|
|
17
|
+
console.log("Usage: browser-cdp <command> [options]");
|
|
18
|
+
console.log("");
|
|
19
|
+
console.log("Commands:");
|
|
20
|
+
console.log(" start [browser] Start browser with CDP enabled");
|
|
21
|
+
console.log(" nav <url> Navigate to URL");
|
|
22
|
+
console.log(" eval '<code>' Evaluate JavaScript in page");
|
|
23
|
+
console.log(" screenshot Take screenshot of current page");
|
|
24
|
+
console.log(" pick '<message>' Interactive element picker");
|
|
25
|
+
console.log("");
|
|
26
|
+
console.log("Environment:");
|
|
27
|
+
console.log(" DEBUG_PORT CDP port (default: 9222)");
|
|
28
|
+
console.log(" BROWSER Browser to use (chrome, brave, comet, edge)");
|
|
29
|
+
console.log(" BROWSER_PATH Custom browser executable path");
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log("Examples:");
|
|
32
|
+
console.log(" browser-cdp start brave --profile");
|
|
33
|
+
console.log(" browser-cdp nav https://google.com");
|
|
34
|
+
console.log(" browser-cdp eval 'document.title'");
|
|
35
|
+
console.log(" browser-cdp screenshot");
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!command || command === "--help" || command === "-h") {
|
|
40
|
+
printUsage();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!commands[command]) {
|
|
44
|
+
console.error(`Unknown command: ${command}`);
|
|
45
|
+
console.log(`Available: ${Object.keys(commands).join(", ")}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Re-run with the specific command script
|
|
50
|
+
process.argv = [process.argv[0], commands[command], ...args];
|
|
51
|
+
await import(commands[command]);
|
package/eval.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
6
|
+
|
|
7
|
+
const code = process.argv.slice(2).join(" ");
|
|
8
|
+
if (!code) {
|
|
9
|
+
console.log("Usage: eval.js 'code'");
|
|
10
|
+
console.log("\nExamples:");
|
|
11
|
+
console.log(' eval.js "document.title"');
|
|
12
|
+
console.log(" eval.js \"document.querySelectorAll('a').length\"");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
17
|
+
const contexts = browser.contexts();
|
|
18
|
+
const context = contexts[0];
|
|
19
|
+
|
|
20
|
+
if (!context) {
|
|
21
|
+
console.error("No browser context found");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pages = context.pages();
|
|
26
|
+
const page = pages[pages.length - 1];
|
|
27
|
+
|
|
28
|
+
if (!page) {
|
|
29
|
+
console.error("No active tab found");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let result;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
result = await page.evaluate((c) => {
|
|
37
|
+
const AsyncFunction = (async () => {}).constructor;
|
|
38
|
+
return new AsyncFunction(`return (${c})`)();
|
|
39
|
+
}, code);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.log("Failed to evaluate expression");
|
|
42
|
+
console.log(` Expression: ${code}`);
|
|
43
|
+
console.log(e);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(result)) {
|
|
48
|
+
for (let i = 0; i < result.length; i++) {
|
|
49
|
+
if (i > 0) console.log("");
|
|
50
|
+
for (const [key, value] of Object.entries(result[i])) {
|
|
51
|
+
console.log(`${key}: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else if (typeof result === "object" && result !== null) {
|
|
55
|
+
for (const [key, value] of Object.entries(result)) {
|
|
56
|
+
console.log(`${key}: ${value}`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
console.log(result);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await browser.close();
|
package/nav.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
6
|
+
|
|
7
|
+
const url = process.argv[2];
|
|
8
|
+
const newTab = process.argv[3] === "--new";
|
|
9
|
+
|
|
10
|
+
if (!url) {
|
|
11
|
+
console.log("Usage: nav.js <url> [--new]");
|
|
12
|
+
console.log("\nExamples:");
|
|
13
|
+
console.log(" nav.js https://example.com # Navigate current tab");
|
|
14
|
+
console.log(" nav.js https://example.com --new # Open in new tab");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
19
|
+
const contexts = browser.contexts();
|
|
20
|
+
const context = contexts[0] || await browser.newContext();
|
|
21
|
+
|
|
22
|
+
if (newTab) {
|
|
23
|
+
const page = await context.newPage();
|
|
24
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
25
|
+
console.log("Opened:", url);
|
|
26
|
+
} else {
|
|
27
|
+
const pages = context.pages();
|
|
28
|
+
const page = pages[pages.length - 1] || await context.newPage();
|
|
29
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
30
|
+
console.log("Navigated to:", url);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await browser.close();
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browser-cdp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser automation via Chrome DevTools Protocol - control Chrome, Brave, Comet, Edge with real browser profiles",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"browser-cdp": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"cli.js",
|
|
14
|
+
"start.js",
|
|
15
|
+
"nav.js",
|
|
16
|
+
"eval.js",
|
|
17
|
+
"screenshot.js",
|
|
18
|
+
"pick.js",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/dpaluy/browser-cdp.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/dpaluy/browser-cdp#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/dpaluy/browser-cdp/issues"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"playwright": "^1.49.0"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"browser",
|
|
38
|
+
"automation",
|
|
39
|
+
"cdp",
|
|
40
|
+
"chrome",
|
|
41
|
+
"brave",
|
|
42
|
+
"playwright",
|
|
43
|
+
"devtools"
|
|
44
|
+
],
|
|
45
|
+
"author": "Majestic Labs",
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
package/pick.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
6
|
+
|
|
7
|
+
const message = process.argv.slice(2).join(" ");
|
|
8
|
+
if (!message) {
|
|
9
|
+
console.log("Usage: pick.js 'message'");
|
|
10
|
+
console.log("\nExample:");
|
|
11
|
+
console.log(' pick.js "Click the submit button"');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
16
|
+
const contexts = browser.contexts();
|
|
17
|
+
const context = contexts[0];
|
|
18
|
+
|
|
19
|
+
if (!context) {
|
|
20
|
+
console.error("No browser context found");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pages = context.pages();
|
|
25
|
+
const page = pages[pages.length - 1];
|
|
26
|
+
|
|
27
|
+
if (!page) {
|
|
28
|
+
console.error("No active tab found");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Inject pick() helper into current page
|
|
33
|
+
await page.evaluate(() => {
|
|
34
|
+
if (!window.pick) {
|
|
35
|
+
window.pick = async (message) => {
|
|
36
|
+
if (!message) {
|
|
37
|
+
throw new Error("pick() requires a message parameter");
|
|
38
|
+
}
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const selections = [];
|
|
41
|
+
const selectedElements = new Set();
|
|
42
|
+
|
|
43
|
+
const overlay = document.createElement("div");
|
|
44
|
+
overlay.style.cssText =
|
|
45
|
+
"position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";
|
|
46
|
+
|
|
47
|
+
const highlight = document.createElement("div");
|
|
48
|
+
highlight.style.cssText =
|
|
49
|
+
"position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.1s";
|
|
50
|
+
overlay.appendChild(highlight);
|
|
51
|
+
|
|
52
|
+
const banner = document.createElement("div");
|
|
53
|
+
banner.style.cssText =
|
|
54
|
+
"position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647";
|
|
55
|
+
|
|
56
|
+
const updateBanner = () => {
|
|
57
|
+
banner.textContent = `${message} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
|
|
58
|
+
};
|
|
59
|
+
updateBanner();
|
|
60
|
+
|
|
61
|
+
document.body.append(banner, overlay);
|
|
62
|
+
|
|
63
|
+
const cleanup = () => {
|
|
64
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
65
|
+
document.removeEventListener("click", onClick, true);
|
|
66
|
+
document.removeEventListener("keydown", onKey, true);
|
|
67
|
+
overlay.remove();
|
|
68
|
+
banner.remove();
|
|
69
|
+
selectedElements.forEach((el) => {
|
|
70
|
+
el.style.outline = "";
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onMove = (e) => {
|
|
75
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
76
|
+
if (!el || overlay.contains(el) || banner.contains(el)) return;
|
|
77
|
+
const r = el.getBoundingClientRect();
|
|
78
|
+
highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${r.top}px;left:${r.left}px;width:${r.width}px;height:${r.height}px`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const buildElementInfo = (el) => {
|
|
82
|
+
const parents = [];
|
|
83
|
+
let current = el.parentElement;
|
|
84
|
+
while (current && current !== document.body) {
|
|
85
|
+
const parentInfo = current.tagName.toLowerCase();
|
|
86
|
+
const id = current.id ? `#${current.id}` : "";
|
|
87
|
+
const cls = current.className
|
|
88
|
+
? `.${current.className.trim().split(/\s+/).join(".")}`
|
|
89
|
+
: "";
|
|
90
|
+
parents.push(parentInfo + id + cls);
|
|
91
|
+
current = current.parentElement;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
tag: el.tagName.toLowerCase(),
|
|
96
|
+
id: el.id || null,
|
|
97
|
+
class: el.className || null,
|
|
98
|
+
text: el.textContent?.trim().slice(0, 200) || null,
|
|
99
|
+
html: el.outerHTML.slice(0, 500),
|
|
100
|
+
parents: parents.join(" > "),
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onClick = (e) => {
|
|
105
|
+
if (banner.contains(e.target)) return;
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
109
|
+
if (!el || overlay.contains(el) || banner.contains(el)) return;
|
|
110
|
+
|
|
111
|
+
if (e.metaKey || e.ctrlKey) {
|
|
112
|
+
if (!selectedElements.has(el)) {
|
|
113
|
+
selectedElements.add(el);
|
|
114
|
+
el.style.outline = "3px solid #10b981";
|
|
115
|
+
selections.push(buildElementInfo(el));
|
|
116
|
+
updateBanner();
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
cleanup();
|
|
120
|
+
const info = buildElementInfo(el);
|
|
121
|
+
resolve(selections.length > 0 ? selections : info);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const onKey = (e) => {
|
|
126
|
+
if (e.key === "Escape") {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
cleanup();
|
|
129
|
+
resolve(null);
|
|
130
|
+
} else if (e.key === "Enter" && selections.length > 0) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
cleanup();
|
|
133
|
+
resolve(selections);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
document.addEventListener("mousemove", onMove, true);
|
|
138
|
+
document.addEventListener("click", onClick, true);
|
|
139
|
+
document.addEventListener("keydown", onKey, true);
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = await page.evaluate((msg) => window.pick(msg), message);
|
|
146
|
+
|
|
147
|
+
if (Array.isArray(result)) {
|
|
148
|
+
for (let i = 0; i < result.length; i++) {
|
|
149
|
+
if (i > 0) console.log("");
|
|
150
|
+
for (const [key, value] of Object.entries(result[i])) {
|
|
151
|
+
console.log(`${key}: ${value}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else if (typeof result === "object" && result !== null) {
|
|
155
|
+
for (const [key, value] of Object.entries(result)) {
|
|
156
|
+
console.log(`${key}: ${value}`);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
console.log(result);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await browser.close();
|
package/screenshot.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { chromium } from "playwright";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
|
|
8
|
+
|
|
9
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
10
|
+
const contexts = browser.contexts();
|
|
11
|
+
const context = contexts[0];
|
|
12
|
+
|
|
13
|
+
if (!context) {
|
|
14
|
+
console.error("No browser context found");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pages = context.pages();
|
|
19
|
+
const page = pages[pages.length - 1];
|
|
20
|
+
|
|
21
|
+
if (!page) {
|
|
22
|
+
console.error("No active tab found");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
27
|
+
const filename = `screenshot-${timestamp}.png`;
|
|
28
|
+
const filepath = join(tmpdir(), filename);
|
|
29
|
+
|
|
30
|
+
await page.screenshot({ path: filepath });
|
|
31
|
+
|
|
32
|
+
console.log(filepath);
|
|
33
|
+
|
|
34
|
+
await browser.close();
|
package/start.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { platform } from "node:os";
|
|
6
|
+
import { chromium } from "playwright";
|
|
7
|
+
|
|
8
|
+
const isMac = platform() === "darwin";
|
|
9
|
+
const isLinux = platform() === "linux";
|
|
10
|
+
|
|
11
|
+
// Browser configurations per platform
|
|
12
|
+
const BROWSERS = {
|
|
13
|
+
chrome: {
|
|
14
|
+
name: "Google Chrome",
|
|
15
|
+
path: isMac
|
|
16
|
+
? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
17
|
+
: "/usr/bin/google-chrome",
|
|
18
|
+
process: "Google Chrome",
|
|
19
|
+
profileSource: isMac
|
|
20
|
+
? `${process.env.HOME}/Library/Application Support/Google/Chrome/`
|
|
21
|
+
: `${process.env.HOME}/.config/google-chrome/`,
|
|
22
|
+
},
|
|
23
|
+
brave: {
|
|
24
|
+
name: "Brave",
|
|
25
|
+
path: isMac
|
|
26
|
+
? "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
27
|
+
: "/usr/bin/brave-browser",
|
|
28
|
+
process: "Brave Browser",
|
|
29
|
+
profileSource: isMac
|
|
30
|
+
? `${process.env.HOME}/Library/Application Support/BraveSoftware/Brave-Browser/`
|
|
31
|
+
: `${process.env.HOME}/.config/BraveSoftware/Brave-Browser/`,
|
|
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
|
+
edge: {
|
|
40
|
+
name: "Microsoft Edge",
|
|
41
|
+
path: isMac
|
|
42
|
+
? "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
|
43
|
+
: "/usr/bin/microsoft-edge",
|
|
44
|
+
process: "Microsoft Edge",
|
|
45
|
+
profileSource: isMac
|
|
46
|
+
? `${process.env.HOME}/Library/Application Support/Microsoft Edge/`
|
|
47
|
+
: `${process.env.HOME}/.config/microsoft-edge/`,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const DEFAULT_PORT = 9222;
|
|
52
|
+
|
|
53
|
+
function printUsage() {
|
|
54
|
+
console.log("Usage: start.js [browser] [--profile] [--port=PORT]");
|
|
55
|
+
console.log("\nBrowsers:");
|
|
56
|
+
console.log(" chrome - Google Chrome (default)");
|
|
57
|
+
console.log(" brave - Brave Browser");
|
|
58
|
+
console.log(" comet - Comet Browser");
|
|
59
|
+
console.log(" edge - Microsoft Edge");
|
|
60
|
+
console.log("\nOptions:");
|
|
61
|
+
console.log(" --profile Copy your browser profile (cookies, logins)");
|
|
62
|
+
console.log(" --port=N Use custom debugging port (default: 9222)");
|
|
63
|
+
console.log("\nEnvironment variables:");
|
|
64
|
+
console.log(" BROWSER Default browser (chrome, brave, comet, edge)");
|
|
65
|
+
console.log(" BROWSER_PATH Custom browser executable path");
|
|
66
|
+
console.log(" DEBUG_PORT Custom debugging port");
|
|
67
|
+
console.log("\nExamples:");
|
|
68
|
+
console.log(" start.js # Start Chrome with fresh profile");
|
|
69
|
+
console.log(" start.js brave --profile # Start Brave with your profile");
|
|
70
|
+
console.log(" start.js comet # Start Comet");
|
|
71
|
+
console.log(" start.js --port=9333 # Start Chrome on port 9333");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse arguments
|
|
76
|
+
const args = process.argv.slice(2);
|
|
77
|
+
let browserName = process.env.BROWSER || "chrome";
|
|
78
|
+
let useProfile = false;
|
|
79
|
+
let port = parseInt(process.env.DEBUG_PORT) || DEFAULT_PORT;
|
|
80
|
+
|
|
81
|
+
for (const arg of args) {
|
|
82
|
+
if (arg === "--help" || arg === "-h") {
|
|
83
|
+
printUsage();
|
|
84
|
+
} else if (arg === "--profile") {
|
|
85
|
+
useProfile = true;
|
|
86
|
+
} else if (arg.startsWith("--port=")) {
|
|
87
|
+
port = parseInt(arg.split("=")[1]);
|
|
88
|
+
} else if (BROWSERS[arg]) {
|
|
89
|
+
browserName = arg;
|
|
90
|
+
} else if (!arg.startsWith("--")) {
|
|
91
|
+
console.error(`Unknown browser: ${arg}`);
|
|
92
|
+
console.log(`Available: ${Object.keys(BROWSERS).join(", ")}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Resolve browser config
|
|
98
|
+
const browserPath = process.env.BROWSER_PATH || BROWSERS[browserName]?.path;
|
|
99
|
+
const browserConfig = BROWSERS[browserName] || {
|
|
100
|
+
name: "Custom",
|
|
101
|
+
path: browserPath,
|
|
102
|
+
process: null,
|
|
103
|
+
profileSource: null,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!browserPath) {
|
|
107
|
+
console.error("No browser path specified");
|
|
108
|
+
printUsage();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!existsSync(browserPath)) {
|
|
112
|
+
console.error(`Browser not found: ${browserPath}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if port is available or has a browser with CDP
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(`http://localhost:${port}/json/version`);
|
|
119
|
+
if (response.ok) {
|
|
120
|
+
// CDP endpoint responded - browser already running
|
|
121
|
+
console.log(`Browser already running on :${port}`);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
} else {
|
|
124
|
+
// Port occupied by non-CDP process
|
|
125
|
+
console.error(`Error: Port ${port} is in use by another process (not a browser with CDP)`);
|
|
126
|
+
console.error(`Try a different port: ./start.js --port=9333`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err.cause?.code === "ECONNREFUSED") {
|
|
131
|
+
// Port is free, proceed to start browser
|
|
132
|
+
} else {
|
|
133
|
+
// Port occupied but not responding to CDP
|
|
134
|
+
console.error(`Error: Port ${port} is in use by another process`);
|
|
135
|
+
console.error(`Try a different port: ./start.js --port=9333`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Setup profile directory
|
|
141
|
+
const cacheBase = isMac
|
|
142
|
+
? `${process.env.HOME}/Library/Caches`
|
|
143
|
+
: `${process.env.HOME}/.cache`;
|
|
144
|
+
const profileDir = `${cacheBase}/browser-cdp/${browserName}`;
|
|
145
|
+
execFileSync("mkdir", ["-p", profileDir], { stdio: "ignore" });
|
|
146
|
+
|
|
147
|
+
if (useProfile && browserConfig.profileSource) {
|
|
148
|
+
console.log(`Syncing profile from ${browserConfig.profileSource}...`);
|
|
149
|
+
try {
|
|
150
|
+
execFileSync("rsync", ["-a", "--delete", browserConfig.profileSource, `${profileDir}/`], {
|
|
151
|
+
stdio: "pipe",
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
console.warn("Warning: Could not sync profile, using fresh profile");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Start browser
|
|
159
|
+
console.log(`Starting ${browserConfig.name} on port ${port}...`);
|
|
160
|
+
|
|
161
|
+
spawn(browserPath, [`--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`], {
|
|
162
|
+
detached: true,
|
|
163
|
+
stdio: "ignore",
|
|
164
|
+
}).unref();
|
|
165
|
+
|
|
166
|
+
// Wait for browser to be ready
|
|
167
|
+
let connected = false;
|
|
168
|
+
for (let i = 0; i < 30; i++) {
|
|
169
|
+
try {
|
|
170
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
171
|
+
await browser.close();
|
|
172
|
+
connected = true;
|
|
173
|
+
break;
|
|
174
|
+
} catch {
|
|
175
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!connected) {
|
|
180
|
+
console.error(`Failed to connect to ${browserConfig.name} on port ${port}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(
|
|
185
|
+
`${browserConfig.name} started on :${port}${useProfile ? " with your profile" : ""}`
|
|
186
|
+
);
|