surf-cli 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -4
- package/dist/content/index.js +116 -0
- package/dist/content/index.js.map +1 -0
- package/dist/manifest.json +2 -11
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +261 -61
- package/dist/service-worker/index.js.map +1 -1
- package/native/abort.cjs +65 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +2 -2
- package/native/chatgpt-client.cjs +47 -31
- package/native/cli.cjs +300 -204
- package/native/client-transport.cjs +168 -0
- package/native/do-executor.cjs +25 -44
- package/native/doctor.cjs +55 -5
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +156 -71
- package/native/grok-client.cjs +98 -89
- package/native/host-helpers.cjs +37 -12
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +800 -620
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -65
- package/native/network-export.cjs +113 -0
- package/native/perplexity-client.cjs +46 -17
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +1 -1
- package/package.json +8 -6
- package/scripts/install-native-host.cjs +36 -5
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +31 -4
- package/dist/content/accessibility-tree.js +0 -11
- package/dist/content/accessibility-tree.js.map +0 -1
- package/dist/content/visual-indicator.js +0 -111
- package/dist/content/visual-indicator.js.map +0 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const { abortError } = require("./abort.cjs");
|
|
2
|
+
|
|
3
|
+
class RequestPendingMap extends Map {
|
|
4
|
+
constructor({ getRequest = () => undefined } = {}) {
|
|
5
|
+
super();
|
|
6
|
+
this.getRequest = getRequest;
|
|
7
|
+
this.drainWaiters = new Map();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
set(id, data) {
|
|
11
|
+
const request = data.request || this.getRequest();
|
|
12
|
+
const cleanup = Boolean(data.cleanup || data.tool === "close_tab");
|
|
13
|
+
const entry = { ...data, id, request, cleanup, aborted: false, settled: false };
|
|
14
|
+
if (request) {
|
|
15
|
+
if (!request.pendingEntries) request.pendingEntries = new Set();
|
|
16
|
+
request.pendingEntries.add(entry);
|
|
17
|
+
if (cleanup) entry.abortCleanup = null;
|
|
18
|
+
else {
|
|
19
|
+
const onAbort = () => {
|
|
20
|
+
if (entry.abortNotified || entry.settled) return;
|
|
21
|
+
entry.abortNotified = true;
|
|
22
|
+
entry.aborted = true;
|
|
23
|
+
const error = abortError(request.signal);
|
|
24
|
+
if (entry.reject) entry.reject(error);
|
|
25
|
+
else entry.onAbort?.(error);
|
|
26
|
+
if (!entry.reject && !entry.onAbort) entry.onComplete?.({ error: error.message, cancelled: true });
|
|
27
|
+
};
|
|
28
|
+
entry.abortCleanup = () => request.signal.removeEventListener("abort", onAbort);
|
|
29
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
30
|
+
if (request.signal.aborted) onAbort();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
super.set(id, entry);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(id) {
|
|
38
|
+
return super.get(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
delete(id) {
|
|
42
|
+
const entry = super.get(id);
|
|
43
|
+
if (!entry) return false;
|
|
44
|
+
this.#removeEntry(entry);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#removeEntry(entry, notify = true) {
|
|
49
|
+
super.delete(entry.id);
|
|
50
|
+
entry.abortCleanup?.();
|
|
51
|
+
if (entry.tombstoneTimer) clearTimeout(entry.tombstoneTimer);
|
|
52
|
+
entry.request?.pendingEntries?.delete(entry);
|
|
53
|
+
if (notify) this.#notifyDrain(entry.request);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#notifyDrain(request) {
|
|
57
|
+
if (!request || request.pendingEntries?.size) return;
|
|
58
|
+
const waiters = this.drainWaiters.get(request);
|
|
59
|
+
if (!waiters) return;
|
|
60
|
+
this.drainWaiters.delete(request);
|
|
61
|
+
for (const waiter of waiters) waiter();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onDrain(request, callback) {
|
|
65
|
+
if (!request?.pendingEntries?.size) {
|
|
66
|
+
callback();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const waiters = this.drainWaiters.get(request) || [];
|
|
70
|
+
waiters.push(callback);
|
|
71
|
+
this.drainWaiters.set(request, waiters);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resolve(id, value) {
|
|
75
|
+
const entry = this.get(id);
|
|
76
|
+
if (!entry) return false;
|
|
77
|
+
this.#removeEntry(entry, false);
|
|
78
|
+
try {
|
|
79
|
+
if (!entry.aborted && !entry.hardBoundary && !entry.settled) {
|
|
80
|
+
entry.settled = true;
|
|
81
|
+
if (entry.resolve) entry.resolve(value);
|
|
82
|
+
else if (entry.onComplete) entry.onComplete(value);
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
this.#notifyDrain(entry.request);
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expire(id, error) {
|
|
91
|
+
const entry = this.get(id);
|
|
92
|
+
if (!entry) return false;
|
|
93
|
+
if (!entry.settled) {
|
|
94
|
+
entry.settled = true;
|
|
95
|
+
entry.aborted = true;
|
|
96
|
+
entry.reject?.(error);
|
|
97
|
+
const request = entry.request;
|
|
98
|
+
const remaining = request ? Math.max(0, request.startedAt + request.deadlineMs - Date.now()) : 0;
|
|
99
|
+
entry.tombstoneTimer = setTimeout(() => this.delete(entry.id), remaining);
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
reject(id, error) {
|
|
105
|
+
const entry = this.get(id);
|
|
106
|
+
if (!entry) return false;
|
|
107
|
+
this.#removeEntry(entry, false);
|
|
108
|
+
try {
|
|
109
|
+
if (!entry.settled) {
|
|
110
|
+
entry.settled = true;
|
|
111
|
+
entry.reject?.(error);
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
this.#notifyDrain(entry.request);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
tombstoneAfterAbort(request) {
|
|
120
|
+
if (!request?.pendingEntries) return;
|
|
121
|
+
for (const entry of request.pendingEntries) {
|
|
122
|
+
if (entry.cleanup || entry.tombstoneTimer) continue;
|
|
123
|
+
const remaining = Math.max(0, request.startedAt + request.deadlineMs - Date.now());
|
|
124
|
+
entry.tombstoneTimer = setTimeout(() => this.delete(entry.id), remaining);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hardDeadline(request) {
|
|
129
|
+
if (!request) return;
|
|
130
|
+
request.hardBoundary = true;
|
|
131
|
+
for (const entry of [...(request.pendingEntries || [])]) {
|
|
132
|
+
entry.hardBoundary = true;
|
|
133
|
+
this.#removeEntry(entry);
|
|
134
|
+
if (!entry.settled) {
|
|
135
|
+
entry.settled = true;
|
|
136
|
+
entry.reject?.(abortError(request.signal, "Request timed out"));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clear() {
|
|
142
|
+
for (const entry of this.values()) this.#removeEntry(entry);
|
|
143
|
+
this.drainWaiters.clear();
|
|
144
|
+
super.clear();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { RequestPendingMap };
|
package/native/socket-path.cjs
CHANGED
|
@@ -4,7 +4,7 @@ const os = require("os");
|
|
|
4
4
|
const IS_WIN = process.platform === "win32";
|
|
5
5
|
const DEFAULT_SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
|
|
6
6
|
const SOCKET_PATH = process.env.SURF_SOCKET || DEFAULT_SOCKET_PATH;
|
|
7
|
-
const SURF_TMP = IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp";
|
|
7
|
+
const SURF_TMP = process.env.SURF_TMP || (IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp");
|
|
8
8
|
|
|
9
9
|
function getSocketTroubleshootingHint() {
|
|
10
10
|
const lines = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"lint:test": "biome check test/",
|
|
44
44
|
"format": "biome format --write .",
|
|
45
45
|
"test": "vitest run",
|
|
46
|
+
"test:e2e:chrome": "node test/e2e/real-chrome.mjs",
|
|
46
47
|
"test:watch": "vitest",
|
|
47
48
|
"test:coverage": "vitest run --coverage",
|
|
48
49
|
"test:ui": "vitest --ui",
|
|
@@ -56,16 +57,17 @@
|
|
|
56
57
|
"crypto-browserify": "^3.12.1",
|
|
57
58
|
"events": "^3.3.0",
|
|
58
59
|
"stream-browserify": "^3.0.0",
|
|
59
|
-
"vite-plugin-node-polyfills": "^0.
|
|
60
|
+
"vite-plugin-node-polyfills": "^0.28.0",
|
|
60
61
|
"zod": "^4.3.6"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
|
-
"@biomejs/biome": "^2.
|
|
64
|
-
"@types/chrome": "^0.
|
|
64
|
+
"@biomejs/biome": "^2.5.4",
|
|
65
|
+
"@types/chrome": "^0.2.2",
|
|
65
66
|
"@vitest/coverage-v8": "^4.1.9",
|
|
66
67
|
"@vitest/ui": "^4.1.9",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
68
|
+
"puppeteer": "25.3.0",
|
|
69
|
+
"typescript": "^7.0.2",
|
|
70
|
+
"vite": "^8.1.4",
|
|
69
71
|
"vitest": "^4.1.9"
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -3,6 +3,8 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const { execFileSync, execSync } = require("child_process");
|
|
6
|
+
const { parseListenEndpoint } = require("../native/listener.cjs");
|
|
7
|
+
const { getStateDir, loadHostIdentity, loadRegistry } = require("../native/remote-auth.cjs");
|
|
6
8
|
|
|
7
9
|
const HOST_NAME = "surf.browser.host";
|
|
8
10
|
|
|
@@ -164,7 +166,7 @@ function wslPathToWindowsPath(wslPath) {
|
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform) {
|
|
169
|
+
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform, listen) {
|
|
168
170
|
fs.mkdirSync(wrapperDir, { recursive: true });
|
|
169
171
|
|
|
170
172
|
if (target === "wsl-windows") {
|
|
@@ -186,13 +188,19 @@ function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform
|
|
|
186
188
|
const hostDir = path.dirname(hostPath);
|
|
187
189
|
const content = `#!/usr/bin/env bash
|
|
188
190
|
cd "${hostDir}"
|
|
189
|
-
exec "${nodePath}" "${hostPath}" "$@"
|
|
191
|
+
${listen ? `: "\${SURF_LISTEN:=${listen}}"\nexport SURF_LISTEN\n` : ""}exec "${nodePath}" "${hostPath}" "$@"
|
|
190
192
|
`;
|
|
191
193
|
fs.writeFileSync(shPath, content);
|
|
192
194
|
fs.chmodSync(shPath, "755");
|
|
193
195
|
return shPath;
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
function assertListenTargetSupported(listen, target) {
|
|
199
|
+
if (listen && (target === "win32" || target === "wsl-windows")) {
|
|
200
|
+
throw new Error("--listen is not supported for Windows native-host wrappers");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
function readExistingManifest(manifestPath) {
|
|
197
205
|
if (!fs.existsSync(manifestPath)) return {};
|
|
198
206
|
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
@@ -268,7 +276,7 @@ function installWindowsRegistry(browser, extensionId, wrapperPath) {
|
|
|
268
276
|
|
|
269
277
|
function parseArgs() {
|
|
270
278
|
const args = process.argv.slice(2);
|
|
271
|
-
const result = { extensionId: null, browsers: ["chrome"], target: "auto" };
|
|
279
|
+
const result = { extensionId: null, browsers: ["chrome"], target: "auto", listen: undefined };
|
|
272
280
|
|
|
273
281
|
for (let i = 0; i < args.length; i++) {
|
|
274
282
|
const arg = args[i];
|
|
@@ -281,6 +289,9 @@ function parseArgs() {
|
|
|
281
289
|
}
|
|
282
290
|
} else if (arg === "--target") {
|
|
283
291
|
result.target = args[++i];
|
|
292
|
+
} else if (arg === "--listen") {
|
|
293
|
+
result.listen = args[++i];
|
|
294
|
+
if (!result.listen || result.listen.startsWith("--")) throw new Error("--listen requires a Tailnet IP and port");
|
|
284
295
|
} else if (arg === "--help" || arg === "-h") {
|
|
285
296
|
printHelp();
|
|
286
297
|
process.exit(0);
|
|
@@ -307,17 +318,24 @@ Options:
|
|
|
307
318
|
Multiple: --browser chrome,brave
|
|
308
319
|
--target Install target: auto, linux, windows
|
|
309
320
|
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
321
|
+
--listen <tailscale-ip>:<port>
|
|
322
|
+
Persist an authenticated Tailnet-only listener endpoint.
|
|
323
|
+
Requires at least one surf remote authorize client first.
|
|
324
|
+
Supports Tailscale IPv4 or IPv6 addresses; POSIX wrappers only.
|
|
310
325
|
|
|
311
326
|
Examples:
|
|
312
327
|
node install-native-host.cjs abcdefghijklmnopabcdefghijklmnop
|
|
313
328
|
node install-native-host.cjs abcdefghijklmnop --browser brave
|
|
314
329
|
node install-native-host.cjs abcdefghijklmnop --browser all
|
|
315
330
|
node install-native-host.cjs abcdefghijklmnop --target linux
|
|
331
|
+
node install-native-host.cjs abcdefghijklmnop --listen 100.64.1.2:4321
|
|
316
332
|
`);
|
|
317
333
|
}
|
|
318
334
|
|
|
319
335
|
function main() {
|
|
320
|
-
|
|
336
|
+
let parsed;
|
|
337
|
+
try { parsed = parseArgs(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
338
|
+
const { extensionId, browsers, target, listen } = parsed;
|
|
321
339
|
|
|
322
340
|
if (!extensionId) {
|
|
323
341
|
console.error("Error: Extension ID required");
|
|
@@ -331,6 +349,17 @@ function main() {
|
|
|
331
349
|
console.error("Expected 32 lowercase letters (a-p)");
|
|
332
350
|
process.exit(1);
|
|
333
351
|
}
|
|
352
|
+
let listener;
|
|
353
|
+
try {
|
|
354
|
+
listener = listen ? parseListenEndpoint(listen).display : undefined;
|
|
355
|
+
if (listener) {
|
|
356
|
+
const stateDir = getStateDir();
|
|
357
|
+
loadHostIdentity(stateDir);
|
|
358
|
+
if (loadRegistry(stateDir).clients.length === 0) {
|
|
359
|
+
throw new Error("--listen requires at least one authorized remote client; run `surf remote authorize <label> --output <path>` first");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
334
363
|
|
|
335
364
|
if (!["auto", "linux", "windows"].includes(target)) {
|
|
336
365
|
console.error("Error: Invalid --target value. Expected auto, linux, or windows");
|
|
@@ -349,6 +378,7 @@ function main() {
|
|
|
349
378
|
}
|
|
350
379
|
|
|
351
380
|
const effectiveTarget = runningInWsl && target !== "linux" ? "wsl-windows" : process.platform;
|
|
381
|
+
try { assertListenTargetSupported(listen, effectiveTarget); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
352
382
|
|
|
353
383
|
const nodePath = findNode();
|
|
354
384
|
if (!nodePath) {
|
|
@@ -377,7 +407,7 @@ function main() {
|
|
|
377
407
|
console.log(`Wrapper dir: ${wrapperDir}`);
|
|
378
408
|
console.log("");
|
|
379
409
|
|
|
380
|
-
const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath, effectiveTarget);
|
|
410
|
+
const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath, effectiveTarget, listener);
|
|
381
411
|
console.log(`Created wrapper: ${wrapperPath}`);
|
|
382
412
|
console.log("");
|
|
383
413
|
|
|
@@ -419,4 +449,5 @@ if (require.main === module) {
|
|
|
419
449
|
module.exports = {
|
|
420
450
|
createWrapper,
|
|
421
451
|
writeManifest,
|
|
452
|
+
assertListenTargetSupported,
|
|
422
453
|
};
|
package/skills/README.md
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
# Surf Skills
|
|
2
2
|
|
|
3
|
-
This directory contains skill files for AI coding agents
|
|
3
|
+
This directory contains skill files for AI coding agents:
|
|
4
|
+
|
|
5
|
+
- **`surf/`** — the core browser-automation reference: every surf command, workflows, AI assistants, troubleshooting.
|
|
6
|
+
- **`deep-x-research/`** — a research procedure built on surf: exhaustive, multi-angle X (Twitter) research with categorized findings and full post-URL traceability. Requires x.com login in Chrome.
|
|
7
|
+
|
|
8
|
+
Install each skill folder the same way (symlink or copy).
|
|
4
9
|
|
|
5
10
|
## Pi Agent
|
|
6
11
|
|
|
7
|
-
To use
|
|
12
|
+
To use a skill with [Pi coding agent](https://github.com/badlogic/pi-mono):
|
|
8
13
|
|
|
9
14
|
```bash
|
|
10
15
|
# Option 1: Symlink (auto-updates)
|
|
11
16
|
ln -s "$(pwd)/skills/surf" ~/.agents/skills/surf
|
|
17
|
+
ln -s "$(pwd)/skills/deep-x-research" ~/.agents/skills/deep-x-research
|
|
12
18
|
|
|
13
19
|
# Option 2: Copy
|
|
14
|
-
cp -r skills/surf ~/.agents/skills/
|
|
20
|
+
cp -r skills/surf skills/deep-x-research ~/.agents/skills/
|
|
15
21
|
```
|
|
16
22
|
|
|
17
|
-
The
|
|
23
|
+
The skills will be available when pi detects browser automation or X research tasks.
|
|
18
24
|
|
|
19
25
|
## Other Agents
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
Each `SKILL.md` file can be adapted for other AI coding agents (Claude Code, Codex) or used as documentation for LLM prompts — copy the skill folder into the agent's skills directory (e.g. `~/.claude/skills/`, `~/.agents/skills/`).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deep-x-research
|
|
3
|
+
description: Deep, exhaustive research on a topic across X (Twitter) by driving Grok (x.com/i/grok) through surf. Use when the user wants comprehensive X research on a concept, technique, trend, tool, or creator scene; needs categorized findings with every claim traceable to post URLs; or when a single Grok query is not enough.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deep X Research
|
|
7
|
+
|
|
8
|
+
Research a topic across X by putting Grok to work from multiple angles — it runs keyword and semantic X searches and watches videos natively — then deliver categorized findings where every claim is traceable to a post URL.
|
|
9
|
+
|
|
10
|
+
Requires: surf installed and connected (`surf doctor`), Chrome logged into x.com. Command reference: the `surf` skill or `surf --help`.
|
|
11
|
+
|
|
12
|
+
**Quota:** X caps Grok requests (typically 15 per 20 hours on a standard plan). Every `surf grok` call spends one. Budget the session before the first query and make each query do multi-angle work — never spend a request on what a quota-free step can answer.
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Decompose the topic and budget the queries
|
|
17
|
+
|
|
18
|
+
Break the topic into angles: showcases/examples, techniques & tutorials, tools, notable creators, community discussion — adapt to the topic. Plan a Grok budget of **4-8 queries** covering every angle (combine related angles into one query rather than spending two). Done when each angle is assigned to a budgeted query.
|
|
19
|
+
|
|
20
|
+
### 2. Grok sweep
|
|
21
|
+
|
|
22
|
+
Run the budgeted queries sequentially. Engineer each so Grok does the fan-out internally and returns traceable sources:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Broad pass — force multi-angle search and URLs
|
|
26
|
+
surf grok "Do deep research on TOPIC on X. Search both latest and top posts, keyword and semantic. Return the most relevant posts with full post URLs (x.com/user/status/ID) and a one-line description of each."
|
|
27
|
+
|
|
28
|
+
# Focused passes — one per remaining angle group
|
|
29
|
+
surf grok "TOPIC on X: tutorials, techniques, and the tools people use. Include post URLs for every example."
|
|
30
|
+
|
|
31
|
+
# Deepest pass — spend DeepSearch on the highest-value angle
|
|
32
|
+
surf grok "TOPIC: notable creators, how the trend is evolving, and the standout posts of the last 6 months. Post URLs required." --deep-search
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Record every post Grok cites: author, one-line gist, full `https://x.com/USER/status/ID` URL. If a response gives claims without URLs, the *next* query in the budget re-asks for sources — never leave an angle sourceless. Done when every planned angle has been queried and the final response adds no new relevant posts, or the budget is spent.
|
|
36
|
+
|
|
37
|
+
### 3. Video pass (visual topics)
|
|
38
|
+
|
|
39
|
+
When the topic involves video, editing, or visual style, spend 1-3 budgeted queries having Grok analyze the strongest video posts — it can watch X videos natively:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
surf grok "Analyze the videos in these posts: URL1 URL2 URL3 — for each, describe the techniques, pacing, and style, and why it works."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Batch several URLs per query to conserve budget. Done when each analyzed video has notes on what it shows and why it matters for the topic.
|
|
46
|
+
|
|
47
|
+
### 4. Enrich and verify — quota-free
|
|
48
|
+
|
|
49
|
+
For each cited post, open it directly with surf (no Grok spend) to verify the URL resolves and harvest detail Grok didn't give:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
surf navigate "https://x.com/USER/status/ID" && surf wait 2
|
|
53
|
+
surf page.read --compact # engagement numbers, thread context
|
|
54
|
+
surf network | grep video.twimg # direct video URL after playback
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Done when every URL destined for the References section has been resolved (dead or hallucinated links dropped or replaced).
|
|
58
|
+
|
|
59
|
+
### 5. Categorize and analyze
|
|
60
|
+
|
|
61
|
+
Group findings into categories that fit the topic. Extract trends: momentum on X, recurring techniques, notable creators, how the topic is evolving. Done when every recorded post is either placed in a category or deliberately dropped as irrelevant.
|
|
62
|
+
|
|
63
|
+
### 6. Report with full traceability
|
|
64
|
+
|
|
65
|
+
```md
|
|
66
|
+
# Deep Research on [Topic]
|
|
67
|
+
|
|
68
|
+
## Summary
|
|
69
|
+
[2-4 paragraphs: state of the topic on X]
|
|
70
|
+
|
|
71
|
+
## Key Trends
|
|
72
|
+
- ...
|
|
73
|
+
|
|
74
|
+
## Categorized Findings
|
|
75
|
+
### [Category]
|
|
76
|
+
- [Finding with inline post reference]
|
|
77
|
+
|
|
78
|
+
## Notable Creators & Techniques
|
|
79
|
+
- ...
|
|
80
|
+
|
|
81
|
+
## References
|
|
82
|
+
1. [Author — one-line description]
|
|
83
|
+
https://x.com/USER/status/ID
|
|
84
|
+
Video: https://video.twimg.com/... (when captured)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The report is done when **every post mentioned anywhere in it appears in References with its full, verified URL** — no bare @handles, no "a viral post showed…" without a link.
|
|
88
|
+
|
|
89
|
+
## Fallback: direct search when the Grok quota is exhausted
|
|
90
|
+
|
|
91
|
+
The x.com search UI costs no Grok requests. Slower and keyword-only, but it keeps the sweep going:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
surf navigate "https://x.com/search?q=QUERY&f=live" && surf wait 3 # Latest
|
|
95
|
+
surf page.read --compact
|
|
96
|
+
surf scroll down 2000 # then page.read again — repeat to load more
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Modes via the `f=` param: **Top is the default (no `f` param)** — there is no `f=top`; `f=live` = Latest, `f=user` = People, `f=media` = Media. Operators compose into the URL-encoded `q=`: `"exact phrase"`, `filter:videos`, `min_faves:100`, `min_retweets:50`, `from:user`, `since:2026-01-01`, `until:2026-06-01`.
|
|
100
|
+
|
|
101
|
+
## Troubleshooting
|
|
102
|
+
|
|
103
|
+
- Grok replies with a rate-limit message → quota exhausted; switch to the fallback sweep and tell the user when the quota resets.
|
|
104
|
+
- Grok queries fail outright → `surf grok --validate`, then retry with a model from the validation output (see the `surf` skill's AI troubleshooting section).
|
|
105
|
+
- Grok cites posts without URLs → re-ask in the next budgeted query; do not invent URLs.
|
|
106
|
+
- Search page shows a login wall → Chrome isn't logged into x.com; ask the user to log in.
|
package/skills/surf/SKILL.md
CHANGED
|
@@ -15,6 +15,28 @@ On macOS, Chrome reads the native messaging manifest at `~/Library/Application S
|
|
|
15
15
|
|
|
16
16
|
If a command reports `Socket connect failed`, run `surf doctor` first, then check the `Attempted socket:` line. Default sockets are `/tmp/surf.sock` on macOS/Linux/WSL2 and `//./pipe/surf` on Windows. If `SURF_SOCKET` is set, the browser-launched host and the shell running `surf` must use the same value.
|
|
17
17
|
|
|
18
|
+
## Remote Surf
|
|
19
|
+
|
|
20
|
+
Remote clients require a per-client credential; Tailnet reachability alone is not authorization. On the POSIX browser host, authorize the client before installing the listener:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
surf remote authorize agent-macbook --output ~/agent-macbook.surf-credential.json
|
|
24
|
+
surf install <extension-id> --listen 100.101.102.103:4321
|
|
25
|
+
surf remote list
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Move the mode-0600 credential to the client through a secure channel. It grants full trusted Surf authority. Use it explicitly or through `SURF_REMOTE` and `SURF_REMOTE_CREDENTIAL`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
surf --remote 100.101.102.103:4321 \
|
|
32
|
+
--remote-credential ~/.config/surf/agent-macbook.json \
|
|
33
|
+
page.read
|
|
34
|
+
|
|
35
|
+
surf remote revoke agent-macbook # Run on the browser host
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Remote paths are client-local by default. `local:./file` is explicit client-local syntax; only `remote:/absolute/path` accesses the browser host directly. Remote transfer supports one upload or ChatGPT/Gemini input and one screenshot, network-export, or Gemini image output. Limits are 256 MiB per file, 512 MiB and 32 files per connection, and 256 KiB decoded chunks. `record`, `aistudio.build`, smoke screenshot directories, directories, and multi-file inputs are not supported remotely. Successful action screenshots and failure `--auto-capture` diagnostics are transferred back to client-local paths.
|
|
39
|
+
|
|
18
40
|
## CLI Quick Reference
|
|
19
41
|
|
|
20
42
|
```bash
|
|
@@ -68,7 +90,7 @@ surf gemini "analyze" --file data.csv # Attach file
|
|
|
68
90
|
surf gemini "a robot surfing" --generate-image /tmp/robot.png
|
|
69
91
|
surf gemini "add sunglasses" --edit-image photo.jpg --output out.jpg
|
|
70
92
|
surf gemini "summarize" --youtube "https://youtube.com/..."
|
|
71
|
-
surf gemini "hello" --model gemini-
|
|
93
|
+
surf gemini "hello" --model gemini-3.5-flash # Models: gemini-3.1-pro (default), gemini-3.5-flash, gemini-3.1-flash-lite
|
|
72
94
|
surf gemini "wide banner" --generate-image /tmp/banner.png --aspect-ratio 16:9
|
|
73
95
|
```
|
|
74
96
|
|
|
@@ -89,6 +111,8 @@ surf grok "find viral AI posts" --deep-search # DeepSearch mode
|
|
|
89
111
|
surf grok "quick question" --model fast # Models: auto, fast, expert, grok-4.20-beta
|
|
90
112
|
```
|
|
91
113
|
|
|
114
|
+
For exhaustive, multi-angle X research with categorized findings and full post-URL traceability, use the `deep-x-research` skill (`skills/deep-x-research/`) instead of a single Grok query.
|
|
115
|
+
|
|
92
116
|
**Grok Validation & Troubleshooting:**
|
|
93
117
|
```bash
|
|
94
118
|
# Validate Grok UI and check available models (no query sent)
|
|
@@ -158,6 +182,7 @@ surf tab.list
|
|
|
158
182
|
surf tab.new "https://google.com"
|
|
159
183
|
surf tab.switch 12345
|
|
160
184
|
surf tab.close 12345
|
|
185
|
+
surf tab.move 12345 --to-window 67890
|
|
161
186
|
surf tab.reload # Reload current tab
|
|
162
187
|
|
|
163
188
|
# Named tabs (aliases)
|
|
@@ -207,12 +232,13 @@ Use `window.new`, `--window-id`, `--tab-id`, and named tabs to keep parallel age
|
|
|
207
232
|
## Input Methods
|
|
208
233
|
|
|
209
234
|
```bash
|
|
210
|
-
# CDP method (real events)
|
|
235
|
+
# CDP method (real events) types at the current focus
|
|
211
236
|
surf type --text "hello"
|
|
212
237
|
surf click --x 100 --y 200
|
|
213
238
|
|
|
214
|
-
#
|
|
215
|
-
surf type
|
|
239
|
+
# Selector/ref targets use frame-aware DOM input
|
|
240
|
+
surf type "hello" --into "#input"
|
|
241
|
+
surf type "hello" --ref e5
|
|
216
242
|
|
|
217
243
|
# Keys
|
|
218
244
|
surf key Enter
|
|
@@ -233,6 +259,7 @@ surf animate-audit --selector ".thing" --duration 2000 --fps 10 # JSON animatio
|
|
|
233
259
|
surf page.read --ref e5 # Get specific element details
|
|
234
260
|
surf page.read --depth 3 # Limit tree depth
|
|
235
261
|
surf page.read --compact # Minimal output for LLM efficiency
|
|
262
|
+
surf page.read --max-bytes 2000 # Cap visible text at a UTF-8 byte boundary
|
|
236
263
|
surf page.text # Plain text content only
|
|
237
264
|
surf page.state # Modals, loading state, scroll info
|
|
238
265
|
```
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const J=new Set(["alert","alertdialog","application","article","banner","blockquote","button","caption","cell","checkbox","code","columnheader","combobox","complementary","contentinfo","definition","deletion","dialog","directory","document","emphasis","feed","figure","form","generic","grid","gridcell","group","heading","img","insertion","link","list","listbox","listitem","log","main","mark","marquee","math","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","meter","navigation","none","note","option","paragraph","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","strong","subscript","superscript","switch","tab","table","tablist","tabpanel","term","textbox","time","timer","toolbar","tooltip","tree","treegrid","treeitem"]);function Q(i){const g=i.tagName.toLowerCase();if(["button","input","select","textarea"].includes(g))return!i.disabled;if(g==="a"&&i.hasAttribute("href"))return!0;if(i.hasAttribute("tabindex")){const r=parseInt(i.getAttribute("tabindex")||"",10);return!isNaN(r)&&r>=0}return i.getAttribute("contenteditable")==="true"}function Z(i){const g=i.getAttribute("role");if(!g)return null;const r=g.split(/\s+/).filter(e=>e);for(const e of r)if(J.has(e))return e;return null}function X(i){const g=i.tagName.toLowerCase(),r=i.getAttribute("type"),e={a:o=>o.hasAttribute("href")?"link":"generic",article:"article",aside:"complementary",button:"button",datalist:"listbox",dd:"definition",details:"group",dialog:"dialog",dt:"term",fieldset:"group",figure:"figure",footer:o=>o.closest("article, aside, main, nav, section")?"generic":"contentinfo",form:o=>o.hasAttribute("aria-label")||o.hasAttribute("aria-labelledby")?"form":"generic",h1:"heading",h2:"heading",h3:"heading",h4:"heading",h5:"heading",h6:"heading",header:o=>o.closest("article, aside, main, nav, section")?"generic":"banner",hr:"separator",img:o=>o.getAttribute("alt")===""?"presentation":"img",li:"listitem",main:"main",math:"math",menu:"list",meter:"meter",nav:"navigation",ol:"list",optgroup:"group",option:"option",output:"status",p:"paragraph",progress:"progressbar",search:"search",section:o=>o.hasAttribute("aria-label")||o.hasAttribute("aria-labelledby")?"region":"generic",select:o=>{const t=o;return t.hasAttribute("multiple")||t.size&&t.size>1?"listbox":"combobox"},table:"table",tbody:"rowgroup",td:"cell",textarea:"textbox",tfoot:"rowgroup",th:"columnheader",thead:"rowgroup",time:"time",tr:"row",ul:"list"};if(g==="input")return{button:"button",checkbox:"checkbox",email:"textbox",file:"button",image:"button",number:"spinbutton",radio:"radio",range:"slider",reset:"button",search:"searchbox",submit:"button",tel:"textbox",text:"textbox",url:"textbox"}[r||""]||"textbox";const c=e[g];return typeof c=="function"?c(i):c||"generic"}function U(i){const g=Z(i);return!g||(g==="none"||g==="presentation")&&Q(i)?X(i):g}window.__piElementMap||(window.__piElementMap={});let ee=0;function q(i,g,r){const e=i._piRef;if(e&&e.role===g&&e.name===r)return e.ref;const c=`e${++ee}`;return i._piRef={role:g,name:r,ref:c},c}function Y(){const i=[];return document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]').forEach(r=>{const e=window.getComputedStyle(r);if(!(e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"&&r.offsetWidth>0&&r.offsetHeight>0))return;const o=r.getAttribute("role")||"dialog";let t=r.getAttribute("aria-label")||r.querySelector('[role="heading"], h1, h2, h3')?.textContent?.trim()||"Dialog";t.length>100&&(t=t.substring(0,100)+"..."),i.push({type:o,description:`${o}: ${t}`,clearedBy:"computer(action=key, text=Escape)"})}),i}const j={wait(i){return new Promise(g=>setTimeout(g,i))},async waitForSelector(i,g={}){const{state:r="visible",timeout:e=2e4}=g,c=t=>{if(!t)return!1;const l=window.getComputedStyle(t);return l.display!=="none"&&l.visibility!=="hidden"&&l.opacity!=="0"&&t.offsetWidth>0&&t.offsetHeight>0},o=()=>{const t=document.querySelector(i);switch(r){case"attached":return t;case"detached":return t?null:document.body;case"hidden":return t?c(t)?null:t:document.body;default:return c(t)?t:null}};return new Promise((t,l)=>{const u=o();if(u){t(r==="detached"||r==="hidden"?null:u);return}const d=new MutationObserver(()=>{const a=o();a&&(d.disconnect(),clearTimeout(w),t(r==="detached"||r==="hidden"?null:a))}),w=setTimeout(()=>{d.disconnect(),l(new Error(`Timeout waiting for "${i}" to be ${r}`))},e);d.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style","class","hidden"]})})},async waitForText(i,g={}){const{selector:r,timeout:e=2e4}=g,c=()=>{const o=r?document.querySelector(r):document.body;if(!o)return null;const t=document.createTreeWalker(o,NodeFilter.SHOW_TEXT);for(;t.nextNode();)if(t.currentNode.textContent?.includes(i))return t.currentNode.parentElement;return null};return new Promise((o,t)=>{const l=c();if(l){o(l);return}const u=new MutationObserver(()=>{const w=c();w&&(u.disconnect(),clearTimeout(d),o(w))}),d=setTimeout(()=>{u.disconnect(),t(new Error(`Timeout waiting for text "${i}"`))},e);u.observe(document.documentElement,{childList:!0,subtree:!0,characterData:!0})})},async waitForHidden(i,g=2e4){await j.waitForSelector(i,{state:"hidden",timeout:g})},getByRole(i,g={}){const{name:r}=g,e={button:["button",'input[type="button"]','input[type="submit"]','input[type="reset"]'],link:["a[href]"],textbox:["input:not([type])",'input[type="text"]','input[type="email"]','input[type="password"]','input[type="search"]','input[type="tel"]','input[type="url"]',"textarea"],checkbox:['input[type="checkbox"]'],radio:['input[type="radio"]'],combobox:["select"],heading:["h1","h2","h3","h4","h5","h6"],list:["ul","ol"],listitem:["li"],navigation:["nav"],main:["main"],banner:["header"],contentinfo:["footer"],form:["form"],img:["img"],table:["table"]},c=[];c.push(...document.querySelectorAll(`[role="${i}"]`));const o=e[i];if(o)for(const l of o)c.push(...document.querySelectorAll(`${l}:not([role])`));if(!r)return c[0]||null;const t=r.toLowerCase().trim();for(const l of c){const u=l.getAttribute("aria-label")?.toLowerCase().trim(),d=l.textContent?.toLowerCase().trim(),w=l.getAttribute("title")?.toLowerCase().trim(),a=l.getAttribute("placeholder")?.toLowerCase().trim();if(u===t||d===t||w===t||a===t||u?.includes(t)||d?.includes(t))return l}return null}};window.__piHelpers||(window.__piHelpers=j,window.piHelpers=j);function I(){return window.__piElementMap}function B(i="interactive",g=15,r,e=!1,c=!1){try{let o=function(n){return U(n)},t=function(n){const h=n.tagName.toLowerCase(),_=n.getAttribute("aria-labelledby");if(_){const y=_.split(/\s+/).map(E=>document.getElementById(E)?.textContent?.trim()||"").filter(Boolean);if(y.length){const E=y.join(" ");return E.length>100?E.substring(0,100)+"...":E}}if(h==="select"){const y=n,E=y.querySelector("option[selected]")||(y.selectedIndex>=0?y.options[y.selectedIndex]:null);if(E?.textContent?.trim())return E.textContent.trim()}const L=n.getAttribute("aria-label");if(L?.trim())return L.trim();const N=n.getAttribute("placeholder");if(N?.trim())return N.trim();const D=n.getAttribute("title");if(D?.trim())return D.trim();const k=n.getAttribute("alt");if(k?.trim())return k.trim();if(n.id){const y=document.querySelector(`label[for="${n.id}"]`);if(y?.textContent?.trim())return y.textContent.trim()}if(h==="input"){const y=n,E=n.getAttribute("type")||"",H=n.getAttribute("value");if(E==="submit"&&H?.trim())return H.trim();if(y.value&&y.value.length<50&&y.value.trim())return y.value.trim()}if(["button","a","summary"].includes(h)){let y="";for(const E of n.childNodes)E.nodeType===Node.TEXT_NODE&&(y+=E.textContent);if(y.trim())return y.trim()}if(/^h[1-6]$/.test(h)){const y=n.textContent;if(y?.trim()){const E=y.trim();return E.length>100?E.substring(0,100)+"...":E}}if(h==="img")return"";let S="";for(const y of n.childNodes)y.nodeType===Node.TEXT_NODE&&(S+=y.textContent);if(S?.trim()&&S.trim().length>=3){const y=S.trim();return y.length>100?y.substring(0,100)+"...":y}return""},l=function(n){const h={},_=n.getAttribute("aria-checked");_==="true"?h.checked=!0:_==="false"?h.checked=!1:_==="mixed"?h.checked="mixed":n instanceof HTMLInputElement&&(n.type==="checkbox"||n.type==="radio")&&(n.type==="checkbox"&&n.indeterminate?h.checked="mixed":h.checked=n.checked);const L=n instanceof HTMLButtonElement||n instanceof HTMLInputElement||n instanceof HTMLSelectElement||n instanceof HTMLTextAreaElement;(n.getAttribute("aria-disabled")==="true"||L&&n.disabled||n.closest("fieldset:disabled"))&&(h.disabled=!0);const N=n.getAttribute("aria-expanded");N==="true"?h.expanded=!0:N==="false"&&(h.expanded=!1);const D=n.getAttribute("aria-pressed");D==="true"?h.pressed=!0:D==="false"?h.pressed=!1:D==="mixed"&&(h.pressed="mixed");const k=n.getAttribute("aria-selected");k==="true"?h.selected=!0:k==="false"&&(h.selected=!1);const S=n.getAttribute("aria-current");S&&S!=="false"&&(h.active=!0);const y=n.tagName.toLowerCase();if(/^h[1-6]$/.test(y))h.level=parseInt(y[1],10);else{const E=n.getAttribute("aria-level");E&&(h.level=parseInt(E,10))}return h},u=function(n){const h=[];return n.checked!==void 0&&h.push(n.checked==="mixed"?"[checked=mixed]":n.checked?"[checked]":"[unchecked]"),n.disabled&&h.push("[disabled]"),n.expanded!==void 0&&h.push(n.expanded?"[expanded]":"[collapsed]"),n.pressed!==void 0&&h.push(n.pressed==="mixed"?"[pressed=mixed]":n.pressed?"[pressed]":"[not-pressed]"),n.selected!==void 0&&h.push(n.selected?"[selected]":"[not-selected]"),n.active&&h.push("[active]"),n.level!==void 0&&h.push(`[level=${n.level}]`),h.join(" ")},d=function(n){const h=window.getComputedStyle(n);return h.display!=="none"&&h.visibility!=="hidden"&&h.opacity!=="0"&&n.offsetWidth>0&&n.offsetHeight>0},w=function(n){const h=n.tagName.toLowerCase();return["a","button","input","select","textarea","details","summary"].includes(h)||n.hasAttribute("onclick")||n.hasAttribute("tabindex")||n.getAttribute("role")==="button"||n.getAttribute("role")==="link"||n.getAttribute("contenteditable")==="true"},a=function(n){const h=n.tagName.toLowerCase();return["h1","h2","h3","h4","h5","h6","nav","main","header","footer","section","article","aside"].includes(h)||n.hasAttribute("role")},f=function(n){return window.getComputedStyle(n).cursor==="pointer"},m=function(n,h){const _=n.tagName.toLowerCase();if(["script","style","meta","link","title","noscript"].includes(_)||h.filter!=="all"&&n.getAttribute("aria-hidden")==="true"||h.filter!=="all"&&!d(n))return!1;if(h.filter!=="all"&&!h.refId){const N=n.getBoundingClientRect();if(!(N.top<window.innerHeight&&N.bottom>0&&N.left<window.innerWidth&&N.right>0))return!1}if(h.filter==="interactive")return w(n);if(w(n)||a(n)||t(n).length>0)return!0;const L=o(n);return h.compact&&new Set(["generic","group","region","article","section","complementary"]).has(L)&&t(n).length===0?!1:L!=="generic"&&L!=="img"},b=function(n,h){const _=[],L={filter:i,refId:r||null,compact:c},N=I(),D=m(n,L)||r&&h===0;if(D){const k=o(n),S=t(n),y=l(n),E=q(n,k,S);window.__piRefs[E]=n,N[E]={element:new WeakRef(n),role:k,name:S};let W=`${" ".repeat(h)}${k}`;if(S){const K=S.replace(/\s+/g," ").replace(/"/g,'\\"');W+=` "${K}"`}W+=` [${E}]`;const R=u(y);R&&(W+=` ${R}`),f(n)&&(W+=" [cursor=pointer]");const P=n.getAttribute("href");P&&(W+=` href="${P}"`);const G=n.getAttribute("type");G&&(W+=` type="${G}"`);const V=n.getAttribute("placeholder");V&&(W+=` placeholder="${V}"`),_.push(W)}if(h<g)for(const k of n.children)_.push(...b(k,D?h+1:h));return _},s=function(n){return n.replace(/\[e\d+\]/g,"[REF]")},p=function(n){const h=new Map;for(const _ of n){if(!_.trim())continue;const L=s(_);h.set(L,(h.get(L)||0)+1)}return h},T=function(n,h){const _=n.split(`
|
|
2
|
-
`),L=h.split(`
|
|
3
|
-
`),N=p(_),D=p(L),k=[],S=[];for(const H of L){if(!H.trim())continue;const W=s(H),R=N.get(W)||0;(D.get(W)||0)>R&&(k.push(H),N.set(W,R+1))}const y=p(_);for(const H of _){if(!H.trim())continue;const W=s(H),R=y.get(W)||0,P=D.get(W)||0;R>P&&(S.push(H),y.set(W,R-1))}if(k.length===0&&S.length===0)return{diff:"[NO CHANGES]",hasChanges:!1};const E=[];return S.length>0&&E.push(...S.map(H=>`- ${H}`)),k.length>0&&E.push(...k.map(H=>`+ ${H}`)),{diff:E.join(`
|
|
4
|
-
`),hasChanges:!0}};window.__piRefs={};const A=I();let C=null;if(r){const n=A[r];if(!n)return{error:`Element with ref_id '${r}' not found. Use read_page without ref_id to get current elements.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};const h=n.element.deref();if(!h)return delete A[r],{error:`Element with ref_id '${r}' no longer exists. Use read_page without ref_id to get current elements.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};C=h}else C=document.body;const O=C?b(C,0):[];for(const n of Object.keys(A))A[n].element.deref()||delete A[n];const $=O.join(`
|
|
5
|
-
`);if($.length>5e4)return{error:`Output exceeds 50000 character limit (${$.length} characters). Try using filter="interactive" or specify a ref_id.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};const M=Y();let x,v=!1;const F=window.__piLastSnapshot;return!e&&!r&&F&&Date.now()-F.timestamp<5e3&&(x=T(F.content,$).diff,v=!0),window.__piLastSnapshot={content:$,timestamp:Date.now()},{pageContent:$+`
|
|
6
|
-
|
|
7
|
-
[Viewport: ${window.innerWidth}x${window.innerHeight}]`,diff:v?x:void 0,viewport:{width:window.innerWidth,height:window.innerHeight},modalStates:M.length>0?M:void 0,modalLimitations:"Only custom modals ([role=dialog]) detected. Native alert/confirm/prompt dialogs and system file choosers cannot be detected from content scripts.",isIncremental:v}}catch(o){return{error:`Error generating accessibility tree: ${o instanceof Error?o.message:"Unknown error"}`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}}}}function z(i){return i.length?/[\n\r]/.test(i)||/^[\s]/.test(i)||/[\s]$/.test(i)||/[:"{}[\]]/.test(i)?'"'+i.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r")+'"':i:'""'}function te(i="interactive",g=15){try{let r=function(s){return U(s)},e=function(s){const p=s.tagName.toLowerCase(),T=s.getAttribute("aria-labelledby");if(T){const x=T.split(/\s+/).map(v=>document.getElementById(v)?.textContent?.trim()||"").filter(Boolean);if(x.length){const v=x.join(" ");return v.length>100?v.substring(0,100)+"...":v}}if(p==="select"){const x=s,v=x.querySelector("option[selected]")||(x.selectedIndex>=0?x.options[x.selectedIndex]:null);if(v?.textContent?.trim())return v.textContent.trim()}const A=s.getAttribute("aria-label");if(A?.trim())return A.trim();const C=s.getAttribute("placeholder");if(C?.trim())return C.trim();const O=s.getAttribute("title");if(O?.trim())return O.trim();const $=s.getAttribute("alt");if($?.trim())return $.trim();if(s.id){const x=document.querySelector(`label[for="${s.id}"]`);if(x?.textContent?.trim())return x.textContent.trim()}if(p==="input"){const x=s,v=s.getAttribute("type")||"",F=s.getAttribute("value");if(v==="submit"&&F?.trim())return F.trim();if(x.value&&x.value.length<50&&x.value.trim())return x.value.trim()}if(["button","a","summary"].includes(p)){let x="";for(const v of s.childNodes)v.nodeType===Node.TEXT_NODE&&(x+=v.textContent);if(x.trim())return x.trim()}if(/^h[1-6]$/.test(p)){const x=s.textContent;if(x?.trim()){const v=x.trim();return v.length>100?v.substring(0,100)+"...":v}}if(p==="img")return"";let M="";for(const x of s.childNodes)x.nodeType===Node.TEXT_NODE&&(M+=x.textContent);if(M?.trim()&&M.trim().length>=3){const x=M.trim();return x.length>100?x.substring(0,100)+"...":x}return""},c=function(s){const p={},T=s.getAttribute("aria-checked");T==="true"?p.checked=!0:T==="false"?p.checked=!1:T==="mixed"?p.checked="mixed":s instanceof HTMLInputElement&&(s.type==="checkbox"||s.type==="radio")&&(s.type==="checkbox"&&s.indeterminate?p.checked="mixed":p.checked=s.checked);const A=s instanceof HTMLButtonElement||s instanceof HTMLInputElement||s instanceof HTMLSelectElement||s instanceof HTMLTextAreaElement;(s.getAttribute("aria-disabled")==="true"||A&&s.disabled||s.closest("fieldset:disabled"))&&(p.disabled=!0);const C=s.getAttribute("aria-expanded");C==="true"?p.expanded=!0:C==="false"&&(p.expanded=!1);const O=s.getAttribute("aria-pressed");O==="true"?p.pressed=!0:O==="false"?p.pressed=!1:O==="mixed"&&(p.pressed="mixed");const $=s.getAttribute("aria-selected");$==="true"?p.selected=!0:$==="false"&&(p.selected=!1);const M=s.getAttribute("aria-current");M&&M!=="false"&&(p.active=!0);const x=s.tagName.toLowerCase();if(/^h[1-6]$/.test(x))p.level=parseInt(x[1],10);else{const v=s.getAttribute("aria-level");v&&(p.level=parseInt(v,10))}return p},o=function(s){const p=[];return s.checked!==void 0&&p.push(s.checked==="mixed"?"[checked=mixed]":s.checked?"[checked]":"[unchecked]"),s.disabled&&p.push("[disabled]"),s.expanded!==void 0&&p.push(s.expanded?"[expanded]":"[collapsed]"),s.pressed!==void 0&&p.push(s.pressed==="mixed"?"[pressed=mixed]":s.pressed?"[pressed]":"[not-pressed]"),s.selected!==void 0&&p.push(s.selected?"[selected]":"[not-selected]"),s.active&&p.push("[active]"),s.level!==void 0&&p.push(`[level=${s.level}]`),p.join(" ")},t=function(s){const p=window.getComputedStyle(s);return p.display!=="none"&&p.visibility!=="hidden"&&p.opacity!=="0"&&s.offsetWidth>0&&s.offsetHeight>0},l=function(s){const p=s.tagName.toLowerCase();return["a","button","input","select","textarea","details","summary"].includes(p)||s.hasAttribute("onclick")||s.hasAttribute("tabindex")||s.getAttribute("role")==="button"||s.getAttribute("role")==="link"||s.getAttribute("contenteditable")==="true"},u=function(s){const p=s.tagName.toLowerCase();return["h1","h2","h3","h4","h5","h6","nav","main","header","footer","section","article","aside"].includes(p)||s.hasAttribute("role")},d=function(s){return window.getComputedStyle(s).cursor==="pointer"},w=function(s,p,T,A){let C=s;p&&(C+=" "+z(p));const O=q(T,s,p);window.__piRefs[O]=T,C+=` [ref=${O}]`;const $=o(A);return $&&(C+=` ${$}`),d(T)&&(C+=" [cursor=pointer]"),C},a=function(s){const p={},T=s.getAttribute("href");T&&(p.url=T);const A=s.getAttribute("placeholder");return A&&(p.placeholder=A),p},f=function(s,p,T){if(p>g)return;const A=s.tagName.toLowerCase();if(["script","style","meta","link","title","noscript"].includes(A)||i!=="all"&&s.getAttribute("aria-hidden")==="true"||i!=="all"&&!t(s))return;if(i!=="all"){const n=s.getBoundingClientRect();if(!(n.top<window.innerHeight&&n.bottom>0&&n.left<window.innerWidth&&n.right>0))return}const C=r(s),O=e(s),$=c(s),M=l(s),x=u(s),v=O.length>0;let F;if(i==="interactive"?F=M:i==="all"?F=!0:F=M||x||v||C!=="generic"&&C!=="img",F){const n=" ".repeat(p),h=w(C,O,s,$),_=a(s),L=[];for(const k of s.children)L.push(k);const N=L.length>0,D=Object.keys(_).length>0;if(!N&&!D)m.push(`${n}- ${h}`);else{m.push(`${n}- ${h}:`);for(const[k,S]of Object.entries(_))m.push(`${n} - /${k}: ${z(S)}`);for(const k of L)f(k,p+1,!0)}}else for(const n of s.children)f(n,p,T)};window.__piRefs={};const m=[];f(document.body,0,!1);const b=m.join(`
|
|
8
|
-
`);return b.length>5e4?{error:`Output exceeds 50000 character limit (${b.length} characters). Try using filter="interactive".`,yaml:"",viewport:{width:window.innerWidth,height:window.innerHeight}}:{yaml:b+`
|
|
9
|
-
|
|
10
|
-
[Viewport: ${window.innerWidth}x${window.innerHeight}]`,viewport:{width:window.innerWidth,height:window.innerHeight}}}catch(r){return{error:`Error generating YAML tree: ${r instanceof Error?r.message:"Unknown error"}`,yaml:"",viewport:{width:window.innerWidth,height:window.innerHeight}}}}function re(i){const g=I(),r=g[i];let e;if(r&&(e=r.element.deref(),e||delete g[i]),!e&&window.__piRefs&&(e=window.__piRefs[i]),!e)return{x:0,y:0,error:`Element ${i} not found. Use read_page to get current elements.`};const c=e.getBoundingClientRect(),o=Math.round(c.left+c.width/2),t=Math.round(c.top+c.height/2);return{x:o,y:t}}function ne(i,g){const r=I(),e=r[i];let c;if(e&&(c=e.element.deref(),c||delete r[i]),!c&&window.__piRefs&&(c=window.__piRefs[i]),!c)return{success:!1,error:`Element ${i} not found. Use read_page to get current elements.`};const o=c.tagName.toLowerCase();try{if(o==="input"){const t=c,l=t.type.toLowerCase();l==="checkbox"||l==="radio"?(t.checked=!!g,t.dispatchEvent(new Event("change",{bubbles:!0}))):(t.value=String(g),t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0})))}else if(o==="textarea"){const t=c;t.value=String(g),t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0}))}else if(o==="select"){const t=c,l=String(g);let u=!1;for(const d of t.options)if(d.value===l||d.textContent?.trim()===l){t.value=d.value,u=!0;break}if(!u)return{success:!1,error:`Option "${g}" not found in select element ${i}`};t.dispatchEvent(new Event("change",{bubbles:!0}))}else if(c.getAttribute("contenteditable")==="true")c.textContent=String(g),c.dispatchEvent(new Event("input",{bubbles:!0}));else return{success:!1,error:`Element ${i} (${o}) is not a form field`};return{success:!0}}catch(t){return{success:!1,error:`Failed to set value: ${t instanceof Error?t.message:"Unknown error"}`}}}function ie(){try{const i=document.querySelector("article"),g=document.querySelector("main");return{text:(i||g||document.body).textContent?.replace(/\s+/g," ").trim().substring(0,5e4)||"",title:document.title,url:window.location.href}}catch(i){return{text:"",title:"",url:"",error:`Failed to extract text: ${i instanceof Error?i.message:"Unknown error"}`}}}function oe(i){const g=I(),r=g[i];let e;return r&&(e=r.element.deref(),e||delete g[i]),!e&&window.__piRefs&&(e=window.__piRefs[i]),e?(e.scrollIntoView({behavior:"smooth",block:"center"}),{success:!0}):{success:!1,error:`Element ${i} not found. Run read_page to get current element refs.`}}function se(i,g,r,e="screenshot.png"){try{const c=atob(i),o=new ArrayBuffer(c.length),t=new Uint8Array(o);for(let f=0;f<c.length;f++)t[f]=c.charCodeAt(f);const l=new Blob([o],{type:"image/png"}),u=new File([l],e,{type:"image/png"});let d=null;if(g){const f=I(),m=f[g];if(m&&(d=m.element.deref(),d||delete f[g]),!d&&window.__piRefs&&(d=window.__piRefs[g]),!d)return{success:!1,error:`Element ${g} not found. Run read_page to get current element refs.`}}else if(r&&(d=document.elementFromPoint(r[0],r[1]),!d))return{success:!1,error:`No element at (${r[0]}, ${r[1]})`};if(!d)return{success:!1,error:"No target element"};if(d.tagName==="INPUT"&&d.type==="file"){const f=d,m=new DataTransfer;return m.items.add(u),f.files=m.files,f.dispatchEvent(new Event("change",{bubbles:!0})),{success:!0}}const w=new DataTransfer;w.items.add(u);const a=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:w});return d.dispatchEvent(a),{success:!0}}catch(c){return{success:!1,error:c instanceof Error?c.message:"Upload failed"}}}chrome.runtime.onMessage.addListener((i,g,r)=>{switch(i.type){case"GENERATE_ACCESSIBILITY_TREE":{const e=i.options||{};if(e.format==="yaml"){const c=te(e.filter||"interactive",e.depth??15),o=Y();c.error?r({error:c.error,pageContent:"",viewport:c.viewport}):r({pageContent:c.yaml,viewport:c.viewport,modalStates:o.length>0?o:void 0,modalLimitations:"Only custom modals ([role=dialog]) detected. Native alert/confirm/prompt dialogs and system file choosers cannot be detected from content scripts."})}else{const c=B(e.filter||"interactive",e.depth??15,e.refId,e.forceFullSnapshot??!1,e.compact??!1);r(c)}break}case"GET_ELEMENT_COORDINATES":{const e=re(i.ref);r(e);break}case"CLICK_ELEMENT":{const e=I(),c=e[i.ref];let o;if(c&&(o=c.element.deref(),o||delete e[i.ref]),!o&&window.__piRefs&&(o=window.__piRefs[i.ref]),!o){r({error:`Element ${i.ref} not found. Use read_page to get current elements.`});break}if(i.button==="triple"){const t=new MouseEvent("click",{bubbles:!0,cancelable:!0,view:window,detail:3});o.dispatchEvent(t)}else i.button==="double"?o.dispatchEvent(new MouseEvent("dblclick",{bubbles:!0,cancelable:!0,view:window})):i.button==="right"?o.dispatchEvent(new MouseEvent("contextmenu",{bubbles:!0,cancelable:!0,view:window})):o.click();r({success:!0});break}case"FORM_INPUT":{const e=ne(i.ref,i.value);r(e);break}case"EVAL_IN_PAGE":{try{const e=document.createElement("script");e.textContent=`(function() { ${i.code} })();`,document.documentElement.appendChild(e),e.remove(),r({success:!0})}catch(e){r({success:!1,error:e instanceof Error?e.message:String(e)})}break}case"GET_PAGE_TEXT":{const e=ie();r(e);break}case"GET_FRAME_BY_SELECTOR":{try{const e=document.querySelector(i.selector);if(!e||e.tagName.toLowerCase()!=="iframe"){r({error:`No iframe found with selector "${i.selector}"`});break}r({url:e.src,name:e.name||void 0})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"GET_FRAME_NAME":{try{r({name:window.name||null})}catch{r({name:null})}break}case"LOCATE_ROLE":{try{const{role:e,name:c,all:o}=i,t=I(),u={button:["button",'input[type="button"]','input[type="submit"]','input[type="reset"]','[role="button"]'],link:["a[href]",'[role="link"]'],textbox:["input:not([type])",'input[type="text"]','input[type="email"]','input[type="password"]','input[type="search"]','input[type="tel"]','input[type="url"]',"textarea",'[role="textbox"]'],checkbox:['input[type="checkbox"]','[role="checkbox"]'],radio:['input[type="radio"]','[role="radio"]'],combobox:["select",'[role="combobox"]'],listbox:['[role="listbox"]',"select[multiple]"],option:["option",'[role="option"]'],heading:["h1","h2","h3","h4","h5","h6",'[role="heading"]'],navigation:["nav",'[role="navigation"]'],main:["main",'[role="main"]'],img:["img[alt]",'[role="img"]'],dialog:["dialog",'[role="dialog"]','[role="alertdialog"]'],tab:['[role="tab"]'],tabpanel:['[role="tabpanel"]'],menu:['[role="menu"]'],menuitem:['[role="menuitem"]']}[e]||[`[role="${e}"]`],d=[];for(const m of u)try{d.push(...document.querySelectorAll(m))}catch{}const w=d.filter(m=>{const b=window.getComputedStyle(m);return b.display!=="none"&&b.visibility!=="hidden"&&m.offsetWidth>0&&m.offsetHeight>0});let a=w;if(c){const m=c.toLowerCase();a=w.filter(b=>{const s=b.getAttribute("aria-label")?.toLowerCase(),p=b.textContent?.trim().toLowerCase(),T=b.getAttribute("title")?.toLowerCase(),A=b.placeholder?.toLowerCase(),C=b.value?.toLowerCase();return s?.includes(m)||p?.includes(m)||T?.includes(m)||A?.includes(m)||C?.includes(m)})}if(a.length===0){r({error:`No element found with role "${e}"${c?` and name "${c}"`:""}`});break}const f=a.map(m=>{const b=q(m,e,c||"");return window.__piRefs=window.__piRefs||{},window.__piRefs[b]=m,t[b]={element:new WeakRef(m),role:e,name:c||""},{ref:b,text:m.textContent?.trim().slice(0,50)}});r(o?{matches:f}:{ref:f[0].ref,text:f[0].text})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"LOCATE_TEXT":{try{const{text:e,exact:c}=i,o=I(),t=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),l=[];for(;t.nextNode();){const a=t.currentNode.textContent||"";if(c?a.trim()===e:a.toLowerCase().includes(e.toLowerCase())){const m=t.currentNode.parentElement;if(m&&!l.includes(m)){const b=window.getComputedStyle(m);b.display!=="none"&&b.visibility!=="hidden"&&l.push(m)}}}if(l.length===0){r({error:`No element found with text "${e}"`});break}const u=l.sort((a,f)=>(a.textContent?.length||0)-(f.textContent?.length||0))[0],d=U(u),w=q(u,d,e);window.__piRefs=window.__piRefs||{},window.__piRefs[w]=u,o[w]={element:new WeakRef(u),role:d,name:e},r({ref:w,text:u.textContent?.trim().slice(0,50)})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"LOCATE_LABEL":{try{const{label:e}=i,c=I(),o=document.querySelectorAll("label");let t=null;for(const d of o)if(d.textContent?.trim().toLowerCase()?.includes(e.toLowerCase())){const a=d.getAttribute("for");if(a&&(t=document.getElementById(a)),t||(t=d.querySelector("input, select, textarea")),t)break}if(!t){const d=e.toLowerCase();t=document.querySelector(`input[aria-label*="${e}" i], input[placeholder*="${e}" i], textarea[aria-label*="${e}" i], textarea[placeholder*="${e}" i], select[aria-label*="${e}" i]`)}if(!t){r({error:`No form field found with label "${e}"`});break}const l=U(t),u=q(t,l,e);window.__piRefs=window.__piRefs||{},window.__piRefs[u]=t,c[u]={element:new WeakRef(t),role:l,name:e},r({ref:u,label:e})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"GET_ELEMENT_STYLES":{try{const{selector:e}=i,c=I(),o=t=>{const l=getComputedStyle(t),u=t.getBoundingClientRect();return{tag:t.tagName.toLowerCase(),text:t.innerText?.trim().slice(0,80)||null,box:{x:Math.round(u.x),y:Math.round(u.y),width:Math.round(u.width),height:Math.round(u.height)},styles:{fontSize:l.fontSize,fontWeight:l.fontWeight,fontFamily:l.fontFamily.split(",")[0].trim().replace(/"/g,""),color:l.color,backgroundColor:l.backgroundColor,borderRadius:l.borderRadius,border:l.border!=="none"&&l.borderWidth!=="0px"?l.border:null,boxShadow:l.boxShadow!=="none"?l.boxShadow:null,padding:l.padding}}};if(/^e\d+$/.test(e)){const t=c[e];let l;if(t&&(l=t.element.deref(),l||delete c[e]),!l&&window.__piRefs&&(l=window.__piRefs[e]),!l){r({error:`Element ${e} not found`});break}r({styles:[o(l)]})}else{const t=document.querySelectorAll(e);if(t.length===0){r({error:`No elements found matching "${e}"`});break}const l=Array.from(t).map(o);r({styles:l})}}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"SELECT_OPTION":{try{const{selector:e,values:c,by:o}=i,t=I();let l=null;if(/^e\d+$/.test(e)){const a=t[e];let f;if(a&&(f=a.element.deref(),f||delete t[e]),!f&&window.__piRefs&&(f=window.__piRefs[e]),!f){r({error:`Element ${e} not found`});break}if(f.tagName!=="SELECT"){r({error:`Element ${e} is not a <select>`});break}l=f}else{if(l=document.querySelector(e),!l){r({error:`No element found matching "${e}"`});break}if(l.tagName!=="SELECT"){r({error:`Element "${e}" is not a <select>`});break}}if(l.multiple)for(const a of l.options)a.selected=!1;const u=[],d=[],w=l.multiple?c:[c[0]];for(const a of w){let f=!1;for(const m of l.options){let b=!1;if(o==="index"?b=m.index===parseInt(a,10):o==="label"?b=m.text.toLowerCase().includes(a.toLowerCase()):b=m.value===a,b){m.selected=!0,u.push(m.value),f=!0;break}}f||d.push(a)}l.dispatchEvent(new Event("change",{bubbles:!0})),d.length>0?r({selected:u,warning:`Values not found: ${d.join(", ")}`}):r({selected:u})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"GET_ELEMENT_TEXT":{try{const{ref:e}=i,c=I(),o=c[e];let t;if(o&&(t=o.element.deref(),t||delete c[e]),!t&&window.__piRefs&&(t=window.__piRefs[e]),!t){r({error:`Element ${e} not found`});break}r({text:t.textContent?.trim()||""})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"SCROLL_TO_ELEMENT":{const e=oe(i.ref);r(e);break}case"UPLOAD_IMAGE":{const e=se(i.base64,i.ref,i.coordinate,i.filename);r(e);break}case"WAIT_FOR_ELEMENT":{const{selector:e,state:c="visible",timeout:o=2e4}=i,t=Math.min(o,6e4),l=a=>{if(!a)return!1;const f=window.getComputedStyle(a);return f.display!=="none"&&f.visibility!=="hidden"&&f.opacity!=="0"&&a.offsetWidth>0&&a.offsetHeight>0},u=()=>{const a=document.querySelector(e);switch(c){case"attached":return!!a;case"detached":return!a;case"hidden":return!a||!l(a);default:return l(a)}},d=Date.now();return new Promise(a=>{if(u()){a({success:!0,waited:Date.now()-d});return}const f=new MutationObserver(()=>{u()&&(f.disconnect(),clearTimeout(m),a({success:!0,waited:Date.now()-d}))}),m=setTimeout(()=>{f.disconnect(),a({success:!1,waited:Date.now()-d,error:`Timeout waiting for "${e}" to be ${c}`})},t);f.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style","class","hidden","disabled"]})}).then(a=>{if(!a.success){r({error:a.error,waited:a.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const f=B("interactive",15,void 0,!0);r({...f,waited:a.waited})}),!0}case"WAIT_FOR_URL":{const{pattern:e,timeout:c=2e4}=i,o=Math.min(c,6e4),t=d=>{if(e.includes("*")){const w=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"<<<GLOBSTAR>>>").replace(/\*/g,"[^/]*").replace(/<<<GLOBSTAR>>>/g,".*");return new RegExp(`^${w}$`).test(d)}return d.includes(e)},l=Date.now();return new Promise(d=>{if(t(window.location.href)){d({success:!0,waited:Date.now()-l});return}let w=!1;const a=()=>{w||t(window.location.href)&&(w=!0,clearInterval(f),clearTimeout(m),window.removeEventListener("popstate",a),window.removeEventListener("hashchange",a),d({success:!0,waited:Date.now()-l}))},f=setInterval(a,100),m=setTimeout(()=>{w||(w=!0,clearInterval(f),window.removeEventListener("popstate",a),window.removeEventListener("hashchange",a),d({success:!1,waited:Date.now()-l,error:`Timeout waiting for URL to match "${e}". Current: ${window.location.href}`}))},o);window.addEventListener("popstate",a),window.addEventListener("hashchange",a)}).then(d=>{if(!d.success){r({error:d.error,waited:d.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const w=B("interactive",15,void 0,!0);r({...w,waited:d.waited})}),!0}case"WAIT_FOR_DOM_STABLE":{const{stable:e=100,timeout:c=5e3}=i,o=Math.min(c,3e4),t=Date.now();return new Promise(u=>{let d=Date.now(),w=!1;const a=()=>{if(w)return;Date.now()-d>=e&&(w=!0,f.disconnect(),clearTimeout(m),clearInterval(b),u({success:!0,waited:Date.now()-t}))},f=new MutationObserver(()=>{d=Date.now()}),m=setTimeout(()=>{w||(w=!0,f.disconnect(),clearInterval(b),u({success:!1,waited:Date.now()-t,error:`Timeout: DOM did not stabilize within ${o}ms`}))},o),b=setInterval(a,Math.max(10,Math.min(50,e/2)));f.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,characterData:!0}),a()}).then(u=>{if(!u.success){r({error:u.error,waited:u.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const d=B("interactive",15,void 0,!0);r({...d,waited:u.waited})}),!0}case"FORM_FILL":{const{data:e}=i;if(!Array.isArray(e))return r({error:"data must be an array of {ref, value} pairs"}),!0;const c=I(),o=[];for(const l of e){const{ref:u,value:d}=l;if(!u){o.push({ref:u||"unknown",success:!1,error:"Missing ref"});continue}const w=c[u];if(!w){o.push({ref:u,success:!1,error:"Element not found (run page.read first)"});continue}const a=w.element.deref();if(!a){delete c[u],o.push({ref:u,success:!1,error:"Element no longer exists"});continue}try{if(a instanceof HTMLInputElement){const f=a.type.toLowerCase();if(f==="checkbox"||f==="radio"){const m=d===!0||d==="true"||d==="1"||d==="checked";a.checked=m,a.dispatchEvent(new Event("change",{bubbles:!0}))}else a.focus(),a.value=String(d),a.dispatchEvent(new Event("input",{bubbles:!0})),a.dispatchEvent(new Event("change",{bubbles:!0}));o.push({ref:u,success:!0})}else a instanceof HTMLTextAreaElement?(a.focus(),a.value=String(d),a.dispatchEvent(new Event("input",{bubbles:!0})),a.dispatchEvent(new Event("change",{bubbles:!0})),o.push({ref:u,success:!0})):a instanceof HTMLSelectElement?(a.value=String(d),a.dispatchEvent(new Event("change",{bubbles:!0})),o.push({ref:u,success:!0})):a.isContentEditable?(a.focus(),a.textContent=String(d),a.dispatchEvent(new Event("input",{bubbles:!0})),o.push({ref:u,success:!0})):o.push({ref:u,success:!1,error:"Element is not fillable"})}catch(f){o.push({ref:u,success:!1,error:f instanceof Error?f.message:String(f)})}}const t=o.filter(l=>!l.success);return r({success:t.length===0,filled:o.filter(l=>l.success).length,failed:t.length,results:o}),!0}case"GET_FILE_INPUT_SELECTOR":{const{ref:e}=i;if(!e)return r({error:"No ref provided"}),!0;const c=I(),o=c[e];if(!o)return r({error:"Element not found (run page.read first)"}),!0;const t=o.element.deref();if(!t)return delete c[e],r({error:"Element no longer exists"}),!0;if(!(t instanceof HTMLInputElement)||t.type!=="file")return r({error:"Element is not a file input"}),!0;const l=`__pi_file_${Date.now()}`;return t.setAttribute("data-pi-file-id",l),r({selector:`[data-pi-file-id="${l}"]`}),!0}case"WAIT_FOR_NETWORK_IDLE":{const{timeout:e=1e4}=i,c=Math.min(e,6e4),o=["doubleclick.net","googlesyndication.com","googletagmanager.com","google-analytics.com","facebook.net","connect.facebook.net","analytics","ads","tracking","pixel","hotjar.com","clarity.ms","mixpanel.com","segment.com","newrelic.com","nr-data.net","/tracker/","/collector/","/beacon/","/telemetry/","/log/","/events/","/track.","/metrics/"],t=["img","image","font","icon"],l=f=>o.some(m=>f.includes(m)),u=f=>{const m=f.initiatorType||"unknown";return!!(t.includes(m)||/\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|eot)(\?|$)/i.test(f.name))},d=()=>{const f=performance.now();return performance.getEntriesByType("resource").filter(b=>{if(b.responseEnd!==0||b.name.startsWith("data:")||b.name.length>500||l(b.name))return!1;const s=f-b.startTime;return!(s>1e4||u(b)&&s>3e3)})},w=Date.now();return new Promise(f=>{const m=()=>{const b=d(),s=Date.now()-w;if(b.length===0){f({success:!0,waited:s});return}if(s>=c){f({success:!1,waited:s,pendingCount:b.length});return}setTimeout(m,100)};m()}).then(f=>{if(!f.success){r({error:`Network not idle after ${f.waited}ms (${f.pendingCount} requests pending)`,waited:f.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const m=B("interactive",15,void 0,!0);r({...m,waited:f.waited})}),!0}case"SEARCH_PAGE":{const{term:e,caseSensitive:c,limit:o}=i,t=ce(e,c||!1,o||10);r({query:e,count:t.length,matches:t});break}case"GET_ELEMENT_BOUNDS_FOR_ANNOTATION":{const e=I(),c=[];for(const[o,t]of Object.entries(e)){const l=t.element.deref();if(!l)continue;const u=l.getBoundingClientRect();u.width<=0||u.height<=0||u.bottom<0||u.top>window.innerHeight||u.right<0||u.left>window.innerWidth||c.push({ref:o,tag:l.tagName.toLowerCase(),bounds:{x:u.x,y:u.y,width:u.width,height:u.height}})}r({elements:c});break}default:return!1}return!1});function ce(i,g,r){const e=[],c=g?i:i.toLowerCase(),o=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),t=I();let l=0;for(;o.nextNode()&&e.length<r;){const u=o.currentNode,d=u.textContent||"",w=g?d:d.toLowerCase();let a=0;for(;(a=w.indexOf(c,a))!==-1&&e.length<r;){const f=u.parentElement;if(!f){a++;continue}const m=document.createRange();m.setStart(u,a),m.setEnd(u,Math.min(a+i.length,d.length));const b=m.getBoundingClientRect();if(b.width===0||b.height===0){a++;continue}const s=u.textContent||"",p=Math.max(0,a-30),T=Math.min(s.length,a+i.length+30),A=s.slice(p,T).trim();let C=null;for(const[O,$]of Object.entries(t)){const M=$.element.deref();if(M&&(M===f||M.contains(f))){C=O;break}}e.push({ref:`m${++l}`,text:s.slice(a,a+i.length),context:A,bounds:{x:Math.round(b.x),y:Math.round(b.y),width:Math.round(b.width),height:Math.round(b.height)},elementRef:C}),a++}}return e}
|
|
11
|
-
//# sourceMappingURL=accessibility-tree.js.map
|