zimlore 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. zimlore-0.1.0/.gitignore +49 -0
  2. zimlore-0.1.0/LICENSE +21 -0
  3. zimlore-0.1.0/NOTICE +21 -0
  4. zimlore-0.1.0/PKG-INFO +206 -0
  5. zimlore-0.1.0/PLAN.md +139 -0
  6. zimlore-0.1.0/README.md +174 -0
  7. zimlore-0.1.0/ROADMAP.md +143 -0
  8. zimlore-0.1.0/conftest.py +1 -0
  9. zimlore-0.1.0/pyproject.toml +107 -0
  10. zimlore-0.1.0/tests/conftest.py +5 -0
  11. zimlore-0.1.0/tests/test_catalog.py +429 -0
  12. zimlore-0.1.0/tests/test_catalog_hardening.py +369 -0
  13. zimlore-0.1.0/tests/test_cli_and_mcp.py +184 -0
  14. zimlore-0.1.0/tests/test_converter_structure.py +261 -0
  15. zimlore-0.1.0/tests/test_hardening.py +381 -0
  16. zimlore-0.1.0/tests/test_license.py +53 -0
  17. zimlore-0.1.0/tests/test_manage.py +355 -0
  18. zimlore-0.1.0/tests/test_manage_safety.py +436 -0
  19. zimlore-0.1.0/tests/test_markdown.py +215 -0
  20. zimlore-0.1.0/tests/test_packaging.py +41 -0
  21. zimlore-0.1.0/tests/test_refs_and_library.py +159 -0
  22. zimlore-0.1.0/tests/test_regions.py +381 -0
  23. zimlore-0.1.0/tests/test_roadmap_rows.py +100 -0
  24. zimlore-0.1.0/tests/test_service.py +541 -0
  25. zimlore-0.1.0/tests/test_snapshot.py +348 -0
  26. zimlore-0.1.0/tests/test_sync.py +419 -0
  27. zimlore-0.1.0/tests/test_usability_fixes.py +280 -0
  28. zimlore-0.1.0/tests/test_versions.py +380 -0
  29. zimlore-0.1.0/tests/zimfixture.py +286 -0
  30. zimlore-0.1.0/zimlore/__init__.py +5 -0
  31. zimlore-0.1.0/zimlore/__main__.py +8 -0
  32. zimlore-0.1.0/zimlore/_libzim.py +37 -0
  33. zimlore-0.1.0/zimlore/catalog.py +592 -0
  34. zimlore-0.1.0/zimlore/cli.py +618 -0
  35. zimlore-0.1.0/zimlore/errors.py +45 -0
  36. zimlore-0.1.0/zimlore/groups.py +30 -0
  37. zimlore-0.1.0/zimlore/library.py +648 -0
  38. zimlore-0.1.0/zimlore/manage.py +632 -0
  39. zimlore-0.1.0/zimlore/markdown.py +1247 -0
  40. zimlore-0.1.0/zimlore/mcp.py +183 -0
  41. zimlore-0.1.0/zimlore/purl.py +130 -0
  42. zimlore-0.1.0/zimlore/py.typed +0 -0
  43. zimlore-0.1.0/zimlore/refs.py +90 -0
  44. zimlore-0.1.0/zimlore/service.py +1126 -0
  45. zimlore-0.1.0/zimlore/snapshot.py +195 -0
  46. zimlore-0.1.0/zimlore/sync.py +269 -0
@@ -0,0 +1,49 @@
1
+ # The corpus is not the point of this repository and must never be in it.
2
+ #
3
+ # ZIM files are other people's content under their own licences (see NOTICE). They live in ZIMLORE_HOME
4
+ # (default ~/.zimlore), never in the checkout. A ZIM is a single file that can be gigabytes, and git history
5
+ # cannot walk it back, so every way of naming one is ignored from the first commit.
6
+ *.zim
7
+ *.zim.*
8
+ *.zimaa
9
+ *.meta4
10
+ *.idx.tmp/
11
+ zim/
12
+ zimlore-home/
13
+ .zimlore/
14
+
15
+ .venv/
16
+ __pycache__/
17
+ *.pyc
18
+ .mypy_cache/
19
+ .pytest_cache/
20
+ .ruff_cache/
21
+ dist/
22
+ build/
23
+ *.egg-info/
24
+ .coverage
25
+ .coverage.*
26
+ htmlcov/
27
+
28
+ # --- Captures and secrets ---
29
+ *.har
30
+ .env
31
+ .env.*
32
+ *.pem
33
+ *.key
34
+
35
+ # --- Scratch: the workspace pre-commit hook refuses these ---
36
+ probe_*.py
37
+ *.log
38
+ *.out
39
+ *.tmp
40
+ scratch/
41
+ tmp/
42
+
43
+ # --- Agent worktrees and local tool state ---
44
+ .claude/worktrees/
45
+ .claude/settings.local.json
46
+ .vscode/
47
+ .idea/
48
+ .DS_Store
49
+ Thumbs.db
zimlore-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Justin Capella
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ documentation files (the "Software"), to deal in the Software without restriction, including without
7
+ limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8
+ Software, and to permit persons to whom the Software is furnished to do so, subject to the following
9
+ conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions
12
+ of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
15
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
17
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
18
+ DEALINGS IN THE SOFTWARE.
19
+
20
+ This licence covers the code in this repository. It does not cover any ITU-T Recommendation text, which is
21
+ copyright ITU and is not included here - see NOTICE.
zimlore-0.1.0/NOTICE ADDED
@@ -0,0 +1,21 @@
1
+ NOTICE
2
+ ======
3
+
4
+ This repository contains no ZIM file and no text from any ZIM file.
5
+
6
+ A ZIM is an archive of someone else's content, and each one carries that content's own licence: DevDocs ZIMs
7
+ archive upstream project documentation under whatever licence each project chose, Stack Exchange ZIMs are
8
+ user-contributed content under Creative Commons terms with attribution requirements, Wikipedia is another
9
+ case again. Those licences differ from each other and from this software's. zimlore therefore never ships,
10
+ vendors or embeds ZIM content: files live in `ZIMLORE_HOME` (default `~/.zimlore`, subdirectory `zim/`) on
11
+ each user's machine, fetched by the user with `zimlore get` or copied there, and `.gitignore` names every way
12
+ a ZIM can be spelled so that one cannot be staged by accident. `tests/test_license.py` is a tripwire against
13
+ the obvious copy, not a proof against a determined one; its test ZIMs are built from invented text at test
14
+ time, in a temporary directory.
15
+
16
+ What zimlore says about a document is a citation (library, ZIM edition date, entry path) plus the licence
17
+ metadata the ZIM itself carries in its `License` field. Many ZIMs, including the DevDocs ones measured while
18
+ writing this, carry none, and zimlore reports that as "not stated in the ZIM" rather than guessing: the
19
+ licence of the text you reuse is the upstream project's, and you should read it there.
20
+
21
+ The code in this repository is licensed under the MIT licence in LICENSE. That licence covers the code only.
zimlore-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.5
2
+ Name: zimlore
3
+ Version: 0.1.0
4
+ Summary: A Kiwix library (ZIM files) served to AI agents over MCP: offline, citable, token-budgeted documentation search and reading. You supply the ZIMs; none are shipped.
5
+ Project-URL: Homepage, https://github.com/b1tninja/zimlore
6
+ Project-URL: Source, https://github.com/b1tninja/zimlore
7
+ Project-URL: Issues, https://github.com/b1tninja/zimlore/issues
8
+ Author-email: Justin Capella <justincapella@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Keywords: context7,devdocs,documentation,kiwix,libzim,mcp,model-context-protocol,offline,openzim,zim
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Documentation
23
+ Classifier: Topic :: Text Processing :: Indexing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: libzim>=4.1; sys_platform != 'win32' or platform_machine != 'ARM64'
27
+ Provides-Extra: all
28
+ Requires-Dist: mcp>=2.3; extra == 'all'
29
+ Provides-Extra: mcp
30
+ Requires-Dist: mcp>=2.3; extra == 'mcp'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # zimlore
34
+
35
+ A [Kiwix](https://kiwix.org) library (ZIM files) served to AI agents over MCP: **offline, citable,
36
+ token-budgeted documentation**. It is the offline counterpart of [Context7](https://github.com/upstash/context7):
37
+ instead of asking a hosted service for snippets, an agent searches and reads ZIM files you hold on disk, and
38
+ every answer carries the ZIM's name, edition date and entry path.
39
+
40
+ You supply the ZIMs. **None are shipped**, and `.gitignore` names every way one could be staged
41
+ (see [NOTICE](NOTICE)).
42
+
43
+ ```
44
+ pip install "zimlore[mcp]"
45
+ zimlore catalog flask # search the public Kiwix catalogue
46
+ zimlore get devdocs_en_flask # download it, verified by SHA-256, into ~/.zimlore/zim
47
+ zimlore search "application factory" -l flask
48
+ zimlore read flask§patterns/appfactories/index
49
+ zimlore mcp # the MCP server, on stdio
50
+ ```
51
+
52
+ Managing what is installed (none of these touch the network):
53
+
54
+ ```
55
+ zimlore du # disk use per library
56
+ zimlore verify [--adopt] # check files against their provenance record and libzim's checksum
57
+ zimlore remove flask --yes # delete (dry run without --yes; --edition DATE or --all when several)
58
+ zimlore prune --keep 2 --yes # drop old zimlore-installed editions and stale partial downloads
59
+ # (--include-unmanaged also deletes files zimlore did not install)
60
+ ```
61
+
62
+ Putting ZIMs in place (`add` is local; `sync` and `get` use the network and resume an interrupted download):
63
+
64
+ ```
65
+ zimlore add some.zim [--link] # copy (or link) a local ZIM in after libzim opens it and its checksum holds
66
+ zimlore sync --group web-testing --dry-run # plan against a manifest or a named group; sizes are approximate
67
+ zimlore sync --manifest .zimlore.toml [--update] # install what is missing; --update stages newer editions
68
+ ```
69
+
70
+ A manifest (`./.zimlore.toml`, else `$ZIMLORE_HOME/libraries.toml`) names catalogue entries only, never URLs or
71
+ paths, so it can be committed: `libraries = ["devdocs_en_python"]`, `groups = ["infra"]`, `max_age_days = 365`,
72
+ `budget = "2GB"` (the library may not grow past it). `sync` never removes anything. The shipped groups
73
+ (`languages-core`, `web-testing`, `infra`) are an editorial default awaiting the maintainer's decision.
74
+
75
+ Every download records a sidecar (`<file>.zimlore.json`: source URL, verified SHA-256, time). A file without one
76
+ reports `provenance: unrecorded` in `zim_libraries`, `zim_resolve` and `zim_cite`; it is never presented as official.
77
+
78
+ Register it with a client (this edits your client's configuration, so it is yours to run):
79
+
80
+ ```
81
+ claude mcp add --scope user zimlore -- <venv>\Scripts\zimlore.exe mcp
82
+ ```
83
+
84
+ ## Installing documentation, by hand or by an agent with a shell
85
+
86
+ Any agent that can run a command can install documentation; nothing needs the MCP server for that. Every
87
+ missing-library error already names the command. The Kiwix catalogue is searchable (`q`, `lang`, `tag`,
88
+ `maxsize`; the OPDS description lists them), so:
89
+
90
+ ```
91
+ zimlore catalog playwright # search the live catalogue
92
+ zimlore catalog --tag devdocs --max-mb 1 # every DevDocs ZIM of at most 1 MB (140 on 2026-11-07)
93
+ zimlore available flask # the same, offline, from a cached snapshot (catalog --refresh)
94
+ zimlore get devdocs_en_flask # download, verified against the .meta4 SHA-256
95
+ zimlore add ~/Downloads/some.zim # a ZIM you already have: opened, checksum-checked, recorded
96
+ zimlore sync --group languages-core --dry-run # plan a whole set first; drop --dry-run to install
97
+ ```
98
+
99
+ Each install writes a provenance record, so `zim_cite` can say where a file came from. Installing never needs
100
+ the MCP server, and the MCP server never installs: planned on-demand installation from a package URL
101
+ (`pkg:pypi/flask@3.1.0` to that version's official docs) is designed in `docs/on-demand-docs.md`, not built.
102
+
103
+ ## Where the files live
104
+
105
+ `ZIMLORE_HOME` (default `~/.zimlore`), subdirectory `zim/`. Set `ZIMLORE_ZIM_PATH` (a list separated by `;` on
106
+ Windows, `:` elsewhere) to read an existing Kiwix library where it already is. Nothing is ever written inside
107
+ the repository. An optional `$ZIMLORE_HOME/aliases.json` maps your own names: `{"py": "devdocs_en_python"}`.
108
+
109
+ ## The tools
110
+
111
+ | Tool | Does |
112
+ |---|---|
113
+ | `zim_libraries` | installed libraries with edition date, size, search type, `freshness`; ranks them against a name |
114
+ | `zim_resolve` | one name or alias (`flask`, `python`, `security.stackexchange.com`) to exactly one library; an ambiguous name returns the candidates |
115
+ | `zim_search` | one library or all of them; hits are refs with an excerpt and a citation |
116
+ | `zim_read` | an article, a section (`#anchor`), an API definition, or one block (`¶n`) as markdown or text, paged by a token budget |
117
+ | `zim_outline` | headings and API definitions with anchors, or a page of a library's articles |
118
+ | `zim_links` | links out, internal ones resolved to refs and marked `in_zim` |
119
+ | `zim_cite` | the citation string, edition, freshness and licence note |
120
+ | `zim_available` | **offline** search of the cached catalogue snapshot: what could be installed, the exact `zimlore get <name>` command, and the snapshot's age |
121
+
122
+ One reference grammar, accepted everywhere: `devdocs_en_flask§blueprints/index#why-blueprints¶3`. The library
123
+ may be any alias (`flask§…`), `::` is the ASCII spelling of `§`, and a bare path works when `library` is given.
124
+ `¶n` counts blocks of *our conversion* of the article, so the entry path and anchor are the durable part.
125
+
126
+ Conventions inherited from `rfclib` and `itulib`:
127
+
128
+ - A missing or empty library is an **`error` object that names the fix**, never an empty list. `hits: []` means
129
+ the search matched nothing, not that the topic is undocumented.
130
+ - Long reads end with `next` (`{"ref", "start"}`); pass it back. A page always makes progress, even when one code
131
+ block alone exceeds the budget (it is returned whole and flagged `oversize`).
132
+ - Every response states `freshness` (`date`, `age_days`, `stale`). A ZIM is a snapshot, and one with no `Date`
133
+ metadata reports `stale: null`, never "fresh".
134
+ - HTML becomes markdown with **code blocks (and their language), tables, headings, lists and definition lists
135
+ kept**; scripts, navigation chrome and inline base64 images are dropped (images appear as `[image]`).
136
+
137
+ ## Offline catalogue
138
+
139
+ `zimlore catalog --refresh` (the one command here that uses the network, besides `catalog QUERY`, `get` and
140
+ `outdated`) stores the English Kiwix catalogue in `$ZIMLORE_HOME/catalog.json`, about 0.4 MB for 1,309 entries.
141
+ After that `zimlore available flask` and `zimlore catalog --offline [flask]` say what could be installed and the
142
+ exact `zimlore get <name>` to run, with no network and with the snapshot's age. The MCP server reads the same
143
+ file: `zim_available`, and a `zim_resolve`/`zim_search` miss carries `obtainable` (the top five), so an agent can
144
+ tell the user what to install instead of concluding a topic is undocumented. Without a snapshot the answer is an
145
+ error naming `zimlore catalog --refresh`; a damaged snapshot is an error too, never an empty list.
146
+
147
+ ## Against Context7
148
+
149
+ | | Context7 | zimlore |
150
+ |---|---|---|
151
+ | Where it runs | hosted MCP server `mcp.context7.com`; the MCP server is MIT, the crawler and API are private | your machine; no network after the download |
152
+ | Rate limits | yes; unkeyed requests get low limits, a free key raises them | none |
153
+ | Privacy | queries go to a third party | nothing leaves the machine |
154
+ | Library names | `resolve-library-id` → `/owner/repo[/version]` | `zim_resolve` → a ZIM `Name` plus its edition date |
155
+ | Versions | pin `/owner/repo/vX.Y.Z`; falls back to the nearest indexed | **pinned by ZIM date**; a ZIM does not record the upstream release number, the pages do |
156
+ | Freshness | continuously re-crawled by the service | as fresh as the ZIM you hold (`zimlore outdated` compares with the catalogue) |
157
+ | Coverage | very large and growing; any GitHub repo can be added | what Kiwix publishes: see below |
158
+ | Result shape | curated code and info snippets ranked by the service | whole pages, or one section/definition, as markdown, cut to your budget |
159
+ | Ranking | service-side, semantic, tuned | Xapian keyword search; **no semantic matching** (see Limits) |
160
+ | Citations | library id | ZIM name, edition date, entry path, anchor |
161
+
162
+ Pick Context7 when you need the newest release of a fast-moving library and are online. Pick zimlore when you
163
+ are offline, behind a policy that forbids sending queries out, want a reproducible snapshot, or want the same
164
+ documents every run. They are not exclusive.
165
+
166
+ ## What Kiwix has for developers
167
+
168
+ Measured against the live catalogue on 2026-11-07 (`docs/research.md` has the method):
169
+
170
+ - **243 DevDocs ZIMs**, 0.67 GB in total (median 0.85 MB, largest scikit-learn at 58 MB), dated 2026-05-04 to
171
+ 2026-10-31: Python, Rust, Go, Node, React, Vue, TypeScript, Django, Flask, FastAPI, PostgreSQL, Docker,
172
+ Kubernetes, Terraform and more. This is the Context7-shaped part, and it is tiny.
173
+ - `docs.python.org_en_all` (3.96 GB, 2026-08-28), `peps.python_en_all`, `devhints.io_en_all`, `archlinux_en_all`,
174
+ `gentoo_en_all`, `www.mankier.com_en_all` (man pages), `php.net_en_all` (**2024-08-13**, old).
175
+ - **Stack Exchange**: 181 English-filter ZIMs, 191 GB in total; `stackoverflow.com_en_all` alone is 114.9 GB
176
+ (2026-07-02).
177
+
178
+ ## Limits
179
+
180
+ - **Keyword search, not semantic.** On 34 questions paraphrased from page titles (`scripts/eval_retrieval.py`,
181
+ DevDocs Flask and Python), the right page was first for **10/34**, in the top 5 for **23/34**, MRR 0.436.
182
+ Questions that share words with the page do well; "isolated environment for project packages" does not find
183
+ "Virtual Environments and Packages". Searching with the library's own vocabulary works much better. With no
184
+ library given, 1 of 4 questions found its page: results from different ZIMs are interleaved because libzim
185
+ exposes no comparable score.
186
+ - **No upstream version.** The ZIM date says when the docs were scraped; which release they document is on the
187
+ pages.
188
+ - **Licences are not in the ZIMs we measured.** Neither DevDocs ZIM had a `License` metadata key; `zim_cite`
189
+ says "not stated in the ZIM" rather than guessing. DevDocs itself is MPL-2.0, but the text it archives is
190
+ each upstream project's.
191
+ - **`libzim` is GPL-3.0-or-later** (the binding; the C++ library is GPL-2.0 per its repository). zimlore is MIT
192
+ and imports it at run time; see `PLAN.md` for what that means for redistribution.
193
+ - Wheels exist for Windows x64, macOS and Linux (glibc and musl) on CPython 3.11+. On Windows ARM64 `pip
194
+ install zimlore` succeeds without `libzim`, and every tool reports the missing binding and the remedy.
195
+
196
+ ## Development
197
+
198
+ ```
199
+ python -m pip install -e . --group dev # pip 25.1+
200
+ python -m pytest -q # no ZIM needed: tests build a synthetic one with libzim's writer
201
+ python -m ruff check . ; python -m ruff format --check . ; python -m mypy zimlore tests
202
+ python scripts/eval_retrieval.py # needs devdocs_en_flask and devdocs_en_python installed
203
+ ```
204
+
205
+ [ROADMAP.md](ROADMAP.md) lists every obligation with the test that would fail if it regressed;
206
+ [PLAN.md](PLAN.md) records the design decisions; [docs/research.md](docs/research.md) the sources.
zimlore-0.1.0/PLAN.md ADDED
@@ -0,0 +1,139 @@
1
+ # zimlore plan
2
+
3
+ zimlore serves a Kiwix library (ZIM files) to AI agents over MCP as an offline-first alternative to Context7.
4
+ This file records the decisions and why. Facts behind them are in [docs/research.md](docs/research.md); status
5
+ of each obligation is in [ROADMAP.md](ROADMAP.md).
6
+
7
+ ## 1. Scope
8
+
9
+ In: find a library by name, search it, read an article/section/definition/block as markdown or text under a token
10
+ budget, follow links, cite with the ZIM's edition date, say how stale a snapshot is, and fetch ZIMs from the
11
+ public catalogue with a verified hash.
12
+
13
+ Out, on purpose:
14
+
15
+ - **Shipping or caching ZIM content.** Files live in `ZIMLORE_HOME`, never in the repository.
16
+ - **Semantic retrieval.** libzim gives Xapian keyword search and a title index. An embedding index would be a
17
+ second corpus to build and keep in step with each ZIM; if it is wanted it is a separate extra, not a promise
18
+ made here (ROADMAP `zl.semantic_search`, `[ ]`).
19
+ - **Re-hosting upstream documentation as curated snippets.** Context7's value is curation; zimlore returns the
20
+ page, section or definition as published.
21
+ - **Replacing kiwix-serve.** It is a library for agents, not a web server for people.
22
+
23
+ ## 2. Decisions
24
+
25
+ **Name: `zimlore`.** Free on PyPI (`names.py`: simple 404, json 404) with *zero* one-keystroke neighbours in the
26
+ 931,439-name snapshot, free as a GitHub repository name under `b1tninja` (404), and not in the workspace. Rejected:
27
+ `zimlib` (ten live neighbours incl. `ziplib`, `zilib`), `zimdoc`/`zimdocs` (one keystroke from `simdoc`/`vimdoc`
28
+ and from each other), `docsmith` (taken), `zimdex` (neighbours `zindex`, `imdex`). Sibling pattern is
29
+ `rfclib`/`itulib`; `lib` would have been natural but every `zim*lib` form collides.
30
+
31
+ **One module owns the native import (`_libzim.py`).** `libzim` is a compiled extension with wheels for Windows
32
+ x64, macOS, Linux (glibc, musl) and no Windows ARM64 wheel. The dependency carries a marker that excludes only
33
+ Windows-on-ARM, so installation succeeds there and every tool answers with the missing-binding remedy instead of
34
+ a compiler error. Everything that does not read a ZIM (CLI `info`, the reference grammar, the converter, the
35
+ catalogue client) works without it. This is the `rfclib[zim]` pattern inverted: rfclib writes ZIMs and makes
36
+ libzim optional; zimlore *is* a ZIM reader, so it is a hard dependency.
37
+
38
+ **HTML to markdown with the standard library only.** `html.parser`, no BeautifulSoup/lxml/markdownify. The
39
+ measured inputs (DevDocs) are regular, and a hand-written converter is the only way to guarantee the three
40
+ things an agent needs and generic converters lose: code blocks with their language, anchors on headings and API
41
+ definitions (so `#anchor` reads work), and the removal of inline base64 images that would cost thousands of
42
+ tokens each. Cost: Stack Exchange and Wikipedia HTML has not been exercised (ROADMAP `zl.html_other_scrapers`).
43
+
44
+ **The unit of paging is the block.** A paragraph, heading, list item, table, definition or code fence is never
45
+ split. A page always contains at least one block, so a single oversized block (a 400-line log) is returned whole
46
+ and flagged rather than cut, because a cursor that cannot advance is worse than a long page. `¶n` is the nth
47
+ block; it indexes zimlore's conversion, so it is documented as less durable than the entry path and anchor.
48
+
49
+ **Sections and definitions are first-class.** DevDocs pages are whole chapters (the `os` page is the whole module).
50
+ `#anchor` reads one `<section id>` or one `<dt id>` definition and its body, which is what makes "just
51
+ `os.getcwd`" cost 40 tokens instead of 20,000.
52
+
53
+ **Library identity is the ZIM `Name`, version is the ZIM `Date`.** Aliases are derived (DevDocs slug, title
54
+ without "Docs", Stack Exchange host, user `aliases.json`) and *ranked*: exact > all words > prefix > substring >
55
+ fuzzy. A tie is an error listing the candidates, never a coin flip. Two editions of one `Name` resolve to the
56
+ newest and the response lists the others. The response says outright that a ZIM does not record the upstream
57
+ release (Context7's `/org/repo/vX.Y.Z` has no equivalent), because the honest alternative to a version number is
58
+ a date.
59
+
60
+ **Versions are not editions; a library has a family.** Producers that build documentation per package version
61
+ name each build differently (`golang_github.com_spf13_cobra_v1.8.1`, `pypi_cryptography_50.0.2`), so by the rule
62
+ above each version is its own library, and measured on the Go prototype that made `cobra` resolve to v1.8.1 over
63
+ v1.10.2 (the alias is one character shorter and the ambiguity guard compared only scores). A `Book` therefore has
64
+ a derived `family` and `version`, with no change to the ZIM format or any stored state. The family is the `purl:`
65
+ tag without its version when the ZIM carries one (the producer's own statement), else the `Name` without a
66
+ trailing `_v?<digits>(.<alnum>)*(-<alnum>)?` token; a `_YYYY-MM` date suffix is an edition marker and is dropped
67
+ first, and a bare integer of more than two digits or a four-digit year (`wikipedia_en_100`, `ubuntu_en_all_2024`)
68
+ is a count or a date, not a release. A name with no such token is its own family with no version, which is every
69
+ DevDocs and Stack Exchange ZIM, so nothing changes for them. Resolution scores a *family* (the best of its members,
70
+ so versions of one library never tie each other and alias length cannot pick a version), then picks inside it:
71
+ no version asked means the newest by `library.version_key` (numeric components, `v` ignored, pre-releases below
72
+ releases, a version with no number last); a version asked (`cobra v1.8.1`, `cobra==1.8.1`, `cobra@1.8`, a purl's
73
+ `@version`) means that exact string, else the newest whose components start with it (`3.1` never matches `3.10`),
74
+ reported as `exact` or `prefix`; no match is an error listing the installed versions, and it names no build
75
+ command because none exists. A whole `Name` always selects that very ZIM. A purl is accepted wherever a library
76
+ name is (`zimlore/purl.py` reads nine types and refuses the rest): it matches a ZIM's `purl:` tag on type,
77
+ namespace and name, and falls back to the package name for ZIMs that carry no tag. Deleting stays per `Name`:
78
+ `prune` keeps the newest N *editions* of each Name and never counts another version as one, and `remove` of a
79
+ package with several versions refuses until a version or `--all-versions` is given. The cost accepted: a ZIM with
80
+ a purl tag and one without are different families even for one package.
81
+
82
+ **Staleness is data.** Every response carries `freshness {date, age_days, stale, stale_after_days}`. `stale` is
83
+ `age > 365` days, a stated convention; `null` when the ZIM has no date. `zimlore outdated` compares with the
84
+ catalogue (network).
85
+
86
+ **Search = title matches + full-text, with a documented fallback.** Exact-title hits first, then strict
87
+ full-text, then remaining title hits, then partial matches marked `some terms`. Partial matches come from
88
+ per-word searches fused by `ln(pages / pages matching)` weights and a title boost, because libzim's parser
89
+ requires every term (found by running it; first eval run scored hit@1 8/34). Across libraries, results are
90
+ interleaved rank by rank and the response says so, since libzim exposes no comparable score. The 34-case eval
91
+ (MRR 0.436, hit@5 23/34) is the honest summary: lexical, and weak on paraphrase.
92
+
93
+ **Citations are built, not typed.** `Title [Name], ZIM edition DATE, entry PATH#anchor`, plus the ZIM's `License`
94
+ metadata or the sentence "not stated in the ZIM". No ZIM we measured had a licence key, and inventing one from
95
+ the project name would be an unverifiable claim.
96
+
97
+ **Network is confined to `catalog.py`, and downloads are verified.** The hash comes from kiwix.org's `.meta4`;
98
+ bytes may come from any mirror (the `.zim` URL redirects to third-party hosts); a mismatch discards the file.
99
+ Only `https` (or `http` to localhost, for tests) is accepted. Nothing in the MCP server touches the network, which
100
+ is the point of the project.
101
+
102
+ **ZIM content is untrusted data.** A ZIM is third-party content: its article text, titles and metadata (`Title`,
103
+ `Name`, `Source`, and therefore the `cite` strings built from them) must be quoted and cited, never followed as
104
+ instructions. The MCP server instructions say so, and the read path is bounded against hostile files (an entry
105
+ size budget, a converter output ceiling, capped list indentation, bounded redirect chains).
106
+
107
+ **Licence of the code: MIT, with a flagged tension.** `libzim` (the binding) is GPL-3.0-or-later and zimlore
108
+ imports it. MIT code that imports a GPL library is fine to publish and to use, but a *combined* binary or image
109
+ that redistributes both is GPL-3.0 as a whole. The siblings (`rfclib[zim]`) take the same position. This is a
110
+ decision for the maintainer (section 5), not something this file can settle.
111
+
112
+ ## 3. Verification model
113
+
114
+ Tests build a tiny synthetic ZIM with libzim's writer (invented text, DevDocs-shaped markup) so the suite needs
115
+ no download; `tests/test_license.py` is the tripwire against ZIM content entering the tree. What the suite cannot
116
+ prove (converter behaviour on real pages, ranking) is measured by `scripts/eval_retrieval.py` and by the live
117
+ MCP run recorded in `docs/research.md`, and ROADMAP rows that depend on them stay `[part]`.
118
+
119
+ ## 4. Portfolio position
120
+
121
+ `PORTFOLIO.md` §4 defines "reference libraries - not tiered" as the corpora every protocol project *cites from*
122
+ (`rfclib`, `itulib`), justified "by a citation some other project could not make". zimlore is **not** that:
123
+ it is a developer-documentation corpus for agents, not a source for protocol citations. It is therefore not
124
+ sequenced in a wave and does not count against the two-project rule, but it also is not covered by the existing
125
+ sentence. A proposed entry is in [docs/portfolio-entry.md](docs/portfolio-entry.md); `PORTFOLIO.md` has **not**
126
+ been edited, because the workspace root is shared and the classification is the maintainer's call.
127
+
128
+ ## 5. Decisions waiting for the maintainer
129
+
130
+ 1. **Does `PORTFOLIO.md` list zimlore, and as what?** Recommendation: a third "reference library" row with the
131
+ sentence widened to "or that agents read from", no wave, no tier.
132
+ 2. **Licence of zimlore** given GPL-3.0-or-later `libzim`. Recommendation: stay MIT (library use is the norm),
133
+ state it in NOTICE (done), and revisit if a bundled distribution is ever planned.
134
+ 3. **Name `zimlore`**, and whether the GitHub repository is `b1tninja/zimlore`. Nothing has been pushed or created.
135
+ 4. **Registering the MCP server** with Claude Code / Cline (`claude mcp add ...`): edits the user's configuration,
136
+ so not done.
137
+ 5. **Publishing to PyPI**, and the release workflow's trusted-publisher setup. Not done.
138
+ 6. **Which ZIMs to install by default** (a starter set). The DevDocs set is 0.67 GB in total and is the natural
139
+ candidate; Stack Overflow is 115 GB and is not.
@@ -0,0 +1,174 @@
1
+ # zimlore
2
+
3
+ A [Kiwix](https://kiwix.org) library (ZIM files) served to AI agents over MCP: **offline, citable,
4
+ token-budgeted documentation**. It is the offline counterpart of [Context7](https://github.com/upstash/context7):
5
+ instead of asking a hosted service for snippets, an agent searches and reads ZIM files you hold on disk, and
6
+ every answer carries the ZIM's name, edition date and entry path.
7
+
8
+ You supply the ZIMs. **None are shipped**, and `.gitignore` names every way one could be staged
9
+ (see [NOTICE](NOTICE)).
10
+
11
+ ```
12
+ pip install "zimlore[mcp]"
13
+ zimlore catalog flask # search the public Kiwix catalogue
14
+ zimlore get devdocs_en_flask # download it, verified by SHA-256, into ~/.zimlore/zim
15
+ zimlore search "application factory" -l flask
16
+ zimlore read flask§patterns/appfactories/index
17
+ zimlore mcp # the MCP server, on stdio
18
+ ```
19
+
20
+ Managing what is installed (none of these touch the network):
21
+
22
+ ```
23
+ zimlore du # disk use per library
24
+ zimlore verify [--adopt] # check files against their provenance record and libzim's checksum
25
+ zimlore remove flask --yes # delete (dry run without --yes; --edition DATE or --all when several)
26
+ zimlore prune --keep 2 --yes # drop old zimlore-installed editions and stale partial downloads
27
+ # (--include-unmanaged also deletes files zimlore did not install)
28
+ ```
29
+
30
+ Putting ZIMs in place (`add` is local; `sync` and `get` use the network and resume an interrupted download):
31
+
32
+ ```
33
+ zimlore add some.zim [--link] # copy (or link) a local ZIM in after libzim opens it and its checksum holds
34
+ zimlore sync --group web-testing --dry-run # plan against a manifest or a named group; sizes are approximate
35
+ zimlore sync --manifest .zimlore.toml [--update] # install what is missing; --update stages newer editions
36
+ ```
37
+
38
+ A manifest (`./.zimlore.toml`, else `$ZIMLORE_HOME/libraries.toml`) names catalogue entries only, never URLs or
39
+ paths, so it can be committed: `libraries = ["devdocs_en_python"]`, `groups = ["infra"]`, `max_age_days = 365`,
40
+ `budget = "2GB"` (the library may not grow past it). `sync` never removes anything. The shipped groups
41
+ (`languages-core`, `web-testing`, `infra`) are an editorial default awaiting the maintainer's decision.
42
+
43
+ Every download records a sidecar (`<file>.zimlore.json`: source URL, verified SHA-256, time). A file without one
44
+ reports `provenance: unrecorded` in `zim_libraries`, `zim_resolve` and `zim_cite`; it is never presented as official.
45
+
46
+ Register it with a client (this edits your client's configuration, so it is yours to run):
47
+
48
+ ```
49
+ claude mcp add --scope user zimlore -- <venv>\Scripts\zimlore.exe mcp
50
+ ```
51
+
52
+ ## Installing documentation, by hand or by an agent with a shell
53
+
54
+ Any agent that can run a command can install documentation; nothing needs the MCP server for that. Every
55
+ missing-library error already names the command. The Kiwix catalogue is searchable (`q`, `lang`, `tag`,
56
+ `maxsize`; the OPDS description lists them), so:
57
+
58
+ ```
59
+ zimlore catalog playwright # search the live catalogue
60
+ zimlore catalog --tag devdocs --max-mb 1 # every DevDocs ZIM of at most 1 MB (140 on 2026-11-07)
61
+ zimlore available flask # the same, offline, from a cached snapshot (catalog --refresh)
62
+ zimlore get devdocs_en_flask # download, verified against the .meta4 SHA-256
63
+ zimlore add ~/Downloads/some.zim # a ZIM you already have: opened, checksum-checked, recorded
64
+ zimlore sync --group languages-core --dry-run # plan a whole set first; drop --dry-run to install
65
+ ```
66
+
67
+ Each install writes a provenance record, so `zim_cite` can say where a file came from. Installing never needs
68
+ the MCP server, and the MCP server never installs: planned on-demand installation from a package URL
69
+ (`pkg:pypi/flask@3.1.0` to that version's official docs) is designed in `docs/on-demand-docs.md`, not built.
70
+
71
+ ## Where the files live
72
+
73
+ `ZIMLORE_HOME` (default `~/.zimlore`), subdirectory `zim/`. Set `ZIMLORE_ZIM_PATH` (a list separated by `;` on
74
+ Windows, `:` elsewhere) to read an existing Kiwix library where it already is. Nothing is ever written inside
75
+ the repository. An optional `$ZIMLORE_HOME/aliases.json` maps your own names: `{"py": "devdocs_en_python"}`.
76
+
77
+ ## The tools
78
+
79
+ | Tool | Does |
80
+ |---|---|
81
+ | `zim_libraries` | installed libraries with edition date, size, search type, `freshness`; ranks them against a name |
82
+ | `zim_resolve` | one name or alias (`flask`, `python`, `security.stackexchange.com`) to exactly one library; an ambiguous name returns the candidates |
83
+ | `zim_search` | one library or all of them; hits are refs with an excerpt and a citation |
84
+ | `zim_read` | an article, a section (`#anchor`), an API definition, or one block (`¶n`) as markdown or text, paged by a token budget |
85
+ | `zim_outline` | headings and API definitions with anchors, or a page of a library's articles |
86
+ | `zim_links` | links out, internal ones resolved to refs and marked `in_zim` |
87
+ | `zim_cite` | the citation string, edition, freshness and licence note |
88
+ | `zim_available` | **offline** search of the cached catalogue snapshot: what could be installed, the exact `zimlore get <name>` command, and the snapshot's age |
89
+
90
+ One reference grammar, accepted everywhere: `devdocs_en_flask§blueprints/index#why-blueprints¶3`. The library
91
+ may be any alias (`flask§…`), `::` is the ASCII spelling of `§`, and a bare path works when `library` is given.
92
+ `¶n` counts blocks of *our conversion* of the article, so the entry path and anchor are the durable part.
93
+
94
+ Conventions inherited from `rfclib` and `itulib`:
95
+
96
+ - A missing or empty library is an **`error` object that names the fix**, never an empty list. `hits: []` means
97
+ the search matched nothing, not that the topic is undocumented.
98
+ - Long reads end with `next` (`{"ref", "start"}`); pass it back. A page always makes progress, even when one code
99
+ block alone exceeds the budget (it is returned whole and flagged `oversize`).
100
+ - Every response states `freshness` (`date`, `age_days`, `stale`). A ZIM is a snapshot, and one with no `Date`
101
+ metadata reports `stale: null`, never "fresh".
102
+ - HTML becomes markdown with **code blocks (and their language), tables, headings, lists and definition lists
103
+ kept**; scripts, navigation chrome and inline base64 images are dropped (images appear as `[image]`).
104
+
105
+ ## Offline catalogue
106
+
107
+ `zimlore catalog --refresh` (the one command here that uses the network, besides `catalog QUERY`, `get` and
108
+ `outdated`) stores the English Kiwix catalogue in `$ZIMLORE_HOME/catalog.json`, about 0.4 MB for 1,309 entries.
109
+ After that `zimlore available flask` and `zimlore catalog --offline [flask]` say what could be installed and the
110
+ exact `zimlore get <name>` to run, with no network and with the snapshot's age. The MCP server reads the same
111
+ file: `zim_available`, and a `zim_resolve`/`zim_search` miss carries `obtainable` (the top five), so an agent can
112
+ tell the user what to install instead of concluding a topic is undocumented. Without a snapshot the answer is an
113
+ error naming `zimlore catalog --refresh`; a damaged snapshot is an error too, never an empty list.
114
+
115
+ ## Against Context7
116
+
117
+ | | Context7 | zimlore |
118
+ |---|---|---|
119
+ | Where it runs | hosted MCP server `mcp.context7.com`; the MCP server is MIT, the crawler and API are private | your machine; no network after the download |
120
+ | Rate limits | yes; unkeyed requests get low limits, a free key raises them | none |
121
+ | Privacy | queries go to a third party | nothing leaves the machine |
122
+ | Library names | `resolve-library-id` → `/owner/repo[/version]` | `zim_resolve` → a ZIM `Name` plus its edition date |
123
+ | Versions | pin `/owner/repo/vX.Y.Z`; falls back to the nearest indexed | **pinned by ZIM date**; a ZIM does not record the upstream release number, the pages do |
124
+ | Freshness | continuously re-crawled by the service | as fresh as the ZIM you hold (`zimlore outdated` compares with the catalogue) |
125
+ | Coverage | very large and growing; any GitHub repo can be added | what Kiwix publishes: see below |
126
+ | Result shape | curated code and info snippets ranked by the service | whole pages, or one section/definition, as markdown, cut to your budget |
127
+ | Ranking | service-side, semantic, tuned | Xapian keyword search; **no semantic matching** (see Limits) |
128
+ | Citations | library id | ZIM name, edition date, entry path, anchor |
129
+
130
+ Pick Context7 when you need the newest release of a fast-moving library and are online. Pick zimlore when you
131
+ are offline, behind a policy that forbids sending queries out, want a reproducible snapshot, or want the same
132
+ documents every run. They are not exclusive.
133
+
134
+ ## What Kiwix has for developers
135
+
136
+ Measured against the live catalogue on 2026-11-07 (`docs/research.md` has the method):
137
+
138
+ - **243 DevDocs ZIMs**, 0.67 GB in total (median 0.85 MB, largest scikit-learn at 58 MB), dated 2026-05-04 to
139
+ 2026-10-31: Python, Rust, Go, Node, React, Vue, TypeScript, Django, Flask, FastAPI, PostgreSQL, Docker,
140
+ Kubernetes, Terraform and more. This is the Context7-shaped part, and it is tiny.
141
+ - `docs.python.org_en_all` (3.96 GB, 2026-08-28), `peps.python_en_all`, `devhints.io_en_all`, `archlinux_en_all`,
142
+ `gentoo_en_all`, `www.mankier.com_en_all` (man pages), `php.net_en_all` (**2024-08-13**, old).
143
+ - **Stack Exchange**: 181 English-filter ZIMs, 191 GB in total; `stackoverflow.com_en_all` alone is 114.9 GB
144
+ (2026-07-02).
145
+
146
+ ## Limits
147
+
148
+ - **Keyword search, not semantic.** On 34 questions paraphrased from page titles (`scripts/eval_retrieval.py`,
149
+ DevDocs Flask and Python), the right page was first for **10/34**, in the top 5 for **23/34**, MRR 0.436.
150
+ Questions that share words with the page do well; "isolated environment for project packages" does not find
151
+ "Virtual Environments and Packages". Searching with the library's own vocabulary works much better. With no
152
+ library given, 1 of 4 questions found its page: results from different ZIMs are interleaved because libzim
153
+ exposes no comparable score.
154
+ - **No upstream version.** The ZIM date says when the docs were scraped; which release they document is on the
155
+ pages.
156
+ - **Licences are not in the ZIMs we measured.** Neither DevDocs ZIM had a `License` metadata key; `zim_cite`
157
+ says "not stated in the ZIM" rather than guessing. DevDocs itself is MPL-2.0, but the text it archives is
158
+ each upstream project's.
159
+ - **`libzim` is GPL-3.0-or-later** (the binding; the C++ library is GPL-2.0 per its repository). zimlore is MIT
160
+ and imports it at run time; see `PLAN.md` for what that means for redistribution.
161
+ - Wheels exist for Windows x64, macOS and Linux (glibc and musl) on CPython 3.11+. On Windows ARM64 `pip
162
+ install zimlore` succeeds without `libzim`, and every tool reports the missing binding and the remedy.
163
+
164
+ ## Development
165
+
166
+ ```
167
+ python -m pip install -e . --group dev # pip 25.1+
168
+ python -m pytest -q # no ZIM needed: tests build a synthetic one with libzim's writer
169
+ python -m ruff check . ; python -m ruff format --check . ; python -m mypy zimlore tests
170
+ python scripts/eval_retrieval.py # needs devdocs_en_flask and devdocs_en_python installed
171
+ ```
172
+
173
+ [ROADMAP.md](ROADMAP.md) lists every obligation with the test that would fail if it regressed;
174
+ [PLAN.md](PLAN.md) records the design decisions; [docs/research.md](docs/research.md) the sources.