A pi extension that gives the agent two metered, cost-conscious tools backed by the Kagi API: kagi_search and the page extractor kagi_extract.
The agent is steered toward cheap behavior through tool prompt guidelines — search first, extract only pages you intend to read, page with offset/limit rather than re-issuing calls — and through two in-memory caches (search result sets keyed by query, extracted pages keyed by URL) that make paging and repeat lookups free. It also enforces a maximum of two uncached searches per agent run, while leaving extraction unrestricted. The extension has zero runtime dependencies: it uses native fetch and Node modules only, and the pi-provided peer packages (@earendil-works/pi-coding-agent, typebox) are resolved at runtime to pi's own bundled copies via the extension loader's jiti alias map, so installing this package never drags in a duplicate copy of pi.
kagi_search and kagi_extract call costs a small amount against that balance — which is exactly why this extension caches aggressively and nudges the agent to be sparing.pi CLI) installed and on your PATH..ts source directly via jiti, so there is no build step).pi-kagi reads its key from the KAGI_API_KEY environment variable at tool-call time. Set it in your shell profile so every pi session inherits it:
# ~/.zshrc, ~/.bashrc, etc.
export KAGI_API_KEY="your_kagi_api_key"
No
.envfile is read or shipped. pi resets and cleans its managed package clones on update, which would silently delete a.envmanaged alongside the extension. A.env.exampleis committed to this repo only as documentation of the variable name — it is never loaded by the extension. Keep your real key in your shell environment.(
.envis gitignored, so if you keep one for other tooling it won't be committed. Just don't rely on the extension to load it.)
This extension is distributed from GitHub only — it is not published to npm. There are two install routes.
pi install (recommended for personal use)pi install git:github.com/themauveavenger/pi-kagi
This clones the repo into pi's managed package directory and writes it to your user settings (~/.pi/agent/settings.json). Unpinned installs are updated by pi update --extensions (or pi update --all).
To pin to a release tag, append @vX.Y.Z:
pi install git:github.com/themauveavenger/pi-kagi@v1.0.0
Pinned refs are not moved by pi update --extensions / pi update --all — those commands only reconcile an existing clone back to its configured ref. To upgrade a pinned install, re-run pi install git:github.com/themauveavenger/pi-kagi@vX.Y.Z with the new tag (or drop the @… pin to track the default branch).
To remove it:
pi remove git:github.com/themauveavenger/pi-kagi
package.json (recommended for shared/reproducible setups)For a project that needs a reproducible, version-controlled extension, add pi-kagi as a git dependency and let pi load it from that project's node_modules:
// package.json in the consuming project
{
"dependencies": {
"pi-kagi": "github:themauveavenger/pi-kagi#v1.0.0"
}
}
npm install
Then register that installed copy with pi as a local-path package in the project's pi settings (.pi/settings.json):
// .pi/settings.json in the consuming project
{
"packages": [
"./node_modules/pi-kagi"
]
}
Or, equivalently, let pi install write that entry for you:
pi install -l ./node_modules/pi-kagi
Why this is lean: npm install of a git dependency installs the package's dependencies only. pi-kagi declares its pi-provided peers (@earendil-works/pi-coding-agent, typebox) as optional peer dependencies (peerDependenciesMeta) and pins them only in devDependencies (for this repo's own typechecking) — so a consumer's npm install pulls in no extra packages: no duplicate copy of pi, no typebox. At runtime pi's extension loader aliases typebox and @earendil-works/pi-coding-agent to its own bundled copies, so the extension loads with zero installed dependencies.
To upgrade, bump the tag in package.json (github:themauveavenger/pi-kagi#vX.Y.Y) and re-run npm install. Pinned git refs are not moved by pi update.
kagi_searchSearch the web with Kagi. Returns a compact markdown list of results.
| Parameter | Type | Default | Notes |
|---|---|---|---|
query | string, required | — | Sent as-is. |
limit | integer 1–25 | 10 | Client-side slice; not sent to the API. |
offset | integer ≥ 1 | 1 | 1-based index to page into the cached result set; no new paid call. |
The full single-pass result set for a query is cached in memory (see Cache lifetime, FIFO eviction at 50 queries), so paging with offset/limit and repeating identical queries both cost nothing. Cache hits are marked (from cache). Non-web result types present in the response (news, direct answers, infoboxes, related searches) are rendered as labeled sections; machine-only noise (props, proxy image URLs, language probabilities) is stripped.
kagi_search permits at most two uncached searches while pi works on one prompt. The allowance resets only after pi settles and control returns to you. Cached repeats and pagination are free and do not count; kagi_extract is not capped.
The footer shows the search budget plus paid-search, paid-extract, and cache-hit totals. Its wording follows the run lifecycle, so a count left on screen while you have control is never mistaken for a standing cap:
| Footer text | Meaning |
|---|---|
search 0/2 per run | No run has begun yet in this session — the number is the per-run allowance. |
search 1/2 this run | A run is in progress with budget left. |
search 2/2 this run (limit reached) | A run is in progress and has spent its budget. |
search 2/2 last run · resets next run | The run finished; that was its cost, and the next run starts fresh. |
Toggle the footer for the current session with /kagi.
kagi_extractExtract a single web page's content as markdown. Long pages are paged with offset/limit in lines, mirroring pi's built-in read tool.
| Parameter | Type | Default | Notes |
|---|---|---|---|
url | string, required | — | A single HTTPS URL. |
limit | integer 1–2000 | 250 | Lines of extracted markdown to return. |
offset | integer ≥ 1 | 1 | 1-based line number to start from. |
Extracted pages are cached by URL (see Cache lifetime, FIFO eviction at 100 pages), so reading further slices of the same page is free. A page that fails extraction inside an otherwise-successful call returns its failure reason as ordinary content (not a tool error) so the agent can fall back to the search snippet. Whole-call HTTP failures throw with a plain-language message including Kagi's error code and meta.trace ID. Every tool response is backstopped by a 50 KB byte cap regardless of line limits.
Both caches live in module scope, so they are shared across sessions: a page or query paid for before /new or /resume is still free afterwards. pi caches the extension factory and re-invokes it per session rather than re-importing the module, so factory-local state would not survive a session switch — module-level state does.
The caches are dropped only by /reload, which clears pi's extension cache and re-evaluates the module, and by switching to a different working directory, which must not leak results across projects. Neither cache has a TTL; entries live until FIFO eviction, so a cached result can be stale. Use refresh: true on kagi_extract for a page that may have changed.
The search budget and the footer's paid/cache counters are deliberately not shared: the budget is per run, and a new session's counters start at zero.
Because the caches outlive the extension factory, they also outlive an individual test. resetSharedCaches() is exported for tests that load the extension more than once and need each case to start cold.
Releases are git tags, kept in sync with the version field in package.json (semver, starting at 1.0.0).
To cut a release:
version in package.json and commit.git tag vX.Y.Z.git push origin vX.Y.Z.Consumers pin to these tags (see the install routes above). There is no npm publish step.
npm install # installs pi + typebox as dev deps for typechecking
npm run typecheck # tsc --noEmit
npm test # node --test "test/**/*.test.ts" (Node 26 native TS type stripping)
The test suite uses a single seam — the tool execute boundary with an injected stub fetch (test/helpers.ts) — so it exercises the client, cache, and format modules together with no network and no mocking library. See docs/planning/kagi-tools/spec.md for the design and the ticket history under docs/planning/kagi-tools/issues/.
License: MIT.
25 commits
TypeScript
99.2%
A pi extension that gives the agent two metered, cost-conscious tools backed by the Kagi API: kagi_search and the page extractor kagi_extract.
The agent is steered toward cheap behavior through tool prompt guidelines — search first, extract only pages you intend to read, page with offset/limit rather than re-issuing calls — and through two in-memory caches (search result sets keyed by query, extracted pages keyed by URL) that make paging and repeat lookups free. It also enforces a maximum of two uncached searches per agent run, while leaving extraction unrestricted. The extension has zero runtime dependencies: it uses native fetch and Node modules only, and the pi-provided peer packages (@earendil-works/pi-coding-agent, typebox) are resolved at runtime to pi's own bundled copies via the extension loader's jiti alias map, so installing this package never drags in a duplicate copy of pi.
kagi_search and kagi_extract call costs a small amount against that balance — which is exactly why this extension caches aggressively and nudges the agent to be sparing.pi CLI) installed and on your PATH..ts source directly via jiti, so there is no build step).pi-kagi reads its key from the KAGI_API_KEY environment variable at tool-call time. Set it in your shell profile so every pi session inherits it:
# ~/.zshrc, ~/.bashrc, etc.
export KAGI_API_KEY="your_kagi_api_key"
No
.envfile is read or shipped. pi resets and cleans its managed package clones on update, which would silently delete a.envmanaged alongside the extension. A.env.exampleis committed to this repo only as documentation of the variable name — it is never loaded by the extension. Keep your real key in your shell environment.(
.envis gitignored, so if you keep one for other tooling it won't be committed. Just don't rely on the extension to load it.)
This extension is distributed from GitHub only — it is not published to npm. There are two install routes.
pi install (recommended for personal use)pi install git:github.com/themauveavenger/pi-kagi
This clones the repo into pi's managed package directory and writes it to your user settings (~/.pi/agent/settings.json). Unpinned installs are updated by pi update --extensions (or pi update --all).
To pin to a release tag, append @vX.Y.Z:
pi install git:github.com/themauveavenger/pi-kagi@v1.0.0
Pinned refs are not moved by pi update --extensions / pi update --all — those commands only reconcile an existing clone back to its configured ref. To upgrade a pinned install, re-run pi install git:github.com/themauveavenger/pi-kagi@vX.Y.Z with the new tag (or drop the @… pin to track the default branch).
To remove it:
pi remove git:github.com/themauveavenger/pi-kagi
package.json (recommended for shared/reproducible setups)For a project that needs a reproducible, version-controlled extension, add pi-kagi as a git dependency and let pi load it from that project's node_modules:
// package.json in the consuming project
{
"dependencies": {
"pi-kagi": "github:themauveavenger/pi-kagi#v1.0.0"
}
}
npm install
Then register that installed copy with pi as a local-path package in the project's pi settings (.pi/settings.json):
// .pi/settings.json in the consuming project
{
"packages": [
"./node_modules/pi-kagi"
]
}
Or, equivalently, let pi install write that entry for you:
pi install -l ./node_modules/pi-kagi
Why this is lean: npm install of a git dependency installs the package's dependencies only. pi-kagi declares its pi-provided peers (@earendil-works/pi-coding-agent, typebox) as optional peer dependencies (peerDependenciesMeta) and pins them only in devDependencies (for this repo's own typechecking) — so a consumer's npm install pulls in no extra packages: no duplicate copy of pi, no typebox. At runtime pi's extension loader aliases typebox and @earendil-works/pi-coding-agent to its own bundled copies, so the extension loads with zero installed dependencies.
To upgrade, bump the tag in package.json (github:themauveavenger/pi-kagi#vX.Y.Y) and re-run npm install. Pinned git refs are not moved by pi update.
kagi_searchSearch the web with Kagi. Returns a compact markdown list of results.
| Parameter | Type | Default | Notes |
|---|---|---|---|
query | string, required | — | Sent as-is. |
limit | integer 1–25 | 10 | Client-side slice; not sent to the API. |
offset | integer ≥ 1 | 1 | 1-based index to page into the cached result set; no new paid call. |
The full single-pass result set for a query is cached in memory (see Cache lifetime, FIFO eviction at 50 queries), so paging with offset/limit and repeating identical queries both cost nothing. Cache hits are marked (from cache). Non-web result types present in the response (news, direct answers, infoboxes, related searches) are rendered as labeled sections; machine-only noise (props, proxy image URLs, language probabilities) is stripped.
kagi_search permits at most two uncached searches while pi works on one prompt. The allowance resets only after pi settles and control returns to you. Cached repeats and pagination are free and do not count; kagi_extract is not capped.
The footer shows the search budget plus paid-search, paid-extract, and cache-hit totals. Its wording follows the run lifecycle, so a count left on screen while you have control is never mistaken for a standing cap:
| Footer text | Meaning |
|---|---|
search 0/2 per run | No run has begun yet in this session — the number is the per-run allowance. |
search 1/2 this run | A run is in progress with budget left. |
search 2/2 this run (limit reached) | A run is in progress and has spent its budget. |
search 2/2 last run · resets next run | The run finished; that was its cost, and the next run starts fresh. |
Toggle the footer for the current session with /kagi.
kagi_extractExtract a single web page's content as markdown. Long pages are paged with offset/limit in lines, mirroring pi's built-in read tool.
| Parameter | Type | Default | Notes |
|---|---|---|---|
url | string, required | — | A single HTTPS URL. |
limit | integer 1–2000 | 250 | Lines of extracted markdown to return. |
offset | integer ≥ 1 | 1 | 1-based line number to start from. |
Extracted pages are cached by URL (see Cache lifetime, FIFO eviction at 100 pages), so reading further slices of the same page is free. A page that fails extraction inside an otherwise-successful call returns its failure reason as ordinary content (not a tool error) so the agent can fall back to the search snippet. Whole-call HTTP failures throw with a plain-language message including Kagi's error code and meta.trace ID. Every tool response is backstopped by a 50 KB byte cap regardless of line limits.
Both caches live in module scope, so they are shared across sessions: a page or query paid for before /new or /resume is still free afterwards. pi caches the extension factory and re-invokes it per session rather than re-importing the module, so factory-local state would not survive a session switch — module-level state does.
The caches are dropped only by /reload, which clears pi's extension cache and re-evaluates the module, and by switching to a different working directory, which must not leak results across projects. Neither cache has a TTL; entries live until FIFO eviction, so a cached result can be stale. Use refresh: true on kagi_extract for a page that may have changed.
The search budget and the footer's paid/cache counters are deliberately not shared: the budget is per run, and a new session's counters start at zero.
Because the caches outlive the extension factory, they also outlive an individual test. resetSharedCaches() is exported for tests that load the extension more than once and need each case to start cold.
Releases are git tags, kept in sync with the version field in package.json (semver, starting at 1.0.0).
To cut a release:
version in package.json and commit.git tag vX.Y.Z.git push origin vX.Y.Z.Consumers pin to these tags (see the install routes above). There is no npm publish step.
npm install # installs pi + typebox as dev deps for typechecking
npm run typecheck # tsc --noEmit
npm test # node --test "test/**/*.test.ts" (Node 26 native TS type stripping)
The test suite uses a single seam — the tool execute boundary with an injected stub fetch (test/helpers.ts) — so it exercises the client, cache, and format modules together with no network and no mocking library. See docs/planning/kagi-tools/spec.md for the design and the ticket history under docs/planning/kagi-tools/issues/.
License: MIT.
25 commits
TypeScript
99.2%