pi-web-kit
Context-efficient web search and fetch tools for Pi.
Package details
Install pi-web-kit from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-web-kit- Package
pi-web-kit- Version
0.4.2- Published
- Oct 25, 2026
- Downloads
- 350/mo · 28/wk
- Author
- mocito
- License
- MIT
- Types
- extension
- Size
- 202.6 KB
- Dependencies
- 1 dependency · 4 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-web-kit
Give Pi current web knowledge, authoritative library docs, and real-world code examples without flooding model context.
pi-web-kit combines search, page reading, version-aware documentation, and code research behind five agent-ready tools with bounded, cache-aware output.
Features
- Research the live web — search current information and read single or multiple pages without leaving Pi.
- Use docs that match the task — resolve libraries and retrieve focused, version-aware documentation with code examples.
- Find proven implementation patterns — search practical usage, setup, migrations, and error context across real code.
- Spend context wisely — compact search results, chunked page reads, bounded output, and fetch caching keep research useful without overwhelming the model.
- Compose research in scripts — typed results let codemode filter and join data without parsing model-facing text.
- Choose your providers — mix Exa, TinyFish, Brave, Firecrawl, markdown.new, and Context7 based on coverage, cost, and credentials.
Installation
Install from npm:
pi install npm:pi-web-kit
Install project-locally with Pi's -l flag:
pi install -l npm:pi-web-kit
During local development from this monorepo:
pi install /path/to/pi-mono/packages/pi-web-kit
For a one-off test run without installing:
pi -e /path/to/pi-mono/packages/pi-web-kit --web-provider-fetch markdown_new --print "Fetch https://example.com"
This is an npm-compatible TypeScript Pi package. Bun is not required. Use Pi 1.1.0 or newer and Node.js >=22.19.0 for the structured tool contracts.
Quick usage
Search:
Find recent documentation for the Pi extension API.
Multi-query search:
Search for recent docs on Pi extensions and Pi tool schemas.
Fetch a page:
Read https://example.com and summarize it.
Fetch a long page in chunks:
Fetch https://example.com/long-doc with limit 8000, then continue with offset 8000.
Pi chooses web_search or web_fetch automatically when the request calls for it. You can also mention provider settings explicitly in prompts, but provider changes usually require config or CLI flags.
Providers
Defaults: provider_search = "exa", provider_fetch = "exa".
| Provider | Search | Fetch | Key |
|---|---|---|---|
exa |
yes | yes | EXA_API_KEY |
tinyfish |
yes | yes | TINYFISH_API_KEY |
brave |
yes | no | BRAVE_SEARCH_API_KEY |
firecrawl |
yes | yes | FIRECRAWL_API_KEY |
markdown_new |
no | yes | none |
Tool schemas are tailored to the configured providers at startup/reload, so only supported provider-specific fields are exposed. Restart/reload Pi after changing provider config.
Provider-native efficiencies are used as follows:
| Service | Efficient path |
|---|---|
| Exa API | Use highlights-first /search, send task intent as objective, and batch /contents fetches when full pages are needed. Auto remains the default; Lite is opt-in. |
| TinyFish | Paginate only as needed; use dedicated filters; batch only enough top-result fetches to fill requested context; pass TTL and intent. |
| Brave | Return pre-extracted LLM Context in one search call with native URL, token, snippet, threshold, and Goggles controls. |
| Firecrawl | Return search metadata by default; use scrape-on-search only when requested; pass cache/main-content options. |
| markdown.new | Keep method: auto so native Markdown falls back to AI and browser rendering only as needed; keep images opt-in. |
| Context7 | Support fast mode to skip reranking and reduce latency. |
| Code examples | Use Exa /search with fixed Auto, live content, and bounded structured excerpts. Never Lite, synthesis, or Agent. |
Configuration
Resolution order: defaults < environment variables < global config < trusted project config < CLI flags. Project config is ignored unless Pi trusts the current project, including in print, JSON, and RPC modes.
Environment variables
PI_OFFLINE=1 # disables install/update telemetry
PI_TELEMETRY=0 # disables install/update telemetry
PI_WEB_KIT_PROVIDER_SEARCH=exa|tinyfish|brave|firecrawl
PI_WEB_KIT_PROVIDER_FETCH=exa|tinyfish|markdown_new|firecrawl
EXA_API_KEY=... # enables Exa provider and code_search
CONTEXT7_API_KEY=... # enables library_search and library_docs
TINYFISH_API_KEY=...
BRAVE_SEARCH_API_KEY=...
FIRECRAWL_API_KEY=...
Config files
Config files, in increasing precedence:
| Scope | Path |
|---|---|
| Global | ~/.pi/agent/pi-web-kit.json |
| Project | .pi-web-kit.json |
The global path follows Pi's getAgentDir() resolver. If PI_CODING_AGENT_DIR
is set, pi-web-kit.json is read from that directory instead of the default.
Example:
{
"provider_search": "firecrawl",
"provider_fetch": "markdown_new",
"apiKeys": {
"firecrawl": "...",
"context7": "...",
"exa": "..."
},
"markdownNew": {
"method": "auto",
"retainImages": false
}
}
Do not commit config files containing secrets. Project .pi-web-kit.json is ignored by this repo's .gitignore, but other repositories may need their own ignore rule.
CLI overrides
pi -e . --web-provider-search firecrawl --web-provider-fetch markdown_new --print "Search and fetch docs"
Provider CLI flags are temporary for the Pi process. Restart/reload Pi after changing provider config so registered tool schemas match the active provider.
Tools
Terminal result views
library_search, library_docs, and code_search show compact completion
summaries instead of raw JSON in the interactive transcript. Expand a tool result
with Pi's expand-tools action (default: Ctrl+O) to inspect ranked library candidates,
available versions, documentation sources, or code source titles and URLs.
Long fields are clipped to the terminal width; version previews show at most two
entries plus the remaining count. Code-search summaries flag reduced output;
expanded views show omitted-source and context counts when present. Library
documentation also shows omitted code/text snippet counts and refinement guidance.
All five tools remove terminal control sequences from untrusted display text
before applying theme styling.
These views change presentation only. Model-facing JSON and typed codemode results retain the same bounded, redacted data, including snippets and code. Final print, JSON, and RPC tool results are unchanged.
web_search
Searches with the active search provider and returns compact results grouped by query.
| Parameter | Type | Description |
|---|---|---|
query |
string | Single search query. |
queries |
string[] | Multiple related search queries. Max 5 after de-duplication. |
numResults |
integer | Desired results per query. Default: 10. Values above the active provider's limit are capped rather than rejected. |
contextTokens |
integer | Desired context across the result set. Default: 8,192; capped at 10,000. For Exa this only bounds returned highlights, not a request for page text. Other providers can enable extraction when explicit. |
purpose |
string | Required for Exa, optional for other search providers; up to 2,000 characters. Exa receives it as top-level objective to guide ranking and evidence selection. This does not select Deep Search or Agent. |
numResults controls source breadth. contextTokens controls grounding depth. The tool automatically keeps native/included Exa highlights and Brave LLM Context. Explicit contextTokens enables extra extraction for TinyFish and Firecrawl; this can add provider calls or provider cost. Exa always requests highlights and prefers them over text or summaries. Use web_fetch for complete pages, exact passages, or missing context. Search results keep a compact snippet plus ranked content and contentFormat, with one shared context budget and the existing 50KB tool-output limit.
Provider caps are Exa 100, Brave 50, and Firecrawl 100. TinyFish is paginated internally through its service maximum of page 10 and may make up to 11 search requests for one query. Search output reports requested, effective, returned, and omitted result/context counts. Brave maxUrls remains as a deprecated alias for numResults.
Other provider-specific parameters are exposed only for the configured provider. These include Exa date/domain filters; TinyFish domain, date, geography, language, and publication filters; Brave locale, freshness, spellcheck, Goggles, and LLM Context controls; and Firecrawl scrape/search options.
TinyFish preserves one-sided date/year bounds. If both endpoints are supplied in reverse order, they are reordered; an upper-only bound never becomes a lower bound.
Output limiting preserves complete source URLs. It reduces evidence or omits whole results rather than shortening citation targets.
Exa modes, filters, and notices
type: "auto"is the default.type: "lite"is an explicit lower-cost option that serves content from Exa's regularly refreshed index without query-time page fetching. When content is requested, Lite can exclude sources with no cached content. Use Auto when coverage or live freshness matters. Lite remains available for generalweb_search, but is never available incode_search.- Lite accepts the exposed domain/path, publication-date, category, result-count,
and purpose controls. Every
maxAgeHoursvalue, including-1, is rejected locally with guidance to use Auto. Unsupported overrides are not silently removed. Legacy search types remain accepted. startCrawlDateandendCrawlDateare deprecated and rejected: Exa ignores them. Publication dates describe a different constraint and are not substituted. Thecompanyandpeoplecategories reject publication-date filters;peoplealso rejectsexcludeDomains.- Exa query strings, including each
queriesentry, are limited to 2,000 characters.typeandcategoryare limited to 64 characters. Each domain filter accepts at most 1,200 entries of up to 2,048 characters each. Publication-date strings are limited to 128 characters and must be ISO 8601 dates. Blank query, purpose, mode, category, and domain/path strings are rejected. These local limits are declared in the tool schema. - Each query group can include
notices: [{ code, message, path? }]that explain changes to retrieval, such as relaxed query restrictions. At most 8 notices with 512-character messages are returned;omittedNoticeCountreports omitted entries. Notice codes are an open set. Arbitrary noticedetailsare excluded. - Ordinary
highlights: trueis the stable baseline. Dynamic Highlights is not enabled automatically: the announcement and current API guide disagree about its GA/beta status. The manual evaluation below tests the documented beta request separately; no beta header is sent by the production provider.
Exa /search calls emit best-effort
pi-web-kit:exa-diagnostics events through pi.events, outside model output.
Consumers can subscribe to capture request IDs, duration in milliseconds,
provider searchTime and costDollars.total when present, known search modes,
warning codes/field paths, and controlled failure categories/HTTP status.
Warning messages, queries, content, headers, and credentials are not included.
At most 8 warning identifiers are emitted, with omittedWarningCount for the
rest. No diagnostic file or remote log is created by this package.
Purpose, Search, Deep Search, and Agent
purpose is task intent, not a research-mode switch. Exa's Search best-practices
guide recommends a required objective in model-facing tools, including ordinary
Auto searches. Keep it short: name the implementation goal, exact dependency
version, and useful sources. The tool sends it as objective in the same request.
It does not send outputSchema, start Agent, or make a second API call.
| Product/path | Work performed | Cost/latency distinction |
|---|---|---|
Search Auto, /search |
Ranked sources and excerpts; Pi performs downstream reasoning | Standard Search price; live crawling adds latency |
Deep Search, /search with a Deep type |
Iterative research; optional synthesis via outputSchema |
Higher published price and latency than Auto; explicitly selected in web_search only |
Exa Agent, /agent/runs |
Asynchronous, multi-step research, enrichment, verification, and optional data partners | Fixed-effort or metered run pricing; not used by this package |
Exa lists no separate objective charge. That is not a claim of zero processing
overhead or identical latency. Auto is currently $7 per 1,000 requests up to 10
results; Deep types have separate prices. See pricing.
JSON returned by this extension is a local structured tool contract, not Exa's
paid synthesis feature.
web_fetch
Fetches page content with the active fetch provider. Results are cached in memory by canonical URL plus provider/config/fetch-affecting options.
| Parameter | Type | Description |
|---|---|---|
url |
string | Single URL. Must be http: or https:. |
urls |
string[] | Multiple URLs. Max 10 after de-duplication. |
offset |
integer | Character offset for cached/ranged reads. Single URL only. |
limit |
integer | Maximum characters to return. Default: 30,000 for one URL, 8,000 for multiple URLs. |
refresh |
boolean | Refetch even if cached. |
maxAgeMs |
integer | Desired maximum local/provider-cached page age in milliseconds. 0 requests live content where supported. |
Provider-specific parameters are exposed only for the configured provider. TinyFish supports purpose, format, links/images, selectors, per-URL timeout, and a seconds-based ttl alias. Exa supports the hours-based maxAgeHours alias. markdown.new supports method / retainImages. Firecrawl supports format, waitFor, mobile, structured location, and its existing maxAge alias. refresh: true also requests live content from Exa, TinyFish, and Firecrawl instead of only bypassing the local cache.
For Exa, explicit maxAgeHours must be an integer from -1 to 720, in
both search and fetch calls. Larger maxAgeMs values remain valid: their
provider-side hour conversion is capped at 720, which requests content at least
as fresh as the caller requires. Sub-hour values round down, possibly to zero.
The original millisecond limit still governs the local cache. Explicit invalid
hour values are rejected even when refresh: true would otherwise override
them.
Exa Contents responses are matched by requested document IDs before canonical URLs. Per-URL error statuses trigger fallback retrieval for only the failed or missing pages; they cannot borrow another URL's result or cache it under the failed URL. Failed pages are not cached. Identity-less legacy redirect results use positional matching only for a complete batch or a single unambiguous pair.
TinyFish and Firecrawl also retain the requested URL across redirects, including
when a redirect lands on another URL in the same batch. url is the requested
identity and fetchedUrl is the returned final URL.
Exa fallback retrieval uses TinyFish, then Firecrawl when their keys are
configured, then markdown.new. Only structured failures or empty content trigger
fallback, never error-like phrases in ordinary page text. Explicit Exa freshness
is translated to TinyFish ttl or Firecrawl maxAge, rounding toward fresher
content. A freshness-constrained request skips markdown.new because it has no
equivalent control. Cached-only Exa (maxAgeHours: -1) never uses another
provider; refresh: true overrides that mode to request live content without
populating its cached-only cache key.
TinyFish links: true and imageLinks: true expose links and imageLinks
arrays of absolute HTTP(S) URLs in each successful result. Invalid,
credential-bearing, duplicate and excess links are omitted. Each array retains
at most 50 URLs, with one shared 12,500-byte link budget across the call.
omittedLinkCount and omittedImageLinkCount include validation and output-budget
omissions. Link URLs remain complete, including useful fragments; these fields
are also available on cached reads. Fetch content limiting preserves both URL
identities and updates range.nextOffset for continuation.
library_search
Resolves packages, frameworks, SDKs, APIs, CLIs, and libraries to canonical library IDs.
| Parameter | Type | Description |
|---|---|---|
libraryName |
string | Library/package/framework name to search for. |
query |
string | Optional user task/question for relevance ranking. |
fast |
boolean | Skip LLM reranking for lower latency. |
limit |
integer | Maximum libraries to return. Range: 1-20. Default: 10. |
Both Context7 tools prefer provider-enforced strict argument schemas when supported. Unsupported providers or schemas retain best-effort tool calling; local validation always applies. In Pi 1.4.0, Anthropic falls back because its strict schemas do not support these numeric bounds.
library_docs
Fetches current docs and code snippets for a library. Provide libraryId, or provide libraryName and the tool resolves the best match first.
| Parameter | Type | Description |
|---|---|---|
libraryId |
string | Canonical library ID, such as /vercel/next.js. |
libraryName |
string | Library name to resolve when libraryId is not known. |
query |
string | Specific docs question or coding task. |
version |
string | Optional version/tag to pin, appended as @version. |
fast |
boolean | Skip LLM reranking for lower latency. |
limit |
integer | Maximum code and info snippets to return. Range: 1-20. Default: 10. |
Documentation output keeps complete snippets, including their code and token
metadata. If the serialized result exceeds 50,000 bytes, whole snippets are
omitted; omittedCodeSnippetCount and omittedInfoSnippetCount report exact
counts. Smaller later snippets can still be returned. A message asks you to
refine the query or lower limit; an empty snippet array with omission counts
means the output budget was reached, not that no documentation exists.
Library search similarly omits whole oversized candidates with
omittedResultCount, preserving IDs and versions.
code_search
Finds practical code examples, implementation context, setup snippets, migrations,
usage patterns, and error-message research through Exa Search Auto. The endpoint
is /search, not the undocumented legacy /context.
| Parameter | Type | Description |
|---|---|---|
query |
string | Required, up to 2,000 characters. Include language, exact dependency version, and operation or error. |
purpose |
string | Required nonblank task intent, up to 2,000 characters; sent as Search objective. |
numResults |
integer | Source breadth, 1–10. Default: 5. The ceiling avoids result-count surcharges. |
contextTokens |
integer | Local excerpt budget, 1–8,192. Default: 8,192. This is an approximate character-based budget, not an upstream token or synthesis request. |
includeDomains, excludeDomains |
string[] | Optional hard domain/path constraints; at most 1,200 entries of up to 2,048 characters each. Prefer soft source preferences in purpose. |
Code search always sends:
{
"type": "auto",
"contents": {
"highlights": true,
"maxAgeHours": 0,
"livecrawlTimeout": 15000
}
}
There is no Lite, legacy fast/instant, Deep, cached-only, historical snapshot,
beta, or Agent override. No summary, outputSchema, or synthesized answer is
requested. It uses the Exa key regardless of the providers selected for general
web search and fetch. Search responses are not locally cached.
maxAgeHours: 0 requests freshly fetched page content under Exa's documented
contract. It does not guarantee that every newly published page is indexed,
that a source matches the project's version, or that the provider's freshness
can be independently attested: Search does not return a per-page verified crawl
timestamp. publishedDate is an estimated publication date, not proof of a fresh
crawl. No publication-date filter is imposed on code search, because current
evergreen references can have old publication dates.
Only highlight evidence is returned. Metadata-only results and responses with
only text/summary fallbacks are omitted and counted in omittedNoContentCount;
the tool never substitutes cached-only retrieval when crawling fails. Errors
surface without automatic retry or endpoint/mode fallback. The HTTP deadline is
45 seconds, including JSON body consumption; the provider's individual live
crawl timeout is 15 seconds.
Results preserve complete source URLs, titles, excerpts, and publication dates
when present, with exact result/context omission accounting within the
50,000-byte JSON limit. Character counts use JavaScript UTF-16 string length;
prefixes do not split surrogate pairs. If evidence is incomplete, narrow the
query or use web_fetch with refresh: true on a cited URL. Excerpts are not
guaranteed to contain complete compilable functions.
Breaking migration: provide purpose for both Exa search tools, replace
tokensNum/"dynamic" with contextTokens, and read results[].content instead
of the old combined response. Legacy response/token metadata and aliases are
removed rather than silently translated.
Cache and limits
web_fetch uses an in-memory cache for the current Pi process.
| Limit | Value |
|---|---|
| Cache TTL | 30 minutes |
| Max cached entries | 100 |
| Max cached bytes | 20 MiB of serialized entries, including keys and retained metadata |
| Max incoming provider response | 8 MiB of decoded body bytes |
| Max URLs per call | 10 |
| Max queries per call | 5 |
Provider numResults caps |
Exa 100; Brave 50; Firecrawl 100 |
| Search context budget | 10,000 tokens |
| Code-search local context budget | 8,192 approximate tokens |
| Code-search result limit | 10 (default 5) |
| Serialized tool output | 50,000 bytes |
| Fetch link arrays | 50 URLs per array; shared 12,500-byte output budget |
| Max URL length | 2048 characters |
Cache keys include the provider, canonical URL, fetch-affecting parameters, relevant provider defaults, and an opaque HMAC-SHA-256 API-key/account scope with a random process-local key. Internal cache keys are never returned in tool output. refresh: true bypasses and replaces the cached entry.
Exa cache scopes also include fallback accounts and markdown.new defaults. Raw provider metadata is not retained by the fetch cache; only page fields and validated, requested link fields are stored. Oversized entries are served for the current call but are not cached.
Provider bodies are streamed through the incoming byte limit before JSON parsing or cache insertion. Requests that exceed it fail instead of returning partial documents. Text and JSON request deadlines include body consumption; markdown.new's 45-second deadline also applies when used as an Exa fallback. Provider API redirects are rejected, not followed with credentials.
Structured results and discovery
All five tools declare outputSchema and return objects through structuredContent.
Codemode receives those objects directly; remove old JSON.parse(await tools.…())
wrappers. Direct calls still return useful JSON text with exactly the same
bounded, redacted value. Renderer details remain compact and are not the
script API.
| Tool | Script result |
|---|---|
web_search |
{ provider, queries: [{ query, requestedResultLimit, effectiveResultLimit, requestedContextTokens?, effectiveContextTokens, contextCharacters, omittedContextCharacters?, resultCount, omittedResultCount?, notices?, omittedNoticeCount?, results }] }. Each result has url and optional title, snippet, content, contentFormat, siteName, publishedDate, position. |
web_fetch |
{ provider, results }. Each item is either { url, error } or { url, fetchedUrl, title?, content, format, cached, refreshed, range, links?, imageLinks?, omittedLinkCount?, omittedImageLinkCount? }. range has offset, limit, returned, total, truncated, hasPrevious, hasNext, and optional nextOffset. |
library_search |
{ provider: "context7", libraryName, query, searchFilterApplied?, omittedResultCount?, results }. Each candidate has id; title, description, branch, dates, state, scores, counts, stars and versions are optional. |
library_docs |
{ provider: "context7", libraryId, query, codeSnippets, infoSnippets, omittedCodeSnippetCount?, omittedInfoSnippetCount?, message? }. Code snippets expose optional title/description/language/ID/page/source/token fields and codeList: [{ language, code }]; info snippets require content with optional page/breadcrumb/token fields. |
code_search |
{ provider: "exa", query, searchType: "auto", maxAgeHours: 0, requestedResultLimit, effectiveResultLimit, requestedContextTokens?, effectiveContextTokens, contextCharacters, omittedContextCharacters?, resultCount, omittedResultCount?, omittedNoContentCount?, notices?, omittedNoticeCount?, results }. Sources have the same fields as web search. Freshness fields describe the request policy, not independent freshness verification. |
Every result also allows the top-level fallback { truncated: true, message }
when metadata alone cannot fit. Check this before reading ordinary fields.
Both text and structured payloads stay within the same 50,000-byte JSON budget.
Fetch slices retain range continuation; search results report omitted counts.
Missing/invalid optional provider fields are omitted. Invalid required fields
fail the call rather than yielding a guessed success object.
const search = await tools.web_search({ query: "Pi extension API", purpose: "Implement a tool with the installed Pi version; prefer official extension docs", numResults: 3 });
if ("truncated" in search) throw new Error(search.message);
const urls = search.queries.flatMap(q => q.results.map(r => r.url));
if (urls.length) {
const pages = await tools.web_fetch({ urls: urls.slice(0, 10) });
if ("truncated" in pages) throw new Error(pages.message);
text(pages.results.filter(r => !("error" in r)).map(r => ({
url: r.url, excerpt: r.content.slice(0, 500), nextOffset: r.range.nextOffset
})));
}
Validation, whole-request/provider, malformed-output and cancellation failures
throw: direct calls become Pi tool errors and scripts must use try/catch or
Promise.allSettled. Individual fetch failures are successful result data with
an error field, not isError: true. These tools do not return structured success
objects alongside isError: true. A trusted result hook can replace that contract;
annotations do not bypass hooks or approvals.
The namespace is web, not a name prefix: calls remain tools.web_fetch(…).
await describeNamespace("web"), await describeTool("web_fetch"), and
await searchTools("page content", { namespace: "web" }) work even with
codemode.inlineBudget: 0. Independent reads can run in parallel. The advisory
hints are read-only, non-destructive, idempotent, and open-world: repeated reads
can return different content and incur provider charges.
Direct exposure remains the default, and codemode is optional. No package-level
deferred/codemode exposure switch is added: it would make inactive direct tools
nested-callable and change the meaning of disabling them. Use Pi's codemode.mode
(on or only) and inlineBudget to reduce declarations while retaining the
active-tool boundary. Optional research tools still require their provider keys.
Explicit defaultTools -name entries are honored at late registration; reload
restores Pi's active selection rather than re-enabling manually disabled tools.
New optional tools activate when credentials first become available on reload,
including existing positive defaultTools selections. Previously registered
tools are not force-reactivated, even if settings still positively select them.
The extension records only previously registered tool names in branch-local
pi-web-kit:registered-tools session metadata, outside model context; it does
not store activation choices or credentials. The record is refreshed before
reload so navigating to older branch history cannot revive a manually disabled
tool. Older sessions without this metadata preserve inactivity on their first
reload, so they may require explicit activation or a restart.
Malformed/unknown-version metadata uses the same
conservative fallback.
CLI exclusions remain host-enforced.
Privacy and security
pi-web-kit sends search queries and fetched URLs to the configured provider. Developer-search tools send library/doc queries to Context7 and code-context queries to Exa when those tools are enabled. Fetch providers may also receive provider-specific options. API keys are read from environment variables or local config files and are used only for provider requests.
The extension rejects non-HTTP(S) URLs and URLs with embedded username/password credentials.
Returned data is projected to declared public fields; raw provider metadata,
Context7 rules, cache keys and HTTP error bodies are not returned. Known API
keys, URL credential patterns and Basic/Bearer authorization-header values are masked in text, structured data,
renderer details and progress. This is not a general sensitive-data scanner:
page content remains untrusted, and Pi retains caller-supplied arguments in its
own transcript. Do not put credentials in queries or URLs.
Report security issues privately. See SECURITY.md.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
provider requires ... API_KEY |
Selected provider needs an API key. | Set the provider's env var or apiKeys config entry. |
| Provider mismatch / schema error after config change | Pi registered tools for the previous startup provider. | Restart/reload Pi after provider changes. |
| Invalid URL / scheme / credentials error | URL validation rejected the input. | Use an absolute http: or https: URL without username/password credentials. |
| Timeout error | Provider request exceeded its timeout. | Retry, reduce URL count, or switch provider. |
| No content returned | Provider returned no matching content or a redirected/canonicalized response could not be mapped. | Retry with refresh: true, fetch a single URL, or switch provider. |
| Large page is truncated | Tool output is bounded to valid JSON under 50KB. | Continue with the returned range.nextOffset. |
Development
Requirements:
- Node.js >= 22.19.0
- npm
Common commands:
npm install
npm run check
npm test
npm audit --omit=dev
npm run pack:dry-run
Context7 strict-sampling checks
The SDK regression tests cover strict-capable OpenAI requests, unsupported routes, Anthropic's numeric-bound fallback, optional argument defaults, and local rejection before Context7 access:
node --import tsx --test packages/pi-web-kit/tests/strict-sampling.test.mjs
To repeat against an installed Pi host instead of the pinned development SDK,
set PI_WEB_KIT_TEST_RUNTIME to its directory containing the pi-ai and
pi-coding-agent packages:
PI_WEB_KIT_TEST_RUNTIME=/path/to/node_modules/@earendil-works \
node --import tsx --test packages/pi-web-kit/tests/strict-sampling.test.mjs
For a small live smoke test, configure Context7 and model credentials, select a strict-capable model, and run from the monorepo root:
pi -ne -e ./packages/pi-web-kit --no-session --no-context-files \
--tools library_search,library_docs --print \
"Call library_search for Node.js with limit 1. Then call library_docs for the returned ID about node:fs readFile with limit 1. Report whether both calls succeeded."
Confirm that both calls succeed without schema errors. This is a paid model and Context7 smoke test, not a guarantee of provider support on every route.
Manual terminal smoke test
From the monorepo root, with Context7 and Exa credentials configured:
pi -ne -e ./packages/pi-web-kit
Ask Pi to run library_search for a library with multiple matches, then
library_docs and code_search for a specific API question. Check that:
- Each running tool shows a short pending message without a percentage.
- Each completed result shows two summary rows rather than raw JSON.
- The expand-tools action (default: Ctrl+O) reveals candidate IDs and short version previews, the resolved docs library and sources, or ranked code URLs.
- Narrowing the terminal clips fields rather than adding wrapped output rows.
- Empty results and provider failures do not show a success heading.
Repeat in regular and fullscreen terminal modes when changing renderer layout. Automated tests cover mocked provider calls through the registered renderers, data-contract preservation, errors, output limits, Unicode, and narrow widths; this manual check covers the host's interactive expansion and redraw behavior.
Manual Exa comparison and smoke test
From the monorepo root, with EXA_API_KEY in the environment:
node --import tsx packages/pi-web-kit/tests/exa-evaluation.mjs --live
This opt-in, paid run sends 12 requests: three fixed public coding queries
(Node.js streams, Pydantic v2 migration, React 19 actions) through production
fresh-only code search, general Auto highlights, general Lite highlights, and
Auto Dynamic Highlights with the documented
Exa-Beta: dynamic-highlights-2026-08-28 header. It makes no retries or follow-up
fetches. Lite and Dynamic are comparison paths for general web search, never
code-search configuration options. All paths request 5 results with an
8,000-character local search-context budget, followed by the same public output
projection and byte limiter used by the tools.
The JSON output includes latency, reported cost when available, raw/returned
character counts, serialized bytes, and source excerpts for human inspection.
approximateTokens is characters divided by four, not a tokenizer measurement.
Review version/operation match, usable code, citations, and whether a follow-up
fetch is needed. This is a small retrieval smoke test, not a downstream
answer-quality benchmark or proof that a returned page matches a dependency
version. The earlier legacy-Context comparison did not test this fresh-only
policy and is not a release gate for migration away from that API.
The deterministic tests exercise all 16 general search/fetch provider combinations, every documented general search mode with freshness controls, strict code-search override rejection, source/budget boundaries, stale-text poisoning, credential masking, notices, HTTP failures, cancellation, and a stalled response body after headers. Root contract tests exercise real Pi direct and codemode dispatch, including rejection before provider access.
Contract references: Search, Highlights, and Code & Docs. The live OpenAPI is linked from those pages; check it again before changing beta defaults.
This package is source-distributed. Pi loads the TypeScript extension files directly via its extension loader.
Contributing
Contributions are welcome. See CONTRIBUTING.md for development workflow and pull request guidelines.
License
MIT. See LICENSE.