surf-cli 2.0.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/LICENSE +21 -0
- package/README.md +426 -0
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/manifest.json +70 -0
- package/dist/options/options.html +30 -0
- package/dist/options/options.js +30 -0
- package/dist/options/options.js.map +1 -0
- package/dist/service-worker/index.js +156 -0
- package/dist/service-worker/index.js.map +1 -0
- package/dist/service-worker-loader.js +1 -0
- package/native/CHANGELOG.md +136 -0
- package/native/README.md +141 -0
- package/native/chatgpt-client.cjs +455 -0
- package/native/cli.cjs +2424 -0
- package/native/config.cjs +87 -0
- package/native/device-presets.cjs +211 -0
- package/native/formatters/network.cjs +402 -0
- package/native/gemini-client.cjs +637 -0
- package/native/host-helpers.cjs +989 -0
- package/native/host-wrapper.py +15 -0
- package/native/host.cjs +1271 -0
- package/native/host.sh +2 -0
- package/native/mcp-server.cjs +511 -0
- package/native/network-store.cjs +851 -0
- package/native/perplexity-client.cjs +561 -0
- package/native/protocol.cjs +27 -0
- package/native/test-host.py +41 -0
- package/native/tests/cli-tests.sh +115 -0
- package/package.json +70 -0
- package/scripts/install-native-host.cjs +308 -0
- package/scripts/uninstall-native-host.cjs +194 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nico Bailon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Surf
|
|
2
|
+
|
|
3
|
+
The CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
surf go "https://example.com"
|
|
7
|
+
surf read
|
|
8
|
+
surf click e5
|
|
9
|
+
surf snap
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Why Surf
|
|
13
|
+
|
|
14
|
+
Browser automation for AI agents is harder than it looks. Most tools require complex setup, tie you to specific AI providers, or break on real-world pages.
|
|
15
|
+
|
|
16
|
+
Surf takes a different approach:
|
|
17
|
+
|
|
18
|
+
**Agent-Agnostic** - Pure CLI commands over Unix socket. Works with Claude Code, GPT, Gemini, Cursor, custom agents, shell scripts - anything that can run commands.
|
|
19
|
+
|
|
20
|
+
**Zero Config** - Install the extension, run commands. No MCP servers to configure, no relay processes, no subscriptions.
|
|
21
|
+
|
|
22
|
+
**Battle-Tested** - Built by reverse-engineering production browser extensions and methodically working through agent-hostile pages like Discord settings. Falls back gracefully when CDP fails.
|
|
23
|
+
|
|
24
|
+
**Smart Defaults** - Screenshots auto-resize to 1200px (saves tokens). Actions auto-capture screenshots (saves round-trips). Errors on restricted pages warn instead of fail.
|
|
25
|
+
|
|
26
|
+
**AI Without API Keys** - Query ChatGPT, Gemini, and Perplexity using your browser's logged-in session. No API keys, no rate limits, no cost.
|
|
27
|
+
|
|
28
|
+
**Network Capture** - Automatically logs all network requests while active. Filter, search, and replay API calls without manually setting up request interception.
|
|
29
|
+
|
|
30
|
+
## Comparison
|
|
31
|
+
|
|
32
|
+
| Feature | Surf | Manus | Claude Extension | DevTools MCP | dev-browser |
|
|
33
|
+
|---------|------|-------|------------------|--------------|-------------|
|
|
34
|
+
| Agent-agnostic | Yes | No (Manus only) | No (Claude only) | Partial | No (Claude skill) |
|
|
35
|
+
| Zero config | Yes | No (subscription) | No (subscription) | No (MCP setup) | No (relay server) |
|
|
36
|
+
| Local-only | Yes | No (cloud) | Partial | Yes | Partial |
|
|
37
|
+
| CLI interface | Yes | No | No | No | No |
|
|
38
|
+
| Free | Yes | No | No | Yes | Yes |
|
|
39
|
+
| AI via browser cookies | Yes | No | No | No | No |
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
### Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 1. Install globally
|
|
47
|
+
npm install -g surf-cli
|
|
48
|
+
|
|
49
|
+
# 2. Load extension in Chrome
|
|
50
|
+
# - Open chrome://extensions
|
|
51
|
+
# - Enable "Developer mode"
|
|
52
|
+
# - Click "Load unpacked"
|
|
53
|
+
# - Paste the path from: surf extension-path
|
|
54
|
+
|
|
55
|
+
# 3. Install native host (copy extension ID from chrome://extensions)
|
|
56
|
+
surf install <extension-id>
|
|
57
|
+
|
|
58
|
+
# 4. Restart Chrome and test
|
|
59
|
+
surf tab.list
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Multi-Browser Support
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
surf install <extension-id> # Chrome (default)
|
|
66
|
+
surf install <extension-id> --browser brave # Brave
|
|
67
|
+
surf install <extension-id> --browser all # All supported browsers
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Supported: `chrome`, `chromium`, `brave`, `edge`, `arc`
|
|
71
|
+
|
|
72
|
+
### Uninstall
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
surf uninstall # Chrome only
|
|
76
|
+
surf uninstall --all # All browsers + wrapper files
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Development Setup
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/nicobailon/surf-cli.git
|
|
83
|
+
cd surf-cli
|
|
84
|
+
npm install
|
|
85
|
+
npm run build
|
|
86
|
+
# Then load dist/ as unpacked extension
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
surf <command> [args] [options]
|
|
93
|
+
surf --help # Basic help
|
|
94
|
+
surf --help-full # All 50+ commands
|
|
95
|
+
surf <command> --help # Command details
|
|
96
|
+
surf --find <query> # Search commands
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Navigation
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
surf go "https://example.com"
|
|
103
|
+
surf back
|
|
104
|
+
surf forward
|
|
105
|
+
surf tab.reload --hard
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Reading Pages
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
surf read # Accessibility tree + visible text content
|
|
112
|
+
surf read --no-text # Accessibility tree only (no text)
|
|
113
|
+
surf read --depth 3 # Limit tree depth (smaller output)
|
|
114
|
+
surf read --compact # Remove empty structural elements
|
|
115
|
+
surf read --depth 3 --compact # Both (60% smaller output)
|
|
116
|
+
surf page.text # Raw text content only
|
|
117
|
+
surf page.state # Modals, loading state, scroll position
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Element refs (`e1`, `e2`, `e3`...) are stable identifiers from the accessibility tree - semantic, predictable, and resilient to DOM changes.
|
|
121
|
+
|
|
122
|
+
### Semantic Locators
|
|
123
|
+
|
|
124
|
+
Find and interact with elements by role, text, or label - no refs or selectors needed:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# By ARIA role
|
|
128
|
+
surf locate.role button --name "Submit" # Find button
|
|
129
|
+
surf locate.role button --name "Submit" --action click # Find and click
|
|
130
|
+
surf locate.role textbox --action fill --value "hello" # Find and fill
|
|
131
|
+
surf locate.role link --all # List all links
|
|
132
|
+
|
|
133
|
+
# By text content
|
|
134
|
+
surf locate.text "Sign In" --action click # Click element with text
|
|
135
|
+
surf locate.text "Accept" --exact # Exact match only
|
|
136
|
+
|
|
137
|
+
# By form label
|
|
138
|
+
surf locate.label "Email" --action fill --value "test@example.com"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Iframe Support
|
|
142
|
+
|
|
143
|
+
Work with content inside iframes:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
surf frame.list # List all frames
|
|
147
|
+
surf frame.switch --index 0 # Switch to first iframe
|
|
148
|
+
surf frame.switch --name "payment" # Switch by frame name
|
|
149
|
+
surf frame.switch --selector "#checkout-frame" # Switch by CSS selector
|
|
150
|
+
|
|
151
|
+
# Now all commands target the iframe
|
|
152
|
+
surf read # Read iframe content
|
|
153
|
+
surf click e5 # Click in iframe
|
|
154
|
+
surf locate.role button --action click
|
|
155
|
+
|
|
156
|
+
surf frame.main # Return to main page
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Interaction
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
surf click e5 # Click by element ref
|
|
163
|
+
surf click --selector ".btn" # Click by CSS selector
|
|
164
|
+
surf click 100 200 # Click by coordinates
|
|
165
|
+
surf type "hello" --submit # Type and press Enter
|
|
166
|
+
surf type "email@example.com" --ref e12 # Type into specific element
|
|
167
|
+
surf key Escape # Press key
|
|
168
|
+
surf scroll.bottom # Scroll to bottom
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Screenshots
|
|
172
|
+
|
|
173
|
+
Screenshots are optimized for AI consumption by default:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
surf screenshot --output /tmp/shot.png # Auto-resized to 1200px max
|
|
177
|
+
surf screenshot --full --output /tmp/hd.png # Full resolution
|
|
178
|
+
surf screenshot --annotate --output /tmp/labeled.png # With element labels
|
|
179
|
+
surf screenshot --fullpage --output /tmp/full.png # Entire page
|
|
180
|
+
surf snap # Quick save to /tmp
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Actions like `click`, `type`, and `scroll` automatically capture a screenshot after execution - no extra command needed.
|
|
184
|
+
|
|
185
|
+
### Tabs
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
surf tab.list
|
|
189
|
+
surf tab.new "https://example.com"
|
|
190
|
+
surf tab.switch 123
|
|
191
|
+
surf tab.close 123
|
|
192
|
+
surf tab.name "dashboard" # Name current tab
|
|
193
|
+
surf tab.switch "dashboard" # Switch by name
|
|
194
|
+
surf tab.group --name "Work" --color blue
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Window Isolation
|
|
198
|
+
|
|
199
|
+
Keep using your browser while the agent works in a separate window:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Create isolated window for agent
|
|
203
|
+
surf window.new "https://example.com"
|
|
204
|
+
# Returns: Window 123456 (tab 789)
|
|
205
|
+
|
|
206
|
+
# All subsequent commands target that window
|
|
207
|
+
surf click e5 --window-id 123456
|
|
208
|
+
surf read --window-id 123456
|
|
209
|
+
surf tab.new "https://other.com" --window-id 123456
|
|
210
|
+
|
|
211
|
+
# Or manage windows directly
|
|
212
|
+
surf window.list # List all windows
|
|
213
|
+
surf window.list --tabs # Include tab details
|
|
214
|
+
surf window.focus 123456 # Bring window to front
|
|
215
|
+
surf window.close 123456 # Close window
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Device Emulation
|
|
219
|
+
|
|
220
|
+
Test responsive designs and mobile layouts:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
surf emulate.device --list # Show available devices
|
|
224
|
+
surf emulate.device "iPhone 14" # Emulate iPhone 14
|
|
225
|
+
surf emulate.device "Pixel 7" # Emulate Pixel 7
|
|
226
|
+
surf emulate.device reset # Return to desktop
|
|
227
|
+
|
|
228
|
+
# Custom viewport
|
|
229
|
+
surf emulate.viewport --width 375 --height 812
|
|
230
|
+
surf emulate.viewport --width 1920 --height 1080 --scale 2
|
|
231
|
+
|
|
232
|
+
# Touch emulation
|
|
233
|
+
surf emulate.touch # Enable touch
|
|
234
|
+
surf emulate.touch --enabled false # Disable touch
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Available devices: iPhone 12-14 (Pro/Max), iPhone SE, iPad (Pro/Mini), Pixel 5-7 (Pro), Galaxy S21-S23, Galaxy Tab S7, Nest Hub (Max).
|
|
238
|
+
|
|
239
|
+
### Performance Tracing
|
|
240
|
+
|
|
241
|
+
Capture performance metrics and traces:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
surf perf.metrics # Current performance metrics
|
|
245
|
+
surf perf.start # Start tracing
|
|
246
|
+
surf perf.stop # Stop and get trace data
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### AI Queries (No API Keys)
|
|
250
|
+
|
|
251
|
+
Query AI models using your browser's logged-in session:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# ChatGPT
|
|
255
|
+
surf chatgpt "explain this code"
|
|
256
|
+
surf chatgpt "summarize" --with-page # Include page context
|
|
257
|
+
surf chatgpt "analyze" --model gpt-4o # Specify model
|
|
258
|
+
surf chatgpt "review" --file code.ts # Attach file
|
|
259
|
+
|
|
260
|
+
# Gemini
|
|
261
|
+
surf gemini "explain quantum computing"
|
|
262
|
+
surf gemini "summarize" --with-page # Include page context
|
|
263
|
+
surf gemini "analyze" --file data.csv # Attach file
|
|
264
|
+
surf gemini "a robot surfing" --generate-image /tmp/robot.png # Generate image
|
|
265
|
+
surf gemini "add sunglasses" --edit-image photo.jpg --output out.jpg
|
|
266
|
+
surf gemini "summarize" --youtube "https://youtube.com/..." # YouTube analysis
|
|
267
|
+
surf gemini "hello" --model gemini-2.5-flash # Model selection
|
|
268
|
+
|
|
269
|
+
# Perplexity
|
|
270
|
+
surf perplexity "what is quantum computing"
|
|
271
|
+
surf perplexity "explain this page" --with-page # Include page context
|
|
272
|
+
surf perplexity "deep dive" --mode research # Research mode (Pro)
|
|
273
|
+
surf perplexity "latest news" --model sonar # Model selection (Pro)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Requires being logged into chatgpt.com, gemini.google.com, or perplexity.ai in Chrome.
|
|
277
|
+
|
|
278
|
+
### Waiting
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
surf wait 2 # Wait 2 seconds
|
|
282
|
+
surf wait.element ".loaded" # Wait for element
|
|
283
|
+
surf wait.network # Wait for network idle
|
|
284
|
+
surf wait.url "/dashboard" # Wait for URL pattern
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Other
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
surf js "return document.title" # Execute JavaScript
|
|
291
|
+
surf search "login" # Find text in page
|
|
292
|
+
surf cookie.list # List cookies
|
|
293
|
+
surf zoom 1.5 # Set zoom to 150%
|
|
294
|
+
surf console # Read console messages
|
|
295
|
+
surf network # Read network requests
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Network Capture
|
|
299
|
+
|
|
300
|
+
Surf automatically captures all network requests while active. No explicit start needed.
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# Overview (token-efficient for LLMs)
|
|
304
|
+
surf network # Recent requests, compact table
|
|
305
|
+
surf network --urls # Just URLs (minimal output)
|
|
306
|
+
surf network --format curl # As curl commands
|
|
307
|
+
|
|
308
|
+
# Filtering
|
|
309
|
+
surf network --origin api.github.com # Filter by origin/domain
|
|
310
|
+
surf network --method POST # Only POST requests
|
|
311
|
+
surf network --type json # Only JSON responses
|
|
312
|
+
surf network --status 4xx,5xx # Only errors
|
|
313
|
+
surf network --since 5m # Last 5 minutes
|
|
314
|
+
surf network --exclude-static # Skip images/fonts/css/js
|
|
315
|
+
|
|
316
|
+
# Drill down
|
|
317
|
+
surf network.get r_001 # Full request/response details
|
|
318
|
+
surf network.body r_001 # Response body (for piping to jq)
|
|
319
|
+
surf network.curl r_001 # Generate curl command
|
|
320
|
+
surf network.origins # List captured domains
|
|
321
|
+
|
|
322
|
+
# Management
|
|
323
|
+
surf network.clear # Clear captured data
|
|
324
|
+
surf network.stats # Capture statistics
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Storage location: `/tmp/surf/` (override with `--network-path` or `SURF_NETWORK_PATH` env).
|
|
328
|
+
Auto-cleanup: 24 hours TTL, 200MB max.
|
|
329
|
+
|
|
330
|
+
## Global Options
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
--tab-id <id> # Target specific tab
|
|
334
|
+
--window-id <id> # Target specific window (isolate agent from your browsing)
|
|
335
|
+
--json # Output raw JSON
|
|
336
|
+
--soft-fail # Warn instead of error (exit 0) on restricted pages
|
|
337
|
+
--no-screenshot # Skip auto-screenshot after actions
|
|
338
|
+
--full # Full resolution screenshots (skip resize)
|
|
339
|
+
--network-path <path> # Custom path for network logs (default: /tmp/surf, or SURF_NETWORK_PATH env)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Socket API
|
|
343
|
+
|
|
344
|
+
For programmatic integration, send JSON to `/tmp/surf.sock`:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
echo '{"type":"tool_request","method":"execute_tool","params":{"tool":"tab.list","args":{}},"id":"1"}' | nc -U /tmp/surf.sock
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Command Groups
|
|
351
|
+
|
|
352
|
+
| Group | Commands |
|
|
353
|
+
|-------|----------|
|
|
354
|
+
| `window.*` | `new`, `list`, `focus`, `close`, `resize` |
|
|
355
|
+
| `tab.*` | `list`, `new`, `switch`, `close`, `name`, `unname`, `named`, `group`, `ungroup`, `groups`, `reload` |
|
|
356
|
+
| `scroll.*` | `top`, `bottom`, `to`, `info` |
|
|
357
|
+
| `page.*` | `read`, `text`, `state` |
|
|
358
|
+
| `locate.*` | `role`, `text`, `label` |
|
|
359
|
+
| `frame.*` | `list`, `switch`, `main`, `js` |
|
|
360
|
+
| `wait.*` | `element`, `network`, `url`, `dom`, `load` |
|
|
361
|
+
| `cookie.*` | `list`, `get`, `set`, `clear` |
|
|
362
|
+
| `bookmark.*` | `add`, `remove`, `list` |
|
|
363
|
+
| `history.*` | `list`, `search` |
|
|
364
|
+
| `dialog.*` | `accept`, `dismiss`, `info` |
|
|
365
|
+
| `emulate.*` | `network`, `cpu`, `geo`, `device`, `viewport`, `touch` |
|
|
366
|
+
| `perf.*` | `start`, `stop`, `metrics` |
|
|
367
|
+
| `network.*` | `get`, `body`, `curl`, `origins`, `clear`, `stats`, `export`, `path` |
|
|
368
|
+
|
|
369
|
+
## Aliases
|
|
370
|
+
|
|
371
|
+
| Alias | Command |
|
|
372
|
+
|-------|---------|
|
|
373
|
+
| `snap` | `screenshot` |
|
|
374
|
+
| `read` | `page.read` |
|
|
375
|
+
| `find` | `search` |
|
|
376
|
+
| `go` | `navigate` |
|
|
377
|
+
|
|
378
|
+
## How It Works
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
CLI (surf) → Unix Socket → Native Host → Chrome Extension → CDP/Scripting API
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Surf uses Chrome DevTools Protocol for most operations, with automatic fallback to `chrome.scripting` API when CDP is unavailable (restricted pages, certain contexts). Screenshots fall back to `captureVisibleTab` when CDP capture fails.
|
|
385
|
+
|
|
386
|
+
## Limitations
|
|
387
|
+
|
|
388
|
+
- Cannot automate `chrome://` pages or the Chrome Web Store (Chrome restriction)
|
|
389
|
+
- First CDP operation on a new tab takes ~100-500ms (debugger attachment)
|
|
390
|
+
- Some operations on restricted pages return warnings instead of results
|
|
391
|
+
|
|
392
|
+
## Linux Support (Experimental)
|
|
393
|
+
|
|
394
|
+
Surf should work on Linux with Chromium. Not yet tested in production.
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
# Install dependencies
|
|
398
|
+
sudo apt install chromium-browser nodejs npm imagemagick
|
|
399
|
+
|
|
400
|
+
# For headless server: add Xvfb + VNC
|
|
401
|
+
sudo apt install xvfb tigervnc-standalone-server
|
|
402
|
+
|
|
403
|
+
# Install Surf and native host
|
|
404
|
+
npm install -g surf-cli
|
|
405
|
+
surf install <extension-id> --browser chromium
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Notes:**
|
|
409
|
+
- Use Chromium (no official Chrome for Linux ARM64)
|
|
410
|
+
- Screenshot resize uses ImageMagick instead of macOS `sips`
|
|
411
|
+
- Headless servers need Xvfb + VNC for initial login setup
|
|
412
|
+
|
|
413
|
+
## Development
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
npm run dev # Watch mode
|
|
417
|
+
npm run build # Production build
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
After changes:
|
|
421
|
+
- **Extension** (`src/`): Reload at `chrome://extensions`
|
|
422
|
+
- **Host** (`native/`): Restart `node native/host.cjs`
|
|
423
|
+
|
|
424
|
+
## License
|
|
425
|
+
|
|
426
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
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(r){const h=r.tagName.toLowerCase();if(["button","input","select","textarea"].includes(h))return!r.disabled;if(h==="a"&&r.hasAttribute("href"))return!0;if(r.hasAttribute("tabindex")){const i=parseInt(r.getAttribute("tabindex")||"",10);return!isNaN(i)&&i>=0}return r.getAttribute("contenteditable")==="true"}function Z(r){const h=r.getAttribute("role");if(!h)return null;const i=h.split(/\s+/).filter(m=>m);for(const m of i)if(J.has(m))return m;return null}function X(r){const h=r.tagName.toLowerCase(),i=r.getAttribute("type"),m={a:p=>p.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:p=>p.closest("article, aside, main, nav, section")?"generic":"contentinfo",form:p=>p.hasAttribute("aria-label")||p.hasAttribute("aria-labelledby")?"form":"generic",h1:"heading",h2:"heading",h3:"heading",h4:"heading",h5:"heading",h6:"heading",header:p=>p.closest("article, aside, main, nav, section")?"generic":"banner",hr:"separator",img:p=>p.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:p=>p.hasAttribute("aria-label")||p.hasAttribute("aria-labelledby")?"region":"generic",select:p=>{const e=p;return e.hasAttribute("multiple")||e.size&&e.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(h==="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"}[i||""]||"textbox";const g=m[h];return typeof g=="function"?g(r):g||"generic"}function U(r){const h=Z(r);return!h||(h==="none"||h==="presentation")&&Q(r)?X(r):h}window.__piElementMap||(window.__piElementMap={});let ee=0;function q(r,h,i){const m=r._piRef;if(m&&m.role===h&&m.name===i)return m.ref;const g=`e${++ee}`;return r._piRef={role:h,name:i,ref:g},g}function Y(){const r=[];return document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]').forEach(i=>{var s,c;const m=window.getComputedStyle(i);if(!(m.display!=="none"&&m.visibility!=="hidden"&&m.opacity!=="0"&&i.offsetWidth>0&&i.offsetHeight>0))return;const p=i.getAttribute("role")||"dialog";let e=i.getAttribute("aria-label")||((c=(s=i.querySelector('[role="heading"], h1, h2, h3'))==null?void 0:s.textContent)==null?void 0:c.trim())||"Dialog";e.length>100&&(e=e.substring(0,100)+"..."),r.push({type:p,description:`${p}: ${e}`,clearedBy:"computer(action=key, text=Escape)"})}),r}const j={wait(r){return new Promise(h=>setTimeout(h,r))},async waitForSelector(r,h={}){const{state:i="visible",timeout:m=2e4}=h,g=e=>{if(!e)return!1;const s=window.getComputedStyle(e);return s.display!=="none"&&s.visibility!=="hidden"&&s.opacity!=="0"&&e.offsetWidth>0&&e.offsetHeight>0},p=()=>{const e=document.querySelector(r);switch(i){case"attached":return e;case"detached":return e?null:document.body;case"hidden":return e?g(e)?null:e:document.body;case"visible":default:return g(e)?e:null}};return new Promise((e,s)=>{const c=p();if(c){e(i==="detached"||i==="hidden"?null:c);return}const a=new MutationObserver(()=>{const u=p();u&&(a.disconnect(),clearTimeout(w),e(i==="detached"||i==="hidden"?null:u))}),w=setTimeout(()=>{a.disconnect(),s(new Error(`Timeout waiting for "${r}" to be ${i}`))},m);a.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style","class","hidden"]})})},async waitForText(r,h={}){const{selector:i,timeout:m=2e4}=h,g=()=>{var s;const p=i?document.querySelector(i):document.body;if(!p)return null;const e=document.createTreeWalker(p,NodeFilter.SHOW_TEXT);for(;e.nextNode();)if((s=e.currentNode.textContent)!=null&&s.includes(r))return e.currentNode.parentElement;return null};return new Promise((p,e)=>{const s=g();if(s){p(s);return}const c=new MutationObserver(()=>{const w=g();w&&(c.disconnect(),clearTimeout(a),p(w))}),a=setTimeout(()=>{c.disconnect(),e(new Error(`Timeout waiting for text "${r}"`))},m);c.observe(document.documentElement,{childList:!0,subtree:!0,characterData:!0})})},async waitForHidden(r,h=2e4){await j.waitForSelector(r,{state:"hidden",timeout:h})},getByRole(r,h={}){var s,c,a,w;const{name:i}=h,m={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"]},g=[];g.push(...document.querySelectorAll(`[role="${r}"]`));const p=m[r];if(p)for(const u of p)g.push(...document.querySelectorAll(`${u}:not([role])`));if(!i)return g[0]||null;const e=i.toLowerCase().trim();for(const u of g){const f=(s=u.getAttribute("aria-label"))==null?void 0:s.toLowerCase().trim(),b=(c=u.textContent)==null?void 0:c.toLowerCase().trim(),l=(a=u.getAttribute("title"))==null?void 0:a.toLowerCase().trim(),t=(w=u.getAttribute("placeholder"))==null?void 0:w.toLowerCase().trim();if(f===e||b===e||l===e||t===e||f!=null&&f.includes(e)||b!=null&&b.includes(e))return u}return null}};window.__piHelpers||(window.__piHelpers=j,window.piHelpers=j);function H(){return window.__piElementMap}function B(r="interactive",h=15,i,m=!1,g=!1){try{let p=function(n){return U(n)},e=function(n){var F,W;const d=n.tagName.toLowerCase(),_=n.getAttribute("aria-labelledby");if(_){const E=_.split(/\s+/).map(C=>{var P;const I=document.getElementById(C);return((P=I==null?void 0:I.textContent)==null?void 0:P.trim())||""}).filter(Boolean);if(E.length){const C=E.join(" ");return C.length>100?C.substring(0,100)+"...":C}}if(d==="select"){const E=n,C=E.querySelector("option[selected]")||(E.selectedIndex>=0?E.options[E.selectedIndex]:null);if((F=C==null?void 0:C.textContent)!=null&&F.trim())return C.textContent.trim()}const $=n.getAttribute("aria-label");if($!=null&&$.trim())return $.trim();const M=n.getAttribute("placeholder");if(M!=null&&M.trim())return M.trim();const O=n.getAttribute("title");if(O!=null&&O.trim())return O.trim();const T=n.getAttribute("alt");if(T!=null&&T.trim())return T.trim();if(n.id){const E=document.querySelector(`label[for="${n.id}"]`);if((W=E==null?void 0:E.textContent)!=null&&W.trim())return E.textContent.trim()}if(d==="input"){const E=n,C=n.getAttribute("type")||"",I=n.getAttribute("value");if(C==="submit"&&(I!=null&&I.trim()))return I.trim();if(E.value&&E.value.length<50&&E.value.trim())return E.value.trim()}if(["button","a","summary"].includes(d)){let E="";for(const C of n.childNodes)C.nodeType===Node.TEXT_NODE&&(E+=C.textContent);if(E.trim())return E.trim()}if(/^h[1-6]$/.test(d)){const E=n.textContent;if(E!=null&&E.trim()){const C=E.trim();return C.length>100?C.substring(0,100)+"...":C}}if(d==="img")return"";let N="";for(const E of n.childNodes)E.nodeType===Node.TEXT_NODE&&(N+=E.textContent);if(N!=null&&N.trim()&&N.trim().length>=3){const E=N.trim();return E.length>100?E.substring(0,100)+"...":E}return""},s=function(n){const d={},_=n.getAttribute("aria-checked");_==="true"?d.checked=!0:_==="false"?d.checked=!1:_==="mixed"?d.checked="mixed":n instanceof HTMLInputElement&&(n.type==="checkbox"||n.type==="radio")&&(n.type==="checkbox"&&n.indeterminate?d.checked="mixed":d.checked=n.checked);const $=n instanceof HTMLButtonElement||n instanceof HTMLInputElement||n instanceof HTMLSelectElement||n instanceof HTMLTextAreaElement;(n.getAttribute("aria-disabled")==="true"||$&&n.disabled||n.closest("fieldset:disabled"))&&(d.disabled=!0);const M=n.getAttribute("aria-expanded");M==="true"?d.expanded=!0:M==="false"&&(d.expanded=!1);const O=n.getAttribute("aria-pressed");O==="true"?d.pressed=!0:O==="false"?d.pressed=!1:O==="mixed"&&(d.pressed="mixed");const T=n.getAttribute("aria-selected");T==="true"?d.selected=!0:T==="false"&&(d.selected=!1);const N=n.getAttribute("aria-current");N&&N!=="false"&&(d.active=!0);const F=n.tagName.toLowerCase();if(/^h[1-6]$/.test(F))d.level=parseInt(F[1],10);else{const W=n.getAttribute("aria-level");W&&(d.level=parseInt(W,10))}return d},c=function(n){const d=[];return n.checked!==void 0&&d.push(n.checked==="mixed"?"[checked=mixed]":n.checked?"[checked]":"[unchecked]"),n.disabled&&d.push("[disabled]"),n.expanded!==void 0&&d.push(n.expanded?"[expanded]":"[collapsed]"),n.pressed!==void 0&&d.push(n.pressed==="mixed"?"[pressed=mixed]":n.pressed?"[pressed]":"[not-pressed]"),n.selected!==void 0&&d.push(n.selected?"[selected]":"[not-selected]"),n.active&&d.push("[active]"),n.level!==void 0&&d.push(`[level=${n.level}]`),d.join(" ")},a=function(n){const d=window.getComputedStyle(n);return d.display!=="none"&&d.visibility!=="hidden"&&d.opacity!=="0"&&n.offsetWidth>0&&n.offsetHeight>0},w=function(n){const d=n.tagName.toLowerCase();return["a","button","input","select","textarea","details","summary"].includes(d)||n.hasAttribute("onclick")||n.hasAttribute("tabindex")||n.getAttribute("role")==="button"||n.getAttribute("role")==="link"||n.getAttribute("contenteditable")==="true"},u=function(n){const d=n.tagName.toLowerCase();return["h1","h2","h3","h4","h5","h6","nav","main","header","footer","section","article","aside"].includes(d)||n.hasAttribute("role")},f=function(n){return window.getComputedStyle(n).cursor==="pointer"},b=function(n,d){const _=n.tagName.toLowerCase();if(["script","style","meta","link","title","noscript"].includes(_)||d.filter!=="all"&&n.getAttribute("aria-hidden")==="true"||d.filter!=="all"&&!a(n))return!1;if(d.filter!=="all"&&!d.refId){const M=n.getBoundingClientRect();if(!(M.top<window.innerHeight&&M.bottom>0&&M.left<window.innerWidth&&M.right>0))return!1}if(d.filter==="interactive")return w(n);if(w(n)||u(n)||e(n).length>0)return!0;const $=p(n);return d.compact&&new Set(["generic","group","region","article","section","complementary"]).has($)&&e(n).length===0?!1:$!=="generic"&&$!=="img"},l=function(n,d){const _=[],$={filter:r,refId:i||null,compact:g},M=H(),O=b(n,$)||i&&d===0;if(O){const T=p(n),N=e(n),F=s(n),W=q(n,T,N);window.__piRefs[W]=n,M[W]={element:new WeakRef(n),role:T,name:N};let C=`${" ".repeat(d)}${T}`;if(N){const K=N.replace(/\s+/g," ").replace(/"/g,'\\"');C+=` "${K}"`}C+=` [${W}]`;const I=c(F);I&&(C+=` ${I}`),f(n)&&(C+=" [cursor=pointer]");const P=n.getAttribute("href");P&&(C+=` href="${P}"`);const G=n.getAttribute("type");G&&(C+=` type="${G}"`);const V=n.getAttribute("placeholder");V&&(C+=` placeholder="${V}"`),_.push(C)}if(d<h)for(const T of n.children)_.push(...l(T,O?d+1:d));return _},t=function(n){return n.replace(/\[e\d+\]/g,"[REF]")},o=function(n){const d=new Map;for(const _ of n){if(!_.trim())continue;const $=t(_);d.set($,(d.get($)||0)+1)}return d},y=function(n,d){const _=n.split(`
|
|
2
|
+
`),$=d.split(`
|
|
3
|
+
`),M=o(_),O=o($),T=[],N=[];for(const E of $){if(!E.trim())continue;const C=t(E),I=M.get(C)||0;(O.get(C)||0)>I&&(T.push(E),M.set(C,I+1))}const F=o(_);for(const E of _){if(!E.trim())continue;const C=t(E),I=F.get(C)||0,P=O.get(C)||0;I>P&&(N.push(E),F.set(C,I-1))}if(T.length===0&&N.length===0)return{diff:"[NO CHANGES]",hasChanges:!1};const W=[];return N.length>0&&W.push(...N.map(E=>`- ${E}`)),T.length>0&&W.push(...T.map(E=>`+ ${E}`)),{diff:W.join(`
|
|
4
|
+
`),hasChanges:!0}};window.__piRefs={};const v=H();let A=null;if(i){const n=v[i];if(!n)return{error:`Element with ref_id '${i}' not found. Use read_page without ref_id to get current elements.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};const d=n.element.deref();if(!d)return delete v[i],{error:`Element with ref_id '${i}' no longer exists. Use read_page without ref_id to get current elements.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};A=d}else A=document.body;const S=A?l(A,0):[];for(const n of Object.keys(v))v[n].element.deref()||delete v[n];const k=S.join(`
|
|
5
|
+
`);if(k.length>5e4)return{error:`Output exceeds 50000 character limit (${k.length} characters). Try using filter="interactive" or specify a ref_id.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};const L=Y();let D,R=!1;const x=window.__piLastSnapshot;return!m&&!i&&x&&Date.now()-x.timestamp<5e3&&(D=y(x.content,k).diff,R=!0),window.__piLastSnapshot={content:k,timestamp:Date.now()},{pageContent:k+`
|
|
6
|
+
|
|
7
|
+
[Viewport: ${window.innerWidth}x${window.innerHeight}]`,diff:R?D:void 0,viewport:{width:window.innerWidth,height:window.innerHeight},modalStates:L.length>0?L: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:R}}catch(p){return{error:`Error generating accessibility tree: ${p instanceof Error?p.message:"Unknown error"}`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}}}}function z(r){return r.length?/[\n\r]/.test(r)||/^[\s]/.test(r)||/[\s]$/.test(r)||/[:"{}[\]]/.test(r)?'"'+r.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r")+'"':r:'""'}function te(r="interactive",h=15){try{let i=function(t){return U(t)},m=function(t){var D,R;const o=t.tagName.toLowerCase(),y=t.getAttribute("aria-labelledby");if(y){const x=y.split(/\s+/).map(n=>{var _;const d=document.getElementById(n);return((_=d==null?void 0:d.textContent)==null?void 0:_.trim())||""}).filter(Boolean);if(x.length){const n=x.join(" ");return n.length>100?n.substring(0,100)+"...":n}}if(o==="select"){const x=t,n=x.querySelector("option[selected]")||(x.selectedIndex>=0?x.options[x.selectedIndex]:null);if((D=n==null?void 0:n.textContent)!=null&&D.trim())return n.textContent.trim()}const v=t.getAttribute("aria-label");if(v!=null&&v.trim())return v.trim();const A=t.getAttribute("placeholder");if(A!=null&&A.trim())return A.trim();const S=t.getAttribute("title");if(S!=null&&S.trim())return S.trim();const k=t.getAttribute("alt");if(k!=null&&k.trim())return k.trim();if(t.id){const x=document.querySelector(`label[for="${t.id}"]`);if((R=x==null?void 0:x.textContent)!=null&&R.trim())return x.textContent.trim()}if(o==="input"){const x=t,n=t.getAttribute("type")||"",d=t.getAttribute("value");if(n==="submit"&&(d!=null&&d.trim()))return d.trim();if(x.value&&x.value.length<50&&x.value.trim())return x.value.trim()}if(["button","a","summary"].includes(o)){let x="";for(const n of t.childNodes)n.nodeType===Node.TEXT_NODE&&(x+=n.textContent);if(x.trim())return x.trim()}if(/^h[1-6]$/.test(o)){const x=t.textContent;if(x!=null&&x.trim()){const n=x.trim();return n.length>100?n.substring(0,100)+"...":n}}if(o==="img")return"";let L="";for(const x of t.childNodes)x.nodeType===Node.TEXT_NODE&&(L+=x.textContent);if(L!=null&&L.trim()&&L.trim().length>=3){const x=L.trim();return x.length>100?x.substring(0,100)+"...":x}return""},g=function(t){const o={},y=t.getAttribute("aria-checked");y==="true"?o.checked=!0:y==="false"?o.checked=!1:y==="mixed"?o.checked="mixed":t instanceof HTMLInputElement&&(t.type==="checkbox"||t.type==="radio")&&(t.type==="checkbox"&&t.indeterminate?o.checked="mixed":o.checked=t.checked);const v=t instanceof HTMLButtonElement||t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement;(t.getAttribute("aria-disabled")==="true"||v&&t.disabled||t.closest("fieldset:disabled"))&&(o.disabled=!0);const A=t.getAttribute("aria-expanded");A==="true"?o.expanded=!0:A==="false"&&(o.expanded=!1);const S=t.getAttribute("aria-pressed");S==="true"?o.pressed=!0:S==="false"?o.pressed=!1:S==="mixed"&&(o.pressed="mixed");const k=t.getAttribute("aria-selected");k==="true"?o.selected=!0:k==="false"&&(o.selected=!1);const L=t.getAttribute("aria-current");L&&L!=="false"&&(o.active=!0);const D=t.tagName.toLowerCase();if(/^h[1-6]$/.test(D))o.level=parseInt(D[1],10);else{const R=t.getAttribute("aria-level");R&&(o.level=parseInt(R,10))}return o},p=function(t){const o=[];return t.checked!==void 0&&o.push(t.checked==="mixed"?"[checked=mixed]":t.checked?"[checked]":"[unchecked]"),t.disabled&&o.push("[disabled]"),t.expanded!==void 0&&o.push(t.expanded?"[expanded]":"[collapsed]"),t.pressed!==void 0&&o.push(t.pressed==="mixed"?"[pressed=mixed]":t.pressed?"[pressed]":"[not-pressed]"),t.selected!==void 0&&o.push(t.selected?"[selected]":"[not-selected]"),t.active&&o.push("[active]"),t.level!==void 0&&o.push(`[level=${t.level}]`),o.join(" ")},e=function(t){const o=window.getComputedStyle(t);return o.display!=="none"&&o.visibility!=="hidden"&&o.opacity!=="0"&&t.offsetWidth>0&&t.offsetHeight>0},s=function(t){const o=t.tagName.toLowerCase();return["a","button","input","select","textarea","details","summary"].includes(o)||t.hasAttribute("onclick")||t.hasAttribute("tabindex")||t.getAttribute("role")==="button"||t.getAttribute("role")==="link"||t.getAttribute("contenteditable")==="true"},c=function(t){const o=t.tagName.toLowerCase();return["h1","h2","h3","h4","h5","h6","nav","main","header","footer","section","article","aside"].includes(o)||t.hasAttribute("role")},a=function(t){return window.getComputedStyle(t).cursor==="pointer"},w=function(t,o,y,v){let A=t;o&&(A+=" "+z(o));const S=q(y,t,o);window.__piRefs[S]=y,A+=` [ref=${S}]`;const k=p(v);return k&&(A+=` ${k}`),a(y)&&(A+=" [cursor=pointer]"),A},u=function(t){const o={},y=t.getAttribute("href");y&&(o.url=y);const v=t.getAttribute("placeholder");return v&&(o.placeholder=v),o},f=function(t,o,y){if(o>h)return;const v=t.tagName.toLowerCase();if(["script","style","meta","link","title","noscript"].includes(v)||r!=="all"&&t.getAttribute("aria-hidden")==="true"||r!=="all"&&!e(t))return;if(r!=="all"){const n=t.getBoundingClientRect();if(!(n.top<window.innerHeight&&n.bottom>0&&n.left<window.innerWidth&&n.right>0))return}const A=i(t),S=m(t),k=g(t),L=s(t),D=c(t),R=S.length>0;let x;if(r==="interactive"?x=L:r==="all"?x=!0:x=L||D||R||A!=="generic"&&A!=="img",x){const n=" ".repeat(o),d=w(A,S,t,k),_=u(t),$=[];for(const T of t.children)$.push(T);const M=$.length>0,O=Object.keys(_).length>0;if(!M&&!O)b.push(`${n}- ${d}`);else{b.push(`${n}- ${d}:`);for(const[T,N]of Object.entries(_))b.push(`${n} - /${T}: ${z(N)}`);for(const T of $)f(T,o+1,!0)}}else for(const n of t.children)f(n,o,y)};window.__piRefs={};const b=[];f(document.body,0,!1);const l=b.join(`
|
|
8
|
+
`);return l.length>5e4?{error:`Output exceeds 50000 character limit (${l.length} characters). Try using filter="interactive".`,yaml:"",viewport:{width:window.innerWidth,height:window.innerHeight}}:{yaml:l+`
|
|
9
|
+
|
|
10
|
+
[Viewport: ${window.innerWidth}x${window.innerHeight}]`,viewport:{width:window.innerWidth,height:window.innerHeight}}}catch(i){return{error:`Error generating YAML tree: ${i instanceof Error?i.message:"Unknown error"}`,yaml:"",viewport:{width:window.innerWidth,height:window.innerHeight}}}}function ne(r){const h=H(),i=h[r];let m;if(i&&(m=i.element.deref(),m||delete h[r]),!m&&window.__piRefs&&(m=window.__piRefs[r]),!m)return{x:0,y:0,error:`Element ${r} not found. Use read_page to get current elements.`};const g=m.getBoundingClientRect(),p=Math.round(g.left+g.width/2),e=Math.round(g.top+g.height/2);return{x:p,y:e}}function re(r,h){var e;const i=H(),m=i[r];let g;if(m&&(g=m.element.deref(),g||delete i[r]),!g&&window.__piRefs&&(g=window.__piRefs[r]),!g)return{success:!1,error:`Element ${r} not found. Use read_page to get current elements.`};const p=g.tagName.toLowerCase();try{if(p==="input"){const s=g,c=s.type.toLowerCase();c==="checkbox"||c==="radio"?(s.checked=!!h,s.dispatchEvent(new Event("change",{bubbles:!0}))):(s.value=String(h),s.dispatchEvent(new Event("input",{bubbles:!0})),s.dispatchEvent(new Event("change",{bubbles:!0})))}else if(p==="textarea"){const s=g;s.value=String(h),s.dispatchEvent(new Event("input",{bubbles:!0})),s.dispatchEvent(new Event("change",{bubbles:!0}))}else if(p==="select"){const s=g,c=String(h);let a=!1;for(const w of s.options)if(w.value===c||((e=w.textContent)==null?void 0:e.trim())===c){s.value=w.value,a=!0;break}if(!a)return{success:!1,error:`Option "${h}" not found in select element ${r}`};s.dispatchEvent(new Event("change",{bubbles:!0}))}else if(g.getAttribute("contenteditable")==="true")g.textContent=String(h),g.dispatchEvent(new Event("input",{bubbles:!0}));else return{success:!1,error:`Element ${r} (${p}) is not a form field`};return{success:!0}}catch(s){return{success:!1,error:`Failed to set value: ${s instanceof Error?s.message:"Unknown error"}`}}}function ie(){var r;try{const h=document.querySelector("article"),i=document.querySelector("main");return{text:((r=(h||i||document.body).textContent)==null?void 0:r.replace(/\s+/g," ").trim().substring(0,5e4))||"",title:document.title,url:window.location.href}}catch(h){return{text:"",title:"",url:"",error:`Failed to extract text: ${h instanceof Error?h.message:"Unknown error"}`}}}function oe(r){const h=H(),i=h[r];let m;return i&&(m=i.element.deref(),m||delete h[r]),!m&&window.__piRefs&&(m=window.__piRefs[r]),m?(m.scrollIntoView({behavior:"smooth",block:"center"}),{success:!0}):{success:!1,error:`Element ${r} not found. Run read_page to get current element refs.`}}function se(r,h,i,m="screenshot.png"){try{const g=atob(r),p=new ArrayBuffer(g.length),e=new Uint8Array(p);for(let f=0;f<g.length;f++)e[f]=g.charCodeAt(f);const s=new Blob([p],{type:"image/png"}),c=new File([s],m,{type:"image/png"});let a=null;if(h){const f=H(),b=f[h];if(b&&(a=b.element.deref(),a||delete f[h]),!a&&window.__piRefs&&(a=window.__piRefs[h]),!a)return{success:!1,error:`Element ${h} not found. Run read_page to get current element refs.`}}else if(i&&(a=document.elementFromPoint(i[0],i[1]),!a))return{success:!1,error:`No element at (${i[0]}, ${i[1]})`};if(!a)return{success:!1,error:"No target element"};if(a.tagName==="INPUT"&&a.type==="file"){const f=a,b=new DataTransfer;return b.items.add(c),f.files=b.files,f.dispatchEvent(new Event("change",{bubbles:!0})),{success:!0}}const w=new DataTransfer;w.items.add(c);const u=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:w});return a.dispatchEvent(u),{success:!0}}catch(g){return{success:!1,error:g instanceof Error?g.message:"Upload failed"}}}chrome.runtime.onMessage.addListener((r,h,i)=>{var m,g,p;switch(r.type){case"GENERATE_ACCESSIBILITY_TREE":{const e=r.options||{};if(e.format==="yaml"){const s=te(e.filter||"interactive",e.depth??15),c=Y();s.error?i({error:s.error,pageContent:"",viewport:s.viewport}):i({pageContent:s.yaml,viewport:s.viewport,modalStates:c.length>0?c: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 s=B(e.filter||"interactive",e.depth??15,e.refId,e.forceFullSnapshot??!1,e.compact??!1);i(s)}break}case"GET_ELEMENT_COORDINATES":{const e=ne(r.ref);i(e);break}case"CLICK_ELEMENT":{const e=H(),s=e[r.ref];let c;if(s&&(c=s.element.deref(),c||delete e[r.ref]),!c&&window.__piRefs&&(c=window.__piRefs[r.ref]),!c){i({error:`Element ${r.ref} not found. Use read_page to get current elements.`});break}if(r.button==="triple"){const a=new MouseEvent("click",{bubbles:!0,cancelable:!0,view:window,detail:3});c.dispatchEvent(a)}else r.button==="double"?c.dispatchEvent(new MouseEvent("dblclick",{bubbles:!0,cancelable:!0,view:window})):r.button==="right"?c.dispatchEvent(new MouseEvent("contextmenu",{bubbles:!0,cancelable:!0,view:window})):c.click();i({success:!0});break}case"FORM_INPUT":{const e=re(r.ref,r.value);i(e);break}case"EVAL_IN_PAGE":{try{const e=document.createElement("script");e.textContent=`(function() { ${r.code} })();`,document.documentElement.appendChild(e),e.remove(),i({success:!0})}catch(e){i({success:!1,error:e instanceof Error?e.message:String(e)})}break}case"GET_PAGE_TEXT":{const e=ie();i(e);break}case"GET_FRAME_BY_SELECTOR":{try{const e=document.querySelector(r.selector);if(!e||e.tagName.toLowerCase()!=="iframe"){i({error:`No iframe found with selector "${r.selector}"`});break}i({url:e.src,name:e.name||void 0})}catch(e){i({error:e instanceof Error?e.message:String(e)})}break}case"GET_FRAME_NAME":{try{i({name:window.name||null})}catch{i({name:null})}break}case"LOCATE_ROLE":{try{const{role:e,name:s,all:c}=r,a=H(),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}"]`],f=[];for(const o of u)try{f.push(...document.querySelectorAll(o))}catch{}const b=f.filter(o=>{const y=window.getComputedStyle(o);return y.display!=="none"&&y.visibility!=="hidden"&&o.offsetWidth>0&&o.offsetHeight>0});let l=b;if(s){const o=s.toLowerCase();l=b.filter(y=>{var D,R,x,n,d;const v=(D=y.getAttribute("aria-label"))==null?void 0:D.toLowerCase(),A=(R=y.textContent)==null?void 0:R.trim().toLowerCase(),S=(x=y.getAttribute("title"))==null?void 0:x.toLowerCase(),k=(n=y.placeholder)==null?void 0:n.toLowerCase(),L=(d=y.value)==null?void 0:d.toLowerCase();return(v==null?void 0:v.includes(o))||(A==null?void 0:A.includes(o))||(S==null?void 0:S.includes(o))||(k==null?void 0:k.includes(o))||(L==null?void 0:L.includes(o))})}if(l.length===0){i({error:`No element found with role "${e}"${s?` and name "${s}"`:""}`});break}const t=l.map(o=>{var v;const y=q(o,e,s||"");return window.__piRefs=window.__piRefs||{},window.__piRefs[y]=o,a[y]={element:new WeakRef(o),role:e,name:s||""},{ref:y,text:(v=o.textContent)==null?void 0:v.trim().slice(0,50)}});i(c?{matches:t}:{ref:t[0].ref,text:t[0].text})}catch(e){i({error:e instanceof Error?e.message:String(e)})}break}case"LOCATE_TEXT":{try{const{text:e,exact:s}=r,c=H(),a=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),w=[];for(;a.nextNode();){const l=a.currentNode.textContent||"";if(s?l.trim()===e:l.toLowerCase().includes(e.toLowerCase())){const o=a.currentNode.parentElement;if(o&&!w.includes(o)){const y=window.getComputedStyle(o);y.display!=="none"&&y.visibility!=="hidden"&&w.push(o)}}}if(w.length===0){i({error:`No element found with text "${e}"`});break}const u=w.sort((l,t)=>{var o,y;return(((o=l.textContent)==null?void 0:o.length)||0)-(((y=t.textContent)==null?void 0:y.length)||0)})[0],f=U(u),b=q(u,f,e);window.__piRefs=window.__piRefs||{},window.__piRefs[b]=u,c[b]={element:new WeakRef(u),role:f,name:e},i({ref:b,text:(m=u.textContent)==null?void 0:m.trim().slice(0,50)})}catch(e){i({error:e instanceof Error?e.message:String(e)})}break}case"LOCATE_LABEL":{try{const{label:e}=r,s=H(),c=document.querySelectorAll("label");let a=null;for(const f of c){const b=(g=f.textContent)==null?void 0:g.trim().toLowerCase();if(b!=null&&b.includes(e.toLowerCase())){const l=f.getAttribute("for");if(l&&(a=document.getElementById(l)),a||(a=f.querySelector("input, select, textarea")),a)break}}if(!a){const f=e.toLowerCase();a=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(!a){i({error:`No form field found with label "${e}"`});break}const w=U(a),u=q(a,w,e);window.__piRefs=window.__piRefs||{},window.__piRefs[u]=a,s[u]={element:new WeakRef(a),role:w,name:e},i({ref:u,label:e})}catch(e){i({error:e instanceof Error?e.message:String(e)})}break}case"GET_ELEMENT_TEXT":{try{const{ref:e}=r,s=H(),c=s[e];let a;if(c&&(a=c.element.deref(),a||delete s[e]),!a&&window.__piRefs&&(a=window.__piRefs[e]),!a){i({error:`Element ${e} not found`});break}i({text:((p=a.textContent)==null?void 0:p.trim())||""})}catch(e){i({error:e instanceof Error?e.message:String(e)})}break}case"SCROLL_TO_ELEMENT":{const e=oe(r.ref);i(e);break}case"UPLOAD_IMAGE":{const e=se(r.base64,r.ref,r.coordinate,r.filename);i(e);break}case"WAIT_FOR_ELEMENT":{const{selector:e,state:s="visible",timeout:c=2e4}=r,a=Math.min(c,6e4),w=l=>{if(!l)return!1;const t=window.getComputedStyle(l);return t.display!=="none"&&t.visibility!=="hidden"&&t.opacity!=="0"&&l.offsetWidth>0&&l.offsetHeight>0},u=()=>{const l=document.querySelector(e);switch(s){case"attached":return!!l;case"detached":return!l;case"hidden":return!l||!w(l);case"visible":default:return w(l)}},f=Date.now();return new Promise(l=>{if(u()){l({success:!0,waited:Date.now()-f});return}const t=new MutationObserver(()=>{u()&&(t.disconnect(),clearTimeout(o),l({success:!0,waited:Date.now()-f}))}),o=setTimeout(()=>{t.disconnect(),l({success:!1,waited:Date.now()-f,error:`Timeout waiting for "${e}" to be ${s}`})},a);t.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style","class","hidden","disabled"]})}).then(l=>{if(!l.success){i({error:l.error,waited:l.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const t=B("interactive",15,void 0,!0);i({...t,waited:l.waited})}),!0}case"WAIT_FOR_URL":{const{pattern:e,timeout:s=2e4}=r,c=Math.min(s,6e4),a=f=>{if(e.includes("*")){const b=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"<<<GLOBSTAR>>>").replace(/\*/g,"[^/]*").replace(/<<<GLOBSTAR>>>/g,".*");return new RegExp(`^${b}$`).test(f)}return f.includes(e)},w=Date.now();return new Promise(f=>{if(a(window.location.href)){f({success:!0,waited:Date.now()-w});return}let b=!1;const l=()=>{b||a(window.location.href)&&(b=!0,clearInterval(t),clearTimeout(o),window.removeEventListener("popstate",l),window.removeEventListener("hashchange",l),f({success:!0,waited:Date.now()-w}))},t=setInterval(l,100),o=setTimeout(()=>{b||(b=!0,clearInterval(t),window.removeEventListener("popstate",l),window.removeEventListener("hashchange",l),f({success:!1,waited:Date.now()-w,error:`Timeout waiting for URL to match "${e}". Current: ${window.location.href}`}))},c);window.addEventListener("popstate",l),window.addEventListener("hashchange",l)}).then(f=>{if(!f.success){i({error:f.error,waited:f.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const b=B("interactive",15,void 0,!0);i({...b,waited:f.waited})}),!0}case"WAIT_FOR_DOM_STABLE":{const{stable:e=100,timeout:s=5e3}=r,c=Math.min(s,3e4),a=Date.now();return new Promise(u=>{let f=Date.now(),b=!1;const l=()=>{if(b)return;Date.now()-f>=e&&(b=!0,t.disconnect(),clearTimeout(o),clearInterval(y),u({success:!0,waited:Date.now()-a}))},t=new MutationObserver(()=>{f=Date.now()}),o=setTimeout(()=>{b||(b=!0,t.disconnect(),clearInterval(y),u({success:!1,waited:Date.now()-a,error:`Timeout: DOM did not stabilize within ${c}ms`}))},c),y=setInterval(l,Math.max(10,Math.min(50,e/2)));t.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,characterData:!0}),l()}).then(u=>{if(!u.success){i({error:u.error,waited:u.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const f=B("interactive",15,void 0,!0);i({...f,waited:u.waited})}),!0}case"FORM_FILL":{const{data:e}=r;if(!Array.isArray(e))return i({error:"data must be an array of {ref, value} pairs"}),!0;const s=H(),c=[];for(const w of e){const{ref:u,value:f}=w;if(!u){c.push({ref:u||"unknown",success:!1,error:"Missing ref"});continue}const b=s[u];if(!b){c.push({ref:u,success:!1,error:"Element not found (run page.read first)"});continue}const l=b.element.deref();if(!l){delete s[u],c.push({ref:u,success:!1,error:"Element no longer exists"});continue}try{if(l instanceof HTMLInputElement){const t=l.type.toLowerCase();if(t==="checkbox"||t==="radio"){const o=f===!0||f==="true"||f==="1"||f==="checked";l.checked=o,l.dispatchEvent(new Event("change",{bubbles:!0}))}else l.focus(),l.value=String(f),l.dispatchEvent(new Event("input",{bubbles:!0})),l.dispatchEvent(new Event("change",{bubbles:!0}));c.push({ref:u,success:!0})}else l instanceof HTMLTextAreaElement?(l.focus(),l.value=String(f),l.dispatchEvent(new Event("input",{bubbles:!0})),l.dispatchEvent(new Event("change",{bubbles:!0})),c.push({ref:u,success:!0})):l instanceof HTMLSelectElement?(l.value=String(f),l.dispatchEvent(new Event("change",{bubbles:!0})),c.push({ref:u,success:!0})):l.isContentEditable?(l.focus(),l.textContent=String(f),l.dispatchEvent(new Event("input",{bubbles:!0})),c.push({ref:u,success:!0})):c.push({ref:u,success:!1,error:"Element is not fillable"})}catch(t){c.push({ref:u,success:!1,error:t instanceof Error?t.message:String(t)})}}const a=c.filter(w=>!w.success);return i({success:a.length===0,filled:c.filter(w=>w.success).length,failed:a.length,results:c}),!0}case"GET_FILE_INPUT_SELECTOR":{const{ref:e}=r;if(!e)return i({error:"No ref provided"}),!0;const s=H(),c=s[e];if(!c)return i({error:"Element not found (run page.read first)"}),!0;const a=c.element.deref();if(!a)return delete s[e],i({error:"Element no longer exists"}),!0;if(!(a instanceof HTMLInputElement)||a.type!=="file")return i({error:"Element is not a file input"}),!0;const w=`__pi_file_${Date.now()}`;return a.setAttribute("data-pi-file-id",w),i({selector:`[data-pi-file-id="${w}"]`}),!0}case"WAIT_FOR_NETWORK_IDLE":{const{timeout:e=1e4}=r,s=Math.min(e,6e4),c=["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/"],a=["img","image","font","icon"],w=t=>c.some(o=>t.includes(o)),u=t=>{const o=t.initiatorType||"unknown";return!!(a.includes(o)||/\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|eot)(\?|$)/i.test(t.name))},f=()=>{const t=performance.now();return performance.getEntriesByType("resource").filter(y=>{if(y.responseEnd!==0||y.name.startsWith("data:")||y.name.length>500||w(y.name))return!1;const v=t-y.startTime;return!(v>1e4||u(y)&&v>3e3)})},b=Date.now();return new Promise(t=>{const o=()=>{const y=f(),v=Date.now()-b;if(y.length===0){t({success:!0,waited:v});return}if(v>=s){t({success:!1,waited:v,pendingCount:y.length});return}setTimeout(o,100)};o()}).then(t=>{if(!t.success){i({error:`Network not idle after ${t.waited}ms (${t.pendingCount} requests pending)`,waited:t.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const o=B("interactive",15,void 0,!0);i({...o,waited:t.waited})}),!0}case"SEARCH_PAGE":{const{term:e,caseSensitive:s,limit:c}=r,a=ce(e,s||!1,c||10);i({query:e,count:a.length,matches:a});break}case"GET_ELEMENT_BOUNDS_FOR_ANNOTATION":{const e=H(),s=[];for(const[c,a]of Object.entries(e)){const w=a.element.deref();if(!w)continue;const u=w.getBoundingClientRect();u.width<=0||u.height<=0||u.bottom<0||u.top>window.innerHeight||u.right<0||u.left>window.innerWidth||s.push({ref:c,tag:w.tagName.toLowerCase(),bounds:{x:u.x,y:u.y,width:u.width,height:u.height}})}i({elements:s});break}default:return!1}return!1});function ce(r,h,i){const m=[],g=h?r:r.toLowerCase(),p=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),e=H();let s=0;for(;p.nextNode()&&m.length<i;){const c=p.currentNode,a=c.textContent||"",w=h?a:a.toLowerCase();let u=0;for(;(u=w.indexOf(g,u))!==-1&&m.length<i;){const f=c.parentElement;if(!f){u++;continue}const b=document.createRange();b.setStart(c,u),b.setEnd(c,Math.min(u+r.length,a.length));const l=b.getBoundingClientRect();if(l.width===0||l.height===0){u++;continue}const t=c.textContent||"",o=Math.max(0,u-30),y=Math.min(t.length,u+r.length+30),v=t.slice(o,y).trim();let A=null;for(const[S,k]of Object.entries(e)){const L=k.element.deref();if(L&&(L===f||L.contains(f))){A=S;break}}m.push({ref:`m${++s}`,text:t.slice(u,u+r.length),context:v,bounds:{x:Math.round(l.x),y:Math.round(l.y),width:Math.round(l.width),height:Math.round(l.height)},elementRef:A}),u++}}return m}
|
|
11
|
+
//# sourceMappingURL=accessibility-tree.js.map
|