@haseebeqx/pi-safari 0.0.0-stage → 0.2.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 +180 -2
- package/package.json +40 -4
- package/src/backend-schemas.ts +232 -0
- package/src/extraction.ts +26 -0
- package/src/index.ts +143 -0
- package/src/inspection.ts +32 -0
- package/src/mcp.ts +117 -0
- package/src/permissions.ts +100 -0
- package/src/safari.ts +305 -0
- package/src/search.ts +51 -0
- package/src/version.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haseeb Annadamban
|
|
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
CHANGED
|
@@ -1,3 +1,181 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-safari
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Safari browsing and automation tools for [Pi](https://github.com/badlogic/pi-mono), using Apple’s local Safari MCP server (`/usr/bin/safaridriver --mcp`). No separate MCP configuration or build step is needed.
|
|
4
|
+
|
|
5
|
+
## Install and set up
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> Already have an MCP-capable client? [You can also use Safari MCP directly without this extension](#use-safari-mcp-directly-without-this-extension).
|
|
9
|
+
|
|
10
|
+
Requires **macOS, Safari 27+, Node 22.19+, and Pi**.
|
|
11
|
+
|
|
12
|
+
1. In **Safari → Settings → Advanced**, enable **Show features for web developers**.
|
|
13
|
+
2. In **Safari → Settings → Developer**, enable **Allow remote automation and external agents**. Complete any Safari connection approval yourself; the extension does not change these settings.
|
|
14
|
+
3. Install and reload/restart Pi:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pi install npm:@haseebeqx/pi-safari
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For a local checkout, use `pi install /absolute/path/to/pi-safari`, or try it temporarily from the repository with `pi -e ./src/index.ts`. Pi supplies the host dependencies; development uses Pi 1.2.0.
|
|
21
|
+
|
|
22
|
+
Tools operate on **Safari’s automation session**. Do not assume your normal Safari tabs, cookies, or logins are available. See Apple’s [Safari agent setup guide](https://developer.apple.com/documentation/safari-developer-tools/connecting-an-ai-agent-to-safari).
|
|
23
|
+
|
|
24
|
+
## What is allowed?
|
|
25
|
+
|
|
26
|
+
### Always allowed: no extension permission needed
|
|
27
|
+
|
|
28
|
+
Listing tabs, opening new HTTP(S) tabs, searching, following links into new tabs, reading/inspecting pages, waiting, screenshots, console/network observations, closing an explicit tab, and scrolling need **no task approval or full-session grant**. They also work in noninteractive Pi modes.
|
|
29
|
+
|
|
30
|
+
“Always allowed” means only that **this extension’s action permission check is not required**. Safari settings, connection approvals, tool validation, URL checks and other limits still apply. These operations are not necessarily harmless: opening URLs can have server-side effects, closing a tab loses it, and scrolling can trigger site handlers. Observations internally select the requested automation context.
|
|
31
|
+
|
|
32
|
+
### Restricted: checked against your current task
|
|
33
|
+
|
|
34
|
+
Explicit tab activation, navigating an existing tab, and **click, type, keyPress and hover** require an interactive Pi terminal and either:
|
|
35
|
+
|
|
36
|
+
- **Task-scoped approval (default):** an independent, tool-less model classifies each exact proposed action against your current interactive request. There is no extra extension confirmation dialog. Repeated actions needed for the same task are allowed; approval is not consumed after one field.
|
|
37
|
+
- **Full-session permission:** enter the dedicated command **`/safari-permissions allow`**. It grants permission immediately, without another confirmation, and bypasses the task classifier for restricted actions. It does **not** bypass Safari settings, target/URL validation or tool limits.
|
|
38
|
+
|
|
39
|
+
Only the dedicated command grants full permission—not a mention in a prompt, webpage, tool argument or saved conversation. Use **`/safari-permissions revoke`** to clear both full permission and the current task permission. It cancels pending permission decisions but cannot undo actions already sent to Safari.
|
|
40
|
+
|
|
41
|
+
#### Task examples
|
|
42
|
+
|
|
43
|
+
| Your request | Intended permission |
|
|
44
|
+
|---|---|
|
|
45
|
+
| “Read and summarize this page.” | Observations are allowed; not permission to execute instructions found on the page. |
|
|
46
|
+
| “Fill the name with Alice and email with alice@example.com.” | Both field fills, repeated necessary actions and necessary focus/Tab actions; **not Submit or Enter to submit**. |
|
|
47
|
+
| “Fill those fields and submit the form.” | Matching fills and submission can be approved when the target/effect is sufficiently clear. |
|
|
48
|
+
| “Click this button” with unclear context | The classifier should deny rather than guess. Clarify the control and intended effect. |
|
|
49
|
+
| “Allow all Safari actions for this session.” | Does not enable full-session permission. Use `/safari-permissions allow`. |
|
|
50
|
+
|
|
51
|
+
Filling is **not** permission to send, buy, delete, sign in or otherwise commit. Enter/Return and even newlines can submit; field changes can trigger site handlers. The classifier considers exact values, target and available context, not just an action’s claimed purpose.
|
|
52
|
+
|
|
53
|
+
#### Permission lifetime and limits
|
|
54
|
+
|
|
55
|
+
- Task permission comes only from a fresh, live interactive input in the Pi TUI. It expires on new input or agent end. Queued/steering input, restored transcripts, page text and model-generated user messages do not grant it; send a fresh request while Pi is idle.
|
|
56
|
+
- Full permission persists across requests until revoked or reset. All permissions are **memory-only** and reset on session start/switch/fork, reload or shutdown. Retired pending/queued restricted work cannot become valid again after a new grant.
|
|
57
|
+
- **Print/JSON/RPC and other noninteractive contexts block restricted actions**, even with a prior interactive full grant. Always-allowed tools and scroll remain available.
|
|
58
|
+
- Each task check adds model latency/cost. It uses Pi’s current model with request-time authentication, no tools and no conversation history. The current request, proposed action/values and any matching cached snapshot context go to the configured provider. Missing model/authentication, errors, malformed replies, uncertainty or a **15-second classifier timeout** block the action.
|
|
59
|
+
- **Semantic classification is probabilistic, not a deterministic security boundary.** It can misunderstand intent or adversarial text. Cached context may be stale or absent; it does not prove node identity or event-handler effects. Review sensitive operations. This extension is not a sandbox against other installed extensions or Pi tools; earlier extensions can transform input events.
|
|
60
|
+
|
|
61
|
+
## Tool reference
|
|
62
|
+
|
|
63
|
+
Use the exact **`targetId` and `epoch`** returned by `safari_tabs` or `safari_open`; never invent or reuse stale handles. Below, **target** means both fields. `?` marks optional parameters. Tools reading page content return untrusted data.
|
|
64
|
+
|
|
65
|
+
### Always allowed tools
|
|
66
|
+
|
|
67
|
+
| Tool | Inputs besides target | Behavior / limits |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `safari_tabs` | No target or inputs | Lists up to 100 automation tabs, with handles and available URL/title; no page bodies. |
|
|
70
|
+
| `safari_open` | No target; `url` | Opens a **new** HTTP(S) tab, never an existing one. Exact normalized URL readiness required; redirects fail closed. Leaves the tab open. |
|
|
71
|
+
| `safari_search` | No target; `query`, `maxResults?`, `region?`, `time?` | Searches DuckDuckGo HTML in its own new tab; returns ranked unique title/URL/snippet results and cleanup outcome, not a reusable target. |
|
|
72
|
+
| `safari_read` | `maxChars?`, `maxLinks?` | Visible body text and link hrefs. Defaults 8000 characters / 20 links; maxima 12000 / 40. |
|
|
73
|
+
| `safari_follow_link` | `expectedUrl`, `selector` | Extracts exactly one visible matching `a[href]` and opens its HTTP(S) URL in a **new tab**. No click handlers, form submission or source-tab navigation. Use the URL from a prior read. |
|
|
74
|
+
| `safari_snapshot` | `entirePage?` | Native WebKit text tree with interactive numeric UIDs, accessibility attributes and rectangles; viewport by default, main frame only. Large extractions may return an artifact path. |
|
|
75
|
+
| `safari_find` | `selector?`, `text?`, `limit?` | Visible main-frame DOM descriptors, **not native UIDs**. At least selector or text required; combined with AND. Default 20 matches, maximum 50. |
|
|
76
|
+
| `safari_inspect` | `selector` | Exactly one visible match: bounded attributes, approximate name, state and rectangle. Rejects ambiguous/truncated matches; no input values, HTML or computed-style dump. |
|
|
77
|
+
| `safari_wait` | `expectedUrl`, `selector?`, `text?`, `timeoutMs?` | Polls visible presence, not absence. Same query rules as find; default 5000 ms, range 0–10000. URL drift fails. |
|
|
78
|
+
| `safari_screenshot` | `fullPage?` | Viewport/full-page PNG as a backend temporary **path**, not an inline image. Does not read the file automatically. |
|
|
79
|
+
| `safari_console` | `limit?` | Buffered messages without clearing; default/maximum 100 entries. |
|
|
80
|
+
| `safari_network` | `urlSubstring?` | Buffered Inspector request summaries without clearing; filter up to 500 characters. No bodies or interception; historical coverage is not guaranteed. |
|
|
81
|
+
| `safari_close` | None | Closes exactly the identified tab, or reports `already-absent`. Rejects stale epochs; never retries or guesses unidentified tabs. |
|
|
82
|
+
| `safari_interact` **scroll only** | `expectedUrl`, `action` | One native scroll with bounded deltas; see interaction format below. |
|
|
83
|
+
|
|
84
|
+
### Restricted tools/actions
|
|
85
|
+
|
|
86
|
+
| Tool | Inputs besides target | Behavior |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `safari_activate` | None | Explicitly selects an automation target; does not guarantee foreground OS window focus. |
|
|
89
|
+
| `safari_navigate` | `expectedUrl`, `url` | Navigates an existing target once after checking its source URL. HTTP(S) only. Destination redirects are rejected **after navigation**, with no rollback. |
|
|
90
|
+
| `safari_interact` **click, type, keyPress, hover** | `expectedUrl`, `action` | One native action, checked against the current task unless full permission is enabled. May submit or navigate; never automatically retried. |
|
|
91
|
+
|
|
92
|
+
### Interaction format
|
|
93
|
+
|
|
94
|
+
First get a fresh `safari_snapshot`. Use its numeric UID—not a CSS selector, DOM ID or descriptor from find/inspect:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"targetId": "page-…",
|
|
99
|
+
"epoch": "…",
|
|
100
|
+
"expectedUrl": "https://example.com/form",
|
|
101
|
+
"action": {
|
|
102
|
+
"type": "type",
|
|
103
|
+
"purpose": "Fill the requested name",
|
|
104
|
+
"node": "42",
|
|
105
|
+
"value": "Alice",
|
|
106
|
+
"replaceAll": true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
All interactions require `purpose` (1–300 characters) and the exact current `expectedUrl`.
|
|
112
|
+
|
|
113
|
+
- `click`, `type`, `hover`: require `node`, a numeric snapshot UID (1–20 digits).
|
|
114
|
+
- `type`: `value` up to 4000 characters; `replaceAll: true` fills rather than appends. No automatic Return after typing.
|
|
115
|
+
- `keyPress`: `value` is a nonempty Safari backend key name, up to 100 characters; key spelling is backend-controlled.
|
|
116
|
+
- `scroll`: `action: {"type":"scroll","purpose":"Read further down","scrollDelta":{"x":0,"y":600}}`. Both deltas must be finite and within ±10000 CSS pixels. No permission check.
|
|
117
|
+
|
|
118
|
+
### Search options
|
|
119
|
+
|
|
120
|
+
`query` is 1–500 characters with no control characters. `maxResults` defaults to 5, range 1–10. `region` defaults to `wt-wt`; choices are `wt-wt`, `us-en`, `uk-en`, `ca-en`, `au-en`, `de-de`, `fr-fr`, `in-en`, `jp-jp`. It is a provider preference, not a strict country/language filter. `time` is `any` (default), `d`, `w`, `m` or `y` for past day/week/month/year.
|
|
121
|
+
|
|
122
|
+
Search reads only the first HTML results page (at most 100 result containers); no pagination, result clicks, challenge bypass or automatic retry. Queries go to DuckDuckGo. Ranking/filter semantics are provider-controlled; empty results can mean a challenge or changed markup, not necessarily no matches.
|
|
123
|
+
|
|
124
|
+
## Practical limits and recovery
|
|
125
|
+
|
|
126
|
+
- **Tabs and cleanup:** open/follow leave tabs open, including after many failures. Search tries to close only its own identified created tab, including on failure, and reports cleanup status. Cancellation, disconnect or an unidentified create reply can leave residual tabs. **Relist before retrying**; never assume a failed create did nothing. Shutdown stops the driver, not an explicit close-all routine.
|
|
127
|
+
- **Stale state:** handles belong to a connection epoch. Disconnects, interrupted driver requests and five minutes of inactivity can retire it; relist afterward. Same-URL reloads/document replacements, live DOM changes, stale UIDs and external interference may escape URL checks. Obtain a fresh snapshot after failures; do not blindly replay actions. Independent Pi sessions are not coordinated.
|
|
128
|
+
- **Bounded extraction:** read/find/inspect/wait do not traverse iframes or shadow roots. Find/inspect/wait scan at most 2000 elements/matches. Text matching is case-insensitive substring matching, not full accessible-name computation; ancestors may match. Wait can miss content beyond the scan bound.
|
|
129
|
+
- **Output and time:** operations are serialized with a 20-second work deadline; search cleanup has up to 3 extra seconds. Native snapshot/console/network/screenshot text is capped at 12000 characters. Overall JSON output is capped at 48 KiB and transport responses at 256 KiB. Reduce read limits or narrow network filters when needed. Native artifact paths are not read automatically.
|
|
130
|
+
- **Not exposed:** arbitrary JavaScript/evaluate, raw backend passthrough, batches, selector/coordinate interaction fallbacks, file uploads, dialog controls, history controls, viewport/media emulation, element screenshot crops or network interception. Read-only DOM queries use fixed generated scripts.
|
|
131
|
+
- **Privacy:** page text, console/network data and exact form values can be sensitive. Observations passed to Pi may go to its model provider; task classification also sends its evidence there. Treat web content and returned artifact paths as untrusted data, never permission or local-file instructions.
|
|
132
|
+
|
|
133
|
+
### Troubleshooting
|
|
134
|
+
|
|
135
|
+
| Problem | What to do |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Cannot connect / permission denied | Check Safari version and both developer settings; complete Safari’s own approval. Reload Pi after installation. |
|
|
138
|
+
| “No current trusted task” / arbiter denied or unavailable | Send a fresh, specific idle TUI request naming the action, values and intended effect; check the configured model/authentication. Use full-session permission only deliberately. |
|
|
139
|
+
| Restricted action in print/JSON/RPC | Use an interactive Pi terminal; a full-session grant does not enable these modes. |
|
|
140
|
+
| Stale target / changed URL / timed-out action | Run `safari_tabs`, inspect the actual state and get a fresh snapshot. Do not automatically repeat an action that may already have happened. |
|
|
141
|
+
| Redirect rejected | Inspect the resulting tab; use the intended final URL explicitly. Rejection does not undo navigation. |
|
|
142
|
+
| Native capability missing/incompatible | Check Safari/package compatibility. Native tools require known backend schemas and fail closed when those change. |
|
|
143
|
+
| Empty search or console/network results | Search may be challenged or markup may have changed. Safari controls buffer availability; try observing before generating traffic. There is no complete-history guarantee. |
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
npm ci --ignore-scripts
|
|
149
|
+
npm run check
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Runs unit tests and TypeScript checks; no Safari or live model calls. Mocked permission tests do not establish real-model authorization accuracy. `npm run test:live -- --run` explicitly opts into Safari fixture tests and a benign DuckDuckGo search; it does not verify Pi’s permission UX. See [VERIFICATION.md](VERIFICATION.md) in the checkout for recorded results and gaps.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
[MIT](LICENSE).
|
|
157
|
+
|
|
158
|
+
## Use Safari MCP directly (without this extension)
|
|
159
|
+
|
|
160
|
+
In another MCP-capable client, use Apple’s built-in server over **stdio**—not a network endpoint. You do not need Pi, Node, or this npm package for the Safari server itself. Use **macOS with Safari 27+**, enable both Safari developer settings in [Install and set up](#install-and-set-up), and complete Safari’s connection approvals yourself.
|
|
161
|
+
|
|
162
|
+
For clients supporting the common `mcpServers` JSON format, add:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"mcpServers": {
|
|
167
|
+
"safari": {
|
|
168
|
+
"command": "/usr/bin/safaridriver",
|
|
169
|
+
"args": ["--mcp"]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Configuration format and file location are client-dependent. Reload/restart your client as needed; it launches the process and manages stdio, so do not start a separate server manually. Apple’s [setup guide](https://developer.apple.com/documentation/safari-developer-tools/connecting-an-ai-agent-to-safari) also documents this Claude Code command:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
claude mcp add safari-mcp -- "/usr/bin/safaridriver" --mcp
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Direct use exposes **Apple’s native MCP tools**, not this extension’s `safari_*` wrappers. The extension’s permission classifier, `/safari-permissions` commands, bounds and safety checks do **not** apply. Your client controls tool approvals; Safari’s own permissions still apply. Capabilities and safety behavior differ from this extension. Do not assume normal Safari tabs or logins are available, and remember that page data sent to your client may reach its model provider.
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haseebeqx/pi-safari",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Safari automation tools for Pi using Apple's built-in local MCP server",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./src/index.ts"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
21
|
+
"typebox": "*"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.0.0",
|
|
25
|
+
"typescript": "^5.9.0",
|
|
26
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
27
|
+
"typebox": "1.3.27"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
31
|
+
"test:live": "node --experimental-strip-types scripts/live-smoke.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"check": "npm test && npm run typecheck",
|
|
34
|
+
"prepublishOnly": "npm run check"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.19"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Verified by initialize + tools/list only, local Safari MCP 1.0.0. No browser actions.
|
|
2
|
+
// Conservative exact schema matching: incompatible optional capabilities fail closed.
|
|
3
|
+
export const BACKEND_SCHEMAS = {
|
|
4
|
+
"browser_console_messages": {
|
|
5
|
+
"properties": {
|
|
6
|
+
"clear": {
|
|
7
|
+
"type": "boolean"
|
|
8
|
+
},
|
|
9
|
+
"level_filter": {
|
|
10
|
+
"items": {
|
|
11
|
+
"enum": [
|
|
12
|
+
"debug",
|
|
13
|
+
"info",
|
|
14
|
+
"warn",
|
|
15
|
+
"error"
|
|
16
|
+
],
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"type": "array"
|
|
20
|
+
},
|
|
21
|
+
"limit": {
|
|
22
|
+
"type": "integer"
|
|
23
|
+
},
|
|
24
|
+
"tab_handle": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"type": "object"
|
|
29
|
+
},
|
|
30
|
+
"get_page_content": {
|
|
31
|
+
"properties": {
|
|
32
|
+
"format": {
|
|
33
|
+
"enum": [
|
|
34
|
+
"markdown",
|
|
35
|
+
"text",
|
|
36
|
+
"textTree",
|
|
37
|
+
"json",
|
|
38
|
+
"html",
|
|
39
|
+
"plainText"
|
|
40
|
+
],
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"includeAccessibilityAttributes": {
|
|
44
|
+
"type": "boolean"
|
|
45
|
+
},
|
|
46
|
+
"includeEventListeners": {
|
|
47
|
+
"type": "boolean"
|
|
48
|
+
},
|
|
49
|
+
"includeRects": {
|
|
50
|
+
"type": "boolean"
|
|
51
|
+
},
|
|
52
|
+
"includeSelectOptions": {
|
|
53
|
+
"type": "boolean"
|
|
54
|
+
},
|
|
55
|
+
"includeSubframes": {
|
|
56
|
+
"type": "boolean"
|
|
57
|
+
},
|
|
58
|
+
"includeURLs": {
|
|
59
|
+
"type": "boolean"
|
|
60
|
+
},
|
|
61
|
+
"maxWordsPerParagraph": {
|
|
62
|
+
"type": "integer"
|
|
63
|
+
},
|
|
64
|
+
"nodeIds": {
|
|
65
|
+
"enum": [
|
|
66
|
+
"none",
|
|
67
|
+
"editable",
|
|
68
|
+
"interactive",
|
|
69
|
+
"allContainers"
|
|
70
|
+
],
|
|
71
|
+
"type": "string"
|
|
72
|
+
},
|
|
73
|
+
"region": {
|
|
74
|
+
"enum": [
|
|
75
|
+
"entire_page",
|
|
76
|
+
"viewport"
|
|
77
|
+
],
|
|
78
|
+
"type": "string"
|
|
79
|
+
},
|
|
80
|
+
"savePath": {
|
|
81
|
+
"type": "string"
|
|
82
|
+
},
|
|
83
|
+
"shortenURLs": {
|
|
84
|
+
"type": "boolean"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"type": "object"
|
|
88
|
+
},
|
|
89
|
+
"list_network_requests": {
|
|
90
|
+
"properties": {
|
|
91
|
+
"clear": {
|
|
92
|
+
"type": "boolean"
|
|
93
|
+
},
|
|
94
|
+
"filter": {
|
|
95
|
+
"properties": {
|
|
96
|
+
"method": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
},
|
|
99
|
+
"status_max": {
|
|
100
|
+
"type": "integer"
|
|
101
|
+
},
|
|
102
|
+
"status_min": {
|
|
103
|
+
"type": "integer"
|
|
104
|
+
},
|
|
105
|
+
"url_substring": {
|
|
106
|
+
"type": "string"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"type": "object"
|
|
110
|
+
},
|
|
111
|
+
"since": {
|
|
112
|
+
"type": "string"
|
|
113
|
+
},
|
|
114
|
+
"tab_handle": {
|
|
115
|
+
"type": "string"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"type": "object"
|
|
119
|
+
},
|
|
120
|
+
"navigate_to_url": {
|
|
121
|
+
"properties": {
|
|
122
|
+
"tab_uuid": {
|
|
123
|
+
"type": "string"
|
|
124
|
+
},
|
|
125
|
+
"url": {
|
|
126
|
+
"type": "string"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"required": [
|
|
130
|
+
"url"
|
|
131
|
+
],
|
|
132
|
+
"type": "object"
|
|
133
|
+
},
|
|
134
|
+
"page_interactions": {
|
|
135
|
+
"properties": {
|
|
136
|
+
"fullText": {
|
|
137
|
+
"type": "boolean"
|
|
138
|
+
},
|
|
139
|
+
"interactions": {
|
|
140
|
+
"items": {
|
|
141
|
+
"properties": {
|
|
142
|
+
"node": {
|
|
143
|
+
"type": "string"
|
|
144
|
+
},
|
|
145
|
+
"point": {
|
|
146
|
+
"properties": {
|
|
147
|
+
"x": {
|
|
148
|
+
"type": "number"
|
|
149
|
+
},
|
|
150
|
+
"y": {
|
|
151
|
+
"type": "number"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"type": "object"
|
|
155
|
+
},
|
|
156
|
+
"pressReturn": {
|
|
157
|
+
"type": "boolean"
|
|
158
|
+
},
|
|
159
|
+
"purpose": {
|
|
160
|
+
"type": "string"
|
|
161
|
+
},
|
|
162
|
+
"replaceAll": {
|
|
163
|
+
"type": "boolean"
|
|
164
|
+
},
|
|
165
|
+
"scrollDelta": {
|
|
166
|
+
"properties": {
|
|
167
|
+
"x": {
|
|
168
|
+
"type": "number"
|
|
169
|
+
},
|
|
170
|
+
"y": {
|
|
171
|
+
"type": "number"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"type": "object"
|
|
175
|
+
},
|
|
176
|
+
"scrollToVisible": {
|
|
177
|
+
"type": "boolean"
|
|
178
|
+
},
|
|
179
|
+
"text": {
|
|
180
|
+
"type": "string"
|
|
181
|
+
},
|
|
182
|
+
"type": {
|
|
183
|
+
"enum": [
|
|
184
|
+
"click",
|
|
185
|
+
"type",
|
|
186
|
+
"keyPress",
|
|
187
|
+
"scroll",
|
|
188
|
+
"selectText",
|
|
189
|
+
"selectMenuItem",
|
|
190
|
+
"hover",
|
|
191
|
+
"highlightText"
|
|
192
|
+
],
|
|
193
|
+
"type": "string"
|
|
194
|
+
},
|
|
195
|
+
"value": {
|
|
196
|
+
"type": "string"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
"required": [
|
|
200
|
+
"purpose",
|
|
201
|
+
"type"
|
|
202
|
+
],
|
|
203
|
+
"type": "object"
|
|
204
|
+
},
|
|
205
|
+
"type": "array"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"required": [
|
|
209
|
+
"interactions"
|
|
210
|
+
],
|
|
211
|
+
"type": "object"
|
|
212
|
+
},
|
|
213
|
+
"screenshot": {
|
|
214
|
+
"properties": {
|
|
215
|
+
"full_page": {
|
|
216
|
+
"type": "boolean"
|
|
217
|
+
},
|
|
218
|
+
"node": {
|
|
219
|
+
"type": "string"
|
|
220
|
+
},
|
|
221
|
+
"savePath": {
|
|
222
|
+
"type": "string"
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"type": "object"
|
|
226
|
+
}
|
|
227
|
+
} as const;
|
|
228
|
+
|
|
229
|
+
export function schemaKey(value: unknown): string {
|
|
230
|
+
const clean = (v: any): any => Array.isArray(v) ? v.map(clean) : v && typeof v === 'object' ? Object.fromEntries(Object.keys(v).filter(k => k !== 'description').sort().map(k => [k, clean(v[k])])) : v;
|
|
231
|
+
return JSON.stringify(clean(value));
|
|
232
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const MAX_OUTPUT = 48 * 1024;
|
|
2
|
+
export function bound(value: unknown): string {
|
|
3
|
+
const text = JSON.stringify(value);
|
|
4
|
+
if (Buffer.byteLength(text) > MAX_OUTPUT) throw new Error('Safari output exceeded 48 KiB; request smaller limits.');
|
|
5
|
+
return text;
|
|
6
|
+
}
|
|
7
|
+
export function webURL(url: string): string {
|
|
8
|
+
const u = new URL(url);
|
|
9
|
+
if (!['http:', 'https:'].includes(u.protocol) || u.username || u.password || url.length > 8192) throw new Error('Only HTTP(S) URLs without embedded credentials are allowed.');
|
|
10
|
+
return u.href;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readyJS(expectedUrl: string): string {
|
|
14
|
+
return `location.href===${JSON.stringify(expectedUrl)} && document.readyState==='complete'`;
|
|
15
|
+
}
|
|
16
|
+
// This script only extracts data. It never clicks, dispatches events, sets markers,
|
|
17
|
+
// or navigates the source context, regardless of its provenance or replacement.
|
|
18
|
+
export function linkJS(selector: string, expectedUrl: string): string {
|
|
19
|
+
if (selector.length < 1 || selector.length > 500) throw new Error('CSS selector must be 1..500 characters.');
|
|
20
|
+
webURL(expectedUrl);
|
|
21
|
+
return `JSON.stringify((()=>{if(location.href!==${JSON.stringify(expectedUrl)})throw Error('Page URL changed; read again');const es=document.querySelectorAll(${JSON.stringify(selector)});if(es.length!==1)throw Error('Selector must match exactly one element');const e=es[0];if(!e.matches('a[href]'))throw Error('Selector must match a link');if(!e.getClientRects().length||getComputedStyle(e).visibility!=='visible'||getComputedStyle(e).display==='none')throw Error('Link hidden');const u=new URL(e.href);if(!/^https?:$/.test(u.protocol)||u.username||u.password)throw Error('Only HTTP(S) links without credentials are allowed');return {href:u.href}})())`;
|
|
22
|
+
}
|
|
23
|
+
export function readJS(chars: number, links: number): string {
|
|
24
|
+
if (!Number.isInteger(chars) || chars < 1 || chars > 12000 || !Number.isInteger(links) || links < 0 || links > 40) throw new Error('maxChars must be 1..12000; maxLinks must be 0..40.');
|
|
25
|
+
return `JSON.stringify((()=>{const text=document.body?.innerText||''; const links=Array.from(document.querySelectorAll('a[href]')).filter(a=>a.getClientRects().length&&getComputedStyle(a).visibility==='visible'&&getComputedStyle(a).display!=='none'); return {url:location.href.slice(0,2048),title:document.title.slice(0,300),readyState:document.readyState,text:text.slice(0,${chars}),textTruncated:text.length>${chars},links:links.slice(0,${links}).map((a,index)=>({index,text:(a.innerText||a.textContent||'').slice(0,120),href:a.href.slice(0,1024)})),linksTruncated:links.length>${links}}})())`;
|
|
26
|
+
}
|