{"_id":"@arcanemachine/pi-web-search","_rev":"5-b00acbbc0ac0966b5d269277543dd4a7","name":"@arcanemachine/pi-web-search","dist-tags":{"latest":"3.0.0"},"versions":{"1.0.0":{"name":"@arcanemachine/pi-web-search","version":"1.0.0","keywords":["pi","pi-coding-agent","pi-extension","pi-package","web-search","duckduckgo","documents"],"author":{"name":"Nicholas Moen"},"license":"MIT","_id":"@arcanemachine/pi-web-search@1.0.0","maintainers":[{"name":"arcanemachine","email":"arcanemachine@gmail.com"}],"homepage":"https://github.com/arcanemachine/pi-web-search#readme","bugs":{"url":"https://github.com/arcanemachine/pi-web-search/issues"},"pi":{"image":"https://raw.githubusercontent.com/arcanemachine/pi-web-search/main/logo.jpg","extensions":["./index.ts"]},"dist":{"shasum":"32526d0f09d8914e78df01ef6612f01b4811ba9b","tarball":"https://registry.npmjs.org/@arcanemachine/pi-web-search/-/pi-web-search-1.0.0.tgz","fileCount":37,"integrity":"sha512-OwfDYcOBI0FrcvW2n5Xkrwq3ES1GbpVlA7Yxjk+/oinnezicdnYCrvhcU6lDToa3JTyjH5lo0BGtV6V5JLdBoA==","signatures":[{"sig":"MEUCIQDSmIlbIs1rnRAjL3FdfxrpodL1Brafb8IWY/D1io3akAIgb75W/cQ3uICjLz5s6kPi7o/39gO+87/v2ClCehy+KWU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":300307},"type":"module","engines":{"node":">=22.19.0"},"gitHead":"a751df93c94b2844b59ed792aab9c9c8e6399ea6","scripts":{"test":"node --import tsx --test test/*.test.ts","build":"tsc --noEmit","watch":"tsc --watch --noEmit","format":"prettier --write index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","typecheck":"tsc --noEmit","format:check":"prettier --check index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","prepublishOnly":"npm run format:check && npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"arcanemachine","email":"arcanemachine@gmail.com"},"repository":{"url":"git+https://github.com/arcanemachine/pi-web-search.git","type":"git"},"_npmVersion":"11.13.0","description":"Bounded web search and static document tools for Pi","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jsdom":"^26.1.0","@sinclair/typebox":"^0.34.49","node-html-markdown":"^2.0.0","@mozilla/readability":"^0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.0","prettier":"^3.8.3","typescript":"^5.7.2","@types/node":"^25.3.3","@types/jsdom":"^30.0.0","@earendil-works/pi-coding-agent":"^0.84.1"},"peerDependencies":{"@earendil-works/pi-coding-agent":">=0.84.1"},"peerDependenciesMeta":{"@earendil-works/pi-coding-agent":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/pi-web-search_1.0.0_1788240963473_0.6226861056578212","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@arcanemachine/pi-web-search","version":"1.0.2","keywords":["pi","pi-coding-agent","pi-extension","pi-package","web-search","duckduckgo","documents"],"author":{"name":"Nicholas Moen"},"license":"MIT","_id":"@arcanemachine/pi-web-search@1.0.2","maintainers":[{"name":"arcanemachine","email":"arcanemachine@gmail.com"}],"homepage":"https://github.com/arcanemachine/pi-web-search#readme","bugs":{"url":"https://github.com/arcanemachine/pi-web-search/issues"},"pi":{"image":"https://raw.githubusercontent.com/arcanemachine/pi-web-search/main/logo.jpg","extensions":["./index.ts"]},"dist":{"shasum":"980981fdb53b6093436ba5495bdf5f1ab91df4ea","tarball":"https://registry.npmjs.org/@arcanemachine/pi-web-search/-/pi-web-search-1.0.2.tgz","fileCount":37,"integrity":"sha512-5TJtOKhIcFP3kdQq4WEXE6tLu99Wb2N7jEkmyhk+NCqww7J3CrTkoqHQGWp78nEQsAWhRBuVW8NWXpELUiSM1A==","signatures":[{"sig":"MEUCIDKMNXSrvrbD1TmhYrhfib6A7EIwr2llFPKSxwaLEH0HAiEAhDhU4xiOFONr+ATIskyY+Ry+Bvd+Jq2NWW1QC2tfp6w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":301461},"type":"module","engines":{"node":">=22.19.0"},"gitHead":"f831143b1f2722ce10f440725c313d0a7455c155","scripts":{"test":"node --import tsx --test test/*.test.ts","build":"tsc --noEmit","watch":"tsc --watch --noEmit","format":"prettier --write index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","typecheck":"tsc --noEmit","format:check":"prettier --check index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","prepublishOnly":"npm run format:check && npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"arcanemachine","email":"arcanemachine@gmail.com"},"repository":{"url":"git+https://github.com/arcanemachine/pi-web-search.git","type":"git"},"_npmVersion":"11.13.0","description":"Bounded web search and static document tools for Pi","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jsdom":"^26.1.0","@sinclair/typebox":"^0.34.49","node-html-markdown":"^2.0.0","@mozilla/readability":"^0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.0","prettier":"^3.8.3","typescript":"^5.7.2","@types/node":"^25.3.3","@types/jsdom":"^30.0.0","@earendil-works/pi-coding-agent":"^0.84.1"},"peerDependencies":{"@earendil-works/pi-coding-agent":">=0.84.1"},"peerDependenciesMeta":{"@earendil-works/pi-coding-agent":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/pi-web-search_1.0.2_1788424757320_0.6634716487164687","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@arcanemachine/pi-web-search","version":"2.0.0","keywords":["pi","pi-coding-agent","pi-extension","pi-package","web-search","duckduckgo","documents"],"author":{"name":"Nicholas Moen"},"license":"MIT","_id":"@arcanemachine/pi-web-search@2.0.0","maintainers":[{"name":"arcanemachine","email":"arcanemachine@gmail.com"}],"homepage":"https://github.com/arcanemachine/pi-web-search#readme","bugs":{"url":"https://github.com/arcanemachine/pi-web-search/issues"},"pi":{"image":"https://raw.githubusercontent.com/arcanemachine/pi-web-search/main/logo.jpg","extensions":["./index.ts"]},"dist":{"shasum":"16d64f30688972b73ca79a60eebf6bd8ef825ba1","tarball":"https://registry.npmjs.org/@arcanemachine/pi-web-search/-/pi-web-search-2.0.0.tgz","fileCount":38,"integrity":"sha512-1nOUd3lbSQCBT6VgItAO6kE8LqIk+fN4507KRYIp9scmgmAW/oWjs7Qj2rgtzt7nFIZ45LUXD9IYOBZrMsVr4Q==","signatures":[{"sig":"MEUCIDlFxALMFs2rK6toLSJNLfwmwqfUKkrpwW+zNB6SsD6hAiEAoa4IFy5CLXA7CS92HEvnMvrYI9UZ/uXVsPdMfVLRmbA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":306759},"type":"module","engines":{"node":">=22.19.0"},"gitHead":"8825b23328134a0ee914d33caa7ba6b0e367d9cc","scripts":{"test":"node --import tsx --test test/*.test.ts","build":"tsc --noEmit","watch":"tsc --watch --noEmit","format":"prettier --write index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","typecheck":"tsc --noEmit","format:check":"prettier --check index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","prepublishOnly":"npm run format:check && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"arcanemachine","email":"arcanemachine@gmail.com"},"repository":{"url":"git+https://github.com/arcanemachine/pi-web-search.git","type":"git"},"_npmVersion":"11.13.0","description":"Bounded web search and static document tools for Pi","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jsdom":"^26.1.0","@sinclair/typebox":"^0.34.49","node-html-markdown":"^2.0.0","@mozilla/readability":"^0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.0","prettier":"^3.8.3","typescript":"^5.7.2","@types/node":"^25.3.3","@types/jsdom":"^30.0.0","@earendil-works/pi-coding-agent":"^0.84.1"},"peerDependencies":{"@earendil-works/pi-coding-agent":">=0.84.1"},"peerDependenciesMeta":{"@earendil-works/pi-coding-agent":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/pi-web-search_2.0.0_1788765815641_0.24987133529198502","host":"s3://npm-registry-packages-npm-production"}},"2.1.0":{"name":"@arcanemachine/pi-web-search","version":"2.1.0","keywords":["pi","pi-coding-agent","pi-extension","pi-package","web-search","duckduckgo","documents"],"author":{"name":"Nicholas Moen"},"license":"MIT","_id":"@arcanemachine/pi-web-search@2.1.0","maintainers":[{"name":"arcanemachine","email":"arcanemachine@gmail.com"}],"homepage":"https://github.com/arcanemachine/pi-web-search#readme","bugs":{"url":"https://github.com/arcanemachine/pi-web-search/issues"},"pi":{"image":"https://raw.githubusercontent.com/arcanemachine/pi-web-search/main/logo.jpg","extensions":["./index.ts"]},"dist":{"shasum":"aa8f6c5b75ed5900bbce49dd0094c3f42ac7f3ff","tarball":"https://registry.npmjs.org/@arcanemachine/pi-web-search/-/pi-web-search-2.1.0.tgz","fileCount":38,"integrity":"sha512-JojfIpmwNHAf/WNHc2sjnWsGa5F9OhCBbEyguYfmx3pRD3sRgzOzIJ3qOBhwu91a5Pc8zrLHoUFhQWhMbgaN9g==","signatures":[{"sig":"MEQCIFL7x8klxoIPIAyrrcgx4MpWzKSEoR1l3MRGGOZFILaQAiB4nz5y0SfzeOEco6AVCN/PFJgDx8DnHhNjs3Ch68Ik8A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIH8xfkRwBixdhwUqdIJJYiDWjRCKgmnFZISjP5Lxr2z5AiEAqYLjz730451jz6dIiE44ZD2i2NDtFyordwvqvtL8bEc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":308782},"type":"module","engines":{"node":">=22.19.0"},"gitHead":"b68d510c43c4059ca36a72c87321f57b526c0773","scripts":{"test":"node --import tsx --test test/*.test.ts","build":"tsc --noEmit","watch":"tsc --watch --noEmit","format":"prettier --write index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","typecheck":"tsc --noEmit","format:check":"prettier --check index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","prepublishOnly":"npm run format:check && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"arcanemachine","email":"arcanemachine@gmail.com"},"repository":{"url":"git+https://github.com/arcanemachine/pi-web-search.git","type":"git"},"_npmVersion":"11.13.0","description":"Bounded web search and static document tools for Pi","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jsdom":"^26.1.0","@sinclair/typebox":"^0.34.49","node-html-markdown":"^2.0.0","@mozilla/readability":"^0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.0","prettier":"^3.8.3","typescript":"^5.7.2","@types/node":"^25.3.3","@types/jsdom":"^30.0.0","@earendil-works/pi-coding-agent":"^0.84.1"},"peerDependencies":{"@earendil-works/pi-coding-agent":"*"},"peerDependenciesMeta":{"@earendil-works/pi-coding-agent":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/pi-web-search_2.1.0_1789103739043_0.7122918791050759","host":"s3://npm-registry-packages-npm-production"}},"3.0.0":{"pi":{"image":"https://raw.githubusercontent.com/arcanemachine/pi-web-search/main/logo.jpg","extensions":["./index.ts"]},"_id":"@arcanemachine/pi-web-search@3.0.0","bugs":{"url":"https://github.com/arcanemachine/pi-web-search/issues"},"dist":{"shasum":"afa4da797aa7c7e2b1ef92e1b3a4c1bb49ce6cf1","tarball":"https://registry.npmjs.org/@arcanemachine/pi-web-search/-/pi-web-search-3.0.0.tgz","fileCount":37,"integrity":"sha512-V0rGTM/v9muK4kle2O5XWzYKzgPLwMxJORf/BKKwkx3klEjWHkCibh0cLj08WRfyszsVj7Jn5YN4G4DeIiGzhw==","signatures":[{"sig":"MEQCIEVFVdUaVlAqAAMvbUTsW4Jf+tfK69g2LJCitsf25xjKAiAZHnr+4XR/zVDDTHW9bwMIRV07v70lZPMgcd8bzzY8gg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDwNFgfIop2m4ZkVtN2aDQMT6A188b02yNzfhVKHvefxAiEAx9aiXMK4di0UKw9gD9AWoy5czq+AWp7aOu+8VkZdbnI="}],"unpackedSize":306644},"name":"@arcanemachine/pi-web-search","type":"module","author":{"name":"Nicholas Moen"},"engines":{"node":">=22.19.0"},"gitHead":"035f764f8bb6a68def7d1d46a285ed9e6f63a771","license":"MIT","scripts":{"test":"node --import tsx --test test/*.test.ts","build":"tsc --noEmit","watch":"tsc --watch --noEmit","format":"prettier --write index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","typecheck":"tsc --noEmit","format:check":"prettier --check index.ts \"src/**/*.ts\" \"test/**/*.ts\" package.json README.md AGENTS.md CHANGELOG.md","prepublishOnly":"npm run format:check && npm run typecheck && npm run test && npm run build"},"version":"3.0.0","_npmUser":{"name":"arcanemachine","email":"arcanemachine@gmail.com"},"homepage":"https://github.com/arcanemachine/pi-web-search#readme","keywords":["pi","pi-coding-agent","pi-extension","pi-package","web-search","duckduckgo","documents"],"repository":{"url":"git+https://github.com/arcanemachine/pi-web-search.git","type":"git"},"_npmVersion":"11.13.0","description":"Bounded web search and static document tools for Pi","directories":{},"maintainers":[{"name":"arcanemachine","email":"arcanemachine@gmail.com"}],"_nodeVersion":"24.16.0","dependencies":{"jsdom":"^26.1.0","@sinclair/typebox":"^0.34.49","node-html-markdown":"^2.0.0","@mozilla/readability":"^0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.0","prettier":"^3.8.3","typescript":"^5.7.2","@types/node":"^25.3.3","@types/jsdom":"^30.0.0","@earendil-works/pi-coding-agent":"^0.84.1"},"peerDependencies":{"@earendil-works/pi-coding-agent":"*"},"peerDependenciesMeta":{"@earendil-works/pi-coding-agent":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pi-web-search_3.0.0_1789903605833_0.8416142690917643"}}},"time":{"created":"2026-09-01T05:36:03.154Z","modified":"2026-09-20T11:26:46.073Z","1.0.0":"2026-09-01T05:36:03.627Z","1.0.2":"2026-09-03T08:39:17.464Z","2.0.0":"2026-09-07T07:23:35.791Z","2.1.0":"2026-09-11T05:15:39.137Z","3.0.0":"2026-09-20T11:26:45.925Z"},"bugs":{"url":"https://github.com/arcanemachine/pi-web-search/issues"},"author":{"name":"Nicholas Moen"},"license":"MIT","homepage":"https://github.com/arcanemachine/pi-web-search#readme","keywords":["pi","pi-coding-agent","pi-extension","pi-package","web-search","duckduckgo","documents"],"repository":{"url":"git+https://github.com/arcanemachine/pi-web-search.git","type":"git"},"description":"Bounded web search and static document tools for Pi","maintainers":[{"name":"arcanemachine","email":"arcanemachine@gmail.com"}],"readme":"# pi-web-search\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/arcanemachine/pi-web-search/main/logo.jpg\" alt=\"pi-web-search logo\" width=\"250\" />\n</p>\n\nA [Pi](https://pi.dev) extension for bounded web search and static document tools.\n\nNo package-specific configuration is required. Web search uses native DuckDuckGo by default. If you supply a Brave API key and do not configure a backend order, Brave is automatically added as a fallback after DuckDuckGo. SearXNG remains opt-in. The extension can read static web pages, search their extracted text, and summarize them with Pi's active model by default.\n\n> Like this extension? See [my other Pi extensions](https://github.com/arcanemachine/pi-projects).\n\n## Requirements\n\n- Pi 0.84.1 or later.\n- Node.js 22.19.0 or later.\n- Outbound HTTP(S) access for search and document tools.\n- A usable Pi model is required only for page summaries.\n- No separate command or Python installation for DuckDuckGo search.\n\n## Installation\n\n### From GitHub\n\nInstall the public Git package globally:\n\n```bash\npi install git:github.com/arcanemachine/pi-web-search\n```\n\nPi clones the Git package, installs its declared JavaScript runtime dependencies, and loads the extension declared by its Pi manifest. Pi packages execute code with the user's permissions, so review the source before installing a package.\n\n### From npm\n\nAfter publication:\n\n```bash\npi install npm:@arcanemachine/pi-web-search\n```\n\n### Project-local installation\n\nFor one project only, install it locally:\n\n```bash\npi install git:github.com/arcanemachine/pi-web-search -l\n```\n\nA global installation writes user settings under `~/.pi/agent/settings.json`. `-l` writes project settings under `.pi/settings.json`; project packages load only after the project is trusted.\n\n### One invocation\n\nTo try it for one Pi invocation without saving it to settings:\n\n```bash\npi -e git:github.com/arcanemachine/pi-web-search\n```\n\nUse these commands to inspect and manage the installation:\n\n```bash\npi list\npi update --extensions\npi remove git:github.com/arcanemachine/pi-web-search\n```\n\nUse `-l` when removing a project-local installation. Start or restart Pi after installing a package or changing its configuration, or use `/reload` while Pi is already running.\n\n## Dependencies at a glance\n\n| Capability                  | Requirement                                                                                                                          |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| `read_url_content`          | No external executable or service; requires outbound HTTP(S).                                                                        |\n| `find_text_in_url_content`  | No external executable or service; requires outbound HTTP(S).                                                                        |\n| `search_web` via DuckDuckGo | Outbound HTTPS to DuckDuckGo's HTML search endpoint; no external executable or service.                                              |\n| `search_web` via SearXNG    | A reachable SearXNG service with JSON enabled.                                                                                       |\n| `search_web` via Brave      | A Brave Search API subscription key in `braveApiKey` settings or `BRAVE_SEARCH_API_KEY`, and outbound HTTPS.                         |\n| HTML normalization          | `jsdom`, Mozilla Readability, and `node-html-markdown`, installed automatically as JavaScript package dependencies by Pi.            |\n| `summarize_url_content`     | Optional model access through the active Pi model or configured `summarizerModel`; enabled by default; disableable by configuration. |\n\nWithout an explicit `backends` setting, DuckDuckGo is always tried first. When `braveApiKey` or `BRAVE_SEARCH_API_KEY` supplies a Brave key, Brave is automatically added as the second backend. This provides DuckDuckGo → Brave fallback without using Brave quota when DuckDuckGo succeeds. SearXNG remains opt-in. The document read and text-finding tools do not require a search backend.\n\n## Choose a search backend\n\n### Default: DuckDuckGo with optional Brave fallback\n\nWhen `backends` is omitted, the extension selects the backend order from the available configuration:\n\n- Without a Brave key: `[\"duckduckgo\"]`\n- With `braveApiKey` or `BRAVE_SEARCH_API_KEY`: `[\"duckduckgo\", \"brave\"]`\n\nBrave runs only when DuckDuckGo returns an operational error, such as HTTP 429, blocking evidence, a timeout, or a fetch failure. A normal DuckDuckGo response with no results does not trigger fallback.\n\nBrave requests can consume quota or incur cost. Supplying a Brave key intentionally opts the implicit default flow into Brave fallback. To keep a key available while preventing automatic Brave calls, set an explicit DuckDuckGo-only backend list.\n\n### DuckDuckGo only\n\nDuckDuckGo is the default, service-free search setup. The package sends a standards-compliant form POST directly to `https://html.duckduckgo.com/html`, parses ordered HTML results, and returns bounded title, URL, and snippet fields. Region, safe-search, and recency options are mapped to the endpoint request. DuckDuckGo may transiently block or rate-limit automated requests; those responses are classified as retryable operational outcomes.\n\nTo force DuckDuckGo-only search—even when a Brave key is available—configure:\n\n```json\n{\n  \"pi-web-search\": {\n    \"backends\": [\"duckduckgo\"]\n  }\n}\n```\n\nThis explicit list disables all fallback backends.\n\n### SearXNG only\n\nSearXNG is an external service that must already be installed and running; this package does not manage it. See the [official installation documentation](https://docs.searxng.org/admin/installation.html), [Search API](https://docs.searxng.org/dev/search_api.html), and [search settings](https://docs.searxng.org/admin/settings/settings_search.html#settings-search).\n\nThe package performs GET requests to `<searxngUrl>/search` with `format=json`. Enable JSON in SearXNG's settings:\n\n```yaml\nsearch:\n  formats:\n    - html\n    - json\n```\n\nInstallations commonly enable only HTML. Requesting an unavailable format returns HTTP 403, and many public instances disable JSON. Restart or reload SearXNG after changing its settings.\n\nThe endpoint must be reachable from the process running Pi. In a container, `127.0.0.1` refers to that container, not automatically to its host.\n\nFor the default URL, an example readiness check is:\n\n```bash\ncurl -fsS \\\n  'http://127.0.0.1:8080/search?q=pi&format=json' \\\n  >/dev/null && echo \"SearXNG JSON API ready\"\n```\n\nIf your instance uses another URL, substitute it in the check. Configure SearXNG only:\n\n```json\n{\n  \"pi-web-search\": {\n    \"backends\": [\"searxng\"],\n    \"searxngUrl\": \"http://127.0.0.1:8080\"\n  }\n}\n```\n\nThis keeps the configured backend limited to SearXNG.\n\n### Add Brave fallback\n\nBrave requires a Brave Search API subscription key and sends queries over HTTPS to the fixed official endpoint. See the official [Web Search API documentation](https://api-dashboard.search.brave.com/api-reference/web/search/get), [API key management](https://api-dashboard.search.brave.com/documentation/guides/authentication), [rate-limit guidance](https://api-dashboard.search.brave.com/documentation/guides/rate-limiting), and [current pricing](https://brave.com/search/api/). Successful calls may consume quota or incur cost; verify the current pricing and account terms before use.\n\nConfigure the key in global `~/.pi/agent/settings.json` or project `.pi/settings.json`:\n\n```json\n{\n  \"pi-web-search\": {\n    \"braveApiKey\": \"your-subscription-token\"\n  }\n}\n```\n\nFor local-only credentials, global settings are generally preferable to project settings so the key is not committed with project files. Project settings override global settings. As a lower-priority alternative, export the key in the environment of the process running Pi:\n\n```bash\nexport BRAVE_SEARCH_API_KEY='your-subscription-token'\n```\n\nA configured `braveApiKey` takes precedence over `BRAVE_SEARCH_API_KEY`. Reload Pi or restart it after changing settings or the environment.\n\nWith no explicit `backends` setting, either key source enables automatic DuckDuckGo → Brave fallback. No additional backend configuration is required.\n\n#### Brave only\n\nTo skip DuckDuckGo and use Brave as the only backend:\n\n```json\n{\n  \"pi-web-search\": {\n    \"backends\": [\"brave\"]\n  }\n}\n```\n\n#### Brave first\n\nTo prioritize Brave while retaining DuckDuckGo as a fallback:\n\n```json\n{\n  \"pi-web-search\": {\n    \"backends\": [\"brave\", \"duckduckgo\"]\n  }\n}\n```\n\nBecause this list is explicit, Brave is attempted first and DuckDuckGo is used only after a Brave operational error.\n\n### DuckDuckGo with SearXNG fallback\n\n```json\n{\n  \"pi-web-search\": {\n    \"backends\": [\"duckduckgo\", \"searxng\"],\n    \"searxngUrl\": \"http://127.0.0.1:8080\"\n  }\n}\n```\n\nWith this configuration:\n\n1. DuckDuckGo is attempted first.\n2. SearXNG is attempted only after an evidenced operational error.\n3. Legitimate `no_results` does not trigger fallback.\n4. Local process rate limiting does not dispatch backends.\n5. Result provenance identifies attempts and the selected backend.\n6. If both fail, the final backend error is returned and earlier failures appear as warnings.\n\n## Verify the installation\n\nUse natural Pi requests to verify each tool:\n\n> Search the web for RFC 9110 and return three results.\n\nThis validates backend dispatch and should return bounded title, URL, and snippet results with backend provenance.\n\nIf Brave is configured and you accept the possible quota or cost, you can verify it explicitly:\n\n> Using Brave Search, find the official RFC 9110 source and return two results.\n\nUse a narrow query and check the returned backend provenance; a successful API call may consume account quota.\n\n> Read `https://www.rfc-editor.org/rfc/rfc9110.html` and show the opening section.\n\nThis validates HTTP retrieval, static HTML parsing, Markdown normalization, and bounded snapshot creation.\n\n> Find `Representation Metadata` in `https://www.rfc-editor.org/rfc/rfc9110.html`.\n\nThis validates literal matching against the normalized snapshot.\n\nSearch snippets are for discovery; read the source before citing it.\n\n## Tools\n\nAll four tools return bounded, human-readable Markdown in their model-visible `content`; machine-readable outcomes remain in the structured `details` field. In Pi's interactive UI, tool rows provide compact invocation/result previews when collapsed and the complete bounded readable result when expanded. They honor Pi's global tool-output expansion setting, so the normal Pi expand/collapse controls work without a package-specific setting.\n\n### `search_web`\n\nSearches the configured backend order only.\n\n```ts\n{\n  query: string;\n  limit?: number;\n  region?: string;\n  safeSearch?: \"on\" | \"off\";\n  timeRange?: \"day\" | \"week\" | \"month\" | \"year\";\n  forceRefresh?: boolean;\n}\n```\n\nWhen `backends` is omitted, the effective backend order is `[\"duckduckgo\"]` without a Brave key and `[\"duckduckgo\", \"brave\"]` with one. An explicit `backends` list replaces this automatic selection. Backends run in order, and the next backend is tried only after an operational error. Legitimate `no_results` and local rate limiting never trigger fallback. `limit` applies to one initial DuckDuckGo HTML page; the backend does not paginate. `forceRefresh` bypasses completed cache entries, not the limiter. The visible result is a numbered Markdown list of titles, URLs, snippets, backend metadata, and warnings; the structured `details` field retains the complete bounded outcome.\n\n### `read_url_content`\n\nFetches an HTTP(S) URL, creates a bounded normalized snapshot, and returns one page. Continuation is stateless: repeat the same URL, mode, and selector and pass the returned `nextOffset` as `offset`. Offsets are zero-based Unicode character positions in normalized content. Overfetching is friendly: an offset past the end returns an empty page at the document end, and a large `maxChars` returns the remaining content. The model-visible footer shows the exact half-open character range, truncation state, and next call when more content exists.\n\n```ts\n{\n  url: string;\n  mode?: \"main\" | \"full\";\n  selector?: string;\n  offset?: number;\n  maxChars?: number;\n  forceRefresh?: boolean;\n}\n```\n\nHTML is parsed without executing scripts and converted to Markdown. Plain text, Markdown, XML text, and JSON use native normalization. Read errors are shown as concise Markdown with a stable error code; full bounded error details remain in `details`. Main-mode extraction selects an explicit CSS selector or deterministically prefers `main`, `[role=\"main\"]`, and `article`; sectioned body-only documents preserve their structured body, while weakly structured pages use best-effort Mozilla Readability extraction before falling back to the body. Full mode and selectors remain deterministic. JavaScript-dependent content is not rendered. Refetching after a page changes applies the requested offset to the current normalized document.\n\n### `summarize_url_content`\n\nGenerates a bounded objective-focused answer from one normalized static document through an isolated model request. The tool remains registered even when execution is disabled, so changing `summarizationEnabled` does not change its public schema. Summarization is enabled by default; setting it to `false` removes only this tool from Pi's active tool set and system prompt after `/reload` or restart, while direct bypass calls return an explicit disabled error.\n\n```ts\n{\n  url: string;\n  objective?: string;\n  mode?: \"main\" | \"full\";\n  selector?: string;\n  forceRefresh?: boolean;\n}\n```\n\nSummarization is enabled by default. `summarizerModel` optionally selects a configured `provider/model`; when it is absent, the active Pi model is used. An invalid configured model never silently falls back. Successful results identify the actual provider/model and configured thinking level when present, preserve source provenance, and return only the bounded generated answer to the parent context. The model sees a bounded initial excerpt and can inspect more of the same snapshot only through private line-read and literal-grep tools. References are best-effort rather than verified citations. Generated summaries are not cached, while normalized source snapshots retain the existing document cache behavior. The visible result presents the generated prose as Markdown with concise source/model metadata; usage accounting, generation counters, references, provenance, and bounds remain in structured `details`.\n\n### `find_text_in_url_content`\n\nFinds literal text in the same normalized snapshots used by `read_url_content`. It uses the same URL, `mode`, `selector`, and Unicode character-offset coordinate system as reading, so a match position can be passed directly to `read_url_content`. Continuation is stateless: repeat the same document-selection arguments and pass `nextOffset` as `offset`.\n\n```ts\n{\n  url: string;\n  query: string;\n  mode?: \"main\" | \"full\";\n  selector?: string;\n  offset?: number;\n  beforeLines?: number;\n  afterLines?: number;\n  maxMatches?: number;\n  maxQuoteChars?: number;\n  caseSensitive?: boolean;\n  forceRefresh?: boolean;\n}\n```\n\nMatches include exact bounded quotes, heading breadcrumbs, line numbers, and half-open normalized character ranges. Matching is literal and non-overlapping; case-insensitive matching retains JavaScript Unicode behavior. Surrounding context defaults to zero lines before and after each match; set `beforeLines` and `afterLines` when context is useful. Overlapping context windows are coalesced, while match counts continue to count occurrences. When output limits bound the result, `nextOffset` identifies the first match not returned. Call again with the same URL, query, mode, selector, and matching options, changing only `offset` or output limits as needed. An offset at or past the document end returns `status: \"no_match\"`.\n\n## Configuration\n\nConfigure a `pi-web-search` object in global `~/.pi/agent/settings.json` or project `.pi/settings.json`. Project properties override matching global properties, while unspecified settings retain their defaults. When `backends` is omitted, the effective order is `[\"duckduckgo\"]` without a Brave key and `[\"duckduckgo\", \"brave\"]` with one. Any explicit `backends` array overrides this automatic selection. The reference below shows `[\"duckduckgo\"]` as an explicit override. Configure only the overrides you intend to change. The three minimal backend examples are in [Choose a search backend](#choose-a-search-backend).\n\n### Complete configuration reference\n\n```json\n{\n  \"pi-web-search\": {\n    \"backends\": [\"duckduckgo\"],\n    \"searxngUrl\": \"http://127.0.0.1:8080\",\n    \"braveApiKey\": \"your-subscription-token\",\n    \"searchTimeoutMs\": 10000,\n    \"searchCacheTtlSeconds\": 120,\n    \"searchRateLimitPerMinute\": 10,\n    \"searchRateLimitBurst\": 3,\n    \"searchMaxResults\": 5,\n    \"searchMaxLimitResults\": 10,\n    \"searchMaxQueryChars\": 500,\n    \"searchMaxTitleChars\": 300,\n    \"searchMaxUrlChars\": 2048,\n    \"searchMaxSnippetChars\": 1000,\n    \"searchMaxOutputBytes\": 24576,\n    \"searchCacheMaxEntries\": 100,\n    \"searchCacheMaxBytes\": 2097152,\n    \"documentTimeoutMs\": 15000,\n    \"documentCacheTtlSeconds\": 300,\n    \"documentMaxDownloadBytes\": 5242880,\n    \"documentMaxNormalizedBytes\": 2097152,\n    \"documentCacheMaxEntries\": 50,\n    \"documentCacheMaxBytes\": 10485760,\n    \"readMaxChars\": 12000,\n    \"readMaxLimitChars\": 40000,\n    \"findMaxQueryChars\": 500,\n    \"findMaxContextLines\": 20,\n    \"findMaxMatches\": 1000,\n    \"findMaxLimitMatches\": 1000,\n    \"findMaxQuoteChars\": 40000,\n    \"findMaxLimitQuoteChars\": 40000,\n    \"summarizationEnabled\": true,\n    \"summarizerModel\": \"provider/model\",\n    \"summarizerThinkingLevel\": \"low\"\n  }\n}\n```\n\nOmit `backends` to use automatic key-aware selection. Set it explicitly to force a particular backend set or order.\n\nThe `*MaxResults`, `*MaxChars`, and corresponding `*MaxLimit*` properties configure defaults and hard caps for model-requested values. Text-finding defaults are intentionally high so ordinary queries return every match; pathological results are bounded and expose a `nextOffset` continuation hint. Summarization is enabled by default. Set `summarizationEnabled` to `false` to disable execution and remove only `summarize_url_content` from Pi's active tool set and system prompt after reload; its registered definition remains available. `summarizerModel` is optional and uses `provider/model` syntax. When absent, summarization uses the active Pi model. `summarizerThinkingLevel` is optional and accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. When omitted, provider defaults apply and the parent thinking level is not inherited. For the supported OpenAI direct-completion APIs (`openai-codex-responses`, `openai-responses`, `azure-openai-responses`, and `openai-completions`), the configured level is passed as `reasoningEffort`; `off` maps to `none` for Codex. Unsupported APIs and non-reasoning models fail before dispatch instead of silently ignoring an explicit level. Invalid, duplicate, non-finite, negative, inconsistent, unknown, or unreasonable settings fail with a configuration error rather than being guessed.\n\nA readable explicit summarizer setup is:\n\n```json\n{\n  \"pi-web-search\": {\n    \"summarizationEnabled\": true,\n    \"summarizerModel\": \"openai-codex/gpt-5.6-luna\",\n    \"summarizerThinkingLevel\": \"low\"\n  }\n}\n```\n\n`SEARXNG_URL` is a lower-priority compatibility fallback only when `searxngUrl` is absent from settings. `CACHE_TTL_MINUTES` is a lower-priority compatibility fallback only when `documentCacheTtlSeconds` is absent. `BRAVE_SEARCH_API_KEY` is a lower-priority fallback only when `braveApiKey` is absent from settings. Package settings are preferred for new configuration. No other package-specific environment configuration is used. Use `/reload` or restart Pi to apply settings changes.\n\n## Troubleshooting\n\n### `backend_unavailable`\n\nFor Brave, this usually means `braveApiKey` and `BRAVE_SEARCH_API_KEY` are both missing or blank. Configure the key in settings or export it in the environment visible to Pi, then reload or restart Pi. A 401 means Brave rejected the subscription token; check the key in the official Brave account console.\n\n### `blocked` from DuckDuckGo or Brave\n\nDuckDuckGo can return transient blocking evidence. DuckDuckGo HTTP 202 or 403 responses, dedicated challenge pages, and other narrowly recognized blocking evidence are classified as `blocked`. Brave HTTP 403 is also classified as `blocked`; check account permissions and service terms. Do not repeatedly hammer either service; wait or configure another backend. No fixed cooldown is guaranteed.\n\n### `fetch_failed` from SearXNG\n\nThe safe connection messages are:\n\n- `SearXNG endpoint refused the connection`\n- `SearXNG hostname could not be resolved`\n- `SearXNG endpoint was unreachable`\n- `SearXNG connection was reset`\n- `SearXNG request failed`\n\nCheck service state, `searxngUrl`, host/container reachability, and the direct JSON API curl shown above.\n\n### SearXNG HTTP 403 / `blocked`\n\nHTTP 403 can mean that JSON is disabled, a reverse proxy denied the request, or access controls rejected it. Verify `json` in `search.formats` and test the direct curl; do not assume every 403 is a JSON-format problem.\n\n### `rate_limited`\n\nDistinguish the local process token bucket from DuckDuckGo HTTP 429 throttling, a SearXNG instance HTTP 429, SearXNG engine diagnostics, and Brave HTTP 429. Brave rate limits include `retryAfterMs` when the response supplies usable reset information. Honor `retryAfterMs` when present, avoid immediate repeated calls, and inspect provenance.\n\n### `timeout`\n\nA backend or remote service exceeded `searchTimeoutMs`. For DuckDuckGo, the timeout covers response fetching and body reading. Check service health before increasing it; tune it only when the environment requires it.\n\nBrave query limits are also backend-local: queries over 400 Unicode characters or 50 whitespace-delimited words return `invalid_request` without truncation. A later configured backend may still be attempted.\n\n### Brave quota, billing, or HTTP 422\n\nBrave HTTP 422 means the API rejected the request parameters; check the query limits and current API documentation. Review your account's quota and billing terms in the official Brave console and pricing page before enabling this backend for repeated searches.\n\n### `parse_failed`\n\nDuckDuckGo responses must be recognizable HTML search pages. Incompatible content types, malformed result containers, unsafe destinations, unrelated pages, and responses over 2 MiB are rejected.\n\nFor SearXNG, possible causes include HTML instead of JSON, disabled JSON, a proxy error page, the wrong endpoint, or an unsupported payload. Test the exact `/search?...&format=json` endpoint.\n\n### `no_results`\n\nThis is a legitimate result, not a backend failure. Fallback intentionally does not run; refine or correct the query.\n\n### Client-rendered shell warning\n\nStatic extraction does not execute JavaScript. Use Playwright or another JavaScript-capable browser when the needed content is rendered only in the browser.\n\n## Privacy and limitations\n\n- DuckDuckGo search requires no separate command or Python installation. It sends the query and caller network address directly to DuckDuckGo over HTTPS.\n- SearXNG mediates upstream connections but can observe the query. Its default URL is `http://127.0.0.1:8080`.\n- Brave receives the query and network information needed to provide API results. Review Brave's current API terms and retention practices; ordinary plans should not be assumed to provide zero-data retention.\n- The Brave subscription key may be read from `braveApiKey` settings or the lower-priority `BRAVE_SEARCH_API_KEY` environment fallback, and is not included in model-visible output. Search results may be cached locally without the key.\n- Document tools send the requested URL and caller network address to the destination server and any permitted HTTP redirects.\n- Enabled summarization sends the normalized source content and objective to the selected model provider. Provider costs, retention, and privacy terms apply; review those terms before use. The actual provider/model is shown in successful output.\n- Summarization does not create a generated-summary cache. The source snapshot may be bounded or truncated, and references are best-effort rather than verified citations.\n\n## Guardrails and outcomes\n\n- Search uses a process-local token bucket with a sustained default rate of 10 logical outbound searches per minute and a burst capacity of 3. Tokens refill continuously, so this is an average rate rather than a strict rolling-window limit. Cache hits and identical in-flight callers are exempt. Pi subagents use separate processes and therefore separate buckets.\n- Search and document caches are process-local TTL/LRU caches bounded by entry count and bytes. Expected operational errors are not cached.\n- Document fetches accept HTTP(S) only, reject embedded credentials, follow at most five redirects, stream at most 5 MiB by default, and enforce timeout/cancellation.\n- Normalized snapshots default to a 2 MiB configured byte cap and always enforce a 50,000-line internal safety cap. Incomplete snapshots carry explicit warnings.\n- Static extraction warns when a page appears to be a client-rendered shell; use Playwright or another JavaScript-capable browser in that case.\n- Model-visible `content` and structured `details` are independently bounded below Pi's 50 KB/2,000-line protocol ceiling. Raw HTML, backend-native payloads, unbounded diagnostics, full cached snapshots, and nested summarizer messages are never returned to the parent.\n- Summarization fails explicitly after bounded invalid or incomplete model behavior; it never presents partial generated prose as a successful summary.\n\nExpected outcomes use structured statuses:\n\n- `ok`\n- `no_results`\n- `no_match`\n- `error`\n\nOperational errors include stable codes such as `invalid_request`, `backend_unavailable`, `rate_limited`, `timeout`, `blocked`, `fetch_failed`, `backend_failed`, and `parse_failed`, plus retry guidance when known. Unexpected invariant failures remain protocol-level errors.\n\n## Research workflow\n\nSearch results are discovery aids; inspect a relevant source before relying on them. Use `summarize_url_content` for a focused explanation of one static page. Use `read_url_content` for exact text, quotations, code, commands, or deliberate pagination. Use `find_text_in_url_content` for targeted literal evidence. For broad, multi-page, or cross-source research, delegate to a suitable research subagent when available. Search, read, and text-finding stay deterministic; summarization is model-generated and isolated.\n\n## Development\n\n```bash\nnpm install\nnpm run format:check\nnpm run typecheck\nnpm test\nnpm run build\nnpm pack --dry-run\n```\n\nThe package is loaded from its TypeScript entrypoint. It does not need a compiled runtime artifact.\n\n## License\n\nMIT. See [LICENSE.md](./LICENSE.md).\n","readmeFilename":"README.md"}