pi-extensions GitHub

pi-web-fetch — browser-grade web fetching & search for pi

Zero-dependency web_fetch / batch_web_fetch / web_search tools, plus web_answer when a TypeSafe key is present: browser-like request headers, clean readable extraction, page metadata, redirect handling, GitHub URLs via the gh CLI, bounded-concurrency batch fetching, and keyless web search via DuckDuckGo.

Tools

web_fetch(url, format?, maxChars?, timeoutMs?, headers?, includeImages?, followAlternates?)
batch_web_fetch(requests, concurrency?, totalMaxChars?)
web_search(query, max_results?)
web_answer(url, question, max_spans?)

web_fetch returns a metadata header (title, url, published, author, site, lang, via) followed by the extracted content in the requested format. batch_web_fetch fans out with bounded concurrency and reports each item's result (or failure) independently. web_search returns ranked DuckDuckGo results (title, URL, snippet) without an API key. web_answer and batch_web_fetch use deferred exposure and load through tool search when needed.

web_answer — the passage, not the page

web_answer(url, question, max_spans?)

Fetching a long page to answer one narrow question pays for the whole page. web_answer fetches it, splits it into passages, and asks TypeSafe's System One whether each passage answers the question — one request, every passage asked at once, plus a Noul for whether the page answers at all.

It returns the passages themselves, quoted verbatim, so the answer can be checked against its own text:

source: https://example.com/docs
[passage 2 · 0.84]
The uploader retries three times, with exponential backoff starting at 200 ms.

Three outcomes, and the tool says which one it is rather than guessing:

Requirements and limits:

Output formats

Format What you get
markdown Default. Clean readable content: headings, paragraphs, links, lists, code fences, tables, blockquotes.
html The same content as cleaned, pretty-printed HTML.
text Plain text with markdown syntax stripped.
json Structured: metadata fields + markdown content.
raw The unmodified response body (html/text), capped at 200k chars by default.

What makes it "smart"

web_search — keyless web search

web_search(query, max_results?) returns ranked results as markdown: title, URL, and snippet. No API key; works with any provider/model.

Engine selection — DuckDuckGo by default; set searxngUrl in settings to point web_search at a self-hosted SearXNG instance (JSON API, aggregates many engines, no scraping):

{ "webFetch": { "searxngUrl": "https://searxng.example.com" } }

A bare base URL gets the /search path appended. format=json must be enabled on the instance (default for self-hosted; most public instances disable it). Invalid or missing instances fall back to an explicit error, never a silent empty result.

Notes:

GitHub URLs use gh

For github.com / gist.github.com URLs, web_fetch prefers the gh CLI when it is installed and authenticated (gh auth status), because it gives authenticated, structured data instead of scraped HTML:

URL shape What you get
github.com/owner/repo repo metadata + README
…/blob/ref/path or …/raw/… raw file content (markdown passthrough or fenced code)
…/tree/ref/path directory listing
…/issues/N, …/pull/N issue/PR with comments
…/discussions/N discussion
…/releases, …/releases/tag/x releases
…/commit/sha, …/commits commit / commit list
gist.github.com/… gist files
github.com/owner profile

gh availability is probed once per session (cached) — set "webFetch": { "useGh": false } in settings to disable. When gh is missing or unauthenticated, GitHub URLs fall back to plain HTTP automatically. Unhandled GitHub paths (/actions, /settings, …) also fall back to HTTP.

Settings

Optional keys in ~/.pi/agent/settings.json or <project>/.pi/settings.json (project overrides global). Flat webFetchDefault* keys and a nested webFetch: { ... } object are both accepted:

{
  "webFetch": {
    "defaultFormat": "markdown",
    "defaultMaxChars": 60000,
    "defaultTimeoutMs": 15000,
    "batchConcurrency": 4,
    "userAgent": "",
    "extraHeaders": {},
    "followAlternates": true,
    "includeImages": false,
    "useGh": true,
    "searxngUrl": ""
  }
}
Key Default Description
defaultFormat markdown Default output format
defaultMaxChars 60000 Default content cap (raw defaults to 200000)
defaultTimeoutMs 15000 Request timeout
batchConcurrency 4 Bounded concurrency for batch_web_fetch
userAgent Chrome UA Override the browser user agent
extraHeaders {} Extra headers sent on every request
followAlternates true Alternate-content fallback
includeImages false Keep markdown image references
useGh true Prefer gh for GitHub URLs
searxngUrl "" Self-hosted SearXNG instance (JSON API) — web_search uses it instead of DuckDuckGo

Limitations

Development

npm test                          # node --test on packages/**/*.test.mjs
npm run check                     # bun build --no-bundle entrypoints + tests

The parsing/extraction/formatting logic lives in plain .mjs modules with node --test coverage; index.ts is only the pi tool wiring.