@docyrus/docyrus 0.0.59 → 0.0.62

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 (45) 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 +321 -25
  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/extensions/browser-tools.ts +229 -0
  28. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +157 -46
  29. package/server-loader.js +580 -232
  30. package/server-loader.js.map +4 -4
  31. package/resources/browser-tools/browser-connect.js +0 -172
  32. package/resources/browser-tools/browser-pick.js +0 -143
  33. package/resources/pi-agent/extensions/docyrus-web-browser.ts +0 -31
  34. package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +0 -169
  35. package/resources/pi-agent/skills/agent-browser/SKILL.md +0 -779
  36. package/resources/pi-agent/skills/agent-browser/references/authentication.md +0 -303
  37. package/resources/pi-agent/skills/agent-browser/references/commands.md +0 -295
  38. package/resources/pi-agent/skills/agent-browser/references/profiling.md +0 -120
  39. package/resources/pi-agent/skills/agent-browser/references/proxy-support.md +0 -194
  40. package/resources/pi-agent/skills/agent-browser/references/session-management.md +0 -193
  41. package/resources/pi-agent/skills/agent-browser/references/snapshot-refs.md +0 -219
  42. package/resources/pi-agent/skills/agent-browser/references/video-recording.md +0 -173
  43. package/resources/pi-agent/skills/agent-browser/templates/authenticated-session.sh +0 -105
  44. package/resources/pi-agent/skills/agent-browser/templates/capture-workflow.sh +0 -69
  45. package/resources/pi-agent/skills/agent-browser/templates/form-automation.sh +0 -62
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Persistent browser daemon — holds a raw CDP WebSocket connection and exposes
5
+ * an HTTP API on localhost. Eliminates per-command connection overhead.
6
+ *
7
+ * Protocol (all POST /):
8
+ * { method, params?, sessionId? } → { result } CDP call
9
+ * { meta: "drain_events" } → { events: [...] } buffered CDP events
10
+ * { meta: "session" } → { sessionId } current session
11
+ * { meta: "set_session", sessionId } → { sessionId } change session
12
+ * { meta: "shutdown" } → { ok: true } graceful stop
13
+ *
14
+ * GET /health → { alive, mode, sessionId, uptime }
15
+ *
16
+ * Env vars:
17
+ * BROWSER_DAEMON_PORT (default 9333)
18
+ * DOCYRUS_BROWSER_SANDBOX=1 for remote mode
19
+ * DOCYRUS_SANDBOX_APP_ID for remote mode
20
+ */
21
+
22
+ import { createServer } from "node:http";
23
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import { execFileSync } from "node:child_process";
26
+ import WebSocket from "ws";
27
+
28
+ // Allow self-signed certs for localhost dev servers
29
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
30
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
31
+ }
32
+
33
+ // ── Config ──────────────────────────────────────────────────────────────────
34
+
35
+ const PORT = parseInt(process.env.BROWSER_DAEMON_PORT || "9333", 10);
36
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
37
+ const EVENT_BUFFER_MAX = 500;
38
+ const CDP_CONNECT_RETRIES = 12;
39
+ const CDP_RETRY_DELAY_MS = 5000;
40
+ const DOCYRUS_DIR = join(process.cwd(), ".docyrus");
41
+ const PID_FILE = join(DOCYRUS_DIR, "browser-daemon.pid");
42
+ const DAEMON_INFO_FILE = join(DOCYRUS_DIR, "browser-daemon.json");
43
+
44
+ // ── State ───────────────────────────────────────────────────────────────────
45
+
46
+ let ws = null;
47
+ let sessionId = null;
48
+ let mode = "local";
49
+ let messageId = 0;
50
+ const pending = new Map(); // id → { resolve, reject, timer }
51
+ const events = []; // circular buffer
52
+ let idleTimer = null;
53
+ const startTime = Date.now();
54
+
55
+ // ── CDP WebSocket Layer ─────────────────────────────────────────────────────
56
+
57
+ function getLocalWsUrl() {
58
+ // Try localhost:9222 debug endpoint first
59
+ try {
60
+ const resp = execFileSync("node", [
61
+ "-e", "fetch('http://localhost:9222/json/version').then(r=>r.json()).then(d=>console.log(d.webSocketDebuggerUrl)).catch(()=>process.exit(1))",
62
+ ], { encoding: "utf8", timeout: 5000 }).trim();
63
+ if (resp.startsWith("ws://")) {
64
+ return resp;
65
+ }
66
+ } catch {}
67
+ throw new Error("Cannot find Chrome WebSocket URL. Is Chrome running with --remote-debugging-port=9222?");
68
+ }
69
+
70
+ async function getRemoteWsUrl() {
71
+ // Read cached session
72
+ const sessionFile = join(DOCYRUS_DIR, "browser-session.json");
73
+ try {
74
+ const cached = JSON.parse(readFileSync(sessionFile, "utf8"));
75
+ if (cached?.expiresAt && new Date(cached.expiresAt).getTime() - 30_000 > Date.now()) {
76
+ return { wsUrl: cached.browserWSEndpoint, authHeader: cached.authHeader, session: cached };
77
+ }
78
+ } catch {}
79
+
80
+ // Resolve app ID
81
+ const appId = process.env.DOCYRUS_SANDBOX_APP_ID
82
+ || (() => {
83
+ try {
84
+ return JSON.parse(readFileSync(join(DOCYRUS_DIR, "browser.json"), "utf8")).appId;
85
+ } catch { return null; }
86
+ })();
87
+
88
+ if (!appId) {
89
+ throw new Error("Sandbox mode but no app ID. Set DOCYRUS_SANDBOX_APP_ID or .docyrus/browser.json");
90
+ }
91
+
92
+ // Resolve API base URL and access token from auth store
93
+ let apiBaseUrl = "https://localhost:3366";
94
+ let accessToken = null;
95
+
96
+ try {
97
+ const configFile = join(DOCYRUS_DIR, "config.json");
98
+ const config = JSON.parse(readFileSync(configFile, "utf8"));
99
+ const envId = config.activeEnvironmentId || "dev";
100
+ const env = (config.environments || []).find((e) => e.id === envId);
101
+ if (env?.apiBaseUrl) { apiBaseUrl = env.apiBaseUrl; }
102
+ } catch {}
103
+
104
+ // Try local auth, then global auth
105
+ for (const authPath of [join(DOCYRUS_DIR, "auth.json"), join(process.env.HOME || "", ".docyrus", "auth.json")]) {
106
+ if (accessToken) {break;}
107
+ try {
108
+ const auth = JSON.parse(readFileSync(authPath, "utf8"));
109
+ // Auth store keys may have /v1 suffix
110
+ for (const key of [apiBaseUrl, `${apiBaseUrl}/v1`]) {
111
+ const active = auth.activeByApiBaseUrl?.[key];
112
+ const userId = typeof active === "string" ? active : active?.userId;
113
+ if (!userId) {continue;}
114
+ // Tokens are in the profiles array, matched by userId + apiBaseUrl
115
+ const profiles = Array.isArray(auth.profiles) ? auth.profiles : [];
116
+ const profile = profiles.find((p) => p.userId === userId && (p.apiBaseUrl === key || p.apiBaseUrl === apiBaseUrl));
117
+ if (profile?.accessToken) { accessToken = profile.accessToken; apiBaseUrl = profile.apiBaseUrl.replace(/\/v1$/, ""); break; }
118
+ }
119
+ } catch {}
120
+ }
121
+
122
+ if (!accessToken) {
123
+ throw new Error("No access token found. Run 'docyrus auth login' first.");
124
+ }
125
+
126
+ // Call createBrowserSession API directly
127
+ const url = `${apiBaseUrl}/v1/ai/sandbox/app/${appId}/createBrowserSession`;
128
+ log(`creating browser session: POST ${url}`);
129
+
130
+ const resp = await fetch(url, {
131
+ method: "POST",
132
+ headers: {
133
+ Authorization: `Bearer ${accessToken}`,
134
+ "Content-Type": "application/json",
135
+ },
136
+ // Allow self-signed certs for local dev
137
+ ...(apiBaseUrl.includes("localhost") ? { dispatcher: undefined } : {}),
138
+ });
139
+
140
+ if (!resp.ok) {
141
+ const body = await resp.text();
142
+ throw new Error(`createBrowserSession failed (${resp.status}): ${body}`);
143
+ }
144
+
145
+ const respBody = await resp.json();
146
+ // API wraps response in { success, data } — unwrap if present
147
+ const data = respBody.data || respBody;
148
+ const now = Date.now();
149
+ const session = {
150
+ ...data,
151
+ createdAt: new Date(now).toISOString(),
152
+ expiresAt: new Date(now + (data.keepAliveMs || 600_000)).toISOString(),
153
+ };
154
+
155
+ mkdirSync(DOCYRUS_DIR, { recursive: true, mode: 0o700 });
156
+ writeFileSync(join(DOCYRUS_DIR, "browser-session.json"), JSON.stringify(session, null, 2) + "\n", { mode: 0o600 });
157
+
158
+ return { wsUrl: data.browserWSEndpoint, authHeader: data.authHeader, session };
159
+ }
160
+
161
+ function isSandboxMode() {
162
+ if (process.env.DOCYRUS_BROWSER_SANDBOX === "1") { return true; }
163
+ try {
164
+ return JSON.parse(readFileSync(join(DOCYRUS_DIR, "browser.json"), "utf8")).mode === "sandbox";
165
+ } catch { return false; }
166
+ }
167
+
168
+ async function connectCdp() {
169
+ let wsUrl, headers = {};
170
+
171
+ if (isSandboxMode()) {
172
+ mode = "remote";
173
+ const remote = await getRemoteWsUrl();
174
+ wsUrl = remote.wsUrl;
175
+ if (remote.authHeader) {
176
+ headers.Authorization = remote.authHeader;
177
+ }
178
+ } else {
179
+ mode = "local";
180
+ wsUrl = getLocalWsUrl();
181
+ }
182
+
183
+ log(`connecting to ${wsUrl} (mode=${mode})`);
184
+
185
+ for (let attempt = 0; attempt < CDP_CONNECT_RETRIES; attempt++) {
186
+ try {
187
+ await new Promise((resolve, reject) => {
188
+ ws = new WebSocket(wsUrl, { headers });
189
+ ws.once("open", resolve);
190
+ ws.once("error", reject);
191
+ setTimeout(() => reject(new Error("timeout")), CDP_RETRY_DELAY_MS);
192
+ });
193
+
194
+ ws.on("message", onCdpMessage);
195
+ ws.on("close", () => { log("CDP WebSocket closed"); ws = null; });
196
+ ws.on("error", (e) => { log(`CDP WebSocket error: ${e.message}`); });
197
+
198
+ log("CDP connected");
199
+ await attachFirstPage();
200
+ return;
201
+ } catch (e) {
202
+ log(`connect attempt ${attempt + 1}/${CDP_CONNECT_RETRIES} failed: ${e.message}`);
203
+ ws = null;
204
+ if (attempt < CDP_CONNECT_RETRIES - 1) {
205
+ await sleep(CDP_RETRY_DELAY_MS);
206
+ }
207
+ }
208
+ }
209
+ throw new Error("CDP connection failed after all retries");
210
+ }
211
+
212
+ function onCdpMessage(raw) {
213
+ const msg = JSON.parse(raw.toString());
214
+
215
+ // Response to a pending request
216
+ if (msg.id !== undefined && pending.has(msg.id)) {
217
+ const { resolve, reject, timer } = pending.get(msg.id);
218
+ pending.delete(msg.id);
219
+ clearTimeout(timer);
220
+ if (msg.error) {
221
+ reject(new Error(msg.error.message || JSON.stringify(msg.error)));
222
+ } else {
223
+ resolve(msg.result || {});
224
+ }
225
+ return;
226
+ }
227
+
228
+ // CDP event — buffer it
229
+ if (msg.method) {
230
+ events.push({
231
+ method: msg.method,
232
+ params: msg.params || {},
233
+ sessionId: msg.sessionId || null,
234
+ });
235
+ while (events.length > EVENT_BUFFER_MAX) {
236
+ events.shift();
237
+ }
238
+ }
239
+ }
240
+
241
+ function sendCdp(method, params = {}, sid) {
242
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
243
+ return Promise.reject(new Error("CDP WebSocket not connected"));
244
+ }
245
+
246
+ const id = ++messageId;
247
+ const msg = { id, method, params };
248
+ if (sid) { msg.sessionId = sid; }
249
+
250
+ return new Promise((resolve, reject) => {
251
+ const timer = setTimeout(() => {
252
+ pending.delete(id);
253
+ reject(new Error(`CDP call ${method} timed out (30s)`));
254
+ }, 30_000);
255
+
256
+ pending.set(id, { resolve, reject, timer });
257
+ ws.send(JSON.stringify(msg));
258
+ });
259
+ }
260
+
261
+ async function attachFirstPage() {
262
+ const { targetInfos } = await sendCdp("Target.getTargets");
263
+ const pages = targetInfos.filter((t) =>
264
+ t.type === "page" && !t.url.startsWith("chrome://") && !t.url.startsWith("chrome-extension://") && !t.url.startsWith("devtools://"),
265
+ );
266
+
267
+ let targetId;
268
+ if (pages.length > 0) {
269
+ targetId = pages[0].targetId;
270
+ } else {
271
+ const created = await sendCdp("Target.createTarget", { url: "about:blank" });
272
+ targetId = created.targetId;
273
+ }
274
+
275
+ const attached = await sendCdp("Target.attachToTarget", { targetId, flatten: true });
276
+ sessionId = attached.sessionId;
277
+ log(`attached target=${targetId} session=${sessionId}`);
278
+
279
+ // Enable key CDP domains
280
+ for (const domain of ["Page", "DOM", "Runtime", "Network"]) {
281
+ try { await sendCdp(`${domain}.enable`, {}, sessionId); }
282
+ catch (e) { log(`enable ${domain}: ${e.message}`); }
283
+ }
284
+ }
285
+
286
+ async function reattachOnStale() {
287
+ log("session stale, re-attaching...");
288
+ try {
289
+ await attachFirstPage();
290
+ return true;
291
+ } catch (e) {
292
+ log(`re-attach failed: ${e.message}`);
293
+ return false;
294
+ }
295
+ }
296
+
297
+ // ── HTTP Server ─────────────────────────────────────────────────────────────
298
+
299
+ function resetIdle() {
300
+ if (idleTimer) { clearTimeout(idleTimer); }
301
+ idleTimer = setTimeout(() => {
302
+ log("idle timeout, shutting down");
303
+ shutdown();
304
+ }, IDLE_TIMEOUT_MS);
305
+ }
306
+
307
+ async function handleRequest(req) {
308
+ resetIdle();
309
+
310
+ // Health check
311
+ if (req.meta === "health" || req._health) {
312
+ return { alive: true, mode, sessionId, uptime: Math.round((Date.now() - startTime) / 1000) };
313
+ }
314
+
315
+ // Meta commands
316
+ if (req.meta === "drain_events") {
317
+ const out = [...events];
318
+ events.length = 0;
319
+ return { events: out };
320
+ }
321
+ if (req.meta === "session") {
322
+ return { sessionId };
323
+ }
324
+ if (req.meta === "set_session") {
325
+ sessionId = req.sessionId;
326
+ return { sessionId };
327
+ }
328
+ if (req.meta === "shutdown") {
329
+ setTimeout(() => shutdown(), 100);
330
+ return { ok: true };
331
+ }
332
+
333
+ // CDP call
334
+ const { method, params = {}, sessionId: sid } = req;
335
+ if (!method) {
336
+ return { error: "missing 'method' field" };
337
+ }
338
+
339
+ // Target.* calls go without session (browser-level)
340
+ const effectiveSid = method.startsWith("Target.") ? undefined : (sid || sessionId);
341
+
342
+ try {
343
+ const result = await sendCdp(method, params, effectiveSid);
344
+ return { result };
345
+ } catch (e) {
346
+ // Auto-recover stale session
347
+ if (e.message?.includes("not found") && effectiveSid === sessionId) {
348
+ if (await reattachOnStale()) {
349
+ try {
350
+ const result = await sendCdp(method, params, sessionId);
351
+ return { result };
352
+ } catch (e2) {
353
+ return { error: e2.message };
354
+ }
355
+ }
356
+ }
357
+ return { error: e.message };
358
+ }
359
+ }
360
+
361
+ function startHttpServer() {
362
+ const server = createServer(async(req, res) => {
363
+ // CORS for local dev
364
+ res.setHeader("Access-Control-Allow-Origin", "*");
365
+ res.setHeader("Content-Type", "application/json");
366
+
367
+ if (req.method === "GET" && req.url === "/health") {
368
+ const result = await handleRequest({ _health: true });
369
+ res.writeHead(200);
370
+ res.end(JSON.stringify(result));
371
+ return;
372
+ }
373
+
374
+ if (req.method !== "POST") {
375
+ res.writeHead(405);
376
+ res.end(JSON.stringify({ error: "POST only" }));
377
+ return;
378
+ }
379
+
380
+ let body = "";
381
+ req.on("data", (chunk) => { body += chunk; });
382
+ req.on("end", async() => {
383
+ try {
384
+ const parsed = JSON.parse(body);
385
+ const result = await handleRequest(parsed);
386
+ res.writeHead(result.error ? 500 : 200);
387
+ res.end(JSON.stringify(result));
388
+ } catch (e) {
389
+ res.writeHead(400);
390
+ res.end(JSON.stringify({ error: e.message }));
391
+ }
392
+ });
393
+ });
394
+
395
+ server.listen(PORT, "127.0.0.1", () => {
396
+ log(`HTTP server on http://127.0.0.1:${PORT} (mode=${mode})`);
397
+ writeDaemonInfo();
398
+ });
399
+
400
+ server.on("error", (e) => {
401
+ if (e.code === "EADDRINUSE") {
402
+ log(`port ${PORT} already in use — daemon may already be running`);
403
+ process.exit(0);
404
+ }
405
+ throw e;
406
+ });
407
+
408
+ return server;
409
+ }
410
+
411
+ // ── Lifecycle ───────────────────────────────────────────────────────────────
412
+
413
+ function writeDaemonInfo() {
414
+ mkdirSync(DOCYRUS_DIR, { recursive: true, mode: 0o700 });
415
+ writeFileSync(PID_FILE, String(process.pid), { mode: 0o600 });
416
+ writeFileSync(DAEMON_INFO_FILE, JSON.stringify({ pid: process.pid, port: PORT, mode, startedAt: new Date().toISOString() }, null, 2) + "\n", { mode: 0o600 });
417
+ }
418
+
419
+ function shutdown() {
420
+ log("shutting down");
421
+ if (ws && ws.readyState === WebSocket.OPEN) {
422
+ ws.close();
423
+ }
424
+ try { unlinkSync(PID_FILE); } catch {}
425
+ try { unlinkSync(DAEMON_INFO_FILE); } catch {}
426
+ process.exit(0);
427
+ }
428
+
429
+ function log(msg) {
430
+ const ts = new Date().toISOString().slice(11, 23);
431
+ process.stderr.write(`[daemon ${ts}] ${msg}\n`);
432
+ }
433
+
434
+ function sleep(ms) {
435
+ return new Promise((r) => setTimeout(r, ms));
436
+ }
437
+
438
+ // ── Main ────────────────────────────────────────────────────────────────────
439
+
440
+ process.on("SIGTERM", shutdown);
441
+ process.on("SIGINT", shutdown);
442
+
443
+ try {
444
+ await connectCdp();
445
+ startHttpServer();
446
+ resetIdle();
447
+ } catch (e) {
448
+ log(`fatal: ${e.message}`);
449
+ try { unlinkSync(PID_FILE); } catch {}
450
+ try { unlinkSync(DAEMON_INFO_FILE); } catch {}
451
+ process.exit(1);
452
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Read @docyrus/devtools state from the active page.
5
+ *
6
+ * Usage:
7
+ * browser-devtools.js state # full devtools state
8
+ * browser-devtools.js errors # API errors
9
+ * browser-devtools.js issues # detected issues
10
+ * browser-devtools.js console # console entries
11
+ * browser-devtools.js console --level error
12
+ */
13
+
14
+ import { ensureDaemon, evaluate, getMode } from "./browser-client.js";
15
+
16
+ const args = process.argv.slice(2);
17
+ const subcommand = args[0];
18
+ const levelIdx = args.indexOf("--level");
19
+ const level = levelIdx !== -1 ? args[levelIdx + 1] : null;
20
+
21
+ if (!subcommand || !["state", "errors", "issues", "console"].includes(subcommand)) {
22
+ console.log("Usage: browser-devtools.js <state|errors|issues|console> [--level <level>]");
23
+ console.log("\nSubcommands:");
24
+ console.log(" state Full @docyrus/devtools state");
25
+ console.log(" errors Collected API errors");
26
+ console.log(" issues Detected API usage issues");
27
+ console.log(" console Console entries (optionally filtered by --level)");
28
+ process.exit(1);
29
+ }
30
+
31
+ await ensureDaemon();
32
+
33
+ try {
34
+ const available = evaluate("typeof window.__DOCYRUS_DEVTOOLS__ !== 'undefined'");
35
+ if (!available) {
36
+ console.error("✗ @docyrus/devtools is not loaded on this page");
37
+ process.exit(1);
38
+ }
39
+
40
+ let result;
41
+ switch (subcommand) {
42
+ case "state":
43
+ result = evaluate("window.__DOCYRUS_DEVTOOLS__.getState()");
44
+ break;
45
+ case "errors":
46
+ result = evaluate("window.__DOCYRUS_DEVTOOLS__.getErrors()");
47
+ break;
48
+ case "issues":
49
+ result = evaluate("window.__DOCYRUS_DEVTOOLS__.getIssues()");
50
+ break;
51
+ case "console":
52
+ result = level
53
+ ? evaluate(`window.__DOCYRUS_DEVTOOLS__.getConsoleEntries(${JSON.stringify(level)})`)
54
+ : evaluate("window.__DOCYRUS_DEVTOOLS__.getConsoleEntries()");
55
+ break;
56
+ }
57
+
58
+ console.log(JSON.stringify({ mode: getMode(), [subcommand]: result }));
59
+ } catch (e) {
60
+ console.error(`✗ Devtools failed: ${e.message}`);
61
+ process.exit(1);
62
+ }
@@ -1,34 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { connectBrowser } from "./browser-connect.js";
3
+ import { ensureDaemon, evaluate, getMode } from "./browser-client.js";
4
+
5
+ const args = process.argv.slice(2);
6
+ const timeoutIdx = args.indexOf("--timeout");
7
+ const codeArgs = timeoutIdx === -1 ? args : args.filter((_, i) => i !== timeoutIdx && i !== timeoutIdx + 1);
8
+ const code = codeArgs.join(" ");
4
9
 
5
- const code = process.argv.slice(2).join(" ");
6
10
  if (!code) {
7
- console.log("Usage: browser-eval.js 'code'");
11
+ console.log("Usage: browser-eval.js 'code' [--timeout <ms>]");
8
12
  console.log("\nExamples:");
9
13
  console.log(' browser-eval.js "document.title"');
10
- console.log(' browser-eval.js "document.querySelectorAll(\'a\').length"');
14
+ console.log(' browser-eval.js "const x = 1; const y = 2; return x + y"');
11
15
  process.exit(1);
12
16
  }
13
17
 
14
- const { browser: b, mode, session } = await connectBrowser();
15
-
16
- const p = (await b.pages()).at(-1);
18
+ await ensureDaemon();
17
19
 
18
- if (!p) {
19
- console.error("✗ No active tab found");
20
+ try {
21
+ // Wrap: try as expression first, fall back to multi-statement function body
22
+ const wrappedCode = `(async () => { try { return (${code}) } catch { ${code} } })()`;
23
+ const result = evaluate(wrappedCode);
24
+ console.log(JSON.stringify({ mode: getMode(), result }));
25
+ } catch (e) {
26
+ console.error(`✗ Eval failed: ${e.message}`);
20
27
  process.exit(1);
21
28
  }
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();
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { ensureDaemon, evaluate, clickAt, typeText, cdp, 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 === undefined) {
15
+ console.log('Usage: browser-fill.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
+ function resolveSelector(target) {
32
+ if (target.startsWith("@e")) {
33
+ const { selector, xpath } = resolveRef(target);
34
+ return { selector, xpath };
35
+ }
36
+ return { selector: target, xpath: null };
37
+ }
38
+
39
+ await ensureDaemon();
40
+
41
+ try {
42
+ const { selector, xpath } = resolveSelector(target);
43
+ const selectorExpr = JSON.stringify(selector);
44
+ const xpathExpr = xpath ? JSON.stringify(xpath) : "null";
45
+
46
+ waitForCondition(`!!document.querySelector(${selectorExpr}) || (${xpathExpr} && !!document.evaluate(${xpathExpr}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue)`, timeout);
47
+
48
+ // Focus the element and select all existing content
49
+ const coords = evaluate(`(() => {
50
+ let el = document.querySelector(${selectorExpr});
51
+ if (!el && ${xpathExpr}) el = document.evaluate(${xpathExpr}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
52
+ if (!el) return null;
53
+ el.focus();
54
+ if (el.select) el.select();
55
+ const r = el.getBoundingClientRect();
56
+ return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
57
+ })()`);
58
+
59
+ if (!coords) { throw new Error(`Element not found: ${target}`); }
60
+
61
+ // Triple-click to select all, then type replacement
62
+ cdp("Input.dispatchMouseEvent", { type: "mousePressed", x: coords.x, y: coords.y, button: "left", clickCount: 3 });
63
+ cdp("Input.dispatchMouseEvent", { type: "mouseReleased", x: coords.x, y: coords.y, button: "left", clickCount: 3 });
64
+ typeText(value);
65
+
66
+ console.log(JSON.stringify({ mode: getMode(), filled: target, value }));
67
+ } catch (e) {
68
+ console.error(`✗ Fill failed: ${e.message}`);
69
+ process.exit(1);
70
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { ensureDaemon, pageInfo, getMode } from "./browser-client.js";
4
+
5
+ await ensureDaemon();
6
+
7
+ try {
8
+ const info = pageInfo();
9
+ console.log(JSON.stringify({ mode: getMode(), ...info }));
10
+ } catch (e) {
11
+ console.error(`✗ Info failed: ${e.message}`);
12
+ process.exit(1);
13
+ }