Skip to content

Repository files navigation

pi-quiver

pi-quiver

Buy Me A Coffee

Ground-truth ingestion for the Pi coding agent: pull real web pages, docs, and local files into context without flooding it.

The problem

Reasoning from a model's training memory instead of the real page, the current docs, or the actual PDF is how agents confidently ship wrong answers about APIs that changed last month. Mature engineering work has to be data-driven - the agent needs to read the real source.

But the moment an agent does that, one fetch or PDF read can dump hundreds of kilobytes of boilerplate into context, degrading every turn after it.

Why pi-quiver exists

fetch brings real web pages and GitHub issues/PRs into context and is size-gated by construction: over 32 KB or 1000 lines spills to a temp file with a preview and a grep/read hint. doc_to_md converts local PDF/DOCX/PPTX/XLSX/XLS files into a Markdown bundle on disk and returns only a concise handle. Ingestion is what makes data-driven work possible; bounded tool results keep it safe.

session-name, sword-header, fast-mode, provider-stall-watchdog, and slack are opt-in ergonomics, recovery, and integration controls: session labeling, a themed startup header, Anthropic fast mode, semantic-stall recovery, and context-safe Slack search/threads/posting with repo-policy injection, @name mention resolution, cached emails, and per-call unfurl control.

Part of the pi agent toolkit

Four independent extensions for the pi coding agent, each owning one concern of running agents seriously:

  • pi-quiver - capabilities (fetch, doc conversion, session tools)
  • pi-cohort - coordination (delegate to focused child agents)
  • pi-condense - context economy (prune context, keep it recoverable)
  • pi-gauntlet - process (the gated brainstorm->ship workflow)

No code dependency between them. pi-quiver is call-level: fetch gates the size of what comes in, while doc_to_md writes a bundle and returns its handle. pi-condense is loop-level: it prunes what's already in context once a tool call is done. Different problem, same discipline.

Mental model

Every ingestion extension here is context-safe by construction, not by convention: fetch size-gates every call, while doc_to_md always writes a bundle and returns a handle instead of inline Markdown. fetch and doc_to_md bring real sources in; session-name, sword-header, fast-mode, provider-stall-watchdog, and slack are opt-in.

flowchart LR
    S[web page] --> T[fetch]
    T --> E[extract main content]
    E --> G{"over 32KB or 1000 lines?"}
    G -->|no| I[return inline to context]
    G -->|yes| F[spill to temp file<br/>return preview + grep/read hint]
    D[local PDF / Office / Excel] --> B[doc_to_md]
    B --> H[Markdown bundle on disk<br/>return handle]
Loading

Quick example

pi install npm:pi-quiver
> fetch https://example.com/some-huge-changelog
Saved-To: /tmp/pi-fetch/2026-...-example.com-....md
60-line preview follows. grep '^#' the file for headings, or read a slice.

A 300 KB changelog page never touches your context window - you get a preview and a path.

Architecture

Extension Tool What it does
extensions/fetch.ts fetch Retrieve URLs over HTTP(S). HTML -> Markdown (Readability extraction, Turndown conversion). Binary saved untouched to a temp file. GitHub issue/PR/repo/actions-run/actions-job URLs auto-route through gh (falls back to HTTP); failed runs/jobs include failed-step logs (best-effort, summary-only otherwise). Same size gate as fetch. Behavior lives in lib/fetch-core.ts; also exposed as the pi-quiver fetch CLI (see Claude Code support).
extensions/doc_to_md.ts doc_to_md Convert a local PDF/DOCX/PPTX/XLSX/XLS to a Markdown bundle on disk (<stem>.md + images/ and spreadsheet sheets/) and return a handle (paths, page count, outline, diagnostics) - never inline Markdown. info mode inspects first; pages selects 1-based pages; every page ends with --- end of page.page_number=N ---. Tiers: pymupdf4llm -> PyMuPDF text (degraded) -> unpdf worker (no Python only). Excel -> sheet inventory, full CSVs, bounded previews, and optional rendered views. Settings under quiver.docToMd.
extensions/session-name.ts /session-name Manual + opt-in automatic session naming, naming rules and deny list, long-session revisits, and Ghostty/Herdr tab rename. OFF by default.
extensions/sword-header.ts /builtin-header Themed ASCII startup header replacing pi's default logo. OFF by default.
extensions/fast-mode.ts /fast Inject Anthropic fast-mode (speed: "fast" + anthropic-beta: fast-mode-2026-02-01) into every Claude Opus 4.8 / Opus 5 request, any thinking level. --fast flag + /fast [on|off|status]. OFF by default.
extensions/provider-stall-watchdog.ts - Opt-in provider-stall recovery, in two tiers: a pre-first-event deadline (firstEventMs, 20s) on every provider request in every mode, and the mid-stream pair (warn at 2 min, recover at 4 min) in TUI runs only. Policy D offers each stall to Pi's retry loop until the stall retry budget (maxStallRetries, default = retry.maxRetries) is exhausted. OFF by default.
extensions/slack.ts slack_search, slack_thread, slack_post, slack_update, slack_delete, slack_pin, slack_upload, slack_cache_refresh Context-safe Slack search/threads/posting with dual user/bot token identities, a workspace-keyed channel/user name->ID cache, fetch-style output size gating, and a transactional headline+detail-thread announce protocol with a documented recovery path. OFF by default. Behavior lives in lib/slack-core.ts and lib/slack-cache.ts.

Full routing rules, size-gate mechanics, and config: doc/fetch.md, doc/doc-to-md.md, doc/slack.md.

Key concepts

Concept Meaning
Size gate Text/Markdown/JSON output over 32 KB or 1000 lines spills to a temp file with a 60-line preview instead of inlining.
Content routing HTML -> Markdown, binary -> untouched file, GitHub URLs -> gh CLI (failed runs/jobs get failed-step logs appended), everything else -> the size gate.
Graceful degradation Optional binaries (gh, uv, LibreOffice) are never hard install-time deps; each has a defined, documented fallback or failure mode.
Opt-in extensions session-name, sword-header, fast-mode, provider-stall-watchdog, and slack do nothing until explicitly enabled in settings.json.
Provider stall recovery The watchdog detects a missing first stream event and missing parsed semantic progress, not network liveness. The pre-first-event tier covers every mode and origin; the mid-stream tier is TUI-only.

When to use

  • An agent needs to reason from a real web page, GitHub issue/PR, or local PDF/DOCX/PPTX instead of memory.
  • You want that ingestion to be safe by default, with no risk of a single call blowing the context budget.
  • A Pi run needs an opt-in guard against provider requests that never produce a first stream event, plus mid-stream silence recovery in interactive TUI sessions.

When NOT to use

  • You need a general-purpose web scraper (JS-rendered pages, pagination, auth flows) - fetch does plain HTTP + Readability extraction, nothing more.
  • You need in-grid chart/image placement, .xls visual detection, .xlsm, or range selection - out of scope; chart/image association is per sheet only.
  • You want automatic session naming, a custom header, fast mode, or stall recovery without opting in - all stay off until you flip the config.
  • You need mid-stream stall recovery in JSON, RPC, or print runs - only the pre-first-event tier arms there; mid-stream silence falls through to pi's transport timeout.

Install

Published to npm as the unscoped pi-quiver package.

User scope (all repos under your pi profile):

pi install npm:pi-quiver

Project scope (current repo only, committable via .pi/settings.json):

pi install -l npm:pi-quiver

Try without installing:

pi -e npm:pi-quiver

From a local checkout (for hacking on the extensions):

git clone git@github.com:jjuraszek/pi-quiver.git ~/repos/pi-quiver
pi -e ~/repos/pi-quiver/extensions/fetch.ts

Prerequisites

The npm package's bundled JS deps install automatically on pi install. A few runtime system binaries are optional; each degrades gracefully when absent:

Prerequisite Needed by If absent
gh (GitHub CLI, installed + gh auth login) fetch GitHub issue/PR/repo/actions-run/actions-job routing Falls back to an HTTP fetch of the rendered page (private repos hit a login wall).
uv (+ managed Python 3.14, fetched on first use) doc_to_md high-fidelity PDF and Excel conversion (preferred route), with pymupdf4llm, openpyxl, xlrd, and pillow Falls back to a system Python >= 3.12 or one-time managed venv; PDF degrades to unpdf only when no capable Python exists. Excel requires the Python backend (no JS fallback).
LibreOffice (soffice on PATH) doc_to_md DOCX/PPTX conversion Office inputs error (no JS fallback for office->PDF); PDFs unaffected.

None is a hard install-time dependency of the package; they are tools you provide in the environment where pi runs.

Opt-in extension config

These extensions are opt-in via settings.json (project .pi/settings.json overrides the global agent-dir layer), nested under an optional "quiver" root:

{
  "quiver": {
    "sessionAutoName": {
      "enabled": false,
      "ghosttyTab": true,
      "herdrTab": true,
      "rules": [],
      "deny": [],
      "revisitFirstTurn": 0,
      "revisitEveryTurns": 0
    }, // or boolean shorthand
    "swordHeader": false, // or { "enabled": true }
    "fastMode": false,                                         // or { "enabled": true }
    "providerStallWatchdog": false,                            // or { "enabled": true }
    "slack": false                                             // or { "enabled": true, ... }
  }
}

Each key resolves independently: within a layer, quiver.<key> wins over a flat top-level <key> by presence alone (even when the winning value is malformed); across layers, each layer's candidate is validated into a partial patch and Object.assigned over the accumulator in layer order (project last), so project fields override matching global fields while unmatched global fields survive - a flat-vs-nested shape difference between layers never changes this. The flat top-level form still works, but only for four legacy keys, frozen at fastMode, sessionAutoName, swordHeader, and providerStallWatchdog - never extended to new settings (see Migrating from flat keys).

pi-quiver lints the whole quiver object in both files each time a setting is resolved and emits one condensed warning (TUI: a Warning: block in the chat; headless: stderr) listing every flat legacy key in use, every unknown quiver.<block>, and every unknown field inside a known block, with the accepted names inline. Unknown keys fall back to their defaults. Each distinct message fires once per pi process; a clean file emits nothing. A wrong value type ("fastMode": "yes") is a separate per-key unrecognized value warning. The accepted names live in QUIVER_CONFIG_KEYS in lib/extension-config.ts - a new setting is registered there or it warns as unknown.

Warning: pi-quiver settings (/Users/x/.pi/agent/settings.json): unknown or misplaced keys - unknown ones fall back to defaults
  "providerStallWatchdog" at top level - move under "quiver"
  "quiver.providerStallWatchdog.timeoutMs" - unknown; accepted: enabled, firstEventMs, warningMs, recoveryMs, maxStallRetries

Worked mixed-shape example: global settings.json has flat "fastMode": false, project .pi/settings.json has "quiver": { "fastMode": { "enabled": true } }. The project layer's patch ({ "enabled": true }) is Object.assigned over the accumulator seeded from global's patch, so the resolved config is { "enabled": true } - same outcome here because enabled is the only field either layer sets, but the merge is per-field: a global field with no project counterpart would survive untouched.

sessionAutoName.enabled makes one extra short LLM call per session (once, after the first turn) to title it; false (default) makes no model calls. rules appends house conventions to the naming prompt (later rules win when they conflict with the built-ins). Literal, case-insensitive deny phrases are stripped from every name; whitespace inside a phrase is loose, so "acme corp" also catches AcmeCorp. revisitFirstTurn re-evaluates the name once that many model round trips have completed, while revisitEveryTurns does so at every multiple; both default to 0 (off) because each revisit costs another short LLM call. For example, 10 and 100 mark round trips 10, 100, 200, 300. Revisits only run when the agent has fully settled (idle, nothing queued) - an automated multi-turn run such as a subagent chain is never renamed or delayed mid-flight; cadence points it crossed fire once, at the settle. A machine-generated name is replaced when stale. A name set by a human is never overwritten: the extension strongly prefers it, and announces a suggestion only when the work has clearly moved on. Counts come from the persisted transcript, so they survive resume.

herdrTab (default true) mirrors the same curated label to the Herdr tab bar over Herdr's unix socket, independent of ghosttyTab - either sink can be toggled off without affecting the other. It's claim-once: the extension adopts a tab only while it still shows its default numeric label (its live 1-based position among the workspace's tabs); a manually renamed tab, or one renamed mid-session by hand, is never touched again for that session - the human's label always wins. One exception: exactly one leading * (herdr-ntfy-notify's armed marker) is not a human rename - it is preserved across renames and the shutdown restore, and removing it keeps the claim. On every shutdown (quit, reload, /new, resume, fork) a claimed tab's label is restored to its then-current default position, so a successor session in the same pane can claim cleanly. It only ever runs in TUI mode, on an attached TTY, under Herdr (HERDR_ENV/HERDR_TAB_ID/HERDR_SOCKET_PATH all set) - pi -p/json/rpc runs and background subagents never touch the tab. Caveats: a hard crash (kill -9) skips the restore and leaves the stale label - rename the tab by hand to recover; and the restored label is internally a custom name (Herdr has no clear-to-auto API), so that tab keeps a number-looking label but stops renumbering on later tab closes/reorders.

fastMode only affects claude-opus-4-8 and claude-opus-5 requests on Anthropic's anthropic-messages API; enabling it opts into premium fast-mode pricing. --fast forces it on for one launch; /fast on|off toggles live. Proxy providers (opencode, cloudflare-ai-gateway) are excluded. fastMode's header injection needs the before_provider_headers hook (pi bundling @earendil-works/pi-coding-agent >= 0.80.5); on older pi the beta header is silently not sent. The anthropic-beta header is discovered at request time by probing pi's own request assembly (no network), so conditional betas pi adds - e.g. server-side-fallback-2026-07-01 for models with server-side fallback - are preserved alongside fast-mode-2026-02-01. If the probe fails, the header falls back to the static OAuth-identity + fast-mode list. See doc/fetch.md and doc/doc-to-md.md for the ingestion tools' full reference; session-name/sword-header behavior above is complete.

pi-ai prices every fast request at standard rates - it has no usage.speed support and no request-level pricing modifier - so fastMode corrects the reported cost itself: a message_end handler scales all four usage.cost components by FAST_MODE_COST_MULTIPLIER (2x) and returns the corrected message. Persisted session JSONL and pi's own native cost display are always exact, since they're written from this corrected message. pi-cohort's live Σ$ reflects the correction only when pi-quiver's message_end handler runs before pi-cohort's - best-effort, depending on extension load order - and is reconciled on pi-cohort's next session_start regardless. The upstream fix (teaching pi-ai's Usage/calculateCost about usage.speed) is the better long-term path and is tracked separately.

Recommended explicit retry and watchdog settings - providerStallWatchdog nests under quiver, while pi-core's own retry stays flat beside it (it is not a pi-quiver setting and is never nested):

{
  "quiver": {
    "providerStallWatchdog": {
      "enabled": true,
      "firstEventMs": 20000,
      "warningMs": 120000,
      "recoveryMs": 240000,
      "maxStallRetries": 3
    }
  },
  "retry": {
    "enabled": true,
    "maxRetries": 3,
    "baseDelayMs": 2000
  }
}
Key Default Where it arms What it measures
enabled false - Master switch. OFF means the extension does nothing.
firstEventMs 20000 every provider request, every mode (tui/print/json/rpc), every origin Silence between the request and the first assistant message_start.
warningMs 120000 mid-stream, ctx.mode === "tui" only Silence since the last non-empty text/thinking/toolcall delta; notifies.
recoveryMs 240000 mid-stream, ctx.mode === "tui" only Same clock; aborts and converts. Must be > warningMs.
maxStallRetries layered retry.maxRetries, else 3 shared by both tiers Watchdog aborts that may convert to a retryable error before stopping.

providerStallWatchdog is OFF by default. Once enabled it arms in two tiers per provider request:

  • Pre-first-event (firstEventMs). Armed at every provider request, in every mode and from every origin - including extension-triggered turns that never emit before_agent_start - and cleared by the first assistant message_start. On expiry the request is aborted and, budget permitting, converted to a retryable error, so an unresponsive request recovers in ~22s (20s detection + Pi's 2s backoff) instead of the ~240s it took when only the mid-stream tier existed.
  • Mid-stream (warningMs / recoveryMs). Armed from the first assistant message_start onward, and only when ctx.mode === "tui". Aborting mid-generation discards billed output tokens and an unattended run has nobody to read the warning, so headless mid-stream silence deliberately falls through to the transport timeout instead.

Raise firstEventMs if your provider is legitimately slow to first event. Queueing gateways, throttled endpoints, and busy single-slot local model servers can hold the connection for well over 20s before their first stream event; every false abort re-uploads the whole context and spends one stall retry.

Leave pi's own httpIdleTimeoutMs (default 300000) at its default. It is the transport backstop, and a single value drives undici's headersTimeout and bodyTimeout - lowering it to get fast pre-stream failure also truncates legitimate mid-stream gaps. firstEventMs is the knob for pre-stream silence.

Verified with Pi 0.80.10: each stall is aborted and offered to Pi retry until maxStallRetries conversions are used; further stalls stop for manual resubmission. Both tiers draw on that one budget. maxStallRetries defaults to the layered retry.maxRetries (Pi default 3, an explicit 0 honoured); 0 is valid and means "detect and stop, never auto-retry". Consecutive stall conversions consume Pi retry attempts without a success reset in between, so keep maxStallRetries <= retry.maxRetries. A successful assistant turn resets the stall counter (mirroring Pi's own retry counter). Automatic continuation needs enabled Pi retry with remaining capacity. Disabled, exhausted, or incompatible retry degrades to manual resubmission. Pending steering or follow-ups return to the editor and are excluded from automatic continuation. Invalid merged watchdog config fails closed.

Operational notes:

  • Settings are read once per session, on the first provider request. Editing settings.json mid-session changes nothing until you restart the session - that includes repairing an invalid block that already disabled the extension.
  • A watchdog abort that the provider ignores escalates after a fixed 10s. Any post-abort stream event re-arms that deadline (bytes prove only that the connection was alive at that instant), so a stream that emits a straggler and then wedges still escalates 10s after its last event. This reduces the hang; it cannot force the provider to stop, and undici's timeouts remain the final backstop.
  • Headless runs report on stderr. In print/json mode pi binds a no-op UI, so watchdog notices go out via console.warn. Nothing is ever written to stdout, which json mode uses for its protocol. In TUI and RPC the notices render as main-window notifications, not the bottom status line.

slack is OFF by default and, once enabled, adds eight slack_* tools (search, thread, post, update, delete, pin, upload, cache refresh) covering context-safe Slack search/threads/posting under dual user/bot token identities. Nested-only from day one (no legacy flat form):

{
  "quiver": {
    "slack": {
      "enabled": true,
      "cachePath": ".pi/slack-cache.json",
      "userTokenEnv": "SLACK_USER_TOKEN",
      "botTokenEnv": "SLACK_BOT_TOKEN",
      "uploadThresholdChars": 4000
    }
  }
}
Key Default Meaning
enabled false Master switch, checked at session_start; toggling takes effect next session.
cachePath user-scope per-OS cache dir Overrides where the workspace-keyed channel/user name->ID cache file is written; relative paths resolve against the repo root.
userTokenEnv SLACK_USER_TOKEN Env var name holding the user token when userTokenCommand is unset (required for slack_search/slack_thread, no bot fallback).
userTokenCommand unset Executable argv array that prints the current user token to stdout; resolved on every user-identity tool call.
userTokenCommandTimeoutSeconds 10 Positive finite timeout in seconds for userTokenCommand. Increase this when an interactive credential helper may need authorization.
botTokenEnv SLACK_BOT_TOKEN Env var name holding the bot token.
uploadThresholdChars 4000 Link-collapsed length above which an announce/thread detail body is delivered as a file upload instead of inline text.

Each setting can also be overridden per-process via PI_QUIVER_SLACK_ENABLED, PI_QUIVER_SLACK_CACHE_PATH, PI_QUIVER_SLACK_USER_TOKEN_ENV, PI_QUIVER_SLACK_BOT_TOKEN_ENV, and PI_QUIVER_SLACK_UPLOAD_THRESHOLD_CHARS - applied on top of the resolved settings.json layers, same override rung the extension's config resolver defines. userTokenCommand and userTokenCommandTimeoutSeconds are settings-only: pi executes the argv directly, without a shell, on every user-identity Slack tool call. For example, macOS Keychain can supply the token with "userTokenCommand": ["security", "find-generic-password", "-s", "slack-user-token", "-w"]. Its stdout is the token; empty output, nonzero exit, or timeout is a sanitized hard error and never falls back to userTokenEnv. Without the command, tokens resolve per call from process env and then .env (or the primary checkout's, for a worktree with none). Bot resolution is unchanged. Restart pi after changing Slack settings because the extension captures them at session start. Full reference incl. cache layering, the announce protocol, and the search.messages/conversations.replies throttle caveats: doc/slack.md.

doc_to_md settings

doc_to_md is always registered. Configure tunables in quiver.docToMd; per-call > quiver.docToMd > PI_DOC_TO_MD_* env (deprecated) > default. The result is a handle; read the Saved-To file (offset/limit) for the Markdown, images live under Images-Dir.

Key Default Meaning
primaryTimeoutMs 60000 pymupdf4llm tier and unpdf tier deadline.
fallbackTimeoutMs 30000 PyMuPDF text tier, PDF info, and Excel rendered-view rasterization deadline.
sofficeTimeoutMs 120000 DOCX/PPTX -> PDF deadline; also the Excel rendered-view export.
excelTimeoutMs 60000 Excel child and Excel info deadline.
warmTimeoutMs 120000 Absolute first-call backend discovery/bootstrap deadline.
pymupdfVersion 1.27.2.3 pymupdf4llm pin, minimum 1.27.0.
imageDpi 150 Render DPI for page images and Excel rendered views (capped by a 16 Mpx budget).
imageFormat png Rendered image format: png or jpg; embedded images retain their extension.
maxOutputBytes 20000000 Child stdout cap in bytes.
outlineMaxEntries 40 Heading outline, TOC, or sheet inventory entries in the handle.

A bundle is <outputDir>/<stem>.md plus <outputDir>/images/ and, for spreadsheets with data, <outputDir>/sheets/; without outputDir, the tool creates a per-call temp root. A conversion owns <stem>.md.lock until it atomically publishes the Markdown. An existing <stem>.md fails the call unless overwrite is set; overwrite replaces that Markdown and the files it owns (images/<stem>-p<N>-<n>.*, images/<stem>-s<idx>[-<n>].*, sheets/<stem>-s<idx>-<slug>.csv), nothing else. Temp bundles are caller-owned - the tool never deletes a bundle it produced.

Excel needs a Python backend with openpyxl, xlrd and pillow - otherwise the call fails with Remedy: install uv, or pip install openpyxl xlrd pillow. The Markdown opens with a ## Sheets table listing every sheet in workbook order (0-based #, worksheet/chartsheet, size, hidden, chart and image counts, rendered view, CSV link for non-empty worksheets), then one section per sheet: a Data: line linking the full-content CSV under sheets/ for non-empty worksheets, chart metadata from the workbook model, embedded images, an optional rendered view, a preview of at most the first 100 rows x 50 columns, and - only when the preview is truncated - a Columns: profile (type, non-empty count, min/max, distinct up to 50). Sizes are the extent of non-empty cells (the info handle reports the raw worksheet dimensions instead, which may be larger). Rendered views (images/<stem>-s<idx>.<fmt>) are produced for sheets carrying charts or images when LibreOffice is on PATH: the workbook is exported one PDF page per sheet and rasterized under a 16 Mpx budget. Any LibreOffice or rasterization failure degrades to Rendered view: unavailable (<reason>) and a handle note; it never fails the conversion. Workbooks whose chartsheet drawings carry a zero-size anchor (openpyxl-authored files; Excel-authored files are unaffected) render as a degenerate page and are reported as such. .xls gets the inventory, CSVs and previews but no visual detection or rendering.

Worst-case wall time is warmTimeoutMs (first call) + sofficeTimeoutMs (Office only) + primaryTimeoutMs + fallbackTimeoutMs + KILL_GRACE_MS x kills (Excel: warmTimeoutMs + excelTimeoutMs + sofficeTimeoutMs + fallbackTimeoutMs + 2 * KILL_GRACE_MS). There is no cap on image count, image bytes, cell count or workbook memory - deliberately; the per-tier timeouts, the rendered-view pixel budget and maxOutputBytes are the bounds.

Migrating from flat keys

The flat top-level form ("fastMode": ... etc. directly under settings.json) is the outdated configuration style. It is legacy-frozen to exactly the four keys above - fastMode, sessionAutoName, swordHeader, providerStallWatchdog - and will never gain a fifth. To migrate, wrap your existing keys under "quiver": { ... } and delete the flat copies:

// before
{ "fastMode": true }

// after
{ "quiver": { "fastMode": true } }

A flat copy keeps resolving (nested wins within a layer, project layer wins across layers) but every flat legacy key in use is listed in the condensed settings warning above ("fastMode" at top level - move under "quiver") until it is moved. Every new pi-quiver setting introduced after this change (for example slack) is nested-only from day one: it has no flat form to fall back to, and a flat slack block is ignored without a warning.

Claude Code support

fetch and doc_to_md cores are also published through the CLI, so Claude Code can use the same routing or bundle-and-handle behavior as pi's native tools - without pi ever seeing Claude-only files.

Exposed: the quiver plugin, served from this repo's .claude-plugin/marketplace.json, with two skills: fetch (invoked as quiver:fetch / /quiver:fetch) and doc-to-md (invoked as quiver:doc-to-md / /quiver:doc-to-md). The fetch skill runs npx -y pi-quiver@latest fetch <url> [flags] via Bash - full parameter parity with the pi tool (--method, --header, --body, --raw, --timeout-ms), same GitHub gh routing (including failed-step logs on failed runs/jobs), same size gate, same binary-to-temp-file handling. See doc/fetch.md for exit codes and flags. The doc-to-md skill runs npx -y pi-quiver@latest doc-to-md [flags] <path> with full flag parity and the same handle output. See doc/doc-to-md.md for exit codes and flags.

Not exposed: the other pi extensions in this package (session-name, sword-header, fast-mode, provider-stall-watchdog, slack) - the marketplace allowlists only ./skills/fetch and ./skills/doc-to-md, and the npm tarball never ships skills/ or .claude-plugin/ (pi's own files allowlist excludes them, and pi's explicit pi.extensions manifest makes them invisible to pi's convention-directory auto-discovery either way).

Add the marketplace and enable the plugin in .claude/settings.json:

{
	"extraKnownMarketplaces": {
		"pi-quiver": { "source": { "source": "github", "repo": "jjuraszek/pi-quiver" } }
	},
	"enabledPlugins": { "quiver@pi-quiver": true }
}

Activates on folder trust.

Release sequencing: the skill goes live only with (or after) the npm release that ships the pi-quiver bin - until that tag is on npm, npx -y pi-quiver@latest fetch resolves a bin-less package and fails.

Development

Deps are peers (@earendil-works/*, @sinclair/typebox) plus the bundled runtime deps; install them transiently and run the full check:

npm install
npm run test:all      # node --test test/*.test.ts  +  tsc --noEmit typecheck

npm test runs the unit tests alone; npm run typecheck runs the type pass. Both run in CI on ubuntu + windows (.github/workflows/test.yml).

How this fits the platform

pi-quiver is how ground truth gets into an agent's context - real pages, PDFs, docs, cleanly and safely. The other three then coordinate work over it (pi-cohort), prune it once it's stale (pi-condense), and govern the process end to end (pi-gauntlet).

Contributing

See CONTRIBUTING.md - issues follow a Context / Problem / Idea / Acceptance Criteria template; PRs run the pi-gauntlet workflow (one-liners exempt from ceremony, never from keeping docs truthful).

Support

If this saves you time, consider buying me a coffee.

Release

Published to npm by CI. Pushing a vX.Y.Z tag triggers .github/workflows/release.yml, which gates on tag == package.json, runs npm run test:all, and publishes with npm publish --provenance --access public via OIDC trusted publishing. Never run npm publish by hand.

Cut a release with the helper script (also exposed as the /release prompt + the release skill at .agents/skills/release/):

bash .agents/skills/release/scripts/release.sh propose      # suggest a level
bash .agents/skills/release/scripts/release.sh patch        # or minor / major
bash .agents/skills/release/scripts/release.sh --dry-run patch

It promotes the ## Unreleased CHANGELOG section to ## vX.Y.Z - <date>, bumps package.json, commits Release <version>, runs the tests, creates and pushes the vX.Y.Z tag, then monitors the publish. See .agents/skills/release/SKILL.md for the full flow (sync-presets --apply rewrites same-form npm:pi-quiver@<old> pins; git-tag pins are reported for manual migration).

About

pi coding-agent extensions with basic tool set optimised for agentic coding

Resources

Contributing

Stars

7 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages