@docyrus/docyrus 0.0.56 → 0.0.58
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/main.js +132 -70
- package/main.js.map +3 -3
- package/package.json +3 -3
- package/resources/browser-tools/browser-connect.js +172 -0
- package/resources/{chrome-tools → browser-tools}/browser-content.js +10 -14
- package/resources/browser-tools/browser-cookies.js +22 -0
- package/resources/browser-tools/browser-eval.js +34 -0
- package/resources/{chrome-tools → browser-tools}/browser-nav.js +8 -14
- package/resources/{chrome-tools → browser-tools}/browser-pick.js +7 -26
- package/resources/browser-tools/browser-run-script.js +53 -0
- package/resources/{chrome-tools → browser-tools}/browser-screenshot.js +8 -14
- package/resources/{chrome-tools → browser-tools}/browser-start.js +21 -4
- package/resources/pi-agent/extensions/docyrus-web-browser.ts +5 -5
- package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +1 -1
- package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +41 -24
- package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +16 -14
- package/server-loader.js +57 -39
- package/server-loader.js.map +4 -4
- package/resources/chrome-tools/browser-cookies.js +0 -35
- package/resources/chrome-tools/browser-eval.js +0 -53
- package/resources/chrome-tools/browser-hn-scraper.js +0 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docyrus/docyrus",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.58",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Docyrus API CLI",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"@repomix/tree-sitter-wasms": "^0.1.16",
|
|
24
24
|
"@sinclair/typebox": "^0.34.48",
|
|
25
25
|
"@xterm/headless": "^5.5.0",
|
|
26
|
-
"diff": "^
|
|
27
|
-
"hono": "^4.12.
|
|
26
|
+
"diff": "^9.0.0",
|
|
27
|
+
"hono": "^4.12.14",
|
|
28
28
|
"ignore-walk": "^8.0.0",
|
|
29
29
|
"incur": "^0.1.6",
|
|
30
30
|
"jsdom": "^29.0.1",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared browser connection module.
|
|
3
|
+
*
|
|
4
|
+
* Reads `.docyrus/browser.json` (or DOCYRUS_BROWSER_SANDBOX env) to decide
|
|
5
|
+
* between local Chrome on :9222 and a remote Cloudflare Browser Rendering
|
|
6
|
+
* session. Remote sessions are cached in `.docyrus/browser-session.json` and
|
|
7
|
+
* reused until they expire.
|
|
8
|
+
*
|
|
9
|
+
* Exports:
|
|
10
|
+
* connectBrowser() → { browser, mode, session }
|
|
11
|
+
* isSandboxMode() → boolean
|
|
12
|
+
* getSessionInfo() → cached session object | null
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import puppeteer from "puppeteer-core";
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
const DOCYRUS_DIR = ".docyrus";
|
|
21
|
+
const BROWSER_CONFIG_FILE = "browser.json";
|
|
22
|
+
const BROWSER_SESSION_FILE = "browser-session.json";
|
|
23
|
+
const LOCAL_TIMEOUT_MS = 5_000;
|
|
24
|
+
const REMOTE_TIMEOUT_MS = 30_000;
|
|
25
|
+
const SESSION_EXPIRY_BUFFER_MS = 30_000;
|
|
26
|
+
|
|
27
|
+
function resolveDocyrusDir() {
|
|
28
|
+
return join(process.cwd(), DOCYRUS_DIR);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJsonFile(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeJsonFile(filePath, data) {
|
|
40
|
+
const dir = join(filePath, "..");
|
|
41
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
42
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
mode: 0o600,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readBrowserConfig() {
|
|
49
|
+
return readJsonFile(join(resolveDocyrusDir(), BROWSER_CONFIG_FILE));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readCachedSession() {
|
|
53
|
+
return readJsonFile(join(resolveDocyrusDir(), BROWSER_SESSION_FILE));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeCachedSession(session) {
|
|
57
|
+
writeJsonFile(join(resolveDocyrusDir(), BROWSER_SESSION_FILE), session);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isSandboxMode() {
|
|
61
|
+
if (process.env.DOCYRUS_BROWSER_SANDBOX === "1") {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const config = readBrowserConfig();
|
|
65
|
+
return config?.mode === "sandbox";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getSessionInfo() {
|
|
69
|
+
if (!isSandboxMode()) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const cached = readCachedSession();
|
|
73
|
+
if (!cached?.expiresAt) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (new Date(cached.expiresAt).getTime() - SESSION_EXPIRY_BUFFER_MS <= Date.now()) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return cached;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveAppId() {
|
|
83
|
+
const config = readBrowserConfig();
|
|
84
|
+
return config?.appId || process.env.DOCYRUS_SANDBOX_APP_ID || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createRemoteSession(appId) {
|
|
88
|
+
const result = spawnSync("docyrus", [
|
|
89
|
+
"curl", "-X", "POST",
|
|
90
|
+
`/api/v1/ai/sandbox/app/${appId}/createBrowserSession`,
|
|
91
|
+
"--format", "json",
|
|
92
|
+
], {
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
env: { ...process.env },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (result.status !== 0) {
|
|
98
|
+
const msg = result.stderr?.trim() || "unknown error";
|
|
99
|
+
console.error(`✗ Failed to create browser session: ${msg}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let data;
|
|
104
|
+
try {
|
|
105
|
+
data = JSON.parse(result.stdout);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error("✗ Invalid session response:", e.message);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const session = {
|
|
113
|
+
...data,
|
|
114
|
+
createdAt: new Date(now).toISOString(),
|
|
115
|
+
expiresAt: new Date(now + (data.keepAliveMs || 600_000)).toISOString(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
writeCachedSession(session);
|
|
119
|
+
return session;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function connectRemote(session) {
|
|
123
|
+
const browser = await Promise.race([
|
|
124
|
+
puppeteer.connect({
|
|
125
|
+
browserWSEndpoint: session.browserWSEndpoint,
|
|
126
|
+
headers: { Authorization: session.authHeader },
|
|
127
|
+
}),
|
|
128
|
+
new Promise((_, reject) =>
|
|
129
|
+
setTimeout(() => reject(new Error("Timed out connecting to remote browser")), REMOTE_TIMEOUT_MS),
|
|
130
|
+
),
|
|
131
|
+
]);
|
|
132
|
+
return browser;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function connectLocal() {
|
|
136
|
+
const browser = await Promise.race([
|
|
137
|
+
puppeteer.connect({
|
|
138
|
+
browserURL: "http://localhost:9222",
|
|
139
|
+
defaultViewport: null,
|
|
140
|
+
}),
|
|
141
|
+
new Promise((_, reject) =>
|
|
142
|
+
setTimeout(() => reject(new Error("timeout")), LOCAL_TIMEOUT_MS),
|
|
143
|
+
),
|
|
144
|
+
]).catch((e) => {
|
|
145
|
+
console.error("✗ Could not connect to browser:", e.message);
|
|
146
|
+
console.error(" Run: docyrus browser start");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
149
|
+
return browser;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function connectBrowser() {
|
|
153
|
+
if (isSandboxMode()) {
|
|
154
|
+
const appId = resolveAppId();
|
|
155
|
+
if (!appId) {
|
|
156
|
+
console.error("✗ Sandbox mode active but no app ID found.");
|
|
157
|
+
console.error(" Set DOCYRUS_SANDBOX_APP_ID or configure .docyrus/browser.json");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let session = getSessionInfo();
|
|
162
|
+
if (!session) {
|
|
163
|
+
session = createRemoteSession(appId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const browser = await connectRemote(session);
|
|
167
|
+
return { browser, mode: "remote", session };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const browser = await connectLocal();
|
|
171
|
+
return { browser, mode: "local", session: null };
|
|
172
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import puppeteer from "puppeteer-core";
|
|
4
3
|
import { Readability } from "@mozilla/readability";
|
|
5
4
|
import { JSDOM } from "jsdom";
|
|
6
5
|
import TurndownService from "turndown";
|
|
7
6
|
import { gfm } from "turndown-plugin-gfm";
|
|
7
|
+
import { connectBrowser } from "./browser-connect.js";
|
|
8
8
|
|
|
9
9
|
// Global timeout - exit if script takes too long
|
|
10
10
|
const TIMEOUT = 30000;
|
|
11
|
-
|
|
11
|
+
setTimeout(() => {
|
|
12
12
|
console.error("✗ Timeout after 30s");
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}, TIMEOUT).unref();
|
|
@@ -24,17 +24,7 @@ if (!url) {
|
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const b = await
|
|
28
|
-
puppeteer.connect({
|
|
29
|
-
browserURL: "http://localhost:9222",
|
|
30
|
-
defaultViewport: null,
|
|
31
|
-
}),
|
|
32
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
|
|
33
|
-
]).catch((e) => {
|
|
34
|
-
console.error("✗ Could not connect to browser:", e.message);
|
|
35
|
-
console.error(" Run: browser-start.js");
|
|
36
|
-
process.exit(1);
|
|
37
|
-
});
|
|
27
|
+
const { browser: b, mode, session } = await connectBrowser();
|
|
38
28
|
|
|
39
29
|
const p = (await b.pages()).at(-1);
|
|
40
30
|
if (!p) {
|
|
@@ -82,7 +72,7 @@ let content;
|
|
|
82
72
|
if (article && article.content) {
|
|
83
73
|
content = htmlToMarkdown(article.content);
|
|
84
74
|
} else {
|
|
85
|
-
|
|
75
|
+
// Fallback
|
|
86
76
|
const fallbackDoc = new JSDOM(outerHTML, { url: finalUrl });
|
|
87
77
|
const fallbackBody = fallbackDoc.window.document;
|
|
88
78
|
fallbackBody.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach((el) => el.remove());
|
|
@@ -95,8 +85,14 @@ if (article && article.content) {
|
|
|
95
85
|
}
|
|
96
86
|
}
|
|
97
87
|
|
|
88
|
+
// Content command outputs markdown directly (not wrapped in JSON) since
|
|
89
|
+
// the extracted text is the primary payload and is consumed as plain text.
|
|
98
90
|
console.log(`URL: ${finalUrl}`);
|
|
99
91
|
if (article?.title) {console.log(`Title: ${article.title}`);}
|
|
92
|
+
if (mode === "remote" && session?.devtoolsFrontendUrl) {
|
|
93
|
+
console.log(`DevTools: ${session.devtoolsFrontendUrl}`);
|
|
94
|
+
}
|
|
95
|
+
console.log(`Mode: ${mode}`);
|
|
100
96
|
console.log("");
|
|
101
97
|
console.log(content);
|
|
102
98
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { connectBrowser } from "./browser-connect.js";
|
|
4
|
+
|
|
5
|
+
const { browser: b, mode, session } = await connectBrowser();
|
|
6
|
+
|
|
7
|
+
const p = (await b.pages()).at(-1);
|
|
8
|
+
|
|
9
|
+
if (!p) {
|
|
10
|
+
console.error("✗ No active tab found");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const cookies = await p.cookies();
|
|
15
|
+
|
|
16
|
+
const result = { mode, cookies };
|
|
17
|
+
if (session?.devtoolsFrontendUrl) {
|
|
18
|
+
result.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
|
|
19
|
+
}
|
|
20
|
+
console.log(JSON.stringify(result));
|
|
21
|
+
|
|
22
|
+
await b.disconnect();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { connectBrowser } from "./browser-connect.js";
|
|
4
|
+
|
|
5
|
+
const code = process.argv.slice(2).join(" ");
|
|
6
|
+
if (!code) {
|
|
7
|
+
console.log("Usage: browser-eval.js 'code'");
|
|
8
|
+
console.log("\nExamples:");
|
|
9
|
+
console.log(' browser-eval.js "document.title"');
|
|
10
|
+
console.log(' browser-eval.js "document.querySelectorAll(\'a\').length"');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { browser: b, mode, session } = await connectBrowser();
|
|
15
|
+
|
|
16
|
+
const p = (await b.pages()).at(-1);
|
|
17
|
+
|
|
18
|
+
if (!p) {
|
|
19
|
+
console.error("✗ No active tab found");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const evalResult = await p.evaluate((c) => {
|
|
24
|
+
const AsyncFunction = (async() => {}).constructor;
|
|
25
|
+
return new AsyncFunction(`return (${c})`)();
|
|
26
|
+
}, code);
|
|
27
|
+
|
|
28
|
+
const output = { mode, result: evalResult };
|
|
29
|
+
if (session?.devtoolsFrontendUrl) {
|
|
30
|
+
output.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
|
|
31
|
+
}
|
|
32
|
+
console.log(JSON.stringify(output));
|
|
33
|
+
|
|
34
|
+
await b.disconnect();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { connectBrowser } from "./browser-connect.js";
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
const newTab = args.includes("--new");
|
|
@@ -16,29 +16,23 @@ if (!url) {
|
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const b = await
|
|
20
|
-
puppeteer.connect({
|
|
21
|
-
browserURL: "http://localhost:9222",
|
|
22
|
-
defaultViewport: null,
|
|
23
|
-
}),
|
|
24
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
|
|
25
|
-
]).catch((e) => {
|
|
26
|
-
console.error("✗ Could not connect to browser:", e.message);
|
|
27
|
-
console.error(" Run: browser-start.js");
|
|
28
|
-
process.exit(1);
|
|
29
|
-
});
|
|
19
|
+
const { browser: b, mode, session } = await connectBrowser();
|
|
30
20
|
|
|
31
21
|
if (newTab) {
|
|
32
22
|
const p = await b.newPage();
|
|
33
23
|
await p.goto(url, { waitUntil: "domcontentloaded" });
|
|
34
|
-
console.log("✓ Opened:", url);
|
|
35
24
|
} else {
|
|
36
25
|
const p = (await b.pages()).at(-1);
|
|
37
26
|
await p.goto(url, { waitUntil: "domcontentloaded" });
|
|
38
27
|
if (reload) {
|
|
39
28
|
await p.reload({ waitUntil: "domcontentloaded" });
|
|
40
29
|
}
|
|
41
|
-
console.log("✓ Navigated to:", url);
|
|
42
30
|
}
|
|
43
31
|
|
|
32
|
+
const result = { mode, url };
|
|
33
|
+
if (session?.devtoolsFrontendUrl) {
|
|
34
|
+
result.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
|
|
35
|
+
}
|
|
36
|
+
console.log(JSON.stringify(result));
|
|
37
|
+
|
|
44
38
|
await b.disconnect();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { connectBrowser } from "./browser-connect.js";
|
|
4
4
|
|
|
5
5
|
const message = process.argv.slice(2).join(" ");
|
|
6
6
|
if (!message) {
|
|
@@ -10,17 +10,7 @@ if (!message) {
|
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const b = await
|
|
14
|
-
puppeteer.connect({
|
|
15
|
-
browserURL: "http://localhost:9222",
|
|
16
|
-
defaultViewport: null,
|
|
17
|
-
}),
|
|
18
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
|
|
19
|
-
]).catch((e) => {
|
|
20
|
-
console.error("✗ Could not connect to browser:", e.message);
|
|
21
|
-
console.error(" Run: browser-start.js");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
});
|
|
13
|
+
const { browser: b, mode, session } = await connectBrowser();
|
|
24
14
|
|
|
25
15
|
const p = (await b.pages()).at(-1);
|
|
26
16
|
|
|
@@ -142,21 +132,12 @@ await p.evaluate(() => {
|
|
|
142
132
|
}
|
|
143
133
|
});
|
|
144
134
|
|
|
145
|
-
const
|
|
135
|
+
const selection = await p.evaluate((msg) => window.pick(msg), message);
|
|
146
136
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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);
|
|
137
|
+
const output = { mode, selection };
|
|
138
|
+
if (session?.devtoolsFrontendUrl) {
|
|
139
|
+
output.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
|
|
160
140
|
}
|
|
141
|
+
console.log(JSON.stringify(output));
|
|
161
142
|
|
|
162
143
|
await b.disconnect();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run a user-supplied CDP script on the active browser session.
|
|
5
|
+
*
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { connectBrowser } from "./browser-connect.js";
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
const scriptPath = args.find((a) => !a.startsWith("--") && args[args.indexOf(a) - 1]?.startsWith("--") === false) ?? args[0];
|
|
19
|
+
|
|
20
|
+
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() };");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { browser, mode, session } = await connectBrowser();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const page = await browser.newPage();
|
|
33
|
+
|
|
34
|
+
const userCode = readFileSync(resolve(scriptPath), "utf8");
|
|
35
|
+
const AsyncFunction = (async() => {}).constructor;
|
|
36
|
+
const fn = new AsyncFunction("browser", "page", userCode);
|
|
37
|
+
const scriptResult = await fn(browser, page);
|
|
38
|
+
|
|
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));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error("✗ CDP script failed:", e.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
} finally {
|
|
48
|
+
try {
|
|
49
|
+
await browser.disconnect();
|
|
50
|
+
} catch {
|
|
51
|
+
// Session may have already expired.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -2,19 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
const b = await
|
|
8
|
-
puppeteer.connect({
|
|
9
|
-
browserURL: "http://localhost:9222",
|
|
10
|
-
defaultViewport: null,
|
|
11
|
-
}),
|
|
12
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
|
|
13
|
-
]).catch((e) => {
|
|
14
|
-
console.error("✗ Could not connect to browser:", e.message);
|
|
15
|
-
console.error(" Run: browser-start.js");
|
|
16
|
-
process.exit(1);
|
|
17
|
-
});
|
|
5
|
+
import { connectBrowser } from "./browser-connect.js";
|
|
6
|
+
|
|
7
|
+
const { browser: b, mode, session } = await connectBrowser();
|
|
18
8
|
|
|
19
9
|
const p = (await b.pages()).at(-1);
|
|
20
10
|
|
|
@@ -29,6 +19,10 @@ const filepath = join(tmpdir(), filename);
|
|
|
29
19
|
|
|
30
20
|
await p.screenshot({ path: filepath });
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
const result = { mode, path: filepath };
|
|
23
|
+
if (session?.devtoolsFrontendUrl) {
|
|
24
|
+
result.devtoolsFrontendUrl = session.devtoolsFrontendUrl;
|
|
25
|
+
}
|
|
26
|
+
console.log(JSON.stringify(result));
|
|
33
27
|
|
|
34
28
|
await b.disconnect();
|
|
@@ -2,16 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn, execSync } from "node:child_process";
|
|
4
4
|
import puppeteer from "puppeteer-core";
|
|
5
|
+
import { isSandboxMode, connectBrowser } from "./browser-connect.js";
|
|
5
6
|
|
|
6
7
|
const useProfile = process.argv[2] === "--profile";
|
|
7
8
|
|
|
8
9
|
if (process.argv[2] && process.argv[2] !== "--profile") {
|
|
9
10
|
console.log("Usage: browser-start.js [--profile]");
|
|
10
11
|
console.log("\nOptions:");
|
|
11
|
-
console.log(" --profile Copy your default Chrome profile (cookies, logins)");
|
|
12
|
+
console.log(" --profile Copy your default Chrome profile (cookies, logins) — local mode only");
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
15
|
|
|
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
|
+
}
|
|
29
|
+
|
|
30
|
+
// Local mode: start Chrome on :9222
|
|
31
|
+
// Note: execSync calls below use only hardcoded paths, not user input.
|
|
15
32
|
const SCRAPING_DIR = `${process.env.HOME}/.cache/browser-tools`;
|
|
16
33
|
|
|
17
34
|
// Check if already running on :9222
|
|
@@ -21,7 +38,7 @@ try {
|
|
|
21
38
|
defaultViewport: null,
|
|
22
39
|
});
|
|
23
40
|
await browser.disconnect();
|
|
24
|
-
console.log(
|
|
41
|
+
console.log(JSON.stringify({ mode: "local", status: "already_running" }));
|
|
25
42
|
process.exit(0);
|
|
26
43
|
} catch {}
|
|
27
44
|
|
|
@@ -34,7 +51,7 @@ try {
|
|
|
34
51
|
} catch {}
|
|
35
52
|
|
|
36
53
|
if (useProfile) {
|
|
37
|
-
console.
|
|
54
|
+
console.error("Syncing profile...");
|
|
38
55
|
execSync(
|
|
39
56
|
`rsync -a --delete \
|
|
40
57
|
--exclude='SingletonLock' \
|
|
@@ -83,4 +100,4 @@ if (!connected) {
|
|
|
83
100
|
process.exit(1);
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
console.log(
|
|
103
|
+
console.log(JSON.stringify({ mode: "local", status: "started", profile: useProfile }));
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { buildDocyrusWebBrowserProtocolInstructions } from "../shared/docyrusWebBrowserProtocol";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
"Docyrus Server sessions must use the docyrus-web-browser preview tools instead of `docyrus
|
|
4
|
+
const DOCYRUS_BROWSER_COMMAND_PATTERN = /\bdocyrus\b(?:\s+-g|\s+--global)?\s+(?:browser|chrome)\b/;
|
|
5
|
+
const DOCYRUS_BROWSER_BLOCK_REASON =
|
|
6
|
+
"Docyrus Server sessions must use the docyrus-web-browser preview tools instead of `docyrus browser`.";
|
|
7
7
|
|
|
8
8
|
export default function docyrusWebBrowserExtension(pi: ExtensionAPI) {
|
|
9
9
|
pi.on("before_agent_start", async(event) => {
|
|
@@ -19,13 +19,13 @@ export default function docyrusWebBrowserExtension(pi: ExtensionAPI) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const command = typeof event.input.command === "string" ? event.input.command.trim() : "";
|
|
22
|
-
if (!command || !
|
|
22
|
+
if (!command || !DOCYRUS_BROWSER_COMMAND_PATTERN.test(command)) {
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return {
|
|
27
27
|
block: true,
|
|
28
|
-
reason:
|
|
28
|
+
reason: DOCYRUS_BROWSER_BLOCK_REASON,
|
|
29
29
|
};
|
|
30
30
|
});
|
|
31
31
|
}
|
|
@@ -141,7 +141,7 @@ export function parseDocyrusWebBrowserRequestFromText(text: string): IDocyrusWeb
|
|
|
141
141
|
export function buildDocyrusWebBrowserProtocolInstructions(): string {
|
|
142
142
|
return [
|
|
143
143
|
"When you need to inspect or automate the Docyrus web preview in server-backed sessions, use the docyrus-web-browser client tools.",
|
|
144
|
-
"Do not use `docyrus
|
|
144
|
+
"Do not use `docyrus browser` or any visible Chrome DevTools workflow in this environment.",
|
|
145
145
|
`Instead, output only a single ${DOCYRUS_WEB_BROWSER_OPEN}...${DOCYRUS_WEB_BROWSER_CLOSE} block and nothing else in the assistant message.`,
|
|
146
146
|
"Inside that block, emit strict JSON with this shape:",
|
|
147
147
|
"{\"tool\":\"web_preview_context\",\"input\":{\"includeSnapshot\":true}}",
|