@docyrus/docyrus 0.0.58 → 0.0.60
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 +46 -0
- package/agent-loader.js +1 -1
- package/agent-loader.js.map +2 -2
- package/main.js +361 -23
- package/main.js.map +2 -2
- package/package.json +1 -1
- package/resources/browser-tools/browser-click.js +74 -0
- package/resources/browser-tools/browser-client.js +236 -0
- package/resources/browser-tools/browser-close.js +19 -0
- package/resources/browser-tools/browser-console.js +73 -0
- package/resources/browser-tools/browser-content.js +36 -75
- package/resources/browser-tools/browser-cookies.js +19 -14
- package/resources/browser-tools/browser-daemon.js +452 -0
- package/resources/browser-tools/browser-devtools.js +62 -0
- package/resources/browser-tools/browser-eval.js +16 -22
- package/resources/browser-tools/browser-fill.js +70 -0
- package/resources/browser-tools/browser-info.js +13 -0
- package/resources/browser-tools/browser-nav.js +21 -22
- package/resources/browser-tools/browser-network.js +91 -0
- package/resources/browser-tools/browser-run-script.js +12 -30
- package/resources/browser-tools/browser-screenshot.js +22 -22
- package/resources/browser-tools/browser-select.js +59 -0
- package/resources/browser-tools/browser-snapshot.js +100 -0
- package/resources/browser-tools/browser-start.js +101 -85
- package/resources/browser-tools/browser-tabs.js +38 -0
- package/resources/browser-tools/browser-wait.js +50 -0
- package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +157 -46
- package/server-loader.js +46 -233
- package/server-loader.js.map +4 -4
- package/resources/browser-tools/browser-connect.js +0 -172
- package/resources/browser-tools/browser-pick.js +0 -143
- package/resources/pi-agent/extensions/docyrus-web-browser.ts +0 -31
- package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +0 -169
- package/resources/pi-agent/skills/agent-browser/SKILL.md +0 -779
- package/resources/pi-agent/skills/agent-browser/references/authentication.md +0 -303
- package/resources/pi-agent/skills/agent-browser/references/commands.md +0 -295
- package/resources/pi-agent/skills/agent-browser/references/profiling.md +0 -120
- package/resources/pi-agent/skills/agent-browser/references/proxy-support.md +0 -194
- package/resources/pi-agent/skills/agent-browser/references/session-management.md +0 -193
- package/resources/pi-agent/skills/agent-browser/references/snapshot-refs.md +0 -219
- package/resources/pi-agent/skills/agent-browser/references/video-recording.md +0 -173
- package/resources/pi-agent/skills/agent-browser/templates/authenticated-session.sh +0 -105
- package/resources/pi-agent/skills/agent-browser/templates/capture-workflow.sh +0 -69
- package/resources/pi-agent/skills/agent-browser/templates/form-automation.sh +0 -62
|
@@ -1,38 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ensureDaemon, navigate, cdp, getMode, evaluate } from "./browser-client.js";
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
const newTab = args.includes("--new");
|
|
7
7
|
const reload = args.includes("--reload");
|
|
8
|
-
const url = args.find(a => !a.startsWith("--"));
|
|
8
|
+
const url = args.find((a) => !a.startsWith("--"));
|
|
9
9
|
|
|
10
10
|
if (!url) {
|
|
11
11
|
console.log("Usage: browser-nav.js <url> [--new] [--reload]");
|
|
12
|
-
console.log("\nExamples:");
|
|
13
|
-
console.log(" browser-nav.js https://example.com # Navigate current tab");
|
|
14
|
-
console.log(" browser-nav.js https://example.com --new # Open in new tab");
|
|
15
|
-
console.log(" browser-nav.js https://example.com --reload # Navigate and force reload");
|
|
16
12
|
process.exit(1);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
await ensureDaemon();
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
try {
|
|
18
|
+
if (newTab) {
|
|
19
|
+
const { targetId } = cdp("Target.createTarget", { url });
|
|
20
|
+
const { sessionId } = cdp("Target.attachToTarget", { targetId, flatten: true });
|
|
21
|
+
cdp("Page.enable", {}, sessionId);
|
|
22
|
+
// Update daemon's active session to the new tab
|
|
23
|
+
const { default: { setSession } } = await import("./browser-client.js");
|
|
24
|
+
setSession(sessionId);
|
|
25
|
+
} else {
|
|
26
|
+
navigate(url, "domcontentloaded");
|
|
27
|
+
if (reload) {
|
|
28
|
+
cdp("Page.reload");
|
|
29
|
+
}
|
|
29
30
|
}
|
|
30
|
-
}
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const currentUrl = evaluate("window.location.href");
|
|
33
|
+
console.log(JSON.stringify({ mode: getMode(), url: currentUrl }));
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error(`✗ Navigation failed: ${e.message}`);
|
|
36
|
+
process.exit(1);
|
|
35
37
|
}
|
|
36
|
-
console.log(JSON.stringify(result));
|
|
37
|
-
|
|
38
|
-
await b.disconnect();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inspect network requests captured by the daemon's event buffer.
|
|
5
|
+
* Network.enable is called when the daemon attaches, so requests are buffered automatically.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* browser-network.js # all requests
|
|
9
|
+
* browser-network.js --method POST # only POST requests
|
|
10
|
+
* browser-network.js --status 4xx # only 4xx responses
|
|
11
|
+
* browser-network.js --url "/api/" # URL substring filter
|
|
12
|
+
* browser-network.js --listen 5000 # listen for 5 seconds
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ensureDaemon, drainEvents, getMode } from "./browser-client.js";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const methodIdx = args.indexOf("--method");
|
|
20
|
+
const statusIdx = args.indexOf("--status");
|
|
21
|
+
const urlIdx = args.indexOf("--url");
|
|
22
|
+
const listenIdx = args.indexOf("--listen");
|
|
23
|
+
const filterMethod = methodIdx !== -1 ? args[methodIdx + 1]?.toUpperCase() : null;
|
|
24
|
+
const filterStatus = statusIdx !== -1 ? args[statusIdx + 1] : null;
|
|
25
|
+
const filterUrl = urlIdx !== -1 ? args[urlIdx + 1] : null;
|
|
26
|
+
const listenMs = listenIdx !== -1 ? parseInt(args[listenIdx + 1], 10) : null;
|
|
27
|
+
|
|
28
|
+
await ensureDaemon();
|
|
29
|
+
|
|
30
|
+
function matchStatus(code, pattern) {
|
|
31
|
+
if (!pattern) {return true;}
|
|
32
|
+
if (pattern.endsWith("xx")) {
|
|
33
|
+
const prefix = pattern.slice(0, -2);
|
|
34
|
+
return String(code).startsWith(prefix);
|
|
35
|
+
}
|
|
36
|
+
return String(code) === pattern;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const requests = new Map(); // requestId → { method, url, status, ... }
|
|
41
|
+
|
|
42
|
+
function processEvents(evts) {
|
|
43
|
+
for (const evt of evts) {
|
|
44
|
+
if (evt.method === "Network.requestWillBeSent") {
|
|
45
|
+
const { requestId, request } = evt.params;
|
|
46
|
+
requests.set(requestId, {
|
|
47
|
+
method: request.method,
|
|
48
|
+
url: request.url,
|
|
49
|
+
status: null,
|
|
50
|
+
statusText: null,
|
|
51
|
+
type: evt.params.type || null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (evt.method === "Network.responseReceived") {
|
|
55
|
+
const { requestId, response } = evt.params;
|
|
56
|
+
const req = requests.get(requestId);
|
|
57
|
+
if (req) {
|
|
58
|
+
req.status = response.status;
|
|
59
|
+
req.statusText = response.statusText;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (listenMs) {
|
|
66
|
+
const deadline = Date.now() + listenMs;
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
processEvents(drainEvents());
|
|
69
|
+
execFileSync(process.execPath, ["-e", "setTimeout(()=>{},200)"], { timeout: 1000 });
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
processEvents(drainEvents());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let entries = [...requests.values()];
|
|
76
|
+
|
|
77
|
+
if (filterMethod) {
|
|
78
|
+
entries = entries.filter((r) => r.method === filterMethod);
|
|
79
|
+
}
|
|
80
|
+
if (filterStatus) {
|
|
81
|
+
entries = entries.filter((r) => r.status !== null && matchStatus(r.status, filterStatus));
|
|
82
|
+
}
|
|
83
|
+
if (filterUrl) {
|
|
84
|
+
entries = entries.filter((r) => r.url.includes(filterUrl));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(JSON.stringify({ mode: getMode(), count: entries.length, requests: entries }));
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error(`✗ Network inspection failed: ${e.message}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
@@ -1,53 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Run a user-supplied
|
|
4
|
+
* Run a user-supplied script on the active browser session.
|
|
5
|
+
* The script receives `cdp`, `evaluate`, and helpers from browser-client.js.
|
|
5
6
|
*
|
|
6
|
-
* Usage:
|
|
7
|
-
*
|
|
8
|
-
* The script file receives `browser` (Puppeteer Browser) and `page` (first Page)
|
|
9
|
-
* as function arguments. Whatever the script returns is printed as JSON to stdout.
|
|
7
|
+
* Usage: browser-run-script.js <script.js>
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import { readFileSync } from "node:fs";
|
|
13
11
|
import { resolve } from "node:path";
|
|
14
|
-
import {
|
|
12
|
+
import { ensureDaemon, cdp, evaluate, navigate, captureScreenshot, clickAt, typeText, pressKey, pageInfo, drainEvents, getMode, waitForCondition } from "./browser-client.js";
|
|
15
13
|
|
|
16
14
|
const args = process.argv.slice(2);
|
|
17
|
-
|
|
18
|
-
const scriptPath = args.find((a) => !a.startsWith("--") && args[args.indexOf(a) - 1]?.startsWith("--") === false) ?? args[0];
|
|
15
|
+
const scriptPath = args.find((a) => !a.startsWith("--")) ?? args[0];
|
|
19
16
|
|
|
20
17
|
if (!scriptPath || scriptPath.startsWith("--")) {
|
|
21
|
-
console.error("Usage: browser-run-script.js <script.js>
|
|
22
|
-
console.error("\nThe script receives
|
|
23
|
-
console.error("\nExample script:");
|
|
24
|
-
console.error(' await page.goto("https://example.com");');
|
|
25
|
-
console.error(" return { title: await page.title() };");
|
|
18
|
+
console.error("Usage: browser-run-script.js <script.js>");
|
|
19
|
+
console.error("\nThe script receives: cdp, evaluate, navigate, captureScreenshot, clickAt, typeText, pressKey, pageInfo, drainEvents, waitForCondition");
|
|
26
20
|
process.exit(1);
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
await ensureDaemon();
|
|
30
24
|
|
|
31
25
|
try {
|
|
32
|
-
const page = await browser.newPage();
|
|
33
|
-
|
|
34
26
|
const userCode = readFileSync(resolve(scriptPath), "utf8");
|
|
35
27
|
const AsyncFunction = (async() => {}).constructor;
|
|
36
|
-
const fn = new AsyncFunction("
|
|
37
|
-
const
|
|
28
|
+
const fn = new AsyncFunction("cdp", "evaluate", "navigate", "captureScreenshot", "clickAt", "typeText", "pressKey", "pageInfo", "drainEvents", "waitForCondition", userCode);
|
|
29
|
+
const result = await fn(cdp, evaluate, navigate, captureScreenshot, clickAt, typeText, pressKey, pageInfo, drainEvents, waitForCondition);
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
if (session?.devtoolsFrontendUrl) {
|
|
41
|
-
output.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
|
|
42
|
-
}
|
|
43
|
-
console.log(JSON.stringify(output, null, 2));
|
|
31
|
+
console.log(JSON.stringify({ mode: getMode(), result: result ?? { ok: true } }, null, 2));
|
|
44
32
|
} catch (e) {
|
|
45
|
-
console.error(
|
|
33
|
+
console.error(`✗ Script failed: ${e.message}`);
|
|
46
34
|
process.exit(1);
|
|
47
|
-
} finally {
|
|
48
|
-
try {
|
|
49
|
-
await browser.disconnect();
|
|
50
|
-
} catch {
|
|
51
|
-
// Session may have already expired.
|
|
52
|
-
}
|
|
53
35
|
}
|
|
@@ -2,27 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { ensureDaemon, captureScreenshot, getMode } from "./browser-client.js";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const fullPage = args.includes("--full");
|
|
10
|
+
const base64 = args.includes("--base64");
|
|
11
|
+
|
|
12
|
+
await ensureDaemon();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const { data } = captureScreenshot({ full: fullPage });
|
|
16
|
+
|
|
17
|
+
if (base64) {
|
|
18
|
+
console.log(JSON.stringify({ mode: getMode(), base64: data, fullPage }));
|
|
19
|
+
} else {
|
|
20
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
21
|
+
const filepath = join(tmpdir(), `screenshot-${timestamp}.png`);
|
|
22
|
+
writeFileSync(filepath, Buffer.from(data, "base64"));
|
|
23
|
+
console.log(JSON.stringify({ mode: getMode(), path: filepath, fullPage }));
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(`✗ Screenshot failed: ${e.message}`);
|
|
13
27
|
process.exit(1);
|
|
14
28
|
}
|
|
15
|
-
|
|
16
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
17
|
-
const filename = `screenshot-${timestamp}.png`;
|
|
18
|
-
const filepath = join(tmpdir(), filename);
|
|
19
|
-
|
|
20
|
-
await p.screenshot({ path: filepath });
|
|
21
|
-
|
|
22
|
-
const result = { mode, path: filepath };
|
|
23
|
-
if (session?.devtoolsFrontendUrl) {
|
|
24
|
-
result.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
|
|
25
|
-
}
|
|
26
|
-
console.log(JSON.stringify(result));
|
|
27
|
-
|
|
28
|
-
await b.disconnect();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ensureDaemon, evaluate, getMode, waitForCondition } from "./browser-client.js";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const timeoutIdx = args.indexOf("--timeout");
|
|
9
|
+
const timeout = timeoutIdx !== -1 ? parseInt(args[timeoutIdx + 1], 10) : 5000;
|
|
10
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--timeout");
|
|
11
|
+
const target = positional[0];
|
|
12
|
+
const value = positional[1];
|
|
13
|
+
|
|
14
|
+
if (!target || !value) {
|
|
15
|
+
console.log('Usage: browser-select.js <@ref|selector> "value" [--timeout <ms>]');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveRef(ref) {
|
|
20
|
+
try {
|
|
21
|
+
const refs = JSON.parse(readFileSync(join(process.cwd(), ".docyrus", "browser-refs.json"), "utf8"));
|
|
22
|
+
const entry = refs[ref];
|
|
23
|
+
if (!entry) { throw new Error(`Unknown ref "${ref}"`); }
|
|
24
|
+
return entry;
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (e.message.includes("Unknown ref")) {throw e;}
|
|
27
|
+
throw new Error("No snapshot refs found. Run \"docyrus browser snapshot\" first.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await ensureDaemon();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { selector, xpath } = target.startsWith("@e") ? resolveRef(target) : { selector: target, xpath: null };
|
|
35
|
+
const selectorExpr = JSON.stringify(selector);
|
|
36
|
+
const xpathExpr = xpath ? JSON.stringify(xpath) : "null";
|
|
37
|
+
const valExpr = JSON.stringify(value);
|
|
38
|
+
|
|
39
|
+
waitForCondition(`!!document.querySelector(${selectorExpr}) || (${xpathExpr} && !!document.evaluate(${xpathExpr}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue)`, timeout);
|
|
40
|
+
|
|
41
|
+
const selected = evaluate(`(() => {
|
|
42
|
+
let el = document.querySelector(${selectorExpr});
|
|
43
|
+
if (!el && ${xpathExpr}) el = document.evaluate(${xpathExpr}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
44
|
+
if (!el || el.tagName !== "SELECT") throw new Error("Target is not a <select> element");
|
|
45
|
+
for (const opt of el.options) {
|
|
46
|
+
if (opt.textContent.trim() === ${valExpr} || opt.value === ${valExpr}) {
|
|
47
|
+
el.value = opt.value;
|
|
48
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
49
|
+
return opt.textContent.trim();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error("Option " + ${valExpr} + " not found");
|
|
53
|
+
})()`);
|
|
54
|
+
|
|
55
|
+
console.log(JSON.stringify({ mode: getMode(), selected: target, value: selected }));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(`✗ Select failed: ${e.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ensureDaemon, evaluate, getMode } from "./browser-client.js";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const includeAll = args.includes("--all");
|
|
9
|
+
const selectorIdx = args.indexOf("--selector");
|
|
10
|
+
const scopeSelector = selectorIdx !== -1 ? args[selectorIdx + 1] : null;
|
|
11
|
+
|
|
12
|
+
await ensureDaemon();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const elements = evaluate(`(() => {
|
|
16
|
+
const root = ${scopeSelector ? `document.querySelector(${JSON.stringify(scopeSelector)})` : "document.body"};
|
|
17
|
+
if (!root) return [];
|
|
18
|
+
|
|
19
|
+
const INTERACTIVE_TAGS = new Set(["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "DETAILS", "SUMMARY"]);
|
|
20
|
+
const INTERACTIVE_ROLES = new Set(["button", "link", "textbox", "checkbox", "radio", "combobox", "listbox", "menuitem", "option", "searchbox", "slider", "spinbutton", "switch", "tab"]);
|
|
21
|
+
const results = [];
|
|
22
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
|
23
|
+
acceptNode(node) {
|
|
24
|
+
if (node.offsetParent === null && node.tagName !== "BODY" && node.tagName !== "HTML") {
|
|
25
|
+
const style = getComputedStyle(node);
|
|
26
|
+
if (style.display === "none" || style.visibility === "hidden") return NodeFilter.FILTER_REJECT;
|
|
27
|
+
}
|
|
28
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let node = walker.currentNode;
|
|
33
|
+
while (node) {
|
|
34
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
35
|
+
const tag = node.tagName;
|
|
36
|
+
const role = node.getAttribute("role");
|
|
37
|
+
const isInteractive = INTERACTIVE_TAGS.has(tag) || INTERACTIVE_ROLES.has(role) || node.hasAttribute("onclick") || node.hasAttribute("tabindex") || node.contentEditable === "true";
|
|
38
|
+
|
|
39
|
+
if (${includeAll} || isInteractive) {
|
|
40
|
+
const entry = { tag: tag.toLowerCase(), text: (node.textContent || "").trim().slice(0, 120) || null };
|
|
41
|
+
if (tag === "INPUT") { entry.type = node.type || "text"; entry.name = node.name || null; entry.value = node.value || null; entry.placeholder = node.placeholder || null; }
|
|
42
|
+
else if (tag === "SELECT") { entry.name = node.name || null; const sel = node.options?.[node.selectedIndex]; entry.value = sel?.textContent?.trim() || null; }
|
|
43
|
+
else if (tag === "TEXTAREA") { entry.name = node.name || null; entry.value = node.value || null; entry.placeholder = node.placeholder || null; }
|
|
44
|
+
else if (tag === "A") { entry.href = node.href || null; }
|
|
45
|
+
if (role) entry.role = role;
|
|
46
|
+
if (node.disabled) entry.disabled = true;
|
|
47
|
+
if (node.id) entry.id = node.id;
|
|
48
|
+
|
|
49
|
+
let selector = tag.toLowerCase();
|
|
50
|
+
if (node.id) selector = "#" + node.id;
|
|
51
|
+
else {
|
|
52
|
+
if (node.name) selector += "[name=\\"" + node.name + "\\"]";
|
|
53
|
+
if (node.type && tag === "INPUT") selector += "[type=\\"" + node.type + "\\"]";
|
|
54
|
+
if (node.className && typeof node.className === "string") {
|
|
55
|
+
const cls = node.className.trim().split(/\\s+/).slice(0, 2).join(".");
|
|
56
|
+
if (cls) selector += "." + cls;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
entry._selector = selector;
|
|
60
|
+
|
|
61
|
+
// XPath for reliable targeting
|
|
62
|
+
const parts = [];
|
|
63
|
+
let cur = node;
|
|
64
|
+
while (cur && cur !== document) {
|
|
65
|
+
let idx = 1;
|
|
66
|
+
let sib = cur.previousElementSibling;
|
|
67
|
+
while (sib) { if (sib.tagName === cur.tagName) idx++; sib = sib.previousElementSibling; }
|
|
68
|
+
parts.unshift(cur.tagName.toLowerCase() + "[" + idx + "]");
|
|
69
|
+
cur = cur.parentElement;
|
|
70
|
+
}
|
|
71
|
+
entry._xpath = "/" + parts.join("/");
|
|
72
|
+
|
|
73
|
+
results.push(entry);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
node = walker.nextNode();
|
|
77
|
+
}
|
|
78
|
+
return results;
|
|
79
|
+
})()`);
|
|
80
|
+
|
|
81
|
+
const snapshot = [];
|
|
82
|
+
const refMap = {};
|
|
83
|
+
for (let i = 0; i < elements.length; i++) {
|
|
84
|
+
const ref = `@e${i + 1}`;
|
|
85
|
+
const { _selector, _xpath, ...display } = elements[i];
|
|
86
|
+
display.ref = ref;
|
|
87
|
+
snapshot.push(display);
|
|
88
|
+
refMap[ref] = { selector: _selector, xpath: _xpath };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const docyrusDir = join(process.cwd(), ".docyrus");
|
|
92
|
+
mkdirSync(docyrusDir, { recursive: true, mode: 0o700 });
|
|
93
|
+
writeFileSync(join(docyrusDir, "browser-refs.json"), JSON.stringify(refMap, null, 2) + "\n", { mode: 0o600 });
|
|
94
|
+
|
|
95
|
+
const url = evaluate("window.location.href");
|
|
96
|
+
console.log(JSON.stringify({ mode: getMode(), url, count: snapshot.length, snapshot }));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`✗ Snapshot failed: ${e.message}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Start the browser daemon. In local mode, launches Chrome if not running.
|
|
5
|
+
* In sandbox mode, the daemon creates a Cloudflare Browser Rendering session.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
9
|
+
import { existsSync, mkdirSync, unlinkSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { ensureDaemon, health } from "./browser-client.js";
|
|
6
12
|
|
|
7
13
|
const useProfile = process.argv[2] === "--profile";
|
|
8
14
|
|
|
@@ -13,91 +19,101 @@ if (process.argv[2] && process.argv[2] !== "--profile") {
|
|
|
13
19
|
process.exit(1);
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
sessionId: session.sessionId,
|
|
23
|
-
tabUrl: session.tabUrl,
|
|
24
|
-
devtoolsFrontendUrl: session.devtoolsFrontendUrl,
|
|
25
|
-
};
|
|
26
|
-
console.log(JSON.stringify(result));
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
22
|
+
// In sandbox mode, just ensure the daemon is running (it handles session creation)
|
|
23
|
+
const isSandbox = process.env.DOCYRUS_BROWSER_SANDBOX === "1" || (() => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(join(process.cwd(), ".docyrus", "browser.json"), "utf8")).mode === "sandbox";
|
|
26
|
+
} catch { return false; }
|
|
27
|
+
})();
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
const SCRAPING_DIR = `${process.env.HOME}/.cache/browser-tools`;
|
|
33
|
-
|
|
34
|
-
// Check if already running on :9222
|
|
35
|
-
try {
|
|
36
|
-
const browser = await puppeteer.connect({
|
|
37
|
-
browserURL: "http://localhost:9222",
|
|
38
|
-
defaultViewport: null,
|
|
39
|
-
});
|
|
40
|
-
await browser.disconnect();
|
|
41
|
-
console.log(JSON.stringify({ mode: "local", status: "already_running" }));
|
|
42
|
-
process.exit(0);
|
|
43
|
-
} catch {}
|
|
44
|
-
|
|
45
|
-
// Setup profile directory
|
|
46
|
-
execSync(`mkdir -p "${SCRAPING_DIR}"`, { stdio: "ignore" });
|
|
47
|
-
|
|
48
|
-
// Remove SingletonLock to allow new instance
|
|
49
|
-
try {
|
|
50
|
-
execSync(`rm -f "${SCRAPING_DIR}/SingletonLock" "${SCRAPING_DIR}/SingletonSocket" "${SCRAPING_DIR}/SingletonCookie"`, { stdio: "ignore" });
|
|
51
|
-
} catch {}
|
|
52
|
-
|
|
53
|
-
if (useProfile) {
|
|
54
|
-
console.error("Syncing profile...");
|
|
55
|
-
execSync(
|
|
56
|
-
`rsync -a --delete \
|
|
57
|
-
--exclude='SingletonLock' \
|
|
58
|
-
--exclude='SingletonSocket' \
|
|
59
|
-
--exclude='SingletonCookie' \
|
|
60
|
-
--exclude='*/Sessions/*' \
|
|
61
|
-
--exclude='*/Current Session' \
|
|
62
|
-
--exclude='*/Current Tabs' \
|
|
63
|
-
--exclude='*/Last Session' \
|
|
64
|
-
--exclude='*/Last Tabs' \
|
|
65
|
-
"${process.env.HOME}/Library/Application Support/Google/Chrome/" "${SCRAPING_DIR}/"`,
|
|
66
|
-
{ stdio: "pipe" },
|
|
67
|
-
);
|
|
68
|
-
}
|
|
29
|
+
if (!isSandbox) {
|
|
30
|
+
// Local mode: ensure Chrome is running on :9222 before starting daemon
|
|
31
|
+
const SCRAPING_DIR = `${process.env.HOME}/.cache/browser-tools`;
|
|
69
32
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
33
|
+
function detectChromePath() {
|
|
34
|
+
if (process.env.CHROME_PATH) { return process.env.CHROME_PATH; }
|
|
35
|
+
const platform = process.platform;
|
|
36
|
+
const candidates = [];
|
|
37
|
+
if (platform === "darwin") {
|
|
38
|
+
candidates.push(
|
|
39
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
40
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
41
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
42
|
+
);
|
|
43
|
+
} else if (platform === "linux") {
|
|
44
|
+
candidates.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium");
|
|
45
|
+
} else if (platform === "win32") {
|
|
46
|
+
const pf = process.env["PROGRAMFILES"] || "C:\\Program Files";
|
|
47
|
+
const pf86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
|
|
48
|
+
const la = process.env.LOCALAPPDATA || "";
|
|
49
|
+
candidates.push(`${pf}\\Google\\Chrome\\Application\\chrome.exe`, `${pf86}\\Google\\Chrome\\Application\\chrome.exe`, `${la}\\Google\\Chrome\\Application\\chrome.exe`);
|
|
50
|
+
}
|
|
51
|
+
for (const bin of ["google-chrome", "chromium", "chromium-browser"]) {
|
|
52
|
+
try {
|
|
53
|
+
const found = execFileSync(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }).trim().split("\n")[0];
|
|
54
|
+
if (found) { candidates.push(found); }
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
return candidates.find((c) => existsSync(c)) || null;
|
|
95
58
|
}
|
|
96
|
-
}
|
|
97
59
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
60
|
+
function detectProfilePath() {
|
|
61
|
+
if (process.platform === "darwin") { return `${process.env.HOME}/Library/Application Support/Google/Chrome/`; }
|
|
62
|
+
if (process.platform === "linux") { return `${process.env.HOME}/.config/google-chrome/`; }
|
|
63
|
+
if (process.platform === "win32") { return `${process.env.LOCALAPPDATA || ""}\\Google\\Chrome\\User Data\\`; }
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if Chrome is already reachable
|
|
68
|
+
let chromeRunning = false;
|
|
69
|
+
try {
|
|
70
|
+
const resp = execFileSync("node", ["-e", "fetch('http://localhost:9222/json/version').then(r=>r.json()).then(()=>console.log('ok')).catch(()=>process.exit(1))"], { encoding: "utf8", timeout: 3000 });
|
|
71
|
+
chromeRunning = resp.trim() === "ok";
|
|
72
|
+
} catch {}
|
|
73
|
+
|
|
74
|
+
if (!chromeRunning) {
|
|
75
|
+
const chromePath = detectChromePath();
|
|
76
|
+
if (!chromePath) {
|
|
77
|
+
console.error("✗ Could not find Chrome or Chromium. Set CHROME_PATH to the browser executable.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
mkdirSync(SCRAPING_DIR, { recursive: true });
|
|
82
|
+
for (const lock of ["SingletonLock", "SingletonSocket", "SingletonCookie"]) {
|
|
83
|
+
try { unlinkSync(`${SCRAPING_DIR}/${lock}`); } catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (useProfile) {
|
|
87
|
+
const profilePath = detectProfilePath();
|
|
88
|
+
if (!profilePath) {
|
|
89
|
+
console.error("✗ Could not detect Chrome profile path.");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
console.error("Syncing profile...");
|
|
93
|
+
execFileSync("rsync", ["-a", "--delete", "--exclude=SingletonLock", "--exclude=SingletonSocket", "--exclude=SingletonCookie", "--exclude=*/Sessions/*", "--exclude=*/Current Session", "--exclude=*/Current Tabs", "--exclude=*/Last Session", "--exclude=*/Last Tabs", profilePath, `${SCRAPING_DIR}/`], { stdio: "pipe" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
spawn(chromePath, ["--remote-debugging-port=9222", `--user-data-dir=${SCRAPING_DIR}`, "--no-first-run", "--no-default-browser-check"], { detached: true, stdio: "ignore" }).unref();
|
|
97
|
+
|
|
98
|
+
// Wait for Chrome to be reachable
|
|
99
|
+
for (let i = 0; i < 30; i++) {
|
|
100
|
+
try {
|
|
101
|
+
execFileSync("node", ["-e", "fetch('http://localhost:9222/json/version').then(r=>r.json()).then(()=>console.log('ok')).catch(()=>process.exit(1))"], { encoding: "utf8", timeout: 3000 });
|
|
102
|
+
chromeRunning = true;
|
|
103
|
+
break;
|
|
104
|
+
} catch {
|
|
105
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!chromeRunning) {
|
|
110
|
+
console.error("✗ Chrome failed to start");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
|
|
116
|
+
// Start/ensure daemon
|
|
117
|
+
await ensureDaemon();
|
|
118
|
+
const info = health();
|
|
119
|
+
console.log(JSON.stringify({ mode: info.mode, status: "ready", sessionId: info.sessionId, profile: useProfile }));
|