@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.
Files changed (44) hide show
  1. package/README.md +46 -0
  2. package/agent-loader.js +1 -1
  3. package/agent-loader.js.map +2 -2
  4. package/main.js +361 -23
  5. package/main.js.map +2 -2
  6. package/package.json +1 -1
  7. package/resources/browser-tools/browser-click.js +74 -0
  8. package/resources/browser-tools/browser-client.js +236 -0
  9. package/resources/browser-tools/browser-close.js +19 -0
  10. package/resources/browser-tools/browser-console.js +73 -0
  11. package/resources/browser-tools/browser-content.js +36 -75
  12. package/resources/browser-tools/browser-cookies.js +19 -14
  13. package/resources/browser-tools/browser-daemon.js +452 -0
  14. package/resources/browser-tools/browser-devtools.js +62 -0
  15. package/resources/browser-tools/browser-eval.js +16 -22
  16. package/resources/browser-tools/browser-fill.js +70 -0
  17. package/resources/browser-tools/browser-info.js +13 -0
  18. package/resources/browser-tools/browser-nav.js +21 -22
  19. package/resources/browser-tools/browser-network.js +91 -0
  20. package/resources/browser-tools/browser-run-script.js +12 -30
  21. package/resources/browser-tools/browser-screenshot.js +22 -22
  22. package/resources/browser-tools/browser-select.js +59 -0
  23. package/resources/browser-tools/browser-snapshot.js +100 -0
  24. package/resources/browser-tools/browser-start.js +101 -85
  25. package/resources/browser-tools/browser-tabs.js +38 -0
  26. package/resources/browser-tools/browser-wait.js +50 -0
  27. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +157 -46
  28. package/server-loader.js +46 -233
  29. package/server-loader.js.map +4 -4
  30. package/resources/browser-tools/browser-connect.js +0 -172
  31. package/resources/browser-tools/browser-pick.js +0 -143
  32. package/resources/pi-agent/extensions/docyrus-web-browser.ts +0 -31
  33. package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +0 -169
  34. package/resources/pi-agent/skills/agent-browser/SKILL.md +0 -779
  35. package/resources/pi-agent/skills/agent-browser/references/authentication.md +0 -303
  36. package/resources/pi-agent/skills/agent-browser/references/commands.md +0 -295
  37. package/resources/pi-agent/skills/agent-browser/references/profiling.md +0 -120
  38. package/resources/pi-agent/skills/agent-browser/references/proxy-support.md +0 -194
  39. package/resources/pi-agent/skills/agent-browser/references/session-management.md +0 -193
  40. package/resources/pi-agent/skills/agent-browser/references/snapshot-refs.md +0 -219
  41. package/resources/pi-agent/skills/agent-browser/references/video-recording.md +0 -173
  42. package/resources/pi-agent/skills/agent-browser/templates/authenticated-session.sh +0 -105
  43. package/resources/pi-agent/skills/agent-browser/templates/capture-workflow.sh +0 -69
  44. 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 { connectBrowser } from "./browser-connect.js";
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
- const { browser: b, mode, session } = await connectBrowser();
15
+ await ensureDaemon();
20
16
 
21
- if (newTab) {
22
- const p = await b.newPage();
23
- await p.goto(url, { waitUntil: "domcontentloaded" });
24
- } else {
25
- const p = (await b.pages()).at(-1);
26
- await p.goto(url, { waitUntil: "domcontentloaded" });
27
- if (reload) {
28
- await p.reload({ waitUntil: "domcontentloaded" });
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 result = { mode, url };
33
- if (session?.devtoolsFrontendUrl) {
34
- result.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
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 CDP script on the active browser session.
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: node browser-run-script.js <script> [--appId <id>] [--appSlug <slug>]
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 { connectBrowser } from "./browser-connect.js";
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> [--appId <id>] [--appSlug <slug>]");
22
- console.error("\nThe script receives `browser` and `page` as arguments.");
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
- const { browser, mode, session } = await connectBrowser();
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("browser", "page", userCode);
37
- const scriptResult = await fn(browser, page);
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
- const output = { mode, result: scriptResult ?? { ok: true } };
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("✗ CDP script failed:", e.message);
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 { connectBrowser } from "./browser-connect.js";
6
-
7
- const { browser: b, mode, session } = await connectBrowser();
8
-
9
- const p = (await b.pages()).at(-1);
10
-
11
- if (!p) {
12
- console.error("✗ No active tab found");
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
- import { spawn, execSync } from "node:child_process";
4
- import puppeteer from "puppeteer-core";
5
- import { isSandboxMode, connectBrowser } from "./browser-connect.js";
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
- // Sandbox mode: create a remote Cloudflare Browser Rendering session
17
- if (isSandboxMode()) {
18
- const { session } = await connectBrowser();
19
-
20
- const result = {
21
- mode: "remote",
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
- // Local mode: start Chrome on :9222
31
- // Note: execSync calls below use only hardcoded paths, not user input.
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
- // Start Chrome with flags to force new instance
71
- spawn(
72
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
73
- [
74
- "--remote-debugging-port=9222",
75
- `--user-data-dir=${SCRAPING_DIR}`,
76
- "--no-first-run",
77
- "--no-default-browser-check",
78
- ],
79
- { detached: true, stdio: "ignore" },
80
- ).unref();
81
-
82
- // Wait for Chrome to be ready
83
- let connected = false;
84
- for (let i = 0; i < 30; i++) {
85
- try {
86
- const browser = await puppeteer.connect({
87
- browserURL: "http://localhost:9222",
88
- defaultViewport: null,
89
- });
90
- await browser.disconnect();
91
- connected = true;
92
- break;
93
- } catch {
94
- await new Promise((r) => setTimeout(r, 500));
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
- if (!connected) {
99
- console.error(" Failed to connect to Chrome");
100
- process.exit(1);
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
- console.log(JSON.stringify({ mode: "local", status: "started", profile: useProfile }));
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 }));