@underactive/pi-topping-web-tools 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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2026-08-17
6
+
7
+ ### Added
8
+
9
+ - Initial bundle: `fetch_markdown` and `web_browser` pi extensions with a shared preapproved-host allowlist, merged from the standalone `pi-fetch-markdown-tool`, `pi-web-browser-tool`, and `pi-web-permissions` packages.
10
+ - `web_browser`: `scroll` action (selector-based `scrollIntoView` or pixel-delta mouse wheel).
11
+ - `web_browser`: `drag` action for drag-and-drop between two selectors.
12
+ - `web_browser`: `upload_file` action to set files on an `<input type=file>`.
13
+ - `web_browser`: `set_dialog_behavior` and `get_dialog_logs` actions to configure and inspect handling of JS `alert`/`confirm`/`prompt` dialogs (dismissed by default).
14
+ - `web_browser`: `list_tabs` and `switch_tab` actions for multi-tab sessions; popups (`target=_blank`, `window.open`) automatically become the active tab.
15
+ - `web_browser`: `frame` parameter on selector-based actions (click, type, hover, select_option, screenshot, get_text, wait_for, get_accessibility_snapshot, scroll, upload_file, drag) to target elements inside an `<iframe>`.
16
+ - `web_browser`: selectors now also accept Playwright's `role=` and `text=` selector engines, not just CSS.
17
+ - `fetch_markdown`: `offset` parameter to paginate past the 100K-character truncation limit; truncated responses report the offset to continue from.
18
+ - `fetch_markdown`: readability article extraction (`@mozilla/readability` + `linkedom`) on HTML pages by default, stripping nav/footer boilerplate; the page title is restored as the leading heading, `raw: true` opts out, and non-article pages fall back to full-page conversion.
19
+ - `fetch_markdown`: conditional revalidation of expired cache entries via `If-None-Match`/`If-Modified-Since`; a 304 reuses the cached markdown and refreshes the TTL.
20
+ - `pdf_extract`: new tool extracting the text layer from remote (`url`) or local (`path`) PDFs via `unpdf`, which `fetch_markdown` and `web_fetch` treat as undisplayable binary content. Output carries `--- Page N ---` markers, `pages` selects a range (`"3"`, `"1-5"`, `"1,4,7-9"`), `offset` paginates past the 100K-character limit, and results are wrapped in `<untrusted-content>`. Local paths resolve through `realpath` before permission checks so symlinks cannot escape the working directory, and extraction is bounded by 25MB, 2000 pages, and a 30s timeout. Adds the `/clear-pdf-extract-cache` command.
21
+
22
+ ### Changed
23
+
24
+ - `fetch_markdown`: raised the maximum HTTP body size from 10 MB to 25 MB so very large single-page documents (e.g. the WHATWG HTML spec) can be fetched and paginated with `offset`.
25
+ - `fetch_markdown`: renamed the internal `sliceMarkdown` helper to `sliceContent` and the `getWithPermittedRedirects` header parameter to `extraHeaders`, so `pdf_extract` can reuse the same content-windowing and redirect logic.
26
+
27
+ ### Fixed
28
+
29
+ - `fetch_markdown`: tool errors rendered as `Received NaNMB (undefined undefined)`; error results now fall back to the error text.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eric Sison
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,175 @@
1
+ # @underactive/pi-topping-web-tools
2
+
3
+ Web tools bundle for [pi](https://github.com/earendil-works/pi): keyless URL-to-markdown fetching, PDF text extraction, and headless Playwright browser automation, sharing one preapproved-host allowlist and permission model.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:@underactive/pi-topping-web-tools
9
+ ```
10
+
11
+ The `web_browser` tool additionally requires Playwright's Chromium (~150MB):
12
+
13
+ ```bash
14
+ npx playwright install chromium
15
+ ```
16
+
17
+ ## Why this extension
18
+
19
+ Web work has two distinct needs: clean context from public sources and browser-level verification of the live experience. This bundle gives pi both, so an agent can move from a documentation question to an interactive check without stitching together unrelated extensions. Public-page retrieval is keyless, while browser automation remains available for the cases where a static fetch cannot answer the question.
20
+
21
+ - **Keep research focused.** `fetch_markdown` converts public documentation, READMEs, and articles into Markdown, extracting the primary article instead of a page's surrounding navigation. Its cache makes iterative questions against the same source faster.
22
+ - **Reach PDF-only sources.** `pdf_extract` reads the text layer of specifications, datasheets, and papers that markdown fetchers treat as undisplayable binary content, and that pi's `read` tool cannot parse.
23
+ - **Validate the rendered product.** `web_browser` runs a fresh headless Chromium context for client-rendered pages, local development servers, and user journeys that static HTML cannot represent. It can inspect the accessibility tree, interact with controls, and surface console and failed-network diagnostics.
24
+ - **Use the appropriate tool, not the heavier tool by default.** Fetch static public material first; move to browser automation when JavaScript, visual state, or user input is part of the question.
25
+ - **Keep control over access.** The tools share a host allowlist and confirmation workflow; fetched page text is explicitly marked as untrusted, and browser sessions do not reuse the system browser profile.
26
+
27
+ ## Examples
28
+
29
+ Give pi a task like the following; the extension supplies the web capabilities needed to complete it.
30
+
31
+ ### Research a dependency before changing code
32
+
33
+ > Read `https://react.dev/reference/react/useEffect`. Summarize the cleanup behavior, identify the parts relevant to subscriptions, and cite the sections I should review before changing this component.
34
+
35
+ `fetch_markdown` returns the page as focused Markdown, making it useful for documentation research without spending context on site chrome. Repeated follow-up questions can reuse the cached response.
36
+
37
+ ### Verify a local user journey
38
+
39
+ > Open `http://localhost:3000`, set a 390×844 viewport, add an item to the cart, and report any failed network requests or console errors. Save a screenshot of the final state.
40
+
41
+ `web_browser` can drive the local application as a user would, then collect the diagnostics that explain a failed interaction rather than only reporting that it failed.
42
+
43
+ ### Investigate a JavaScript-heavy page
44
+
45
+ > Review the migration guide at `https://example.com/migration`. If the important content is not present in the static page, inspect the live page in the browser and return the breaking changes that affect this project.
46
+
47
+ Start with `fetch_markdown` for efficient static content; use `web_browser` when the page depends on client-side rendering or requires interaction. The two tools let the agent choose the lowest-cost path that still produces reliable evidence.
48
+
49
+ ## Tools
50
+
51
+ | Tool | Description |
52
+ |------|-------------|
53
+ | `fetch_markdown` | Fetch a public URL and return its content as markdown (cached, keyless) |
54
+ | `pdf_extract` | Extract the text layer from a remote or local PDF, with page markers |
55
+ | `web_browser` | Headless Chromium automation — navigate, click, type, screenshot, evaluate, logs, cookies |
56
+
57
+ ## `fetch_markdown`
58
+
59
+ Complements pi's built-in search-backed `web_fetch` with a lightweight, keyless fetch path for public documentation, READMEs, and articles:
60
+
61
+ - **Native fetch** — no API keys, no provider routing
62
+ - **HTML → markdown** — [turndown](https://github.com/mixmark-io/turndown) conversion for readable agent context
63
+ - **Readability extraction** — [@mozilla/readability](https://github.com/mozilla/readability) (via [linkedom](https://github.com/WebReflection/linkedom)) strips nav/footer/sidebar boilerplate from article pages by default; pass `raw: true` for the full page. Falls back to full-page conversion when a page doesn't look like an article
64
+ - **15-minute LRU cache with revalidation** — faster repeat lookups; expired entries are revalidated with `If-None-Match`/`If-Modified-Since` when the server sent `ETag`/`Last-Modified`, so unchanged pages cost a 304 instead of a full download
65
+ - **Strict redirect handling** — redirects that change host, scheme, or port, or that leave a preapproved path, return the redirect URL instead of being followed; call again to follow (`www.` prefixes are treated as the same host)
66
+ - **Host confirmation** — prompts for non-preapproved hosts (session-scoped allow/deny)
67
+
68
+ Usage notes:
69
+
70
+ - HTTP URLs are upgraded to HTTPS automatically
71
+ - Content is returned in 100K-character windows; truncated responses report the `offset` to pass for the next window
72
+ - For GitHub repo content, prefer `web_fetch` or the `gh` CLI
73
+ - JS-rendered SPAs may return empty markdown (static HTML only)
74
+ - Authenticated or private pages (Google Docs, Confluence, Jira) are not supported
75
+
76
+ Command: `/clear-fetch-markdown-cache` — clear the in-memory URL cache.
77
+
78
+ ## `pdf_extract`
79
+
80
+ PDFs are otherwise unreachable: `fetch_markdown` and pi's `web_fetch` classify `application/pdf` as binary, and the `read` tool cannot parse it. This tool extracts the text layer via [unpdf](https://github.com/unjs/unpdf) (a serverless build of Mozilla's PDF.js, keyless and with no native dependencies).
81
+
82
+ - **Remote or local** — pass `url` for a remote PDF or `path` for an absolute path or `file://` URL; provide exactly one
83
+ - **Page markers** — output is delimited with `--- Page N ---` so pages can be cited
84
+ - **Page selection** — `pages` accepts `"3"`, `"1-5"`, or `"1,4,7-9"`; defaults to every page
85
+ - **15-minute LRU cache** — the whole parse is cached, so a later `pages` or `offset` call reuses it; local files are re-read when their modification time changes
86
+ - **Host and file confirmation** — remote URLs use the shared allowlist; local files inside the working directory are read without prompting, anything outside prompts (session-scoped allow/deny)
87
+
88
+ Usage notes:
89
+
90
+ - Content is returned in 100K-character windows; truncated responses report the `offset` to pass for the next window
91
+ - Scanned or image-only PDFs have no text layer and report that explicitly — there is no OCR
92
+ - Limits: 25MB download, 2000 pages, and a 30s extraction timeout
93
+
94
+ Command: `/clear-pdf-extract-cache` — clear the in-memory PDF text cache.
95
+
96
+ ## `web_browser`
97
+
98
+ Single tool with an `action` parameter:
99
+
100
+ | Action | Parameters | Description |
101
+ |--------|------------|-------------|
102
+ | `navigate` | `url` | Open a URL or `file://` path (HTTP upgraded to HTTPS except localhost/.local/private IPs) |
103
+ | `screenshot` | `fullPage?`, `selector?`, `toFile?` | Capture PNG screenshot (base64 image, or temp file path when `toFile=true`) |
104
+ | `click` | `selector` | Click a CSS selector |
105
+ | `type` | `selector`, `text`, `mode?` | Fill (`mode=fill`, default) or key-by-key type (`mode=press`) into a selector |
106
+ | `hover` | `selector` | Hover over a CSS selector (tooltips, hover-menus) |
107
+ | `press` | `key` | Press a keyboard key globally (Enter, Escape, Tab, etc.) |
108
+ | `select_option` | `selector`, `values` | Select option(s) in a `<select>` dropdown (comma-separated values) |
109
+ | `set_viewport` | `width`, `height` | Resize the browser viewport (persists until changed or browser closes) |
110
+ | `evaluate` | `script` | Run JavaScript in the page context (re-checks permission for the current page URL) |
111
+ | `get_content` | — | Return raw page HTML |
112
+ | `get_text` | `selector?` | Return visible text (layout-aware innerText; default selector `body`) |
113
+ | `get_markdown` | — | Return page content as markdown (headings, lists, links) |
114
+ | `wait_for` | `selector?`, `state?`, `networkidle?` | Wait for selector state and/or network idle |
115
+ | `get_accessibility_snapshot` | `selector?` | Return ARIA accessibility tree (YAML) |
116
+ | `get_console_logs` | — | Drain captured console/page errors |
117
+ | `get_network_logs` | — | Drain captured failed requests and 4xx/5xx responses |
118
+ | `get_cookies` | — | Return cookie metadata as JSON (name, domain, path, flags; values are redacted) |
119
+ | `set_cookies` | `cookies` | Set cookies (each needs `url` or `domain`+`path`) |
120
+ | `go_back` / `go_forward` / `reload` | — | Browser history navigation / reload |
121
+ | `scroll` | `selector?`, `frame?`, `deltaX?`, `deltaY?` | Scroll an element into view (`selector`) or scroll by pixel deltas (default `deltaY` = viewport height) |
122
+ | `drag` | `selector`, `targetSelector`, `frame?` | Drag-and-drop from `selector` to `targetSelector` |
123
+ | `upload_file` | `selector`, `files`, `frame?` | Set files on an `<input type=file>` (absolute paths) |
124
+ | `set_dialog_behavior` | `dialogAction`, `promptText?` | Configure how future JS dialogs (alert/confirm/prompt) are resolved for the rest of the session (default: dismiss) |
125
+ | `get_dialog_logs` | — | Drain captured dialogs (type, message, default value, resolution) |
126
+ | `list_tabs` | — | List open tabs (index, URL, title, active) |
127
+ | `switch_tab` | `index` | Switch the active tab (from `list_tabs`) |
128
+ | `close` | — | Close the browser |
129
+
130
+ Optional `timeout` (ms, default 30000) applies to navigation and to selector-based actions (click, type, hover, select_option, screenshot, get_text, get_markdown, wait_for, get_accessibility_snapshot, scroll, drag, upload_file). Other actions use Playwright defaults.
131
+
132
+ Selectors are CSS selectors, or Playwright's `role=` and `text=` selector engines (e.g. `role=button[name='Submit']`, `text=Sign in`). Pass `frame` (a CSS selector for the containing `<iframe>`) to scope click/type/hover/select_option/screenshot/get_text/wait_for/get_accessibility_snapshot/scroll/upload_file/drag into that frame.
133
+
134
+ Opening a link with `target=_blank` or `window.open` creates a popup that automatically becomes the active tab — subsequent actions target it until you `switch_tab` back. Use `list_tabs` to see all open tabs.
135
+
136
+ JS dialogs (`alert`/`confirm`/`prompt`) are dismissed by default so they never block automation; call `set_dialog_behavior` before the action that triggers one if you need `accept` instead (and, for `prompt`, a `promptText` value).
137
+
138
+ Commands:
139
+
140
+ - `/browser` — show open/closed status, current URL, title, tab count, console/network/dialog counts, and session-approved host count
141
+ - `/browser-close` — force close the browser
142
+ - `/browser-screenshot` — save a full-page screenshot to a temp PNG file
143
+
144
+ Status line: a footer segment (`browser: example.com`) appears while the browser is open.
145
+
146
+ ## Security model (shared)
147
+
148
+ - **Preapproved hosts**: common documentation and dev sites are allowed without prompting. The allowlist lives in `src/permissions.ts` and is shared by both tools.
149
+ - **User confirmation**: other hosts prompt Allow once / Allow for this session / Deny.
150
+ - **URL validation**: all tools reject embedded credentials and overlong URLs. `fetch_markdown` and `pdf_extract` additionally reject loopback, private, and link-local hosts and any non-HTTP(S) scheme on a `url`. `web_browser` accepts `localhost`, private IPs, and `file://` paths so it can drive local dev servers and local HTML — each still prompts for confirmation unless session-approved.
151
+ - **Local file resolution**: `pdf_extract` resolves a `path` through `realpath` before any check, so a symlink pointing outside the working directory is treated as outside and prompts. Directories, FIFOs, and devices are rejected, and the size limit is enforced from `stat` before any bytes are read.
152
+ - **Isolated browser context**: each session uses a fresh Chromium context (no shared profile/cookies with your system browser).
153
+ - **Untrusted content boundary**: `fetch_markdown` results are wrapped in `<untrusted-content url="…">` tags, and `pdf_extract` results in `<untrusted-content source="…">`, so the model treats fetched page and document text as data, not instructions.
154
+ - **PDF resource limits**: extraction is bounded by download size, page count, and a timeout. Because the bundled PDF.js runs on the event loop rather than a worker, the timeout bounds extraction across its await points but cannot interrupt a fully synchronous parse — the size and page caps are the primary defence.
155
+
156
+ ## Limitations
157
+
158
+ - `web_browser` requires the Chromium download.
159
+ - Screenshots consume significant context on vision models — prefer `get_text`, `get_markdown`, or `get_accessibility_snapshot`.
160
+ - Network logs capture only failed requests and 4xx/5xx responses (not all traffic).
161
+ - `pdf_extract` reads only an existing text layer; scanned documents need OCR, which is out of scope.
162
+
163
+ ### Popups and navigation
164
+
165
+ Popups inherit the same security model as same-tab link clicks: opening a `target=_blank` link or calling `window.open` is not gated by the host allowlist, since it's JS-initiated navigation rather than a tool-driven `navigate` call. `evaluate` remains gated per active page URL — the permission check follows the active-tab cursor automatically, so switching to a popup and calling `evaluate` re-checks permission for that popup's URL.
166
+
167
+ ## Development
168
+
169
+ Requires Node >= 22.19 (tests run TypeScript directly via `node --test`).
170
+
171
+ ```bash
172
+ npm install
173
+ npm run typecheck
174
+ npm test # browser tests require Chromium
175
+ ```
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@underactive/pi-topping-web-tools",
3
+ "version": "0.1.0",
4
+ "description": "Pi extensions for the web: fetch_markdown (URL to markdown, cached, keyless), pdf_extract (PDF text extraction) and web_browser (headless Playwright automation), sharing a preapproved-host allowlist.",
5
+ "keywords": ["pi-package", "pi-extension"],
6
+ "license": "MIT",
7
+ "author": "Eric Sison",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/underactive/pi-topping-web-tools.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/underactive/pi-topping-web-tools/issues"
14
+ },
15
+ "homepage": "https://github.com/underactive/pi-topping-web-tools#readme",
16
+ "type": "module",
17
+ "engines": {
18
+ "node": ">=22.19.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "src/**/*.ts",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "scripts": {
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "node --test test/*.test.ts"
32
+ },
33
+ "pi": {
34
+ "extensions": [
35
+ "./src/fetch-markdown.ts",
36
+ "./src/pdf-extract.ts",
37
+ "./src/web-browser/index.ts"
38
+ ]
39
+ },
40
+ "dependencies": {
41
+ "@mozilla/readability": "^0.6.0",
42
+ "linkedom": "^0.18.13",
43
+ "lru-cache": "^11.0.0",
44
+ "playwright": "^1.52.0",
45
+ "turndown": "^7.2.0",
46
+ "unpdf": "^1.8.1"
47
+ },
48
+ "peerDependencies": {
49
+ "@earendil-works/pi-ai": "*",
50
+ "@earendil-works/pi-coding-agent": "*",
51
+ "@earendil-works/pi-tui": "*",
52
+ "typebox": "*"
53
+ },
54
+ "devDependencies": {
55
+ "@earendil-works/pi-ai": "0.82.1",
56
+ "@earendil-works/pi-coding-agent": "0.82.1",
57
+ "@earendil-works/pi-tui": "0.82.1",
58
+ "@types/node": "^24.0.0",
59
+ "@types/turndown": "^5.0.5",
60
+ "typebox": "^1.3.3",
61
+ "typescript": "^5.9.3"
62
+ }
63
+ }