scuttle-browser 0.1.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 +235 -0
- package/dist/accessibility.d.ts +34 -0
- package/dist/accessibility.js +240 -0
- package/dist/accessibility.js.map +1 -0
- package/dist/actions.d.ts +19 -0
- package/dist/actions.js +120 -0
- package/dist/actions.js.map +1 -0
- package/dist/browser.d.ts +31 -0
- package/dist/browser.js +79 -0
- package/dist/browser.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +227 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axel Krantz
|
|
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,235 @@
|
|
|
1
|
+
# Scuttle
|
|
2
|
+
|
|
3
|
+
Browser bridge for LLMs. Lets AI agents browse the web by converting pages into compact, annotated accessibility trees — no screenshots or raw HTML required.
|
|
4
|
+
|
|
5
|
+
Scuttle runs as an [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server. Any MCP-compatible client (Claude Code, Claude Desktop, Cursor, etc.) can use it to navigate, read, and interact with web pages.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
Instead of feeding raw HTML (too many tokens, too much noise) or screenshots (unreliable for spatial reasoning), Scuttle extracts the browser's **accessibility tree** — the same semantic structure used by screen readers. It then:
|
|
10
|
+
|
|
11
|
+
1. **Prunes** invisible and decorative nodes
|
|
12
|
+
2. **Assigns numeric IDs** to every interactable element (`[1] button "Submit"`, `[2] textbox "Search"`)
|
|
13
|
+
3. **Labels unnamed elements** using a fallback chain: visible text → aria-label → placeholder → nearby heading → positional description
|
|
14
|
+
4. **Accepts actions** by element ID — `click(1)`, `type(2, "hello")`
|
|
15
|
+
|
|
16
|
+
The result is a compact, token-efficient representation that LLMs can reason about and act on.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Page: Hacker News
|
|
20
|
+
URL: https://news.ycombinator.com/
|
|
21
|
+
Interactable elements: 227
|
|
22
|
+
────────────────────────────────────────────────────────────
|
|
23
|
+
heading "Hacker News"
|
|
24
|
+
[1] link "Hacker News"
|
|
25
|
+
[2] link "new"
|
|
26
|
+
[3] link "past"
|
|
27
|
+
...
|
|
28
|
+
row
|
|
29
|
+
cell "1."
|
|
30
|
+
[12] link "Show HN: Something cool"
|
|
31
|
+
"Show HN: Something cool"
|
|
32
|
+
cell "142 points by user 3 hours ago"
|
|
33
|
+
[14] link "user"
|
|
34
|
+
[15] link "85 comments"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Clone and install
|
|
41
|
+
git clone https://github.com/axelkrantz/scuttle.git
|
|
42
|
+
cd scuttle
|
|
43
|
+
npm install # installs dependencies + downloads Chromium
|
|
44
|
+
npm run build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or install globally:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g scuttle-browser
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
### Claude Code
|
|
56
|
+
|
|
57
|
+
Add to your project's `.mcp.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"scuttle": {
|
|
63
|
+
"command": "node",
|
|
64
|
+
"args": ["/path/to/scuttle/dist/index.js"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Claude Desktop
|
|
71
|
+
|
|
72
|
+
Add to `claude_desktop_config.json`:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"scuttle": {
|
|
78
|
+
"command": "node",
|
|
79
|
+
"args": ["/path/to/scuttle/dist/index.js"]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### With npx (no clone needed)
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"scuttle": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["-y", "scuttle-browser"]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Environment variables
|
|
99
|
+
|
|
100
|
+
| Variable | Default | Description |
|
|
101
|
+
|----------|---------|-------------|
|
|
102
|
+
| `SCUTTLE_HEADLESS` | `true` | Set to `false` to show the browser window |
|
|
103
|
+
| `SCUTTLE_VIEWPORT_WIDTH` | `1280` | Browser viewport width in pixels |
|
|
104
|
+
| `SCUTTLE_VIEWPORT_HEIGHT` | `720` | Browser viewport height in pixels |
|
|
105
|
+
| `SCUTTLE_TIMEOUT` | `30000` | Navigation timeout in milliseconds |
|
|
106
|
+
| `SCUTTLE_SETTLE_TIME` | `500` | Post-navigation settle time in ms (for SPAs that hydrate after DOM ready) |
|
|
107
|
+
|
|
108
|
+
Example with visible browser:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"scuttle": {
|
|
114
|
+
"command": "node",
|
|
115
|
+
"args": ["/path/to/scuttle/dist/index.js"],
|
|
116
|
+
"env": {
|
|
117
|
+
"SCUTTLE_HEADLESS": "false"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Tools
|
|
125
|
+
|
|
126
|
+
### `navigate`
|
|
127
|
+
|
|
128
|
+
Go to a URL.
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
navigate({ url: "https://example.com" })
|
|
132
|
+
→ "Navigated to: Example Domain\nURL: https://example.com/"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `observe`
|
|
136
|
+
|
|
137
|
+
Get the current page state as an annotated accessibility tree. Every interactable element gets a numeric ID in brackets.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
observe()
|
|
141
|
+
→ Page: Example Domain
|
|
142
|
+
URL: https://example.com/
|
|
143
|
+
Interactable elements: 1
|
|
144
|
+
────────────────────────────────────────────────────────────
|
|
145
|
+
heading "Example Domain"
|
|
146
|
+
"This domain is for use in illustrative examples..."
|
|
147
|
+
[1] link "More information..."
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `act`
|
|
151
|
+
|
|
152
|
+
Perform actions on the page using element IDs from `observe`.
|
|
153
|
+
|
|
154
|
+
| Action | Parameters | Description |
|
|
155
|
+
|--------|-----------|-------------|
|
|
156
|
+
| `click` | `id` | Click an element |
|
|
157
|
+
| `type` | `id`, `text` | Clear and type text into an input |
|
|
158
|
+
| `select` | `id`, `text` | Select a dropdown option |
|
|
159
|
+
| `hover` | `id` | Hover over an element |
|
|
160
|
+
| `scroll` | `direction` (`"up"` or `"down"`) | Scroll the page |
|
|
161
|
+
| `key` | `key` (e.g., `"Enter"`, `"Tab"`) | Press a keyboard key |
|
|
162
|
+
| `wait` | `ms` (max 10000) | Wait for dynamic content |
|
|
163
|
+
| `back` | — | Browser back |
|
|
164
|
+
| `forward` | — | Browser forward |
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
act({ action: "click", id: 1 })
|
|
168
|
+
→ "Clicked link 'More information...'"
|
|
169
|
+
|
|
170
|
+
act({ action: "type", id: 5, text: "search query" })
|
|
171
|
+
→ "Typed 'search query' into textbox 'Search'"
|
|
172
|
+
|
|
173
|
+
act({ action: "scroll", direction: "down" })
|
|
174
|
+
→ "Scrolled down"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `screenshot`
|
|
178
|
+
|
|
179
|
+
Take a PNG screenshot of the current viewport. Returns a base64-encoded image. Useful when the accessibility tree alone isn't enough to understand the layout.
|
|
180
|
+
|
|
181
|
+
### `get_text`
|
|
182
|
+
|
|
183
|
+
Extract all visible text from the page. Best for reading articles, docs, or any text-heavy content without the structural overhead of `observe`.
|
|
184
|
+
|
|
185
|
+
## Usage pattern
|
|
186
|
+
|
|
187
|
+
The typical agent loop is:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
1. navigate(url) → go to a page
|
|
191
|
+
2. observe() → read the page state
|
|
192
|
+
3. act(...) → interact with an element
|
|
193
|
+
4. observe() → read the updated state
|
|
194
|
+
5. repeat 3-4 → until the task is done
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Architecture
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
|
201
|
+
│ LLM/Agent │◄───►│ Scuttle │◄───►│ Playwright │
|
|
202
|
+
│ (MCP client)│ │ (MCP server) │ │ (Chromium) │
|
|
203
|
+
└─────────────┘ └──────────────┘ └─────────────┘
|
|
204
|
+
│
|
|
205
|
+
┌─────┴─────┐
|
|
206
|
+
│ │
|
|
207
|
+
Accessibility Action
|
|
208
|
+
Extractor Executor
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- **Browser Manager** — manages Playwright browser lifecycle, navigation, and screenshots
|
|
212
|
+
- **Accessibility Extractor** — snapshots the accessibility tree via CDP, prunes it, assigns IDs, and handles element labeling
|
|
213
|
+
- **Action Executor** — maps element IDs to Playwright locators, executes actions with fallbacks
|
|
214
|
+
|
|
215
|
+
### Element labeling strategy
|
|
216
|
+
|
|
217
|
+
When elements lack good names (looking at you, `<div class="css-1a2b3c">`), Scuttle applies a fallback chain:
|
|
218
|
+
|
|
219
|
+
1. **Visible text** — `[5] button "Add to cart"`
|
|
220
|
+
2. **Aria attributes** — `[6] textbox aria-label="Email address"`
|
|
221
|
+
3. **Value** — `[7] combobox [value: "United States"]`
|
|
222
|
+
4. **Context** — `[8] button (under "Account Settings")`
|
|
223
|
+
5. **Role fallback** — `[9] [unnamed button]`
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npm run dev # watch mode — recompiles on changes
|
|
229
|
+
npm run build # one-time build
|
|
230
|
+
npm start # run the MCP server
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Page, Locator } from "playwright";
|
|
2
|
+
export interface AnnotatedNode {
|
|
3
|
+
id: number | null;
|
|
4
|
+
role: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
value: string;
|
|
8
|
+
children: AnnotatedNode[];
|
|
9
|
+
}
|
|
10
|
+
export interface ElementRef {
|
|
11
|
+
id: number;
|
|
12
|
+
role: string;
|
|
13
|
+
label: string;
|
|
14
|
+
locator: Locator;
|
|
15
|
+
}
|
|
16
|
+
export declare class AccessibilityExtractor {
|
|
17
|
+
private elementMap;
|
|
18
|
+
private nextId;
|
|
19
|
+
private cdp;
|
|
20
|
+
private cdpPage;
|
|
21
|
+
reset(): void;
|
|
22
|
+
getElementMap(): Map<number, ElementRef>;
|
|
23
|
+
private getCDP;
|
|
24
|
+
extract(page: Page): Promise<{
|
|
25
|
+
tree: string;
|
|
26
|
+
elementCount: number;
|
|
27
|
+
}>;
|
|
28
|
+
private cdpNodesToTree;
|
|
29
|
+
private processNode;
|
|
30
|
+
private processChildren;
|
|
31
|
+
private buildLabel;
|
|
32
|
+
private buildLocator;
|
|
33
|
+
private serialize;
|
|
34
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const INTERACTABLE_ROLES = new Set([
|
|
2
|
+
"link",
|
|
3
|
+
"button",
|
|
4
|
+
"textbox",
|
|
5
|
+
"searchbox",
|
|
6
|
+
"combobox",
|
|
7
|
+
"listbox",
|
|
8
|
+
"option",
|
|
9
|
+
"checkbox",
|
|
10
|
+
"radio",
|
|
11
|
+
"switch",
|
|
12
|
+
"slider",
|
|
13
|
+
"spinbutton",
|
|
14
|
+
"tab",
|
|
15
|
+
"menuitem",
|
|
16
|
+
"menuitemcheckbox",
|
|
17
|
+
"menuitemradio",
|
|
18
|
+
"treeitem",
|
|
19
|
+
]);
|
|
20
|
+
const SKIP_ROLES = new Set([
|
|
21
|
+
"none",
|
|
22
|
+
"presentation",
|
|
23
|
+
"generic",
|
|
24
|
+
"LineBreak",
|
|
25
|
+
"InlineTextBox",
|
|
26
|
+
]);
|
|
27
|
+
const STRUCTURAL_ROLES = new Set([
|
|
28
|
+
"group",
|
|
29
|
+
"list",
|
|
30
|
+
"listitem",
|
|
31
|
+
"navigation",
|
|
32
|
+
"main",
|
|
33
|
+
"complementary",
|
|
34
|
+
"banner",
|
|
35
|
+
"contentinfo",
|
|
36
|
+
"region",
|
|
37
|
+
"form",
|
|
38
|
+
"article",
|
|
39
|
+
"section",
|
|
40
|
+
"toolbar",
|
|
41
|
+
"menu",
|
|
42
|
+
"menubar",
|
|
43
|
+
"tablist",
|
|
44
|
+
"tabpanel",
|
|
45
|
+
"tree",
|
|
46
|
+
"grid",
|
|
47
|
+
"row",
|
|
48
|
+
"rowgroup",
|
|
49
|
+
"cell",
|
|
50
|
+
"columnheader",
|
|
51
|
+
"rowheader",
|
|
52
|
+
]);
|
|
53
|
+
export class AccessibilityExtractor {
|
|
54
|
+
elementMap = new Map();
|
|
55
|
+
nextId = 1;
|
|
56
|
+
cdp = null;
|
|
57
|
+
cdpPage = null;
|
|
58
|
+
reset() {
|
|
59
|
+
this.elementMap.clear();
|
|
60
|
+
this.nextId = 1;
|
|
61
|
+
}
|
|
62
|
+
getElementMap() {
|
|
63
|
+
return this.elementMap;
|
|
64
|
+
}
|
|
65
|
+
async getCDP(page) {
|
|
66
|
+
// Reuse CDP session for the same page to avoid handshake overhead
|
|
67
|
+
if (this.cdp && this.cdpPage === page) {
|
|
68
|
+
return this.cdp;
|
|
69
|
+
}
|
|
70
|
+
if (this.cdp) {
|
|
71
|
+
await this.cdp.detach().catch(() => { });
|
|
72
|
+
}
|
|
73
|
+
this.cdp = await page.context().newCDPSession(page);
|
|
74
|
+
this.cdpPage = page;
|
|
75
|
+
return this.cdp;
|
|
76
|
+
}
|
|
77
|
+
async extract(page) {
|
|
78
|
+
this.reset();
|
|
79
|
+
const cdp = await this.getCDP(page);
|
|
80
|
+
const { nodes } = await cdp.send("Accessibility.getFullAXTree");
|
|
81
|
+
if (!nodes || nodes.length === 0) {
|
|
82
|
+
return { tree: "[Empty page — no accessibility tree available]", elementCount: 0 };
|
|
83
|
+
}
|
|
84
|
+
const snapshot = this.cdpNodesToTree(nodes);
|
|
85
|
+
if (!snapshot) {
|
|
86
|
+
return { tree: "[Empty page — no accessibility tree available]", elementCount: 0 };
|
|
87
|
+
}
|
|
88
|
+
const annotated = this.processNode(snapshot, page, []);
|
|
89
|
+
const text = this.serialize(annotated, 0);
|
|
90
|
+
return { tree: text, elementCount: this.elementMap.size };
|
|
91
|
+
}
|
|
92
|
+
cdpNodesToTree(nodes) {
|
|
93
|
+
if (nodes.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
const nodeMap = new Map();
|
|
96
|
+
const childIds = new Map();
|
|
97
|
+
for (const node of nodes) {
|
|
98
|
+
const axNode = {
|
|
99
|
+
nodeId: node.nodeId,
|
|
100
|
+
role: node.role?.value || "unknown",
|
|
101
|
+
name: node.name?.value || "",
|
|
102
|
+
description: node.description?.value || "",
|
|
103
|
+
value: node.value?.value || "",
|
|
104
|
+
children: [],
|
|
105
|
+
};
|
|
106
|
+
nodeMap.set(node.nodeId, axNode);
|
|
107
|
+
if (node.childIds) {
|
|
108
|
+
childIds.set(node.nodeId, node.childIds);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Link children
|
|
112
|
+
for (const [parentId, kids] of childIds) {
|
|
113
|
+
const parent = nodeMap.get(parentId);
|
|
114
|
+
if (parent) {
|
|
115
|
+
for (const childId of kids) {
|
|
116
|
+
const child = nodeMap.get(childId);
|
|
117
|
+
if (child)
|
|
118
|
+
parent.children.push(child);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Root is the first node
|
|
123
|
+
return nodeMap.get(nodes[0].nodeId) || null;
|
|
124
|
+
}
|
|
125
|
+
processNode(node, page, ancestorNames) {
|
|
126
|
+
const role = node.role;
|
|
127
|
+
const name = node.name.trim();
|
|
128
|
+
if (SKIP_ROLES.has(role)) {
|
|
129
|
+
const children = this.processChildren(node.children || [], page, ancestorNames);
|
|
130
|
+
if (children.length === 1)
|
|
131
|
+
return children[0];
|
|
132
|
+
if (children.length === 0)
|
|
133
|
+
return null;
|
|
134
|
+
return { id: null, role: "group", name: "", description: "", value: "", children };
|
|
135
|
+
}
|
|
136
|
+
const isInteractable = INTERACTABLE_ROLES.has(role);
|
|
137
|
+
// Push/pop to avoid array spread per node
|
|
138
|
+
if (name)
|
|
139
|
+
ancestorNames.push(name);
|
|
140
|
+
const children = this.processChildren(node.children || [], page, ancestorNames);
|
|
141
|
+
if (name)
|
|
142
|
+
ancestorNames.pop();
|
|
143
|
+
if (!isInteractable && STRUCTURAL_ROLES.has(role) && !name) {
|
|
144
|
+
if (children.length === 0)
|
|
145
|
+
return null;
|
|
146
|
+
if (children.length === 1)
|
|
147
|
+
return children[0];
|
|
148
|
+
}
|
|
149
|
+
let id = null;
|
|
150
|
+
if (isInteractable) {
|
|
151
|
+
id = this.nextId++;
|
|
152
|
+
const label = this.buildLabel(node, role, ancestorNames);
|
|
153
|
+
const locator = this.buildLocator(page, node, role, name);
|
|
154
|
+
this.elementMap.set(id, { id, role, label, locator });
|
|
155
|
+
}
|
|
156
|
+
if (role === "text" && !name && children.length === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
role,
|
|
162
|
+
name,
|
|
163
|
+
description: (node.description || "").trim(),
|
|
164
|
+
value: (node.value || "").trim(),
|
|
165
|
+
children,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
processChildren(children, page, ancestorNames) {
|
|
169
|
+
const results = [];
|
|
170
|
+
for (const child of children) {
|
|
171
|
+
const processed = this.processNode(child, page, ancestorNames);
|
|
172
|
+
if (processed)
|
|
173
|
+
results.push(processed);
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
buildLabel(node, role, ancestorNames) {
|
|
178
|
+
const name = (node.name || "").trim();
|
|
179
|
+
const desc = (node.description || "").trim();
|
|
180
|
+
// Priority 1: visible name/text
|
|
181
|
+
if (name)
|
|
182
|
+
return name;
|
|
183
|
+
// Priority 2: description (often aria-label or title)
|
|
184
|
+
if (desc)
|
|
185
|
+
return desc;
|
|
186
|
+
// Priority 3: value (for inputs with pre-filled values)
|
|
187
|
+
const value = (node.value || "").trim();
|
|
188
|
+
if (value)
|
|
189
|
+
return `[value: ${value}]`;
|
|
190
|
+
// Priority 4: contextual — use nearest ancestor name
|
|
191
|
+
const nearestContext = ancestorNames[ancestorNames.length - 1];
|
|
192
|
+
if (nearestContext)
|
|
193
|
+
return `(under "${nearestContext}")`;
|
|
194
|
+
// Priority 5: positional/role fallback
|
|
195
|
+
return `[unnamed ${role}]`;
|
|
196
|
+
}
|
|
197
|
+
buildLocator(page, node, role, name) {
|
|
198
|
+
// Use Playwright's role-based locator for best reliability
|
|
199
|
+
if (name) {
|
|
200
|
+
return page.getByRole(role, { name, exact: false });
|
|
201
|
+
}
|
|
202
|
+
return page.getByRole(role);
|
|
203
|
+
}
|
|
204
|
+
serialize(node, depth) {
|
|
205
|
+
if (!node)
|
|
206
|
+
return "";
|
|
207
|
+
const indent = " ".repeat(depth);
|
|
208
|
+
const parts = [];
|
|
209
|
+
// Build the line for this node
|
|
210
|
+
let line = indent;
|
|
211
|
+
if (node.id !== null) {
|
|
212
|
+
line += `[${node.id}] `;
|
|
213
|
+
}
|
|
214
|
+
const showRole = node.id !== null || STRUCTURAL_ROLES.has(node.role) || node.role === "heading";
|
|
215
|
+
if (showRole) {
|
|
216
|
+
line += node.role;
|
|
217
|
+
}
|
|
218
|
+
if (node.name) {
|
|
219
|
+
line += showRole ? ` "${node.name}"` : `"${node.name}"`;
|
|
220
|
+
}
|
|
221
|
+
if (node.description && node.description !== node.name) {
|
|
222
|
+
line += ` — ${node.description}`;
|
|
223
|
+
}
|
|
224
|
+
if (node.value) {
|
|
225
|
+
line += ` [value: "${node.value}"]`;
|
|
226
|
+
}
|
|
227
|
+
const trimmed = line.trim();
|
|
228
|
+
if (trimmed) {
|
|
229
|
+
parts.push(line);
|
|
230
|
+
}
|
|
231
|
+
// Serialize children
|
|
232
|
+
for (const child of node.children) {
|
|
233
|
+
const childStr = this.serialize(child, depth + (trimmed ? 1 : 0));
|
|
234
|
+
if (childStr)
|
|
235
|
+
parts.push(childStr);
|
|
236
|
+
}
|
|
237
|
+
return parts.join("\n");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=accessibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../src/accessibility.ts"],"names":[],"mappings":"AAkBA,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,MAAM;IACN,QAAQ;IACR,SAAS;IACT,WAAW;IACX,UAAU;IACV,SAAS;IACT,QAAQ;IACR,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,YAAY;IACZ,KAAK;IACL,UAAU;IACV,kBAAkB;IAClB,eAAe;IACf,UAAU;CACX,CAAC,CAAC;AAEH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,MAAM;IACN,cAAc;IACd,SAAS;IACT,WAAW;IACX,eAAe;CAChB,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,OAAO;IACP,MAAM;IACN,UAAU;IACV,YAAY;IACZ,MAAM;IACN,eAAe;IACf,QAAQ;IACR,aAAa;IACb,QAAQ;IACR,MAAM;IACN,SAAS;IACT,SAAS;IACT,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;IACT,UAAU;IACV,MAAM;IACN,MAAM;IACN,KAAK;IACL,UAAU;IACV,MAAM;IACN,cAAc;IACd,WAAW;CACZ,CAAC,CAAC;AAUH,MAAM,OAAO,sBAAsB;IACzB,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC3C,MAAM,GAAG,CAAC,CAAC;IACX,GAAG,GAAsB,IAAI,CAAC;IAC9B,OAAO,GAAgB,IAAI,CAAC;IAEpC,KAAK;QACH,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAClB,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,IAAU;QAC7B,kEAAkE;QAClE,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,GAAG,CAAC;QAClB,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAU;QACtB,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAChE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,IAAI,EAAE,gDAAgD,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACrF,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,IAAI,EAAE,gDAAgD,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACrF,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;IAC5D,CAAC;IAEO,cAAc,CAAC,KAAY;QACjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0C,CAAC;QAClE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;QAE7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG;gBACb,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,SAAS;gBACnC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;gBAC5B,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE;gBAC1C,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;gBAC9B,QAAQ,EAAE,EAAiB;aAC5B,CAAC;YACF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACjC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QAED,gBAAgB;QAChB,KAAK,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACrC,IAAI,MAAM,EAAE,CAAC;gBACX,KAAK,MAAM,OAAO,IAAI,IAAI,EAAE,CAAC;oBAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBACnC,IAAI,KAAK;wBAAE,MAAM,CAAC,QAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC1C,CAAC;YACH,CAAC;QACH,CAAC;QAED,yBAAyB;QACzB,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;IAC9C,CAAC;IAEO,WAAW,CACjB,IAAe,EACf,IAAU,EACV,aAAuB;QAEvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAE9B,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YAChF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;QACrF,CAAC;QAED,MAAM,cAAc,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEpD,0CAA0C;QAC1C,IAAI,IAAI;YAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;QAChF,IAAI,IAAI;YAAE,aAAa,CAAC,GAAG,EAAE,CAAC;QAE9B,IAAI,CAAC,cAAc,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC3D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QAED,IAAI,EAAE,GAAkB,IAAI,CAAC;QAC7B,IAAI,cAAc,EAAE,CAAC;YACnB,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YACzD,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAC1D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO;YACL,EAAE;YACF,IAAI;YACJ,IAAI;YACJ,WAAW,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YAC5C,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YAChC,QAAQ;SACT,CAAC;IACJ,CAAC;IAEO,eAAe,CACrB,QAAqB,EACrB,IAAU,EACV,aAAuB;QAEvB,MAAM,OAAO,GAAoB,EAAE,CAAC;QACpC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YAC/D,IAAI,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,UAAU,CAAC,IAAe,EAAE,IAAY,EAAE,aAAuB;QACvE,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAE7C,gCAAgC;QAChC,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QAEtB,sDAAsD;QACtD,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QAEtB,wDAAwD;QACxD,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACxC,IAAI,KAAK;YAAE,OAAO,WAAW,KAAK,GAAG,CAAC;QAEtC,qDAAqD;QACrD,MAAM,cAAc,GAAG,aAAa,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC/D,IAAI,cAAc;YAAE,OAAO,WAAW,cAAc,IAAI,CAAC;QAEzD,uCAAuC;QACvC,OAAO,YAAY,IAAI,GAAG,CAAC;IAC7B,CAAC;IAEO,YAAY,CAAC,IAAU,EAAE,IAAe,EAAE,IAAY,EAAE,IAAY;QAC1E,2DAA2D;QAC3D,IAAI,IAAI,EAAE,CAAC;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,IAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,IAAW,CAAC,CAAC;IACrC,CAAC;IAEO,SAAS,CAAC,IAA0B,EAAE,KAAa;QACzD,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,+BAA+B;QAC/B,IAAI,IAAI,GAAG,MAAM,CAAC;QAClB,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;YACrB,IAAI,IAAI,IAAI,IAAI,CAAC,EAAE,IAAI,CAAC;QAC1B,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC;QAChG,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;QACpB,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC;QAC1D,CAAC;QAED,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;YACvD,IAAI,IAAI,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACnC,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,IAAI,aAAa,IAAI,CAAC,KAAK,IAAI,CAAC;QACtC,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,qBAAqB;QACrB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClE,IAAI,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Page } from "playwright";
|
|
2
|
+
import { ElementRef } from "./accessibility.js";
|
|
3
|
+
export type ActionResult = {
|
|
4
|
+
success: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export declare class ActionExecutor {
|
|
8
|
+
private page;
|
|
9
|
+
private elementMap;
|
|
10
|
+
constructor(page: Page, elementMap: Map<number, ElementRef>);
|
|
11
|
+
private getElement;
|
|
12
|
+
click(id: number): Promise<ActionResult>;
|
|
13
|
+
type(id: number, text: string): Promise<ActionResult>;
|
|
14
|
+
selectOption(id: number, value: string): Promise<ActionResult>;
|
|
15
|
+
hover(id: number): Promise<ActionResult>;
|
|
16
|
+
scroll(direction: "up" | "down"): Promise<ActionResult>;
|
|
17
|
+
pressKey(key: string): Promise<ActionResult>;
|
|
18
|
+
wait(ms?: number): Promise<ActionResult>;
|
|
19
|
+
}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class ActionExecutor {
|
|
2
|
+
page;
|
|
3
|
+
elementMap;
|
|
4
|
+
constructor(page, elementMap) {
|
|
5
|
+
this.page = page;
|
|
6
|
+
this.elementMap = elementMap;
|
|
7
|
+
}
|
|
8
|
+
getElement(id) {
|
|
9
|
+
const el = this.elementMap.get(id);
|
|
10
|
+
if (!el) {
|
|
11
|
+
throw new Error(`Element [${id}] not found. Run observe first to get the current page state.`);
|
|
12
|
+
}
|
|
13
|
+
return el;
|
|
14
|
+
}
|
|
15
|
+
async click(id) {
|
|
16
|
+
const el = this.getElement(id);
|
|
17
|
+
const loc = el.locator.first();
|
|
18
|
+
try {
|
|
19
|
+
await loc.click({ timeout: 5000 });
|
|
20
|
+
await this.page.waitForTimeout(500);
|
|
21
|
+
return { success: true, message: `Clicked ${el.role} "${el.label}"` };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
// JS fallback for elements covered by overlays or intercepted by frameworks
|
|
25
|
+
try {
|
|
26
|
+
await loc.evaluate((node) => node.click());
|
|
27
|
+
await this.page.waitForTimeout(500);
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
message: `Clicked ${el.role} "${el.label}" (via JS fallback)`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
message: `Failed to click [${id}] ${el.role} "${el.label}": ${err}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async type(id, text) {
|
|
42
|
+
const el = this.getElement(id);
|
|
43
|
+
const loc = el.locator.first();
|
|
44
|
+
try {
|
|
45
|
+
await loc.click({ timeout: 5000 });
|
|
46
|
+
await loc.fill(text);
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
message: `Typed "${text}" into ${el.role} "${el.label}"`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// Keyboard fallback for React-controlled inputs that reject fill()
|
|
54
|
+
try {
|
|
55
|
+
await this.page.keyboard.press("Control+A");
|
|
56
|
+
await this.page.keyboard.press("Backspace");
|
|
57
|
+
await this.page.keyboard.type(text, { delay: 50 });
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
message: `Typed "${text}" into ${el.role} "${el.label}" (via keyboard fallback)`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
message: `Failed to type into [${id}] ${el.role} "${el.label}": ${err}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async selectOption(id, value) {
|
|
72
|
+
const el = this.getElement(id);
|
|
73
|
+
try {
|
|
74
|
+
await el.locator.first().selectOption(value, { timeout: 5000 });
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
message: `Selected "${value}" in ${el.role} "${el.label}"`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
message: `Failed to select in [${id}] ${el.role} "${el.label}": ${err}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async hover(id) {
|
|
88
|
+
const el = this.getElement(id);
|
|
89
|
+
try {
|
|
90
|
+
await el.locator.first().hover({ timeout: 5000 });
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
message: `Hovered over ${el.role} "${el.label}"`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
message: `Failed to hover [${id}] ${el.role} "${el.label}": ${err}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async scroll(direction) {
|
|
104
|
+
const delta = direction === "down" ? 500 : -500;
|
|
105
|
+
await this.page.mouse.wheel(0, delta);
|
|
106
|
+
await this.page.waitForTimeout(300);
|
|
107
|
+
return { success: true, message: `Scrolled ${direction}` };
|
|
108
|
+
}
|
|
109
|
+
async pressKey(key) {
|
|
110
|
+
await this.page.keyboard.press(key);
|
|
111
|
+
await this.page.waitForTimeout(300);
|
|
112
|
+
return { success: true, message: `Pressed key "${key}"` };
|
|
113
|
+
}
|
|
114
|
+
async wait(ms = 2000) {
|
|
115
|
+
const capped = Math.min(ms, 10000);
|
|
116
|
+
await this.page.waitForTimeout(capped);
|
|
117
|
+
return { success: true, message: `Waited ${capped}ms` };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Page } from "playwright";
|
|
2
|
+
export interface BrowserConfig {
|
|
3
|
+
headless: boolean;
|
|
4
|
+
viewportWidth: number;
|
|
5
|
+
viewportHeight: number;
|
|
6
|
+
timeout: number;
|
|
7
|
+
settleTime: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class BrowserManager {
|
|
10
|
+
private browser;
|
|
11
|
+
private context;
|
|
12
|
+
private page;
|
|
13
|
+
private config;
|
|
14
|
+
constructor(config?: Partial<BrowserConfig>);
|
|
15
|
+
getPage(): Promise<Page>;
|
|
16
|
+
private pageState;
|
|
17
|
+
navigate(url: string): Promise<{
|
|
18
|
+
url: string;
|
|
19
|
+
title: string;
|
|
20
|
+
}>;
|
|
21
|
+
goBack(): Promise<{
|
|
22
|
+
url: string;
|
|
23
|
+
title: string;
|
|
24
|
+
}>;
|
|
25
|
+
goForward(): Promise<{
|
|
26
|
+
url: string;
|
|
27
|
+
title: string;
|
|
28
|
+
}>;
|
|
29
|
+
screenshot(): Promise<Buffer>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
function loadConfig() {
|
|
3
|
+
return {
|
|
4
|
+
headless: process.env.SCUTTLE_HEADLESS !== "false",
|
|
5
|
+
viewportWidth: parseInt(process.env.SCUTTLE_VIEWPORT_WIDTH || "1280", 10),
|
|
6
|
+
viewportHeight: parseInt(process.env.SCUTTLE_VIEWPORT_HEIGHT || "720", 10),
|
|
7
|
+
timeout: parseInt(process.env.SCUTTLE_TIMEOUT || "30000", 10),
|
|
8
|
+
settleTime: parseInt(process.env.SCUTTLE_SETTLE_TIME || "500", 10),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export class BrowserManager {
|
|
12
|
+
browser = null;
|
|
13
|
+
context = null;
|
|
14
|
+
page = null;
|
|
15
|
+
config;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = { ...loadConfig(), ...config };
|
|
18
|
+
}
|
|
19
|
+
async getPage() {
|
|
20
|
+
if (!this.browser || !this.browser.isConnected()) {
|
|
21
|
+
this.browser = await chromium.launch({ headless: this.config.headless });
|
|
22
|
+
this.context = await this.browser.newContext({
|
|
23
|
+
viewport: {
|
|
24
|
+
width: this.config.viewportWidth,
|
|
25
|
+
height: this.config.viewportHeight,
|
|
26
|
+
},
|
|
27
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
28
|
+
});
|
|
29
|
+
this.page = await this.context.newPage();
|
|
30
|
+
}
|
|
31
|
+
if (!this.page || this.page.isClosed()) {
|
|
32
|
+
this.page = await this.context.newPage();
|
|
33
|
+
}
|
|
34
|
+
return this.page;
|
|
35
|
+
}
|
|
36
|
+
async pageState(page) {
|
|
37
|
+
return { url: page.url(), title: await page.title() };
|
|
38
|
+
}
|
|
39
|
+
async navigate(url) {
|
|
40
|
+
const page = await this.getPage();
|
|
41
|
+
await page.goto(url, {
|
|
42
|
+
waitUntil: "domcontentloaded",
|
|
43
|
+
timeout: this.config.timeout,
|
|
44
|
+
});
|
|
45
|
+
if (this.config.settleTime > 0) {
|
|
46
|
+
await page.waitForTimeout(this.config.settleTime);
|
|
47
|
+
}
|
|
48
|
+
return this.pageState(page);
|
|
49
|
+
}
|
|
50
|
+
async goBack() {
|
|
51
|
+
const page = await this.getPage();
|
|
52
|
+
await page.goBack({
|
|
53
|
+
waitUntil: "domcontentloaded",
|
|
54
|
+
timeout: this.config.timeout,
|
|
55
|
+
});
|
|
56
|
+
return this.pageState(page);
|
|
57
|
+
}
|
|
58
|
+
async goForward() {
|
|
59
|
+
const page = await this.getPage();
|
|
60
|
+
await page.goForward({
|
|
61
|
+
waitUntil: "domcontentloaded",
|
|
62
|
+
timeout: this.config.timeout,
|
|
63
|
+
});
|
|
64
|
+
return this.pageState(page);
|
|
65
|
+
}
|
|
66
|
+
async screenshot() {
|
|
67
|
+
const page = await this.getPage();
|
|
68
|
+
return await page.screenshot({ type: "png" });
|
|
69
|
+
}
|
|
70
|
+
async close() {
|
|
71
|
+
if (this.browser) {
|
|
72
|
+
await this.browser.close();
|
|
73
|
+
this.browser = null;
|
|
74
|
+
this.context = null;
|
|
75
|
+
this.page = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { BrowserManager } from "./browser.js";
|
|
6
|
+
import { AccessibilityExtractor } from "./accessibility.js";
|
|
7
|
+
import { ActionExecutor } from "./actions.js";
|
|
8
|
+
const browser = new BrowserManager();
|
|
9
|
+
const extractor = new AccessibilityExtractor();
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: "scuttle",
|
|
12
|
+
version: "0.1.0",
|
|
13
|
+
});
|
|
14
|
+
function formatPageHeader(title, url, extra) {
|
|
15
|
+
const lines = [`Page: ${title}`, `URL: ${url}`];
|
|
16
|
+
if (extra)
|
|
17
|
+
lines.push(extra);
|
|
18
|
+
lines.push("─".repeat(60));
|
|
19
|
+
return lines.join("\n") + "\n";
|
|
20
|
+
}
|
|
21
|
+
function toolError(msg) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: msg }],
|
|
24
|
+
isError: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
server.tool("navigate", "Navigate to a URL. Returns the page title and URL after loading.", { url: z.string().describe("The URL to navigate to") }, async ({ url }) => {
|
|
28
|
+
try {
|
|
29
|
+
const result = await browser.navigate(url);
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: `Navigated to: ${result.title}\nURL: ${result.url}`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return toolError(`Navigation failed: ${err}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
server.tool("observe", `Get the current page state as an annotated accessibility tree. Each interactable element has a numeric ID in brackets like [1], [2], etc. Use these IDs with the "act" tool to interact with elements. The tree shows the semantic structure: headings, links, buttons, text inputs, etc. Call this after navigate and after every action to see the updated page state.`, {}, async () => {
|
|
44
|
+
try {
|
|
45
|
+
const page = await browser.getPage();
|
|
46
|
+
const url = page.url();
|
|
47
|
+
const [title, { tree, elementCount }] = await Promise.all([
|
|
48
|
+
page.title(),
|
|
49
|
+
extractor.extract(page),
|
|
50
|
+
]);
|
|
51
|
+
const header = formatPageHeader(title, url, `Interactable elements: ${elementCount}`);
|
|
52
|
+
return { content: [{ type: "text", text: header + tree }] };
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return toolError(`Observe failed: ${err}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
server.tool("act", `Perform an action on the current page. Available actions:
|
|
59
|
+
- click(id): Click an element by its numeric ID from observe
|
|
60
|
+
- type(id, text): Type text into an input field (clears existing text first)
|
|
61
|
+
- select(id, value): Select an option in a dropdown
|
|
62
|
+
- hover(id): Hover over an element to reveal tooltips or menus
|
|
63
|
+
- scroll(direction): Scroll the page "up" or "down"
|
|
64
|
+
- key(key): Press a keyboard key (e.g., "Enter", "Tab", "Escape", "ArrowDown")
|
|
65
|
+
- wait(ms): Wait for a specified time in milliseconds (max 10000ms)
|
|
66
|
+
- back(): Go back in browser history
|
|
67
|
+
- forward(): Go forward in browser history
|
|
68
|
+
|
|
69
|
+
Always call "observe" after acting to see the updated page state.`, {
|
|
70
|
+
action: z
|
|
71
|
+
.enum([
|
|
72
|
+
"click",
|
|
73
|
+
"type",
|
|
74
|
+
"select",
|
|
75
|
+
"hover",
|
|
76
|
+
"scroll",
|
|
77
|
+
"key",
|
|
78
|
+
"wait",
|
|
79
|
+
"back",
|
|
80
|
+
"forward",
|
|
81
|
+
])
|
|
82
|
+
.describe("The action to perform"),
|
|
83
|
+
id: z
|
|
84
|
+
.number()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Element ID from observe (required for click, type, select, hover)"),
|
|
87
|
+
text: z
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Text to type or option to select (required for type and select)"),
|
|
91
|
+
direction: z
|
|
92
|
+
.enum(["up", "down"])
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('Scroll direction (required for scroll, default "down")'),
|
|
95
|
+
key: z
|
|
96
|
+
.string()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Key to press (required for key action)"),
|
|
99
|
+
ms: z
|
|
100
|
+
.number()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Milliseconds to wait (for wait action, default 2000)"),
|
|
103
|
+
}, async ({ action, id, text, direction, key, ms }) => {
|
|
104
|
+
try {
|
|
105
|
+
const page = await browser.getPage();
|
|
106
|
+
const executor = new ActionExecutor(page, extractor.getElementMap());
|
|
107
|
+
let result;
|
|
108
|
+
switch (action) {
|
|
109
|
+
case "click":
|
|
110
|
+
if (id === undefined)
|
|
111
|
+
return toolError("click requires an element id");
|
|
112
|
+
result = await executor.click(id);
|
|
113
|
+
break;
|
|
114
|
+
case "type":
|
|
115
|
+
if (id === undefined)
|
|
116
|
+
return toolError("type requires an element id");
|
|
117
|
+
if (text === undefined)
|
|
118
|
+
return toolError("type requires text");
|
|
119
|
+
result = await executor.type(id, text);
|
|
120
|
+
break;
|
|
121
|
+
case "select":
|
|
122
|
+
if (id === undefined)
|
|
123
|
+
return toolError("select requires an element id");
|
|
124
|
+
if (!text)
|
|
125
|
+
return toolError("select requires a value (pass in text param)");
|
|
126
|
+
result = await executor.selectOption(id, text);
|
|
127
|
+
break;
|
|
128
|
+
case "hover":
|
|
129
|
+
if (id === undefined)
|
|
130
|
+
return toolError("hover requires an element id");
|
|
131
|
+
result = await executor.hover(id);
|
|
132
|
+
break;
|
|
133
|
+
case "scroll":
|
|
134
|
+
result = await executor.scroll(direction || "down");
|
|
135
|
+
break;
|
|
136
|
+
case "key":
|
|
137
|
+
if (!key)
|
|
138
|
+
return toolError("key action requires a key name");
|
|
139
|
+
result = await executor.pressKey(key);
|
|
140
|
+
break;
|
|
141
|
+
case "wait":
|
|
142
|
+
result = await executor.wait(ms);
|
|
143
|
+
break;
|
|
144
|
+
case "back": {
|
|
145
|
+
const backResult = await browser.goBack();
|
|
146
|
+
result = {
|
|
147
|
+
success: true,
|
|
148
|
+
message: `Went back to: ${backResult.title} (${backResult.url})`,
|
|
149
|
+
};
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "forward": {
|
|
153
|
+
const fwdResult = await browser.goForward();
|
|
154
|
+
result = {
|
|
155
|
+
success: true,
|
|
156
|
+
message: `Went forward to: ${fwdResult.title} (${fwdResult.url})`,
|
|
157
|
+
};
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: result.message + (result.success ? "" : " [FAILED]"),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
isError: !result.success,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return toolError(`Action failed: ${err}`);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
server.tool("screenshot", "Take a screenshot of the current page viewport. Returns a PNG image. Use this when the accessibility tree alone is insufficient to understand the page layout or to verify visual state.", {}, async () => {
|
|
176
|
+
try {
|
|
177
|
+
const buf = await browser.screenshot();
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "image",
|
|
182
|
+
data: buf.toString("base64"),
|
|
183
|
+
mimeType: "image/png",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
return toolError(`Screenshot failed: ${err}`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
server.tool("get_text", "Extract all visible text content from the current page. Useful for reading articles, documentation, or any text-heavy page without the structural markup of observe.", {}, async () => {
|
|
193
|
+
try {
|
|
194
|
+
const page = await browser.getPage();
|
|
195
|
+
const url = page.url();
|
|
196
|
+
const [title, text] = await Promise.all([
|
|
197
|
+
page.title(),
|
|
198
|
+
page.innerText("body"),
|
|
199
|
+
]);
|
|
200
|
+
const trimmed = text.length > 50000
|
|
201
|
+
? text.slice(0, 50000) + "\n\n[...truncated]"
|
|
202
|
+
: text;
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{ type: "text", text: formatPageHeader(title, url) + trimmed },
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
return toolError(`get_text failed: ${err}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
async function main() {
|
|
214
|
+
const transport = new StdioServerTransport();
|
|
215
|
+
await server.connect(transport);
|
|
216
|
+
const shutdown = async () => {
|
|
217
|
+
await browser.close();
|
|
218
|
+
process.exit(0);
|
|
219
|
+
};
|
|
220
|
+
process.on("SIGINT", shutdown);
|
|
221
|
+
process.on("SIGTERM", shutdown);
|
|
222
|
+
}
|
|
223
|
+
main().catch((err) => {
|
|
224
|
+
console.error("Scuttle failed to start:", err);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
});
|
|
227
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scuttle-browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser bridge for LLMs — exposes web pages as annotated accessibility trees via MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"scuttle": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsc --watch",
|
|
19
|
+
"test": "node test-smoke.mjs",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"postinstall": "npx playwright install chromium || echo 'Run: npx playwright install chromium'"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"browser",
|
|
27
|
+
"web-browsing",
|
|
28
|
+
"llm",
|
|
29
|
+
"ai-agent",
|
|
30
|
+
"accessibility",
|
|
31
|
+
"playwright",
|
|
32
|
+
"automation"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/axelkrantz/scuttle"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
44
|
+
"playwright": "^1.52.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.15.3",
|
|
48
|
+
"typescript": "^5.8.3"
|
|
49
|
+
}
|
|
50
|
+
}
|