Frozen. The signature it remembers quietly stopped existing.
one source two readers no server
Documentation as an npm dependency. Agents read it. People read the same thing.
A library publishes its docs as a package. You install it beside the code, and one command answers from the version you actually installed — offline, without a page in context.
one markdown source
The Markdown the library already writes. Parsed once, then emitted as both of these.
for the agent · installed
for the person · published
acme.dev/docs/verify#drift — cited by the answer
the gap
Three ways to answer one question
You have @acme/docspack@1.4.0 in your lockfile. The agent asks
how to verify a webhook. Only one path is bound to the version you are running.
Today's truth. Whatever the vendor publishes now — not what you installed.
Correct by construction. The docs were installed with the code.
how it works
Three moves, no server
Vendors publish docs to npm
A docs package is an ordinary npm package: Markdown split into chunks, plus a manifest describing them. Its version tracks the library's.
// package.json "devDependencies": { "@acme/docspack": "1.4.0" }
docspack indexes them locally
One sync walks the docs packages this project depends on and writes their chunks into one SQLite database per machine. Keyed by name and version, so a release is read once no matter how many projects use it. It reads each installed library's own type declarations too, so a name nobody wrote a page about still has an answer.
$ npx docspack sync + @acme/docspack@1.4.0 128 chunks indexed + hono@4.13.3 272 chunks indexed (declarations) = @other/docspack@0.0.8 44 chunks cached
Agents ask the index
Every agent already has a shell, so the setup is done: no server to spawn, no per-client configuration, nothing resident when nobody is asking. One command writes the instruction into the AGENTS.md or CLAUDE.md this project already keeps — plus a Claude Code skill when there is one — and a second fails CI once that wiring drifts from what the tool does.
$ npx docspack agent install + AGENTS.md + .claude/skills/docspack/SKILL.md # in CI, so a pasted line cannot go stale $ npx docspack agent check ok AGENTS.md
Per query, of which 41ms is Node starting and 60ms is docspack. Invisible next to the seconds an agent spends thinking.
Porter stemming, so authenticate finds a chunk that only says authentication.
docspack mcp serves the same index over the protocol, byte-identical answers, if you would rather have a tool.
instead of
Why not the thing you already have
The alternatives are not less capable. They differ on one axis: which version of the truth reaches the model, and how much of it.
| docspack | Docs site | MCP server | Agent skill | |
|---|---|---|---|---|
| Matches your lockfile | Always | Latest only | Latest only | Authored once |
| Works offline | Yes | No | No | Yes |
| Resident in context | Never | Pasted pages | Tool defs, always | Whole skill, always |
| Names the docs never mention | From the installed build | Absent | Absent | Absent |
| Cost of one answer | ≤ 3,000 tokens | Whole page | Whole page + trip | Whole document |
| Setup per project | One command | None | Per client | Per team |
docspack
- Matches your lockfile
- Always
- Works offline
- Yes
- Resident in context
- Never
- Names the docs never mention
- From the installed build
- Cost of one answer
- ≤ 3,000 tokens
- Setup per project
- One command
Docs site
- Matches your lockfile
- Latest only
- Works offline
- No
- Resident in context
- Pasted pages
- Names the docs never mention
- Absent
- Cost of one answer
- Whole page
- Setup per project
- None
MCP server
- Matches your lockfile
- Latest only
- Works offline
- No
- Resident in context
- Tool defs, always
- Names the docs never mention
- Absent
- Cost of one answer
- Whole page + trip
- Setup per project
- Per client
Agent skill
- Matches your lockfile
- Authored once
- Works offline
- Yes
- Resident in context
- Whole skill, always
- Names the docs never mention
- Absent
- Cost of one answer
- Whole document
- Setup per project
- Per team
A docs MCP server that fetches
Closest relative, and no reason it cannot exist — docspack mcp is exactly that. The difference is what sits behind it: a fetching server returns the same text to every project, pays the network on each call, and ranks whole pages because there is no local index.
An agent skill
A skill teaches an agent how to do something — your review checklist, your deploy runbook. It is authored once and loaded whole. Dependency docs change when you bump a version, and nobody wants to hand-edit them. Skills and docspack compose: the skill says “check the docs first”.
Vendoring docs into the repo
Copying Markdown into CLAUDE.md is the right instinct — it just does not survive upgrades. Nothing notices when it goes stale, and a flat folder has no index, so the agent reads the whole file to find one line.
A vector database
Better recall for vague questions, at the price of an embedding pipeline, a service, and a re-ingestion story on every change. Aimed at a corpus you do not have. Yours is small, textual, and already versioned by the package manager.
measured
What an answer actually costs
Against 815 chunks of one vendor's published
documentation — 155,056 tokens, mirrored from their
llms.txt. Every figure here is generated from
one measurement run, not typed in.
of the documentation reaches the model, at most — and a response is capped at 3,000 tokens no matter what was asked.
tokens for the largest answer measured. A warm CDN returns 34,546 tokens of HTML to answer the same question.
with fetch, DNS and TCP removed from the process. Not configured off — removed, so any attempt to reach out would throw.
of what hono and zod export is mentioned anywhere in their own documentation. The rest is declared in the installed build.
The full measurement Per-query token budgets, P50/P95 timings, and the coverage gap in hono and zod
In context
In time
| Step | p50 | p95 | samples |
|---|---|---|---|
| Node starting the runtime, before docspack runs at all | 40.65ms | 49.95ms | 60 |
| docspack ask, end to end a whole process, spawn to answer | 100.81ms | 113.41ms | 60 |
| The query itself discovery, open, rank and budget, in process | 0.54ms | 0.85ms | 240 |
| The full-text search ranking 815 chunks | 0.51ms | 0.84ms | 240 |
| One documentation page, fetched one warm CDN-cached page, measured from the benchmark host | 48ms | 63ms | 20 |
Half the API is documented nowhere
A documentation site is written page by page, around tasks. An API grows name by
name. The two never converge, and nothing measures the difference — so we did,
against two projects that document themselves well. Every uncovered name is
declared in the package already in node_modules, which is
where docspack is already running.
hono@4.13.3
zod@4.4.3
Intel(R) Xeon(R) Processor @ 2.80GHz · 4 cores · linux x64 · Node v22.22.2 · docspack 0.4.0 · 2026-08-22 · pnpm --filter @docspack/bench perf reproduces it ·
the raw measurement
hono@4.13.3 · zod@4.4.3 · 2026-09-02 · pnpm --filter @docspack/bench coverage reproduces
it · the raw measurement
your own sources
The same mechanism, over material nobody published
A context window is exhausted just as easily by a project's own material — notes,
decision records, an export, rows out of a query. docspack index
indexes what you already have, and docspack recall answers from
it. No driver, no server, no embedding step.
Records arrive as JSON, so anything that can emit a row can be indexed. A record
carrying an id becomes exactly one chunk under that id — a row's identity
is its key, and splitting it would either duplicate that key or throw it away.
Your files change underneath the index. Published documentation is immutable for the life of its version; your notes are not, and are often edited by the same agent asking about them. So each source's size, modification time and hash are recorded, and an answer leads with a warning naming what has moved. A stale answer is quoted correctly and still wrong, which is worse than no answer.
recall is a separate command from ask on purpose.
ask promises an answer from the versions you installed, and one filter in
the query path keeps that promise: your notes can never reach an answer about a
dependency, and a dependency's documentation can never reach an answer about your notes.
# Markdown you already have $ npx docspack index --from ./notes # or rows, from anything that can emit JSON $ sqlite3 -json shop.db 'select id, title, body as text from posts' \ | npx docspack index --from-json - $ npx docspack recall "what did we decide about retries" ## @local/notes@0.0.0/retry-policy We settled on three attempts with exponential backoff. Anything that touches billing is never retried automatically. # edit a source, and the next answer says so NOTE: the corpus is out of date. 1 indexed source has changed since it was built: notes/retries.md.
Whether it beats letting an agent grep the files
Measured, not assumed — on 269 chunks of prose nobody
wrote for retrieval, against 17 questions fixed before the first run. Reading
everything grep suggests is the more accurate arm. It costs about
14× the tokens.
269 chunks · ~93,518 tokens · 17 held-out questions · commit c221778
the format
A docs package is just files
Any tool can read one. The manifest maps chunks to search terms, so indexing needs no model in the loop. Every field is written down in the package specification, and the rest of the manual is in the documentation.
node_modules/@acme/docspack/ ├─ package.json holds the version ├─ llms.txt table of contents └─ .llms/ ├─ manifest.json chunk routing table ├─ chunks/ │ ├─ api-auth.md │ └─ webhooks.md └─ schemas/ optional openapi
{
"name": "@acme/docspack",
"version": "1.4.0",
"chunks": [
{
"id": "api-auth",
"tokens": 150,
"tags": ["authentication", "bearer"]
}
]
} Generate one from what you already have
Split Markdown at headings, or walk an OpenAPI document. The only command that
touches the network reads a project's public llms.txt.
$ npx docspack build --from ./docs $ npx docspack build --openapi ./api.json $ npx docspack build hono
publish docs · sheaf
One source. A site for people, an index for agents.
docspack answers an agent from documentation somebody published. Sheaf is where that documentation becomes the site a person reads. The same Markdown is parsed once into a content graph, and both the pages and a docspack package can be emitted from it, so the two cannot describe different documentation. This site is built that way: the manual you are one click from is rendered from the graph.
It mounts, it does not generate
A docs framework usually hands you an app and one exit. Sheaf hands you a graph and components: render them in the Astro site, React app or Preact app you already have, at whatever path you already own. There is no generated project, so there is nothing to eject from.
An answer that links back
A graph knows where its pages are published, so a chunk cites the URL and anchor it came from rather than a filename. The agent's answer carries a link a person can open — which is the part a folder of Markdown cannot know.
Four packages, and you take the ones you need
sheaf
Sheaf's content graph: a directory of Markdown as one serializable object.
sheaf-astro
Renders a Sheaf graph with Astro's own Markdown pipeline.
sheaf-react
The Sheaf docs shell — sidebar, page, contents and pager, built on cascivo.
sheaf-emit
Turns a Sheaf graph into a docspack package, so one source serves both audiences.
// one directory of Markdown, parsed once const graph = await buildGraph("./docs"); // → the page a person reads const { html, headings } = await renderPage(page); // → the package their agent answers from await emitDocspackPackage(graph, { baseUrl: "https://acme.dev/docs", }); // which is what puts a link in the answer <!-- docspack: from https://acme.dev/docs/verify#drift -->
Early — v0.1.0, and versioned accordingly · how the two halves fit · npm · source
feedback
The agent noticed the docs are wrong
An agent has the documentation, the installed library and a failing program in front of it at the same moment. That signal is worth capturing, and today it evaporates.
The obvious version — let the agent file an issue — points a firehose at the people the whole model depends on. curl ended its bug bounty in January 2026 over AI-generated slop. So the goal is not to make feedback easy. It is to make high-signal feedback cheap and low-signal feedback impossible to transmit.
Claims must be falsifiable: drift has to name the identifier, anything
else has to carry a reproduction, and there is no kind for "this page is confusing".
Repeats increment a counter, so twelve agents hitting one bug produce one report that
says twelve.
Nothing is sent, and nothing can be — docspack contains no code that transmits a
report. A vendor who wants them declares a channel in their own
package.json, and a human decides whether to open the prefilled issue.
# the agent records what it found $ npx docspack feedback add \ --chunk @acme/docspack@1.4.0/api-auth \ --kind drift \ --evidence "client.setKey is not exported" # a human reviews, then decides $ npx docspack feedback submit 182ec604a6ea drift @acme/docspack@1.4.0/api-auth Open: https://github.com/acme/sdk/issues/new?… // the vendor opts in — or gets nothing "feedback": { "github": "acme/sdk", "accepts": ["drift"] }
guarantees
What holds, every query
Offline by construction
sync, ask, search and list make no network requests. They read node_modules and a local SQLite file.
Version-scoped answers
The shared index may hold five versions of a library. A project only ever sees the one it installed.
Bounded responses
Three chunks and 3,000 tokens by default, counted from the manifest before content is returned.
Untrusted input, treated as such
Community packages are labelled, and a manifest path that escapes the package is refused rather than read.
questions
Before you install it
What is docspack?
docspack distributes a library's documentation as an ordinary npm package, so the docs install alongside the code and stay pinned to the same version. It indexes those packages into a local SQLite database and answers questions from it with one command, offline.
Do I need to run a server?
No. docspack is a CLI: the agent runs docspack ask in the shell it already has. There is nothing resident when nobody is asking. An MCP server ships too (docspack mcp) and serves the same index, for assistants that cannot run commands.
What if a library does not publish a docs package?
Build one yourself with docspack build --from ./docs, from an OpenAPI document, or from a project's public llms.txt. The result is a normal npm package you can publish or keep private.
What if the library's own documentation never mentions it?
That is the common case: measured against two projects that document themselves well, their own documentation mentions about half of what they export. docspack sync also indexes each installed library's type declarations, so an answer about an undocumented name leads with the declaration from the build you installed and says the documentation does not cover it. Declarations are matched by name and never ranked against prose, and docspack sync --no-artifacts turns them off.
How much context does an answer consume?
Three chunks and 3,000 tokens by default. The cap is counted from the manifest before any content is returned, so a query cannot overrun it. Measured against one vendor's published docs, at most 1.1% of the corpus reaches the model.
Does docspack send anything anywhere?
No. sync, ask, search and list make no network requests at all, and docspack contains no code that transmits a feedback report. The only command that touches the network is docspack build, which reads a project's public llms.txt when you ask it to.
Three commands, nothing to paste.
MIT. Requires Node 22.5.0+. Currently v1.2.0. The package format is specified, and breaking it is a major release.
$ pnpm add -D @acme/docspack $ pnpm dlx docspack sync $ pnpm dlx docspack agent install
The first installs the docs beside the code. The second indexes them. The third tells
your agent the index exists — and docspack agent check fails CI if that
wiring ever drifts.